gdharness 0.9.1 → 0.9.3
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 +182 -39
- package/build/godot/addons/gdharness_editor/bridge_client.gd +63 -27
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +27 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +6 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +8 -6
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +52 -33
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +31 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +8 -6
- package/build/index.js +216 -89
- package/package.json +1 -1
|
@@ -125,6 +125,33 @@ func restart_editor(_args: Dictionary) -> Dictionary:
|
|
|
125
125
|
return {"ok": true, "restarting": true, "saved": true}
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
## Ends this editor, having saved, so that whoever opened it can open it again.
|
|
129
|
+
##
|
|
130
|
+
## The editor's half of restarting one a server started. Godot's own restart cannot carry the ports
|
|
131
|
+
## such an editor was given, for the reason above: the engine consumes the arguments and hands none
|
|
132
|
+
## of them back. So the server starts it again itself, and this is the part only the editor can do.
|
|
133
|
+
##
|
|
134
|
+
## Saved first, exactly as the restart above saves. The editor is going either way, and unsaved
|
|
135
|
+
## scenes are not this tool's to lose.
|
|
136
|
+
func quit_editor(_args: Dictionary) -> Dictionary:
|
|
137
|
+
if EditorInterface.is_playing_scene():
|
|
138
|
+
EditorInterface.stop_playing_scene()
|
|
139
|
+
|
|
140
|
+
EditorInterface.save_all_scenes()
|
|
141
|
+
|
|
142
|
+
# Deferred so this answer is on its way out before the editor goes.
|
|
143
|
+
_leave.call_deferred()
|
|
144
|
+
|
|
145
|
+
return {"ok": true, "quitting": true, "saved": true}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
## Asked of the tree the editor's own window is in, because this object is not in one.
|
|
149
|
+
static func _leave() -> void:
|
|
150
|
+
var base: Control = EditorInterface.get_base_control()
|
|
151
|
+
if base != null and base.get_tree() != null:
|
|
152
|
+
base.get_tree().quit()
|
|
153
|
+
|
|
154
|
+
|
|
128
155
|
func playing_status(_args: Dictionary) -> Dictionary:
|
|
129
156
|
var playing: bool = EditorInterface.is_playing_scene()
|
|
130
157
|
# The debugger's port with it, read now rather than remembered: it is taken again before every
|
|
@@ -2,6 +2,8 @@ extends RefCounted
|
|
|
2
2
|
|
|
3
3
|
## A picture of the running game, as a PNG written where the server asked for it.
|
|
4
4
|
|
|
5
|
+
const Values = preload("runtime_values.gd")
|
|
6
|
+
|
|
5
7
|
var _host: Node
|
|
6
8
|
|
|
7
9
|
|
|
@@ -18,9 +20,10 @@ func capture_viewport(params: Dictionary) -> Dictionary:
|
|
|
18
20
|
if viewport_path.is_empty():
|
|
19
21
|
return capture_screenshot(params)
|
|
20
22
|
|
|
21
|
-
var
|
|
22
|
-
if
|
|
23
|
-
return
|
|
23
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, viewport_path)
|
|
24
|
+
if standing.has("message"):
|
|
25
|
+
return standing
|
|
26
|
+
var node: Node = standing["node"]
|
|
24
27
|
if not node is Viewport:
|
|
25
28
|
return {"type": "error", "message": "Node is not a Viewport: " + viewport_path}
|
|
26
29
|
return _capture(node, params)
|
|
@@ -440,9 +440,10 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
440
440
|
if node_path.is_empty():
|
|
441
441
|
return {"type": "error", "message": "Node path required"}
|
|
442
442
|
|
|
443
|
-
var
|
|
444
|
-
if
|
|
445
|
-
return
|
|
443
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
444
|
+
if standing.has("message"):
|
|
445
|
+
return standing
|
|
446
|
+
var node: Node = standing["node"]
|
|
446
447
|
if node is Node3D:
|
|
447
448
|
return await _click_in_the_world(node_path, node, params)
|
|
448
449
|
if not node is Control:
|
|
@@ -580,9 +581,10 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
580
581
|
var node_path: String = str(params.get("path", ""))
|
|
581
582
|
if node_path.is_empty():
|
|
582
583
|
return {"type": "error", "message": "Node path required"}
|
|
583
|
-
var
|
|
584
|
-
if
|
|
585
|
-
return
|
|
584
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
585
|
+
if standing.has("message"):
|
|
586
|
+
return standing
|
|
587
|
+
var node: Node = standing["node"]
|
|
586
588
|
|
|
587
589
|
var menu: PopupMenu = _menu_of(node)
|
|
588
590
|
if menu == null:
|
|
@@ -34,9 +34,10 @@ func get_tree(params: Dictionary) -> Dictionary:
|
|
|
34
34
|
var max_depth: int = int(params.get("depth", 3))
|
|
35
35
|
var include_properties: bool = bool(params.get("include_properties", false))
|
|
36
36
|
|
|
37
|
-
var
|
|
38
|
-
if
|
|
39
|
-
return
|
|
37
|
+
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
38
|
+
if reached.has("message"):
|
|
39
|
+
return reached
|
|
40
|
+
var root: Node = reached["node"]
|
|
40
41
|
|
|
41
42
|
return {"type": "tree", "root": _serialize_node_tree(root, 0, max_depth, include_properties)}
|
|
42
43
|
|
|
@@ -64,9 +65,10 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
64
65
|
if not wanted["script"].is_empty() and not wanted["script"].begins_with("res://"):
|
|
65
66
|
wanted["script"] = "res://" + wanted["script"]
|
|
66
67
|
|
|
67
|
-
var
|
|
68
|
-
if
|
|
69
|
-
return
|
|
68
|
+
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
69
|
+
if reached.has("message"):
|
|
70
|
+
return reached
|
|
71
|
+
var root: Node = reached["node"]
|
|
70
72
|
|
|
71
73
|
var found: Array[Dictionary] = []
|
|
72
74
|
var pending: Array[Node] = [root]
|
|
@@ -128,7 +130,7 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
|
|
|
128
130
|
entry["has_property"] = false
|
|
129
131
|
return entry
|
|
130
132
|
var holder: Object = reached["holder"]
|
|
131
|
-
var named: String = reached["
|
|
133
|
+
var named: String = reached["name"]
|
|
132
134
|
var has: bool = _has_property(holder, named)
|
|
133
135
|
entry["has_property"] = has
|
|
134
136
|
if has:
|
|
@@ -208,9 +210,10 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
208
210
|
var include_hidden: bool = bool(params.get("include_hidden", false))
|
|
209
211
|
var limit: int = clampi(int(params.get("limit", READ_LIMIT)), 1, READ_LIMIT)
|
|
210
212
|
|
|
211
|
-
var
|
|
212
|
-
if
|
|
213
|
-
return
|
|
213
|
+
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
214
|
+
if reached.has("message"):
|
|
215
|
+
return reached
|
|
216
|
+
var root: Node = reached["node"]
|
|
214
217
|
|
|
215
218
|
# One line further than asked for, so that whether anything was left behind is read off the
|
|
216
219
|
# walk rather than guessed at from the count: a panel of exactly as many lines as the caller
|
|
@@ -275,9 +278,10 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
275
278
|
if node_path.is_empty():
|
|
276
279
|
return {"type": "error", "message": "Node path required"}
|
|
277
280
|
|
|
278
|
-
var
|
|
279
|
-
if
|
|
280
|
-
return
|
|
281
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
282
|
+
if standing.has("message"):
|
|
283
|
+
return standing
|
|
284
|
+
var node: Node = standing["node"]
|
|
281
285
|
|
|
282
286
|
if node is Control:
|
|
283
287
|
var control: Control = node
|
|
@@ -426,16 +430,17 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
426
430
|
if node_path.is_empty() or property.is_empty():
|
|
427
431
|
return {"type": "error", "message": "Node path and property required"}
|
|
428
432
|
|
|
429
|
-
var
|
|
430
|
-
if
|
|
431
|
-
return
|
|
433
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
434
|
+
if standing.has("message"):
|
|
435
|
+
return standing
|
|
436
|
+
var node: Node = standing["node"]
|
|
432
437
|
|
|
433
438
|
var reached: Dictionary = _reached(node, node_path, property)
|
|
434
439
|
if reached.has("message"):
|
|
435
440
|
return reached
|
|
436
441
|
|
|
437
442
|
var holder: Object = reached["holder"]
|
|
438
|
-
var named: String = reached["
|
|
443
|
+
var named: String = reached["name"]
|
|
439
444
|
# Asked of the property list rather than read and compared to null, because a property the
|
|
440
445
|
# holder does not have and a property that is null both read as null.
|
|
441
446
|
if not _has_property(holder, named):
|
|
@@ -449,11 +454,14 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
449
454
|
}
|
|
450
455
|
|
|
451
456
|
|
|
452
|
-
## What [param
|
|
453
|
-
## it, and the last name along that path.
|
|
457
|
+
## What [param reaching] names something on, which is the node itself until the name has a colon
|
|
458
|
+
## in it, and the last name along that path.
|
|
454
459
|
##
|
|
455
460
|
## A game's state does not sit on nodes, it hangs off them: the speed of the clock is a property of
|
|
456
461
|
## a [RefCounted] held by a [RefCounted] held by the root, and that is what an agent asks about.
|
|
462
|
+
## What a game does hangs off them the same way, so the last name is a property to read, a property
|
|
463
|
+
## to write or a method to call, and the walk to it is one walk.
|
|
464
|
+
##
|
|
457
465
|
## Colons, because that is the separator [method Object.get_indexed] already takes. Walked a step
|
|
458
466
|
## at a time rather than handed to that method, which answers null for a path that goes wrong
|
|
459
467
|
## halfway along and for one that ends on null.
|
|
@@ -461,8 +469,8 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
461
469
|
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
462
470
|
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
463
471
|
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
|
|
464
|
-
func _reached(node: Node, node_path: String,
|
|
465
|
-
var parts: PackedStringArray =
|
|
472
|
+
func _reached(node: Node, node_path: String, reaching: String) -> Dictionary:
|
|
473
|
+
var parts: PackedStringArray = reaching.split(":")
|
|
466
474
|
var holder: Object = node
|
|
467
475
|
var called: String = node_path
|
|
468
476
|
for step: int in parts.size() - 1:
|
|
@@ -475,7 +483,7 @@ func _reached(node: Node, node_path: String, property: String) -> Dictionary:
|
|
|
475
483
|
var wanted: String = parts[step + 1]
|
|
476
484
|
return {"type": "error", "message": "%s holds no object to read %s off" % [called, wanted]}
|
|
477
485
|
holder = next
|
|
478
|
-
return {"holder": holder, "
|
|
486
|
+
return {"holder": holder, "name": parts[parts.size() - 1], "called": called}
|
|
479
487
|
|
|
480
488
|
|
|
481
489
|
## Whether [param holder] declares [param named]. Its own function because three ops ask it and a
|
|
@@ -495,9 +503,10 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
495
503
|
if node_path.is_empty() or property.is_empty():
|
|
496
504
|
return {"type": "error", "message": "Node path and property required"}
|
|
497
505
|
|
|
498
|
-
var
|
|
499
|
-
if
|
|
500
|
-
return
|
|
506
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
507
|
+
if standing.has("message"):
|
|
508
|
+
return standing
|
|
509
|
+
var node: Node = standing["node"]
|
|
501
510
|
|
|
502
511
|
# Through a path as well, for the reason [method _reached] gives, and read back off the same
|
|
503
512
|
# holder afterwards: a set that does not take says so by answering with the old value, which is
|
|
@@ -507,7 +516,7 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
507
516
|
return reached
|
|
508
517
|
|
|
509
518
|
var holder: Object = reached["holder"]
|
|
510
|
-
var named: String = reached["
|
|
519
|
+
var named: String = reached["name"]
|
|
511
520
|
var old_value: Variant = holder.get(named)
|
|
512
521
|
holder.set(named, _values.fitted(value, typeof(old_value)))
|
|
513
522
|
|
|
@@ -528,18 +537,28 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
528
537
|
if node_path.is_empty() or method.is_empty():
|
|
529
538
|
return {"type": "error", "message": "Node path and method required"}
|
|
530
539
|
|
|
531
|
-
var
|
|
532
|
-
if
|
|
533
|
-
return
|
|
540
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
541
|
+
if standing.has("message"):
|
|
542
|
+
return standing
|
|
543
|
+
var node: Node = standing["node"]
|
|
534
544
|
|
|
535
|
-
|
|
536
|
-
|
|
545
|
+
# Through a path as well, for the reason [method _reached] gives. What a game does hangs off its
|
|
546
|
+
# nodes as much as its state does, so reading `_game:run:day` while being unable to call
|
|
547
|
+
# `_game:run:advance` answers half of what a node holds and refuses the other half.
|
|
548
|
+
var reached: Dictionary = _reached(node, node_path, method)
|
|
549
|
+
if reached.has("message"):
|
|
550
|
+
return reached
|
|
551
|
+
|
|
552
|
+
var holder: Object = reached["holder"]
|
|
553
|
+
var named: String = reached["name"]
|
|
554
|
+
if not holder.has_method(named):
|
|
555
|
+
return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
|
|
537
556
|
|
|
538
557
|
var deserialized_args: Array = []
|
|
539
558
|
for index: int in args.size():
|
|
540
|
-
deserialized_args.append(_values.fitted(args[index], _values.parameter_type(
|
|
559
|
+
deserialized_args.append(_values.fitted(args[index], _values.parameter_type(holder, named, index)))
|
|
541
560
|
|
|
542
|
-
var result: Variant =
|
|
561
|
+
var result: Variant = holder.callv(named, deserialized_args)
|
|
543
562
|
|
|
544
563
|
return {"type": "method_result", "path": node_path, "method": method, "result": _values.serialize(result)}
|
|
545
564
|
|
|
@@ -42,6 +42,37 @@ func serialize(value: Variant) -> Variant:
|
|
|
42
42
|
return call(serialiser, value) if not serialiser.is_empty() else value
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
## The node [param path] names under [param root], or a refusal saying why there is not one.
|
|
46
|
+
##
|
|
47
|
+
## The other direction of what a node serialises as, and here for that reason: a node crosses the
|
|
48
|
+
## wire as its path, so a path crossing back is a node or it is nothing. One answer for every op,
|
|
49
|
+
## because each of them read a path for itself and they could disagree about what one means.
|
|
50
|
+
##
|
|
51
|
+
## A colon is the disagreement worth its own sentence. Godot reads everything after the first one
|
|
52
|
+
## as subnames and [method Node.get_node_or_null] drops them, so "/root/Main:_game:run" was quietly
|
|
53
|
+
## answered about "/root/Main", and an op that then said the path had no method `advance` was right
|
|
54
|
+
## about an object nobody had asked for. Colons reach through what a node holds, and the arguments
|
|
55
|
+
## that take them are the property and the method.
|
|
56
|
+
static func node_at(root: Node, path: String) -> Dictionary:
|
|
57
|
+
if path.is_empty():
|
|
58
|
+
return {"type": "error", "message": "Node path required"}
|
|
59
|
+
|
|
60
|
+
if path.contains(":"):
|
|
61
|
+
var shape: String = (
|
|
62
|
+
'"%s" names a node and colons reach past one: "%s" is the node,'
|
|
63
|
+
+ ' and "%s" goes in the property or the method'
|
|
64
|
+
)
|
|
65
|
+
return {
|
|
66
|
+
"type": "error",
|
|
67
|
+
"message": shape % [path, path.get_slice(":", 0), path.substr(path.find(":") + 1)],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var node: Node = root.get_node_or_null(path)
|
|
71
|
+
if node == null:
|
|
72
|
+
return {"type": "error", "message": "Node not found: " + path}
|
|
73
|
+
return {"node": node}
|
|
74
|
+
|
|
75
|
+
|
|
45
76
|
func _serialize_nil(_value: Variant) -> Variant:
|
|
46
77
|
return null
|
|
47
78
|
|
|
@@ -53,9 +53,10 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
53
53
|
if node_path.is_empty() or signal_name.is_empty():
|
|
54
54
|
return {"type": "error", "message": "Node path and signal name required"}
|
|
55
55
|
|
|
56
|
-
var
|
|
57
|
-
if
|
|
58
|
-
return
|
|
56
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
57
|
+
if standing.has("message"):
|
|
58
|
+
return standing
|
|
59
|
+
var node: Node = standing["node"]
|
|
59
60
|
if not node.has_signal(signal_name):
|
|
60
61
|
return {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
|
|
61
62
|
|
|
@@ -90,8 +91,9 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
90
91
|
return _out_of_range("timeout_ms", timeout_ms, 1, CEILING_MSEC)
|
|
91
92
|
if node_path.is_empty():
|
|
92
93
|
return {"type": "error", "message": "Node path required"}
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
95
|
+
if standing.has("message"):
|
|
96
|
+
return standing
|
|
95
97
|
if not says.is_empty():
|
|
96
98
|
return await _wait_until_said(node_path, says, timeout_ms)
|
|
97
99
|
if property.is_empty():
|
|
@@ -99,7 +101,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
99
101
|
if not params.has("value"):
|
|
100
102
|
return {"type": "error", "message": "A value to wait for is required"}
|
|
101
103
|
|
|
102
|
-
var node: Node =
|
|
104
|
+
var node: Node = standing["node"]
|
|
103
105
|
|
|
104
106
|
var current: Variant = node.get(property)
|
|
105
107
|
var wanted: Variant = _values.fitted(params["value"], typeof(current))
|