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.
- package/build/cli.js +396 -96
- package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +64 -15
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +50 -1
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +10 -3
- package/build/godot/operations/godot_operations.gd +0 -2
- package/build/godot/operations/import_pipeline.gd +250 -85
- package/build/index.js +405 -108
- package/build/keeper.js +205 -23
- package/package.json +1 -1
|
@@ -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
|
|
15
|
-
##
|
|
16
|
-
##
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
30
|
-
var watched: int = filesystem.
|
|
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
|
|
38
|
+
push_error("gdharness could not watch for completed scans: %d" % watched)
|
|
33
39
|
|
|
34
40
|
|
|
35
|
-
func
|
|
36
|
-
|
|
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
|
|
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
|
-
"
|
|
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":
|