gdharness 1.3.8 → 1.3.10

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 (78) hide show
  1. package/build/cli.js +4668 -3229
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
  3. package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
  4. package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
  6. package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
  7. package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
  8. package/build/godot/addons/gdharness_editor/reading.gd +112 -6
  9. package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
  10. package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
  11. package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
  12. package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
  13. package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
  14. package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
  15. package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
  16. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
  17. package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
  18. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
  19. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
  20. package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
  21. package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
  22. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
  23. package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
  24. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
  25. package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
  26. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
  27. package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
  28. package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
  29. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
  30. package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
  31. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
  32. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
  33. package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
  34. package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
  35. package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
  36. package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
  37. package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
  38. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
  39. package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
  40. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
  41. package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
  42. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
  43. package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
  44. package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
  45. package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
  46. package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
  47. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
  48. package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
  49. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
  50. package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
  51. package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
  52. package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
  53. package/build/godot/operations/animation_reads.gd +115 -4
  54. package/build/godot/operations/audio_buses.gd +93 -7
  55. package/build/godot/operations/classdb_queries.gd +119 -7
  56. package/build/godot/operations/declared_values.gd +272 -0
  57. package/build/godot/operations/dependencies.gd +154 -41
  58. package/build/godot/operations/editor_tools.gd +29 -13
  59. package/build/godot/operations/export_presets.gd +602 -4
  60. package/build/godot/operations/file_reads.gd +115 -18
  61. package/build/godot/operations/gdscript_analysis.gd +14 -0
  62. package/build/godot/operations/gdscript_authoring.gd +12 -2
  63. package/build/godot/operations/godot_operations.gd +18 -2
  64. package/build/godot/operations/import_pipeline.gd +20 -221
  65. package/build/godot/operations/input_actions.gd +127 -51
  66. package/build/godot/operations/project_config.gd +113 -68
  67. package/build/godot/operations/project_diagnostics.gd +4 -0
  68. package/build/godot/operations/project_validation.gd +266 -0
  69. package/build/godot/operations/property_paths.gd +89 -16
  70. package/build/godot/operations/property_values.gd +180 -121
  71. package/build/godot/operations/reading.gd +112 -6
  72. package/build/godot/operations/scene_connections.gd +222 -0
  73. package/build/godot/operations/serialisation.gd +19 -29
  74. package/build/godot/operations/setting_reads.gd +121 -0
  75. package/build/godot/operations/value_shapes.gd +128 -1
  76. package/build/godot/tests/listed.gd +20 -13
  77. package/build/keeper.js +1402 -707
  78. package/package.json +1 -1
@@ -16,15 +16,19 @@ const GdhSerialisation = preload("serialisation.gd")
16
16
 
17
17
 
18
18
  ## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf and
19
- ## editableChildren on a node that instances another scene and every stored property when
20
- ## [param include_properties] is set, the file's value or the default where it sets none.
19
+ ## editableChildren on a node that instances another scene and its stored properties when
20
+ ## [param include_properties] is set, the file's value or the default where it sets none, and on a
21
+ ## node another file makes setHere, the properties this file sets on it. Defaults the file leaves
22
+ ## alone are counted as defaultsLeftOut rather than listed unless [param include_defaults] is set.
21
23
  ## [param depth] -1 reads every level.
22
- static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
24
+ static func scene_tree(
25
+ scene_path: String, depth: int, include_properties: bool, include_defaults: bool
26
+ ) -> Dictionary:
23
27
  var laid: Dictionary = _laid(scene_path)
24
28
  if not laid.get("ok", false):
25
29
  return laid
26
30
  var nodes: Dictionary = laid["nodes"]
27
- return {"ok": true, "tree": _entry(nodes, ".", include_properties, depth, 0)}
31
+ return {"ok": true, "tree": _entry(nodes, ".", include_properties, include_defaults, depth, 0)}
28
32
 
29
33
 
30
34
  ## The node at [param node_path] in [param scene_path] as {ok, name, type, properties}, laid as
@@ -119,6 +123,9 @@ static func _laid(scene_path: String) -> Dictionary:
119
123
  return {"ok": false, "error": refused}
120
124
  if not nodes.has("."):
121
125
  return {"ok": false, "error": "%s holds no nodes" % scene_path}
126
+ var base: SceneState = packed.get_state().get_base_scene_state()
127
+ if base != null:
128
+ nodes["."]["inherits"] = base.get_path()
122
129
  return {"ok": true, "nodes": nodes}
123
130
 
124
131
 
@@ -173,8 +180,13 @@ static func _lay(
173
180
  continue
174
181
  else:
175
182
  nodes[path] = _made(str(state.get_node_type(index)))
183
+ nodes[path]["madeHere"] = loaded != null
176
184
  if ids[index] >= 0:
177
185
  nodes[path]["uniqueId"] = ids[index]
186
+ # Laid at the root with no scene handed in is a scene the one being read inherits, and the
187
+ # deepest is laid first, so this is the file that makes the node.
188
+ if prefix == "." and loaded == null:
189
+ nodes[path]["inheritedFrom"] = state.get_path()
178
190
  # The name is the state's even for an entry laid already, since an inherited scene names its
179
191
  # root itself; a root instanced inside another scene is renamed by that scene once laid.
180
192
  if not recovered:
@@ -195,10 +207,15 @@ static func _lay(
195
207
  if not grouped.has(str(group)):
196
208
  grouped.append(str(group))
197
209
  var held: Dictionary = nodes[path]["properties"]
210
+ # What the file being read sets, kept apart for a node another file makes, whose values read
211
+ # the same whether this scene changed them or that one gave them.
212
+ var laid: Dictionary = nodes[path]
213
+ var set_here: Array = laid.get_or_add("setHere", [])
198
214
  for property: int in state.get_node_property_count(index):
199
- held[str(state.get_node_property_name(index, property))] = state.get_node_property_value(
200
- index, property
201
- )
215
+ var named: String = str(state.get_node_property_name(index, property))
216
+ held[named] = state.get_node_property_value(index, property)
217
+ if loaded != null and not set_here.has(named):
218
+ set_here.append(named)
202
219
  # Only the scene being read and the scenes it inherits share its root, the owner whose editable
203
220
  # instances a write here sets; a file instanced below makes its own owner's editable.
204
221
  if prefix == ".":
@@ -314,7 +331,12 @@ static func _joined(prefix: String, given: String) -> String:
314
331
 
315
332
 
316
333
  static func _entry(
317
- nodes: Dictionary, path: String, include_properties: bool, depth: int, current_depth: int
334
+ nodes: Dictionary,
335
+ path: String,
336
+ include_properties: bool,
337
+ include_defaults: bool,
338
+ depth: int,
339
+ current_depth: int
318
340
  ) -> Dictionary:
319
341
  var node: Dictionary = nodes[path]
320
342
  var children: Array[Dictionary] = []
@@ -324,6 +346,12 @@ static func _entry(
324
346
  data["instanceOf"] = node["instanceOf"]
325
347
  if node.has("editableChildren"):
326
348
  data["editableChildren"] = node["editableChildren"]
349
+ # An inherited scene's file holds only what it changes, so the read says what it inherits and
350
+ # which nodes come from there, as the editor greys them.
351
+ if node.has("inherits"):
352
+ data["inherits"] = node["inherits"]
353
+ elif node.has("inheritedFrom"):
354
+ data["inheritedFrom"] = node["inheritedFrom"]
327
355
  var groups: Array[String] = node["groups"]
328
356
  if not groups.is_empty():
329
357
  var sorted: Array[String] = groups.duplicate()
@@ -331,7 +359,21 @@ static func _entry(
331
359
  data["groups"] = sorted
332
360
  if include_properties:
333
361
  var given: Dictionary = node["properties"]
334
- data["properties"] = _stored(str(node["type"]), given)
362
+ var stored: Dictionary = _stored(str(node["type"]), given, include_defaults)
363
+ data["properties"] = stored["properties"]
364
+ if stored["left_out"] > 0:
365
+ data["defaultsLeftOut"] = stored["left_out"]
366
+ var cells: int = _cell_count(str(node["type"]), given)
367
+ if cells >= 0:
368
+ data["cellCount"] = cells
369
+ data["cellsListedBy"] = (
370
+ "scene_node get with nodePath %s, which lists them as set_tilemap_cells takes them" % path
371
+ )
372
+ if not node.get("madeHere", false):
373
+ var kept: Array = node.get("setHere", [])
374
+ var set_here: Array = kept.duplicate()
375
+ set_here.sort()
376
+ data["setHere"] = set_here
335
377
  var below: Array[String] = node["children"]
336
378
  # Counted rather than left as an empty list, which reads as a node with no children.
337
379
  if depth >= 0 and current_depth >= depth:
@@ -339,19 +381,56 @@ static func _entry(
339
381
  data["childrenNotListed"] = below.size()
340
382
  return data
341
383
  for child: String in below:
342
- children.append(_entry(nodes, child, include_properties, depth, current_depth + 1))
384
+ children.append(_entry(nodes, child, include_properties, include_defaults, depth, current_depth + 1))
343
385
  return data
344
386
 
345
387
 
388
+ ## How many cells a TileMapLayer or TileMap of [param type] holds under the values [param given], or
389
+ ## -1 when it is another node or the file sets none of its cells. The file keeps them as packed bytes,
390
+ ## which a read of the tree answered with alone, so a caller decoded the base64 to learn what was
391
+ ## painted. Only cells the file sets are counted: an instance's come from its own scene.
392
+ static func _cell_count(type: String, given: Dictionary) -> int:
393
+ if not ClassDB.class_exists(type) or not ClassDB.can_instantiate(type):
394
+ return -1
395
+ var single: bool = ClassDB.is_parent_class(type, "TileMapLayer")
396
+ if not single and not ClassDB.is_parent_class(type, "TileMap"):
397
+ return -1
398
+ var painted: bool = false
399
+ for property: Variant in given:
400
+ var named: String = str(property)
401
+ if named == "tile_map_data" or (named.begins_with("layer_") and named.ends_with("/tile_data")):
402
+ painted = true
403
+ if not painted:
404
+ return -1
405
+ var bare: Node = ClassDB.instantiate(type)
406
+ for property: Variant in given:
407
+ if property != "script":
408
+ bare.set(str(property), given[property])
409
+ var count: int = 0
410
+ if single:
411
+ count = (bare as TileMapLayer).get_used_cells().size()
412
+ else:
413
+ var layered: TileMap = bare as TileMap
414
+ for layer: int in layered.get_layers_count():
415
+ count += layered.get_used_cells(layer).size()
416
+ bare.free()
417
+ return count
418
+
419
+
346
420
  ## Every property a node of [param type] stores, with the value [param given] sets or the default,
347
- ## read as a node holding those values would answer them.
421
+ ## read as a node holding those values would answer them, as {properties, left_out}.
348
422
  ##
349
423
  ## The engine's half is read off a bare node of the class with the file's values set on it, which
350
424
  ## runs no script: what a node lists depends on its values (a thread group's settings appear only
351
425
  ## once it has one) and some setters set others (a Control's anchors preset sets how it grows), so
352
426
  ## the class's defaults and the file's entries alone answered neither. The script's half is its
353
427
  ## declared defaults under the file's values.
354
- static func _stored(type: String, given: Dictionary) -> Dictionary:
428
+ ##
429
+ ## Without [param include_defaults], properties keeps only what the file sets and what differs from
430
+ ## a node of the class holding none of its values, and left_out counts the rest. Every Control
431
+ ## carries a hundred properties, and ten of them came to 57 KB of defaults around the few values a
432
+ ## menu scene set.
433
+ static func _stored(type: String, given: Dictionary, include_defaults: bool) -> Dictionary:
355
434
  var attached: Variant = given.get("script")
356
435
  var script: Script = attached if attached is Script else null
357
436
  var scripted: Dictionary = {}
@@ -361,8 +440,10 @@ static func _stored(type: String, given: Dictionary) -> Dictionary:
361
440
  if not property.is_empty() and GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
362
441
  scripted[property] = script.get_property_default_value(property)
363
442
  var values: Dictionary = {}
443
+ var defaults: Dictionary = {}
364
444
  if ClassDB.class_exists(type) and ClassDB.can_instantiate(type):
365
445
  var bare: Object = ClassDB.instantiate(type)
446
+ var plain: Object = ClassDB.instantiate(type)
366
447
  for property: Variant in given:
367
448
  if property != "script" and not scripted.has(property):
368
449
  bare.set(str(property), given[property])
@@ -370,18 +451,30 @@ static func _stored(type: String, given: Dictionary) -> Dictionary:
370
451
  var property: String = str(declared.get("name", ""))
371
452
  if not property.is_empty() and GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
372
453
  values[property] = bare.get(property)
373
- if bare is Node:
374
- bare.free()
454
+ defaults[property] = plain.get(property)
455
+ for made: Object in [bare, plain]:
456
+ if made is Node:
457
+ made.free()
375
458
  for property: String in scripted:
376
459
  values[property] = scripted[property]
460
+ defaults[property] = scripted[property]
377
461
  # Given last, which keeps what a bare node cannot hold: the script itself, and a path to a node
378
462
  # for a property that holds the node once it is in a tree.
379
463
  values.merge(given, true)
380
464
  var serialiser: GdhSerialisation = GdhSerialisation.new()
381
465
  var answered: Dictionary = {}
466
+ var left_out: int = 0
382
467
  for property: Variant in values:
468
+ var as_default: bool = (
469
+ not given.has(property)
470
+ and defaults.has(property)
471
+ and GdhSame.same(values[property], defaults[property])
472
+ )
473
+ if as_default and not include_defaults:
474
+ left_out += 1
475
+ continue
383
476
  answered[property] = serialiser.serialize_value(values[property])
384
- return answered
477
+ return {"properties": answered, "left_out": left_out}
385
478
 
386
479
 
387
480
  ## What a resource file holds, in the form modify takes, so a value read here can be written back.
@@ -394,12 +487,16 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
394
487
  var file: String = res_path.get_slice("::", 0)
395
488
  if not FileAccess.file_exists(file):
396
489
  return {"ok": false, "error": "No resource at " + file}
397
- var loaded: Resource = ResourceLoader.load(file, "", ResourceLoader.CACHE_MODE_IGNORE)
398
- if loaded == null:
490
+ var whole: Resource = ResourceLoader.load(file, "", ResourceLoader.CACHE_MODE_IGNORE)
491
+ if whole == null:
399
492
  return {"ok": false, "error": "%s did not load as a resource." % file}
493
+ # The file's resource is held until the read is done, since what it keeps can depend on it: a
494
+ # TileSet that goes unlinks its sources, and an atlas source with no TileSet lists none of its
495
+ # tiles' physics, navigation or custom data.
496
+ var loaded: Resource = whole
400
497
  if file != res_path:
401
498
  var id: String = res_path.trim_prefix(file + "::")
402
- var inner: Resource = _kept_in(loaded, file, id, {})
499
+ var inner: Resource = _kept_in(whole, file, id, {})
403
500
  if inner == null:
404
501
  return {"ok": false, "error": "%s keeps no resource with the id %s." % [file, id]}
405
502
  loaded = inner
@@ -76,6 +76,9 @@ func get_gdscript_info(params: Dictionary) -> Dictionary:
76
76
  if not full_script_path.begins_with("res://"):
77
77
  full_script_path = "res://" + full_script_path
78
78
 
79
+ # Read as GDScript, a scene came back as a script extending RefCounted with nothing in it.
80
+ if full_script_path.get_extension().to_lower() != "gd":
81
+ return _log.failure(_not_a_script(full_script_path))
79
82
  if not FileAccess.file_exists(full_script_path):
80
83
  return _log.failure(
81
84
  GdhFileWalk.new().with_nearest_files(
@@ -334,6 +337,17 @@ func _inner_class(
334
337
  return body
335
338
 
336
339
 
340
+ ## The refusal for [param path], which is not a .gd file, with the tool that reads what it is.
341
+ static func _not_a_script(path: String) -> String:
342
+ var said: String = "%s is not a GDScript file, and script_info reads .gd files only" % path
343
+ match path.get_extension().to_lower():
344
+ "tscn", "scn":
345
+ return said + ": scene_tree reads a scene's nodes, and with properties the script each carries."
346
+ "tres", "res":
347
+ return said + ": resource_edit read reads a resource."
348
+ return said + "."
349
+
350
+
337
351
  static func _indentation(line: String) -> String:
338
352
  return line.substr(0, line.length() - line.strip_edges(true, false).length())
339
353
 
@@ -259,7 +259,7 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
259
259
  )
260
260
  return _log.failure(
261
261
  (
262
- "Nothing was changed: %s parses as it is and would not with these additions: %s"
262
+ "Nothing was changed: %s parses now, and would not with these additions: %s"
263
263
  % [script_path, _reasons(caught.raised, full_script_path)]
264
264
  )
265
265
  )
@@ -505,7 +505,17 @@ func _add_constant(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
505
505
  or line.begins_with("class ")
506
506
  ):
507
507
  break
508
- return after if _insert(lines, after, [const_line], placed) else -1
508
+ after = _past_class_doc(lines, after)
509
+ # Set apart from the header or the signals above and from what follows, as the style guide lays
510
+ # out its groups; one below the constants already there joins them.
511
+ var block: Array[String] = [const_line]
512
+ var above: String = lines[after - 1].strip_edges() if after > 0 else ""
513
+ var below: String = lines[after].strip_edges() if after < lines.size() else ""
514
+ if not above.is_empty() and not GdhPatterns.without_annotations(above).begins_with("const "):
515
+ block.push_front("")
516
+ if not below.is_empty() and not GdhPatterns.without_annotations(below).begins_with("const "):
517
+ block.push_back("")
518
+ return after + block.find(const_line) if _insert(lines, after, block, placed) else -1
509
519
 
510
520
 
511
521
  # Inserts the declaration and answers with its zero-based index, or -1 for one that could not be placed.
@@ -24,6 +24,7 @@ const GdhFileReads = preload("file_reads.gd")
24
24
  const GdhGdscriptAnalysis = preload("gdscript_analysis.gd")
25
25
  const GdhGdscriptAuthoring = preload("gdscript_authoring.gd")
26
26
  const GdhImportPipeline = preload("import_pipeline.gd")
27
+ const GdhProjectValidation = preload("project_validation.gd")
27
28
  const GdhInputActions = preload("input_actions.gd")
28
29
  const GdhLog = preload("logger.gd")
29
30
  const GdhPlugins = preload("plugins.gd")
@@ -51,6 +52,8 @@ const BEGUN: String = "[gdharness] begun"
51
52
  const AFTER_THE_AUTOLOADS: Array[String] = [
52
53
  "check_scripts",
53
54
  "create_script",
55
+ # An exporter made in _init, before an editor's main loop has started, crashed the engine.
56
+ "export_options",
54
57
  "modify_script",
55
58
  "read_animation_states",
56
59
  "read_animations",
@@ -188,7 +191,8 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
188
191
  GdhFileReads.scene_tree(
189
192
  str(params.get("scene_path", "")),
190
193
  GdhRead.as_int(params.get("depth", -1), -1),
191
- GdhRead.as_bool(params.get("include_properties", false))
194
+ GdhRead.as_bool(params.get("include_properties", false)),
195
+ GdhRead.as_bool(params.get("include_defaults", false))
192
196
  )
193
197
  )
194
198
  "read_resource":
@@ -229,8 +233,14 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
229
233
  payload = GdhExportPresets.new(_log).list_export_presets(params)
230
234
  "add_export_preset":
231
235
  payload = GdhExportPresets.new(_log).add_export_preset(params)
236
+ "set_export_preset":
237
+ payload = GdhExportPresets.new(_log).set_export_preset(params)
238
+ "remove_export_preset":
239
+ payload = GdhExportPresets.new(_log).remove_export_preset(params)
240
+ "export_options":
241
+ payload = GdhExportPresets.new(_log).export_options(params)
232
242
  "validate_project":
233
- payload = GdhImportPipeline.new(_log).validate_project(params)
243
+ payload = GdhProjectValidation.new(_log).validate_project(params)
234
244
 
235
245
  # Dependencies and project inspection
236
246
  "get_dependencies":
@@ -247,6 +257,8 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
247
257
  payload = GdhProjectConfig.new(_log).get_project_setting(params)
248
258
  "set_project_setting":
249
259
  payload = GdhProjectConfig.new(_log).set_project_setting(params)
260
+ "unset_project_setting":
261
+ payload = GdhProjectConfig.new(_log).unset_project_setting(params)
250
262
  "add_autoload":
251
263
  payload = GdhProjectConfig.new(_log).add_autoload(params)
252
264
  "remove_autoload":
@@ -293,6 +305,10 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
293
305
  payload = GdhAudioBuses.new(_log).set_audio_bus_effect(params)
294
306
  "set_audio_bus_volume":
295
307
  payload = GdhAudioBuses.new(_log).set_audio_bus_volume(params)
308
+ "rename_audio_bus":
309
+ payload = GdhAudioBuses.new(_log).rename_audio_bus(params)
310
+ "set_audio_bus_send":
311
+ payload = GdhAudioBuses.new(_log).set_audio_bus_send(params)
296
312
 
297
313
  # ClassDB
298
314
  "query_classes":
@@ -1,8 +1,5 @@
1
1
  extends RefCounted
2
2
 
3
- const GdhDependencies = preload("dependencies.gd")
4
- const GdhExportPresets = preload("export_presets.gd")
5
- const GdhScriptCheck = preload("script_check.gd")
6
3
  const GdhRead = preload("reading.gd")
7
4
  const GdhSerialisation = preload("serialisation.gd")
8
5
  const GdhFileWalk = preload("file_walk.gd")
@@ -40,7 +37,14 @@ const IMPORTABLE_EXTENSIONS: Array[String] = [
40
37
  "fbx",
41
38
  "obj",
42
39
  "dae",
40
+ "csv",
41
+ "glsl",
43
42
  ]
43
+ ## What the engine imports through Blender, which it finds only through an editor setting. With the
44
+ ## setting unset, a headless import stops at the first of these and imports nothing after it.
45
+ const BLENDER_EXTENSIONS: Array[String] = ["blend"]
46
+ const BLENDER_SETTING: String = "filesystem/import/blender/blender_path"
47
+ const BLENDER_ENABLED: String = "filesystem/import/blender/enabled"
44
48
  ## The format version each importer writes as `importer_version` where it is above 0, which the
45
49
  ## editor reimports anything older than. Read off sidecars 4.7.2 writes: scene and animation_library
46
50
  ## write 1, and the texture importers write none. A case imports one fresh and compares, so an engine
@@ -348,224 +352,6 @@ static func _all_ints(held: Variant, keys: bool) -> bool:
348
352
  return true
349
353
 
350
354
 
351
- # Validate project for export
352
- func validate_project(params: Dictionary) -> Dictionary:
353
- var preset_name: String = str(params.get("preset", ""))
354
- var include_suggestions: bool = GdhRead.as_bool(params.get("include_suggestions", true), true)
355
-
356
- _log.info("Validating project" + (" for preset: " + preset_name if not preset_name.is_empty() else ""))
357
-
358
- var issues: Array[Dictionary] = []
359
- var warnings: Array[Dictionary] = []
360
- var checks_performed: Array[String] = []
361
-
362
- checks_performed.append("project_file")
363
- if not FileAccess.file_exists("res://project.godot"):
364
- issues.append(
365
- _finding(
366
- "error",
367
- "project_file",
368
- "project.godot not found",
369
- "Ensure you are running this from a valid Godot project directory",
370
- include_suggestions
371
- )
372
- )
373
-
374
- checks_performed.append("main_scene")
375
- var main_scene: String = str(ProjectSettings.get_setting("application/run/main_scene", ""))
376
- if main_scene.is_empty():
377
- issues.append(
378
- _finding(
379
- "error",
380
- "main_scene",
381
- "No main scene set",
382
- "Set a main scene in Project Settings > Application > Run > Main Scene",
383
- include_suggestions
384
- )
385
- )
386
- elif not GdhFileWalk.missing_reason(main_scene).is_empty():
387
- issues.append(
388
- _finding(
389
- "error",
390
- "main_scene",
391
- "Main scene " + GdhFileWalk.missing_reason(main_scene),
392
- "Update the main scene setting or create the missing scene file",
393
- include_suggestions
394
- )
395
- )
396
-
397
- # A file never imported is on disk, so the reference checks pass it, and still nothing loads it:
398
- # a scene using one fails to load in a run or an export.
399
- checks_performed.append("imports")
400
- var never: Array[String] = []
401
- for path: String in _files.find_files_with_extensions("res://", IMPORTABLE_EXTENSIONS):
402
- if not FileAccess.file_exists(path + ".import"):
403
- never.append(path)
404
- if not never.is_empty():
405
- never.sort()
406
- warnings.append(
407
- _finding(
408
- "warning",
409
- "imports",
410
- never_imported_line(never),
411
- "Run project_import reimport, or open the project in the editor, whose scan imports them",
412
- include_suggestions
413
- )
414
- )
415
-
416
- checks_performed.append("export_presets")
417
- if not FileAccess.file_exists("res://export_presets.cfg"):
418
- warnings.append(
419
- _finding(
420
- "warning",
421
- "export_presets",
422
- "No export presets configured",
423
- "Add a preset with project_export add_preset, or in the editor: Project > Export",
424
- include_suggestions
425
- )
426
- )
427
-
428
- # The preset asked about is looked for rather than only logged: validating "for" a preset that
429
- # does not exist answered valid, as though it had been checked.
430
- if not preset_name.is_empty():
431
- checks_performed.append("export_preset")
432
- var named: Array[String] = GdhExportPresets.names()
433
- if preset_name not in named:
434
- issues.append(
435
- _finding(
436
- "error",
437
- "export_preset",
438
- (
439
- "No export preset is named "
440
- + preset_name
441
- + (
442
- ": the project has none"
443
- if named.is_empty()
444
- else ": the presets are " + ", ".join(named)
445
- )
446
- ),
447
- "Pass one of the preset names, or add the preset with project_export add_preset",
448
- include_suggestions
449
- )
450
- )
451
-
452
- checks_performed.append("icon")
453
- var icon_path: String = str(ProjectSettings.get_setting("application/config/icon", ""))
454
- if icon_path.is_empty():
455
- warnings.append(
456
- _finding(
457
- "warning",
458
- "icon",
459
- "No application icon set",
460
- "Set an icon in Project Settings > Application > Config > Icon",
461
- include_suggestions
462
- )
463
- )
464
- elif not FileAccess.file_exists(icon_path):
465
- warnings.append(
466
- _finding(
467
- "warning",
468
- "icon",
469
- "Icon file does not exist: " + icon_path,
470
- "Update the icon path or add the missing icon file",
471
- include_suggestions
472
- )
473
- )
474
-
475
- checks_performed.append("project_name")
476
- var project_name: String = str(ProjectSettings.get_setting("application/config/name", ""))
477
- if project_name.is_empty():
478
- warnings.append(
479
- _finding(
480
- "warning",
481
- "project_name",
482
- "No project name set",
483
- "Set a project name in Project Settings > Application > Config > Name",
484
- include_suggestions
485
- )
486
- )
487
-
488
- checks_performed.append("scripts")
489
- var script_files: Array[String] = _files.find_files_with_extensions("res://", ["gd"])
490
- var scripts_checked: int = 0
491
- var script_issues: Array[Dictionary] = []
492
-
493
- # A hundred scripts is enough to say whether the project is tidy without a large one
494
- # turning validation into a full read of its source tree.
495
- for script_path: String in script_files.slice(0, 100):
496
- scripts_checked += 1
497
- var file: FileAccess = FileAccess.open(script_path, FileAccess.READ)
498
- if file:
499
- var content: String = file.get_as_text()
500
- file.close()
501
-
502
- if "# TODO" in content or "# FIXME" in content:
503
- script_issues.append({"path": script_path, "issue": "Contains TODO/FIXME comments"})
504
- if "pass # TODO" in content:
505
- script_issues.append({"path": script_path, "issue": "Contains unimplemented functions"})
506
-
507
- # Compiled by the server and handed in, as for the health check: a script the engine refuses
508
- # stops the export's game at the first scene using it, which the comment check above never saw.
509
- var compiled: Dictionary = params.get("compiled", {})
510
- if compiled.has("checked"):
511
- checks_performed.append("compiles")
512
- var failures: Array = compiled.get("failures", [])
513
- for failure: Dictionary in failures:
514
- issues.append(
515
- _finding(
516
- "error",
517
- "compiles",
518
- GdhScriptCheck.failure_line(failure),
519
- "script_diagnostics gives every error in it with its line",
520
- include_suggestions
521
- )
522
- )
523
-
524
- checks_performed.append("references")
525
- for entry: Dictionary in GdhDependencies.new(_log).missing_references():
526
- issues.append(
527
- _finding(
528
- "error",
529
- "references",
530
- GdhDependencies.missing_line(entry),
531
- "Restore the file, or point the reference at the file that replaced it",
532
- include_suggestions
533
- )
534
- )
535
-
536
- if script_issues.size() > 0:
537
- var warning: Dictionary = _finding(
538
- "warning",
539
- "scripts",
540
- str(script_issues.size()) + " script issues found",
541
- "Review and resolve TODO/FIXME items before release",
542
- include_suggestions
543
- )
544
- warning["details"] = script_issues.slice(0, 5)
545
- warnings.append(warning)
546
-
547
- return {
548
- "valid": issues.is_empty(),
549
- "issues": issues,
550
- "warnings": warnings,
551
- "checks_performed": checks_performed,
552
- "scripts_checked": scripts_checked,
553
- # Said beside the count, so a project of four hundred scripts is not read as checked whole.
554
- "scripts_total": script_files.size(),
555
- "issue_count": issues.size(),
556
- "warning_count": warnings.size()
557
- }
558
-
559
-
560
- func _finding(
561
- type: String, check: String, message: String, suggestion: String, include_suggestion: bool
562
- ) -> Dictionary:
563
- var finding: Dictionary = {"type": type, "check": check, "message": message}
564
- if include_suggestion:
565
- finding["suggestion"] = suggestion
566
- return finding
567
-
568
-
569
355
  func _tally(summary: Dictionary, status: Dictionary) -> void:
570
356
  summary["total"] += 1
571
357
  var state: String = status["status"]
@@ -662,6 +448,19 @@ func _never_imported(resource_path: String) -> String:
662
448
  var extension: String = resource_path.get_extension().to_lower()
663
449
  if extension in IMPORTABLE_EXTENSIONS:
664
450
  return ""
451
+ if extension in BLENDER_EXTENSIONS:
452
+ if not ProjectSettings.get_setting(BLENDER_ENABLED, true):
453
+ return "the project sets " + BLENDER_ENABLED + " to false, so the engine leaves them unimported"
454
+ return (
455
+ "the engine imports ."
456
+ + extension
457
+ + " files through Blender, which it finds through the editor setting "
458
+ + BLENDER_SETTING
459
+ + ". An editor with it set imports them on its scan; an import pass with it unset stops at"
460
+ + " the first one and imports nothing, and setting "
461
+ + BLENDER_ENABLED
462
+ + " to false has the engine leave them unimported"
463
+ )
665
464
  # Loadable without a sidecar is what a scene, a script or a saved resource is: read as it is.
666
465
  if ResourceLoader.exists(resource_path):
667
466
  return "the engine loads ." + extension + " files as they are, without importing them"