gdharness 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +125 -0
- package/build/cli.js +30353 -0
- package/build/godot/addons/auto_reload/auto_reload.gd +89 -0
- package/build/godot/addons/auto_reload/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/bridge_client.gd +197 -0
- package/build/godot/addons/gdharness_editor/plugin.cfg +6 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +88 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +104 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +397 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +85 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +427 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +885 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +292 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +77 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +254 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +283 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +169 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +119 -0
- package/build/godot/operations/audio_buses.gd +125 -0
- package/build/godot/operations/class_cache.gd +180 -0
- package/build/godot/operations/classdb_queries.gd +252 -0
- package/build/godot/operations/dependencies.gd +293 -0
- package/build/godot/operations/file_walk.gd +55 -0
- package/build/godot/operations/gdscript_analysis.gd +288 -0
- package/build/godot/operations/gdscript_authoring.gd +419 -0
- package/build/godot/operations/godot_operations.gd +186 -0
- package/build/godot/operations/import_pipeline.gd +390 -0
- package/build/godot/operations/input_actions.gd +237 -0
- package/build/godot/operations/logger.gd +33 -0
- package/build/godot/operations/plugins.gd +159 -0
- package/build/godot/operations/project_config.gd +153 -0
- package/build/godot/operations/project_diagnostics.gd +159 -0
- package/build/godot/operations/resource_files.gd +132 -0
- package/build/godot/operations/serialisation.gd +154 -0
- package/build/index.js +29252 -0
- package/package.json +57 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## What the server asks about the running tree: its shape, one node, the nodes matching a
|
|
4
|
+
## question, where one is on screen, a property set, a method called, the metrics.
|
|
5
|
+
|
|
6
|
+
const Values = preload("runtime_values.gd")
|
|
7
|
+
|
|
8
|
+
## The most nodes one find answers with, unless asked for fewer: enough for any real query and
|
|
9
|
+
## far short of the tree dump a query exists to avoid.
|
|
10
|
+
const FIND_LIMIT: int = 100
|
|
11
|
+
const FIND_LIMIT_CEILING: int = 1000
|
|
12
|
+
|
|
13
|
+
var _host: Node
|
|
14
|
+
var _values: Values
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## The host is the autoload, which is how the tree is reached: it is not in one when the
|
|
18
|
+
## modules are built, and a fixture may put it in one later.
|
|
19
|
+
func _init(host: Node, values: Values) -> void:
|
|
20
|
+
_host = host
|
|
21
|
+
_values = values
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
func get_tree(params: Dictionary) -> Dictionary:
|
|
25
|
+
var root_path: String = str(params.get("root", "/root"))
|
|
26
|
+
var max_depth: int = int(params.get("depth", 3))
|
|
27
|
+
var include_properties: bool = bool(params.get("include_properties", false))
|
|
28
|
+
|
|
29
|
+
var root: Node = _host.get_tree().root.get_node_or_null(root_path)
|
|
30
|
+
if root == null:
|
|
31
|
+
return {"type": "error", "message": "Node not found: " + root_path}
|
|
32
|
+
|
|
33
|
+
return {"type": "tree", "root": _serialize_node_tree(root, 0, max_depth, include_properties)}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Nodes matching every filter given, as paths, so a caller can name what it wants without
|
|
37
|
+
## reading the whole tree to find it. `class` matches native classes and their subclasses, and
|
|
38
|
+
## the global name of a script class; `name` is a case-insensitive glob; `script` is a path.
|
|
39
|
+
func find_nodes(params: Dictionary) -> Dictionary:
|
|
40
|
+
var root_path: String = str(params.get("root", "/root"))
|
|
41
|
+
var wanted_class: String = str(params.get("class", ""))
|
|
42
|
+
var wanted_script: String = str(params.get("script", ""))
|
|
43
|
+
var wanted_name: String = str(params.get("name", ""))
|
|
44
|
+
var wanted_group: String = str(params.get("group", ""))
|
|
45
|
+
var limit: int = clampi(int(params.get("limit", FIND_LIMIT)), 1, FIND_LIMIT_CEILING)
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
wanted_class.is_empty()
|
|
49
|
+
and wanted_script.is_empty()
|
|
50
|
+
and wanted_name.is_empty()
|
|
51
|
+
and wanted_group.is_empty()
|
|
52
|
+
):
|
|
53
|
+
return {"type": "error", "message": "find_nodes needs at least one of class, script, name, group"}
|
|
54
|
+
if not wanted_script.is_empty() and not wanted_script.begins_with("res://"):
|
|
55
|
+
wanted_script = "res://" + wanted_script
|
|
56
|
+
|
|
57
|
+
var root: Node = _host.get_tree().root.get_node_or_null(root_path)
|
|
58
|
+
if root == null:
|
|
59
|
+
return {"type": "error", "message": "Node not found: " + root_path}
|
|
60
|
+
|
|
61
|
+
var found: Array[Dictionary] = []
|
|
62
|
+
var pending: Array[Node] = [root]
|
|
63
|
+
var truncated: bool = false
|
|
64
|
+
while not pending.is_empty():
|
|
65
|
+
var node: Node = pending.pop_front()
|
|
66
|
+
if _matches(node, wanted_class, wanted_script, wanted_name, wanted_group):
|
|
67
|
+
if found.size() >= limit:
|
|
68
|
+
truncated = true
|
|
69
|
+
break
|
|
70
|
+
found.append(_serialize_node(node, false))
|
|
71
|
+
var children: Array[Node] = node.get_children()
|
|
72
|
+
for index: int in range(children.size() - 1, -1, -1):
|
|
73
|
+
pending.push_front(children[index])
|
|
74
|
+
|
|
75
|
+
return {"type": "nodes", "count": found.size(), "truncated": truncated, "nodes": found}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
func _matches(
|
|
79
|
+
node: Node, wanted_class: String, wanted_script: String, wanted_name: String, wanted_group: String
|
|
80
|
+
) -> bool:
|
|
81
|
+
if not wanted_group.is_empty() and not node.is_in_group(wanted_group):
|
|
82
|
+
return false
|
|
83
|
+
if not wanted_name.is_empty() and not str(node.name).matchn(wanted_name):
|
|
84
|
+
return false
|
|
85
|
+
var script: Variant = node.get_script()
|
|
86
|
+
if not wanted_script.is_empty():
|
|
87
|
+
if not script is Script:
|
|
88
|
+
return false
|
|
89
|
+
var attached: Script = script
|
|
90
|
+
if attached.resource_path != wanted_script:
|
|
91
|
+
return false
|
|
92
|
+
if not wanted_class.is_empty() and not node.is_class(wanted_class):
|
|
93
|
+
if not script is Script:
|
|
94
|
+
return false
|
|
95
|
+
var attached: Script = script
|
|
96
|
+
if attached.get_global_name() != wanted_class:
|
|
97
|
+
return false
|
|
98
|
+
return true
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
## Where a node is on screen: a Control's rectangle, or a Node2D's position, in both the
|
|
102
|
+
## canvas coordinates the node reports and the window pixels input arrives in. The two differ
|
|
103
|
+
## whenever the project stretches its viewport, which is what made a rect unusable for a click.
|
|
104
|
+
func get_rect(params: Dictionary) -> Dictionary:
|
|
105
|
+
var node_path: String = str(params.get("path", ""))
|
|
106
|
+
if node_path.is_empty():
|
|
107
|
+
return {"type": "error", "message": "Node path required"}
|
|
108
|
+
|
|
109
|
+
var node: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
110
|
+
if node == null:
|
|
111
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
112
|
+
|
|
113
|
+
if node is Control:
|
|
114
|
+
var control: Control = node
|
|
115
|
+
var to_window: Transform2D = control.get_viewport().get_final_transform()
|
|
116
|
+
var canvas_rect: Rect2 = control.get_global_rect()
|
|
117
|
+
return {
|
|
118
|
+
"type": "rect",
|
|
119
|
+
"path": node_path,
|
|
120
|
+
"visible": control.is_visible_in_tree(),
|
|
121
|
+
"canvas": _values.serialize(canvas_rect),
|
|
122
|
+
"window": _values.serialize(to_window * canvas_rect),
|
|
123
|
+
}
|
|
124
|
+
if node is Node2D:
|
|
125
|
+
var item: Node2D = node
|
|
126
|
+
var canvas_position: Vector2 = item.get_global_transform_with_canvas().origin
|
|
127
|
+
var window_position: Vector2 = item.get_viewport().get_final_transform() * canvas_position
|
|
128
|
+
return {
|
|
129
|
+
"type": "point",
|
|
130
|
+
"path": node_path,
|
|
131
|
+
"visible": item.is_visible_in_tree(),
|
|
132
|
+
"canvas": _values.serialize(canvas_position),
|
|
133
|
+
"window": _values.serialize(window_position),
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
"type": "error", "message": "%s is a %s, which has no place on screen" % [node_path, node.get_class()]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
func get_property(params: Dictionary) -> Dictionary:
|
|
141
|
+
var node_path: String = str(params.get("path", ""))
|
|
142
|
+
var property: String = str(params.get("property", ""))
|
|
143
|
+
|
|
144
|
+
if node_path.is_empty() or property.is_empty():
|
|
145
|
+
return {"type": "error", "message": "Node path and property required"}
|
|
146
|
+
|
|
147
|
+
var node: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
148
|
+
if node == null:
|
|
149
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
150
|
+
|
|
151
|
+
# Asked of the property list rather than read and compared to null, because a property the
|
|
152
|
+
# node does not have and a property that is null both read as null.
|
|
153
|
+
var known: bool = false
|
|
154
|
+
for entry: Dictionary in node.get_property_list():
|
|
155
|
+
if str(entry["name"]) == property:
|
|
156
|
+
known = true
|
|
157
|
+
break
|
|
158
|
+
if not known:
|
|
159
|
+
return {"type": "error", "message": "%s has no property %s" % [node_path, property]}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
"type": "property",
|
|
163
|
+
"path": node_path,
|
|
164
|
+
"property": property,
|
|
165
|
+
"value": _values.serialize(node.get(property)),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
func set_property(params: Dictionary) -> Dictionary:
|
|
170
|
+
var node_path: String = str(params.get("path", ""))
|
|
171
|
+
var property: String = str(params.get("property", ""))
|
|
172
|
+
var value: Variant = params.get("value")
|
|
173
|
+
|
|
174
|
+
if node_path.is_empty() or property.is_empty():
|
|
175
|
+
return {"type": "error", "message": "Node path and property required"}
|
|
176
|
+
|
|
177
|
+
var node: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
178
|
+
if node == null:
|
|
179
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
180
|
+
|
|
181
|
+
var old_value: Variant = node.get(property)
|
|
182
|
+
node.set(property, _values.fitted(value, typeof(old_value)))
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"type": "property_set",
|
|
186
|
+
"path": node_path,
|
|
187
|
+
"property": property,
|
|
188
|
+
"old_value": _values.serialize(old_value),
|
|
189
|
+
"new_value": _values.serialize(node.get(property))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
func call_method(params: Dictionary) -> Dictionary:
|
|
194
|
+
var node_path: String = str(params.get("path", ""))
|
|
195
|
+
var method: String = str(params.get("method", ""))
|
|
196
|
+
var args: Array = params.get("args", [])
|
|
197
|
+
|
|
198
|
+
if node_path.is_empty() or method.is_empty():
|
|
199
|
+
return {"type": "error", "message": "Node path and method required"}
|
|
200
|
+
|
|
201
|
+
var node: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
202
|
+
if node == null:
|
|
203
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
204
|
+
|
|
205
|
+
if not node.has_method(method):
|
|
206
|
+
return {"type": "error", "message": "Method not found: " + method}
|
|
207
|
+
|
|
208
|
+
var deserialized_args: Array = []
|
|
209
|
+
for index: int in args.size():
|
|
210
|
+
deserialized_args.append(_values.fitted(args[index], _values.parameter_type(node, method, index)))
|
|
211
|
+
|
|
212
|
+
var result: Variant = node.callv(method, deserialized_args)
|
|
213
|
+
|
|
214
|
+
return {"type": "method_result", "path": node_path, "method": method, "result": _values.serialize(result)}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
func get_metrics(params: Dictionary) -> Dictionary:
|
|
218
|
+
var metrics: Array = params.get("metrics", [])
|
|
219
|
+
var all: Dictionary = {
|
|
220
|
+
"fps": Engine.get_frames_per_second(),
|
|
221
|
+
"frame_time": Performance.get_monitor(Performance.TIME_PROCESS),
|
|
222
|
+
"physics_time": Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS),
|
|
223
|
+
"memory_static": Performance.get_monitor(Performance.MEMORY_STATIC),
|
|
224
|
+
"memory_static_max": Performance.get_monitor(Performance.MEMORY_STATIC_MAX),
|
|
225
|
+
"object_count": Performance.get_monitor(Performance.OBJECT_COUNT),
|
|
226
|
+
"object_resource_count": Performance.get_monitor(Performance.OBJECT_RESOURCE_COUNT),
|
|
227
|
+
"object_node_count": Performance.get_monitor(Performance.OBJECT_NODE_COUNT),
|
|
228
|
+
"object_orphan_node_count": Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT),
|
|
229
|
+
"render_total_objects": Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME),
|
|
230
|
+
"render_total_primitives": Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME),
|
|
231
|
+
"render_total_draw_calls": Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if metrics.is_empty():
|
|
235
|
+
return {"type": "metrics", "data": all}
|
|
236
|
+
|
|
237
|
+
# A caller that names metrics gets those and no others, and hears about a name that is
|
|
238
|
+
# not one rather than getting everything back as if the list had not been sent.
|
|
239
|
+
var unknown: Array[String] = []
|
|
240
|
+
var selected: Dictionary = {}
|
|
241
|
+
for metric: Variant in metrics:
|
|
242
|
+
if all.has(metric):
|
|
243
|
+
selected[metric] = all[metric]
|
|
244
|
+
else:
|
|
245
|
+
unknown.append(str(metric))
|
|
246
|
+
if not unknown.is_empty():
|
|
247
|
+
return {
|
|
248
|
+
"type": "error",
|
|
249
|
+
"message": "Unknown metrics: %s. Available: %s" % [", ".join(unknown), ", ".join(all.keys())]
|
|
250
|
+
}
|
|
251
|
+
return {"type": "metrics", "data": selected}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
func _serialize_node_tree(node: Node, depth: int, max_depth: int, include_properties: bool) -> Dictionary:
|
|
255
|
+
var result: Dictionary = _serialize_node(node, include_properties)
|
|
256
|
+
|
|
257
|
+
if depth < max_depth:
|
|
258
|
+
var children: Array = []
|
|
259
|
+
for child: Node in node.get_children():
|
|
260
|
+
children.append(_serialize_node_tree(child, depth + 1, max_depth, include_properties))
|
|
261
|
+
result["children"] = children
|
|
262
|
+
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
func _serialize_node(node: Node, include_properties: bool) -> Dictionary:
|
|
267
|
+
var result: Dictionary = {"name": node.name, "type": node.get_class(), "path": str(node.get_path())}
|
|
268
|
+
|
|
269
|
+
var script: Variant = node.get_script()
|
|
270
|
+
if script is Script:
|
|
271
|
+
var attached: Script = script
|
|
272
|
+
result["script"] = attached.resource_path
|
|
273
|
+
|
|
274
|
+
if include_properties:
|
|
275
|
+
var properties: Dictionary = {}
|
|
276
|
+
for prop: Dictionary in node.get_property_list():
|
|
277
|
+
if prop["usage"] & PROPERTY_USAGE_STORAGE:
|
|
278
|
+
var property_name: String = prop["name"]
|
|
279
|
+
if not property_name.begins_with("_"):
|
|
280
|
+
properties[property_name] = _values.serialize(node.get(property_name))
|
|
281
|
+
result["properties"] = properties
|
|
282
|
+
|
|
283
|
+
return result
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## What crosses the wire between the game and the server, in both directions: Godot values
|
|
4
|
+
## as JSON-safe dictionaries, and JSON back into the values a property or parameter wants.
|
|
5
|
+
|
|
6
|
+
# What each Godot type becomes on the wire, keyed on typeof() rather than written as a chain of
|
|
7
|
+
# `is` tests whose order has to be trusted: Resource had to be tested before Object, or every
|
|
8
|
+
# resource came back as a bare class name with its path dropped.
|
|
9
|
+
#
|
|
10
|
+
# Method names rather than Callables or lambdas: a lambda spanning more than one line inside a
|
|
11
|
+
# dictionary literal is where gdformat loses track of every comment in the file and writes them
|
|
12
|
+
# all again into the lambda body, on every run.
|
|
13
|
+
const SERIALISERS: Dictionary = {
|
|
14
|
+
TYPE_NIL: "_serialize_nil",
|
|
15
|
+
TYPE_VECTOR2: "_serialize_vector2",
|
|
16
|
+
TYPE_VECTOR3: "_serialize_vector3",
|
|
17
|
+
TYPE_VECTOR2I: "_serialize_vector2i",
|
|
18
|
+
TYPE_VECTOR3I: "_serialize_vector3i",
|
|
19
|
+
TYPE_COLOR: "_serialize_color",
|
|
20
|
+
TYPE_NODE_PATH: "_serialize_node_path",
|
|
21
|
+
TYPE_ARRAY: "_serialize_array",
|
|
22
|
+
TYPE_RECT2: "_serialize_rect2",
|
|
23
|
+
TYPE_TRANSFORM2D: "_serialize_transform2d",
|
|
24
|
+
TYPE_DICTIONARY: "_serialize_dictionary",
|
|
25
|
+
TYPE_OBJECT: "_serialize_object",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Converts a Godot value into something JSON can carry. A type with no entry in the table
|
|
30
|
+
## passes through as itself, which is what the JSON-native ones want.
|
|
31
|
+
func serialize(value: Variant) -> Variant:
|
|
32
|
+
var serialiser: String = SERIALISERS.get(typeof(value), "")
|
|
33
|
+
return call(serialiser, value) if not serialiser.is_empty() else value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
func _serialize_nil(_value: Variant) -> Variant:
|
|
37
|
+
return null
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
func _serialize_vector2(value: Vector2) -> Dictionary:
|
|
41
|
+
return {"_type": "Vector2", "x": value.x, "y": value.y}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
func _serialize_vector3(value: Vector3) -> Dictionary:
|
|
45
|
+
return {"_type": "Vector3", "x": value.x, "y": value.y, "z": value.z}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
func _serialize_vector2i(value: Vector2i) -> Dictionary:
|
|
49
|
+
return {"_type": "Vector2i", "x": value.x, "y": value.y}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
func _serialize_vector3i(value: Vector3i) -> Dictionary:
|
|
53
|
+
return {"_type": "Vector3i", "x": value.x, "y": value.y, "z": value.z}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
func _serialize_color(value: Color) -> Dictionary:
|
|
57
|
+
return {"_type": "Color", "r": value.r, "g": value.g, "b": value.b, "a": value.a}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
func _serialize_node_path(value: NodePath) -> Dictionary:
|
|
61
|
+
return {"_type": "NodePath", "path": str(value)}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
func _serialize_array(value: Array) -> Array:
|
|
65
|
+
return value.map(serialize)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
func _serialize_rect2(value: Rect2) -> Dictionary:
|
|
69
|
+
return {"_type": "Rect2", "position": serialize(value.position), "size": serialize(value.size)}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
func _serialize_transform2d(value: Transform2D) -> Dictionary:
|
|
73
|
+
return {
|
|
74
|
+
"_type": "Transform2D",
|
|
75
|
+
"origin": serialize(value.origin),
|
|
76
|
+
"x": serialize(value.x),
|
|
77
|
+
"y": serialize(value.y)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
func _serialize_dictionary(value: Dictionary) -> Dictionary:
|
|
82
|
+
var serialised: Dictionary = {}
|
|
83
|
+
for key: Variant in value:
|
|
84
|
+
serialised[str(key)] = serialize(value[key])
|
|
85
|
+
return serialised
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## The one case that genuinely needs the class hierarchy, since a Resource is also an Object and
|
|
89
|
+
## its path is the half worth having. A node in the tree is answered with its path, so a
|
|
90
|
+
## property that points at one can be fed straight back to any tool that takes a node.
|
|
91
|
+
func _serialize_object(value: Object) -> Dictionary:
|
|
92
|
+
if value is Resource:
|
|
93
|
+
var resource: Resource = value
|
|
94
|
+
return {"_type": "Resource", "path": resource.resource_path, "class": resource.get_class()}
|
|
95
|
+
if value is Node:
|
|
96
|
+
var node: Node = value
|
|
97
|
+
if node.is_inside_tree():
|
|
98
|
+
return {"_type": "Node", "class": node.get_class(), "path": str(node.get_path())}
|
|
99
|
+
return {"_type": "Object", "class": value.get_class()}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## Rebuilds a Godot value from the shape serialize gave it.
|
|
103
|
+
func deserialize(value: Variant) -> Variant:
|
|
104
|
+
if value == null:
|
|
105
|
+
return null
|
|
106
|
+
if value is Array:
|
|
107
|
+
var items: Array = value
|
|
108
|
+
var rebuilt: Array = []
|
|
109
|
+
for item: Variant in items:
|
|
110
|
+
rebuilt.append(deserialize(item))
|
|
111
|
+
return rebuilt
|
|
112
|
+
if not value is Dictionary:
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
var fields: Dictionary = value
|
|
116
|
+
if not fields.has("_type"):
|
|
117
|
+
var rebuilt: Dictionary = {}
|
|
118
|
+
for key: Variant in fields:
|
|
119
|
+
rebuilt[key] = deserialize(fields[key])
|
|
120
|
+
return rebuilt
|
|
121
|
+
|
|
122
|
+
match fields["_type"]:
|
|
123
|
+
"Vector2":
|
|
124
|
+
return Vector2(fields.get("x", 0), fields.get("y", 0))
|
|
125
|
+
"Vector3":
|
|
126
|
+
return Vector3(fields.get("x", 0), fields.get("y", 0), fields.get("z", 0))
|
|
127
|
+
"Vector2i":
|
|
128
|
+
return Vector2i(fields.get("x", 0), fields.get("y", 0))
|
|
129
|
+
"Vector3i":
|
|
130
|
+
return Vector3i(fields.get("x", 0), fields.get("y", 0), fields.get("z", 0))
|
|
131
|
+
"Color":
|
|
132
|
+
return Color(fields.get("r", 0), fields.get("g", 0), fields.get("b", 0), fields.get("a", 1))
|
|
133
|
+
"NodePath":
|
|
134
|
+
return NodePath(fields.get("path", ""))
|
|
135
|
+
return value
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
## The declared type of one parameter of a method, or TYPE_NIL when the method or the parameter
|
|
139
|
+
## is not there, which reads as "no opinion".
|
|
140
|
+
func parameter_type(object: Object, method: String, index: int) -> int:
|
|
141
|
+
for entry: Dictionary in object.get_method_list():
|
|
142
|
+
if entry.get("name", "") != method:
|
|
143
|
+
continue
|
|
144
|
+
var params: Array = entry.get("args", [])
|
|
145
|
+
if index < 0 or index >= params.size():
|
|
146
|
+
return TYPE_NIL
|
|
147
|
+
var parameter: Dictionary = params[index]
|
|
148
|
+
return int(parameter.get("type", TYPE_NIL))
|
|
149
|
+
return TYPE_NIL
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
## A value from the wire fitted to the type a property or parameter declares. Arguments arrive
|
|
153
|
+
## as strings often enough, and callv refuses a "2.0" where a float is wanted, so a string that
|
|
154
|
+
## reads as the wanted scalar is read as one.
|
|
155
|
+
func fitted(value: Variant, type: int) -> Variant:
|
|
156
|
+
var rebuilt: Variant = deserialize(value)
|
|
157
|
+
if type == TYPE_NIL or typeof(rebuilt) == type:
|
|
158
|
+
return rebuilt
|
|
159
|
+
|
|
160
|
+
var simple: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING]
|
|
161
|
+
if not simple.has(type) or not simple.has(typeof(rebuilt)):
|
|
162
|
+
return rebuilt
|
|
163
|
+
|
|
164
|
+
if rebuilt is String and type != TYPE_STRING:
|
|
165
|
+
var parsed: Variant = JSON.parse_string(rebuilt)
|
|
166
|
+
if typeof(parsed) != TYPE_NIL and typeof(parsed) != TYPE_STRING:
|
|
167
|
+
rebuilt = parsed
|
|
168
|
+
|
|
169
|
+
return type_convert(rebuilt, type)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## The commands that take time: they let the game run and answer when what was waited for has
|
|
4
|
+
## happened, or when the time ran out, so the caller never sleeps for a guessed length.
|
|
5
|
+
|
|
6
|
+
const Values = preload("runtime_values.gd")
|
|
7
|
+
|
|
8
|
+
## The longest one wait may last, whatever the request says: past this the server has long
|
|
9
|
+
## since given up on the reply.
|
|
10
|
+
const CEILING_MSEC: int = 120000
|
|
11
|
+
|
|
12
|
+
var _host: Node
|
|
13
|
+
var _values: Values
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
func _init(host: Node, values: Values) -> void:
|
|
17
|
+
_host = host
|
|
18
|
+
_values = values
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
func wait_frames(params: Dictionary) -> Dictionary:
|
|
22
|
+
var frames: int = clampi(int(params.get("frames", 1)), 1, 600)
|
|
23
|
+
var started: int = Time.get_ticks_msec()
|
|
24
|
+
for _frame: int in frames:
|
|
25
|
+
await _host.get_tree().process_frame
|
|
26
|
+
return {"type": "waited", "frames": frames, "elapsed_ms": Time.get_ticks_msec() - started}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Waits for a signal to fire, and answers with what it carried, or that the time ran out.
|
|
30
|
+
func wait_signal(params: Dictionary) -> Dictionary:
|
|
31
|
+
var node_path: String = str(params.get("path", ""))
|
|
32
|
+
var signal_name: String = str(params.get("signal", ""))
|
|
33
|
+
var timeout_ms: int = clampi(int(params.get("timeout_ms", 5000)), 1, CEILING_MSEC)
|
|
34
|
+
if node_path.is_empty() or signal_name.is_empty():
|
|
35
|
+
return {"type": "error", "message": "Node path and signal name required"}
|
|
36
|
+
|
|
37
|
+
var node: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
38
|
+
if node == null:
|
|
39
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
40
|
+
if not node.has_signal(signal_name):
|
|
41
|
+
return {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
|
|
42
|
+
|
|
43
|
+
var catcher: SignalCatcher = SignalCatcher.new()
|
|
44
|
+
catcher.arity = _signal_arity(node, signal_name)
|
|
45
|
+
var callable: Callable = catcher._on_fired
|
|
46
|
+
node.connect(signal_name, callable, CONNECT_ONE_SHOT)
|
|
47
|
+
var started: int = Time.get_ticks_msec()
|
|
48
|
+
while not catcher.fired and Time.get_ticks_msec() - started < timeout_ms:
|
|
49
|
+
await _host.get_tree().process_frame
|
|
50
|
+
if not catcher.fired and is_instance_valid(node) and node.is_connected(signal_name, callable):
|
|
51
|
+
node.disconnect(signal_name, callable)
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"type": "signal",
|
|
55
|
+
"path": node_path,
|
|
56
|
+
"signal": signal_name,
|
|
57
|
+
"fired": catcher.fired,
|
|
58
|
+
"args": _values.serialize(catcher.args),
|
|
59
|
+
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## Waits until a property reads as the value given, or the time runs out, and answers with
|
|
64
|
+
## what it read last either way.
|
|
65
|
+
func wait_until(params: Dictionary) -> Dictionary:
|
|
66
|
+
var node_path: String = str(params.get("path", ""))
|
|
67
|
+
var property: String = str(params.get("property", ""))
|
|
68
|
+
var timeout_ms: int = clampi(int(params.get("timeout_ms", 5000)), 1, CEILING_MSEC)
|
|
69
|
+
if node_path.is_empty() or property.is_empty():
|
|
70
|
+
return {"type": "error", "message": "Node path and property required"}
|
|
71
|
+
if not params.has("value"):
|
|
72
|
+
return {"type": "error", "message": "A value to wait for is required"}
|
|
73
|
+
|
|
74
|
+
var node: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
75
|
+
if node == null:
|
|
76
|
+
return {"type": "error", "message": "Node not found: " + node_path}
|
|
77
|
+
|
|
78
|
+
var current: Variant = node.get(property)
|
|
79
|
+
var wanted: Variant = _values.fitted(params["value"], typeof(current))
|
|
80
|
+
var started: int = Time.get_ticks_msec()
|
|
81
|
+
while current != wanted and Time.get_ticks_msec() - started < timeout_ms:
|
|
82
|
+
await _host.get_tree().process_frame
|
|
83
|
+
if not is_instance_valid(node):
|
|
84
|
+
return {"type": "error", "message": "%s was freed while waiting" % node_path}
|
|
85
|
+
current = node.get(property)
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
"type": "condition",
|
|
89
|
+
"path": node_path,
|
|
90
|
+
"property": property,
|
|
91
|
+
"met": current == wanted,
|
|
92
|
+
"value": _values.serialize(current),
|
|
93
|
+
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
func _signal_arity(node: Node, signal_name: String) -> int:
|
|
98
|
+
for entry: Dictionary in node.get_signal_list():
|
|
99
|
+
if entry.get("name", "") == signal_name:
|
|
100
|
+
var declared: Array = entry.get("args", [])
|
|
101
|
+
return declared.size()
|
|
102
|
+
return 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## Remembers that a signal fired and what it carried, for a wait that polls rather than
|
|
106
|
+
## awaits the signal directly, so the wait can also give up. The handler accepts up to five
|
|
107
|
+
## arguments, which covers every signal the engine declares, and records as many as the signal
|
|
108
|
+
## has.
|
|
109
|
+
class SignalCatcher:
|
|
110
|
+
extends RefCounted
|
|
111
|
+
var fired: bool = false
|
|
112
|
+
var arity: int = 0
|
|
113
|
+
var args: Array = []
|
|
114
|
+
|
|
115
|
+
func _on_fired(
|
|
116
|
+
a: Variant = null, b: Variant = null, c: Variant = null, d: Variant = null, e: Variant = null
|
|
117
|
+
) -> void:
|
|
118
|
+
fired = true
|
|
119
|
+
args = [a, b, c, d, e].slice(0, mini(arity, 5))
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
# The bus layout is a project resource the engine loads at startup and this process holds in
|
|
4
|
+
# memory, so every change is written back to that file or it is gone with the process.
|
|
5
|
+
|
|
6
|
+
const Log = preload("logger.gd")
|
|
7
|
+
|
|
8
|
+
var _log: Log
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
func _init(p_log: Log) -> void:
|
|
12
|
+
_log = p_log
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
func create_audio_bus(params: Dictionary) -> Dictionary:
|
|
16
|
+
var bus_name: String = str(params.get("bus_name", ""))
|
|
17
|
+
if bus_name.is_empty():
|
|
18
|
+
return _log.failure("bus_name is required")
|
|
19
|
+
var parent_idx: int = int(params.get("parent_bus_index", 0))
|
|
20
|
+
if parent_idx < 0 or parent_idx >= AudioServer.bus_count:
|
|
21
|
+
return _log.failure("No bus at index " + str(parent_idx))
|
|
22
|
+
|
|
23
|
+
AudioServer.add_bus(parent_idx + 1)
|
|
24
|
+
var new_idx: int = AudioServer.bus_count - 1
|
|
25
|
+
AudioServer.set_bus_name(new_idx, bus_name)
|
|
26
|
+
if parent_idx > 0:
|
|
27
|
+
AudioServer.set_bus_send(new_idx, AudioServer.get_bus_name(parent_idx))
|
|
28
|
+
|
|
29
|
+
var layout: String = _save_layout()
|
|
30
|
+
if layout.is_empty():
|
|
31
|
+
return {}
|
|
32
|
+
return {"bus": _bus(new_idx), "layout": layout}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
func get_audio_buses(_params: Dictionary) -> Dictionary:
|
|
36
|
+
var buses: Array[Dictionary] = []
|
|
37
|
+
for i: int in range(AudioServer.bus_count):
|
|
38
|
+
buses.append(_bus(i))
|
|
39
|
+
return {"bus_count": AudioServer.bus_count, "buses": buses, "layout": _layout_path()}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
func set_audio_bus_effect(params: Dictionary) -> Dictionary:
|
|
43
|
+
var bus_idx: int = int(params.get("bus_index", 0))
|
|
44
|
+
var effect_idx: int = int(params.get("effect_index", 0))
|
|
45
|
+
var effect_type: String = str(params.get("effect_type", ""))
|
|
46
|
+
var enabled: bool = bool(params.get("enabled", true))
|
|
47
|
+
if bus_idx < 0 or bus_idx >= AudioServer.bus_count:
|
|
48
|
+
return _log.failure("No bus at index " + str(bus_idx))
|
|
49
|
+
|
|
50
|
+
var effect: AudioEffect = _effect_named(effect_type)
|
|
51
|
+
if effect == null:
|
|
52
|
+
return _log.failure("Unknown effect type: " + effect_type)
|
|
53
|
+
|
|
54
|
+
# The slot has to exist before an effect can be placed at that index.
|
|
55
|
+
while AudioServer.get_bus_effect_count(bus_idx) <= effect_idx:
|
|
56
|
+
AudioServer.add_bus_effect(bus_idx, AudioEffectAmplify.new())
|
|
57
|
+
|
|
58
|
+
AudioServer.add_bus_effect(bus_idx, effect, effect_idx)
|
|
59
|
+
AudioServer.set_bus_effect_enabled(bus_idx, effect_idx, enabled)
|
|
60
|
+
|
|
61
|
+
var layout: String = _save_layout()
|
|
62
|
+
if layout.is_empty():
|
|
63
|
+
return {}
|
|
64
|
+
return {"bus": _bus(bus_idx), "effect_index": effect_idx, "effect_type": effect_type, "layout": layout}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
func set_audio_bus_volume(params: Dictionary) -> Dictionary:
|
|
68
|
+
var bus_idx: int = int(params.get("bus_index", 0))
|
|
69
|
+
var volume_db: float = float(params.get("volume_db", 0.0))
|
|
70
|
+
if bus_idx < 0 or bus_idx >= AudioServer.bus_count:
|
|
71
|
+
return _log.failure("No bus at index " + str(bus_idx))
|
|
72
|
+
|
|
73
|
+
AudioServer.set_bus_volume_db(bus_idx, volume_db)
|
|
74
|
+
|
|
75
|
+
var layout: String = _save_layout()
|
|
76
|
+
if layout.is_empty():
|
|
77
|
+
return {}
|
|
78
|
+
return {"bus": _bus(bus_idx), "layout": layout}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
func _bus(index: int) -> Dictionary:
|
|
82
|
+
var effects: Array[Dictionary] = []
|
|
83
|
+
for e: int in range(AudioServer.get_bus_effect_count(index)):
|
|
84
|
+
var effect: Dictionary = {
|
|
85
|
+
"index": e,
|
|
86
|
+
"type": AudioServer.get_bus_effect(index, e).get_class(),
|
|
87
|
+
"enabled": AudioServer.is_bus_effect_enabled(index, e),
|
|
88
|
+
}
|
|
89
|
+
effects.append(effect)
|
|
90
|
+
return {
|
|
91
|
+
"index": index,
|
|
92
|
+
"name": AudioServer.get_bus_name(index),
|
|
93
|
+
"volume_db": AudioServer.get_bus_volume_db(index),
|
|
94
|
+
"mute": AudioServer.is_bus_mute(index),
|
|
95
|
+
"solo": AudioServer.is_bus_solo(index),
|
|
96
|
+
"send": AudioServer.get_bus_send(index),
|
|
97
|
+
"effects": effects,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
func _layout_path() -> String:
|
|
102
|
+
return str(ProjectSettings.get_setting("audio/buses/default_bus_layout", "res://default_bus_layout.tres"))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# The path the layout was written to, or empty with the reason on stderr.
|
|
106
|
+
func _save_layout() -> String:
|
|
107
|
+
var path: String = _layout_path()
|
|
108
|
+
var err: Error = ResourceSaver.save(AudioServer.generate_bus_layout(), path)
|
|
109
|
+
if err != OK:
|
|
110
|
+
_log.error("Failed to save the bus layout to " + path + ": " + error_string(err))
|
|
111
|
+
return ""
|
|
112
|
+
return path
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# Any effect class the engine has, by its class name or the part after "AudioEffect".
|
|
116
|
+
func _effect_named(effect_type: String) -> AudioEffect:
|
|
117
|
+
var cls: String = effect_type if effect_type.begins_with("AudioEffect") else "AudioEffect" + effect_type
|
|
118
|
+
if not ClassDB.class_exists(cls) or not ClassDB.is_parent_class(cls, "AudioEffect"):
|
|
119
|
+
return null
|
|
120
|
+
if not ClassDB.can_instantiate(cls):
|
|
121
|
+
return null
|
|
122
|
+
var instance: Variant = ClassDB.instantiate(cls)
|
|
123
|
+
if instance is AudioEffect:
|
|
124
|
+
return instance
|
|
125
|
+
return null
|