gdharness 1.3.2 → 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 (46) hide show
  1. package/build/cli.js +163 -85
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -11
  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/file_walk.gd +134 -0
  6. package/build/godot/addons/gdharness_editor/file_walk.gd.uid +1 -0
  7. package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
  8. package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
  9. package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
  10. package/build/godot/addons/gdharness_editor/property_values.gd +249 -227
  11. package/build/godot/addons/gdharness_editor/reading.gd +15 -0
  12. package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
  13. package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
  14. package/build/godot/addons/gdharness_editor/scene_file.gd +100 -15
  15. package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
  16. package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -3
  17. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +402 -55
  18. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
  19. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
  20. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +88 -5
  22. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -337
  23. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
  24. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
  25. package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
  26. package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
  27. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
  28. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
  29. package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
  30. package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
  31. package/build/godot/operations/animation_reads.gd +14 -11
  32. package/build/godot/operations/declared_values.gd +157 -4
  33. package/build/godot/operations/dependencies.gd +14 -39
  34. package/build/godot/operations/editor_tools.gd +7 -3
  35. package/build/godot/operations/file_reads.gd +15 -3
  36. package/build/godot/operations/file_walk.gd +1 -0
  37. package/build/godot/operations/gdscript_analysis.gd +229 -75
  38. package/build/godot/operations/gdscript_authoring.gd +142 -34
  39. package/build/godot/operations/import_pipeline.gd +17 -6
  40. package/build/godot/operations/logger.gd +7 -0
  41. package/build/godot/operations/property_paths.gd +225 -0
  42. package/build/godot/operations/property_values.gd +249 -227
  43. package/build/godot/operations/reading.gd +15 -0
  44. package/build/godot/operations/same_values.gd +177 -0
  45. package/build/godot/operations/serialisation.gd +75 -3
  46. 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
  )
@@ -202,9 +153,7 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
202
153
  # A class that is not a node went into a Node variable, which is a runtime error the caller
203
154
  # heard about only as "Invalid tool result".
204
155
  if not ClassDB.class_exists(root_node_type) or not ClassDB.is_parent_class(root_node_type, "Node"):
205
- return {
206
- "ok": false, "error": "rootNodeType must be a node class, and %s is not one." % root_node_type
207
- }
156
+ return {"ok": false, "error": GdhPlacing.not_a_node_class("rootNodeType", root_node_type)}
208
157
  if not ClassDB.can_instantiate(root_node_type):
209
158
  return {"ok": false, "error": "%s cannot be instantiated." % root_node_type}
210
159
  var unfit: String = GdhSceneFile.unfit_name(root_node_name)
@@ -221,7 +170,7 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
221
170
  scene.asked(root, ["script"])
222
171
 
223
172
  if not script_path.is_empty():
224
- 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)
225
174
  var script: Script = load(full_script_path) as Script
226
175
  if script == null:
227
176
  root.free()
@@ -243,6 +192,7 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
243
192
  # name is no property the write compares.
244
193
  root.name = root_node_name
245
194
 
195
+ var had_folder: bool = DirAccess.dir_exists_absolute(scene_path.get_base_dir())
246
196
  var folder: String = GdhSceneFile.make_folder_for(scene_path)
247
197
  if not folder.is_empty():
248
198
  root.free()
@@ -251,12 +201,17 @@ func _create_of_a_class(args: Dictionary) -> Dictionary:
251
201
  if not written.is_empty():
252
202
  return written
253
203
 
254
- return {"ok": true, "scenePath": scene_path, "rootNodeType": asked_type, "rootNodeName": root_node_name}
204
+ var answer: Dictionary = {
205
+ "ok": true, "scenePath": scene_path, "rootNodeType": asked_type, "rootNodeName": root_node_name
206
+ }
207
+ if not had_folder:
208
+ answer["madeFolder"] = scene_path.get_base_dir()
209
+ return answer
255
210
 
256
211
 
257
212
  func list_scene_nodes(args: Dictionary) -> Dictionary:
258
213
  var project_path: String = str(args.get("projectPath", ""))
259
- 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", "")))
260
215
  var depth: int = GdhRead.as_int(args.get("depth", -1), -1)
261
216
  var include_properties: bool = GdhRead.as_bool(args.get("includeProperties", false))
262
217
 
@@ -270,12 +225,13 @@ func list_scene_nodes(args: Dictionary) -> Dictionary:
270
225
 
271
226
  func add_node(args: Dictionary) -> Dictionary:
272
227
  var project_path: String = str(args.get("projectPath", ""))
273
- 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", "")))
274
229
  var node_type: String = str(args.get("nodeType", ""))
275
230
  var instance_path: String = str(args.get("instancePath", ""))
276
231
  var node_name: String = str(args.get("nodeName", ""))
277
232
  var parent_node_path: String = str(args.get("parentNodePath", "."))
278
233
  var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
234
+ var adding: Array = GdhGroups.asked(args, "addGroups")
279
235
 
280
236
  if node_name.is_empty():
281
237
  return {"ok": false, "error": "Missing nodeName"}
@@ -301,7 +257,7 @@ func add_node(args: Dictionary) -> Dictionary:
301
257
  return refused
302
258
 
303
259
  var root: Node = scene.root
304
- var parent: Node = _find_node(root, parent_node_path)
260
+ var parent: Node = GdhSceneFile.node_at(root, parent_node_path)
305
261
  if not parent:
306
262
  return scene.refuse(scene.not_found("Parent node not found: " + parent_node_path, parent_node_path))
307
263
  var foreign: String = scene.foreign(parent)
@@ -328,9 +284,11 @@ func add_node(args: Dictionary) -> Dictionary:
328
284
  if not unsettled.is_empty():
329
285
  return scene.refuse("Nothing was written: " + unsettled)
330
286
  scene.asked(new_node, properties.keys())
331
- var refused_property: String = _properties.write_all(new_node, properties)
332
- if not refused_property.is_empty():
333
- 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)
334
292
 
335
293
  var new_path: String = scene.path_of(new_node)
336
294
  # Read before the write, which frees the scene it was added to.
@@ -342,11 +300,16 @@ func add_node(args: Dictionary) -> Dictionary:
342
300
  "index": new_node.get_index(),
343
301
  }
344
302
  _properties.note_write(answer, new_node)
303
+ if not adding.is_empty():
304
+ answer.merge(GdhGroups.read(scene, new_node))
345
305
  var written: Dictionary = scene.write(
346
306
  func(saved: Node) -> String:
347
- return _kept(
307
+ var unkept: String = _kept(
348
308
  scene, saved, new_node, properties.keys() + (["script"] if class_script != null else [])
349
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
350
313
  )
351
314
  if not written.is_empty():
352
315
  return written
@@ -358,7 +321,7 @@ func add_node(args: Dictionary) -> Dictionary:
358
321
 
359
322
  func delete_node(args: Dictionary) -> Dictionary:
360
323
  var project_path: String = str(args.get("projectPath", ""))
361
- 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", "")))
362
325
  var node_path: String = str(args.get("nodePath", ""))
363
326
 
364
327
  if node_path.is_empty() or node_path == ".":
@@ -377,14 +340,18 @@ func delete_node(args: Dictionary) -> Dictionary:
377
340
  if not refused.is_empty():
378
341
  return refused
379
342
 
380
- var node: Node = _find_node(scene.root, node_path)
343
+ var node: Node = GdhSceneFile.node_at(scene.root, node_path)
381
344
  if not node:
382
345
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
383
- var fixed: String = scene.fixed(node)
346
+ var fixed: String = scene.fixed(node, true)
384
347
  if not fixed.is_empty():
385
348
  return scene.refuse(fixed)
386
349
 
387
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))
388
355
  node.get_parent().remove_child(node)
389
356
  node.free()
390
357
  var carried: GdhPaths = GdhPaths.new()
@@ -400,13 +367,15 @@ func delete_node(args: Dictionary) -> Dictionary:
400
367
  return written
401
368
 
402
369
  var answer: Dictionary = {"ok": true, "deletedNodePath": at}
370
+ if not went.is_empty():
371
+ answer["deletedWithIt"] = went
403
372
  answer.merge(GdhKin.follow(scene_path, at, "", carried))
404
373
  return answer
405
374
 
406
375
 
407
376
  func duplicate_node(args: Dictionary) -> Dictionary:
408
377
  var project_path: String = str(args.get("projectPath", ""))
409
- 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", "")))
410
379
  var node_path: String = str(args.get("nodePath", ""))
411
380
  var new_name: String = str(args.get("newName", ""))
412
381
  var parent_path: String = str(args.get("parentNodePath", ""))
@@ -421,7 +390,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
421
390
  return refused
422
391
 
423
392
  var root: Node = scene.root
424
- var source: Node = _find_node(root, node_path)
393
+ var source: Node = GdhSceneFile.node_at(root, node_path)
425
394
  if not source:
426
395
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
427
396
  if source == root:
@@ -432,7 +401,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
432
401
 
433
402
  var target_parent: Node = source.get_parent()
434
403
  if not parent_path.is_empty():
435
- target_parent = _find_node(root, parent_path)
404
+ target_parent = GdhSceneFile.node_at(root, parent_path)
436
405
  if not target_parent:
437
406
  return scene.refuse(scene.not_found("Parent not found: " + parent_path, parent_path))
438
407
  foreign = scene.foreign(target_parent)
@@ -441,6 +410,13 @@ func duplicate_node(args: Dictionary) -> Dictionary:
441
410
  var unusable: String = scene.unusable_name(target_parent, new_name)
442
411
  if not unusable.is_empty():
443
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
444
420
 
445
421
  var copies: Dictionary = {}
446
422
  var duplicated_node: Node = GdhPlacing.copied(source, root, copies)
@@ -449,6 +425,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
449
425
 
450
426
  duplicated_node.name = new_name
451
427
  target_parent.add_child(duplicated_node)
428
+ GdhPlacing.place(duplicated_node, place)
452
429
  GdhPlacing.own(duplicated_node, root)
453
430
  GdhPlacing.reconnect(copies, source)
454
431
  scene.copied(copies)
@@ -457,8 +434,11 @@ func duplicate_node(args: Dictionary) -> Dictionary:
457
434
  var placed: Array = [scene.path_of(target_parent), duplicated_node.get_index()]
458
435
  var written: Dictionary = scene.write(
459
436
  func(saved: Node) -> String:
460
- if saved.get_node_or_null(new_path) == null:
437
+ var copy: Node = saved.get_node_or_null(new_path)
438
+ if copy == null:
461
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]]
462
442
  return ""
463
443
  )
464
444
  if not written.is_empty():
@@ -476,7 +456,7 @@ func duplicate_node(args: Dictionary) -> Dictionary:
476
456
 
477
457
  func reparent_node(args: Dictionary) -> Dictionary:
478
458
  var project_path: String = str(args.get("projectPath", ""))
479
- 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", "")))
480
460
  var node_path: String = str(args.get("nodePath", ""))
481
461
  var new_parent_path: String = str(args.get("newParentPath", ""))
482
462
 
@@ -492,8 +472,8 @@ func reparent_node(args: Dictionary) -> Dictionary:
492
472
  return refused
493
473
 
494
474
  var root: Node = scene.root
495
- var node: Node = _find_node(root, node_path)
496
- 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)
497
477
  if not node:
498
478
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
499
479
  if not new_parent:
@@ -519,13 +499,24 @@ func reparent_node(args: Dictionary) -> Dictionary:
519
499
  return scene.refuse(unplaceable)
520
500
 
521
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)
522
506
  # Unowned while it moves: a node taken out of the tree keeps its owner, and adding it under the new
523
507
  # parent warned that the owner was not above it. Node.reparent warns the same way.
524
508
  node.owner = null
525
- node.get_parent().remove_child(node)
509
+ old_parent.remove_child(node)
526
510
  new_parent.add_child(node)
527
511
  GdhPlacing.place(node, place)
528
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)}
529
520
 
530
521
  var new_path: String = scene.path_of(node)
531
522
  var index: int = node.get_index()
@@ -540,7 +531,7 @@ func reparent_node(args: Dictionary) -> Dictionary:
540
531
  return "%s would still be in the scene as well when it loads." % old_path
541
532
  if copy.get_index() != index:
542
533
  return "%s would load at index %d, not %d." % [new_path, copy.get_index(), index]
543
- return ""
534
+ return _kept(scene, saved, node, also_changed.keys())
544
535
  )
545
536
  if not written.is_empty():
546
537
  carried.put_back(scene_path)
@@ -553,6 +544,8 @@ func reparent_node(args: Dictionary) -> Dictionary:
553
544
  "newNodePath": new_path,
554
545
  "index": index,
555
546
  }
547
+ if not also_changed.is_empty():
548
+ answer["alsoChanged"] = also_changed
556
549
  if new_path != old_path:
557
550
  answer.merge(GdhKin.follow(scene_path, old_path, new_path, carried))
558
551
  return answer
@@ -560,12 +553,14 @@ func reparent_node(args: Dictionary) -> Dictionary:
560
553
 
561
554
  func set_node_properties(args: Dictionary) -> Dictionary:
562
555
  var project_path: String = str(args.get("projectPath", ""))
563
- 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", "")))
564
557
  var node_path: String = str(args.get("nodePath", "."))
565
558
  var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
566
559
  var editable: Variant = args.get("editableChildren")
567
- if properties.is_empty() and editable == null:
568
- 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."}
569
564
 
570
565
  var opened: Array = _open(scene_path, true)
571
566
  var scene: GdhSceneFile = opened[0]
@@ -573,7 +568,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
573
568
  if not refused.is_empty():
574
569
  return refused
575
570
 
576
- var node: Node = _find_node(scene.root, node_path)
571
+ var node: Node = GdhSceneFile.node_at(scene.root, node_path)
577
572
  if not node:
578
573
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
579
574
  var foreign: String = scene.foreign(node)
@@ -599,7 +594,10 @@ func set_node_properties(args: Dictionary) -> Dictionary:
599
594
  scene.asked(node, properties.keys())
600
595
  var refused_property: String = _properties.write_all(node, properties)
601
596
  if not refused_property.is_empty():
602
- 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)
603
601
 
604
602
  var held: Dictionary = {}
605
603
  for key: Variant in properties:
@@ -607,11 +605,15 @@ func set_node_properties(args: Dictionary) -> Dictionary:
607
605
  var new_path: String = scene.path_of(node)
608
606
  var answer: Dictionary = {"ok": true, "nodePath": new_path, "properties": held}
609
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", [])
610
610
  var carried: GdhPaths = GdhPaths.new()
611
611
  var _rewritten: int = carried.carry(scene, {old_path: new_path} if new_path != old_path else {})
612
612
  var written: Dictionary = scene.write(
613
613
  func(saved: Node) -> String:
614
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)
615
617
  if unkept.is_empty() and editable != null:
616
618
  if saved.is_editable_instance(saved.get_node(new_path)) != GdhRead.as_bool(editable):
617
619
  return (
@@ -635,7 +637,7 @@ func set_node_properties(args: Dictionary) -> Dictionary:
635
637
 
636
638
  func get_node_properties(args: Dictionary) -> Dictionary:
637
639
  var project_path: String = str(args.get("projectPath", ""))
638
- 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", "")))
639
641
  var node_path: String = str(args.get("nodePath", "."))
640
642
  var include_defaults: bool = GdhRead.as_bool(args.get("includeDefaults", false))
641
643
 
@@ -645,7 +647,7 @@ func get_node_properties(args: Dictionary) -> Dictionary:
645
647
  if not refused.is_empty():
646
648
  return refused
647
649
 
648
- var node: Node = _find_node(scene.root, node_path)
650
+ var node: Node = GdhSceneFile.node_at(scene.root, node_path)
649
651
  if not node:
650
652
  return scene.refuse(scene.not_found("Node not found: " + node_path, node_path))
651
653
 
@@ -653,9 +655,18 @@ func get_node_properties(args: Dictionary) -> Dictionary:
653
655
  # declares, the class's for the rest. Compared with a bare instance of the class, every exported
654
656
  # variable read as changed; and a value this scene sets back to the default was hidden, so the
655
657
  # names this scene's file sets are always answered.
656
- 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)
657
660
  var cells: Dictionary = GdhResourceTools.cells_of(node)
658
- 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
659
670
  if node != scene.root and node.owner != null and not node.scene_file_path.is_empty():
660
671
  answer["instanceOf"] = node.scene_file_path
661
672
  answer["editableChildren"] = scene.root.is_editable_instance(node)
@@ -675,11 +686,11 @@ func get_node_properties(args: Dictionary) -> Dictionary:
675
686
 
676
687
  func save_scene(args: Dictionary) -> Dictionary:
677
688
  var project_path: String = str(args.get("projectPath", ""))
678
- 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", "")))
679
690
  var new_path_raw: String = str(args.get("newPath", ""))
680
691
  var target_path: String = scene_path
681
692
  if not new_path_raw.is_empty():
682
- target_path = _to_scene_res_path(project_path, new_path_raw)
693
+ target_path = GdhSceneFile.res_path(project_path, new_path_raw)
683
694
 
684
695
  if target_path != scene_path:
685
696
  var unsaved: String = GdhSceneFile.unsaved_in_editor(target_path)
@@ -747,249 +758,3 @@ func _save_open_scene(scene_path: String) -> Dictionary:
747
758
  "from": "editor",
748
759
  "unsavedInEditor": GdhSceneFile.unsaved(scene_path)
749
760
  }
750
-
751
-
752
- func connect_signal(args: Dictionary) -> Dictionary:
753
- var project_path: String = str(args.get("projectPath", ""))
754
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
755
- var source_node_path: String = str(args.get("sourceNodePath", ""))
756
- var signal_name: String = str(args.get("signalName", ""))
757
- var target_node_path: String = str(args.get("targetNodePath", ""))
758
- var method_name: String = str(args.get("methodName", ""))
759
- # A connection without CONNECT_PERSIST is a runtime one, and PackedScene.pack drops those on
760
- # the way out: without this the scene saves unchanged and this answers success over nothing.
761
- var flags: int = GdhRead.as_int(args.get("flags", 0)) | Object.CONNECT_PERSIST
762
-
763
- if (
764
- source_node_path.is_empty()
765
- or signal_name.is_empty()
766
- or target_node_path.is_empty()
767
- or method_name.is_empty()
768
- ):
769
- return {"ok": false, "error": "Missing required signal connection arguments"}
770
-
771
- var opened: Array = _open(scene_path, true)
772
- var scene: GdhSceneFile = opened[0]
773
- var refused: Dictionary = opened[1]
774
- if not refused.is_empty():
775
- return refused
776
-
777
- var ends: Array = _connection_ends(scene, source_node_path, signal_name, target_node_path)
778
- if ends[0] == null:
779
- return scene.refuse(str(ends[1]))
780
- var source: Node = ends[0]
781
- var target: Node = ends[1]
782
-
783
- # Reconnected rather than left alone when the flags differ, so the connection saved is the one
784
- # asked for. Left alone, the answer gave the flags asked for and the file kept the old ones.
785
- var callable: Callable = Callable(target, method_name)
786
- var method_found: bool = target.has_method(method_name)
787
- var flags_before: int = -1
788
- for connection: Dictionary in source.get_signal_connection_list(signal_name):
789
- if connection.get("callable") == callable:
790
- flags_before = GdhRead.as_int(connection.get("flags", 0))
791
- if source.is_connected(signal_name, callable):
792
- source.disconnect(signal_name, callable)
793
- var connect_result: Error = source.connect(signal_name, callable, flags)
794
- if connect_result != OK:
795
- return scene.refuse("Failed to connect signal: " + error_string(connect_result))
796
-
797
- var from: String = scene.path_of(source)
798
- var to: String = scene.path_of(target)
799
- var saved_flags: Array = [-1]
800
- var written: Dictionary = scene.write(
801
- func(saved: Node) -> String:
802
- saved_flags[0] = _connection_flags(saved, from, signal_name, to, method_name)
803
- if saved_flags[0] < 0:
804
- return "the connection would not be in the scene when it loads."
805
- return ""
806
- )
807
- if not written.is_empty():
808
- return written
809
-
810
- var answer: Dictionary = {
811
- "ok": true,
812
- "sourceNodePath": from,
813
- "signalName": signal_name,
814
- "targetNodePath": to,
815
- "methodName": method_name,
816
- "flags": saved_flags[0],
817
- "flagNames": GdhSceneFile.flag_names(GdhRead.as_int(saved_flags[0])),
818
- # Connected either way, as the editor's own dialog allows, since the method may be written
819
- # next; a caller told nothing learnt of a misspelt one only when the signal fired.
820
- "methodFound": method_found
821
- }
822
- # A second connect of the same pair read exactly like the first, so a caller could not tell a
823
- # connection made from one that was already there, and a list showing one where two were made
824
- # looked like a lost write.
825
- if flags_before >= 0:
826
- answer["alreadyConnected"] = true
827
- if flags_before != GdhRead.as_int(saved_flags[0]):
828
- answer["flagsBefore"] = GdhSceneFile.flag_names(flags_before)
829
- return answer
830
-
831
-
832
- ## The two ends of a connection in [param scene], as [source, target], or [null, why not].
833
- func _connection_ends(
834
- scene: GdhSceneFile, source_node_path: String, signal_name: String, target_node_path: String
835
- ) -> Array:
836
- var source: Node = _find_node(scene.root, source_node_path)
837
- var target: Node = _find_node(scene.root, target_node_path)
838
- if not source:
839
- return [null, scene.not_found("Source node not found: " + source_node_path, source_node_path)]
840
- if not target:
841
- return [null, scene.not_found("Target node not found: " + target_node_path, target_node_path)]
842
- # pack drops a connection with either end inside an instanced scene, and the answer said it was
843
- # made or removed.
844
- for end: Node in [source, target]:
845
- var foreign: String = scene.foreign(end)
846
- if not foreign.is_empty():
847
- return [null, foreign]
848
- if not source.has_signal(signal_name):
849
- var said: String = "%s has no signal %s" % [source_node_path, signal_name]
850
- return [null, _missing_on(source, GdhRead.offered(said, signal_name, GdhRead.signals_of(source)))]
851
- return [source, target]
852
-
853
-
854
- ## The flags of the persistent connection from [param from]'s [param signal_name] to
855
- ## [param method] on [param to] in the tree under [param root], or -1 when there is none.
856
- func _connection_flags(root: Node, from: String, signal_name: String, to: String, method: String) -> int:
857
- var source: Node = root.get_node_or_null(from)
858
- var target: Node = root.get_node_or_null(to)
859
- if source == null or target == null or not source.has_signal(signal_name):
860
- return -1
861
- for conn: Dictionary in source.get_signal_connection_list(signal_name):
862
- var flags: int = GdhRead.as_int(conn.get("flags", 0))
863
- var callable: Callable = conn.get("callable", Callable())
864
- if (
865
- flags & Object.CONNECT_PERSIST
866
- and callable.get_object() == target
867
- and str(callable.get_method()) == method
868
- ):
869
- return flags
870
- return -1
871
-
872
-
873
- func disconnect_signal(args: Dictionary) -> Dictionary:
874
- var project_path: String = str(args.get("projectPath", ""))
875
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
876
- var source_node_path: String = str(args.get("sourceNodePath", ""))
877
- var signal_name: String = str(args.get("signalName", ""))
878
- var target_node_path: String = str(args.get("targetNodePath", ""))
879
- var method_name: String = str(args.get("methodName", ""))
880
-
881
- if (
882
- source_node_path.is_empty()
883
- or signal_name.is_empty()
884
- or target_node_path.is_empty()
885
- or method_name.is_empty()
886
- ):
887
- return {"ok": false, "error": "Missing required signal disconnection arguments"}
888
-
889
- var opened: Array = _open(scene_path, true)
890
- var scene: GdhSceneFile = opened[0]
891
- var refused: Dictionary = opened[1]
892
- if not refused.is_empty():
893
- return refused
894
-
895
- var ends: Array = _connection_ends(scene, source_node_path, signal_name, target_node_path)
896
- if ends[0] == null:
897
- return scene.refuse(str(ends[1]))
898
- var source: Node = ends[0]
899
- var target: Node = ends[1]
900
-
901
- var from: String = scene.path_of(source)
902
- var to: String = scene.path_of(target)
903
- # A connection that was never there saved the scene unchanged and answered as removed, so a
904
- # typo in either name read as success.
905
- if _connection_flags(scene.root, from, signal_name, to, method_name) < 0:
906
- return scene.refuse(
907
- "%s has no connection from %s.%s to %s.%s." % [scene_path, from, signal_name, to, method_name]
908
- )
909
- source.disconnect(signal_name, Callable(target, method_name))
910
-
911
- var written: Dictionary = scene.write(
912
- func(saved: Node) -> String:
913
- if _connection_flags(saved, from, signal_name, to, method_name) >= 0:
914
- return "the connection would still be in the scene when it loads."
915
- return ""
916
- )
917
- if not written.is_empty():
918
- return written
919
-
920
- return {
921
- "ok": true,
922
- "removed": true,
923
- "sourceNodePath": from,
924
- "signalName": signal_name,
925
- "targetNodePath": to,
926
- "methodName": method_name
927
- }
928
-
929
-
930
- func list_connections(args: Dictionary) -> Dictionary:
931
- var project_path: String = str(args.get("projectPath", ""))
932
- var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
933
- var filter_path: String = str(args.get("nodePath", ""))
934
-
935
- var opened: Array = _open(scene_path, false)
936
- var scene: GdhSceneFile = opened[0]
937
- var refused: Dictionary = opened[1]
938
- if not refused.is_empty():
939
- return refused
940
-
941
- var root: Node = scene.root
942
- var filtered: Node = null
943
- if not filter_path.is_empty():
944
- filtered = _find_node(root, filter_path)
945
- if filtered == null:
946
- return scene.refuse(scene.not_found("Node not found: " + filter_path, filter_path))
947
- var nodes: Array = []
948
- _collect_nodes_recursive(root, ".", nodes)
949
-
950
- # Only persistent connections, which are the ones a scene file holds: a Container connects
951
- # signals of each child to itself as the child is added, and those were answered as if the file
952
- # held them. Matched on either end, since the filter is for connections involving the node.
953
- var connections: Array = []
954
- for entry: Dictionary in nodes:
955
- var path: String = str(entry["path"])
956
- var node: Node = entry["node"]
957
- for signal_info: Dictionary in node.get_signal_list():
958
- var signal_name: String = str(signal_info.get("name", ""))
959
- if signal_name.is_empty():
960
- continue
961
- for conn: Dictionary in node.get_signal_connection_list(signal_name):
962
- var flags: int = GdhRead.as_int(conn.get("flags", 0))
963
- if not flags & Object.CONNECT_PERSIST:
964
- continue
965
- var callable: Callable = conn.get("callable", Callable())
966
- var target_obj: Object = callable.get_object()
967
- if filtered != null and node != filtered and target_obj != filtered:
968
- continue
969
- var target_path: String = ""
970
- var editable: bool = scene.foreign(node).is_empty()
971
- if target_obj is Node:
972
- target_path = str(root.get_path_to(target_obj as Node))
973
- editable = editable and scene.foreign(target_obj as Node).is_empty()
974
- (
975
- connections
976
- . append(
977
- {
978
- "sourceNodePath": path,
979
- "signalName": signal_name,
980
- "targetNodePath": target_path,
981
- "methodName": str(callable.get_method()),
982
- "flags": flags,
983
- "flagNames": GdhSceneFile.flag_names(flags),
984
- # False for a connection an instanced scene makes inside itself, which only that
985
- # scene can change.
986
- "editable": editable
987
- }
988
- )
989
- )
990
-
991
- scene.close()
992
- var listed: Dictionary = {"ok": true, "connections": connections}
993
- if Engine.is_editor_hint():
994
- listed["unsavedInEditor"] = GdhSceneFile.unsaved(scene_path)
995
- return listed