gdharness 1.3.12 → 1.3.14

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 (35) hide show
  1. package/build/cli.js +32860 -31865
  2. package/build/godot/addons/gdharness_editor/bridge_client.gd +49 -0
  3. package/build/godot/addons/gdharness_editor/file_walk.gd +7 -4
  4. package/build/godot/addons/gdharness_editor/serialisation.gd +2 -0
  5. package/build/godot/addons/gdharness_editor/value_shapes.gd +39 -0
  6. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +19 -0
  7. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +63 -8
  8. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +3 -1
  9. package/build/godot/addons/gdharness_runtime/serialisation.gd +2 -0
  10. package/build/godot/addons/gdharness_runtime/value_shapes.gd +39 -0
  11. package/build/godot/addons/gdharness_tests/bench.gd +28 -0
  12. package/build/godot/addons/gdharness_tests/bench.gd.uid +1 -0
  13. package/build/godot/addons/gdharness_tests/event.gd +92 -0
  14. package/build/godot/addons/gdharness_tests/event.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_tests/share.gd +289 -0
  16. package/build/godot/addons/gdharness_tests/share.gd.uid +1 -0
  17. package/build/godot/addons/gdharness_tests/suites.gd +34 -0
  18. package/build/godot/addons/gdharness_tests/suites.gd.uid +1 -0
  19. package/build/godot/addons/gdharness_tests/suites.tscn +6 -0
  20. package/build/godot/addons/gdharness_tests/tweens.gd +24 -0
  21. package/build/godot/addons/gdharness_tests/tweens.gd.uid +1 -0
  22. package/build/godot/addons/gdharness_tests/wait.gd +53 -0
  23. package/build/godot/addons/gdharness_tests/wait.gd.uid +1 -0
  24. package/build/godot/loader/loader.gd +28 -0
  25. package/build/godot/operations/class_cache.gd +1 -1
  26. package/build/godot/operations/file_walk.gd +7 -4
  27. package/build/godot/operations/input_actions.gd +2 -2
  28. package/build/godot/operations/plugins.gd +2 -2
  29. package/build/godot/operations/project_config.gd +37 -10
  30. package/build/godot/operations/project_file.gd +112 -32
  31. package/build/godot/operations/serialisation.gd +2 -0
  32. package/build/godot/operations/value_shapes.gd +39 -0
  33. package/build/godot/tests/listed.gd +130 -19
  34. package/build/keeper.js +8410 -936
  35. package/package.json +1 -1
@@ -19,6 +19,9 @@ const DIGEST_MARKER: String = "res://addons/gdharness_editor/.gdharness-digest"
19
19
  ## step with `announcementPath` in src/bridge-announce.ts.
20
20
  const ANNOUNCEMENT: String = "res://.godot/gdharness-bridge.json"
21
21
  const ANNOUNCE_PROTOCOL: int = 1
22
+ ## Where each editor of this project says which ports it serves; see [method _ports_record].
23
+ const EDITORS_RECORDED: String = "res://.godot/gdharness-editors"
24
+ const EDITORS_RECORDED_PROTOCOL: int = 1
22
25
  const RECONNECT_DELAY: float = 3.0
23
26
  const MAX_RECONNECT_DELAY: float = 30.0
24
27
 
@@ -128,6 +131,52 @@ func _ready() -> void:
128
131
 
129
132
  set_process(true)
130
133
  _initialized = true
134
+ _record_ports()
135
+
136
+
137
+ func _exit_tree() -> void:
138
+ var record: String = _ports_record()
139
+ if FileAccess.file_exists(record):
140
+ var _removed: Error = DirAccess.remove_absolute(record)
141
+
142
+
143
+ ## The file saying which ports this editor serves, named by its process id, so `gdharness lsp` can
144
+ ## find its language server when a server moved it off the default. One for each process rather
145
+ ## than one for the project, because a gate's `--import` loads this addon too, and its plugin is
146
+ ## shown none of the options that would say so; the reader tells the two apart by command line.
147
+ ## Kept in step with `editorRecordsIn` in src/lsp-bridge.ts.
148
+ static func _ports_record() -> String:
149
+ return ProjectSettings.globalize_path(EDITORS_RECORDED).path_join("%d.json" % OS.get_process_id())
150
+
151
+
152
+ func _record_ports() -> void:
153
+ if not Engine.is_editor_hint():
154
+ return
155
+ var made: Error = DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(EDITORS_RECORDED))
156
+ if made != OK and made != ERR_ALREADY_EXISTS:
157
+ return
158
+ var file: FileAccess = FileAccess.open(_ports_record(), FileAccess.WRITE)
159
+ if file == null:
160
+ return
161
+ var written: bool = (
162
+ file
163
+ . store_string(
164
+ (
165
+ JSON
166
+ . stringify(
167
+ {
168
+ "protocol": EDITORS_RECORDED_PROTOCOL,
169
+ "pid": OS.get_process_id(),
170
+ "lsp_port": _serves(LSP_ASKED, LSP_SETTING),
171
+ "dap_port": _serves(DAP_ASKED, DAP_SETTING),
172
+ "written_at_ms": int(Time.get_unix_time_from_system() * 1000.0),
173
+ }
174
+ )
175
+ )
176
+ )
177
+ )
178
+ if not written:
179
+ push_warning("gdharness: could not write " + _ports_record())
131
180
 
132
181
 
133
182
  func _process(_delta: float) -> void:
@@ -77,10 +77,13 @@ func is_installed_by_gdharness(path: String) -> bool:
77
77
  return FileAccess.file_exists(path + ".gdharness-version")
78
78
 
79
79
 
80
- # Files under `path` whose name ends with `extension`, which is passed with its dot.
81
- func find_files(path: String, extension: String) -> Array[String]:
80
+ # Files under `path` whose name ends with `extension`, which is passed with its dot. An addon
81
+ # gdharness installed is walked into when [param with_ours] is set, for a caller rebuilding what
82
+ # the engine itself would: the engine lists the global classes of every addon, and a list rebuilt
83
+ # without ours dropped the one class the tests addon declares.
84
+ func find_files(path: String, extension: String, with_ours: bool = false) -> Array[String]:
82
85
  var files: Array[String] = []
83
- if is_stepped_over(path) or is_installed_by_gdharness(path):
86
+ if is_stepped_over(path) or (not with_ours and is_installed_by_gdharness(path)):
84
87
  return files
85
88
 
86
89
  var dir: DirAccess = DirAccess.open(path)
@@ -92,7 +95,7 @@ func find_files(path: String, extension: String) -> Array[String]:
92
95
 
93
96
  while file_name != "":
94
97
  if dir.current_is_dir() and not file_name.begins_with("."):
95
- files.append_array(find_files(path + file_name + "/", extension))
98
+ files.append_array(find_files(path + file_name + "/", extension, with_ours))
96
99
  elif file_name.ends_with(extension):
97
100
  files.append(path + file_name)
98
101
 
@@ -222,6 +222,8 @@ func deserialize_value(value: Variant) -> Variant:
222
222
  # to compile a script that gives a Variant to a typed parameter: the operations are compiled
223
223
  # under the target project's warning levels, so one such project lost every headless call.
224
224
  var tag: String = str(fields["_type"])
225
+ if GdhShapes.LIST_TAGS.has(tag):
226
+ return GdhShapes.packed_tagged(fields, fitted)
225
227
  var builder: String = BUILDERS.get(tag, "")
226
228
  if builder.is_empty() or not GdhShapes.buildable(fields, tag):
227
229
  return fields
@@ -20,6 +20,18 @@ const PACKED_ELEMENTS: Dictionary = {
20
20
  TYPE_PACKED_VECTOR4_ARRAY: TYPE_VECTOR4,
21
21
  TYPE_PACKED_COLOR_ARRAY: TYPE_COLOR,
22
22
  }
23
+ ## The tags of the packed arrays that cross as a plain list, and the type each builds.
24
+ const LIST_TAGS: Dictionary = {
25
+ "PackedInt32Array": TYPE_PACKED_INT32_ARRAY,
26
+ "PackedInt64Array": TYPE_PACKED_INT64_ARRAY,
27
+ "PackedFloat32Array": TYPE_PACKED_FLOAT32_ARRAY,
28
+ "PackedFloat64Array": TYPE_PACKED_FLOAT64_ARRAY,
29
+ "PackedStringArray": TYPE_PACKED_STRING_ARRAY,
30
+ "PackedVector2Array": TYPE_PACKED_VECTOR2_ARRAY,
31
+ "PackedVector3Array": TYPE_PACKED_VECTOR3_ARRAY,
32
+ "PackedVector4Array": TYPE_PACKED_VECTOR4_ARRAY,
33
+ "PackedColorArray": TYPE_PACKED_COLOR_ARRAY,
34
+ }
23
35
 
24
36
  # What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
25
37
  # tag somebody wrote by mistake or wrote for a different shape, and building one out of defaults
@@ -44,6 +56,15 @@ const REQUIRED: Dictionary = {
44
56
  "Color": [["r", "g", "b"]],
45
57
  "NodePath": [["path"]],
46
58
  "PackedByteArray": [["base64"]],
59
+ "PackedInt32Array": [["value"]],
60
+ "PackedInt64Array": [["value"]],
61
+ "PackedFloat32Array": [["value"]],
62
+ "PackedFloat64Array": [["value"]],
63
+ "PackedStringArray": [["value"]],
64
+ "PackedVector2Array": [["value"]],
65
+ "PackedVector3Array": [["value"]],
66
+ "PackedVector4Array": [["value"]],
67
+ "PackedColorArray": [["value"]],
47
68
  "float": [["value"]],
48
69
  "int": [["value"]],
49
70
  "RID": [["id"]],
@@ -79,6 +100,10 @@ static func written_as(type: int) -> String:
79
100
  var alternatives: Array = REQUIRED.get(tag, [])
80
101
  if alternatives.is_empty() or tag in ["float", "int", "Dictionary"]:
81
102
  return ""
103
+ # A packed list is written as the plain list wherever its type is declared, so its tag is no
104
+ # remedy there.
105
+ if PACKED_ELEMENTS.has(type) and type != TYPE_PACKED_BYTE_ARRAY:
106
+ return ""
82
107
  var keys: Array = alternatives[0]
83
108
  var optional: Array = OPTIONAL.get(tag, [])
84
109
  var parts: Array[String] = []
@@ -172,6 +197,20 @@ static func fraction(value: Variant, type: int, elements: Array = []) -> bool:
172
197
  return false
173
198
 
174
199
 
200
+ ## A packed array tagged with its type, {"_type": "PackedStringArray", "value": [...]}, for where
201
+ ## nothing declares what a plain list is to become: a setting the engine does not declare, or a
202
+ ## Variant property, takes a list as an Array. Each item is fitted as a declared list's would be,
203
+ ## and a list with an item that does not fit stays the tag it arrived as, for the caller to refuse.
204
+ static func packed_tagged(fields: Dictionary, fit: Callable) -> Variant:
205
+ var tag: String = str(fields["_type"])
206
+ if not buildable(fields, tag) or not fields["value"] is Array:
207
+ return fields
208
+ var type: int = LIST_TAGS[tag]
209
+ var items: Array = fields["value"]
210
+ var built: Variant = packed_from(items, type, fit)
211
+ return built if typeof(built) == type else fields
212
+
213
+
175
214
  ## The packed array [param type] built from [param items], each fitted by [param fit] to what the
176
215
  ## array holds, or [param items] as they are when one cannot become that, for the caller to refuse.
177
216
  ## Element by element, so a point given as its fields is read as one: converted whole, a list of
@@ -71,6 +71,10 @@ const SCRIPT_RUNS_SETTING: String = "gdharness/runtime/serve_script_runs"
71
71
  ## not served. Kept in step with `QUIET_SCENES_SETTING` in src/setup.ts.
72
72
  const QUIET_SCENES_SETTING: String = "gdharness/runtime/quiet_scenes_under"
73
73
 
74
+ ## What every user argument `gdharness spread` hands its engines starts with. Kept in step with
75
+ ## the constants of GdhShare in addons/gdharness_tests/share.gd.
76
+ const SPREAD_ARGUMENTS: String = "--gdh-share"
77
+
74
78
  ## What the engine takes a bare argument for as the scene to run, by its extension.
75
79
  const SCENE_EXTENSIONS: PackedStringArray = ["tscn", "scn", "escn", "res", "tres"]
76
80
 
@@ -489,6 +493,12 @@ func _start_server() -> void:
489
493
  _enabled = false
490
494
  return
491
495
 
496
+ # Nor an engine `gdharness spread` started, whatever folder its scene is in: its workers and the
497
+ # engine gathering their outcomes play a bench rather than the game, several at once.
498
+ if spread_engine(OS.get_cmdline_user_args()):
499
+ _enabled = false
500
+ return
501
+
492
502
  _server = TCPServer.new()
493
503
  # listen() defaults bind_address to "*", which exposes the game to the whole network.
494
504
  var bind_address: String = str(ProjectSettings.get_setting(BIND_ADDRESS_SETTING, DEFAULT_BIND_ADDRESS))
@@ -540,6 +550,15 @@ static func quiet_scene_in(given: PackedStringArray, prefixes: Variant) -> Strin
540
550
  return ""
541
551
 
542
552
 
553
+ ## Whether [param given], the engine's user arguments, are those `gdharness spread` hands its
554
+ ## engines, which the tests addon's GdhShare reads.
555
+ static func spread_engine(given: PackedStringArray) -> bool:
556
+ for argument: String in given:
557
+ if argument.begins_with(SPREAD_ARGUMENTS):
558
+ return true
559
+ return false
560
+
561
+
543
562
  ## Whether the engine was told to run a script rather than the game.
544
563
  func _script_run() -> bool:
545
564
  var given: PackedStringArray = OS.get_cmdline_args()
@@ -694,12 +694,27 @@ static func _reads_the_result(asked: Variant) -> bool:
694
694
 
695
695
  ## What [param asked] reads off the object a call answered with: every variable its script declares
696
696
  ## when [param asked] is true, or the properties a list names, each a path walked from the object.
697
+ ## Off each object of a list or a dictionary the call answered with too, in the result's shape.
697
698
  ##
698
- ## In the same answer because an object handed back is otherwise only its class and an id, and
699
- ## reading one field of it meant finding the same object again by a path into the game's private
700
- ## state, which works only where the caller already knows a key. A name that is not there is listed
701
- ## rather than refusing the answer, since the call has run by then and its result is what was asked.
699
+ ## In the same answer because an object handed back is otherwise only its class and its instance
700
+ ## id, and reading one field of it meant finding the same object again by a path into the game's
701
+ ## private state, which works only where the caller already knows a key; a catalogue of records
702
+ ## handed back as a list took one call per record. A name that is not there is listed rather than
703
+ ## refusing the answer, since the call has run by then and its result is what was asked.
702
704
  func _read_off_result(result: Variant, asked: Variant) -> Dictionary:
705
+ var missing: Array[String] = []
706
+ if result is Array:
707
+ var list: Array = result
708
+ var read_each: Array = []
709
+ for index: int in list.size():
710
+ read_each.append(_read_off_element(list[index], "[%d]" % index, asked, missing))
711
+ return _read_off_many(read_each, list, "Array", missing)
712
+ if result is Dictionary:
713
+ var map: Dictionary = result
714
+ var read_by_key: Dictionary = {}
715
+ for key: Variant in map:
716
+ read_by_key[str(key)] = _read_off_element(map[key], "[%s]" % str(key), asked, missing)
717
+ return _read_off_many(read_by_key, map.values(), "Dictionary", missing)
703
718
  if result is Object and not is_instance_valid(result):
704
719
  return {
705
720
  "properties_note": "the call answered an object that has since been freed, so nothing was read"
@@ -710,6 +725,49 @@ func _read_off_result(result: Variant, asked: Variant) -> Dictionary:
710
725
  "the call answered %s, which has no properties to read" % type_string(typeof(result))
711
726
  }
712
727
  var object: Object = result
728
+ var one: Dictionary = _read_off_object(object, asked)
729
+ missing = one["missing"]
730
+ var answered: Dictionary = {"result_properties": one["read"]}
731
+ if not missing.is_empty():
732
+ answered["result_properties_missing"] = missing
733
+ return answered
734
+
735
+
736
+ ## What was read off one element of a list or a dictionary a call answered with, or null for one
737
+ ## that is no object; why a name was not there goes into [param missing], led by [param named].
738
+ func _read_off_element(element: Variant, named: String, asked: Variant, missing: Array[String]) -> Variant:
739
+ if not element is Object:
740
+ return null
741
+ if not is_instance_valid(element):
742
+ missing.append("%s is an object that has since been freed" % named)
743
+ return null
744
+ var object: Object = element
745
+ var one: Dictionary = _read_off_object(object, asked)
746
+ var whys: Array[String] = one["missing"]
747
+ for why: String in whys:
748
+ missing.append("%s %s" % [named, why])
749
+ return one["read"]
750
+
751
+
752
+ ## The answer for a list or a dictionary, [param read] holding what each element gave, unless none
753
+ ## of [param elements] is an object.
754
+ static func _read_off_many(
755
+ read: Variant, elements: Array, shape: String, missing: Array[String]
756
+ ) -> Dictionary:
757
+ if not elements.any(func(element: Variant) -> bool: return element is Object):
758
+ return {
759
+ "properties_note":
760
+ "the call answered %s with no object in it, so there are no properties to read" % shape
761
+ }
762
+ var answered: Dictionary = {"result_properties": read}
763
+ if not missing.is_empty():
764
+ answered["result_properties_missing"] = missing
765
+ return answered
766
+
767
+
768
+ ## What [param asked] reads off [param object], under read, and why each name it could not read is
769
+ ## not there, under missing.
770
+ func _read_off_object(object: Object, asked: Variant) -> Dictionary:
713
771
  var wanted: Array[String] = []
714
772
  if asked is Array:
715
773
  for name: Variant in asked:
@@ -724,10 +782,7 @@ func _read_off_result(result: Variant, asked: Variant) -> Dictionary:
724
782
  missing.append(str(walked["message"]))
725
783
  else:
726
784
  read[path] = _values.serialize(walked["value"])
727
- var answered: Dictionary = {"result_properties": read}
728
- if not missing.is_empty():
729
- answered["result_properties_missing"] = missing
730
- return answered
785
+ return {"read": read, "missing": missing}
731
786
 
732
787
 
733
788
  ## The variables the script attached to [param object] declares, in the order it declares them.
@@ -892,10 +892,12 @@ class GdhTrace:
892
892
  elif value is Dictionary:
893
893
  var map: Dictionary = value
894
894
  _last = map.duplicate(true)
895
+ # Game time unrounded: a frame can be a tenth of a millisecond long, and a rate worked out over
896
+ # one from times cut to a ten-thousandth was off by its own size.
895
897
  var change: Dictionary = {
896
898
  "frame": frame,
897
899
  "elapsed_ms": elapsed,
898
- "game_time": snappedf(game_time, 0.0001),
900
+ "game_time": game_time,
899
901
  "value": values.serialize(value),
900
902
  }
901
903
  if changes.size() < MOST_CHANGES:
@@ -222,6 +222,8 @@ func deserialize_value(value: Variant) -> Variant:
222
222
  # to compile a script that gives a Variant to a typed parameter: the operations are compiled
223
223
  # under the target project's warning levels, so one such project lost every headless call.
224
224
  var tag: String = str(fields["_type"])
225
+ if GdhShapes.LIST_TAGS.has(tag):
226
+ return GdhShapes.packed_tagged(fields, fitted)
225
227
  var builder: String = BUILDERS.get(tag, "")
226
228
  if builder.is_empty() or not GdhShapes.buildable(fields, tag):
227
229
  return fields
@@ -20,6 +20,18 @@ const PACKED_ELEMENTS: Dictionary = {
20
20
  TYPE_PACKED_VECTOR4_ARRAY: TYPE_VECTOR4,
21
21
  TYPE_PACKED_COLOR_ARRAY: TYPE_COLOR,
22
22
  }
23
+ ## The tags of the packed arrays that cross as a plain list, and the type each builds.
24
+ const LIST_TAGS: Dictionary = {
25
+ "PackedInt32Array": TYPE_PACKED_INT32_ARRAY,
26
+ "PackedInt64Array": TYPE_PACKED_INT64_ARRAY,
27
+ "PackedFloat32Array": TYPE_PACKED_FLOAT32_ARRAY,
28
+ "PackedFloat64Array": TYPE_PACKED_FLOAT64_ARRAY,
29
+ "PackedStringArray": TYPE_PACKED_STRING_ARRAY,
30
+ "PackedVector2Array": TYPE_PACKED_VECTOR2_ARRAY,
31
+ "PackedVector3Array": TYPE_PACKED_VECTOR3_ARRAY,
32
+ "PackedVector4Array": TYPE_PACKED_VECTOR4_ARRAY,
33
+ "PackedColorArray": TYPE_PACKED_COLOR_ARRAY,
34
+ }
23
35
 
24
36
  # What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
25
37
  # tag somebody wrote by mistake or wrote for a different shape, and building one out of defaults
@@ -44,6 +56,15 @@ const REQUIRED: Dictionary = {
44
56
  "Color": [["r", "g", "b"]],
45
57
  "NodePath": [["path"]],
46
58
  "PackedByteArray": [["base64"]],
59
+ "PackedInt32Array": [["value"]],
60
+ "PackedInt64Array": [["value"]],
61
+ "PackedFloat32Array": [["value"]],
62
+ "PackedFloat64Array": [["value"]],
63
+ "PackedStringArray": [["value"]],
64
+ "PackedVector2Array": [["value"]],
65
+ "PackedVector3Array": [["value"]],
66
+ "PackedVector4Array": [["value"]],
67
+ "PackedColorArray": [["value"]],
47
68
  "float": [["value"]],
48
69
  "int": [["value"]],
49
70
  "RID": [["id"]],
@@ -79,6 +100,10 @@ static func written_as(type: int) -> String:
79
100
  var alternatives: Array = REQUIRED.get(tag, [])
80
101
  if alternatives.is_empty() or tag in ["float", "int", "Dictionary"]:
81
102
  return ""
103
+ # A packed list is written as the plain list wherever its type is declared, so its tag is no
104
+ # remedy there.
105
+ if PACKED_ELEMENTS.has(type) and type != TYPE_PACKED_BYTE_ARRAY:
106
+ return ""
82
107
  var keys: Array = alternatives[0]
83
108
  var optional: Array = OPTIONAL.get(tag, [])
84
109
  var parts: Array[String] = []
@@ -172,6 +197,20 @@ static func fraction(value: Variant, type: int, elements: Array = []) -> bool:
172
197
  return false
173
198
 
174
199
 
200
+ ## A packed array tagged with its type, {"_type": "PackedStringArray", "value": [...]}, for where
201
+ ## nothing declares what a plain list is to become: a setting the engine does not declare, or a
202
+ ## Variant property, takes a list as an Array. Each item is fitted as a declared list's would be,
203
+ ## and a list with an item that does not fit stays the tag it arrived as, for the caller to refuse.
204
+ static func packed_tagged(fields: Dictionary, fit: Callable) -> Variant:
205
+ var tag: String = str(fields["_type"])
206
+ if not buildable(fields, tag) or not fields["value"] is Array:
207
+ return fields
208
+ var type: int = LIST_TAGS[tag]
209
+ var items: Array = fields["value"]
210
+ var built: Variant = packed_from(items, type, fit)
211
+ return built if typeof(built) == type else fields
212
+
213
+
175
214
  ## The packed array [param type] built from [param items], each fitted by [param fit] to what the
176
215
  ## array holds, or [param items] as they are when one cannot become that, for the caller to refuse.
177
216
  ## Element by element, so a point given as its fields is read as one: converted whole, a list of
@@ -0,0 +1,28 @@
1
+ class_name GdhBench
2
+ extends RefCounted
3
+
4
+ ## Work played a step a frame, for the script profiler. The engine sends the profiler what each
5
+ ## frame called and holds names for only so many functions a frame, so work played inside one frame
6
+ ## comes back with most of its functions unnamed; spread over frames, every step is named.
7
+ ##
8
+ ## func _ready() -> void:
9
+ ## var timed: Dictionary = await GdhBench.each_frame(get_tree(), 400, _run.advance)
10
+ ## print("bench: %d days, %.2f ms a day" % [timed["steps"], timed["ms_each"]])
11
+ ## get_tree().quit()
12
+
13
+
14
+ ## Calls [param step] once a frame, up to [param most] times, and stops early when it returns
15
+ ## false. Answers how many steps ran under "steps", and under "ms_each" the wall-clock milliseconds a
16
+ ## step took on average, the step alone and not the frame around it.
17
+ static func each_frame(tree: SceneTree, most: int, step: Callable) -> Dictionary:
18
+ var steps: int = 0
19
+ var spent_usec: int = 0
20
+ while steps < most:
21
+ await tree.process_frame
22
+ var began: int = Time.get_ticks_usec()
23
+ var going: Variant = step.call()
24
+ spent_usec += Time.get_ticks_usec() - began
25
+ steps += 1
26
+ if going is bool and going == false:
27
+ break
28
+ return {"steps": steps, "ms_each": spent_usec / 1000.0 / maxf(1.0, float(steps))}
@@ -0,0 +1 @@
1
+ uid://dk4ffqm2xbsn7
@@ -0,0 +1,92 @@
1
+ class_name GdhEvent
2
+ extends RefCounted
3
+ ## Input events built in one line, for a case that hands them to a control or to the engine.
4
+ ##
5
+ ## A position is the point in the window, which is where `Input.parse_input_event` delivers the
6
+ ## event and what a control's own `_gui_input` reads when it is handed the event directly. The
7
+ ## global position is set to the same point, as the window's own events carry it, for code that
8
+ ## reads that field instead.
9
+
10
+
11
+ ## [param button] pressed at [param at], or released when [param pressed] is false.
12
+ static func click(
13
+ at: Vector2, button: MouseButton = MOUSE_BUTTON_LEFT, pressed: bool = true
14
+ ) -> InputEventMouseButton:
15
+ var made: InputEventMouseButton = InputEventMouseButton.new()
16
+ made.button_index = button
17
+ made.pressed = pressed
18
+ made.position = at
19
+ made.global_position = at
20
+ if pressed:
21
+ made.button_mask = _mask_of(button)
22
+ return made
23
+
24
+
25
+ ## The release of [param button] at [param at], which ends the press [method click] began.
26
+ static func release(at: Vector2, button: MouseButton = MOUSE_BUTTON_LEFT) -> InputEventMouseButton:
27
+ return click(at, button, false)
28
+
29
+
30
+ ## One turn of the wheel at [param at], up or down. The engine sends a wheel turn as a press.
31
+ static func wheel(at: Vector2, up: bool = true) -> InputEventMouseButton:
32
+ return click(at, MOUSE_BUTTON_WHEEL_UP if up else MOUSE_BUTTON_WHEEL_DOWN)
33
+
34
+
35
+ ## The mouse moved to [param at] by [param moved], holding [param held], a mask of buttons such
36
+ ## as MOUSE_BUTTON_MASK_LEFT for a drag.
37
+ static func motion(at: Vector2, moved: Vector2 = Vector2.ZERO, held: int = 0) -> InputEventMouseMotion:
38
+ var made: InputEventMouseMotion = InputEventMouseMotion.new()
39
+ made.position = at
40
+ made.global_position = at
41
+ made.relative = moved
42
+ made.button_mask = held
43
+ return made
44
+
45
+
46
+ ## [param key_pressed] pressed, or released when [param pressed] is false. The key is set as both the
47
+ ## keycode and the physical keycode, so a binding to either matches it.
48
+ static func key(key_pressed: Key, pressed: bool = true) -> InputEventKey:
49
+ var made: InputEventKey = InputEventKey.new()
50
+ made.keycode = key_pressed
51
+ made.physical_keycode = key_pressed
52
+ made.pressed = pressed
53
+ return made
54
+
55
+
56
+ ## [param button] of the pad [param device] pressed, or released when [param pressed] is false.
57
+ static func pad(button: JoyButton, pressed: bool = true, device: int = 0) -> InputEventJoypadButton:
58
+ var made: InputEventJoypadButton = InputEventJoypadButton.new()
59
+ made.button_index = button
60
+ made.pressed = pressed
61
+ made.pressure = 1.0 if pressed else 0.0
62
+ made.device = device
63
+ return made
64
+
65
+
66
+ ## [param axis_moved] of the pad [param device] at [param value], from -1 to 1.
67
+ static func axis(axis_moved: JoyAxis, value: float, device: int = 0) -> InputEventJoypadMotion:
68
+ var made: InputEventJoypadMotion = InputEventJoypadMotion.new()
69
+ made.axis = axis_moved
70
+ made.axis_value = value
71
+ made.device = device
72
+ return made
73
+
74
+
75
+ ## The action [param action_named] pressed at [param strength], or released when [param pressed] is false.
76
+ static func action(action_named: StringName, pressed: bool = true, strength: float = 1.0) -> InputEventAction:
77
+ var made: InputEventAction = InputEventAction.new()
78
+ made.action = action_named
79
+ made.pressed = pressed
80
+ made.strength = strength if pressed else 0.0
81
+ return made
82
+
83
+
84
+ static func _mask_of(button: MouseButton) -> int:
85
+ match button:
86
+ MOUSE_BUTTON_LEFT:
87
+ return MOUSE_BUTTON_MASK_LEFT
88
+ MOUSE_BUTTON_RIGHT:
89
+ return MOUSE_BUTTON_MASK_RIGHT
90
+ MOUSE_BUTTON_MIDDLE:
91
+ return MOUSE_BUTTON_MASK_MIDDLE
92
+ return 0
@@ -0,0 +1 @@
1
+ uid://cmwm44sgxytkj