gdharness 1.1.21 → 1.1.22
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 +1036 -525
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +8 -4
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +28 -8
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +20 -9
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +25 -10
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +949 -435
- package/package.json +1 -1
|
@@ -198,7 +198,7 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
198
198
|
var map: Dictionary = holder
|
|
199
199
|
if not method_of(named).is_empty():
|
|
200
200
|
return method_of(named) in MAP_CALLS
|
|
201
|
-
return map
|
|
201
|
+
return not _key_in(map, named).is_empty()
|
|
202
202
|
if holder is Object:
|
|
203
203
|
var object: Object = holder
|
|
204
204
|
var method: String = method_of(named)
|
|
@@ -208,6 +208,23 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
208
208
|
return false
|
|
209
209
|
|
|
210
210
|
|
|
211
|
+
## The key of [param map] a step names, in a list of one, or an empty list when it names none.
|
|
212
|
+
##
|
|
213
|
+
## A step is text, and a map is keyed by whatever the game put in it: text, a StringName, or a
|
|
214
|
+
## number, which a map of units by id is. Keyed by 3, "3" is not a key of it, measured on 4.7.2, so a
|
|
215
|
+
## step that reads as a number is tried as one after the two spellings of text.
|
|
216
|
+
static func _key_in(map: Dictionary, named: String) -> Array:
|
|
217
|
+
var spellings: Array = [named, StringName(named)]
|
|
218
|
+
if named.is_valid_int():
|
|
219
|
+
spellings.append(int(named))
|
|
220
|
+
if named.is_valid_float():
|
|
221
|
+
spellings.append(float(named))
|
|
222
|
+
for key: Variant in spellings:
|
|
223
|
+
if map.has(key):
|
|
224
|
+
return [key]
|
|
225
|
+
return []
|
|
226
|
+
|
|
227
|
+
|
|
211
228
|
## The method a step written as a call names, "get_viewport()" being get_viewport, or "" for a
|
|
212
229
|
## step that is a name.
|
|
213
230
|
static func method_of(named: String) -> String:
|
|
@@ -243,10 +260,7 @@ static func write(holder: Variant, named: String, value: Variant) -> void:
|
|
|
243
260
|
var map: Dictionary = holder
|
|
244
261
|
# Written back under the key it was found under, since a map keyed by StringName and one
|
|
245
262
|
# keyed by String both read the same way and a write to the wrong one adds a second entry.
|
|
246
|
-
|
|
247
|
-
map[named] = value
|
|
248
|
-
else:
|
|
249
|
-
map[StringName(named)] = value
|
|
263
|
+
map[_key_in(map, named)[0]] = value
|
|
250
264
|
return
|
|
251
265
|
var object: Object = holder
|
|
252
266
|
object.set(named, value)
|
|
@@ -265,9 +279,7 @@ static func read_under(holder: Variant, named: String) -> Variant:
|
|
|
265
279
|
return list[_index_in(named, len(holder))]
|
|
266
280
|
if holder is Dictionary:
|
|
267
281
|
var map: Dictionary = holder
|
|
268
|
-
|
|
269
|
-
return map[named]
|
|
270
|
-
return map[StringName(named)]
|
|
282
|
+
return map[_key_in(map, named)[0]]
|
|
271
283
|
var object: Object = holder
|
|
272
284
|
var method: String = method_of(named)
|
|
273
285
|
if not method.is_empty():
|
|
@@ -311,6 +323,14 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
311
323
|
return container_calls_refused(called_as, "a map", container_call)
|
|
312
324
|
var map: Dictionary = holder
|
|
313
325
|
var keys: Array = map.keys()
|
|
326
|
+
# A key reading as the step and still not reached is one of a type a step cannot spell, and
|
|
327
|
+
# saying the map has no key 3 while listing 3 among its keys contradicts itself.
|
|
328
|
+
for key: Variant in keys:
|
|
329
|
+
if str(key) == named:
|
|
330
|
+
return (
|
|
331
|
+
"%s has a key that reads %s, but it is %s, which a path step cannot name"
|
|
332
|
+
% [called_as, named, type_string(typeof(key))]
|
|
333
|
+
)
|
|
314
334
|
# Eight of them, because a map keyed by something unexpected is told by the first few and a
|
|
315
335
|
# map of two hundred would bury the sentence saying which key was missing.
|
|
316
336
|
var some: Array[String] = []
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
3
|
## What the server asks about the running tree: its shape, one node, the nodes matching a
|
|
4
|
-
## question, where one is on screen, a property
|
|
4
|
+
## question, where one is on screen, a property's value, the metrics. What it changes, a property
|
|
5
|
+
## written or a method called, is in runtime_changes.gd.
|
|
5
6
|
|
|
6
7
|
const Paths = preload("runtime_paths.gd")
|
|
7
8
|
const Read = preload("reading.gd")
|
|
@@ -360,6 +361,11 @@ static func _drawn(node: Node) -> bool:
|
|
|
360
361
|
var control: CanvasItem = node as CanvasItem
|
|
361
362
|
if control != null:
|
|
362
363
|
return control.visible
|
|
364
|
+
# A hidden layer hides everything drawn on it, a pause menu's for one, while each of those keeps
|
|
365
|
+
# its own visible flag, so read as shown it matched the words of a menu nobody could see.
|
|
366
|
+
var layer: CanvasLayer = node as CanvasLayer
|
|
367
|
+
if layer != null:
|
|
368
|
+
return layer.visible
|
|
363
369
|
var spatial: Node3D = node as Node3D
|
|
364
370
|
if spatial != null:
|
|
365
371
|
return spatial.visible
|
|
@@ -562,304 +568,6 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
562
568
|
}
|
|
563
569
|
|
|
564
570
|
|
|
565
|
-
func set_property(params: Dictionary) -> Dictionary:
|
|
566
|
-
var node_path: String = str(params.get("path", ""))
|
|
567
|
-
var property: String = str(params.get("property", ""))
|
|
568
|
-
var value: Variant = params.get("value")
|
|
569
|
-
|
|
570
|
-
if node_path.is_empty() or property.is_empty():
|
|
571
|
-
return {"type": "error", "message": "Node path and property required"}
|
|
572
|
-
|
|
573
|
-
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
574
|
-
if standing.has("message"):
|
|
575
|
-
return standing
|
|
576
|
-
var node: Node = standing["node"]
|
|
577
|
-
|
|
578
|
-
# Through a path as well, for the reason [method Paths.walk_to] gives, and read back off the same
|
|
579
|
-
# holder afterwards: a set that does not take says so by answering with the old value, which is
|
|
580
|
-
# how a typed container refusing a write is told from one accepting it.
|
|
581
|
-
var reached: Dictionary = Paths.walk_to(node, node_path, property)
|
|
582
|
-
if reached.has("message"):
|
|
583
|
-
return reached
|
|
584
|
-
|
|
585
|
-
var holder: Variant = reached["holder"]
|
|
586
|
-
var named: String = reached["name"]
|
|
587
|
-
# A call can be walked through and read, and is not a place: what it returns is the method's
|
|
588
|
-
# to hand out, and writing "into" it would set nothing the game keeps.
|
|
589
|
-
if not Paths.method_of(named).is_empty():
|
|
590
|
-
return {
|
|
591
|
-
"type": "error",
|
|
592
|
-
"message":
|
|
593
|
-
(
|
|
594
|
-
"%s:%s is a call, and a call is not a place to write: name a property"
|
|
595
|
-
% [reached["called"], named]
|
|
596
|
-
)
|
|
597
|
-
}
|
|
598
|
-
if not Paths.can_read(holder, named):
|
|
599
|
-
return {"type": "error", "message": Paths.nothing_there(holder, named, str(reached["called"]))}
|
|
600
|
-
var old_value: Variant = Paths.read_under(holder, named)
|
|
601
|
-
# The same rule the call path holds: a value that cannot become what the property holds is
|
|
602
|
-
# refused rather than written. Writing it means the engine picks something, and what it picks
|
|
603
|
-
# for a word where a number goes is zero, which the answer then reports as the new value.
|
|
604
|
-
var slot: Dictionary = Values.slot_declared(holder, named)
|
|
605
|
-
var wanted: int = slot["type"] if slot["type"] != TYPE_NIL else typeof(old_value)
|
|
606
|
-
var given: Variant
|
|
607
|
-
if wanted == TYPE_OBJECT and (value is String or value is Dictionary):
|
|
608
|
-
var named_object: Dictionary = _object_named(node, node_path, value, str(slot["class"]))
|
|
609
|
-
if named_object.has("message"):
|
|
610
|
-
return {
|
|
611
|
-
"type": "error",
|
|
612
|
-
"message": "%s.%s holds an object: %s" % [reached["called"], named, named_object["message"]]
|
|
613
|
-
}
|
|
614
|
-
given = named_object["object"]
|
|
615
|
-
else:
|
|
616
|
-
given = _values.fitted(value, wanted)
|
|
617
|
-
if not Values.acceptable(given, wanted):
|
|
618
|
-
return {
|
|
619
|
-
"type": "error",
|
|
620
|
-
"message":
|
|
621
|
-
(
|
|
622
|
-
"%s.%s holds %s and the value given is %s, which cannot become one."
|
|
623
|
-
% [reached["called"], named, type_string(wanted), type_string(typeof(given))]
|
|
624
|
-
)
|
|
625
|
-
}
|
|
626
|
-
var typed: Dictionary = _values.typed_like(given, old_value, _element_object.bind(node, node_path))
|
|
627
|
-
if typed.has("message"):
|
|
628
|
-
return {
|
|
629
|
-
"type": "error",
|
|
630
|
-
"message":
|
|
631
|
-
"%s.%s is a typed %s: %s." % [reached["called"], named, _sort_of(old_value), typed["message"]]
|
|
632
|
-
}
|
|
633
|
-
given = typed["value"]
|
|
634
|
-
# The write alone, setter included, which is where a property's cost is.
|
|
635
|
-
var began: int = Time.get_ticks_usec()
|
|
636
|
-
Paths.write(holder, named, given)
|
|
637
|
-
var elapsed: int = Time.get_ticks_usec() - began
|
|
638
|
-
var now: Variant = Paths.read_under(holder, named)
|
|
639
|
-
# Read back rather than trusted. The engine drops a write it will not take without a word, and
|
|
640
|
-
# the answer then showed the old value as the new one, shaped as a success: a typed container
|
|
641
|
-
# refusing a plain list did exactly that. A value the property changed on the way in, a setter
|
|
642
|
-
# clamping it, still changed it, so only a write that left the property as it was is refused.
|
|
643
|
-
# And only one that asked for a change: the value the property already held, written again, left
|
|
644
|
-
# it as it was because nothing was asked of it.
|
|
645
|
-
var already: bool = _held_already(given, old_value)
|
|
646
|
-
if (
|
|
647
|
-
Values.comparable(given, old_value)
|
|
648
|
-
and not already
|
|
649
|
-
and Values.comparable(now, old_value)
|
|
650
|
-
and now == old_value
|
|
651
|
-
):
|
|
652
|
-
return {
|
|
653
|
-
"type": "error",
|
|
654
|
-
"message":
|
|
655
|
-
(
|
|
656
|
-
(
|
|
657
|
-
"%s.%s was given %s and still holds %s: the engine kept what it had,"
|
|
658
|
-
+ " which a property it will not write does."
|
|
659
|
-
)
|
|
660
|
-
% [
|
|
661
|
-
reached["called"],
|
|
662
|
-
named,
|
|
663
|
-
JSON.stringify(_values.serialize(given)),
|
|
664
|
-
JSON.stringify(_values.serialize(now))
|
|
665
|
-
]
|
|
666
|
-
)
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
var answer: Dictionary = {
|
|
670
|
-
"type": "property_set",
|
|
671
|
-
"path": node_path,
|
|
672
|
-
"property": property,
|
|
673
|
-
"old_value": _values.serialize(old_value),
|
|
674
|
-
"new_value": _values.serialize(now),
|
|
675
|
-
"elapsed_usec": elapsed
|
|
676
|
-
}
|
|
677
|
-
if already and Values.comparable(now, old_value) and now == old_value:
|
|
678
|
-
answer["unchanged"] = true
|
|
679
|
-
return answer
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
## Whether [param given] is the value [param held] already is, at the precision the property keeps.
|
|
683
|
-
##
|
|
684
|
-
## A float arrives as 64 bits and most engine properties keep 32: Camera3D.h_offset given -1.1
|
|
685
|
-
## reads back as -1.10000002384186, so comparing the two exactly took a write of the value already
|
|
686
|
-
## held for one the engine refused (#780). Rounding both to 32 bits is exact for a 32-bit property.
|
|
687
|
-
## A 64-bit one can take a write this calls the same and read back changed, and the caller calls a
|
|
688
|
-
## write unchanged only when it reads back as it was. What that cannot tell apart is a 64-bit
|
|
689
|
-
## property whose setter ignored a value differing from the held one below 32 bits: it is answered
|
|
690
|
-
## unchanged rather than refused. Vectors and colours hold 32-bit parts in the value itself, so they
|
|
691
|
-
## already compare at the property's precision.
|
|
692
|
-
static func _held_already(given: Variant, held: Variant) -> bool:
|
|
693
|
-
if not Values.comparable(given, held):
|
|
694
|
-
return false
|
|
695
|
-
if given == held:
|
|
696
|
-
return true
|
|
697
|
-
if typeof(given) != TYPE_FLOAT or typeof(held) != TYPE_FLOAT:
|
|
698
|
-
return false
|
|
699
|
-
return PackedFloat32Array([given])[0] == PackedFloat32Array([held])[0]
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
func call_method(params: Dictionary) -> Dictionary:
|
|
703
|
-
var node_path: String = str(params.get("path", ""))
|
|
704
|
-
var method: String = str(params.get("method", ""))
|
|
705
|
-
var args: Array = params.get("args", [])
|
|
706
|
-
|
|
707
|
-
if node_path.is_empty() or method.is_empty():
|
|
708
|
-
return {"type": "error", "message": "Node path and method required"}
|
|
709
|
-
|
|
710
|
-
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
711
|
-
if standing.has("message"):
|
|
712
|
-
return standing
|
|
713
|
-
var node: Node = standing["node"]
|
|
714
|
-
|
|
715
|
-
# Through a path as well, for the reason [method Paths.walk_to] gives. What a game does hangs off its
|
|
716
|
-
# nodes as much as its state does, so reading `_game:run:day` while being unable to call
|
|
717
|
-
# `_game:run:advance` answers half of what a node holds and refuses the other half.
|
|
718
|
-
var reached: Dictionary = Paths.walk_to(node, node_path, method)
|
|
719
|
-
if reached.has("message"):
|
|
720
|
-
return reached
|
|
721
|
-
|
|
722
|
-
# A list or a map answers the calls a path makes on one, and nothing else: said in those terms
|
|
723
|
-
# rather than as "has no method", which reads as a misspelling of a method that was never going
|
|
724
|
-
# to be there, and with the element spelled out, since a method on what it holds is the usual aim.
|
|
725
|
-
if not reached["holder"] is Object:
|
|
726
|
-
var sort: String = "a list"
|
|
727
|
-
if reached["holder"] is Dictionary:
|
|
728
|
-
sort = "a map"
|
|
729
|
-
elif Paths.packed(reached["holder"]):
|
|
730
|
-
sort = "a packed list"
|
|
731
|
-
var spelled: String = str(reached["name"])
|
|
732
|
-
if Paths.method_of(spelled).is_empty():
|
|
733
|
-
spelled += "()"
|
|
734
|
-
if args.is_empty() and Paths.can_read(reached["holder"], spelled):
|
|
735
|
-
var read_from: int = Time.get_ticks_usec()
|
|
736
|
-
var answered: Variant = Paths.read_under(reached["holder"], spelled)
|
|
737
|
-
var read_for: int = Time.get_ticks_usec() - read_from
|
|
738
|
-
return {
|
|
739
|
-
"type": "method_result",
|
|
740
|
-
"path": node_path,
|
|
741
|
-
"method": method,
|
|
742
|
-
"result": _values.serialize(answered),
|
|
743
|
-
"elapsed_usec": read_for,
|
|
744
|
-
}
|
|
745
|
-
var why: String = (
|
|
746
|
-
"%s:%s takes no arguments" % [reached["called"], spelled]
|
|
747
|
-
if Paths.can_read(reached["holder"], spelled)
|
|
748
|
-
else Paths.nothing_there(reached["holder"], spelled, str(reached["called"]))
|
|
749
|
-
)
|
|
750
|
-
return {
|
|
751
|
-
"type": "error",
|
|
752
|
-
"message":
|
|
753
|
-
(
|
|
754
|
-
"%s. A method of what %s holds is called on the element, as in %s:0:%s"
|
|
755
|
-
% [why, sort, reached["called"], reached["name"]]
|
|
756
|
-
)
|
|
757
|
-
}
|
|
758
|
-
var holder: Object = reached["holder"]
|
|
759
|
-
var named: String = reached["name"]
|
|
760
|
-
# The method a call ends on is the one being called whether or not it carries the brackets a
|
|
761
|
-
# step along the way would: "get_viewport():gui_get_focus_owner()" is the same call as without
|
|
762
|
-
# the last pair.
|
|
763
|
-
if not Paths.method_of(named).is_empty():
|
|
764
|
-
named = Paths.method_of(named)
|
|
765
|
-
if not holder.has_method(named):
|
|
766
|
-
return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
|
|
767
|
-
|
|
768
|
-
# Checked before the call rather than left to it. `callv` raises inside the game when an
|
|
769
|
-
# argument cannot be converted, and an error raised in a game somebody is playing holds it at a
|
|
770
|
-
# debugger break: every later call then reports a game that is not responding, which points
|
|
771
|
-
# nowhere near the argument. A wrong argument costs a refusal, never the session's game.
|
|
772
|
-
var deserialized_args: Array = []
|
|
773
|
-
for index: int in args.size():
|
|
774
|
-
var wants: int = _values.parameter_type(holder, named, index)
|
|
775
|
-
if wants == TYPE_OBJECT and (args[index] is String or args[index] is Dictionary):
|
|
776
|
-
var declared: String = _values.parameter_class(holder, named, index)
|
|
777
|
-
var named_object: Dictionary = _object_named(node, node_path, args[index], declared)
|
|
778
|
-
if named_object.has("message"):
|
|
779
|
-
return {
|
|
780
|
-
"type": "error",
|
|
781
|
-
"message":
|
|
782
|
-
"%s.%s argument %d: %s" % [reached["called"], named, index + 1, named_object["message"]]
|
|
783
|
-
}
|
|
784
|
-
deserialized_args.append(named_object["object"])
|
|
785
|
-
continue
|
|
786
|
-
var given: Variant = _values.fitted(args[index], wants)
|
|
787
|
-
if not Values.acceptable(given, wants):
|
|
788
|
-
return {
|
|
789
|
-
"type": "error",
|
|
790
|
-
"message":
|
|
791
|
-
(
|
|
792
|
-
"%s.%s takes %s as argument %d and was given %s, which cannot be converted."
|
|
793
|
-
% [reached["called"], named, type_string(wants), index + 1, type_string(typeof(given))]
|
|
794
|
-
)
|
|
795
|
-
}
|
|
796
|
-
# A typed list or map is built as one, since a plain one handed to such a parameter raises
|
|
797
|
-
# inside the game, which is what every check here exists to stop.
|
|
798
|
-
var container: Variant = _values.parameter_container(holder, named, index)
|
|
799
|
-
if container != null:
|
|
800
|
-
var typed: Dictionary = _values.typed_like(
|
|
801
|
-
given, container, _element_object.bind(node, node_path)
|
|
802
|
-
)
|
|
803
|
-
if typed.has("message"):
|
|
804
|
-
return {
|
|
805
|
-
"type": "error",
|
|
806
|
-
"message":
|
|
807
|
-
(
|
|
808
|
-
"%s.%s takes a typed %s as argument %d: %s."
|
|
809
|
-
% [reached["called"], named, _sort_of(container), index + 1, typed["message"]]
|
|
810
|
-
)
|
|
811
|
-
}
|
|
812
|
-
given = typed["value"]
|
|
813
|
-
deserialized_args.append(given)
|
|
814
|
-
|
|
815
|
-
# Timed around the call alone, since the bridge's round trip is a second or more and swamps what
|
|
816
|
-
# one click's work costs; the frame metrics read whichever frame is last, which is not this one.
|
|
817
|
-
var began: int = Time.get_ticks_usec()
|
|
818
|
-
var result: Variant = holder.callv(named, deserialized_args)
|
|
819
|
-
var elapsed: int = Time.get_ticks_usec() - began
|
|
820
|
-
|
|
821
|
-
return {
|
|
822
|
-
"type": "method_result",
|
|
823
|
-
"path": node_path,
|
|
824
|
-
"method": method,
|
|
825
|
-
"result": _values.serialize(result),
|
|
826
|
-
"elapsed_usec": elapsed
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
## [method _object_named] with the path first, which is the order [method Values.typed_like] calls
|
|
831
|
-
## an element resolver in.
|
|
832
|
-
func _element_object(given: Variant, declared: String, node: Node, node_path: String) -> Dictionary:
|
|
833
|
-
return _object_named(node, node_path, given, declared)
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
## "list" or "map", for a refusal about a typed container.
|
|
837
|
-
static func _sort_of(container: Variant) -> String:
|
|
838
|
-
return "list" if container is Array else "map"
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
## The object [param given] names for a slot or a parameter declared as [param declared], as
|
|
842
|
-
## `{"object": ...}`, or `{"message": ...}` saying why it names none that fits.
|
|
843
|
-
##
|
|
844
|
-
## JSON cannot carry an object, so one the game holds is named by its path, read from the node the
|
|
845
|
-
## call or the write was made on, and handed over as that instance rather than a copy: a method
|
|
846
|
-
## given a piece of the game's state acts on the piece the game keeps, and a slot written with one
|
|
847
|
-
## holds the game's own.
|
|
848
|
-
##
|
|
849
|
-
## The text null names no object, which is what an object slot or parameter takes to be emptied. A
|
|
850
|
-
## caller's null arrived as that text in #772 and #783, and was looked up as a node called "null".
|
|
851
|
-
func _object_named(node: Node, node_path: String, given: Variant, declared: String) -> Dictionary:
|
|
852
|
-
if given is String and str(given).strip_edges() == "null":
|
|
853
|
-
return {"object": null}
|
|
854
|
-
var found: Dictionary = Paths.object_at(_host.get_tree().root, node, node_path, given)
|
|
855
|
-
if found.has("message"):
|
|
856
|
-
return found
|
|
857
|
-
var instance: Object = found["object"]
|
|
858
|
-
if not declared.is_empty() and not Values.is_a(instance, declared):
|
|
859
|
-
return {"message": "%s is %s, not a %s" % [str(given), Values.class_of(instance), declared]}
|
|
860
|
-
return found
|
|
861
|
-
|
|
862
|
-
|
|
863
571
|
func get_metrics(params: Dictionary) -> Dictionary:
|
|
864
572
|
var metrics: Array = params.get("metrics", [])
|
|
865
573
|
var all: Dictionary = {
|
|
@@ -17,20 +17,32 @@ extends RefCounted
|
|
|
17
17
|
## sit inside another viewport. A SubViewport drawn some other way, onto a mesh for one, has no
|
|
18
18
|
## place on the screen to carry the point to, and the point stays in it.
|
|
19
19
|
static func reach(viewport: Viewport, point: Vector2) -> Dictionary:
|
|
20
|
+
var out: Array[Dictionary] = steps_out(viewport, point)
|
|
21
|
+
return out.back()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Every viewport a pointer at [param point] in [param viewport] passes through on the way out to
|
|
25
|
+
## the one it arrives at, innermost first, as {"viewport", "point", "through"}: the point in that
|
|
26
|
+
## viewport's space, and the embedded [Window] or the [SubViewportContainer] in it that shows the
|
|
27
|
+
## step before, null for the first. What [method reach] answers is the last of them.
|
|
28
|
+
static func steps_out(viewport: Viewport, point: Vector2) -> Array[Dictionary]:
|
|
29
|
+
var out: Array[Dictionary] = [{"viewport": viewport, "point": point, "through": null}]
|
|
20
30
|
while true:
|
|
21
31
|
var window: Window = viewport as Window
|
|
22
32
|
if window != null and window.is_embedded() and window.get_parent() != null:
|
|
23
33
|
point += Vector2(window.position)
|
|
24
34
|
viewport = window.get_parent().get_viewport()
|
|
35
|
+
out.append({"viewport": viewport, "point": point, "through": window})
|
|
25
36
|
continue
|
|
26
37
|
var shown_by: SubViewportContainer = viewport.get_parent() as SubViewportContainer
|
|
27
38
|
if viewport is SubViewport and shown_by != null:
|
|
28
39
|
var scaled: Vector2 = point * float(shown_by.stretch_shrink) if shown_by.stretch else point
|
|
29
40
|
point = shown_by.get_global_transform_with_canvas() * scaled
|
|
30
41
|
viewport = shown_by.get_viewport()
|
|
42
|
+
out.append({"viewport": viewport, "point": point, "through": shown_by})
|
|
31
43
|
continue
|
|
32
44
|
break
|
|
33
|
-
return
|
|
45
|
+
return out
|
|
34
46
|
|
|
35
47
|
|
|
36
48
|
## Where [param point], in [param viewport]'s own coordinates, is in the pixels of the window the
|
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
const Queries = preload("runtime_queries.gd")
|
|
6
6
|
const Read = preload("reading.gd")
|
|
7
7
|
const Says = preload("runtime_says.gd")
|
|
8
|
+
const Screen = preload("runtime_screen.gd")
|
|
8
9
|
const Values = preload("runtime_values.gd")
|
|
9
10
|
const Words = preload("runtime_words.gd")
|
|
10
11
|
|
|
@@ -68,8 +69,12 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
68
69
|
var areas: Array[Rect2] = _areas_of(drawn)
|
|
69
70
|
var covered: int = 0
|
|
70
71
|
var cover: Node = null
|
|
72
|
+
var outside: int = 0
|
|
71
73
|
var open: Array[int] = []
|
|
72
74
|
for index: int in matched.size():
|
|
75
|
+
if _off_screen(matched[index]):
|
|
76
|
+
outside += 1
|
|
77
|
+
continue
|
|
73
78
|
var over: Node = _cover_of(matched[index], drawn, areas)
|
|
74
79
|
if over == null:
|
|
75
80
|
open.append(index)
|
|
@@ -98,8 +103,8 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
98
103
|
"type": "error",
|
|
99
104
|
"message":
|
|
100
105
|
(
|
|
101
|
-
'no control on screen%s says "%s", so nothing was clicked%s%s'
|
|
102
|
-
% [under, wanted, _hidden_note(hidden), _covered_note(covered, cover)]
|
|
106
|
+
'no control on screen%s says "%s", so nothing was clicked%s%s%s'
|
|
107
|
+
% [under, wanted, _hidden_note(hidden), _outside_note(outside), _covered_note(covered, cover)]
|
|
103
108
|
)
|
|
104
109
|
}
|
|
105
110
|
var best: int = ranks.count(ranks[0])
|
|
@@ -137,10 +142,13 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
137
142
|
return {"path": str(targets[index].get_path()), "found": found}
|
|
138
143
|
|
|
139
144
|
|
|
140
|
-
## How good a match [param target] is for a click:
|
|
141
|
-
## the whole of its words over a part of them.
|
|
145
|
+
## How good a match [param target] is for a click: an enabled button over anything else, and then
|
|
146
|
+
## the whole of its words over a part of them. A disabled button ranks with what is not a button,
|
|
147
|
+
## since a click on it presses nothing: counted as a button, one tied with an enabled button saying
|
|
148
|
+
## the same words and the click was refused as not clear.
|
|
142
149
|
static func _rank(target: Control, whole: bool) -> int:
|
|
143
|
-
|
|
150
|
+
var button: BaseButton = target as BaseButton
|
|
151
|
+
return (2 if button != null and not button.disabled else 0) + (1 if whole else 0)
|
|
144
152
|
|
|
145
153
|
|
|
146
154
|
## Whether one of [param wanted]'s alternatives is the whole of [param said], or the whole of one of
|
|
@@ -178,14 +186,17 @@ static func _described(count: int, rank: int, wanted: String, under: String) ->
|
|
|
178
186
|
return "%d %ss on screen%s say %s" % [count, what, under, how]
|
|
179
187
|
|
|
180
188
|
|
|
181
|
-
## Why the matches below [param rank] were passed over, said of one of them when [param one].
|
|
189
|
+
## Why the matches below [param rank] were passed over, said of one of them when [param one]. As
|
|
190
|
+
## what the code knows, which is whether a control is an enabled button: a card taking clicks in its
|
|
191
|
+
## own `gui_input` is not one, and calling it something that cannot be pressed was a guess.
|
|
182
192
|
static func _passed_over(rank: int, one: bool) -> String:
|
|
183
193
|
var says: String = "says" if one else "say"
|
|
194
|
+
var is_not: String = "is not an enabled button" if one else "are not enabled buttons"
|
|
184
195
|
match rank:
|
|
185
196
|
3:
|
|
186
|
-
return "
|
|
197
|
+
return "%s or %s it as part of more" % [is_not, says]
|
|
187
198
|
2:
|
|
188
|
-
return
|
|
199
|
+
return is_not
|
|
189
200
|
_:
|
|
190
201
|
return "%s it as part of more" % says
|
|
191
202
|
|
|
@@ -197,6 +208,14 @@ static func _hidden_note(hidden: int) -> String:
|
|
|
197
208
|
return "; %d hidden control%s" % [hidden, " says it" if hidden == 1 else "s say it"]
|
|
198
209
|
|
|
199
210
|
|
|
211
|
+
## What to add to a refusal that found nothing on screen, when words matched controls a click cannot
|
|
212
|
+
## reach: off the viewport, or clipped away by a container it does not scroll.
|
|
213
|
+
static func _outside_note(outside: int) -> String:
|
|
214
|
+
if outside == 0:
|
|
215
|
+
return ""
|
|
216
|
+
return "; %d control%s it outside what the screen shows" % [outside, " says" if outside == 1 else "s say"]
|
|
217
|
+
|
|
218
|
+
|
|
200
219
|
## What to add to a refusal that found nothing on screen, when the controls saying it are covered.
|
|
201
220
|
static func _covered_note(covered: int, cover: Node) -> String:
|
|
202
221
|
if covered == 0:
|
|
@@ -271,26 +290,22 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
|
|
|
271
290
|
## page covered was picked over the ones in view, scrolled up under the page and pressed there.
|
|
272
291
|
##
|
|
273
292
|
## [param areas] holds the box around each of [param drawn], so everything whose box misses the
|
|
274
|
-
## point is passed over before anything costlier is asked of it.
|
|
293
|
+
## point is passed over before anything costlier is asked of it. A window's box is in the space of
|
|
294
|
+
## the viewport it is embedded in, which is not the target's when the target is in another window,
|
|
295
|
+
## so a window is judged by [method _window_over] instead.
|
|
275
296
|
static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2]) -> Node:
|
|
276
297
|
var viewport: Viewport = target.get_viewport()
|
|
277
|
-
var point: Vector2 =
|
|
278
|
-
if _clipped_away(target, point):
|
|
279
|
-
point = _where_it_lands(target, point)
|
|
298
|
+
var point: Vector2 = _where_it_is_clicked(target)
|
|
280
299
|
var layer: int = _layer_of(target)
|
|
281
300
|
for at: int in drawn.size():
|
|
282
|
-
if not areas[at].has_point(point):
|
|
283
|
-
continue
|
|
284
301
|
var other: Node = drawn[at]
|
|
285
302
|
var window: Window = other as Window
|
|
286
303
|
if window != null:
|
|
287
|
-
if (
|
|
288
|
-
window.get_parent() != null
|
|
289
|
-
and window.get_parent().get_viewport() == viewport
|
|
290
|
-
and not window.is_ancestor_of(target)
|
|
291
|
-
):
|
|
304
|
+
if _window_over(target, point, window):
|
|
292
305
|
return window
|
|
293
306
|
continue
|
|
307
|
+
if not areas[at].has_point(point):
|
|
308
|
+
continue
|
|
294
309
|
var control: Control = other
|
|
295
310
|
if control == target or control.is_ancestor_of(target) or target.is_ancestor_of(control):
|
|
296
311
|
continue
|
|
@@ -304,6 +319,42 @@ static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2])
|
|
|
304
319
|
return null
|
|
305
320
|
|
|
306
321
|
|
|
322
|
+
## Where a click at [param target] lands in its viewport: its centre, or where scrolling will bring
|
|
323
|
+
## the centre when a ScrollContainer holding it clips it away, as the click scrolls it first.
|
|
324
|
+
static func _where_it_is_clicked(target: Control) -> Vector2:
|
|
325
|
+
var point: Vector2 = target.get_global_transform_with_canvas() * (target.size * 0.5)
|
|
326
|
+
if _clipped_away(target, point):
|
|
327
|
+
point = _where_it_lands(target, point)
|
|
328
|
+
return point
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
## Whether the embedded [param window] is drawn over [param point], [param target]'s centre in its
|
|
332
|
+
## own viewport, with that point carried out to the viewport the window is embedded in.
|
|
333
|
+
##
|
|
334
|
+
## Over it when the window holds the point there and is not the window the target is in, and, when
|
|
335
|
+
## the target is in another window embedded in the same place, when it is stacked above that one:
|
|
336
|
+
## the embedder's list of windows runs from the bottom up, measured on 4.7.2. Judged only in the
|
|
337
|
+
## target's own viewport, a confirmation over a settings dialog did not cover the dialog's OK, and
|
|
338
|
+
## a click by words tied it with the confirmation's own.
|
|
339
|
+
static func _window_over(target: Control, point: Vector2, window: Window) -> bool:
|
|
340
|
+
if window.get_parent() == null or window.is_ancestor_of(target):
|
|
341
|
+
return false
|
|
342
|
+
var host: Viewport = window.get_parent().get_viewport()
|
|
343
|
+
for step: Dictionary in Screen.steps_out(target.get_viewport(), point):
|
|
344
|
+
if step["viewport"] != host:
|
|
345
|
+
continue
|
|
346
|
+
var there: Vector2 = step["point"]
|
|
347
|
+
if not Rect2(window.position, window.size).has_point(there):
|
|
348
|
+
return false
|
|
349
|
+
var through: Variant = step["through"]
|
|
350
|
+
if not through is Window:
|
|
351
|
+
return true
|
|
352
|
+
var own: Window = through
|
|
353
|
+
var stacked: Array[Window] = host.get_embedded_subwindows()
|
|
354
|
+
return stacked.find(window) > stacked.find(own)
|
|
355
|
+
return false
|
|
356
|
+
|
|
357
|
+
|
|
307
358
|
## Whether [param control] holds [param point], given in its viewport's coordinates.
|
|
308
359
|
static func _holds(control: Control, point: Vector2) -> bool:
|
|
309
360
|
var placed: Transform2D = control.get_global_transform_with_canvas()
|
|
@@ -316,17 +367,18 @@ static func _holds(control: Control, point: Vector2) -> bool:
|
|
|
316
367
|
|
|
317
368
|
|
|
318
369
|
## Where [param target]'s centre, now at [param point], will be once the click has scrolled it into
|
|
319
|
-
## view: moved as little as puts the whole of it inside each
|
|
320
|
-
## which is how
|
|
321
|
-
##
|
|
370
|
+
## view: moved as little as puts the whole of it inside each ScrollContainer holding it, innermost
|
|
371
|
+
## first, which is how one brings a control into view. A target larger than a container is centred
|
|
372
|
+
## in it. Only a ScrollContainer, because that is all a click scrolls: another container clipping
|
|
373
|
+
## what it holds, a collapsed section for one, leaves the target where it is.
|
|
322
374
|
static func _where_it_lands(target: Control, point: Vector2) -> Vector2:
|
|
323
375
|
var drawn: Transform2D = target.get_global_transform_with_canvas()
|
|
324
376
|
var half: Vector2 = (drawn * Rect2(Vector2.ZERO, target.size)).size * 0.5
|
|
325
377
|
var landed: Vector2 = point
|
|
326
378
|
var walk: Node = target.get_parent()
|
|
327
379
|
while walk != null:
|
|
328
|
-
var holder:
|
|
329
|
-
if holder != null
|
|
380
|
+
var holder: ScrollContainer = walk as ScrollContainer
|
|
381
|
+
if holder != null:
|
|
330
382
|
var shown: Rect2 = holder.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, holder.size)
|
|
331
383
|
var low: Vector2 = shown.position + half
|
|
332
384
|
var high: Vector2 = shown.end - half
|
|
@@ -338,6 +390,20 @@ static func _where_it_lands(target: Control, point: Vector2) -> Vector2:
|
|
|
338
390
|
return landed
|
|
339
391
|
|
|
340
392
|
|
|
393
|
+
## Whether a click could not reach [param target] however it scrolled: its centre, where the click
|
|
394
|
+
## would bring it, outside its viewport or the one showing that on the screen, or clipped away by a
|
|
395
|
+
## container the click does not scroll. A drawer parked off the side of the screen, or anything
|
|
396
|
+
## outside the 64 by 64 viewport of a game with no window, was listed among the controls on screen.
|
|
397
|
+
static func _off_screen(target: Control) -> bool:
|
|
398
|
+
var point: Vector2 = _where_it_is_clicked(target)
|
|
399
|
+
if _clipped_away(target, point) or not target.get_viewport().get_visible_rect().has_point(point):
|
|
400
|
+
return true
|
|
401
|
+
var reached: Dictionary = Screen.reach(target.get_viewport(), point)
|
|
402
|
+
var viewport: Viewport = reached["viewport"]
|
|
403
|
+
var there: Vector2 = reached["point"]
|
|
404
|
+
return not viewport.get_visible_rect().has_point(there)
|
|
405
|
+
|
|
406
|
+
|
|
341
407
|
## Whether a container clipping what it holds cuts [param point] off from [param control].
|
|
342
408
|
static func _clipped_away(control: Control, point: Vector2) -> bool:
|
|
343
409
|
var walk: Node = control.get_parent()
|