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.
Files changed (70) hide show
  1. package/build/cli.js +571 -363
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -19
  3. package/build/godot/addons/gdharness_editor/bridge_client.gd +9 -9
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +60 -25
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +7 -7
  6. package/build/godot/addons/gdharness_editor/plugin.gd +6 -6
  7. package/build/godot/addons/gdharness_editor/property_values.gd +124 -62
  8. package/build/godot/addons/gdharness_editor/reading.gd +28 -0
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +28 -23
  10. package/build/godot/addons/gdharness_editor/serialisation.gd +45 -94
  11. package/build/godot/addons/gdharness_editor/tool_executor.gd +24 -24
  12. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +52 -41
  13. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +4 -4
  14. package/build/godot/addons/gdharness_editor/tools/play_tools.gd +2 -2
  15. package/build/godot/addons/gdharness_editor/tools/profile_tools.gd +14 -14
  16. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +57 -50
  17. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +17 -17
  18. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +14 -12
  19. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +7 -7
  20. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +82 -78
  21. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +4 -4
  22. package/build/godot/addons/gdharness_editor/value_shapes.gd +111 -0
  23. package/build/godot/addons/gdharness_editor/value_shapes.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_runtime/reading.gd +28 -0
  25. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +18 -18
  26. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +32 -32
  27. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +50 -18
  28. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +86 -80
  29. package/build/godot/addons/gdharness_runtime/runtime_input.gd +65 -69
  30. package/build/godot/addons/gdharness_runtime/runtime_items.gd +6 -6
  31. package/build/godot/addons/gdharness_runtime/runtime_menus.gd +8 -8
  32. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +13 -21
  33. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +10 -10
  34. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +7 -7
  35. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +41 -41
  36. package/build/godot/addons/gdharness_runtime/runtime_says.gd +3 -3
  37. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +33 -33
  38. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +20 -20
  39. package/build/godot/addons/gdharness_runtime/runtime_values.gd +25 -24
  40. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +56 -50
  41. package/build/godot/addons/gdharness_runtime/serialisation.gd +45 -94
  42. package/build/godot/addons/gdharness_runtime/value_shapes.gd +111 -0
  43. package/build/godot/addons/gdharness_runtime/value_shapes.gd.uid +1 -0
  44. package/build/godot/operations/animation_reads.gd +7 -19
  45. package/build/godot/operations/audio_buses.gd +10 -10
  46. package/build/godot/operations/class_cache.gd +16 -16
  47. package/build/godot/operations/classdb_queries.gd +12 -12
  48. package/build/godot/operations/dependencies.gd +33 -29
  49. package/build/godot/operations/editor_tools.gd +12 -12
  50. package/build/godot/operations/export_presets.gd +10 -8
  51. package/build/godot/operations/file_reads.gd +60 -25
  52. package/build/godot/operations/gdscript_analysis.gd +20 -20
  53. package/build/godot/operations/gdscript_authoring.gd +17 -15
  54. package/build/godot/operations/godot_operations.gd +64 -63
  55. package/build/godot/operations/import_pipeline.gd +20 -20
  56. package/build/godot/operations/input_actions.gd +17 -17
  57. package/build/godot/operations/logger.gd +1 -1
  58. package/build/godot/operations/plugins.gd +8 -8
  59. package/build/godot/operations/project_config.gd +24 -17
  60. package/build/godot/operations/project_diagnostics.gd +9 -9
  61. package/build/godot/operations/property_values.gd +124 -62
  62. package/build/godot/operations/reading.gd +28 -0
  63. package/build/godot/operations/rename_checks.gd +3 -3
  64. package/build/godot/operations/resource_files.gd +5 -5
  65. package/build/godot/operations/script_check.gd +7 -7
  66. package/build/godot/operations/serialisation.gd +45 -94
  67. package/build/godot/operations/value_shapes.gd +111 -0
  68. package/build/keeper.js +10 -4
  69. package/package.json +1 -2
  70. 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 Read = preload("reading.gd")
13
- const Says = preload("runtime_says.gd")
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 = Read.json_or_null(text)
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: Placing = Placing.new()
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 = Read.as_int(params.get("limit", 0), 0)
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 Says.matches(str(element[field]), said):
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 Read.as_int(ProjectSettings.get_setting("accessibility/general/accessibility_support", 0), 0):
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: Placing) -> void:
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: Placing, is_window: bool) -> void:
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(Read.as_int(inner[NODE_ID], 0))
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: Placing) -> Array[Node]:
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
- Read.as_float(box.get("x", 0)),
571
- Read.as_float(box.get("y", 0)),
572
- Read.as_float(box.get("width", 0)),
573
- Read.as_float(box.get("height", 0))
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: Placing, found: Array[Node]) -> void:
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(Read.as_int(part, 0) & 0xFFFFFFFF)
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 Placing:
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 Accessibility = preload("runtime_accessibility.gd")
14
- const Capture = preload("runtime_capture.gd")
15
- const Changes = preload("runtime_changes.gd")
16
- const ErrorReport = preload("error_report.gd")
17
- const InputCommands = preload("runtime_input.gd")
18
- const Pad = preload("runtime_pad.gd")
19
- const Queries = preload("runtime_queries.gd")
20
- const Read = preload("reading.gd")
21
- const Targets = preload("runtime_targets.gd")
22
- const Typing = preload("runtime_typing.gd")
23
- const Values = preload("runtime_values.gd")
24
- const Waits = preload("runtime_waits.gd")
25
-
26
- const PROTOCOL: int = 2
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: Values = Values.new()
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: Queries = Queries.new(self, values)
92
- var _changes: Changes = Changes.new(self, values)
93
- var _input: InputCommands = InputCommands.new(self, values)
94
- var _typing: Typing = Typing.new(self)
95
- var _pad: Pad = Pad.new(self)
96
- var _capture: Capture = Capture.new(self, values)
97
- var _waits: Waits = Waits.new(self, values)
98
- var _accessibility: Accessibility = Accessibility.new(self)
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: ErrorReport
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) != Targets.HEADLESS_VIEWPORT:
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 = Read.as_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 Read.as_int(
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 = ErrorReport.new(directory.path_join(ERROR_REPORT_NAME % OS.get_process_id()))
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(Read.as_int(sent[1]))
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 Read.as_bool(ProjectSettings.get_setting(SCRIPT_RUNS_SETTING, false)):
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 = Read.as_int(ProjectSettings.get_setting(PORT_SETTING, 0))
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, older ones included.
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 Read = preload("reading.gd")
6
- const Values = preload("runtime_values.gd")
7
- const Queries = preload("runtime_queries.gd")
8
- const Screen = preload("runtime_screen.gd")
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: Values
15
+ var _values: GdhValues
16
16
 
17
17
 
18
- func _init(host: Node, values: Values) -> void:
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 = Values.node_at(_host.get_tree().root, viewport_path)
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
- if not window.visible or window.is_embedded():
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 = Read.as_int(params.get("zoom", 1), 1)
110
- var sized: bool = Read.as_int(params.get("width", 0)) > 0 or Read.as_int(params.get("height", 0)) > 0
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: Values, given: Variant) -> Variant:
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 Screen.rect_in_window(
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 = Queries.in_frame(spatial)
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 Screen.rect_in_window(spatial.get_viewport(), covered)
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 Screen.rect_in_window(item.get_viewport(), item.get_global_transform_with_canvas() * own)
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 = Values.node_at(_host.get_tree().root, node_path)
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 Queries.shown(node):
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, Read.as_int(params.get("width", 0)), Read.as_int(params.get("height", 0))
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)