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.
- package/build/cli.js +32860 -31865
- package/build/godot/addons/gdharness_editor/bridge_client.gd +49 -0
- package/build/godot/addons/gdharness_editor/file_walk.gd +7 -4
- package/build/godot/addons/gdharness_editor/serialisation.gd +2 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +39 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +19 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +63 -8
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +3 -1
- package/build/godot/addons/gdharness_runtime/serialisation.gd +2 -0
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +39 -0
- package/build/godot/addons/gdharness_tests/bench.gd +28 -0
- package/build/godot/addons/gdharness_tests/bench.gd.uid +1 -0
- package/build/godot/addons/gdharness_tests/event.gd +92 -0
- package/build/godot/addons/gdharness_tests/event.gd.uid +1 -0
- package/build/godot/addons/gdharness_tests/share.gd +289 -0
- package/build/godot/addons/gdharness_tests/share.gd.uid +1 -0
- package/build/godot/addons/gdharness_tests/suites.gd +34 -0
- package/build/godot/addons/gdharness_tests/suites.gd.uid +1 -0
- package/build/godot/addons/gdharness_tests/suites.tscn +6 -0
- package/build/godot/addons/gdharness_tests/tweens.gd +24 -0
- package/build/godot/addons/gdharness_tests/tweens.gd.uid +1 -0
- package/build/godot/addons/gdharness_tests/wait.gd +53 -0
- package/build/godot/addons/gdharness_tests/wait.gd.uid +1 -0
- package/build/godot/loader/loader.gd +28 -0
- package/build/godot/operations/class_cache.gd +1 -1
- package/build/godot/operations/file_walk.gd +7 -4
- package/build/godot/operations/input_actions.gd +2 -2
- package/build/godot/operations/plugins.gd +2 -2
- package/build/godot/operations/project_config.gd +37 -10
- package/build/godot/operations/project_file.gd +112 -32
- package/build/godot/operations/serialisation.gd +2 -0
- package/build/godot/operations/value_shapes.gd +39 -0
- package/build/godot/tests/listed.gd +130 -19
- package/build/keeper.js +8410 -936
- 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
|
-
|
|
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
|
|
699
|
-
## reading one field of it meant finding the same object again by a path into the game's
|
|
700
|
-
## state, which works only where the caller already knows a key
|
|
701
|
-
##
|
|
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
|
-
|
|
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":
|
|
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
|