gdharness 1.1.4 → 1.1.6

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.
@@ -80,10 +80,13 @@ func _init_tools() -> void:
80
80
  "list_connections": [_scene_tools, "list_connections"],
81
81
  "rescan_filesystem": [_scene_tools, "rescan_filesystem"],
82
82
  "scan_status": [_scene_tools, "scan_status"],
83
+ "reimport_files": [_scene_tools, "reimport_files"],
83
84
  "global_classes": [_class_tools, "global_classes"],
84
85
  "reload_script": [_class_tools, "reload_script"],
85
86
  # Settings, for a caller that asked the editor rather than the file on disk
86
87
  "get_project_setting": [_settings_tools, "get_project_setting"],
88
+ "adopt_project_settings": [_settings_tools, "adopt_project_settings"],
89
+ "adopt_audio_bus_layout": [_settings_tools, "adopt_audio_bus_layout"],
87
90
  # Resource tools
88
91
  "create_resource": [_resource_tools, "create_resource"],
89
92
  "modify_resource": [_resource_tools, "modify_resource"],
@@ -11,29 +11,35 @@ var _editor_plugin: EditorPlugin = null
11
11
  var _values: Serialisation = Serialisation.new()
12
12
  var _properties: PropertyValues = PropertyValues.new()
13
13
 
14
- ## How many times the editor has said its filesystem changed, which a scan run on the main thread
15
- ## does before `scan()` returns: counted so a rescan can tell a scan that ran there from one the
16
- ## editor declined.
17
- var _scans_finished: int = 0
18
-
19
- ## When the last scan finished, on the engine's clock, or -1 before the first. The editor writes
14
+ ## How many scans the editor has completed, counted off `sources_changed`, which it emits at the end
15
+ ## of every scan once the reimport the scan found is done: the one moment a scan is over. Neither
16
+ ## flag it offers says so. A threaded scan stops reporting itself when its thread finishes, and its
17
+ ## import runs a frame or more later when the thread is joined; an editor in the background took
18
+ ## long enough over that frame that a rescan answered before the import of 211 changed scenes began.
19
+ var _scans_completed: int = 0
20
+
21
+ ## When the last scan completed, on the engine's clock, or -1 before the first. The editor writes
20
22
  ## the class cache a frame after it stops reporting a scan, measured on 4.7.2, so a game started
21
23
  ## in between reads a file being rewritten; how long ago the scan ended is what tells a caller
22
24
  ## whether that write can still be coming.
23
25
  var _scan_finished_at_msec: int = -1
24
26
 
27
+ ## How many reimports asked for through [method reimport_files] have finished. Counted here rather
28
+ ## than off `resources_reimported`, which a scan's own import emits as well.
29
+ var _reimports_completed: int = 0
30
+
25
31
 
26
32
  func set_editor_plugin(plugin: EditorPlugin) -> void:
27
33
  _editor_plugin = plugin
28
34
  var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
29
- if not filesystem.filesystem_changed.is_connected(_scan_finished):
30
- var watched: int = filesystem.filesystem_changed.connect(_scan_finished)
35
+ if not filesystem.sources_changed.is_connected(_scan_completed):
36
+ var watched: int = filesystem.sources_changed.connect(_scan_completed)
31
37
  if watched != OK:
32
- push_error("gdharness could not watch for finished scans: %d" % watched)
38
+ push_error("gdharness could not watch for completed scans: %d" % watched)
33
39
 
34
40
 
35
- func _scan_finished() -> void:
36
- _scans_finished += 1
41
+ func _scan_completed(_sources_exist: bool) -> void:
42
+ _scans_completed += 1
37
43
  _scan_finished_at_msec = Time.get_ticks_msec()
38
44
 
39
45
 
@@ -658,8 +664,8 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
658
664
  # conditions from its progress dialog. Declined and said, so the caller waits and asks again.
659
665
  var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
660
666
  var started: bool = false
667
+ var completed_before: int = _scans_completed
661
668
  if not Read.as_bool(args.get("statusOnly", false)) and not busy:
662
- var finished_before: int = _scans_finished
663
669
  filesystem.scan()
664
670
  # Whether the scan ran, read off the editor rather than assumed. `scan()` returns without
665
671
  # a word while the thread of the scan before is still to be joined, which is a frame or
@@ -668,16 +674,19 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
668
674
  # downstream, a rescan after 698 files were rewritten answered finished in 1.5s and the
669
675
  # one changed file stayed on its old import. A threaded scan reports itself from inside
670
676
  # the call, and one run on this thread has finished and signalled before it returns.
671
- started = filesystem.is_scanning() or _scans_finished != finished_before
677
+ started = filesystem.is_scanning() or _scans_completed != completed_before
672
678
 
673
679
  # Importing is reported separately from scanning, and a class is not registered until
674
- # both are done, so a caller watching only one of them can look too early.
680
+ # both are done, so a caller watching only one of them can look too early. The two counts are
681
+ # what a caller waits on: the scan it asked for is over once the count has moved past the one
682
+ # from before it was asked.
675
683
  return {
676
684
  "ok": true,
677
685
  "started": started,
678
686
  "scanning": filesystem.is_scanning(),
679
687
  "importing": filesystem.is_importing(),
680
- "scansFinished": _scans_finished,
688
+ "scansCompletedBefore": completed_before,
689
+ "scansCompleted": _scans_completed,
681
690
  }
682
691
 
683
692
 
@@ -696,4 +705,44 @@ func scan_status(_args: Dictionary) -> Dictionary:
696
705
  "importing": filesystem.is_importing(),
697
706
  "sinceScanFinishedMs":
698
707
  -1 if _scan_finished_at_msec < 0 else Time.get_ticks_msec() - _scan_finished_at_msec,
708
+ "reimportsCompleted": _reimports_completed,
699
709
  }
710
+
711
+
712
+ ## Reimport the files named under `paths`, whatever their state, the way the editor's Reimport
713
+ ## button does, and report whether the reimport was started.
714
+ ##
715
+ ## Started on the next frame rather than run here: the editor imports on its main thread, and a
716
+ ## reimport of a few hundred scenes outlasts the wait on any one command. The caller waits for
717
+ ## `reimportsCompleted` in [method scan_status] to move past `reimportsCompletedBefore`. Declined
718
+ ## while the editor scans or imports, since `reimport_files` refuses to run inside an import.
719
+ func reimport_files(args: Dictionary) -> Dictionary:
720
+ if not _editor_plugin:
721
+ return {"ok": false, "error": "Editor plugin unavailable"}
722
+ var paths: Array[String] = []
723
+ var given: Variant = args.get("paths", [])
724
+ if given is Array:
725
+ for path: Variant in given:
726
+ paths.append(str(path))
727
+ if paths.is_empty():
728
+ return {"ok": false, "error": "reimport_files needs paths"}
729
+ var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
730
+ var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
731
+ if not busy:
732
+ _reimport.call_deferred(PackedStringArray(paths))
733
+ return {
734
+ "ok": true,
735
+ "started": not busy,
736
+ "scanning": filesystem.is_scanning(),
737
+ "importing": filesystem.is_importing(),
738
+ "reimportsCompletedBefore": _reimports_completed,
739
+ }
740
+
741
+
742
+ func _reimport(paths: PackedStringArray) -> void:
743
+ var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
744
+ # A scan can begin between the answer and this frame, and its import would refuse this one.
745
+ while filesystem.is_scanning() or filesystem.is_importing():
746
+ await get_tree().process_frame
747
+ filesystem.reimport_files(paths)
748
+ _reimports_completed += 1
@@ -1,7 +1,8 @@
1
1
  @tool
2
2
  extends Node
3
3
 
4
- ## Project settings read from the open editor, for a caller that asked for the editor's answer.
4
+ ## Project settings read from the open editor, for a caller that asked for the editor's answer, and
5
+ ## taken up by it after a write to the file.
5
6
  ##
6
7
  ## The same question answered headless starts an engine, loads project.godot from disk and exits.
7
8
  ## This answers from the ProjectSettings the editor already has, which costs nothing and is not the
@@ -56,3 +57,51 @@ func _settings_under(prefix: String) -> Dictionary:
56
57
 
57
58
  found.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["setting"] < b["setting"])
58
59
  return {"ok": true, "prefix": prefix, "count": found.size(), "settings": found}
60
+
61
+
62
+ ## Takes the named settings from project.godot as it is on disk, after something else wrote it.
63
+ ##
64
+ ## The editor loads the file once, at startup. Written from outside, it goes on holding the old
65
+ ## values, answers them when asked, and writes them back over the new ones the next time it saves
66
+ ## the project settings. A setting gone from the file goes back to its default, or goes altogether
67
+ ## when the engine declares none, as an autoload does once it is removed.
68
+ func adopt_project_settings(args: Dictionary) -> Dictionary:
69
+ var file: ConfigFile = ConfigFile.new()
70
+ var loaded: Error = file.load("res://project.godot")
71
+ if loaded != OK:
72
+ return {"ok": false, "error": "Could not read project.godot: " + error_string(loaded)}
73
+ var adopted: Array[String] = []
74
+ var named: Variant = args.get("settings", [])
75
+ if not named is Array:
76
+ return {"ok": false, "error": "settings must be a list of setting names"}
77
+ var settings: Array = named
78
+ for entry: Variant in settings:
79
+ var setting: String = str(entry)
80
+ var at: int = setting.find("/")
81
+ if at <= 0:
82
+ continue
83
+ var section: String = setting.substr(0, at)
84
+ var key: String = setting.substr(at + 1)
85
+ if file.has_section_key(section, key):
86
+ ProjectSettings.set_setting(setting, file.get_value(section, key))
87
+ elif ProjectSettings.property_can_revert(setting):
88
+ ProjectSettings.set_setting(setting, ProjectSettings.property_get_revert(setting))
89
+ else:
90
+ ProjectSettings.set_setting(setting, null)
91
+ adopted.append(setting)
92
+ return {"ok": true, "adopted": adopted}
93
+
94
+
95
+ ## Loads the bus layout the project names into the editor's audio server, after something else
96
+ ## wrote it: the editor holds the layout it loaded and saves that one when its audio panel changes.
97
+ func adopt_audio_bus_layout(_args: Dictionary) -> Dictionary:
98
+ var path: String = str(
99
+ ProjectSettings.get_setting("audio/buses/default_bus_layout", "res://default_bus_layout.tres")
100
+ )
101
+ var layout: AudioBusLayout = ResourceLoader.load(
102
+ path, "AudioBusLayout", ResourceLoader.CACHE_MODE_REPLACE
103
+ )
104
+ if layout == null:
105
+ return {"ok": false, "error": "Could not load the bus layout at " + path}
106
+ AudioServer.set_bus_layout(layout)
107
+ return {"ok": true, "layout": path}
@@ -13,13 +13,13 @@ func _init(host: Node) -> void:
13
13
 
14
14
 
15
15
  func capture_screenshot(params: Dictionary) -> Dictionary:
16
- return _capture(_host.get_tree().root, params)
16
+ return await _capture(_host.get_tree().root, params)
17
17
 
18
18
 
19
19
  func capture_viewport(params: Dictionary) -> Dictionary:
20
20
  var viewport_path: String = str(params.get("viewportPath", ""))
21
21
  if viewport_path.is_empty():
22
- return capture_screenshot(params)
22
+ return await capture_screenshot(params)
23
23
 
24
24
  var standing: Dictionary = Values.node_at(_host.get_tree().root, viewport_path)
25
25
  if standing.has("message"):
@@ -28,7 +28,7 @@ func capture_viewport(params: Dictionary) -> Dictionary:
28
28
  if not node is Viewport:
29
29
  return {"type": "error", "message": "Node is not a Viewport: " + viewport_path}
30
30
  var viewport: Viewport = node
31
- return _capture(viewport, params)
31
+ return await _capture(viewport, params)
32
32
 
33
33
 
34
34
  ## The server names the file, so a game cannot point it at a path of its own choosing; a call
@@ -66,6 +66,13 @@ func _capture(viewport: Viewport, params: Dictionary) -> Dictionary:
66
66
  ),
67
67
  }
68
68
 
69
+ # The game announces itself before its first frame, and until a frame has been drawn the texture
70
+ # holds nothing the game drew: blank on one machine, solid white on another, which a caller took
71
+ # for the game flashing white on boot. Measured on 4.7.2 with both renderers, only the first
72
+ # processed frame reads it so; the next one holds the scene.
73
+ while Engine.get_frames_drawn() == 0:
74
+ await _host.get_tree().process_frame
75
+
69
76
  var viewport_texture: ViewportTexture = viewport.get_texture()
70
77
  if viewport_texture == null:
71
78
  return {"type": "error", "message": "No viewport texture available"}
@@ -118,8 +118,6 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
118
118
  payload = ImportPipeline.new(_log).get_import_options(params)
119
119
  "set_import_options":
120
120
  payload = ImportPipeline.new(_log).set_import_options(params)
121
- "reimport_resource":
122
- payload = ImportPipeline.new(_log).reimport_resource(params)
123
121
  "list_export_presets":
124
122
  payload = ImportPipeline.new(_log).list_export_presets(params)
125
123
  "validate_project":