gdharness 1.2.15 → 1.2.16
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 +346 -76
- package/build/godot/addons/gdharness_editor/property_values.gd +28 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +27 -8
- package/build/godot/addons/gdharness_editor/serialisation.gd +20 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +172 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +60 -47
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +46 -4
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +13 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +1 -17
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +29 -3
- package/build/godot/addons/gdharness_runtime/serialisation.gd +20 -0
- package/build/godot/operations/audio_buses.gd +35 -0
- package/build/godot/operations/classdb_queries.gd +31 -2
- package/build/godot/operations/file_walk.gd +9 -2
- package/build/godot/operations/gdscript_authoring.gd +4 -5
- package/build/godot/operations/godot_operations.gd +4 -0
- package/build/godot/operations/input_actions.gd +27 -0
- package/build/godot/operations/project_config.gd +8 -1
- package/build/godot/operations/project_diagnostics.gd +9 -5
- package/build/godot/operations/serialisation.gd +20 -0
- package/build/index.js +333 -63
- package/package.json +1 -1
|
@@ -17,10 +17,6 @@ const Targets = preload("runtime_targets.gd")
|
|
|
17
17
|
## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
|
|
18
18
|
const TO_SMALL: int = 32
|
|
19
19
|
|
|
20
|
-
## What a game started without a window gets whatever the project settings say, and the usual
|
|
21
|
-
## reason a control is out of reach. Only worth telling somebody when it is what they have.
|
|
22
|
-
const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
23
|
-
|
|
24
20
|
## Set, to the desktop's name, for a process gdharness started on a desktop of its own and for
|
|
25
21
|
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
26
22
|
const DESKTOP_VARIABLE: String = "GDHARNESS_DESKTOP"
|
|
@@ -654,20 +650,8 @@ func _out_of_reach(
|
|
|
654
650
|
)
|
|
655
651
|
|
|
656
652
|
|
|
657
|
-
## What to add to a refusal about a point outside the viewport, when the reason is that nobody
|
|
658
|
-
## gave this game a window. The rect on its own does not say it, and it is the usual reason.
|
|
659
|
-
##
|
|
660
|
-
## The size is only claimed where it is true: the rect is printed beside this, so naming 64 by 64
|
|
661
|
-
## over a viewport somebody has resized says two different things in one sentence.
|
|
662
653
|
func _no_window_note(viewport: Viewport) -> String:
|
|
663
|
-
|
|
664
|
-
return ""
|
|
665
|
-
if viewport.get_visible_rect().size == HEADLESS_VIEWPORT:
|
|
666
|
-
return (
|
|
667
|
-
". This game has no window, and a game with no window has a 64 by 64 viewport whatever "
|
|
668
|
-
+ "the project settings say: run it with a window to reach this control"
|
|
669
|
-
)
|
|
670
|
-
return ". This game has no window: run it with a window to reach this control"
|
|
654
|
+
return Targets.no_window_note(viewport, _host.get_tree().root)
|
|
671
655
|
|
|
672
656
|
|
|
673
657
|
## Sends a click's press or release into [param viewport], through Input when that is the game's
|
|
@@ -12,6 +12,10 @@ const Words = preload("runtime_words.gd")
|
|
|
12
12
|
## How many of the controls saying the same words a refusal names, with the index for each.
|
|
13
13
|
const LISTED_CANDIDATES: int = 10
|
|
14
14
|
|
|
15
|
+
## What a game started without a window gets whatever the project settings say, and the usual
|
|
16
|
+
## reason a control is out of reach. Only worth telling somebody when it is what they have.
|
|
17
|
+
const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
18
|
+
|
|
15
19
|
## What a match's rank is made of, one bit each, the highest deciding first.
|
|
16
20
|
const TAKES_POINTER: int = 4
|
|
17
21
|
const ENABLED_BUTTON: int = 2
|
|
@@ -139,7 +143,7 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
139
143
|
under,
|
|
140
144
|
wanted,
|
|
141
145
|
_hidden_note(hidden),
|
|
142
|
-
_outside_note(outside),
|
|
146
|
+
_outside_note(outside, root),
|
|
143
147
|
_covered_note(covered, cover),
|
|
144
148
|
_open_menu_note(root, wanted),
|
|
145
149
|
]
|
|
@@ -393,10 +397,32 @@ static func _hidden_note(hidden: int) -> String:
|
|
|
393
397
|
|
|
394
398
|
## What to add to a refusal that found nothing on screen, when words matched controls a click cannot
|
|
395
399
|
## reach: off the viewport, or clipped away by a container it does not scroll.
|
|
396
|
-
static func _outside_note(outside: int) -> String:
|
|
400
|
+
static func _outside_note(outside: int, root: Node) -> String:
|
|
397
401
|
if outside == 0:
|
|
398
402
|
return ""
|
|
399
|
-
|
|
403
|
+
var note: String = (
|
|
404
|
+
"; %d control%s it outside what the screen shows" % [outside, " says" if outside == 1 else "s say"]
|
|
405
|
+
)
|
|
406
|
+
var window: Window = root as Window
|
|
407
|
+
return note if window == null else note + no_window_note(window, window)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
## What to add to a refusal about a point outside the viewport, when the reason is that nobody
|
|
411
|
+
## gave this game a window. The rect on its own does not say it, and it is the usual reason: a
|
|
412
|
+
## headless game's button twenty pixels in from the corner was refused as off screen, and the
|
|
413
|
+
## refusal read as though the button were not there.
|
|
414
|
+
##
|
|
415
|
+
## The size is only claimed where it is true: the rect is printed beside this, so naming 64 by 64
|
|
416
|
+
## over a viewport somebody has resized says two different things in one sentence.
|
|
417
|
+
static func no_window_note(viewport: Viewport, root: Window) -> String:
|
|
418
|
+
if viewport != root or root.can_draw():
|
|
419
|
+
return ""
|
|
420
|
+
if viewport.get_visible_rect().size == HEADLESS_VIEWPORT:
|
|
421
|
+
return (
|
|
422
|
+
". This game has no window, and a game with no window has a 64 by 64 viewport whatever "
|
|
423
|
+
+ "the project settings say: run it with a window to reach this control"
|
|
424
|
+
)
|
|
425
|
+
return ". This game has no window: run it with a window to reach this control"
|
|
400
426
|
|
|
401
427
|
|
|
402
428
|
## What to add to a refusal that found nothing on screen, when the controls saying it are covered.
|
|
@@ -540,6 +540,9 @@ func _serialize_colors(value: PackedColorArray) -> Array:
|
|
|
540
540
|
|
|
541
541
|
# A pathless Resource says so rather than inventing an empty path for the caller to load.
|
|
542
542
|
func _serialize_object(value: Object) -> Variant:
|
|
543
|
+
if value is InputEvent:
|
|
544
|
+
var event: InputEvent = value
|
|
545
|
+
return _describe_input_event(event)
|
|
543
546
|
if value is Resource:
|
|
544
547
|
var resource: Resource = value
|
|
545
548
|
if resource.resource_path.is_empty():
|
|
@@ -552,6 +555,23 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
552
555
|
return _describe_object(value)
|
|
553
556
|
|
|
554
557
|
|
|
558
|
+
## An input event, by what it is bound to: the engine's own words for it and every field it stores
|
|
559
|
+
## away from its default. As a bare resource it was its class and nothing else, so an input action
|
|
560
|
+
## read back from project.godot could not say whether it was bound to Space or to a joypad button.
|
|
561
|
+
func _describe_input_event(event: InputEvent) -> Dictionary:
|
|
562
|
+
var described: Dictionary = {"class": event.get_class(), "_type": "Resource", "text": event.as_text()}
|
|
563
|
+
for property: Dictionary in event.get_property_list():
|
|
564
|
+
var usage: int = property.get("usage", 0)
|
|
565
|
+
var name: String = str(property.get("name", ""))
|
|
566
|
+
if not (usage & PROPERTY_USAGE_STORAGE) or name.begins_with("resource_") or name == "script":
|
|
567
|
+
continue
|
|
568
|
+
var held: Variant = event.get(name)
|
|
569
|
+
if held == ClassDB.class_get_property_default_value(event.get_class(), name):
|
|
570
|
+
continue
|
|
571
|
+
described[name] = serialize_value(held)
|
|
572
|
+
return described
|
|
573
|
+
|
|
574
|
+
|
|
555
575
|
## A plain object, said in enough detail to tell it from the one beside it.
|
|
556
576
|
##
|
|
557
577
|
## `{"class": "RefCounted"}` was all of it, so a roster of twelve people came back as the word
|
|
@@ -44,6 +44,41 @@ func create_audio_bus(params: Dictionary) -> Dictionary:
|
|
|
44
44
|
return {"bus": _bus(new_idx), "buses": buses, "layout": layout}
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
func remove_audio_bus(params: Dictionary) -> Dictionary:
|
|
48
|
+
var bus_name: String = str(params.get("bus_name", ""))
|
|
49
|
+
if bus_name.is_empty():
|
|
50
|
+
return _log.failure("bus_name is required")
|
|
51
|
+
var index: int = AudioServer.get_bus_index(bus_name)
|
|
52
|
+
if index == -1:
|
|
53
|
+
var names: Array[String] = []
|
|
54
|
+
for i: int in range(AudioServer.bus_count):
|
|
55
|
+
names.append(AudioServer.get_bus_name(i))
|
|
56
|
+
return _log.failure("No bus named %s. The buses are %s." % [bus_name, ", ".join(names)])
|
|
57
|
+
if index == 0:
|
|
58
|
+
return _log.failure("%s is the bus every other ends in, so it cannot be removed." % bus_name)
|
|
59
|
+
|
|
60
|
+
# A bus sending to this one would be left sending to a name nothing has, which the engine reads
|
|
61
|
+
# as Master; sent on to where this one went instead, the chain it was part of keeps its shape.
|
|
62
|
+
var onward: String = AudioServer.get_bus_send(index)
|
|
63
|
+
var resent: Array[String] = []
|
|
64
|
+
for i: int in range(AudioServer.bus_count):
|
|
65
|
+
if i != index and AudioServer.get_bus_send(i) == bus_name:
|
|
66
|
+
AudioServer.set_bus_send(i, onward)
|
|
67
|
+
resent.append(AudioServer.get_bus_name(i))
|
|
68
|
+
AudioServer.remove_bus(index)
|
|
69
|
+
|
|
70
|
+
var layout: String = _save_layout()
|
|
71
|
+
if layout.is_empty():
|
|
72
|
+
return {}
|
|
73
|
+
var buses: Array[Dictionary] = []
|
|
74
|
+
for i: int in range(AudioServer.bus_count):
|
|
75
|
+
buses.append(_bus(i))
|
|
76
|
+
var answer: Dictionary = {"removed": bus_name, "buses": buses, "layout": layout}
|
|
77
|
+
if not resent.is_empty():
|
|
78
|
+
answer["resent"] = {"buses": resent, "to": onward}
|
|
79
|
+
return answer
|
|
80
|
+
|
|
81
|
+
|
|
47
82
|
func get_audio_buses(_params: Dictionary) -> Dictionary:
|
|
48
83
|
var buses: Array[Dictionary] = []
|
|
49
84
|
for i: int in range(AudioServer.bus_count):
|
|
@@ -101,7 +101,7 @@ func query_class_info(params: Dictionary) -> Dictionary:
|
|
|
101
101
|
)
|
|
102
102
|
|
|
103
103
|
if not ClassDB.class_exists(class_name_str):
|
|
104
|
-
return
|
|
104
|
+
return _not_found(class_name_str)
|
|
105
105
|
|
|
106
106
|
var member: String = str(params.get("member", ""))
|
|
107
107
|
if not member.is_empty():
|
|
@@ -152,6 +152,35 @@ func query_class_info(params: Dictionary) -> Dictionary:
|
|
|
152
152
|
# is as often an ancestor's as the class's own, so the ancestry is walked without being asked. Each
|
|
153
153
|
# match says which class declares it. Nothing found names the members whose names contain the one
|
|
154
154
|
# asked for, since the usual miss is a spelling.
|
|
155
|
+
## Why [param wanted] has no answer here. A class_name a project script declares is not in ClassDB,
|
|
156
|
+
## and "Class not found" for one read as though the project had no such class.
|
|
157
|
+
func _not_found(wanted: String) -> Dictionary:
|
|
158
|
+
for entry: Dictionary in ProjectSettings.get_global_class_list():
|
|
159
|
+
if str(entry.get("class", "")) == wanted:
|
|
160
|
+
return (
|
|
161
|
+
_log
|
|
162
|
+
. failure(
|
|
163
|
+
(
|
|
164
|
+
(
|
|
165
|
+
"%s is not an engine class: it is the class_name %s declares, and editor_classes reads "
|
|
166
|
+
+ "the engine's ClassDB only. script_info structure on that script, with "
|
|
167
|
+
+ "includeInherited, answers what it declares and what it inherits."
|
|
168
|
+
)
|
|
169
|
+
% [wanted, str(entry.get("path", ""))]
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
return _log.failure(
|
|
174
|
+
(
|
|
175
|
+
(
|
|
176
|
+
"Class not found: %s. editor_classes reads the engine's ClassDB, so a class_name a project "
|
|
177
|
+
+ "script declares is not in it either; script_info structure reads one."
|
|
178
|
+
)
|
|
179
|
+
% wanted
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
155
184
|
func _member_of(class_name_str: String, member: String) -> Dictionary:
|
|
156
185
|
var found: Array[Dictionary] = []
|
|
157
186
|
var current: String = class_name_str
|
|
@@ -223,7 +252,7 @@ func inspect_inheritance(params: Dictionary) -> Dictionary:
|
|
|
223
252
|
_log.info("Inspecting inheritance for: " + class_name_str)
|
|
224
253
|
|
|
225
254
|
if not ClassDB.class_exists(class_name_str):
|
|
226
|
-
return
|
|
255
|
+
return _not_found(class_name_str)
|
|
227
256
|
|
|
228
257
|
var ancestors: Array[String] = []
|
|
229
258
|
var current: String = class_name_str
|
|
@@ -35,10 +35,17 @@ func is_stepped_over(path: String) -> bool:
|
|
|
35
35
|
return FileAccess.file_exists(path + ".gdignore")
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
# An addon gdharness installed, told by the marker setup writes into it. The engine loads it, but
|
|
39
|
+
# nobody on the project wrote it: the health check counted its thirty-odd scripts as the project's,
|
|
40
|
+
# 39 beside the 5 project_info's own count gave, which the server's walks already step over.
|
|
41
|
+
func is_installed_by_gdharness(path: String) -> bool:
|
|
42
|
+
return FileAccess.file_exists(path + ".gdharness-version")
|
|
43
|
+
|
|
44
|
+
|
|
38
45
|
# Files under `path` whose name ends with `extension`, which is passed with its dot.
|
|
39
46
|
func find_files(path: String, extension: String) -> Array[String]:
|
|
40
47
|
var files: Array[String] = []
|
|
41
|
-
if is_stepped_over(path):
|
|
48
|
+
if is_stepped_over(path) or is_installed_by_gdharness(path):
|
|
42
49
|
return files
|
|
43
50
|
|
|
44
51
|
var dir: DirAccess = DirAccess.open(path)
|
|
@@ -62,7 +69,7 @@ func find_files(path: String, extension: String) -> Array[String]:
|
|
|
62
69
|
# Files under `path` whose extension is in `extensions`, which are passed without their dot.
|
|
63
70
|
func find_files_with_extensions(path: String, extensions: Array) -> Array[String]:
|
|
64
71
|
var files: Array[String] = []
|
|
65
|
-
if is_stepped_over(path):
|
|
72
|
+
if is_stepped_over(path) or is_installed_by_gdharness(path):
|
|
66
73
|
return files
|
|
67
74
|
|
|
68
75
|
var dir: DirAccess = DirAccess.open(path)
|
|
@@ -63,12 +63,11 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
63
63
|
if content.is_empty():
|
|
64
64
|
if not cls_name_param.is_empty():
|
|
65
65
|
script_content += "class_name " + cls_name_param + "\n"
|
|
66
|
-
script_content += "extends " + extends_class + "\n
|
|
66
|
+
script_content += "extends " + extends_class + "\n"
|
|
67
|
+
# Only what was asked for: with no template the file was given an empty _ready beside a
|
|
68
|
+
# template_used of "none", which a caller had then to delete or leave lying in the script.
|
|
67
69
|
if not template.is_empty():
|
|
68
|
-
script_content += _script_template(template)
|
|
69
|
-
else:
|
|
70
|
-
script_content += "func _ready() -> void:\n"
|
|
71
|
-
script_content += "\tpass\n"
|
|
70
|
+
script_content += "\n" + _script_template(template)
|
|
72
71
|
|
|
73
72
|
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.WRITE)
|
|
74
73
|
if not file:
|
|
@@ -186,10 +186,14 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
186
186
|
payload = Plugins.new(_log).disable_plugin(params)
|
|
187
187
|
"add_input_action":
|
|
188
188
|
payload = InputActions.new(_log).add_input_action(params)
|
|
189
|
+
"remove_input_action":
|
|
190
|
+
payload = InputActions.new(_log).remove_input_action(params)
|
|
189
191
|
|
|
190
192
|
# Audio
|
|
191
193
|
"create_audio_bus":
|
|
192
194
|
payload = AudioBuses.new(_log).create_audio_bus(params)
|
|
195
|
+
"remove_audio_bus":
|
|
196
|
+
payload = AudioBuses.new(_log).remove_audio_bus(params)
|
|
193
197
|
"get_audio_buses":
|
|
194
198
|
payload = AudioBuses.new(_log).get_audio_buses(params)
|
|
195
199
|
"set_audio_bus_effect":
|
|
@@ -184,6 +184,33 @@ func add_input_action(params: Dictionary) -> Dictionary:
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
# Remove an input action the project defines, which add_input_action had no way back from.
|
|
188
|
+
func remove_input_action(params: Dictionary) -> Dictionary:
|
|
189
|
+
var action_name: String = str(params.get("action_name", ""))
|
|
190
|
+
if action_name.is_empty():
|
|
191
|
+
return _log.failure("action_name is required")
|
|
192
|
+
var setting: String = "input/" + action_name
|
|
193
|
+
if not ProjectSettings.has_setting(setting):
|
|
194
|
+
return _log.failure("This project has no input action named " + action_name)
|
|
195
|
+
# The engine registers its own ui_ actions with a default, so clearing one from project.godot
|
|
196
|
+
# puts the engine's binding back rather than removing the action.
|
|
197
|
+
if ProjectSettings.property_get_revert(setting) != null:
|
|
198
|
+
return _log.failure(
|
|
199
|
+
(
|
|
200
|
+
(
|
|
201
|
+
"%s is one of the engine's own actions: removing it from project.godot restores the "
|
|
202
|
+
+ "engine's binding rather than removing it. Rebind it with add_input_action instead."
|
|
203
|
+
)
|
|
204
|
+
% action_name
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
ProjectSettings.set_setting(setting, null)
|
|
208
|
+
var err: Error = ProjectSettings.save()
|
|
209
|
+
if err != OK:
|
|
210
|
+
return _log.failure("Failed to save project.godot: " + error_string(err))
|
|
211
|
+
return {"action_name": action_name, "removed": true}
|
|
212
|
+
|
|
213
|
+
|
|
187
214
|
# The value project.godot stores for one input action.
|
|
188
215
|
#
|
|
189
216
|
# It has to be a Dictionary holding real InputEvent objects. The engine writes those as the
|
|
@@ -29,7 +29,14 @@ func get_project_setting(params: Dictionary) -> Dictionary:
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
if result["exists"]:
|
|
32
|
-
|
|
32
|
+
var value: Variant = ProjectSettings.get_setting(setting_path)
|
|
33
|
+
result["value"] = _values.serialize_value(value)
|
|
34
|
+
# A setting the engine registers exists whether or not the project sets it, so exists did
|
|
35
|
+
# not say whether project.godot has a line for it. At its default it has none: the engine
|
|
36
|
+
# drops such a key when it saves.
|
|
37
|
+
var default_value: Variant = ProjectSettings.property_get_revert(setting_path)
|
|
38
|
+
if default_value != null:
|
|
39
|
+
result["at_default"] = value == default_value
|
|
33
40
|
else:
|
|
34
41
|
result["value"] = null
|
|
35
42
|
result["message"] = "Setting does not exist"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
extends RefCounted
|
|
2
2
|
|
|
3
3
|
const FileWalk = preload("file_walk.gd")
|
|
4
|
+
const ImportPipeline = preload("import_pipeline.gd")
|
|
4
5
|
const Log = preload("logger.gd")
|
|
5
6
|
|
|
6
7
|
var _log: Log
|
|
@@ -40,10 +41,11 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
40
41
|
if "resources" in check_categories:
|
|
41
42
|
var details: Array[String] = []
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
# Every format the engine imports, the list the import status reads: five of them were checked
|
|
45
|
+
# here, so a project of glTF models, fonts and WebP textures had nothing to reimport.
|
|
46
|
+
var resources: Array[String] = _files.find_files_with_extensions(
|
|
47
|
+
"res://", ImportPipeline.IMPORTABLE_EXTENSIONS
|
|
48
|
+
)
|
|
47
49
|
|
|
48
50
|
var missing_imports: int = 0
|
|
49
51
|
for res: String in resources:
|
|
@@ -58,7 +60,9 @@ func get_project_health(params: Dictionary) -> Dictionary:
|
|
|
58
60
|
"name": "Resources",
|
|
59
61
|
"passed": details.is_empty(),
|
|
60
62
|
"details": details,
|
|
61
|
-
|
|
63
|
+
# What the check read: files the engine imports. A .tres is a resource and was counted
|
|
64
|
+
# under this name as none, beside a project holding one.
|
|
65
|
+
"importable_files": resources.size()
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
# 3. Scripts Check
|
|
@@ -540,6 +540,9 @@ func _serialize_colors(value: PackedColorArray) -> Array:
|
|
|
540
540
|
|
|
541
541
|
# A pathless Resource says so rather than inventing an empty path for the caller to load.
|
|
542
542
|
func _serialize_object(value: Object) -> Variant:
|
|
543
|
+
if value is InputEvent:
|
|
544
|
+
var event: InputEvent = value
|
|
545
|
+
return _describe_input_event(event)
|
|
543
546
|
if value is Resource:
|
|
544
547
|
var resource: Resource = value
|
|
545
548
|
if resource.resource_path.is_empty():
|
|
@@ -552,6 +555,23 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
552
555
|
return _describe_object(value)
|
|
553
556
|
|
|
554
557
|
|
|
558
|
+
## An input event, by what it is bound to: the engine's own words for it and every field it stores
|
|
559
|
+
## away from its default. As a bare resource it was its class and nothing else, so an input action
|
|
560
|
+
## read back from project.godot could not say whether it was bound to Space or to a joypad button.
|
|
561
|
+
func _describe_input_event(event: InputEvent) -> Dictionary:
|
|
562
|
+
var described: Dictionary = {"class": event.get_class(), "_type": "Resource", "text": event.as_text()}
|
|
563
|
+
for property: Dictionary in event.get_property_list():
|
|
564
|
+
var usage: int = property.get("usage", 0)
|
|
565
|
+
var name: String = str(property.get("name", ""))
|
|
566
|
+
if not (usage & PROPERTY_USAGE_STORAGE) or name.begins_with("resource_") or name == "script":
|
|
567
|
+
continue
|
|
568
|
+
var held: Variant = event.get(name)
|
|
569
|
+
if held == ClassDB.class_get_property_default_value(event.get_class(), name):
|
|
570
|
+
continue
|
|
571
|
+
described[name] = serialize_value(held)
|
|
572
|
+
return described
|
|
573
|
+
|
|
574
|
+
|
|
555
575
|
## A plain object, said in enough detail to tell it from the one beside it.
|
|
556
576
|
##
|
|
557
577
|
## `{"class": "RefCounted"}` was all of it, so a roster of twelve people came back as the word
|