gdharness 1.2.30 → 1.2.32

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.
Files changed (37) hide show
  1. package/build/cli.js +7197 -6159
  2. package/build/godot/addons/gdharness_editor/bridge_client.gd +14 -0
  3. package/build/godot/addons/gdharness_editor/file_reads.gd +237 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd.uid +1 -0
  5. package/build/godot/addons/gdharness_editor/plugin.gd +27 -0
  6. package/build/godot/addons/gdharness_editor/property_values.gd +80 -54
  7. package/build/godot/addons/gdharness_editor/scene_file.gd +38 -1
  8. package/build/godot/addons/gdharness_editor/serialisation.gd +46 -0
  9. package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
  10. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +22 -0
  11. package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +64 -1
  12. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +82 -44
  13. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +178 -0
  14. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +73 -124
  16. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +39 -0
  17. package/build/godot/addons/gdharness_runtime/runtime_hover.gd +285 -0
  18. package/build/godot/addons/gdharness_runtime/runtime_hover.gd.uid +1 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_input.gd +151 -106
  20. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +112 -0
  21. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd.uid +1 -0
  22. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +5 -4
  23. package/build/godot/addons/gdharness_runtime/runtime_values.gd +1 -47
  24. package/build/godot/addons/gdharness_runtime/serialisation.gd +46 -0
  25. package/build/godot/operations/audio_buses.gd +14 -1
  26. package/build/godot/operations/class_cache.gd +146 -35
  27. package/build/godot/operations/classdb_queries.gd +19 -12
  28. package/build/godot/operations/file_reads.gd +237 -0
  29. package/build/godot/operations/gdscript_authoring.gd +136 -19
  30. package/build/godot/operations/godot_operations.gd +24 -0
  31. package/build/godot/operations/project_config.gd +16 -6
  32. package/build/godot/operations/property_values.gd +767 -0
  33. package/build/godot/operations/rename_checks.gd +5 -2
  34. package/build/godot/operations/serialisation.gd +46 -0
  35. package/build/index.js +4896 -3872
  36. package/build/keeper.js +184 -71
  37. package/package.json +1 -1
@@ -3,8 +3,11 @@ extends Node
3
3
 
4
4
  ## Scenes and their nodes, edited in the open editor and read back from what it holds.
5
5
 
6
+ const FileReads = preload("../file_reads.gd")
6
7
  const Read = preload("../reading.gd")
8
+ const Placing = preload("scene_placing.gd")
7
9
  const PropertyValues = preload("../property_values.gd")
10
+ const ResourceTools = preload("resource_tools.gd")
8
11
  const SceneFile = preload("../scene_file.gd")
9
12
  const Serialisation = preload("../serialisation.gd")
10
13
 
@@ -101,8 +104,8 @@ func _kept(scene: SceneFile, saved: Node, node: Node, names: Array) -> String:
101
104
  return "%s is not in the scene as it would be saved." % at
102
105
  for key: Variant in names:
103
106
  var property: String = str(key)
104
- var loaded: Variant = copy.get(property)
105
- if not scene.kept(saved, loaded, node.get(property)):
107
+ var loaded: Variant = _properties.read_at(copy, property)
108
+ if not scene.kept(saved, loaded, _properties.read_at(node, property)):
106
109
  return (
107
110
  (
108
111
  "%s.%s would load from the file as %s: the scene does not keep it, because the property "
@@ -113,81 +116,6 @@ func _kept(scene: SceneFile, saved: Node, node: Node, names: Array) -> String:
113
116
  return ""
114
117
 
115
118
 
116
- ## Owns [param node] and the nodes under it to [param root], except those an instanced scene
117
- ## inside it owns: giving those to the root made the pack write the instance's own nodes into this
118
- ## scene as overrides.
119
- func _copy(source: Node, root: Node, copies: Dictionary) -> Node:
120
- var copy: Node = null
121
- if not source.scene_file_path.is_empty():
122
- # An instanced scene made again from its file, so the copy is an instance too. Node.duplicate
123
- # makes a plain node of the instance's class, and the pack saved it with every property of the
124
- # instance's root frozen into this scene.
125
- var packed: PackedScene = ResourceLoader.load(source.scene_file_path, "PackedScene") as PackedScene
126
- if packed == null:
127
- return null
128
- copy = packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
129
- for declared: Dictionary in source.get_property_list():
130
- if not (Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
131
- continue
132
- var property: String = str(declared.get("name", ""))
133
- var value: Variant = source.get(property)
134
- if not PropertyValues.same(copy.get(property), value):
135
- copy.set(property, value)
136
- for group: StringName in source.get_groups():
137
- if not copy.is_in_group(group):
138
- copy.add_to_group(group, true)
139
- else:
140
- copy = source.duplicate(Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS)
141
- for child: Node in copy.get_children():
142
- copy.remove_child(child)
143
- child.free()
144
- copy.name = source.name
145
- copies[source] = copy
146
- # Only what this scene owns: an instance's own nodes come with the instance.
147
- for child: Node in source.get_children():
148
- if child.owner == root:
149
- var copied: Node = _copy(child, root, copies)
150
- if copied == null:
151
- return null
152
- copy.add_child(copied)
153
- return copy
154
-
155
-
156
- ## Gives each copy in [param copies] the persistent connections its source has, to the copy of the
157
- ## target when the target was copied too. A connection an instance makes inside itself is skipped:
158
- ## the instance made again from its file already has it.
159
- func _reconnect(copies: Dictionary, top: Node) -> void:
160
- for key: Variant in copies:
161
- var source: Node = key
162
- var copy: Node = copies[key]
163
- for info: Dictionary in source.get_signal_list():
164
- var signal_name: String = str(info.get("name", ""))
165
- for conn: Dictionary in source.get_signal_connection_list(signal_name):
166
- var flags: int = Read.as_int(conn.get("flags", 0))
167
- var callable: Callable = conn.get("callable", Callable())
168
- var target: Object = callable.get_object()
169
- if not flags & Object.CONNECT_PERSIST or target == null:
170
- continue
171
- var to: Object = copies.get(target, target)
172
- if to == target and target is Node and (target == top or top.is_ancestor_of(target as Node)):
173
- continue
174
- var copied: Callable = Callable(to, callable.get_method())
175
- if (
176
- not copy.is_connected(signal_name, copied)
177
- and copy.connect(signal_name, copied, flags) != OK
178
- ):
179
- push_error(
180
- "gdharness: could not copy the connection %s of %s" % [signal_name, source.name]
181
- )
182
-
183
-
184
- func _own(node: Node, root: Node) -> void:
185
- node.owner = root
186
- for child: Node in node.get_children():
187
- if child.owner == null or child.owner == root:
188
- _own(child, root)
189
-
190
-
191
119
  ## [param node]'s stored properties as a read answers them. A property holding a node is answered
192
120
  ## as the path to it: the copy read here is never in the tree, and an object description named a
193
121
  ## node that was freed when the call ended.
@@ -240,27 +168,6 @@ func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
240
168
  return {}
241
169
 
242
170
 
243
- func _build_node_tree(
244
- node: Node, include_properties: bool, depth: int, current_depth: int, node_path: String
245
- ) -> Dictionary:
246
- var children: Array[Dictionary] = []
247
- var data: Dictionary = {
248
- "name": str(node.name), "type": node.get_class(), "path": node_path, "children": children
249
- }
250
-
251
- if include_properties:
252
- data["properties"] = _properties_of(node, true, {})
253
-
254
- if depth >= 0 and current_depth >= depth:
255
- return data
256
-
257
- for child: Node in node.get_children():
258
- var child_path: String = str(child.name) if node_path == "." else node_path + "/" + str(child.name)
259
- children.append(_build_node_tree(child, include_properties, depth, current_depth + 1, child_path))
260
-
261
- return data
262
-
263
-
264
171
  func _collect_nodes_recursive(node: Node, path: String, out_nodes: Array) -> void:
265
172
  out_nodes.append({"path": path, "node": node})
266
173
  for child: Node in node.get_children():
@@ -349,31 +256,29 @@ func list_scene_nodes(args: Dictionary) -> Dictionary:
349
256
  var depth: int = Read.as_int(args.get("depth", -1), -1)
350
257
  var include_properties: bool = Read.as_bool(args.get("includeProperties", false))
351
258
 
352
- var opened: Array = _open(scene_path, false)
353
- var scene: SceneFile = opened[0]
354
- var refused: Dictionary = opened[1]
355
- if not refused.is_empty():
356
- return refused
357
-
358
- var tree: Dictionary = _build_node_tree(scene.root, include_properties, depth, 0, ".")
359
- scene.close()
360
- return {"ok": true, "tree": tree, "unsavedInEditor": SceneFile.unsaved(scene_path)}
259
+ # Read as a headless engine reads it with no editor connected, so the answer does not depend on
260
+ # whether one is.
261
+ var read: Dictionary = FileReads.scene_tree(scene_path, depth, include_properties)
262
+ if read.get("ok", false):
263
+ read["unsavedInEditor"] = SceneFile.unsaved(scene_path)
264
+ return read
361
265
 
362
266
 
363
267
  func add_node(args: Dictionary) -> Dictionary:
364
268
  var project_path: String = str(args.get("projectPath", ""))
365
269
  var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
366
270
  var node_type: String = str(args.get("nodeType", ""))
271
+ var instance_path: String = str(args.get("instancePath", ""))
367
272
  var node_name: String = str(args.get("nodeName", ""))
368
273
  var parent_node_path: String = str(args.get("parentNodePath", "."))
369
274
  var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
370
275
 
371
- if node_type.is_empty() or node_name.is_empty():
372
- return {"ok": false, "error": "Missing nodeType or nodeName"}
373
- if not ClassDB.class_exists(node_type) or not ClassDB.is_parent_class(node_type, "Node"):
374
- return {"ok": false, "error": "nodeType must be a node class, and %s is not one." % node_type}
375
- if not ClassDB.can_instantiate(node_type):
376
- return {"ok": false, "error": "%s cannot be instantiated." % node_type}
276
+ if node_name.is_empty():
277
+ return {"ok": false, "error": "Missing nodeName"}
278
+ var making: Array = Placing.what_to_add(node_type, instance_path, scene_path)
279
+ var packed: PackedScene = making[0]
280
+ if not str(making[1]).is_empty():
281
+ return {"ok": false, "error": making[1]}
377
282
 
378
283
  var opened: Array = _open(scene_path, true)
379
284
  var scene: SceneFile = opened[0]
@@ -391,24 +296,39 @@ func add_node(args: Dictionary) -> Dictionary:
391
296
  var unusable: String = scene.unusable_name(parent, node_name)
392
297
  if not unusable.is_empty():
393
298
  return scene.refuse(unusable)
299
+ var place: Variant = args.get("index")
300
+ var unplaceable: String = Placing.unplaceable(parent, null, place)
301
+ if not unplaceable.is_empty():
302
+ return scene.refuse(unplaceable)
394
303
 
395
304
  # In the tree before its properties, so a property holding a node can find it by path.
396
- var new_node: Node = ClassDB.instantiate(node_type)
305
+ var new_node: Node = Placing.made(packed, node_type)
397
306
  new_node.name = node_name
398
307
  parent.add_child(new_node)
399
- _own(new_node, root)
308
+ Placing.place(new_node, place)
309
+ Placing.own(new_node, root)
400
310
  var refused_property: String = _properties.write_all(new_node, properties)
401
311
  if not refused_property.is_empty():
402
312
  return scene.refuse(refused_property)
403
313
 
404
314
  var new_path: String = scene.path_of(new_node)
315
+ # Read before the write, which frees the scene it was added to.
316
+ var answer: Dictionary = {
317
+ "ok": true,
318
+ "nodeName": node_name,
319
+ "nodeType": new_node.get_class(),
320
+ "nodePath": new_path,
321
+ "index": new_node.get_index(),
322
+ }
405
323
  var written: Dictionary = scene.write(
406
324
  func(saved: Node) -> String: return _kept(scene, saved, new_node, properties.keys())
407
325
  )
408
326
  if not written.is_empty():
409
327
  return written
410
328
 
411
- return {"ok": true, "nodeName": node_name, "nodeType": node_type, "nodePath": new_path}
329
+ if packed != null:
330
+ answer["instanceOf"] = instance_path
331
+ return answer
412
332
 
413
333
 
414
334
  func delete_node(args: Dictionary) -> Dictionary:
@@ -487,14 +407,14 @@ func duplicate_node(args: Dictionary) -> Dictionary:
487
407
  return scene.refuse(unusable)
488
408
 
489
409
  var copies: Dictionary = {}
490
- var duplicated_node: Node = _copy(source, root, copies)
410
+ var duplicated_node: Node = Placing.copied(source, root, copies)
491
411
  if not duplicated_node:
492
412
  return scene.refuse("Failed to duplicate node: " + node_path)
493
413
 
494
414
  duplicated_node.name = new_name
495
415
  target_parent.add_child(duplicated_node)
496
- _own(duplicated_node, root)
497
- _reconnect(copies, source)
416
+ Placing.own(duplicated_node, root)
417
+ Placing.reconnect(copies, source)
498
418
 
499
419
  var new_path: String = scene.path_of(duplicated_node)
500
420
  var written: Dictionary = scene.write(
@@ -548,25 +468,40 @@ func reparent_node(args: Dictionary) -> Dictionary:
548
468
  var unusable: String = scene.unusable_name(new_parent, str(node.name), node)
549
469
  if not unusable.is_empty():
550
470
  return scene.refuse(unusable)
471
+ var place: Variant = args.get("index")
472
+ var unplaceable: String = Placing.unplaceable(new_parent, node, place)
473
+ if not unplaceable.is_empty():
474
+ return scene.refuse(unplaceable)
551
475
 
552
476
  var old_path: String = scene.path_of(node)
553
477
  node.get_parent().remove_child(node)
554
478
  new_parent.add_child(node)
555
- _own(node, root)
479
+ Placing.place(node, place)
480
+ Placing.own(node, root)
556
481
 
557
482
  var new_path: String = scene.path_of(node)
483
+ var index: int = node.get_index()
558
484
  var written: Dictionary = scene.write(
559
485
  func(saved: Node) -> String:
560
- if saved.get_node_or_null(new_path) == null:
486
+ var copy: Node = saved.get_node_or_null(new_path)
487
+ if copy == null:
561
488
  return "%s would not be in the scene when it loads." % new_path
562
489
  if old_path != new_path and saved.get_node_or_null(old_path) != null:
563
490
  return "%s would still be in the scene as well when it loads." % old_path
491
+ if copy.get_index() != index:
492
+ return "%s would load at index %d, not %d." % [new_path, copy.get_index(), index]
564
493
  return ""
565
494
  )
566
495
  if not written.is_empty():
567
496
  return written
568
497
 
569
- return {"ok": true, "nodePath": old_path, "newParentPath": new_parent_path, "newNodePath": new_path}
498
+ return {
499
+ "ok": true,
500
+ "nodePath": old_path,
501
+ "newParentPath": new_parent_path,
502
+ "newNodePath": new_path,
503
+ "index": index,
504
+ }
570
505
 
571
506
 
572
507
  func set_node_properties(args: Dictionary) -> Dictionary:
@@ -601,7 +536,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
601
536
 
602
537
  var held: Dictionary = {}
603
538
  for key: Variant in properties:
604
- held[str(key)] = _readable(node, node.get(str(key)))
539
+ held[str(key)] = _readable(node, _properties.read_at(node, str(key)))
605
540
  var new_path: String = scene.path_of(node)
606
541
  var written: Dictionary = scene.write(
607
542
  func(saved: Node) -> String: return _kept(scene, saved, node, properties.keys())
@@ -633,6 +568,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
633
568
  # variable read as changed; and a value this scene sets back to the default was hidden, so the
634
569
  # names this scene's file sets are always answered.
635
570
  var props: Dictionary = _properties_of(node, include_defaults, scene.set_here(node))
571
+ var cells: Dictionary = ResourceTools.cells_of(node)
636
572
  scene.close()
637
573
  var answer: Dictionary = {
638
574
  "ok": true,
@@ -640,6 +576,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
640
576
  "properties": props,
641
577
  "unsavedInEditor": SceneFile.unsaved(scene_path)
642
578
  }
579
+ answer.merge(cells)
643
580
  # An empty answer read as a node with nothing on it, rather than one with nothing changed.
644
581
  if props.is_empty() and not include_defaults:
645
582
  answer["note"] = (
@@ -757,6 +694,10 @@ func connect_signal(args: Dictionary) -> Dictionary:
757
694
  # asked for. Left alone, the answer gave the flags asked for and the file kept the old ones.
758
695
  var callable: Callable = Callable(target, method_name)
759
696
  var method_found: bool = target.has_method(method_name)
697
+ var flags_before: int = -1
698
+ for connection: Dictionary in source.get_signal_connection_list(signal_name):
699
+ if connection.get("callable") == callable:
700
+ flags_before = Read.as_int(connection.get("flags", 0))
760
701
  if source.is_connected(signal_name, callable):
761
702
  source.disconnect(signal_name, callable)
762
703
  var connect_result: Error = source.connect(signal_name, callable, flags)
@@ -776,7 +717,7 @@ func connect_signal(args: Dictionary) -> Dictionary:
776
717
  if not written.is_empty():
777
718
  return written
778
719
 
779
- return {
720
+ var answer: Dictionary = {
780
721
  "ok": true,
781
722
  "sourceNodePath": from,
782
723
  "signalName": signal_name,
@@ -788,6 +729,14 @@ func connect_signal(args: Dictionary) -> Dictionary:
788
729
  # next; a caller told nothing learnt of a misspelt one only when the signal fired.
789
730
  "methodFound": method_found
790
731
  }
732
+ # A second connect of the same pair read exactly like the first, so a caller could not tell a
733
+ # connection made from one that was already there, and a list showing one where two were made
734
+ # looked like a lost write.
735
+ if flags_before >= 0:
736
+ answer["alreadyConnected"] = true
737
+ if flags_before != Read.as_int(saved_flags[0]):
738
+ answer["flagsBefore"] = _flag_names(flags_before)
739
+ return answer
791
740
 
792
741
 
793
742
  ## The two ends of a connection in [param scene], as [source, target], or [null, why not].
@@ -49,6 +49,10 @@ const PROFILER_PROBE_CALLS: int = 10000
49
49
  ## on every 2048th call and only counts the others.
50
50
  const POLLS_PER_READ: int = 2048
51
51
 
52
+ ## The most frames a close waits for its answer to leave: a client that stopped reading would
53
+ ## otherwise hold the close for ever, and the server is past caring about the answer by then.
54
+ const CLOSE_AFTER_FRAMES: int = 30
55
+
52
56
  ## Set in a game's environment by whatever starts it to be profiled from its start: the server for
53
57
  ## a run it spawns, the editor for a play. Kept in step with `PROFILE_FROM_START` in the server.
54
58
  const PROFILE_FROM_START: String = "GDHARNESS_PROFILE_FROM_START"
@@ -118,6 +122,8 @@ var _commands: Dictionary = {}
118
122
  var _loaded_version: String = _read_marker(VERSION_MARKER)
119
123
  var _loaded_digest: String = _read_marker(DIGEST_MARKER)
120
124
  var _profile_received: bool = false
125
+ var _close_asked: bool = false
126
+ var _close_frames: int = 0
121
127
  var _profiler_calibrated: bool = false
122
128
 
123
129
 
@@ -140,6 +146,7 @@ func _init() -> void:
140
146
  "inject_text": _typing.inject_text,
141
147
  "inject_mouse_click": _input.inject_mouse_click,
142
148
  "inject_mouse_motion": _input.inject_mouse_motion,
149
+ "inject_mouse_drag": _input.inject_mouse_drag,
143
150
  "inject_joypad_button": _pad.inject_joypad_button,
144
151
  "inject_joypad_motion": _pad.inject_joypad_motion,
145
152
  "click": _input.click,
@@ -148,6 +155,7 @@ func _init() -> void:
148
155
  "wait_signal": _waits.wait_signal,
149
156
  "wait_until": _waits.wait_until,
150
157
  "send_profile": _send_profile_now,
158
+ "close_game": _close_game,
151
159
  }
152
160
  _wait_for_the_profiler()
153
161
 
@@ -341,6 +349,14 @@ func _process(_delta: float) -> void:
341
349
  var _had_pending: bool = _pending.erase(client)
342
350
  var _had_outgoing: bool = _outgoing.erase(client)
343
351
 
352
+ # After the answer to the close has gone out: the close request reaches this node too, which then
353
+ # drops its clients and whatever they were still owed.
354
+ if _close_asked:
355
+ _close_frames += 1
356
+ if _close_frames >= CLOSE_AFTER_FRAMES or _outgoing.values().all(_is_empty):
357
+ _close_asked = false
358
+ _deliver_close()
359
+
344
360
 
345
361
  ## Hands the socket as much of what it is owed as it will take, and says whether it is still worth
346
362
  ## talking to. Reading comes first in the frame, so a client that asked and left is noticed here
@@ -578,6 +594,29 @@ func _ping(_params: Dictionary) -> Dictionary:
578
594
  return {"type": "pong", "timestamp": Time.get_unix_time_from_system()}
579
595
 
580
596
 
597
+ ## The game asked to close, as a player's close asks it: answered at once and delivered once the
598
+ ## answer has gone out. `acceptsQuit` says whether the tree quits on the request by itself; a game
599
+ ## that turned that off decides for itself, and may ask the player first.
600
+ func _close_game(_params: Dictionary) -> Dictionary:
601
+ _close_asked = true
602
+ _close_frames = 0
603
+ return {"type": "closing", "acceptsQuit": get_tree().auto_accept_quit}
604
+
605
+
606
+ ## What a player's close does: the close request reaches every node, and then the root window says
607
+ ## it was asked to close, which the tree answers by quitting when it accepts quits. So the game's own
608
+ ## close runs, a crash guard's included, where a stop that ended the process ran none of it.
609
+ func _deliver_close() -> void:
610
+ var root: Window = get_tree().root
611
+ root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
612
+ root.close_requested.emit()
613
+
614
+
615
+ static func _is_empty(owed: Variant) -> bool:
616
+ var bytes: PackedByteArray = owed
617
+ return bytes.is_empty()
618
+
619
+
581
620
  ## One JSON object and a newline, queued for [method _drain] rather than written here.
582
621
  ##
583
622
  ## Raw bytes rather than put_utf8_string, which would prefix them with a length the other side is