gdharness 1.2.42 → 1.3.1
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 +571 -363
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -19
- package/build/godot/addons/gdharness_editor/bridge_client.gd +9 -9
- package/build/godot/addons/gdharness_editor/file_reads.gd +60 -25
- package/build/godot/addons/gdharness_editor/headless_check.gd +7 -7
- package/build/godot/addons/gdharness_editor/plugin.gd +6 -6
- package/build/godot/addons/gdharness_editor/property_values.gd +124 -62
- package/build/godot/addons/gdharness_editor/reading.gd +28 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +28 -23
- package/build/godot/addons/gdharness_editor/serialisation.gd +45 -94
- package/build/godot/addons/gdharness_editor/tool_executor.gd +24 -24
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +52 -41
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +4 -4
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
- package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +14 -14
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +57 -50
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +17 -17
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +14 -12
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +7 -7
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +82 -78
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +4 -4
- package/build/godot/addons/gdharness_editor/value_shapes.gd +111 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +28 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +18 -18
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +32 -32
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +50 -18
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +86 -80
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +65 -69
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +6 -6
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +8 -8
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +13 -21
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -10
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +7 -7
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +41 -41
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +3 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +33 -33
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +20 -20
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +25 -24
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +56 -50
- package/build/godot/addons/gdharness_runtime/serialisation.gd +45 -94
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +111 -0
- package/build/godot/addons/gdharness_runtime/value_shapes.gd.uid +1 -0
- package/build/godot/operations/animation_reads.gd +7 -19
- package/build/godot/operations/audio_buses.gd +10 -10
- package/build/godot/operations/class_cache.gd +16 -16
- package/build/godot/operations/classdb_queries.gd +12 -12
- package/build/godot/operations/dependencies.gd +33 -29
- package/build/godot/operations/editor_tools.gd +12 -12
- package/build/godot/operations/export_presets.gd +10 -8
- package/build/godot/operations/file_reads.gd +60 -25
- package/build/godot/operations/gdscript_analysis.gd +20 -20
- package/build/godot/operations/gdscript_authoring.gd +17 -15
- package/build/godot/operations/godot_operations.gd +64 -63
- package/build/godot/operations/import_pipeline.gd +20 -20
- package/build/godot/operations/input_actions.gd +17 -17
- package/build/godot/operations/logger.gd +1 -1
- package/build/godot/operations/plugins.gd +8 -8
- package/build/godot/operations/project_config.gd +24 -17
- package/build/godot/operations/project_diagnostics.gd +9 -9
- package/build/godot/operations/property_values.gd +124 -62
- package/build/godot/operations/reading.gd +28 -0
- package/build/godot/operations/rename_checks.gd +3 -3
- package/build/godot/operations/resource_files.gd +5 -5
- package/build/godot/operations/script_check.gd +7 -7
- package/build/godot/operations/serialisation.gd +45 -94
- package/build/godot/operations/value_shapes.gd +111 -0
- package/build/keeper.js +10 -4
- package/package.json +1 -2
- package/build/index.js +0 -45101
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## How each engine type built from fields is written: the fields it needs, the ones it reads when
|
|
4
|
+
## they are there, and what a refusal says about a value that is not one.
|
|
5
|
+
##
|
|
6
|
+
## This file is the one beside the operations, and `bun run sync:gd` copies it into each addon.
|
|
7
|
+
|
|
8
|
+
# What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
|
|
9
|
+
# tag somebody wrote by mistake or wrote for a different shape, and building one out of defaults
|
|
10
|
+
# answers with a Transform2D of zeros where the caller meant something else entirely. Unbuilt, it
|
|
11
|
+
# comes back as it arrived, and whoever asked can read it against the type they were expecting.
|
|
12
|
+
const REQUIRED: Dictionary = {
|
|
13
|
+
"Vector2": [["x", "y"]],
|
|
14
|
+
"Vector2i": [["x", "y"]],
|
|
15
|
+
"Vector3": [["x", "y", "z"]],
|
|
16
|
+
"Vector3i": [["x", "y", "z"]],
|
|
17
|
+
"Vector4": [["x", "y", "z", "w"]],
|
|
18
|
+
"Vector4i": [["x", "y", "z", "w"]],
|
|
19
|
+
"Rect2": [["position", "size"], ["x", "y", "width", "height"]],
|
|
20
|
+
"Rect2i": [["position", "size"], ["x", "y", "width", "height"]],
|
|
21
|
+
"Plane": [["normal", "d"]],
|
|
22
|
+
"Quaternion": [["x", "y", "z", "w"]],
|
|
23
|
+
"AABB": [["position", "size"]],
|
|
24
|
+
"Basis": [["x", "y", "z"]],
|
|
25
|
+
"Transform2D": [["x", "y", "origin"]],
|
|
26
|
+
"Transform3D": [["basis", "origin"]],
|
|
27
|
+
"Projection": [["x", "y", "z", "w"]],
|
|
28
|
+
"Color": [["r", "g", "b"]],
|
|
29
|
+
"NodePath": [["path"]],
|
|
30
|
+
"PackedByteArray": [["base64"]],
|
|
31
|
+
"float": [["value"]],
|
|
32
|
+
"int": [["value"]],
|
|
33
|
+
"RID": [["id"]],
|
|
34
|
+
"Dictionary": [["entries"]],
|
|
35
|
+
}
|
|
36
|
+
## The fields a builder reads when they are there and does without when not.
|
|
37
|
+
const OPTIONAL: Dictionary = {
|
|
38
|
+
"Color": ["a"],
|
|
39
|
+
"PackedByteArray": ["size"],
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## How a value of [param type] is written to be read as one, for a refusal to name, or empty for a
|
|
44
|
+
## type that is not built from fields. A refusal naming only the two types leaves a caller to find
|
|
45
|
+
## the shape in the description of another op.
|
|
46
|
+
static func written_as(type: int) -> String:
|
|
47
|
+
var tag: String = type_string(type)
|
|
48
|
+
var alternatives: Array = REQUIRED.get(tag, [])
|
|
49
|
+
if alternatives.is_empty() or tag in ["float", "int", "Dictionary"]:
|
|
50
|
+
return ""
|
|
51
|
+
var keys: Array = alternatives[0]
|
|
52
|
+
var optional: Array = OPTIONAL.get(tag, [])
|
|
53
|
+
var parts: Array[String] = []
|
|
54
|
+
for key: Variant in keys + optional:
|
|
55
|
+
parts.append('"%s": ...' % key)
|
|
56
|
+
var shape: String = "{%s}" % ", ".join(parts)
|
|
57
|
+
if type == TYPE_COLOR:
|
|
58
|
+
return shape + ' or as its html code, "#ff8000"'
|
|
59
|
+
return shape
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## The keys of [param fields] that a [param tag] built from them would not read, one list for each
|
|
63
|
+
## spelling of its fields carried whole. A rectangle has two: its corners, and four numbers.
|
|
64
|
+
##
|
|
65
|
+
## A builder drops a key it does not know without a word, so {"x": 1, "y": 2, "z": 3} for a Vector2
|
|
66
|
+
## would be written as (1, 2) and answered as written: a value is built only when a list is empty.
|
|
67
|
+
static func spare(fields: Dictionary, tag: String) -> Array:
|
|
68
|
+
var optional: Array = OPTIONAL.get(tag, [])
|
|
69
|
+
var lists: Array = []
|
|
70
|
+
var alternatives: Array = REQUIRED.get(tag, [])
|
|
71
|
+
for alternative: Variant in alternatives:
|
|
72
|
+
var keys: Array = alternative
|
|
73
|
+
if keys.all(func(key: Variant) -> bool: return fields.has(key)):
|
|
74
|
+
var over: Callable = func(key: Variant) -> bool:
|
|
75
|
+
return str(key) != "_type" and not keys.has(key) and not optional.has(key)
|
|
76
|
+
lists.append(fields.keys().filter(over))
|
|
77
|
+
return lists
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## The sentences a refusal of [param value] where a [param type] goes ends with: which of its keys
|
|
81
|
+
## kept it from being read as one, and how one is written. Empty for a type not built from fields.
|
|
82
|
+
##
|
|
83
|
+
## The refusal names only the two types, and the key is what the caller has to take out.
|
|
84
|
+
static func remedy(value: Variant, type: int) -> String:
|
|
85
|
+
var shape: String = written_as(type)
|
|
86
|
+
if shape.is_empty():
|
|
87
|
+
return ""
|
|
88
|
+
return _unread(value, type) + " A %s is sent as %s." % [type_string(type), shape]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
static func _unread(value: Variant, type: int) -> String:
|
|
92
|
+
if not value is Dictionary:
|
|
93
|
+
return ""
|
|
94
|
+
var fields: Dictionary = value
|
|
95
|
+
var tag: String = type_string(type)
|
|
96
|
+
var lists: Array = spare(fields, tag)
|
|
97
|
+
var built: bool = lists.any(func(over: Array) -> bool: return over.is_empty())
|
|
98
|
+
if lists.is_empty() or built or str(fields.get("_type", tag)) != tag:
|
|
99
|
+
return ""
|
|
100
|
+
lists.sort_custom(func(a: Array, b: Array) -> bool: return a.size() < b.size())
|
|
101
|
+
var left: Array = lists[0]
|
|
102
|
+
var named: PackedStringArray = []
|
|
103
|
+
for key: Variant in left:
|
|
104
|
+
var _named: bool = named.append('"%s"' % key)
|
|
105
|
+
var fields_called: String = "field called" if named.size() == 1 else "fields called"
|
|
106
|
+
var note: String = (
|
|
107
|
+
" A %s has no %s %s, so the value is not read as one" % [tag, fields_called, " or ".join(named)]
|
|
108
|
+
)
|
|
109
|
+
if left.has("type") and not fields.has("_type"):
|
|
110
|
+
return note + ': a value names its type under "_type".'
|
|
111
|
+
return note + "."
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://c3dfdk66o90zz
|
|
@@ -139,3 +139,31 @@ static func missing_below(
|
|
|
139
139
|
"its children are " + shown if not children.is_empty() else "it has no children",
|
|
140
140
|
]
|
|
141
141
|
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
## [param said], the refusal for a [param kind] the scene does not have at [param node_path], with
|
|
145
|
+
## what is there when the scene has a node there, [param found] its class, and [param holders], the
|
|
146
|
+
## paths of the nodes in the scene that are one, so the caller has the path to pass. Names up to
|
|
147
|
+
## [constant NAMED_CHILDREN] of them.
|
|
148
|
+
static func not_of_kind(
|
|
149
|
+
said: String, node_path: String, found: String, kind: String, holders: PackedStringArray
|
|
150
|
+
) -> String:
|
|
151
|
+
var told: String = said + ("" if said.ends_with(".") else ".")
|
|
152
|
+
if not found.is_empty():
|
|
153
|
+
told += ' The node at "%s" is %s, not %s.' % [node_path, with_article(found), with_article(kind)]
|
|
154
|
+
if holders.is_empty():
|
|
155
|
+
return told + " The scene holds no %s." % kind
|
|
156
|
+
var quoted: PackedStringArray = []
|
|
157
|
+
for holder: String in holders.slice(0, NAMED_CHILDREN):
|
|
158
|
+
var _quoted: bool = quoted.append('"%s"' % holder)
|
|
159
|
+
var shown: String = ", ".join(quoted)
|
|
160
|
+
if holders.size() > NAMED_CHILDREN:
|
|
161
|
+
shown += " and %d more" % (holders.size() - NAMED_CHILDREN)
|
|
162
|
+
if holders.size() == 1:
|
|
163
|
+
return told + " The scene's %s is at %s." % [kind, shown]
|
|
164
|
+
return told + " The scene's %ss are at %s." % [kind, shown]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
|
|
168
|
+
static func with_article(word: String) -> String:
|
|
169
|
+
return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
|
|
@@ -9,8 +9,8 @@ extends RefCounted
|
|
|
9
9
|
## its first question makes the engine build the tree, and what it reads is what a screen reader is
|
|
10
10
|
## told, names left empty included.
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const GdhRead = preload("reading.gd")
|
|
13
|
+
const GdhSays = preload("runtime_says.gd")
|
|
14
14
|
|
|
15
15
|
## How long the client keeps walking the tree for it to settle: the engine answers the first question
|
|
16
16
|
## with its windows alone and fills them in over the next frames.
|
|
@@ -260,7 +260,7 @@ func get_accessibility(params: Dictionary) -> Dictionary:
|
|
|
260
260
|
var text: String = FileAccess.get_file_as_string(out)
|
|
261
261
|
# Not checked: a file that is not there was never written, which the answer below says.
|
|
262
262
|
var _removed: Error = DirAccess.remove_absolute(out)
|
|
263
|
-
var read: Variant =
|
|
263
|
+
var read: Variant = GdhRead.json_or_null(text)
|
|
264
264
|
if not read is Dictionary:
|
|
265
265
|
return _refused("UI Automation answered nothing readable: %s" % text.left(500))
|
|
266
266
|
var answer: Dictionary = read
|
|
@@ -270,7 +270,7 @@ func get_accessibility(params: Dictionary) -> Dictionary:
|
|
|
270
270
|
var windows: Array = answer.get("windows", [])
|
|
271
271
|
var made_by: Dictionary = {}
|
|
272
272
|
_note_elements(_host.get_tree().root, made_by)
|
|
273
|
-
var placing:
|
|
273
|
+
var placing: GdhPlacing = GdhPlacing.new()
|
|
274
274
|
var elements: int = 0
|
|
275
275
|
var unnamed: int = 0
|
|
276
276
|
for at: int in windows.size():
|
|
@@ -373,7 +373,7 @@ static func _narrow(result: Dictionary, params: Dictionary) -> void:
|
|
|
373
373
|
if window is Dictionary:
|
|
374
374
|
var holder: Dictionary = window
|
|
375
375
|
_keep_only(holder, fields)
|
|
376
|
-
var limit: int =
|
|
376
|
+
var limit: int = GdhRead.as_int(params.get("limit", 0), 0)
|
|
377
377
|
if limit > 0:
|
|
378
378
|
var left: Array[int] = [limit]
|
|
379
379
|
var omitted: Array[int] = [0]
|
|
@@ -401,7 +401,7 @@ static func _matches(element: Dictionary, under: String, said: String) -> bool:
|
|
|
401
401
|
return false
|
|
402
402
|
if not said.is_empty():
|
|
403
403
|
for field: String in ["name", "value", "value_text"]:
|
|
404
|
-
if element.has(field) and
|
|
404
|
+
if element.has(field) and GdhSays.matches(str(element[field]), said):
|
|
405
405
|
return true
|
|
406
406
|
return false
|
|
407
407
|
return true
|
|
@@ -452,7 +452,7 @@ static func _cut(element: Dictionary, left: Array[int], omitted: Array[int]) ->
|
|
|
452
452
|
|
|
453
453
|
## The project's accessibility mode, as the command line's --accessibility names them.
|
|
454
454
|
static func _setting() -> String:
|
|
455
|
-
match
|
|
455
|
+
match GdhRead.as_int(ProjectSettings.get_setting("accessibility/general/accessibility_support", 0), 0):
|
|
456
456
|
1:
|
|
457
457
|
return "always"
|
|
458
458
|
2:
|
|
@@ -498,7 +498,7 @@ static func _window_with(node: Node, window_id: int) -> Window:
|
|
|
498
498
|
## UI Automation's runtime id for an element ends in the four halves of AccessKit's id for it, and
|
|
499
499
|
## Godot gives AccessKit the element's RID: measured on 4.7.2, /root/Main's element answered
|
|
500
500
|
## 114,1,0,0 with RID 114 << 32 | 1. Which pair holds it is not assumed, so both are tried.
|
|
501
|
-
static func _match(element: Dictionary, made_by: Dictionary, placing:
|
|
501
|
+
static func _match(element: Dictionary, made_by: Dictionary, placing: GdhPlacing) -> void:
|
|
502
502
|
var children: Array = element.get("children", [])
|
|
503
503
|
for child: Variant in children:
|
|
504
504
|
if not child is Dictionary:
|
|
@@ -530,14 +530,14 @@ static func _match(element: Dictionary, made_by: Dictionary, placing: Placing) -
|
|
|
530
530
|
## column of plain containers came out straight under the window. So the node is found where it
|
|
531
531
|
## must be: below the shown element's node, itself left out of the tree (a node that is shown would
|
|
532
532
|
## have the part under it), drawn around the part, and, for text, holding its words.
|
|
533
|
-
static func _attribute(element: Dictionary, holder: Node, placing:
|
|
533
|
+
static func _attribute(element: Dictionary, holder: Node, placing: GdhPlacing, is_window: bool) -> void:
|
|
534
534
|
var children: Array = element.get("children", [])
|
|
535
535
|
for child: Variant in children:
|
|
536
536
|
if not child is Dictionary:
|
|
537
537
|
continue
|
|
538
538
|
var inner: Dictionary = child
|
|
539
539
|
if inner.has(NODE_ID):
|
|
540
|
-
var node: Node = instance_from_id(
|
|
540
|
+
var node: Node = instance_from_id(GdhRead.as_int(inner[NODE_ID], 0))
|
|
541
541
|
var _dropped: bool = inner.erase(NODE_ID)
|
|
542
542
|
_attribute(inner, node if node != null else holder, placing, false)
|
|
543
543
|
continue
|
|
@@ -561,16 +561,16 @@ static func _attribute(element: Dictionary, holder: Node, placing: Placing, is_w
|
|
|
561
561
|
## The nodes that may have made [param element], below [param holder]: the deepest ones drawn
|
|
562
562
|
## around it whose own elements are not shown, narrowed to those holding its words when that leaves
|
|
563
563
|
## any. [param holder] alone when none is drawn around it, or when it has no bounds to look by.
|
|
564
|
-
static func _owners_of(element: Dictionary, holder: Node, placing:
|
|
564
|
+
static func _owners_of(element: Dictionary, holder: Node, placing: GdhPlacing) -> Array[Node]:
|
|
565
565
|
var bounds: Variant = element.get("bounds")
|
|
566
566
|
if not bounds is Dictionary:
|
|
567
567
|
return [holder]
|
|
568
568
|
var box: Dictionary = bounds
|
|
569
569
|
var drawn: Rect2 = Rect2(
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
570
|
+
GdhRead.as_float(box.get("x", 0)),
|
|
571
|
+
GdhRead.as_float(box.get("y", 0)),
|
|
572
|
+
GdhRead.as_float(box.get("width", 0)),
|
|
573
|
+
GdhRead.as_float(box.get("height", 0))
|
|
574
574
|
)
|
|
575
575
|
var found: Array[Node] = []
|
|
576
576
|
_drawn_around(holder, drawn, placing, found)
|
|
@@ -590,7 +590,7 @@ static func _owners_of(element: Dictionary, holder: Node, placing: Placing) -> A
|
|
|
590
590
|
|
|
591
591
|
## Where a Control is drawn is read off its screen transform, which for a window that is not
|
|
592
592
|
## embedded is in that window's own pixels, as the bounds are.
|
|
593
|
-
static func _drawn_around(node: Node, drawn: Rect2, placing:
|
|
593
|
+
static func _drawn_around(node: Node, drawn: Rect2, placing: GdhPlacing, found: Array[Node]) -> void:
|
|
594
594
|
for child: Node in node.get_children(true):
|
|
595
595
|
# A node that is shown has its parts under it, and so do the nodes below it.
|
|
596
596
|
if placing.shown.has(child.get_instance_id()):
|
|
@@ -632,7 +632,7 @@ static func _text_of(node: Node) -> String:
|
|
|
632
632
|
static func _made(parts: Array, made_by: Dictionary) -> Node:
|
|
633
633
|
var halves: Array[int] = []
|
|
634
634
|
for part: Variant in parts:
|
|
635
|
-
halves.append(
|
|
635
|
+
halves.append(GdhRead.as_int(part, 0) & 0xFFFFFFFF)
|
|
636
636
|
for at: int in [0, 2]:
|
|
637
637
|
var id: int = (halves[at] << 32) | halves[at + 1]
|
|
638
638
|
if made_by.has(id):
|
|
@@ -642,7 +642,7 @@ static func _made(parts: Array, made_by: Dictionary) -> Node:
|
|
|
642
642
|
|
|
643
643
|
## Counts kept while the elements are placed, and the nodes whose own elements UI Automation shows,
|
|
644
644
|
## by instance id.
|
|
645
|
-
class
|
|
645
|
+
class GdhPlacing:
|
|
646
646
|
extends RefCounted
|
|
647
647
|
var asked: int = 0
|
|
648
648
|
var placed: int = 0
|
|
@@ -10,20 +10,20 @@ extends Node
|
|
|
10
10
|
## wanted. Both directions of the socket carry one JSON object per line. Every request names an
|
|
11
11
|
## id and the reply carries it back.
|
|
12
12
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const PROTOCOL: int =
|
|
13
|
+
const GdhAccessibility = preload("runtime_accessibility.gd")
|
|
14
|
+
const GdhCapture = preload("runtime_capture.gd")
|
|
15
|
+
const GdhChanges = preload("runtime_changes.gd")
|
|
16
|
+
const GdhErrorReport = preload("error_report.gd")
|
|
17
|
+
const GdhInputCommands = preload("runtime_input.gd")
|
|
18
|
+
const GdhPad = preload("runtime_pad.gd")
|
|
19
|
+
const GdhQueries = preload("runtime_queries.gd")
|
|
20
|
+
const GdhRead = preload("reading.gd")
|
|
21
|
+
const GdhTargets = preload("runtime_targets.gd")
|
|
22
|
+
const GdhTyping = preload("runtime_typing.gd")
|
|
23
|
+
const GdhValues = preload("runtime_values.gd")
|
|
24
|
+
const GdhWaits = preload("runtime_waits.gd")
|
|
25
|
+
|
|
26
|
+
const PROTOCOL: int = 3
|
|
27
27
|
const DEFAULT_BIND_ADDRESS: String = "127.0.0.1"
|
|
28
28
|
const BIND_ADDRESS_SETTING: String = "gdharness/runtime/bind_address"
|
|
29
29
|
## 0 asks the operating system for a free port, which is the default and what the server
|
|
@@ -83,19 +83,19 @@ const ERROR_REPORT_NAME: String = "runtime-%d.log"
|
|
|
83
83
|
const VERSION_MARKER: String = "res://addons/gdharness_runtime/.gdharness-version"
|
|
84
84
|
const DIGEST_MARKER: String = "res://addons/gdharness_runtime/.gdharness-digest"
|
|
85
85
|
|
|
86
|
-
var values:
|
|
86
|
+
var values: GdhValues = GdhValues.new()
|
|
87
87
|
|
|
88
88
|
# The modules are members and not locals of _init, because a Callable holds its object by id
|
|
89
89
|
# rather than by reference, and a module referenced only by the Callables in the command table
|
|
90
90
|
# would be freed on the way out of _init, leaving every command "unknown".
|
|
91
|
-
var _queries:
|
|
92
|
-
var _changes:
|
|
93
|
-
var _input:
|
|
94
|
-
var _typing:
|
|
95
|
-
var _pad:
|
|
96
|
-
var _capture:
|
|
97
|
-
var _waits:
|
|
98
|
-
var _accessibility:
|
|
91
|
+
var _queries: GdhQueries = GdhQueries.new(self, values)
|
|
92
|
+
var _changes: GdhChanges = GdhChanges.new(self, values)
|
|
93
|
+
var _input: GdhInputCommands = GdhInputCommands.new(self, values)
|
|
94
|
+
var _typing: GdhTyping = GdhTyping.new(self)
|
|
95
|
+
var _pad: GdhPad = GdhPad.new(self)
|
|
96
|
+
var _capture: GdhCapture = GdhCapture.new(self, values)
|
|
97
|
+
var _waits: GdhWaits = GdhWaits.new(self, values)
|
|
98
|
+
var _accessibility: GdhAccessibility = GdhAccessibility.new(self)
|
|
99
99
|
|
|
100
100
|
var _server: TCPServer
|
|
101
101
|
var _clients: Array[StreamPeerTCP] = []
|
|
@@ -114,7 +114,7 @@ var _enabled: bool = true
|
|
|
114
114
|
var _announcement: String = ""
|
|
115
115
|
## Held for as long as the game runs: a Logger the engine has is kept by reference, and one
|
|
116
116
|
## that goes away mid-run takes the report with it.
|
|
117
|
-
var _errors:
|
|
117
|
+
var _errors: GdhErrorReport
|
|
118
118
|
## Every command, by the name a request uses, as the module method that answers it.
|
|
119
119
|
var _commands: Dictionary = {}
|
|
120
120
|
## The addon this game loaded, read once at start: an upgrade rewrites the markers under a running
|
|
@@ -174,7 +174,7 @@ func _size_a_windowless_game() -> void:
|
|
|
174
174
|
if DisplayServer.get_name() != "headless" or _script_run() or not OS.is_debug_build():
|
|
175
175
|
return
|
|
176
176
|
var root: Window = get_tree().root
|
|
177
|
-
if Vector2(root.size) !=
|
|
177
|
+
if Vector2(root.size) != GdhTargets.HEADLESS_VIEWPORT:
|
|
178
178
|
return
|
|
179
179
|
root.size = Vector2i(_window_dimension("width", 1152), _window_dimension("height", 648))
|
|
180
180
|
|
|
@@ -182,12 +182,12 @@ func _size_a_windowless_game() -> void:
|
|
|
182
182
|
## The window's [param axis] as the project opens it: the override when one is set, otherwise the
|
|
183
183
|
## viewport's size, whose engine default is [param fallback].
|
|
184
184
|
static func _window_dimension(axis: String, fallback: int) -> int:
|
|
185
|
-
var override: int =
|
|
185
|
+
var override: int = GdhRead.as_int(
|
|
186
186
|
ProjectSettings.get_setting("display/window/size/window_%s_override" % axis, 0)
|
|
187
187
|
)
|
|
188
188
|
if override > 0:
|
|
189
189
|
return override
|
|
190
|
-
return
|
|
190
|
+
return GdhRead.as_int(
|
|
191
191
|
ProjectSettings.get_setting("display/window/size/viewport_%s" % axis, fallback), fallback
|
|
192
192
|
)
|
|
193
193
|
|
|
@@ -342,7 +342,7 @@ func _report_errors() -> void:
|
|
|
342
342
|
var made: Error = DirAccess.make_dir_recursive_absolute(directory)
|
|
343
343
|
if made != OK and made != ERR_ALREADY_EXISTS:
|
|
344
344
|
return
|
|
345
|
-
_errors =
|
|
345
|
+
_errors = GdhErrorReport.new(directory.path_join(ERROR_REPORT_NAME % OS.get_process_id()))
|
|
346
346
|
OS.add_logger(_errors)
|
|
347
347
|
|
|
348
348
|
|
|
@@ -404,7 +404,7 @@ func _drain(client: StreamPeerTCP) -> bool:
|
|
|
404
404
|
var sent: Array = client.put_partial_data(owed)
|
|
405
405
|
if sent[0] != OK:
|
|
406
406
|
return false
|
|
407
|
-
_outgoing[client] = owed.slice(
|
|
407
|
+
_outgoing[client] = owed.slice(GdhRead.as_int(sent[1]))
|
|
408
408
|
return true
|
|
409
409
|
|
|
410
410
|
|
|
@@ -438,14 +438,14 @@ func _start_server() -> void:
|
|
|
438
438
|
# own path: a gate that starts sixteen engines at once announces sixteen games that are not
|
|
439
439
|
# games, and a client asking the runtime anything while they run can be answered by whichever
|
|
440
440
|
# of them replies first. Measured on two projects before it was written here.
|
|
441
|
-
if _script_run() and not
|
|
441
|
+
if _script_run() and not GdhRead.as_bool(ProjectSettings.get_setting(SCRIPT_RUNS_SETTING, false)):
|
|
442
442
|
_enabled = false
|
|
443
443
|
return
|
|
444
444
|
|
|
445
445
|
_server = TCPServer.new()
|
|
446
446
|
# listen() defaults bind_address to "*", which exposes the game to the whole network.
|
|
447
447
|
var bind_address: String = str(ProjectSettings.get_setting(BIND_ADDRESS_SETTING, DEFAULT_BIND_ADDRESS))
|
|
448
|
-
var wanted_port: int =
|
|
448
|
+
var wanted_port: int = GdhRead.as_int(ProjectSettings.get_setting(PORT_SETTING, 0))
|
|
449
449
|
var error: Error = _server.listen(wanted_port, bind_address)
|
|
450
450
|
if error != OK:
|
|
451
451
|
# A warning, not an error: callers treat any ERROR line on stderr as a failed
|
|
@@ -502,7 +502,7 @@ func _announce(bind_address: String) -> void:
|
|
|
502
502
|
# Written whole under a name no server reads and moved into place, so no look ever meets
|
|
503
503
|
# the file empty: a server sweeping the directory between an open and a write took the
|
|
504
504
|
# announcement for a dead game's leftover, and the directory is shared by every server on
|
|
505
|
-
# the machine
|
|
505
|
+
# the machine.
|
|
506
506
|
var staged: String = path + ".tmp"
|
|
507
507
|
var file: FileAccess = FileAccess.open(staged, FileAccess.WRITE)
|
|
508
508
|
if file == null:
|
|
@@ -2,20 +2,20 @@ extends RefCounted
|
|
|
2
2
|
|
|
3
3
|
## A picture of the running game, as a PNG written where the server asked for it.
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
5
|
+
const GdhRead = preload("reading.gd")
|
|
6
|
+
const GdhValues = preload("runtime_values.gd")
|
|
7
|
+
const GdhQueries = preload("runtime_queries.gd")
|
|
8
|
+
const GdhScreen = preload("runtime_screen.gd")
|
|
9
9
|
|
|
10
10
|
## How far zoom enlarges at most. Sixteen makes a 3-pixel ring 48 pixels across, past any need to
|
|
11
11
|
## see it, and a 200-pixel region 3200, past the size anybody reads a picture at.
|
|
12
12
|
const ZOOM_MOST: int = 16
|
|
13
13
|
|
|
14
14
|
var _host: Node
|
|
15
|
-
var _values:
|
|
15
|
+
var _values: GdhValues
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
func _init(host: Node, values:
|
|
18
|
+
func _init(host: Node, values: GdhValues) -> void:
|
|
19
19
|
_host = host
|
|
20
20
|
_values = values
|
|
21
21
|
|
|
@@ -29,13 +29,42 @@ func capture_viewport(params: Dictionary) -> Dictionary:
|
|
|
29
29
|
if viewport_path.is_empty():
|
|
30
30
|
return await capture_screenshot(params)
|
|
31
31
|
|
|
32
|
-
var standing: Dictionary =
|
|
32
|
+
var standing: Dictionary = GdhValues.node_at(_host.get_tree().root, viewport_path)
|
|
33
33
|
if standing.has("message"):
|
|
34
34
|
return standing
|
|
35
35
|
var node: Node = standing["node"]
|
|
36
36
|
if not node is Viewport:
|
|
37
37
|
return {"type": "error", "message": "Node is not a Viewport: " + viewport_path}
|
|
38
38
|
var viewport: Viewport = node
|
|
39
|
+
# The root's own state is refused in _capture. Another window of the game's that is hidden or
|
|
40
|
+
# minimised holds no frame, and asking it for its image makes the engine log the empty read into
|
|
41
|
+
# the game's output, so the run would stop unclean for an error the capture raised.
|
|
42
|
+
if viewport is Window and viewport != _host.get_tree().root:
|
|
43
|
+
var window: Window = viewport
|
|
44
|
+
if not window.visible:
|
|
45
|
+
return {
|
|
46
|
+
"type": "error",
|
|
47
|
+
"message":
|
|
48
|
+
(
|
|
49
|
+
(
|
|
50
|
+
"The Window at %s is hidden, so nothing is drawn to it. Show it (its visible "
|
|
51
|
+
+ "property) and ask again."
|
|
52
|
+
)
|
|
53
|
+
% viewport_path
|
|
54
|
+
),
|
|
55
|
+
}
|
|
56
|
+
if not window.can_draw():
|
|
57
|
+
return {
|
|
58
|
+
"type": "error",
|
|
59
|
+
"message":
|
|
60
|
+
(
|
|
61
|
+
(
|
|
62
|
+
"Nothing is being drawn to the Window at %s, which is minimised. Restore it and "
|
|
63
|
+
+ "ask again."
|
|
64
|
+
)
|
|
65
|
+
% viewport_path
|
|
66
|
+
),
|
|
67
|
+
}
|
|
39
68
|
if not viewport is SubViewport:
|
|
40
69
|
return await _capture(viewport, params)
|
|
41
70
|
# Drawn now rather than taken as it stands. A SubViewport the game does not redraw every frame
|
|
@@ -74,7 +103,8 @@ func _lay_native_windows(image: Image) -> void:
|
|
|
74
103
|
if node == root or not node is Window:
|
|
75
104
|
continue
|
|
76
105
|
var window: Window = node
|
|
77
|
-
|
|
106
|
+
# A minimised window is not on the screen, and its texture holds the last frame it drew.
|
|
107
|
+
if not window.visible or window.is_embedded() or not window.can_draw():
|
|
78
108
|
continue
|
|
79
109
|
var drawn: Image = window.get_texture().get_image()
|
|
80
110
|
if drawn == null or drawn.is_empty():
|
|
@@ -106,8 +136,10 @@ static func scaled_to(drawn: Vector2i, width: int, height: int) -> Vector2i:
|
|
|
106
136
|
func _cut_asked(params: Dictionary, in_window: bool) -> Dictionary:
|
|
107
137
|
var region_given: Variant = params.get("region")
|
|
108
138
|
var node_path: String = str(params.get("nodePath", ""))
|
|
109
|
-
var zoom: int =
|
|
110
|
-
var sized: bool =
|
|
139
|
+
var zoom: int = GdhRead.as_int(params.get("zoom", 1), 1)
|
|
140
|
+
var sized: bool = (
|
|
141
|
+
GdhRead.as_int(params.get("width", 0)) > 0 or GdhRead.as_int(params.get("height", 0)) > 0
|
|
142
|
+
)
|
|
111
143
|
if region_given != null and not node_path.is_empty():
|
|
112
144
|
return _refused("region and nodePath each name the part to capture; give one of them")
|
|
113
145
|
if (region_given != null or not node_path.is_empty()) and not in_window:
|
|
@@ -148,7 +180,7 @@ static func _refused(message: String) -> Dictionary:
|
|
|
148
180
|
## The rectangle [param given] names: the four numbers x, y, width and height, or a rectangle as the
|
|
149
181
|
## runtime answers one, its two corners tagged with their type, passed back as it came. Null when it
|
|
150
182
|
## is neither or has no area.
|
|
151
|
-
static func region_of(values:
|
|
183
|
+
static func region_of(values: GdhValues, given: Variant) -> Variant:
|
|
152
184
|
if not given is Dictionary:
|
|
153
185
|
return null
|
|
154
186
|
var fields: Dictionary = given
|
|
@@ -175,21 +207,21 @@ static func region_of(values: Values, given: Variant) -> Variant:
|
|
|
175
207
|
static func drawn_rect_of(node: Node) -> Variant:
|
|
176
208
|
if node is Control:
|
|
177
209
|
var control: Control = node
|
|
178
|
-
return
|
|
210
|
+
return GdhScreen.rect_in_window(
|
|
179
211
|
control.get_viewport(),
|
|
180
212
|
control.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, control.size)
|
|
181
213
|
)
|
|
182
214
|
if node is Node3D:
|
|
183
215
|
var spatial: Node3D = node
|
|
184
|
-
var found: Dictionary =
|
|
216
|
+
var found: Dictionary = GdhQueries.in_frame(spatial)
|
|
185
217
|
if not found.has("rect"):
|
|
186
218
|
return "%s is not drawn by any current camera, so it covers nothing on screen" % node.get_path()
|
|
187
219
|
var covered: Rect2 = found["rect"]
|
|
188
|
-
return
|
|
220
|
+
return GdhScreen.rect_in_window(spatial.get_viewport(), covered)
|
|
189
221
|
if node is Node2D and node.has_method("get_rect"):
|
|
190
222
|
var item: Node2D = node
|
|
191
223
|
var own: Rect2 = item.call("get_rect")
|
|
192
|
-
return
|
|
224
|
+
return GdhScreen.rect_in_window(item.get_viewport(), item.get_global_transform_with_canvas() * own)
|
|
193
225
|
var no_extent: String = (
|
|
194
226
|
"%s is a %s, which has a position on screen and no extent; give region around the point "
|
|
195
227
|
+ "runtime_inspect rect answers for it"
|
|
@@ -292,7 +324,7 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
|
|
|
292
324
|
var region: Variant = asked.get("region")
|
|
293
325
|
if asked.has("node_path"):
|
|
294
326
|
var node_path: String = asked["node_path"]
|
|
295
|
-
var standing: Dictionary =
|
|
327
|
+
var standing: Dictionary = GdhValues.node_at(_host.get_tree().root, node_path)
|
|
296
328
|
if standing.has("message"):
|
|
297
329
|
return standing
|
|
298
330
|
var node: Node = standing["node"]
|
|
@@ -302,7 +334,7 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
|
|
|
302
334
|
return _refused(why)
|
|
303
335
|
region = placed
|
|
304
336
|
# A hidden node's rectangle is still a place, and what is drawn there is whatever is behind it.
|
|
305
|
-
if not
|
|
337
|
+
if not GdhQueries.shown(node):
|
|
306
338
|
described["note"] = (
|
|
307
339
|
"%s is hidden, so this is what is drawn where it would be rather than the node" % node_path
|
|
308
340
|
)
|
|
@@ -335,7 +367,7 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
|
|
|
335
367
|
else:
|
|
336
368
|
var drawn: Vector2i = Vector2i(image.get_width(), image.get_height())
|
|
337
369
|
var target: Vector2i = scaled_to(
|
|
338
|
-
drawn,
|
|
370
|
+
drawn, GdhRead.as_int(params.get("width", 0)), GdhRead.as_int(params.get("height", 0))
|
|
339
371
|
)
|
|
340
372
|
if target != drawn:
|
|
341
373
|
image.resize(target.x, target.y)
|