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.
@@ -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 node: Node = _host.get_tree().root.get_node_or_null(viewport_path)
22
- if node == null:
23
- return {"type": "error", "message": "Viewport not found: " + viewport_path}
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 node: Node = _host.get_tree().root.get_node_or_null(node_path)
444
- if node == null:
445
- return {"type": "error", "message": "Node not found: " + node_path}
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 node: Node = _host.get_tree().root.get_node_or_null(node_path)
584
- if node == null:
585
- return {"type": "error", "message": "Node not found: " + node_path}
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 root: Node = _host.get_tree().root.get_node_or_null(root_path)
38
- if root == null:
39
- return {"type": "error", "message": "Node not found: " + root_path}
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 root: Node = _host.get_tree().root.get_node_or_null(root_path)
68
- if root == null:
69
- return {"type": "error", "message": "Node not found: " + root_path}
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["property"]
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 root: Node = _host.get_tree().root.get_node_or_null(root_path)
212
- if root == null:
213
- return {"type": "error", "message": "Node not found: " + root_path}
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 node: Node = _host.get_tree().root.get_node_or_null(node_path)
279
- if node == null:
280
- return {"type": "error", "message": "Node not found: " + node_path}
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 node: Node = _host.get_tree().root.get_node_or_null(node_path)
430
- if node == null:
431
- return {"type": "error", "message": "Node not found: " + node_path}
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["property"]
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 property] is a property of, which is the node itself until the name has a colon in
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, property: String) -> Dictionary:
465
- var parts: PackedStringArray = property.split(":")
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, "property": parts[parts.size() - 1], "called": called}
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 node: Node = _host.get_tree().root.get_node_or_null(node_path)
499
- if node == null:
500
- return {"type": "error", "message": "Node not found: " + node_path}
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["property"]
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 node: Node = _host.get_tree().root.get_node_or_null(node_path)
532
- if node == null:
533
- return {"type": "error", "message": "Node not found: " + node_path}
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
- if not node.has_method(method):
536
- return {"type": "error", "message": "Method not found: " + method}
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(node, method, index)))
559
+ deserialized_args.append(_values.fitted(args[index], _values.parameter_type(holder, named, index)))
541
560
 
542
- var result: Variant = node.callv(method, deserialized_args)
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 node: Node = _host.get_tree().root.get_node_or_null(node_path)
57
- if node == null:
58
- return {"type": "error", "message": "Node not found: " + node_path}
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
- if _host.get_tree().root.get_node_or_null(node_path) == null:
94
- return {"type": "error", "message": "Node not found: " + node_path}
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 = _host.get_tree().root.get_node_or_null(node_path)
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))