gdharness 1.3.3 → 1.3.4

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 (43) hide show
  1. package/build/cli.js +105 -74
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +3 -2
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  7. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  8. package/build/godot/addons/gdharness_editor/property_values.gd +195 -221
  9. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  10. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  11. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/scene_file.gd +97 -14
  13. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  14. package/build/godot/addons/gdharness_editor/tool_executor.gd +9 -3
  15. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +38 -35
  16. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  17. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  18. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  19. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +68 -2
  20. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +94 -333
  21. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  22. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  23. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  24. package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
  25. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  26. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  27. package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
  28. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  29. package/build/godot/operations/animation_reads.gd +3 -2
  30. package/build/godot/operations/declared_values.gd +157 -4
  31. package/build/godot/operations/dependencies.gd +14 -39
  32. package/build/godot/operations/editor_tools.gd +4 -3
  33. package/build/godot/operations/file_reads.gd +15 -3
  34. package/build/godot/operations/gdscript_analysis.gd +229 -75
  35. package/build/godot/operations/gdscript_authoring.gd +142 -34
  36. package/build/godot/operations/import_pipeline.gd +17 -6
  37. package/build/godot/operations/logger.gd +7 -0
  38. package/build/godot/operations/property_paths.gd +225 -0
  39. package/build/godot/operations/property_values.gd +195 -221
  40. package/build/godot/operations/reading.gd +15 -0
  41. package/build/godot/operations/same_values.gd +177 -0
  42. package/build/godot/operations/serialisation.gd +75 -3
  43. package/package.json +1 -1
@@ -5,11 +5,13 @@ extends Node
5
5
 
6
6
  const GdhFileReads = preload("../file_reads.gd")
7
7
  const GdhRead = preload("../reading.gd")
8
+ const GdhGroups = preload("scene_groups.gd")
8
9
  const GdhKin = preload("scene_kin.gd")
9
10
  const GdhPaths = preload("scene_paths.gd")
10
11
  const GdhPlacing = preload("scene_placing.gd")
11
12
  const GdhPropertyValues = preload("../property_values.gd")
12
13
  const GdhResourceTools = preload("resource_tools.gd")
14
+ const GdhSame = preload("../same_values.gd")
13
15
  const GdhSceneFile = preload("../scene_file.gd")
14
16
  const GdhSerialisation = preload("../serialisation.gd")
15
17
 
@@ -22,56 +24,12 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
22
24
  _editor_plugin = plugin
23
25
 
24
26
 
25
- func _ensure_res_path(path: String) -> String:
26
- if not path.begins_with("res://"):
27
- return "res://" + path
28
- return path
29
-
30
-
31
- func _to_scene_res_path(project_path: String, scene_path: String) -> String:
32
- var p: String = scene_path.strip_edges()
33
- if p.begins_with("res://"):
34
- return p
35
-
36
- if project_path.strip_edges() != "":
37
- var normalized_project: String = project_path.replace("\\", "/")
38
- var normalized_scene: String = p.replace("\\", "/")
39
- if normalized_scene.begins_with(normalized_project):
40
- var rel: String = normalized_scene.substr(normalized_project.length())
41
- if rel.begins_with("/"):
42
- rel = rel.substr(1)
43
- return _ensure_res_path(rel)
44
-
45
- return _ensure_res_path(p)
46
-
47
-
48
27
  ## [param scene_path] opened for a tool, as [scene, refusal]: the refusal is {} when it opened.
49
28
  func _open(scene_path: String, for_writing: bool) -> Array:
50
29
  var scene: GdhSceneFile = GdhSceneFile.new()
51
30
  return [scene, scene.open(scene_path, for_writing)]
52
31
 
53
32
 
54
- func _find_node(root: Node, path: String) -> Node:
55
- if path == "." or path.is_empty():
56
- return root
57
- return root.get_node_or_null(path)
58
-
59
-
60
- ## Why the property or signal [param missing] could not be found on [param node], adding the one
61
- ## cause the name alone does not show: a script that does not compile leaves the editor holding a
62
- ## placeholder with no properties or signals at all, and the refusal named the property instead.
63
- ## Such a script has no engine class beneath it. Compiling a copy of the source to find out failed on
64
- ## every script with a class_name, since the copy hides the global class the original declares.
65
- func _missing_on(node: Node, missing: String) -> String:
66
- var script: Script = node.get_script()
67
- if script == null or not str(script.get_instance_base_type()).is_empty():
68
- return missing
69
- return (
70
- "%s. Its script, %s, does not compile, so none of what it declares is known here."
71
- % [missing.trim_suffix("."), script.resource_path]
72
- )
73
-
74
-
75
33
  ## Whether each of [param names] reads the same off [param saved] as off [param node], answering
76
34
  ## the first that does not, or "".
77
35
  func _kept(scene: GdhSceneFile, saved: Node, node: Node, names: Array) -> String:
@@ -116,7 +74,7 @@ func _properties_of(node: Node, include_defaults: bool, set_here: Dictionary) ->
116
74
  if scripted.has(property)
117
75
  else ClassDB.class_get_property_default_value(node.get_class(), property)
118
76
  )
119
- if GdhPropertyValues.same(value, default):
77
+ if GdhSame.same(value, default):
120
78
  continue
121
79
  props[property] = _readable(node, value)
122
80
  return props
@@ -145,27 +103,20 @@ func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
145
103
  return {}
146
104
 
147
105
 
148
- func _collect_nodes_recursive(node: Node, path: String, out_nodes: Array) -> void:
149
- out_nodes.append({"path": path, "node": node})
150
- for child: Node in node.get_children():
151
- var child_path: String = str(child.name) if path == "." else path + "/" + str(child.name)
152
- _collect_nodes_recursive(child, child_path, out_nodes)
153
-
154
-
155
106
  func _inherited(args: Dictionary) -> Dictionary:
156
107
  var project_path: String = str(args.get("projectPath", ""))
157
108
  var script_path: String = str(args.get("script", ""))
158
109
  return GdhKin.inherited(
159
110
  args,
160
111
  _new_scene_path(args),
161
- _to_scene_res_path(project_path, str(args.get("inherits", ""))),
162
- "" if script_path.is_empty() else _to_scene_res_path(project_path, script_path)
112
+ GdhSceneFile.res_path(project_path, str(args.get("inherits", ""))),
113
+ "" if script_path.is_empty() else GdhSceneFile.res_path(project_path, script_path)
163
114
  )
164
115
 
165
116
 
166
117
  ## The res:// path a create in [param args] writes, given .tscn when it has no extension, or "".
167
118
  func _new_scene_path(args: Dictionary) -> String:
168
- var scene_path: String = _to_scene_res_path(
119
+ var scene_path: String = GdhSceneFile.res_path(
169
120
  str(args.get("projectPath", "")), str(args.get("scenePath", ""))
170
121
  )
171
122
  if scene_path == "res://":
@@ -191,7 +142,7 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
191
142
  var asked_type: String = root_node_type
192
143
  var made: Dictionary = GdhSceneFile.made_as(
193
144
  root_node_type,
194
- "" if script_path.is_empty() else _to_scene_res_path(project_path, script_path),
145
+ "" if script_path.is_empty() else GdhSceneFile.res_path(project_path, script_path),
195
146
  "Node",
196
147
  "rootNodeType"
197
148
  )
@@ -219,7 +170,7 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
219
170
  scene.asked(root, ["script"])
220
171
 
221
172
  if not script_path.is_empty():
222
- var full_script_path: String = _to_scene_res_path(project_path, script_path)
173
+ var full_script_path: String = GdhSceneFile.res_path(project_path, script_path)
223
174
  var script: Script = load(full_script_path) as Script
224
175
  if script == null:
225
176
  root.free()
@@ -260,7 +211,7 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
260
211
 
261
212
  func list_scene_nodes(args: Dictionary) -> Dictionary:
262
213
  var project_path: String = str(args.get("projectPath", ""))
263
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
214
+ var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
264
215
  var depth: int = GdhRead.as_int(args.get("depth", -1), -1)
265
216
  var include_properties: bool = GdhRead.as_bool(args.get("includeProperties", false))
266
217
 
@@ -274,12 +225,13 @@ func list_scene_nodes(args: Dictionary) -> Dictionary:
274
225
 
275
226
  func add_node(args: Dictionary) -> Dictionary:
276
227
  var project_path: String = str(args.get("projectPath", ""))
277
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
228
+ var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
278
229
  var node_type: String = str(args.get("nodeType", ""))
279
230
  var instance_path: String = str(args.get("instancePath", ""))
280
231
  var node_name: String = str(args.get("nodeName", ""))
281
232
  var parent_node_path: String = str(args.get("parentNodePath", "."))
282
233
  var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
234
+ var adding: Array = GdhGroups.asked(args, "addGroups")
283
235
 
284
236
  if node_name.is_empty():
285
237
  return {"ok": false, "error": "Missing nodeName"}
@@ -305,7 +257,7 @@ func add_node(args: Dictionary) -> Dictionary:
305
257
  return refused
306
258
 
307
259
  var root: Node = scene.root
308
- var parent: Node = _find_node(root, parent_node_path)
260
+ var parent: Node = GdhSceneFile.node_at(root, parent_node_path)
309
261
  if not parent:
310
262
  return scene.refuse(scene.not_found("Parent node not found: " + parent_node_path, parent_node_path))
311
263
  var foreign: String = scene.foreign(parent)
@@ -332,9 +284,11 @@ func add_node(args: Dictionary) -> Dictionary:
332
284
  if not unsettled.is_empty():
333
285
  return scene.refuse("Nothing was written: " + unsettled)
334
286
  scene.asked(new_node, properties.keys())
335
- var refused_property: String = _properties.write_all(new_node, properties)
336
- if not refused_property.is_empty():
337
- return scene.refuse(refused_property)
287
+ var unwritten: String = _properties.write_all(new_node, properties)
288
+ if unwritten.is_empty():
289
+ unwritten = GdhGroups.change(scene, new_node, adding, [])
290
+ if not unwritten.is_empty():
291
+ return scene.refuse(unwritten)
338
292
 
339
293
  var new_path: String = scene.path_of(new_node)
340
294
  # Read before the write, which frees the scene it was added to.
@@ -346,11 +300,16 @@ func add_node(args: Dictionary) -> Dictionary:
346
300
  "index": new_node.get_index(),
347
301
  }
348
302
  _properties.note_write(answer, new_node)
303
+ if not adding.is_empty():
304
+ answer.merge(GdhGroups.read(scene, new_node))
349
305
  var written: Dictionary = scene.write(
350
306
  func(saved: Node) -> String:
351
- return _kept(
307
+ var unkept: String = _kept(
352
308
  scene, saved, new_node, properties.keys() + (["script"] if class_script != null else [])
353
309
  )
310
+ if unkept.is_empty() and saved.get_node_or_null(new_path) != null:
311
+ unkept = GdhGroups.unkept(saved.get_node(new_path), adding, [])
312
+ return unkept
354
313
  )
355
314
  if not written.is_empty():
356
315
  return written
@@ -362,7 +321,7 @@ func add_node(args: Dictionary) -> Dictionary:
362
321
 
363
322
  func delete_node(args: Dictionary) -> Dictionary:
364
323
  var project_path: String = str(args.get("projectPath", ""))
365
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
324
+ var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
366
325
  var node_path: String = str(args.get("nodePath", ""))
367
326
 
368
327
  if node_path.is_empty() or node_path == ".":
@@ -381,14 +340,18 @@ func delete_node(args: Dictionary) -> Dictionary:
381
340
  if not refused.is_empty():
382
341
  return refused
383
342
 
384
- var node: Node = _find_node(scene.root, node_path)
343
+ var node: Node = GdhSceneFile.node_at(scene.root, node_path)
385
344
  if not node:
386
345
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
387
- var fixed: String = scene.fixed(node)
346
+ var fixed: String = scene.fixed(node, true)
388
347
  if not fixed.is_empty():
389
348
  return scene.refuse(fixed)
390
349
 
391
350
  var at: String = scene.path_of(node)
351
+ var went: Array[String] = []
352
+ for under: Node in node.find_children("*", "", true, false):
353
+ if under.owner == scene.root:
354
+ went.append(scene.path_of(under))
392
355
  node.get_parent().remove_child(node)
393
356
  node.free()
394
357
  var carried: GdhPaths = GdhPaths.new()
@@ -404,13 +367,15 @@ func delete_node(args: Dictionary) -> Dictionary:
404
367
  return written
405
368
 
406
369
  var answer: Dictionary = {"ok": true, "deletedNodePath": at}
370
+ if not went.is_empty():
371
+ answer["deletedWithIt"] = went
407
372
  answer.merge(GdhKin.follow(scene_path, at, "", carried))
408
373
  return answer
409
374
 
410
375
 
411
376
  func duplicate_node(args: Dictionary) -> Dictionary:
412
377
  var project_path: String = str(args.get("projectPath", ""))
413
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
378
+ var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
414
379
  var node_path: String = str(args.get("nodePath", ""))
415
380
  var new_name: String = str(args.get("newName", ""))
416
381
  var parent_path: String = str(args.get("parentNodePath", ""))
@@ -425,7 +390,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
425
390
  return refused
426
391
 
427
392
  var root: Node = scene.root
428
- var source: Node = _find_node(root, node_path)
393
+ var source: Node = GdhSceneFile.node_at(root, node_path)
429
394
  if not source:
430
395
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
431
396
  if source == root:
@@ -436,7 +401,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
436
401
 
437
402
  var target_parent: Node = source.get_parent()
438
403
  if not parent_path.is_empty():
439
- target_parent = _find_node(root, parent_path)
404
+ target_parent = GdhSceneFile.node_at(root, parent_path)
440
405
  if not target_parent:
441
406
  return scene.refuse(scene.not_found("Parent not found: " + parent_path, parent_path))
442
407
  foreign = scene.foreign(target_parent)
@@ -445,6 +410,13 @@ func duplicate_node(args: Dictionary) -> Dictionary:
445
410
  var unusable: String = scene.unusable_name(target_parent, new_name)
446
411
  if not unusable.is_empty():
447
412
  return scene.refuse(unusable)
413
+ var place: Variant = args.get("index")
414
+ var unplaceable: String = GdhPlacing.unplaceable(target_parent, null, place)
415
+ if not unplaceable.is_empty():
416
+ return scene.refuse(unplaceable)
417
+ # The editor's Duplicate puts the copy right after the original.
418
+ if place == null and target_parent == source.get_parent():
419
+ place = source.get_index() + 1
448
420
 
449
421
  var copies: Dictionary = {}
450
422
  var duplicated_node: Node = GdhPlacing.copied(source, root, copies)
@@ -453,6 +425,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
453
425
 
454
426
  duplicated_node.name = new_name
455
427
  target_parent.add_child(duplicated_node)
428
+ GdhPlacing.place(duplicated_node, place)
456
429
  GdhPlacing.own(duplicated_node, root)
457
430
  GdhPlacing.reconnect(copies, source)
458
431
  scene.copied(copies)
@@ -461,8 +434,11 @@ func duplicate_node(args: Dictionary) -> Dictionary:
461
434
  var placed: Array = [scene.path_of(target_parent), duplicated_node.get_index()]
462
435
  var written: Dictionary = scene.write(
463
436
  func(saved: Node) -> String:
464
- if saved.get_node_or_null(new_path) == null:
437
+ var copy: Node = saved.get_node_or_null(new_path)
438
+ if copy == null:
465
439
  return "the copy would not be in the scene when it loads."
440
+ if copy.get_index() != placed[1]:
441
+ return "%s would load at index %d, not %d." % [new_path, copy.get_index(), placed[1]]
466
442
  return ""
467
443
  )
468
444
  if not written.is_empty():
@@ -480,7 +456,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
480
456
 
481
457
  func reparent_node(args: Dictionary) -> Dictionary:
482
458
  var project_path: String = str(args.get("projectPath", ""))
483
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
459
+ var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
484
460
  var node_path: String = str(args.get("nodePath", ""))
485
461
  var new_parent_path: String = str(args.get("newParentPath", ""))
486
462
 
@@ -496,8 +472,8 @@ func reparent_node(args: Dictionary) -> Dictionary:
496
472
  return refused
497
473
 
498
474
  var root: Node = scene.root
499
- var node: Node = _find_node(root, node_path)
500
- var new_parent: Node = _find_node(root, new_parent_path)
475
+ var node: Node = GdhSceneFile.node_at(root, node_path)
476
+ var new_parent: Node = GdhSceneFile.node_at(root, new_parent_path)
501
477
  if not node:
502
478
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
503
479
  if not new_parent:
@@ -523,13 +499,24 @@ func reparent_node(args: Dictionary) -> Dictionary:
523
499
  return scene.refuse(unplaceable)
524
500
 
525
501
  var old_path: String = scene.path_of(node)
502
+ var old_parent: Node = node.get_parent()
503
+ var drawn_at: Dictionary = {}
504
+ for property: String in GdhPlacing.placing(node):
505
+ drawn_at[property] = _properties.read_at(node, property)
526
506
  # Unowned while it moves: a node taken out of the tree keeps its owner, and adding it under the new
527
507
  # parent warned that the owner was not above it. Node.reparent warns the same way.
528
508
  node.owner = null
529
- node.get_parent().remove_child(node)
509
+ old_parent.remove_child(node)
530
510
  new_parent.add_child(node)
531
511
  GdhPlacing.place(node, place)
532
512
  GdhPlacing.own(node, root)
513
+ if GdhRead.as_bool(args.get("keepGlobalTransform", true)):
514
+ GdhPlacing.keep_in_place(node, old_parent)
515
+ var also_changed: Dictionary = {}
516
+ for property: String in drawn_at:
517
+ var now: Variant = _properties.read_at(node, property)
518
+ if not GdhSame.same(now, drawn_at[property]):
519
+ also_changed[property] = {"was": _readable(node, drawn_at[property]), "now": _readable(node, now)}
533
520
 
534
521
  var new_path: String = scene.path_of(node)
535
522
  var index: int = node.get_index()
@@ -544,7 +531,7 @@ func reparent_node(args: Dictionary) -> Dictionary:
544
531
  return "%s would still be in the scene as well when it loads." % old_path
545
532
  if copy.get_index() != index:
546
533
  return "%s would load at index %d, not %d." % [new_path, copy.get_index(), index]
547
- return ""
534
+ return _kept(scene, saved, node, also_changed.keys())
548
535
  )
549
536
  if not written.is_empty():
550
537
  carried.put_back(scene_path)
@@ -557,6 +544,8 @@ func reparent_node(args: Dictionary) -> Dictionary:
557
544
  "newNodePath": new_path,
558
545
  "index": index,
559
546
  }
547
+ if not also_changed.is_empty():
548
+ answer["alsoChanged"] = also_changed
560
549
  if new_path != old_path:
561
550
  answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
562
551
  return answer
@@ -564,12 +553,14 @@ func reparent_node(args: Dictionary) -> Dictionary:
564
553
 
565
554
  func set_node_properties(args: Dictionary) -> Dictionary:
566
555
  var project_path: String = str(args.get("projectPath", ""))
567
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
556
+ var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
568
557
  var node_path: String = str(args.get("nodePath", "."))
569
558
  var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
570
559
  var editable: Variant = args.get("editableChildren")
571
- if properties.is_empty() and editable == null:
572
- return {"ok": false, "error": "set takes properties, editableChildren, or both."}
560
+ var adding: Array = GdhGroups.asked(args, "addGroups")
561
+ var removing: Array = GdhGroups.asked(args, "removeGroups")
562
+ if properties.is_empty() and editable == null and adding.is_empty() and removing.is_empty():
563
+ return {"ok": false, "error": "set takes properties, editableChildren, addGroups or removeGroups."}
573
564
 
574
565
  var opened: Array = _open(scene_path, true)
575
566
  var scene: GdhSceneFile = opened[0]
@@ -577,7 +568,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
577
568
  if not refused.is_empty():
578
569
  return refused
579
570
 
580
- var node: Node = _find_node(scene.root, node_path)
571
+ var node: Node = GdhSceneFile.node_at(scene.root, node_path)
581
572
  if not node:
582
573
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
583
574
  var foreign: String = scene.foreign(node)
@@ -603,7 +594,10 @@ func set_node_properties(args: Dictionary) -> Dictionary:
603
594
  scene.asked(node, properties.keys())
604
595
  var refused_property: String = _properties.write_all(node, properties)
605
596
  if not refused_property.is_empty():
606
- return scene.refuse(_missing_on(node, refused_property))
597
+ return scene.refuse(GdhSceneFile.missing_on(node, refused_property))
598
+ var ungrouped: String = GdhGroups.change(scene, node, adding, removing)
599
+ if not ungrouped.is_empty():
600
+ return scene.refuse(ungrouped)
607
601
 
608
602
  var held: Dictionary = {}
609
603
  for key: Variant in properties:
@@ -611,11 +605,15 @@ func set_node_properties(args: Dictionary) -> Dictionary:
611
605
  var new_path: String = scene.path_of(node)
612
606
  var answer: Dictionary = {"ok": true, "nodePath": new_path, "properties": held}
613
607
  _properties.note_write(answer, node)
608
+ if not adding.is_empty() or not removing.is_empty():
609
+ answer["groups"] = GdhGroups.read(scene, node).get("groups", [])
614
610
  var carried: GdhPaths = GdhPaths.new()
615
611
  var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
616
612
  var written: Dictionary = scene.write(
617
613
  func(saved: Node) -> String:
618
614
  var unkept: String = _kept(scene, saved, node, properties.keys())
615
+ if unkept.is_empty():
616
+ unkept = GdhGroups.unkept(saved.get_node(new_path), adding, removing)
619
617
  if unkept.is_empty() and editable != null:
620
618
  if saved.is_editable_instance(saved.get_node(new_path)) != GdhRead.as_bool(editable):
621
619
  return (
@@ -639,7 +637,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
639
637
 
640
638
  func get_node_properties(args: Dictionary) -> Dictionary:
641
639
  var project_path: String = str(args.get("projectPath", ""))
642
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
640
+ var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
643
641
  var node_path: String = str(args.get("nodePath", "."))
644
642
  var include_defaults: bool = GdhRead.as_bool(args.get("includeDefaults", false))
645
643
 
@@ -649,7 +647,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
649
647
  if not refused.is_empty():
650
648
  return refused
651
649
 
652
- var node: Node = _find_node(scene.root, node_path)
650
+ var node: Node = GdhSceneFile.node_at(scene.root, node_path)
653
651
  if not node:
654
652
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
655
653
 
@@ -657,9 +655,18 @@ func get_node_properties(args: Dictionary) -> Dictionary:
657
655
  # declares, the class's for the rest. Compared with a bare instance of the class, every exported
658
656
  # variable read as changed; and a value this scene sets back to the default was hidden, so the
659
657
  # names this scene's file sets are always answered.
660
- var props: Dictionary = _properties_of(node, include_defaults, scene.set_here(node))
658
+ var set_here: Dictionary = scene.set_here(node)
659
+ var props: Dictionary = _properties_of(node, include_defaults, set_here)
661
660
  var cells: Dictionary = GdhResourceTools.cells_of(node)
662
- var answer: Dictionary = {"ok": true, "nodePath": node_path, "properties": props}
661
+ var at: String = scene.path_of(node)
662
+ var here: Array = set_here.keys()
663
+ here.sort()
664
+ var answer: Dictionary = {"ok": true, "nodePath": at, "properties": props, "setHere": here}
665
+ answer.merge(GdhSceneFile.class_of(node))
666
+ answer.merge(GdhGroups.read(scene, node))
667
+ var from: String = scene.values_from(node, at)
668
+ if not from.is_empty():
669
+ answer["valuesFrom"] = from
663
670
  if node != scene.root and node.owner != null and not node.scene_file_path.is_empty():
664
671
  answer["instanceOf"] = node.scene_file_path
665
672
  answer["editableChildren"] = scene.root.is_editable_instance(node)
@@ -679,11 +686,11 @@ func get_node_properties(args: Dictionary) -> Dictionary:
679
686
 
680
687
  func save_scene(args: Dictionary) -> Dictionary:
681
688
  var project_path: String = str(args.get("projectPath", ""))
682
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
689
+ var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
683
690
  var new_path_raw: String = str(args.get("newPath", ""))
684
691
  var target_path: String = scene_path
685
692
  if not new_path_raw.is_empty():
686
- target_path = _to_scene_res_path(project_path, new_path_raw)
693
+ target_path = GdhSceneFile.res_path(project_path, new_path_raw)
687
694
 
688
695
  if target_path != scene_path:
689
696
  var unsaved: String = GdhSceneFile.unsaved_in_editor(target_path)
@@ -751,249 +758,3 @@ func _save_open_scene(scene_path: String) -> Dictionary:
751
758
  "from": "editor",
752
759
  "unsavedInEditor": GdhSceneFile.unsaved(scene_path)
753
760
  }
754
-
755
-
756
- func connect_signal(args: Dictionary) -> Dictionary:
757
- var project_path: String = str(args.get("projectPath", ""))
758
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
759
- var source_node_path: String = str(args.get("sourceNodePath", ""))
760
- var signal_name: String = str(args.get("signalName", ""))
761
- var target_node_path: String = str(args.get("targetNodePath", ""))
762
- var method_name: String = str(args.get("methodName", ""))
763
- # A connection without CONNECT_PERSIST is a runtime one, and PackedScene.pack drops those on
764
- # the way out: without this the scene saves unchanged and this answers success over nothing.
765
- var flags: int = GdhRead.as_int(args.get("flags", 0)) | Object.CONNECT_PERSIST
766
-
767
- if (
768
- source_node_path.is_empty()
769
- or signal_name.is_empty()
770
- or target_node_path.is_empty()
771
- or method_name.is_empty()
772
- ):
773
- return {"ok": false, "error": "Missing required signal connection arguments"}
774
-
775
- var opened: Array = _open(scene_path, true)
776
- var scene: GdhSceneFile = opened[0]
777
- var refused: Dictionary = opened[1]
778
- if not refused.is_empty():
779
- return refused
780
-
781
- var ends: Array = _connection_ends(scene, source_node_path, signal_name, target_node_path)
782
- if ends[0] == null:
783
- return scene.refuse(str(ends[1]))
784
- var source: Node = ends[0]
785
- var target: Node = ends[1]
786
-
787
- # Reconnected rather than left alone when the flags differ, so the connection saved is the one
788
- # asked for. Left alone, the answer gave the flags asked for and the file kept the old ones.
789
- var callable: Callable = Callable(target, method_name)
790
- var method_found: bool = target.has_method(method_name)
791
- var flags_before: int = -1
792
- for connection: Dictionary in source.get_signal_connection_list(signal_name):
793
- if connection.get("callable") == callable:
794
- flags_before = GdhRead.as_int(connection.get("flags", 0))
795
- if source.is_connected(signal_name, callable):
796
- source.disconnect(signal_name, callable)
797
- var connect_result: Error = source.connect(signal_name, callable, flags)
798
- if connect_result != OK:
799
- return scene.refuse("Failed to connect signal: " + error_string(connect_result))
800
-
801
- var from: String = scene.path_of(source)
802
- var to: String = scene.path_of(target)
803
- var saved_flags: Array = [-1]
804
- var written: Dictionary = scene.write(
805
- func(saved: Node) -> String:
806
- saved_flags[0] = _connection_flags(saved, from, signal_name, to, method_name)
807
- if saved_flags[0] < 0:
808
- return "the connection would not be in the scene when it loads."
809
- return ""
810
- )
811
- if not written.is_empty():
812
- return written
813
-
814
- var answer: Dictionary = {
815
- "ok": true,
816
- "sourceNodePath": from,
817
- "signalName": signal_name,
818
- "targetNodePath": to,
819
- "methodName": method_name,
820
- "flags": saved_flags[0],
821
- "flagNames": GdhSceneFile.flag_names(GdhRead.as_int(saved_flags[0])),
822
- # Connected either way, as the editor's own dialog allows, since the method may be written
823
- # next; a caller told nothing learnt of a misspelt one only when the signal fired.
824
- "methodFound": method_found
825
- }
826
- # A second connect of the same pair read exactly like the first, so a caller could not tell a
827
- # connection made from one that was already there, and a list showing one where two were made
828
- # looked like a lost write.
829
- if flags_before >= 0:
830
- answer["alreadyConnected"] = true
831
- if flags_before != GdhRead.as_int(saved_flags[0]):
832
- answer["flagsBefore"] = GdhSceneFile.flag_names(flags_before)
833
- return answer
834
-
835
-
836
- ## The two ends of a connection in [param scene], as [source, target], or [null, why not].
837
- func _connection_ends(
838
- scene: GdhSceneFile, source_node_path: String, signal_name: String, target_node_path: String
839
- ) -> Array:
840
- var source: Node = _find_node(scene.root, source_node_path)
841
- var target: Node = _find_node(scene.root, target_node_path)
842
- if not source:
843
- return [null, scene.not_found("Source node not found: " + source_node_path, source_node_path)]
844
- if not target:
845
- return [null, scene.not_found("Target node not found: " + target_node_path, target_node_path)]
846
- # pack drops a connection with either end inside an instanced scene, and the answer said it was
847
- # made or removed.
848
- for end: Node in [source, target]:
849
- var foreign: String = scene.foreign(end)
850
- if not foreign.is_empty():
851
- return [null, foreign]
852
- if not source.has_signal(signal_name):
853
- var said: String = "%s has no signal %s" % [source_node_path, signal_name]
854
- return [null, _missing_on(source, GdhRead.offered(said, signal_name, GdhRead.signals_of(source)))]
855
- return [source, target]
856
-
857
-
858
- ## The flags of the persistent connection from [param from]'s [param signal_name] to
859
- ## [param method] on [param to] in the tree under [param root], or -1 when there is none.
860
- func _connection_flags(root: Node, from: String, signal_name: String, to: String, method: String) -> int:
861
- var source: Node = root.get_node_or_null(from)
862
- var target: Node = root.get_node_or_null(to)
863
- if source == null or target == null or not source.has_signal(signal_name):
864
- return -1
865
- for conn: Dictionary in source.get_signal_connection_list(signal_name):
866
- var flags: int = GdhRead.as_int(conn.get("flags", 0))
867
- var callable: Callable = conn.get("callable", Callable())
868
- if (
869
- flags & Object.CONNECT_PERSIST
870
- and callable.get_object() == target
871
- and str(callable.get_method()) == method
872
- ):
873
- return flags
874
- return -1
875
-
876
-
877
- func disconnect_signal(args: Dictionary) -> Dictionary:
878
- var project_path: String = str(args.get("projectPath", ""))
879
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
880
- var source_node_path: String = str(args.get("sourceNodePath", ""))
881
- var signal_name: String = str(args.get("signalName", ""))
882
- var target_node_path: String = str(args.get("targetNodePath", ""))
883
- var method_name: String = str(args.get("methodName", ""))
884
-
885
- if (
886
- source_node_path.is_empty()
887
- or signal_name.is_empty()
888
- or target_node_path.is_empty()
889
- or method_name.is_empty()
890
- ):
891
- return {"ok": false, "error": "Missing required signal disconnection arguments"}
892
-
893
- var opened: Array = _open(scene_path, true)
894
- var scene: GdhSceneFile = opened[0]
895
- var refused: Dictionary = opened[1]
896
- if not refused.is_empty():
897
- return refused
898
-
899
- var ends: Array = _connection_ends(scene, source_node_path, signal_name, target_node_path)
900
- if ends[0] == null:
901
- return scene.refuse(str(ends[1]))
902
- var source: Node = ends[0]
903
- var target: Node = ends[1]
904
-
905
- var from: String = scene.path_of(source)
906
- var to: String = scene.path_of(target)
907
- # A connection that was never there saved the scene unchanged and answered as removed, so a
908
- # typo in either name read as success.
909
- if _connection_flags(scene.root, from, signal_name, to, method_name) < 0:
910
- return scene.refuse(
911
- "%s has no connection from %s.%s to %s.%s." % [scene_path, from, signal_name, to, method_name]
912
- )
913
- source.disconnect(signal_name, Callable(target, method_name))
914
-
915
- var written: Dictionary = scene.write(
916
- func(saved: Node) -> String:
917
- if _connection_flags(saved, from, signal_name, to, method_name) >= 0:
918
- return "the connection would still be in the scene when it loads."
919
- return ""
920
- )
921
- if not written.is_empty():
922
- return written
923
-
924
- return {
925
- "ok": true,
926
- "removed": true,
927
- "sourceNodePath": from,
928
- "signalName": signal_name,
929
- "targetNodePath": to,
930
- "methodName": method_name
931
- }
932
-
933
-
934
- func list_connections(args: Dictionary) -> Dictionary:
935
- var project_path: String = str(args.get("projectPath", ""))
936
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
937
- var filter_path: String = str(args.get("nodePath", ""))
938
-
939
- var opened: Array = _open(scene_path, false)
940
- var scene: GdhSceneFile = opened[0]
941
- var refused: Dictionary = opened[1]
942
- if not refused.is_empty():
943
- return refused
944
-
945
- var root: Node = scene.root
946
- var filtered: Node = null
947
- if not filter_path.is_empty():
948
- filtered = _find_node(root, filter_path)
949
- if filtered == null:
950
- return scene.refuse(scene.not_found("Node not found: " + filter_path, filter_path))
951
- var nodes: Array = []
952
- _collect_nodes_recursive(root, ".", nodes)
953
-
954
- # Only persistent connections, which are the ones a scene file holds: a Container connects
955
- # signals of each child to itself as the child is added, and those were answered as if the file
956
- # held them. Matched on either end, since the filter is for connections involving the node.
957
- var connections: Array = []
958
- for entry: Dictionary in nodes:
959
- var path: String = str(entry["path"])
960
- var node: Node = entry["node"]
961
- for signal_info: Dictionary in node.get_signal_list():
962
- var signal_name: String = str(signal_info.get("name", ""))
963
- if signal_name.is_empty():
964
- continue
965
- for conn: Dictionary in node.get_signal_connection_list(signal_name):
966
- var flags: int = GdhRead.as_int(conn.get("flags", 0))
967
- if not flags & Object.CONNECT_PERSIST:
968
- continue
969
- var callable: Callable = conn.get("callable", Callable())
970
- var target_obj: Object = callable.get_object()
971
- if filtered != null and node != filtered and target_obj != filtered:
972
- continue
973
- var target_path: String = ""
974
- var editable: bool = scene.foreign(node).is_empty()
975
- if target_obj is Node:
976
- target_path = str(root.get_path_to(target_obj as Node))
977
- editable = editable and scene.foreign(target_obj as Node).is_empty()
978
- (
979
- connections
980
- . append(
981
- {
982
- "sourceNodePath": path,
983
- "signalName": signal_name,
984
- "targetNodePath": target_path,
985
- "methodName": str(callable.get_method()),
986
- "flags": flags,
987
- "flagNames": GdhSceneFile.flag_names(flags),
988
- # False for a connection an instanced scene makes inside itself, which only that
989
- # scene can change.
990
- "editable": editable
991
- }
992
- )
993
- )
994
-
995
- scene.close()
996
- var listed: Dictionary = {"ok": true, "connections": connections}
997
- if Engine.is_editor_hint():
998
- listed["unsavedInEditor"] = GdhSceneFile.unsaved(scene_path)
999
- return listed