Repository: KoBeWi/Godot-Project-Builds
Branch: master
Commit: e0c524339de4
Files: 277
Total size: 832.2 KB
Directory structure:
gitextract_0jov5i6a/
├── .gitattributes
├── .gitignore
├── Icons/
│ ├── Add.svg.import
│ ├── ArrowDown.svg.import
│ ├── ArrowLeft.svg.import
│ ├── ArrowRight.svg.import
│ ├── ArrowUp.svg.import
│ ├── Back.svg.import
│ ├── Copy.svg.import
│ ├── Duplicate.svg.import
│ ├── Edit.svg.import
│ ├── Folder.svg.import
│ ├── Icon.png.import
│ ├── Inherit.svg.import
│ ├── MissingIcon.svg.import
│ ├── Paste.svg.import
│ ├── Play.svg.import
│ ├── Remove.svg.import
│ └── Script.svg.import
├── LICENSE.txt
├── Media/
│ └── .gdignore
├── Nodes/
│ ├── Command.gd
│ ├── Command.gd.uid
│ ├── Command.tscn
│ ├── GUI/
│ │ ├── DeleteButton.tscn
│ │ ├── DirectorySelector.tscn
│ │ ├── Disablabler.gd
│ │ ├── Disablabler.gd.uid
│ │ ├── ExitShortcut.tres
│ │ ├── StringContainer.gd
│ │ ├── StringContainer.gd.uid
│ │ └── StringContainer.tscn
│ ├── Hourglass.png.import
│ ├── PresetTemplate.tscn
│ ├── ProjectEntry.tscn
│ ├── RoutinePreview.tscn
│ ├── Task.gd
│ ├── Task.gd.uid
│ ├── TaskContainer.tscn
│ └── TaskPreview.tscn
├── README.md
├── Scenes/
│ ├── Execution.gd
│ ├── Execution.gd.uid
│ ├── Execution.tscn
│ ├── Main.gd
│ ├── Main.gd.uid
│ ├── Main.tscn
│ ├── ProjectManager.gd
│ ├── ProjectManager.gd.uid
│ ├── ProjectManager.tscn
│ ├── RoutineBuilder.gd
│ ├── RoutineBuilder.gd.uid
│ └── RoutineBuilder.tscn
├── Scripts/
│ ├── Data.gd
│ ├── Data.gd.uid
│ └── Templates/
│ └── Task/
│ ├── EmptyTask.gd
│ └── EmptyTask.gd.uid
├── Tasks/
│ ├── ClearDirectory.tscn
│ ├── CopyFiles.tscn
│ ├── CustomTask.tscn
│ ├── ExportProject.tscn
│ ├── ExportProjectFromTemplate.tscn
│ ├── ExportTask.gd
│ ├── ExportTask.gd.uid
│ ├── PackZIP.tscn
│ ├── ScriptTask/
│ │ ├── BaseScriptTask.gd
│ │ ├── BaseScriptTask.gd.uid
│ │ ├── ClearDirectory.gd
│ │ ├── ClearDirectory.gd.uid
│ │ ├── CopyFiles.gd
│ │ ├── CopyFiles.gd.uid
│ │ ├── PackZIP.gd
│ │ └── PackZIP.gd.uid
│ ├── ScriptTask.gd
│ ├── ScriptTask.gd.uid
│ ├── SubRoutine.tscn
│ ├── UploadEpic.tscn
│ ├── UploadGOG.tscn
│ ├── UploadItch.tscn
│ └── UploadSteam.tscn
├── Tests/
│ ├── GutConfig.json
│ ├── Projects/
│ │ ├── .gdignore
│ │ └── TestProject1/
│ │ ├── DeepDir/
│ │ │ ├── DirFile1.txt
│ │ │ ├── DirFile2.txt
│ │ │ └── SubDir/
│ │ │ └── SubDirFile1.txt
│ │ ├── EmptyDir/
│ │ │ └── .gdignore
│ │ ├── File1.txt
│ │ ├── MixedDir/
│ │ │ ├── MdFile1.md
│ │ │ ├── MdFile2.md
│ │ │ ├── TxtFile1.txt
│ │ │ └── TxtFile2.txt
│ │ ├── project.godot
│ │ └── project_builds_config.txt
│ ├── TestExecution.gd
│ └── TestExecution.gd.uid
├── addons/
│ ├── Prefab/
│ │ ├── Prefab.gd
│ │ └── Prefab.gd.uid
│ ├── ProjectBuilder/
│ │ ├── ProjectBuilderPlugin.gd
│ │ ├── ProjectBuilderPlugin.gd.uid
│ │ └── plugin.cfg
│ └── gut/
│ ├── GutScene.gd
│ ├── GutScene.gd.uid
│ ├── GutScene.tscn
│ ├── LICENSE.md
│ ├── UserFileViewer.gd
│ ├── UserFileViewer.gd.uid
│ ├── UserFileViewer.tscn
│ ├── autofree.gd
│ ├── autofree.gd.uid
│ ├── awaiter.gd
│ ├── awaiter.gd.uid
│ ├── cli/
│ │ ├── gut_cli.gd
│ │ ├── gut_cli.gd.uid
│ │ ├── optparse.gd
│ │ └── optparse.gd.uid
│ ├── collected_script.gd
│ ├── collected_script.gd.uid
│ ├── collected_test.gd
│ ├── collected_test.gd.uid
│ ├── comparator.gd
│ ├── comparator.gd.uid
│ ├── compare_result.gd
│ ├── compare_result.gd.uid
│ ├── diff_formatter.gd
│ ├── diff_formatter.gd.uid
│ ├── diff_tool.gd
│ ├── diff_tool.gd.uid
│ ├── double_templates/
│ │ ├── function_template.txt
│ │ ├── init_template.txt
│ │ └── script_template.txt
│ ├── double_tools.gd
│ ├── double_tools.gd.uid
│ ├── doubler.gd
│ ├── doubler.gd.uid
│ ├── dynamic_gdscript.gd
│ ├── dynamic_gdscript.gd.uid
│ ├── fonts/
│ │ ├── AnonymousPro-Bold.ttf.import
│ │ ├── AnonymousPro-BoldItalic.ttf.import
│ │ ├── AnonymousPro-Italic.ttf.import
│ │ ├── AnonymousPro-Regular.ttf.import
│ │ ├── CourierPrime-Bold.ttf.import
│ │ ├── CourierPrime-BoldItalic.ttf.import
│ │ ├── CourierPrime-Italic.ttf.import
│ │ ├── CourierPrime-Regular.ttf.import
│ │ ├── LobsterTwo-Bold.ttf.import
│ │ ├── LobsterTwo-BoldItalic.ttf.import
│ │ ├── LobsterTwo-Italic.ttf.import
│ │ ├── LobsterTwo-Regular.ttf.import
│ │ └── OFL.txt
│ ├── gui/
│ │ ├── BottomPanelShortcuts.gd
│ │ ├── BottomPanelShortcuts.gd.uid
│ │ ├── BottomPanelShortcuts.tscn
│ │ ├── GutBottomPanel.gd
│ │ ├── GutBottomPanel.gd.uid
│ │ ├── GutBottomPanel.tscn
│ │ ├── GutControl.gd
│ │ ├── GutControl.gd.uid
│ │ ├── GutControl.tscn
│ │ ├── GutRunner.gd
│ │ ├── GutRunner.gd.uid
│ │ ├── GutRunner.tscn
│ │ ├── GutSceneTheme.tres
│ │ ├── MinGui.tscn
│ │ ├── NormalGui.tscn
│ │ ├── OutputText.gd
│ │ ├── OutputText.gd.uid
│ │ ├── OutputText.tscn
│ │ ├── ResizeHandle.gd
│ │ ├── ResizeHandle.gd.uid
│ │ ├── ResizeHandle.tscn
│ │ ├── ResultsTree.gd
│ │ ├── ResultsTree.gd.uid
│ │ ├── ResultsTree.tscn
│ │ ├── RunAtCursor.gd
│ │ ├── RunAtCursor.gd.uid
│ │ ├── RunAtCursor.tscn
│ │ ├── RunResults.gd
│ │ ├── RunResults.gd.uid
│ │ ├── RunResults.tscn
│ │ ├── Settings.tscn
│ │ ├── ShortcutButton.gd
│ │ ├── ShortcutButton.gd.uid
│ │ ├── ShortcutButton.tscn
│ │ ├── arrow.png.import
│ │ ├── editor_globals.gd
│ │ ├── editor_globals.gd.uid
│ │ ├── gut_config_gui.gd
│ │ ├── gut_config_gui.gd.uid
│ │ ├── gut_gui.gd
│ │ ├── gut_gui.gd.uid
│ │ ├── gut_user_preferences.gd
│ │ ├── gut_user_preferences.gd.uid
│ │ ├── panel_controls.gd
│ │ ├── panel_controls.gd.uid
│ │ ├── play.png.import
│ │ ├── script_text_editor_controls.gd
│ │ └── script_text_editor_controls.gd.uid
│ ├── gut.gd
│ ├── gut.gd.uid
│ ├── gut_cmdln.gd
│ ├── gut_cmdln.gd.uid
│ ├── gut_config.gd
│ ├── gut_config.gd.uid
│ ├── gut_plugin.gd
│ ├── gut_plugin.gd.uid
│ ├── gut_to_move.gd
│ ├── gut_to_move.gd.uid
│ ├── gut_vscode_debugger.gd
│ ├── gut_vscode_debugger.gd.uid
│ ├── hook_script.gd
│ ├── hook_script.gd.uid
│ ├── icon.png.import
│ ├── images/
│ │ ├── Folder.svg.import
│ │ ├── Script.svg.import
│ │ ├── green.png.import
│ │ ├── red.png.import
│ │ └── yellow.png.import
│ ├── inner_class_registry.gd
│ ├── inner_class_registry.gd.uid
│ ├── input_factory.gd
│ ├── input_factory.gd.uid
│ ├── input_sender.gd
│ ├── input_sender.gd.uid
│ ├── junit_xml_export.gd
│ ├── junit_xml_export.gd.uid
│ ├── lazy_loader.gd
│ ├── lazy_loader.gd.uid
│ ├── logger.gd
│ ├── logger.gd.uid
│ ├── method_maker.gd
│ ├── method_maker.gd.uid
│ ├── one_to_many.gd
│ ├── one_to_many.gd.uid
│ ├── orphan_counter.gd
│ ├── orphan_counter.gd.uid
│ ├── parameter_factory.gd
│ ├── parameter_factory.gd.uid
│ ├── parameter_handler.gd
│ ├── parameter_handler.gd.uid
│ ├── plugin.cfg
│ ├── printers.gd
│ ├── printers.gd.uid
│ ├── result_exporter.gd
│ ├── result_exporter.gd.uid
│ ├── script_parser.gd
│ ├── script_parser.gd.uid
│ ├── signal_watcher.gd
│ ├── signal_watcher.gd.uid
│ ├── source_code_pro.fnt
│ ├── source_code_pro.fnt.import
│ ├── spy.gd
│ ├── spy.gd.uid
│ ├── strutils.gd
│ ├── strutils.gd.uid
│ ├── stub_params.gd
│ ├── stub_params.gd.uid
│ ├── stubber.gd
│ ├── stubber.gd.uid
│ ├── summary.gd
│ ├── summary.gd.uid
│ ├── test.gd
│ ├── test.gd.uid
│ ├── test_collector.gd
│ ├── test_collector.gd.uid
│ ├── thing_counter.gd
│ ├── thing_counter.gd.uid
│ ├── utils.gd
│ ├── utils.gd.uid
│ ├── version_conversion.gd
│ ├── version_conversion.gd.uid
│ ├── version_numbers.gd
│ ├── version_numbers.gd.uid
│ ├── warnings_manager.gd
│ └── warnings_manager.gd.uid
├── export_presets.cfg
├── project.godot
└── project_builds_config.txt
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
.gitattributes export-ignore
.gitignore export-ignore
LICENSE.txt export-ignore
README.md export-ignore
Media export-ignore
Tests export-ignore
addons/gut export-ignore
================================================
FILE: .gitignore
================================================
.godot/
.export/
.vscode/
.editorconfig
================================================
FILE: Icons/Add.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://da1w5je87d6kc"
path="res://.godot/imported/Add.svg-4650b3c697b6839f9aaa41e5dae42c99.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Add.svg"
dest_files=["res://.godot/imported/Add.svg-4650b3c697b6839f9aaa41e5dae42c99.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/ArrowDown.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bidwkeg0fiqcn"
path="res://.godot/imported/ArrowDown.svg-26309e74d52d72e71bb12cc3f043a6a4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/ArrowDown.svg"
dest_files=["res://.godot/imported/ArrowDown.svg-26309e74d52d72e71bb12cc3f043a6a4.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/ArrowLeft.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cp6em75230mi1"
path="res://.godot/imported/ArrowLeft.svg-8d8b9602b89315b7a20fcbae852b1ee0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/ArrowLeft.svg"
dest_files=["res://.godot/imported/ArrowLeft.svg-8d8b9602b89315b7a20fcbae852b1ee0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/ArrowRight.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bv4tqa6ixyi1h"
path="res://.godot/imported/ArrowRight.svg-94e81311cb12d9360d091b2f31b26fc1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/ArrowRight.svg"
dest_files=["res://.godot/imported/ArrowRight.svg-94e81311cb12d9360d091b2f31b26fc1.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/ArrowUp.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://6orgtpcieuls"
path="res://.godot/imported/ArrowUp.svg-070bb575945f18f3f16743ecbc2b9556.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/ArrowUp.svg"
dest_files=["res://.godot/imported/ArrowUp.svg-070bb575945f18f3f16743ecbc2b9556.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/Back.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dt6drd8cw1dhl"
path="res://.godot/imported/Back.svg-c3e09721b4490970ae5f7e525a9bc927.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Back.svg"
dest_files=["res://.godot/imported/Back.svg-c3e09721b4490970ae5f7e525a9bc927.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/Copy.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dgcqwwuv1m48b"
path="res://.godot/imported/Copy.svg-e2e213b265521710a772cfe568c3515f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Copy.svg"
dest_files=["res://.godot/imported/Copy.svg-e2e213b265521710a772cfe568c3515f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/Duplicate.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dnlqsyyx311xx"
path="res://.godot/imported/Duplicate.svg-fb11c0cbdffda6bd84053ae70f58784a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Duplicate.svg"
dest_files=["res://.godot/imported/Duplicate.svg-fb11c0cbdffda6bd84053ae70f58784a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/Edit.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cfbagouqtgqfs"
path="res://.godot/imported/Edit.svg-bb367a6b4627125abbe892de75002153.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Edit.svg"
dest_files=["res://.godot/imported/Edit.svg-bb367a6b4627125abbe892de75002153.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/Folder.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dhuukef617r6s"
path="res://.godot/imported/Folder.svg-7206efeac34d05a6803ca6e96330a38a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Folder.svg"
dest_files=["res://.godot/imported/Folder.svg-7206efeac34d05a6803ca6e96330a38a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/Icon.png.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bwbaksun3rqh4"
path="res://.godot/imported/Icon.png-3c3ffee33c137a7bef3d4ae0dd705a04.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Icon.png"
dest_files=["res://.godot/imported/Icon.png-3c3ffee33c137a7bef3d4ae0dd705a04.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
================================================
FILE: Icons/Inherit.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://csjjbw7dnc51a"
path="res://.godot/imported/Inherit.svg-7dcf09ace19505a7a9e3f2bd1933fa98.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Inherit.svg"
dest_files=["res://.godot/imported/Inherit.svg-7dcf09ace19505a7a9e3f2bd1933fa98.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/MissingIcon.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dwlbcdps7ydkj"
path="res://.godot/imported/MissingIcon.svg-3d686d232f88a0b563b74f5451d11129.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/MissingIcon.svg"
dest_files=["res://.godot/imported/MissingIcon.svg-3d686d232f88a0b563b74f5451d11129.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/Paste.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://3i2umr3kmry6"
path="res://.godot/imported/Paste.svg-c118cb57c4a613cc3b4fb177ad9ce2a5.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Paste.svg"
dest_files=["res://.godot/imported/Paste.svg-c118cb57c4a613cc3b4fb177ad9ce2a5.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/Play.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bwtchwpfrbqsw"
path="res://.godot/imported/Play.svg-a90ebd04990ba96f2b12411054fcefec.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Play.svg"
dest_files=["res://.godot/imported/Play.svg-a90ebd04990ba96f2b12411054fcefec.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/Remove.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://byn071xu1q5op"
path="res://.godot/imported/Remove.svg-fb94c466537cabd8d31d0dfa32623105.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Remove.svg"
dest_files=["res://.godot/imported/Remove.svg-fb94c466537cabd8d31d0dfa32623105.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: Icons/Script.svg.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ckqqfrho1pqcd"
path="res://.godot/imported/Script.svg-1bf269a1aea2aa7aa66daea0346cce3d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Icons/Script.svg"
dest_files=["res://.godot/imported/Script.svg-1bf269a1aea2aa7aa66daea0346cce3d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
================================================
FILE: LICENSE.txt
================================================
MIT License
Copyright (c) 2024 Tomek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Media/.gdignore
================================================
================================================
FILE: Nodes/Command.gd
================================================
extends Control
@onready var time: Label = %Time
@onready var output_label: RichTextLabel = %OutputLabel
var task_text: String
var command: String
var arguments: PackedStringArray
var sensitive_strings: PackedStringArray
var raw_text: String
var error: String
var log_file: FileAccess
var program: ProgramInstance
var timer: float
var finish_code: int
signal success
signal fail
func _ready() -> void:
%TaskText.text = task_text
log_file.store_line("--- " + task_text + " ---\n")
if not error.is_empty():
%Status.text = "Invalid"
%Status.modulate = Color.RED
%Command.text = error
%Command.modulate = Color.RED
%Code.text = ""
%Animation.queue_free()
fail.emit()
set_process(false)
return
raw_text = command + " " + " ".join(arguments)
if sensitive_strings.is_empty():
%Command.text = raw_text
else:
var command_text := raw_text
for string in sensitive_strings:
command_text = command_text.replace(string, "*".repeat(string.length()))
%Command.text = command_text
var pipe_data := OS.execute_with_pipe(command, arguments)
program = ProgramInstance.create_for_pipe(pipe_data)
program.output_line.connect(output_line)
program.start()
func output_line(line: String, is_error: bool):
log_file.store_line(line)
if is_error:
output_label.push_color(Color.RED)
if line.contains("\u001b"):
line = line.\
replace("\u001b[1m", "[b]").\
replace("\u001b[22m", "[/b]").\
replace("\u001b[90m", "[color=gray]").\
replace("\u001b[92m", "[color=green]").\
replace("\u001b[39m", "[/color]").\
replace("\u001b[0m", "")
output_label.append_text(line)
if is_error:
output_label.pop()
output_label.append_text("\n")
func _process(delta: float) -> void:
if program.is_running > 0:
timer += delta
var intime := int(timer)
time.text = "%02d:%02d:%02d" % [intime / 3600, intime / 60 % 60, intime % 60]
return
program.finalize()
set_process(false)
%Animation.queue_free()
if program.result == 0:
%Status.text = "Success"
%Status.modulate = Color.GREEN
%Code.modulate = Color.GREEN
success.emit()
else:
%Status.text = "Fail"
%Status.modulate = Color.RED
%Code.modulate = Color.RED
fail.emit()
%Code.text = str(program.result)
finish_code = program.result
func _on_command_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
DisplayServer.clipboard_set(raw_text)
var copied: Label = %Copied
copied.position = %Command.get_local_mouse_position()
copied.modulate.a = 1.0
copied.show()
var tween := copied.create_tween()
tween.tween_property(copied, ^"modulate:a", 0.0, 0.5).set_delay(1)
tween.tween_callback(copied.hide)
func _exit_tree() -> void:
if not program:
return
if program.is_running > 0:
program.stop()
program.finalize()
class ProgramInstance:
var pid: int
var stdio: FileAccess
var stderr: FileAccess
var finish_mutext: Mutex
var is_running: int
var finalized: bool
var result: int
var io_thread: Thread
var err_thread: Thread
signal output_line(line: String, is_error: bool)
static func create_for_pipe(data: Dictionary) -> ProgramInstance:
var instance := ProgramInstance.new()
if data.is_empty():
return instance
instance.pid = data["pid"]
instance.stdio = data["stdio"]
instance.stderr = data["stderr"]
instance.finish_mutext = Mutex.new()
instance.is_running = 2
return instance
func start():
io_thread = Thread.new()
io_thread.start(pipe_read.bind(stdio))
err_thread = Thread.new()
err_thread.start(pipe_read.bind(stderr))
func pipe_read(pipe: FileAccess):
var is_err := pipe == stderr
var buffer_empty: bool
while is_running > 0:
if pipe.get_error() != OK or not OS.is_process_running(pid):
break
if buffer_empty:
output_line.emit.call_deferred("", is_err)
buffer_empty = false
var line := pipe.get_line()
if line.is_empty():
buffer_empty = true
else:
output_line.emit.call_deferred(line, is_err)
finish_mutext.lock()
is_running -= 1
finish_mutext.unlock()
func stop():
if is_running <= 0:
return
is_running = 0
OS.kill(pid)
stdio.close()
stderr.close()
func finalize():
if finalized:
return
io_thread.wait_to_finish()
err_thread.wait_to_finish()
result = OS.get_process_exit_code(pid)
finalized = true
func toggle_output() -> void:
output_label.visible = not output_label.visible
func copy_output() -> void:
DisplayServer.clipboard_set(output_label.get_parsed_text())
================================================
FILE: Nodes/Command.gd.uid
================================================
uid://c5deg6k04uij2
================================================
FILE: Nodes/Command.tscn
================================================
[gd_scene load_steps=24 format=3 uid="uid://b6hrktoise2ji"]
[ext_resource type="Script" uid="uid://c5deg6k04uij2" path="res://Nodes/Command.gd" id="1_l0kiu"]
[ext_resource type="Texture2D" uid="uid://dno0spmwh4kov" path="res://Nodes/Hourglass.png" id="2_qna3k"]
[ext_resource type="Texture2D" uid="uid://csjjbw7dnc51a" path="res://Icons/Inherit.svg" id="3_kytcc"]
[ext_resource type="Texture2D" uid="uid://dgcqwwuv1m48b" path="res://Icons/Copy.svg" id="4_0o8ph"]
[sub_resource type="AtlasTexture" id="AtlasTexture_tas32"]
atlas = ExtResource("2_qna3k")
region = Rect2(0, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_k5q5b"]
atlas = ExtResource("2_qna3k")
region = Rect2(42, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_01srn"]
atlas = ExtResource("2_qna3k")
region = Rect2(84, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_yxfrt"]
atlas = ExtResource("2_qna3k")
region = Rect2(126, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_7bn30"]
atlas = ExtResource("2_qna3k")
region = Rect2(168, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_kvoov"]
atlas = ExtResource("2_qna3k")
region = Rect2(210, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_m48gl"]
atlas = ExtResource("2_qna3k")
region = Rect2(252, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_1fsum"]
atlas = ExtResource("2_qna3k")
region = Rect2(294, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_f1t7x"]
atlas = ExtResource("2_qna3k")
region = Rect2(336, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_nbd80"]
atlas = ExtResource("2_qna3k")
region = Rect2(378, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_1pyat"]
atlas = ExtResource("2_qna3k")
region = Rect2(420, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_a8rgr"]
atlas = ExtResource("2_qna3k")
region = Rect2(462, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_t6u08"]
atlas = ExtResource("2_qna3k")
region = Rect2(504, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_blf8y"]
atlas = ExtResource("2_qna3k")
region = Rect2(546, 0, 42, 42)
[sub_resource type="AtlasTexture" id="AtlasTexture_046av"]
atlas = ExtResource("2_qna3k")
region = Rect2(588, 0, 42, 42)
[sub_resource type="SpriteFrames" id="SpriteFrames_sf5fb"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_tas32")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_k5q5b")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_01srn")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_yxfrt")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_7bn30")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_kvoov")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_m48gl")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_1fsum")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_f1t7x")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_nbd80")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_1pyat")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_a8rgr")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_t6u08")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_blf8y")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_046av")
}],
"loop": true,
"name": &"default",
"speed": 5.0
}]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_gis47"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0, 0, 0, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nmsdb"]
bg_color = Color(0, 0, 0, 0.12549)
[sub_resource type="GDScript" id="GDScript_jmu3s"]
script/source = "extends RichTextLabel
@export var maximum_height: float
func _ready() -> void:
minimum_size_changed.connect(on_minimum_size_changed)
func on_minimum_size_changed():
if get_minimum_size().y > 300:
custom_minimum_size.y = 300
fit_content = false
"
[node name="Command" type="PanelContainer"]
offset_right = 1000.0
offset_bottom = 26.0
size_flags_horizontal = 3
script = ExtResource("1_l0kiu")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 8
theme_override_constants/margin_right = 8
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
custom_minimum_size = Vector2(1000, 0)
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="VBoxContainer2" type="VBoxContainer" parent="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Status" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
text = "Executing Command"
[node name="Time" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
text = "00:00:00"
horizontal_alignment = 1
[node name="Code" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(30, 0)
layout_mode = 2
horizontal_alignment = 1
[node name="Control" type="Control" parent="MarginContainer/VBoxContainer/HBoxContainer/Code"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
grow_horizontal = 2
grow_vertical = 2
[node name="Animation" type="AnimatedSprite2D" parent="MarginContainer/VBoxContainer/HBoxContainer/Code/Control"]
unique_name_in_owner = true
scale = Vector2(0.47619, 0.47619)
sprite_frames = SubResource("SpriteFrames_sf5fb")
autoplay = "default"
frame = 6
frame_progress = 0.150927
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TaskText" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 0
text = "Task"
horizontal_alignment = 1
text_overrun_behavior = 3
[node name="Command" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 0
text = "What"
autowrap_mode = 2
[node name="Copied" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Command"]
unique_name_in_owner = true
visible = false
z_index = 1
layout_mode = 0
offset_left = 24.0
offset_top = 23.0
offset_right = 183.0
offset_bottom = 54.0
theme_override_colors/font_color = Color(1, 1, 0, 1)
theme_override_styles/normal = SubResource("StyleBoxFlat_gis47")
text = "Copied to clipboard"
[node name="HSeparator" type="HSeparator" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="Output" type="VBoxContainer" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/Output"]
layout_mode = 2
alignment = 1
[node name="Button2" type="Button" parent="MarginContainer/VBoxContainer/Output/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 4
text = "Toggle Output"
icon = ExtResource("3_kytcc")
[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/Output/HBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 4
text = "Copy Output"
icon = ExtResource("4_0o8ph")
[node name="OutputLabel" type="RichTextLabel" parent="MarginContainer/VBoxContainer/Output"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
focus_mode = 2
theme_override_styles/normal = SubResource("StyleBoxFlat_nmsdb")
fit_content = true
scroll_following = true
autowrap_mode = 2
threaded = true
progress_bar_delay = -1
selection_enabled = true
script = SubResource("GDScript_jmu3s")
maximum_height = 600.0
[connection signal="gui_input" from="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/TaskText" to="." method="_on_command_gui_input"]
[connection signal="gui_input" from="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer/Command" to="." method="_on_command_gui_input"]
[connection signal="pressed" from="MarginContainer/VBoxContainer/Output/HBoxContainer/Button2" to="." method="toggle_output"]
[connection signal="pressed" from="MarginContainer/VBoxContainer/Output/HBoxContainer/Button" to="." method="copy_output"]
================================================
FILE: Nodes/GUI/DeleteButton.tscn
================================================
[gd_scene load_steps=4 format=3 uid="uid://m6a3ajkud85h"]
[ext_resource type="Texture2D" uid="uid://byn071xu1q5op" path="res://Icons/Remove.svg" id="1_xvgi5"]
[sub_resource type="GDScript" id="GDScript_v5hcl"]
script/source = "extends Button
@onready var label: Label = $Label
signal confirmed
var timer: float
func _ready() -> void:
set_process(false)
func _pressed() -> void:
if label.visible:
confirmed.emit()
else:
timer = 0.5
label.show()
set_process(true)
func _process(delta: float) -> void:
timer -= delta
if timer <= 0:
label.hide()
set_process(false)
"
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_liddr"]
content_margin_left = 2.0
content_margin_right = 2.0
bg_color = Color(0.145098, 0.156863, 0.188235, 1)
[node name="DeleteButton" type="Button"]
icon = ExtResource("1_xvgi5")
script = SubResource("GDScript_v5hcl")
[node name="Label" type="Label" parent="."]
visible = false
z_index = 1
layout_mode = 1
anchors_preset = 11
anchor_left = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 2.0
offset_right = 174.0
grow_horizontal = 0
grow_vertical = 2
theme_override_styles/normal = SubResource("StyleBoxFlat_liddr")
text = "Click again to confirm"
================================================
FILE: Nodes/GUI/DirectorySelector.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://cyl1d6reu3mk4"]
[ext_resource type="Texture2D" uid="uid://dhuukef617r6s" path="res://Icons/Folder.svg" id="1_34bwh"]
[sub_resource type="GDScript" id="GDScript_af0jw"]
script/source = "extends HBoxContainer
enum Mode { SELECT_FOLDER, OPEN_FILE, SAVE_FILE, SELECT_WHATEVER, FAKE_SELECT_FOLDER }
enum Scope { GLOBAL, PROJECT }
enum MissingMode { IGNORE, WARN, ERROR }
@onready var line_edit: LineEdit = $LineEdit
@onready var file_dialog: FileDialog = $FileDialog
@export var mode: Mode
@export var scope: Scope
@export var missing_mode: MissingMode
@export var filters: PackedStringArray
@export var empty_is_valid: bool
signal path_changed
var text: String:
set(t):
line_edit.text = t
validate()
get:
return line_edit.text
func _ready() -> void:
line_edit.text_changed.connect(path_changed.emit.unbind(1))
match mode:
Mode.SELECT_FOLDER, Mode.FAKE_SELECT_FOLDER:
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
Mode.OPEN_FILE:
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
Mode.SAVE_FILE:
file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
Mode.SELECT_WHATEVER:
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_ANY
if scope == Scope.PROJECT:
file_dialog.current_dir = Data.project_path
file_dialog.filters = filters
validate()
func open_dialog() -> void:
file_dialog.current_dir = text.get_base_dir()
file_dialog.popup_centered_ratio(0.5)
func _path_selected(path: String) -> void:
if scope == Scope.PROJECT:
text = path.trim_prefix(Data.project_path)
text = text.trim_prefix(\"/\")
else:
text = path
path_changed.emit()
func validate():
if missing_mode == MissingMode.IGNORE:
return
var path := line_edit.text
if empty_is_valid and path.is_empty():
modulate = Color.WHITE
return
if scope == Scope.PROJECT:
path = Data.project_path.path_join(path)
var valid: bool
if not line_edit.text.is_empty():
match mode:
Mode.OPEN_FILE, Mode.SAVE_FILE, Mode.FAKE_SELECT_FOLDER:
valid = FileAccess.file_exists(path)
Mode.SELECT_FOLDER:
valid = DirAccess.dir_exists_absolute(path)
Mode.SELECT_WHATEVER:
valid = FileAccess.file_exists(path) or DirAccess.dir_exists_absolute(path)
if valid:
modulate = Color.WHITE
return
match missing_mode:
MissingMode.WARN:
modulate = Color.YELLOW
MissingMode.ERROR:
modulate = Color.RED
"
[node name="DirectorySelector" type="HBoxContainer"]
offset_right = 256.0
offset_bottom = 31.0
script = SubResource("GDScript_af0jw")
[node name="LineEdit" type="LineEdit" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="Button" type="Button" parent="."]
layout_mode = 2
icon = ExtResource("1_34bwh")
[node name="FileDialog" type="FileDialog" parent="."]
access = 2
[connection signal="text_changed" from="LineEdit" to="." method="validate" unbinds=1]
[connection signal="pressed" from="Button" to="." method="open_dialog"]
[connection signal="dir_selected" from="FileDialog" to="." method="_path_selected"]
[connection signal="file_selected" from="FileDialog" to="." method="_path_selected"]
================================================
FILE: Nodes/GUI/Disablabler.gd
================================================
extends Control
@onready var initial_mouse := mouse_filter
@onready var initial_focus := focus_mode
func set_noexist(noexist: bool):
modulate.a = float(not noexist)
process_mode = PROCESS_MODE_DISABLED if noexist else PROCESS_MODE_INHERIT
mouse_filter = MOUSE_FILTER_IGNORE if noexist else initial_mouse
focus_mode = Control.FOCUS_NONE if noexist else initial_focus
================================================
FILE: Nodes/GUI/Disablabler.gd.uid
================================================
uid://dlv30xan3u6wy
================================================
FILE: Nodes/GUI/ExitShortcut.tres
================================================
[gd_resource type="Shortcut" load_steps=2 format=3 uid="uid://d0olqx0bjlhp1"]
[sub_resource type="InputEventAction" id="InputEventAction_i6wlr"]
action = &"ui_cancel"
[resource]
events = [SubResource("InputEventAction_i6wlr")]
================================================
FILE: Nodes/GUI/StringContainer.gd
================================================
extends ScrollContainer
@onready var strings: HBoxContainer = %Strings
var string_prefab: PackedScene
signal changed
func _ready() -> void:
string_prefab = Prefab.create(%StringPrefab)
func _add_string() -> LineEdit:
var string: LineEdit = string_prefab.instantiate()
string.gui_input.connect(string_gui_input.bind(string))
string.text_changed.connect(emit_changed.unbind(1))
strings.add_child(string)
return string
func string_gui_input(event: InputEvent, edit: LineEdit):
if not edit.editable:
return
if event is InputEventKey:
if event.pressed and event.keycode == KEY_DELETE:
edit.queue_free()
edit.tree_exited.connect(emit_changed, CONNECT_DEFERRED)
func get_strings() -> PackedStringArray:
return strings.get_children().map(func(line_edit: LineEdit) -> String: return line_edit.text)
func set_strings(strins: PackedStringArray):
for s in strins:
var strin := _add_string()
strin.text = s
func emit_changed():
changed.emit()
================================================
FILE: Nodes/GUI/StringContainer.gd.uid
================================================
uid://b640rv8k8mmlp
================================================
FILE: Nodes/GUI/StringContainer.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://bfbht01onlf1a"]
[ext_resource type="Texture2D" uid="uid://da1w5je87d6kc" path="res://Icons/Add.svg" id="1_nij3a"]
[ext_resource type="Script" uid="uid://b640rv8k8mmlp" path="res://Nodes/GUI/StringContainer.gd" id="1_oa0rg"]
[node name="StringContainer" type="ScrollContainer"]
offset_right = 187.0
offset_bottom = 28.0
horizontal_scroll_mode = 2
vertical_scroll_mode = 0
script = ExtResource("1_oa0rg")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="Strings" type="HBoxContainer" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="StringPrefab" type="LineEdit" parent="HBoxContainer/Strings"]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
expand_to_text_length = true
[node name="Button" type="Button" parent="HBoxContainer"]
layout_mode = 2
icon = ExtResource("1_nij3a")
[connection signal="pressed" from="HBoxContainer/Button" to="." method="_add_string"]
[connection signal="pressed" from="HBoxContainer/Button" to="." method="emit_changed" flags=3]
================================================
FILE: Nodes/Hourglass.png.import
================================================
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dno0spmwh4kov"
path="res://.godot/imported/Hourglass.png-c86f4464194dba98591f5477463c57ed.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Nodes/Hourglass.png"
dest_files=["res://.godot/imported/Hourglass.png-c86f4464194dba98591f5477463c57ed.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
================================================
FILE: Nodes/PresetTemplate.tscn
================================================
[gd_scene load_steps=8 format=3 uid="uid://bsgg4mwgvd5vi"]
[ext_resource type="PackedScene" uid="uid://bfbht01onlf1a" path="res://Nodes/GUI/StringContainer.tscn" id="1_egcl1"]
[ext_resource type="PackedScene" uid="uid://cyl1d6reu3mk4" path="res://Nodes/GUI/DirectorySelector.tscn" id="2_at7ma"]
[ext_resource type="Texture2D" uid="uid://dnlqsyyx311xx" path="res://Icons/Duplicate.svg" id="3_yself"]
[ext_resource type="Texture2D" uid="uid://csjjbw7dnc51a" path="res://Icons/Inherit.svg" id="4_ricvi"]
[ext_resource type="PackedScene" uid="uid://m6a3ajkud85h" path="res://Nodes/GUI/DeleteButton.tscn" id="5_bn1pp"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qhetb"]
content_margin_left = 8.0
content_margin_top = 8.0
content_margin_right = 8.0
content_margin_bottom = 8.0
draw_center = false
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(1, 1, 1, 0.501961)
[sub_resource type="GDScript" id="GDScript_4k674"]
script/source = "extends Control
var inherit: String
var parent: Node
signal changed
func get_data() -> Dictionary:
var data: Dictionary
data[\"name\"] = %Name.text
data[\"custom_features\"] = %CustomFeatures.get_strings()
data[\"include_filters\"] = %IncludeFilters.get_strings()
data[\"exclude_filters\"] = %ExcludeFilters.get_strings()
data[\"export_path\"] = %ExportPath.text
data[\"inherit\"] = inherit
return data
func set_data(data: Dictionary):
%Name.text = data[\"name\"]
%CustomFeatures.set_strings(data[\"custom_features\"])
%IncludeFilters.set_strings(data[\"include_filters\"])
%ExcludeFilters.set_strings(data[\"exclude_filters\"])
%ExportPath.text = data[\"export_path\"]
inherit = data.get(\"inherit\", \"\")
update_inheritance()
validate_name()
func connect_duplicate(callback: Callable):
%Duplicate.pressed.connect(callback)
func connect_inherit(callback: Callable):
%Inherit.pressed.connect(callback)
func connect_delete(callback: Callable):
%Delete.confirmed.connect(callback)
func update_inheritance():
if inherit.is_empty():
return
if not parent:
for template in get_parent().get_children():
if template.get_template_name() == inherit:
parent = template
parent.changed.connect(update_inheritance)
break
if parent.get_template_name() != inherit:
inherit = parent.get_template_name()
self_modulate = Color.YELLOW
%Inherits.show()
%Inherits.text = \"Inherits: %s\" % inherit
$Timer.start()
func sync_strings() -> void:
var other_template: Dictionary = parent.get_data()
update_strings(%CustomFeatures, other_template[\"custom_features\"])
update_strings(%IncludeFilters, other_template[\"include_filters\"])
update_strings(%ExcludeFilters, other_template[\"exclude_filters\"])
func update_strings(string_container: Control, strings: PackedStringArray):
for string: LineEdit in string_container.strings.get_children():
if string.text in strings:
string.editable = false
elif not string.editable:
string.free()
var my_strings: PackedStringArray = string_container.get_strings()
for string in strings:
if not string in my_strings:
var strin: LineEdit = string_container._add_string()
strin.text = string
strin.editable = false
func get_template_name() -> String:
return %Name.text
func validate_name() -> void:
if get_parent().get_children().any(func(template: Node) -> bool: return template != self and template.get_template_name() == get_template_name()):
%Name.modulate = Color.RED
else:
%Name.modulate = Color.WHITE
func emit_changed():
changed.emit()
"
[node name="PresetTemplate" type="PanelContainer"]
offset_right = 771.0
offset_bottom = 246.0
theme_override_styles/panel = SubResource("StyleBoxFlat_qhetb")
script = SubResource("GDScript_4k674")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="GridContainer" type="GridContainer" parent="VBoxContainer"]
layout_mode = 2
columns = 2
[node name="Label" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
text = "Name"
horizontal_alignment = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/GridContainer"]
layout_mode = 2
[node name="Name" type="LineEdit" parent="VBoxContainer/GridContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
caret_blink = true
caret_blink_interval = 0.5
[node name="Inherits" type="Label" parent="VBoxContainer/GridContainer/HBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 0, 1)
text = "Inherits: %s"
[node name="Label2" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
text = "Custom Features"
horizontal_alignment = 2
[node name="CustomFeatures" parent="VBoxContainer/GridContainer" instance=ExtResource("1_egcl1")]
unique_name_in_owner = true
layout_mode = 2
[node name="Label3" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
text = "Include Filters"
horizontal_alignment = 2
[node name="IncludeFilters" parent="VBoxContainer/GridContainer" instance=ExtResource("1_egcl1")]
unique_name_in_owner = true
layout_mode = 2
[node name="Label4" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
text = "Exclude Filters"
horizontal_alignment = 2
[node name="ExcludeFilters" parent="VBoxContainer/GridContainer" instance=ExtResource("1_egcl1")]
unique_name_in_owner = true
layout_mode = 2
[node name="Label5" type="Label" parent="VBoxContainer/GridContainer"]
layout_mode = 2
text = "Export Base"
horizontal_alignment = 2
[node name="ExportPath" parent="VBoxContainer/GridContainer" instance=ExtResource("2_at7ma")]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
scope = 1
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="Duplicate" type="Button" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 6
text = "Duplicate"
icon = ExtResource("3_yself")
alignment = 0
[node name="Inherit" type="Button" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 6
text = "Inherit"
icon = ExtResource("4_ricvi")
alignment = 0
[node name="Delete" parent="VBoxContainer/HBoxContainer" instance=ExtResource("5_bn1pp")]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 6
text = "Delete"
[node name="Timer" type="Timer" parent="."]
wait_time = 0.5
one_shot = true
[connection signal="text_changed" from="VBoxContainer/GridContainer/HBoxContainer/Name" to="." method="validate_name" unbinds=1]
[connection signal="text_changed" from="VBoxContainer/GridContainer/HBoxContainer/Name" to="." method="emit_changed" unbinds=1]
[connection signal="changed" from="VBoxContainer/GridContainer/CustomFeatures" to="." method="emit_changed"]
[connection signal="changed" from="VBoxContainer/GridContainer/IncludeFilters" to="." method="emit_changed"]
[connection signal="changed" from="VBoxContainer/GridContainer/ExcludeFilters" to="." method="emit_changed"]
[connection signal="timeout" from="Timer" to="." method="sync_strings"]
================================================
FILE: Nodes/ProjectEntry.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://cc3kmk1iwkf5p"]
[sub_resource type="GDScript" id="GDScript_8e84h"]
script/source = "extends MarginContainer
func set_project(path: String, callback: Callable):
%Path.text = path
var config := ConfigFile.new()
config.load(path.path_join(\"project.godot\"))
%Name.text = config.get_value(\"application\", \"config/name\", \"[unnamed]\")
var icon_path: String = config.get_value(\"application\", \"config/icon\", \"\")
var icon := get_icon(icon_path, path)
if not icon:
icon = preload(\"uid://dwlbcdps7ydkj\")
%Icon.texture = icon
var local_config_file := Data.get_project_config_path(path, config)
var task := FileAccess.open(path.path_join(local_config_file), FileAccess.READ)
if not task:
%Tasks.text = \"Project builder not configured\"
else:
var data: Dictionary = str_to_var(task.get_as_text())
%Tasks.text = \"%d routines\" % data.get(\"routines\", []).size()
$Button.pressed.connect(callback.bind(path))
func get_icon(path: String, project_path: String) -> Texture2D:
if path.is_empty():
return null
if path.begins_with(\"uid://\"):
var uid_cache := FileAccess.open(project_path.path_join(\".godot/uid_cache.bin\"), FileAccess.READ)
if not uid_cache:
return null
var entry_count := uid_cache.get_32()
for i in entry_count:
var id := uid_cache.get_64()
var length := uid_cache.get_32()
var buffer := uid_cache.get_buffer(length)
if ResourceUID.id_to_text(id) == path:
path = buffer.get_string_from_ascii()
break
path = path.replace(\"res:/\", project_path)
if FileAccess.file_exists(path):
var image := Image.load_from_file(path)
if image:
return ImageTexture.create_from_image(image)
return null
"
[node name="ProjectEntry" type="MarginContainer"]
custom_minimum_size = Vector2(800, 0)
offset_right = 495.0
offset_bottom = 113.0
size_flags_horizontal = 3
size_flags_vertical = 3
script = SubResource("GDScript_8e84h")
[node name="Button" type="Button" parent="."]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
mouse_filter = 2
theme_override_constants/margin_left = 8
theme_override_constants/margin_top = 8
theme_override_constants/margin_right = 8
theme_override_constants/margin_bottom = 8
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
mouse_filter = 2
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 2
[node name="Icon" type="TextureRect" parent="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(128, 128)
layout_mode = 2
mouse_filter = 2
expand_mode = 1
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 2
[node name="Name" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_font_sizes/font_size = 40
text = "Project Name"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Tasks" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
text = "Tasks"
horizontal_alignment = 1
vertical_alignment = 1
[node name="Path" type="Label" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Path"
================================================
FILE: Nodes/RoutinePreview.tscn
================================================
[gd_scene load_steps=9 format=3 uid="uid://cctfsldanqcig"]
[ext_resource type="Texture2D" uid="uid://dnlqsyyx311xx" path="res://Icons/Duplicate.svg" id="1_1u4fl"]
[ext_resource type="Texture2D" uid="uid://cp6em75230mi1" path="res://Icons/ArrowLeft.svg" id="1_17425"]
[ext_resource type="Texture2D" uid="uid://bwtchwpfrbqsw" path="res://Icons/Play.svg" id="1_hqapv"]
[ext_resource type="Texture2D" uid="uid://cfbagouqtgqfs" path="res://Icons/Edit.svg" id="1_m46hf"]
[ext_resource type="Texture2D" uid="uid://bv4tqa6ixyi1h" path="res://Icons/ArrowRight.svg" id="2_0d0yc"]
[ext_resource type="PackedScene" uid="uid://m6a3ajkud85h" path="res://Nodes/GUI/DeleteButton.tscn" id="6_hh3q2"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_e2baq"]
content_margin_left = 8.0
content_margin_top = 8.0
content_margin_right = 8.0
content_margin_bottom = 8.0
draw_center = false
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(1, 1, 1, 0.501961)
[sub_resource type="GDScript" id="GDScript_s214c"]
script/source = "extends Control
@onready var move_left: Button = %MoveLeft
@onready var move_right: Button = %MoveRight
var data: Dictionary
func _ready() -> void:
update_buttons.call_deferred()
func set_routine_data(d: Dictionary):
data = d
%Name.text = data[\"name\"]
if data[\"tasks\"].is_empty():
%TaskCount.text = \"No Tasks\"
%Execute.disabled = true
return
%TaskCount.text %= data[\"tasks\"].size()
var task_list: PackedStringArray
for task in data[\"tasks\"]:
var task_info: Dictionary = Data.tasks[task[\"scene\"]]
task_list.append(task_info[\"name\"])
%TaskList.text = \"\\n\".join(task_list)
func connect_execute(target: Callable):
%Execute.pressed.connect(target)
func connect_edit(target: Callable):
%Edit.pressed.connect(target)
func connect_duplicate(target: Callable):
%Duplicate.pressed.connect(target)
func delete() -> void:
Data.routines.erase(data)
Data.queue_save_local_config()
queue_free()
func update_buttons():
move_left.disabled = get_index() == 0
move_right.disabled = get_index() == get_parent().get_child_count() - 1
func refresh_parent():
owner.sync_routines()
Data.queue_save_local_config()
for child in get_parent().get_children():
child.update_buttons()
func _on_move_left_pressed() -> void:
get_parent().move_child(self, get_index() - 1)
refresh_parent()
func _on_move_right_pressed() -> void:
get_parent().move_child(self, get_index() + 1)
refresh_parent()
"
[node name="Routinepreview" type="PanelContainer"]
custom_minimum_size = Vector2(400, 0)
offset_right = 303.0
offset_bottom = 203.0
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_e2baq")
script = SubResource("GDScript_s214c")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="MoveLeft" type="Button" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
icon = ExtResource("1_17425")
[node name="Name" type="Label" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 20
text = "Routine Name"
horizontal_alignment = 1
[node name="MoveRight" type="Button" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
icon = ExtResource("2_0d0yc")
[node name="TaskCount" type="Label" parent="VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
text = "%d tasks"
horizontal_alignment = 1
[node name="TaskList" type="Label" parent="VBoxContainer"]
unique_name_in_owner = true
modulate = Color(0.8, 0.8, 0.8, 1)
layout_mode = 2
size_flags_vertical = 3
horizontal_alignment = 1
[node name="GridContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="Execute" type="Button" parent="VBoxContainer/GridContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Execute"
icon = ExtResource("1_hqapv")
alignment = 0
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Edit" type="Button" parent="VBoxContainer/GridContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Edit"
icon = ExtResource("1_m46hf")
alignment = 0
[node name="Duplicate" type="Button" parent="VBoxContainer/GridContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Duplicate"
icon = ExtResource("1_1u4fl")
alignment = 0
[node name="DeleteButton" parent="VBoxContainer" instance=ExtResource("6_hh3q2")]
layout_mode = 2
size_flags_horizontal = 8
text = "Delete"
[connection signal="pressed" from="VBoxContainer/HBoxContainer/MoveLeft" to="." method="_on_move_left_pressed"]
[connection signal="pressed" from="VBoxContainer/HBoxContainer/MoveRight" to="." method="_on_move_right_pressed"]
[connection signal="confirmed" from="VBoxContainer/DeleteButton" to="." method="delete"]
================================================
FILE: Nodes/Task.gd
================================================
extends Control
class_name Task
## If [code]true[/code], the script will receive [method _initialize_project] and [method _process_file] calls when a project is opened.
@export var has_static_configuration: bool
## Set [code]true[/code] when the task is using any sensitive setting, like passwords.
@export var has_sensitive_data: bool
## Default values for data properties. Set them inside [method _initialize].
var defaults: Dictionary#[String, Variant]
## The actual data values. Use [method _load] and [method _store] for serializing it.
var data: Dictionary#[String, Variant]
## Set this for displaying error message in [method _prevalidate] and [method _validate].
var error_message: String
## The name that displays on the list of tasks. It should be a constant string.
func _get_task_name() -> String:
return "Empty Task"
## The name that displays when the task is being executed. You can provide extra details about the configuration from [member data].
func _get_execute_string() -> String:
return _get_task_name()
## Called when a project is loaded if [member has_static_configuration] is [code]true[/code]. You can use it to initialize static data specific to a project, which should be global to each instance of this task. If you have a cache file created in [method _end_project_scan], it should be loaded here.
static func _initialize_project() -> void:
pass
## Called when a project is loaded for the first time or when the user starts rescan, if [member has_static_configuration] is [code]true[/code]. You can use it to clear file cache if you have one.
static func _begin_project_scan() -> void:
pass
## Called after [method _begin_project_scan] if [member has_static_configuration] is [code]true[/code]. This method will be invoked for every file in the project. You can use it to collect configuration files specific for this task. Since this method is not called on every project load, you need to cache the result.
static func _process_file(path: String) -> void:
pass
## Called when project scanning is finished. Here you can cache the result of scan using [method get_cache_file].
static func _end_project_scan() -> void:
pass
## Called when the task is being initialized. Use it to fill [member defaults] and initialize child [Control] node values.
func _initialize() -> void:
pass
## Called at the beginning of a routine to check for obvious mistakes and missing information. Return [code]false[/code] if the task can't run and fill [member error_message].
func _prevalidate() -> bool:
return true
## Called just before task is executed to check for invalid configuration, especially when it depends on previous tasks. Return [code]false[/code] if the task can't run and fill [member error_message].
func _validate() -> bool:
return true
## Return the executable name that will run this task.
func _get_command() -> String:
return ""
## Return the arguments provided for launching the executable.
func _get_arguments() -> PackedStringArray:
return []
## Called before running the task. Use it to setup necessary configuration for running the task, especially temporary one.
func _prepare() -> void:
pass
## Called after the task has finished or when aborting execution. Use it to cleanup temporary configuration created in [method _prepare].
func _cleanup() -> void:
pass
## Called when the task data is being loaded. Use [member data] to retrieve data and push it to child [Control] nodes.
func _load() -> void:
pass
## Called when the task data is being saved. Store any persistent information in the [member data] [Dictionary]. Any missing property will be filled from [member defaults].
func _store() -> void:
pass
## Return the description of this task and its parameters.
func _get_task_info() -> PackedStringArray:
return [
"Task description",
"Argument Name|Description",
]
func load_data(new_data: Dictionary) -> void:
data = new_data.merged(defaults)
_load()
func store_data() -> Dictionary:
_store()
return data.merged(defaults)
static func create_instance(scene: String) -> Task:
return Data.tasks[scene]["scene_cache"].instantiate()
## Returns a [FileAccess] instance for the specified file and flags or [code]null[/code] if file can't be created/loaded. [param file] is name of the file, which will be located in a cache directory.
static func get_cache_file(file: String, flags: FileAccess.ModeFlags) -> FileAccess:
DirAccess.make_dir_absolute("user://FileCache")
return FileAccess.open("user://FileCache".path_join(str(Data.project_path.get_file(), " - ", file)), flags)
================================================
FILE: Nodes/Task.gd.uid
================================================
uid://qg3dm7um8fj4
================================================
FILE: Nodes/TaskContainer.tscn
================================================
[gd_scene load_steps=7 format=3 uid="uid://fktnevmh7mia"]
[ext_resource type="Texture2D" uid="uid://bwtchwpfrbqsw" path="res://Icons/Play.svg" id="1_0viqh"]
[ext_resource type="Texture2D" uid="uid://6orgtpcieuls" path="res://Icons/ArrowUp.svg" id="1_efkxy"]
[ext_resource type="Texture2D" uid="uid://bidwkeg0fiqcn" path="res://Icons/ArrowDown.svg" id="2_r8s42"]
[ext_resource type="Texture2D" uid="uid://dgcqwwuv1m48b" path="res://Icons/Copy.svg" id="4_fcytw"]
[ext_resource type="PackedScene" uid="uid://m6a3ajkud85h" path="res://Nodes/GUI/DeleteButton.tscn" id="5_ewh0j"]
[sub_resource type="GDScript" id="GDScript_f3a8a"]
script/source = "extends PanelContainer
var task: Task
signal copied
func _ready() -> void:
update_buttons.call_deferred()
func set_task_scene(scene: String) -> Task:
task = Task.create_instance(scene)
%TaskName.text = task._get_task_name()
%TaskHere.add_child(task)
return task
func update_buttons():
%Up.disabled = get_index() == 0
%Down.disabled = get_index() == get_parent().get_child_count() - 1
func move_up() -> void:
get_parent().move_child(self, get_index() - 1)
update_buttons()
func move_down() -> void:
get_parent().move_child(self, get_index() + 1)
update_buttons()
func erase() -> void:
queue_free()
func copy():
var task_data := Dictionary()
task_data[\"scene\"] = task.scene_file_path.get_file().get_basename()
task_data[\"data\"] = task.store_data()
Data.copied_task = task_data
copied.emit()
func test_task() -> void:
owner.test_task(task)
"
[node name="TaskContainer" type="PanelContainer"]
offset_right = 587.0
offset_bottom = 48.0
script = SubResource("GDScript_f3a8a")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
layout_mode = 2
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_right = 4
[node name="Button" type="Button" parent="VBoxContainer/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 0
tooltip_text = "Execute This Task"
icon = ExtResource("1_0viqh")
[node name="TaskName" type="Label" parent="VBoxContainer/MarginContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Task Name"
horizontal_alignment = 1
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 8
[node name="Up" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Move Up"
icon = ExtResource("1_efkxy")
[node name="Down" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Move Down"
icon = ExtResource("2_r8s42")
[node name="Button3" type="Button" parent="VBoxContainer/MarginContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Copy"
icon = ExtResource("4_fcytw")
[node name="VSeparator" type="VSeparator" parent="VBoxContainer/MarginContainer/HBoxContainer"]
layout_mode = 2
[node name="DeleteButton" parent="VBoxContainer/MarginContainer/HBoxContainer" instance=ExtResource("5_ewh0j")]
layout_mode = 2
[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer"]
layout_mode = 2
[node name="TaskHere" type="MarginContainer" parent="VBoxContainer/PanelContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/margin_left = 10
theme_override_constants/margin_top = 10
theme_override_constants/margin_right = 10
theme_override_constants/margin_bottom = 10
[connection signal="pressed" from="VBoxContainer/MarginContainer/Button" to="." method="test_task"]
[connection signal="pressed" from="VBoxContainer/MarginContainer/HBoxContainer/Up" to="." method="move_up"]
[connection signal="pressed" from="VBoxContainer/MarginContainer/HBoxContainer/Down" to="." method="move_down"]
[connection signal="pressed" from="VBoxContainer/MarginContainer/HBoxContainer/Button3" to="." method="copy"]
[connection signal="confirmed" from="VBoxContainer/MarginContainer/HBoxContainer/DeleteButton" to="." method="erase"]
================================================
FILE: Nodes/TaskPreview.tscn
================================================
[gd_scene load_steps=4 format=3 uid="uid://dv8u0se3f7m7s"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_75d6r"]
content_margin_left = 8.0
content_margin_top = 8.0
content_margin_right = 8.0
content_margin_bottom = 8.0
draw_center = false
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(1, 1, 1, 0.501961)
[sub_resource type="GDScript" id="GDScript_d6iij"]
script/source = "extends Control
@onready var task_description: RichTextLabel = %TaskDescription
var task: Dictionary
func _ready() -> void:
var task_instance := Task.create_instance(task[\"scene\"])
%TaskContainer.add_child(task_instance)
%TaskName.text = task_instance._get_task_name()
var info := task_instance._get_task_info()
task_description.append_text(info[0])
if info.size() < 2:
return
task_description.append_text(\"\\n\\n[center]Parameters[/center]\")
for i in range(1, info.size()):
var argument := info[i]
task_description.append_text(\"\\n[b]%s:[/b] %s\" % [argument.get_slice(\"|\", 0), argument.get_slice(\"|\", 1)])
"
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6p44c"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0, 0, 0, 0.25098)
[node name="TaskPreview" type="PanelContainer"]
offset_right = 344.0
offset_bottom = 221.0
theme_override_styles/panel = SubResource("StyleBoxFlat_75d6r")
script = SubResource("GDScript_d6iij")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
theme_override_constants/separation = 20
[node name="TaskContainer" type="PanelContainer" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
theme_override_styles/panel = SubResource("StyleBoxFlat_6p44c")
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TaskName" type="Label" parent="HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 20
text = "Task Name"
horizontal_alignment = 1
[node name="TaskDescription" type="RichTextLabel" parent="HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
bbcode_enabled = true
fit_content = true
scroll_active = false
================================================
FILE: README.md
================================================
#
Godot Project Builder
Project Builder is an automation tool made in Godot Engine and focused on exporting and publishing Godot projects. The builder works by running "routines", which are composed of "tasks". Each task involves running a predefined command with customized arguments. The builder comes with numberous built-in tasks, which include exporting project, publishing to popular stores (Steam, GOG, Epic, itch.io), and file operations. Task setup is fully visual.
Running the tool from source requires at least Godot 4.3. You can pick an executable from the official releases. Project Builder can be used with projects from older Godot versions, including Godot 3.
## Overview
Project Builder can be launched from a release executable or from source, using Godot Engine. When you start Project Builder, the first thing you will see is the project list.

It's similar to Godot's own Project Manager, but more minimal. It automatically lists the projects you have registered in Godot, with information whether Project Builder is configured for that project and how many routines it has. You can provide a custom project list file (in case you run in self-contained mode or just want the list to be better organized), by setting it in the text field at the top.
Clicking a project will open the main project view.

It shows projects title at the top and is divided into 4 tabs: Routines, Templates, Tasks and Config. They are explained below.
### Routines
This tab shows overview of all routines in your project. You can create a routine by clicking Add Routine button, which will add an empty one:

From the main view, routines can be executed, edited, duplicated or deleted. When you create and edit a routine, it will be automatically saved in a dedicated project build configuration file stored in your project's folder. By default the file is located at `res://project_builds_config.txt`. When a routine has tasks, it will show a brief list.

#### Editing Routines
By clicking the Edit button in a routine, you will open an editing screen where you can assign tasks to a routine.

On this screen you'll see a list of all tasks assigned to the routine. You can add a task by clicking Add Task button and selecting a task from the list.

After the task is added, you can use its controls to setup it. Reference about task usages and its configuration is available in the [Tasks](#tasks) tab or in the [task list](#list-of-available-tasks) in this README.
At the top of the task preview is its title and various buttons. The top-left button allows you to quickly execute a single task from the routine (see [Executing](#executing)). The top-right has buttons for rearranging tasks, a Copy button and a Delete button. Copy will copy the task data into an internal clipboard and you will be able to paste the task into the same routine, other routines, or even other projects. The Delete button requires double-click to activate, to prevent accidental deleting. A deleted task cannot be recovered, as currently the Project Builder does not support undo/redo.
Above the task list is the routine name field, where you can rename it. Note that each routine needs an unique name and if there is a conflict, you won't be able to leave this screen (unless you use the Discard Changes button). Aside from name, there is a dropdown for controlling what happens when a task fails (see [Executing](#executing)). Clicking Back will go back to the main screen and save the routine. The routine is also saved when you close the Project Builder.
#### Executing
By clicking the Execute button in a routine or using using the quick-execute button in routine editing screen, you will enter the execution mode. If you did this by accident, you have 2 seconds (default) to press Escape and cancel task (afterwards you have to close the application if you want to stop it). The initial delay can be configured in Global Config.

Execution works simply by running tasks one after another. A task will run a command with arguments and display the output in real time. Once a task finishes, you will see whether it succeeded or failed, its exit code, and the time it took to execute.

Top of the task is its execution name (i.e. a more detailed task name) and the exact command that is executed when the task runs (in the example above, the task invoked Godot with `--export-release`). Clicking that command will copy it to the clipboard (so you can e.g. paste it into terminal, edit and run manually). Below is Toggle Output button that allows you to hide output and Copy Output, which will copy it to the clipboard. Output is also written to log files that can be accessed from Open Log Folder on the project's main screen.
Normally when a task fails, the whole execution will stop. You can configure that in routine settings; the other option is to continue executing normally. When a routine finishes, you will see the total time it took to execute.

The routine may also fail before it starts running. This can happen when some tasks have prevalidation step, i.e. they check prematurely if their configuration is invalid and guaranteed to fail the task. E.g. Upload Steam task will fail if you didn't provide Steam CMD path.

### Preset Templates
The second tab in the main screen. It allows defining templates for export presets. It's a way to share properties like file filters or feature tags between mutliple presets, making multiple export targets easier to manage.

A template is composed of Name, Custom Features, Include Filters, Exclude Filters and Export Base. Name has to be unique; in case of conflict, duplicate templates will be ignored. Custom Features are equivalent of the custom features in Features tab of export dialog, while Filters are equivalent of Include/Exclude Filters in Resources tab. Images for reference:
 
Export Base is a base directory used when exporting with this template. It's later prefixed with the actual file (see [Export Project From Template](#export-project-from-template)).
Templates can inherit other templates. Inheriting template will include features and filters of the parent and auto-update them, so you can e.g. make one configuration with your include filters and make all other templates use it. You no longer have to worry about updating all export presets when you add a new file extension. Export Base is not inherited.
### Tasks
The third tab in the main screen. It's a readonly list of all available tasks, with their descriptions and argument list. You can use it as a quick reference of what does what. More detailed information is available in this README at [List of Available Tasks](#list-of-available-tasks).

The tasks are listed from the Tasks directory of Project Builder. Each task is a separate scene with the root inheriting a special Task class, which extends Control. All configuration controls are part of the task scene.
#### Custom Tasks
You can create your own tasks by adding new scenes to the Tasks folder. The easiest way to create a Task is by opening the Project Builder source project and creating a new scene using Task class for root. There is a script template that makes it easier.
When implementing methods, refer to the documentation of Task class and the existing tasks implementation (the default tasks have their code in built-in scripts). If you are using a stand-alone version of the Project Builder, you'll have to copy your new task to the Tasks folder of your installation. In the future there might be support for project local tasks, but currently this feature is not a priority.
### Config
The last tab in the main screen. Here you can configure Project Builder. It includes both global config that applies to all your projects and a local config, which is per-project. The local config is stored in the builds config file mentioned before, while global config is stored in Project Builder's user data folder.

Both configurations are also organized into foldable tabs of related settings.
#### Global Configuration
- **Godot**
- **Godot Path:** Path to the Godot executable. It will be used for exporting projects.
- **Steam**
- **Steam CMD Path:** Path to `steamcmd.exe` that comes with Steam SDK. It's needed if you want to publish builds to Steam.
- **Username:** Login used for authentication when uploading games. Note that Project Builder does not support console input, so you can't use account that uses Steam mobile app for authentication (it requires inputting a code with each login). E-mail 2FA only requires single authentication, which you can do externally.
- **Password:** Password for the above account. Project Builder will automatically hide password when executing a command, but while the password is not stored in the project folder, it can be found in the global configuration file of Project Builder.
- **GOG**
- **Pipeline Builder Path:** Path to Pipeline Builder executable used to publish builds to GOG.
- **Username:** Used for authentication when uploading games, just like in Steam.
- **Password:** Password for the above account.
- **Epic**
- **Build Patch Tool Path:** Path to Build Patch Tool executable used to publish builds to Epic Games.
- **Organization ID**, **Client ID:** The organization and client ID provided for your developer account.
- **Client Secret:** The client secret provided for your developer account. This is a sensitive data, like the Steam and GOG passwords.
- **Client Secret Env Variable:** Name of the environment variable that contains your Client Secret. This is an alternate authentication method supported by Build Patch Tool and it will be used instead of Client Secret if provided.
- **Itch**
- **Butler Path:** Path to itch.io's Butler executable used to publish builds to itch.io.
- **Username:** Login used for authentication when uploading games. Note that unlike the other upload tools, you have to launch butler and login to cache your credentials (this is not handled by Project Builder). It has to be done once.
#### Local Configuration
- **Godot**
- **Project Builder Configuration Path:** Path to Project Builder configuration file (by default `project_builds_config.txt`). You need to press Apply button to actually change this setting. Doing so will also automatically move the file. Unlike other local settings, this one is stored in `project.godot`, under hidden `_project_builder_config_path` setting. Note that in Local Config the path is not editable and you can change only the directory. Manually changing the path is not advised.
- **Godot Exec For This Project:** Executable used for exporting the current project. If you leave this empty, the default one will be used. This option is useful when you have projects using different Godot versions.
- **Epic**
- **Product ID**, **Artifact ID:** Product and artifact IDs provided for your game.
- **Cloud Directory:** Directory required by Build Patch Tool for its operation. It's effectively a cache directory, so you can add it to your VCS ignore list.
- **Itch**
- **Game Name:** Name of your game. This has to match the itch.io page (e.g. if your game is at `username.itch.io/my_game`, Game Name should be `my_game`).
- **Default Channel:** App channel to which the game will be uploaded if not specified by the task.
- **Version File:** File to use for `userversion-file` argument. If not provided, the argument will not be passed. If "Use Project Version" is enabled in upload task, the file is ignored.
#### Project Scan
At the bottom of the Config tab is a Run Project Scan button. It launches a scan of all files in your project. This is required by some tasks and will run automatically when the project is opened for the first time. If a task is relying on scan results (as noted [in its description](#list-of-available-tasks)), you may want to run the scan if the data is outdated. It has to be done manually, for performance reasons (crawling every file in the project can be expensive).
#### Required Paths
This applies to tasks and configuration. You will sometimes notice that a file/directory field is marked yellow or red. This denotes required fields. If a field is yellow, it means that the target file/directory must exist at the time the task is executed, so at most it has to be created as a result of preceding tasks. If a field is red it means that the task will fail at the prevalidation stage.

### Project Builder Plugin
Project Builder has an optional plugin that allows for better integration with your project. Using the plugin you can change the location of your project's build config file and run Project Builder directly for your project. At the bottom of the Config tab you can find the plugin status in your project:

It will tell you whether the plugin is installed or not, and if it needs update. If the plugin is not installed or outdated, you can click Install Project Builder Plugin to add the plugin to your project (you'll need to manually enable it in Project Settings).
The addon also adds a Project Builder submenu to Project -> Tools menu. From there you can run Project Builder or directly execute any of the registered routines. These options are also added to Command Palette under Project Builder category.
### Running From Command Line
While Project Builder has a visual execution mode, you can use it as a tool for configuring automated tasks and run them from command line. Project Builder supports a number of command line arguments. You need to pass them after `--` in your command. These arguments are:
- `--projects-file-path`: Overrides the path of project list file.
- `--open-project `: Starts Project Builder and opens the provided `project`.
- `--execute-routine `: On launch, executes the routine `routine`.
- `--exit`: If executing routine, automatically exits after finishing or error.
Example line for running builder for project at `C:/My Project` in headless mode, executing routine `Export Game` and exiting:
```
godot --path "C:/Project Builder" -- --open-project "C:/My Project" --execute_routine "Export Game" --exit
```
The `--path` part can be omitted when running from Project Builder installation directory.
## List of Available Tasks
This sections lists all default tasks shipped with Project Builder.
### Clear Directory Files

Removes all files in a directory. The files are not deleted permanently, instead they are moved to a dedicated `Trash` directory in Project Builder's user data. If you used this task accidentally or want to recover files, you can find them in that directory. Note that Trash holds only one copy of the file, so if you "delete" it again, it will be overwritten.
This task is useful for preparing export directory, to make sure that it doesn't have lefotver files.
**Options**
- **Target Directory:** The directory in your project that's going to be cleaned.
- **Include Files:** Filters applied to each processed file to determine whether it should be included. Use it when you want to delete only some files of the directory. If empty, files will be included by default.
- **Exclude Files:** Filters applied to each processed file to determine whether it should be excluded. Use it when your directory has files that shouldn't be deleted. If empty, no file will be excluded by default.
### Copy Files(s)

Copies a file or directory from source path to destination. It can also recursively copy whole directory. If files already exist at the target location, they will be overwritten. If the target directory does not exist, it will be created.
Useful for copying additional files not included with export.
**Options**
- **Source Path:** The source path to copy. If a file is selected, only this file will be copied. If a directory is selected, all files will be copied to the target location.
- **Target Path:** The destination where the files will be copied. If source is a file and target is a directory, the file will be copied to the directory and keep its name. If target is a file, the file will be copied and renamed to the new name. If source is a directory, all files will be copied to the target directory.
- **Recursive:** When copying directory, this option enables copying sub-directories.
### Custom Task

An arbitrary task for operations not covered by other tasks, to use when it's not worth it to make a new task type. The task allows to specify a raw command and argument list that will be invoked during execution. It's very flexible, but requires manually providing all necessary data.
Note that working directory is undefined, so you should use absolute paths both for command and arguments. You can use `$project` placeholder, which will be automatically substituted with path to the current project, e.g. `$project/.godot`.
**Options**
- **Command:** Command that will be executed. It will be invoked like from a terminal. You can use `$godot` or `$local_godot` special names to use current Godot executable or project-configured Godot executable respectively.
- **Arguments:** List of arguments provided for the command. They are automatically wrapped in quotes when necessary.
### Export Project

Exports the project using the given preset (it will list the presets defined in `export_presets.cfg`). Also allows to provide custom export path. If the target directory does not exist, it will be automatically created. This task uses the Godot executable specified in [config](#config). If you are exporting a Godot 3 project, make sure to provide a Godot 3 executable.
**Options**
- **Preset:** Export preset used to export the project.
- **Custom Path:** If provided, the preset's path will be overriden. If empty, the default path will be used.
- **Debug:** Determines whether debug or release build is exported.
### Export Project From Template

Exports the project using the given base preset and [preset template](#preset-templates). It works by temporarily adding a new preset to `export_presets.cfg` file. The new preset will be a copy of the base preset with some properties modified based on the template.
**Options**
- **Base Preset:** Base export preset from the project's preset list.
- **Preset Template:** The template from the list of templates defined in Project Builder.
- **Path Suffix:** Partial path that will be joined with Export Base property of the [template](#preset-templates). E.g. you can set the base path to `Export/Steam` and then you can specify suffix based on platform, like `Windows/MyGame.exe` (suffix has to include the file name). If Export Base was not defined, the default path from export preset will be used.
- **Debug:** Determines whether debug or release build is exported.
### Pack ZIP

Packs the given directory (including subfolders) to a ZIP archive. You can specify include and exclude filters for files, which work using globbing. You can define both include and exclude filters and they will be both evaluated for each file.
Useful when you want to pack an exported project to share it. The ZIP is created using Godot's ZIPPacker class.
**Options**
- **Source Directory:** The directory which is going to be packed. The ZIP will have the same structure.
- **Target File Path:** The path to the resulting ZIP file.
- **Include Files:** Filters applied to each processed file to determine whether it should be included. Use it when you want to pack only some files of the directory. If empty, files will be included by default.
- **Exclude Files:** Filters applied to each processed file to determine whether it should be excluded. Use it when your directory has files that shouldn't be packed. If empty, no file will be excluded by default.
### Sub-Routine

Includes another routine as a task. The other routine's tasks will be seamlessly added to the execution list. This is useful when you e.g. have export and upload in separate routines, but want one that does everything.
**Options**
- **Routine:** The routine that will be processed by this task, from the list of routines defined in your project.
### Upload Epic

Uploads files to Epic Games using Build Patch Tool. Uploading to Epic requires executing a lengthy command, so this task is especially helpful in getting it right. Before using it you need to fill Epic sections in your global and local configs.
This task uses the project's version, which is read from `application/config/version` Project Setting of your project. Make sure it's unique for each upload. You can use [my auto versioning addon](https://github.com/KoBeWi/Godot-Auto-Export-Version) to handle that easily ;) Note that Epic imposes some limits on the version string (related to available characters and length). Refer to the [Build Patch Tool manual](https://dev.epicgames.com/docs/epic-games-store/publishing-tools/uploading-binaries/bpt-instructions-150#how-to-upload-a-binary) for details, but unless you use crazy version strings, you won't run into a problem.
**Options**
- **Build Root:** The root directory of the files you want to upload.
- **Executable Name:** The executable file used for launching the application (relative to the root directory).
- **Version Prefix:** String prefixed to your project's version. Epic requires each upload to have unique version, so you can define a per-platform prefix for each task. It will be appended to your project version with a dash, e.g. if your project has version `1.0` and prefix is `win`, the uploaded version will be `1.0-win`.
### Upload GOG

Uploads files to GOG using Pipeline Builder. This task has no configuration in itself, you only pick the GOG's JSON configuration file that will be used. Make sure to fill the GOG global configuration before using this task. Like Upload Epic, this task uses project's version.
**Options**
- **JSON File:** The JSON file used for upload. The list will show only file names, their full path is in tooltip. The list of JSONs will automatically include all GOG's JSON files in your project [when the project is scanned](#project-scan).
- **Branch:** Optional branch where the files will be uploaded.
- **Branch Password:** Password for the branch, if it's protected.
### Upload Itch

Uploads files to itch.io using butler. Before using it, fill the global and local configuration for Itch and make sure butler has cached credentials.
**Options**
- **Source Folder:** The folder containing your exported project files.
- **Channel:** Channel where the files will be uploaded. The name is semi-important, refer to [butler's manual](https://itch.io/docs/butler/pushing.html).
### Upload Steam

Uploads files to Steam using Steam CMD. Just like GOG, the whole setup is inside configuration file (but here it's VDF).
**Options**
- **VDF File:** The VDF file used for upload. Same as JSON in Upload GOG task.
## Closing Words
Project Builder evolved from build tools I created for Lumencraft. I wrote a small build system in Python, but eventually it got so many options that running from command line got annoying. From this experience I came up with a system where you can construct your various tasks from predefined building blocks, which are way easier to use.
If you have ideas for more useful tasks, feel free to suggest them in the Issues page.
___
You can find all my addons on my [profile page](https://github.com/KoBeWi).
================================================
FILE: Scenes/Execution.gd
================================================
extends Control
signal finished
@onready var task_limbo: Node2D = %TaskLimbo
@onready var commands_container: VBoxContainer = %CommandsContainer
@onready var error_container: Control = %Errors
var routine: Dictionary
var current_task_index: int
var current_task: Task
var task_in_progress: bool
var task_error: String
var on_fail: int
var sensitive_strings: PackedStringArray
var log_file: FileAccess
var fail_count: int
var separator_prefab: PackedScene
func _ready() -> void:
separator_prefab = Prefab.create(%Separator)
var errors: PackedStringArray
routine = Data.current_routine
on_fail = routine["on_fail"]
for task in routine["tasks"]:
var task_instance := Task.create_instance(task["scene"])
task_limbo.add_child(task_instance)
task_instance._initialize()
task_instance.load_data(task["data"])
if not task_instance._prevalidate():
errors.append("%s: %s" % [task_instance._get_execute_string(), task_instance.error_message])
var wait := 2
if not errors.is_empty():
%ErrorsParent.show()
%Delay.hide()
for error in errors:
var label := Label.new()
label.text = error
label.modulate = Color.RED
error_container.add_child(label)
finish()
return
else:
wait = Data.global_config["execution_delay"]
%Delay.text %= wait
if wait > 0:
await get_tree().create_timer(wait).timeout
%Delay.hide()
DirAccess.make_dir_recursive_absolute("user://BuildLogs")
var logs: Array[String]
logs.assign(DirAccess.get_files_at("user://BuildLogs"))
if logs.size() >= 10:
logs.sort_custom(func(log1: String, log2: String):
return FileAccess.get_modified_time("user://BuildLogs".path_join(log1)))
for logg in logs.slice(9):
DirAccess.remove_absolute("user://BuildLogs".path_join(logg))
for setting in Data.sensitive_settings:
var string: String = Data.global_config.get(setting, "")
if string.is_empty():
string = Data.local_config.get(setting, "")
if not string.is_empty():
sensitive_strings.append(string)
var filename := "user://" + ("BuildLogs/Log-%s.log" % Time.get_datetime_string_from_system()).replace(":", "-")
log_file = FileAccess.open(filename, FileAccess.WRITE)
next_command()
func next_command():
if current_task_index == task_limbo.get_child_count():
finish()
return
log_file.store_line("")
current_task = task_limbo.get_child(current_task_index)
var command := preload("res://Nodes/Command.tscn").instantiate()
command.log_file = log_file
if current_task._validate():
task_in_progress = true
current_task._prepare()
else:
command.error = current_task.error_message
command.task_text = current_task._get_execute_string()
command.command = current_task._get_command()
command.arguments = current_task._get_arguments()
if current_task.has_sensitive_data:
command.sensitive_strings = sensitive_strings
command.success.connect(task_finished.bind(true), CONNECT_ONE_SHOT | CONNECT_DEFERRED)
command.fail.connect(task_finished.bind(false), CONNECT_ONE_SHOT | CONNECT_DEFERRED)
commands_container.add_child(command)
current_task_index += 1
func task_finished(success: bool):
current_task._cleanup()
task_in_progress = false
var command := commands_container.get_child(-1)
var intime: int = command.timer
log_file.store_line("\n> Finished with code %d, time: %02d:%02d:%02d." % [command.finish_code, intime / 3600, intime / 60 % 60, intime % 60])
log_file.flush()
commands_container.add_child(separator_prefab.instantiate())
if not success:
fail_count += 1
if on_fail == 0:
finish()
return
next_command()
func finish():
finished.emit()
if Data.auto_exit:
get_tree().quit(fail_count)
return
%Button.show()
var total_time: float
for command in commands_container.get_children():
if &"timer" in command:
total_time += command.timer
var intime := int(total_time)
%Time.text %= [intime / 3600, intime / 60 % 60, intime % 60]
%Time.show()
func go_back() -> void:
get_tree().change_scene_to_packed(Data.main)
func _exit_tree() -> void:
Data.reset_current_routine()
if task_in_progress:
current_task._cleanup()
================================================
FILE: Scenes/Execution.gd.uid
================================================
uid://gj2kvs6p5asi
================================================
FILE: Scenes/Execution.tscn
================================================
[gd_scene load_steps=6 format=3 uid="uid://dqm1wdopgkdkp"]
[ext_resource type="Script" uid="uid://gj2kvs6p5asi" path="res://Scenes/Execution.gd" id="1_rwjih"]
[ext_resource type="Shortcut" uid="uid://d0olqx0bjlhp1" path="res://Nodes/GUI/ExitShortcut.tres" id="2_qbdqm"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_6oq72"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tsvn6"]
content_margin_left = 8.0
content_margin_top = 8.0
content_margin_right = 8.0
content_margin_bottom = 8.0
bg_color = Color(0, 0, 0, 0.501961)
[sub_resource type="StyleBoxLine" id="StyleBoxLine_ykpnb"]
color = Color(1, 1, 1, 1)
grow_begin = -50.0
grow_end = -50.0
thickness = 2
[node name="Execution" type="ScrollContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 3
script = ExtResource("1_rwjih")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="Delay" type="Label" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Starting in %d seconds.
Press Escape to cancel."
horizontal_alignment = 1
[node name="Button" type="Button" parent="VBoxContainer/Delay"]
layout_mode = 0
theme_override_styles/normal = SubResource("StyleBoxEmpty_6oq72")
shortcut = ExtResource("2_qbdqm")
[node name="ErrorsParent" type="PanelContainer" parent="VBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_tsvn6")
[node name="Errors" type="VBoxContainer" parent="VBoxContainer/ErrorsParent"]
unique_name_in_owner = true
layout_mode = 2
[node name="Label" type="Label" parent="VBoxContainer/ErrorsParent/Errors"]
modulate = Color(1, 0, 0, 1)
layout_mode = 2
text = "Some tasks are invalid!"
horizontal_alignment = 1
[node name="CommandsContainer" type="VBoxContainer" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="Separator" type="HSeparator" parent="VBoxContainer/CommandsContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 30
theme_override_styles/separator = SubResource("StyleBoxLine_ykpnb")
[node name="Time" type="Label" parent="VBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
theme_override_font_sizes/font_size = 30
text = "Finished. Total time: %02d:%02d:%02d."
horizontal_alignment = 1
[node name="Button" type="Button" parent="VBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 8
shortcut = ExtResource("2_qbdqm")
shortcut_in_tooltip = false
text = "< Back"
[node name="TaskLimbo" type="Node2D" parent="."]
unique_name_in_owner = true
visible = false
[connection signal="pressed" from="VBoxContainer/Delay/Button" to="." method="go_back"]
[connection signal="pressed" from="VBoxContainer/Button" to="." method="go_back"]
================================================
FILE: Scenes/Main.gd
================================================
extends Control
@onready var template_container: Control = %TemplateContainer
@onready var routine_container: Control = %RoutineContainer
@onready var task_container: Control = %TaskContainer
var task_queue: Array[Dictionary]
var task_queue_index: int
func _ready() -> void:
var config := ConfigFile.new()
config.load(Data.project_path.path_join("project.godot"))
%Title.text %= config.get_value("application", "config/name", "[unnamed]")
if Data.from_plugin:
%Back.hide()
for routine in Data.routines:
add_routine(routine)
for template in Data.templates:
var temp := _add_template_pressed()
temp.set_data(template)
task_queue.assign(Data.tasks.values())
set_physics_process(false)
if Data.first_load:
for task in Data.static_initialize_tasks:
task._initialize_project()
Data.first_load = false
if Data.initial_load:
run_project_scan()
Data.initial_load = false
func process_files(directory: String):
for file in DirAccess.get_files_at(directory):
for task in Data.static_initialize_tasks:
task._process_file(directory.path_join(file))
for dir in DirAccess.get_directories_at(directory):
if not dir.begins_with("."):
process_files(directory.path_join(dir))
func _physics_process(delta: float) -> void:
var task := task_queue[task_queue_index]
var preview := preload("res://Nodes/TaskPreview.tscn").instantiate()
preview.task = task
task_container.add_child(preview)
task_queue_index += 1
if task_queue_index == task_queue.size():
set_physics_process(false)
task_queue.clear()
func _exit_tree() -> void:
if Data.project_path.is_empty():
return
sync_templates()
func _add_template_pressed() -> Control:
var template := preload("res://Nodes/PresetTemplate.tscn").instantiate()
template_container.add_child(template)
template.connect_duplicate(duplicate_template.bind(template))
template.connect_inherit(inherit_template.bind(template))
template.connect_delete(delete_template.bind(template))
return template
func _add_routine_pressed() -> void:
add_routine(Data.create_routine())
func add_routine(data: Dictionary):
var routine := preload("res://Nodes/RoutinePreview.tscn").instantiate()
routine_container.add_child(routine)
routine.owner = self
routine.set_routine_data(data)
routine.connect_execute(exec_routine.bind(data))
routine.connect_edit(edit_routine.bind(data))
routine.connect_duplicate(duplicate_routine.bind(data))
func exec_routine(data: Dictionary):
Data.current_routine = data
get_tree().change_scene_to_file("res://Scenes/Execution.tscn")
func edit_routine(data: Dictionary):
Data.current_routine = data
get_tree().change_scene_to_file("res://Scenes/RoutineBuilder.tscn")
func duplicate_routine(data: Dictionary):
data = data.duplicate(true)
var new_name: String = data["name"]
var suffix := " (Copy)"
var unique: bool
while not unique:
unique = true
for routine in Data.routines:
if routine["name"] == new_name + suffix:
suffix = " (Copy %d)" % (suffix.to_int() + 1)
unique = false
data["name"] = new_name + suffix
add_routine(data)
sync_routines()
Data.queue_save_local_config()
func duplicate_template(template: Control):
var dup := _add_template_pressed()
var data: Dictionary = template.get_data().duplicate()
data["name"] = Data.get_unique_name(Data.templates, data["name"], "(Copy %d)", 0)
dup.set_data(data)
sync_templates()
Data.save_local_config()
for other in template_container.get_children():
if other.get_index() > template.get_index() and other.inherit != template.get_template_name():
template_container.move_child(dup, other.get_index())
break
await get_tree().process_frame
%TemplateScroll.ensure_control_visible(dup)
func inherit_template(template: Control):
var dup := _add_template_pressed()
var data: Dictionary = template.get_data().duplicate()
data["inherit"] = data["name"]
data["name"] = Data.get_unique_name(Data.templates, data["name"], "(Inherited %d)", 0)
dup.set_data(data)
sync_templates()
Data.save_local_config()
for other in template_container.get_children():
if other.get_index() > template.get_index() and other.inherit != template.get_template_name():
template_container.move_child(dup, other.get_index())
break
await get_tree().process_frame
%TemplateScroll.ensure_control_visible(dup)
func delete_template(template: Control):
template.queue_free()
sync_templates()
Data.queue_save_local_config()
func sync_routines():
Data.routines.assign(routine_container.get_children().map(func(routine: Control) -> Dictionary:
return routine.data))
func sync_templates():
Data.templates.assign(template_container.get_children().map(func(template: Control) -> Dictionary:
return template.get_data()))
func go_back() -> void:
sync_templates()
Data.save_local_config()
Data.project_path = ""
get_tree().change_scene_to_file("res://Scenes/ProjectManager.tscn")
func run_project_scan() -> void:
for task in Data.static_initialize_tasks:
task._begin_project_scan()
process_files(Data.project_path)
for task in Data.static_initialize_tasks:
task._end_project_scan()
%ScanFinished.show()
%ScanFinished.modulate.a = 1.0
var tween := create_tween()
tween.tween_property(%ScanFinished, ^"modulate:a", 0.0, 0.5).set_delay(0.5)
tween.tween_callback(%ScanFinished.hide)
func tab_changed(tab: int) -> void:
if tab == 2 and not task_queue.is_empty():
set_physics_process(true)
func open_logs() -> void:
OS.shell_open(ProjectSettings.globalize_path("user://BuildLogs"))
================================================
FILE: Scenes/Main.gd.uid
================================================
uid://bjg08mlxtbkx5
================================================
FILE: Scenes/Main.tscn
================================================
[gd_scene load_steps=19 format=3 uid="uid://ba3dlg1fj2hxv"]
[ext_resource type="Script" uid="uid://bjg08mlxtbkx5" path="res://Scenes/Main.gd" id="1_r1xkq"]
[ext_resource type="Texture2D" uid="uid://dt6drd8cw1dhl" path="res://Icons/Back.svg" id="2_fue2w"]
[ext_resource type="PackedScene" uid="uid://cyl1d6reu3mk4" path="res://Nodes/GUI/DirectorySelector.tscn" id="2_ood4h"]
[ext_resource type="Texture2D" uid="uid://bidwkeg0fiqcn" path="res://Icons/ArrowDown.svg" id="3_50glp"]
[ext_resource type="Texture2D" uid="uid://6orgtpcieuls" path="res://Icons/ArrowUp.svg" id="3_jw32o"]
[ext_resource type="Shortcut" uid="uid://d0olqx0bjlhp1" path="res://Nodes/GUI/ExitShortcut.tres" id="4_bgxb8"]
[ext_resource type="Texture2D" uid="uid://ckqqfrho1pqcd" path="res://Icons/Script.svg" id="6_kgk8y"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_x81aq"]
bg_color = Color(0, 0, 0, 0.12549)
border_width_top = 4
border_color = Color(0.16, 0.16, 0.16, 0.752941)
[sub_resource type="GDScript" id="GDScript_yepl2"]
resource_name = "DebugDiscard"
script/source = "@tool
extends Control
func _validate_property(property: Dictionary) -> void:
if property.name == \"current_tab\" or property.name == \"scroll_vertical\":
property.usage = PROPERTY_USAGE_EDITOR
"
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nuonq"]
content_margin_right = 4.0
bg_color = Color(0, 0, 0, 0.12549)
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_cy4oc"]
content_margin_right = 4.0
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jw32o"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.136, 0.61186665, 0.8, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_v1gob"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.0969, 0.435955, 0.57, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[sub_resource type="Theme" id="Theme_v1gob"]
FoldableContainer/icons/expanded_arrow = ExtResource("3_jw32o")
FoldableContainer/icons/folded_arrow = ExtResource("3_50glp")
FoldableContainer/styles/title_collapsed_hover_panel = SubResource("StyleBoxFlat_jw32o")
FoldableContainer/styles/title_collapsed_panel = SubResource("StyleBoxFlat_v1gob")
FoldableContainer/styles/title_hover_panel = SubResource("StyleBoxFlat_jw32o")
FoldableContainer/styles/title_panel = SubResource("StyleBoxFlat_v1gob")
[sub_resource type="GDScript" id="GDScript_rk2qg"]
resource_name = "Config"
script/source = "extends HBoxContainer
func _ready() -> void:
register_global_setting(%GlobalGodot, \"godot_path\", OS.get_executable_path())
register_global_setting(%ExecutionDelay, \"execution_delay\", 2)
register_global_setting(%SteamCMD, \"steam_cmd_path\", \"\")
register_global_setting(%SteamUsername, \"steam_username\", \"\")
register_global_setting(%SteamPassword, \"steam_password\", \"\", true)
register_global_setting(%PipelineBuilderPath, \"pipeline_builder_path\", \"\")
register_global_setting(%GOGUsername, \"gog_username\", \"\")
register_global_setting(%GOGPassword, \"gog_password\", \"\", true)
register_global_setting(%BuildPatchToolPath, \"build_patch_tool_path\", \"\")
register_global_setting(%EpicOrganizationID, \"epic_organization_id\", \"\")
register_global_setting(%EpicClientID, \"epic_client_id\", \"\")
register_global_setting(%EpicClientSecret, \"epic_client_secret\", \"\", true)
register_global_setting(%EpicClientSecretEnvVar, \"epic_client_secret_env_var\", \"\")
register_global_setting(%ItchButlerPath, \"itch_butler_path\", \"\")
register_global_setting(%ItchUsername, \"itch_username\", \"\")
register_local_setting(%LocalGodot, \"godot_path\", \"\")
register_local_setting(%EpicProductID, \"epic_product_id\", \"\")
register_local_setting(%EpicArtifactID, \"epic_artifact_id\", \"\")
register_local_setting(%EpicCloudDir, \"epic_cloud_dir\", \"\")
register_local_setting(%ItchGameName, \"itch_game_name\", \"\")
register_local_setting(%ItchDefaultChannel, \"itch_default_channel\", \"\")
register_local_setting(%ItchVersionFile, \"itch_version_file\", \"\")
%ConfigPath.text = Data.local_config_file
%ConfigPath.path_changed.connect(config_path_updated, CONNECT_DEFERRED)
update_plugin_status()
func register_global_setting(control: Control, setting: String, default: Variant, sensitive := false):
register_setting(control, setting, default, Data.global_config, on_global_setting_changed, sensitive)
func register_local_setting(control: Control, setting: String, default: Variant, sensitive := false):
register_setting(control, setting, default, Data.local_config, on_local_setting_changed, sensitive)
func register_setting(control: Control, setting: String, default: Variant, config: Dictionary, callback: Callable, sensitive: bool):
if setting in config:
control.text = config[setting]
else:
config[setting] = default
var sygnał
var binds: Array
sygnał = control.get(&\"path_changed\")
if sygnał:
binds.append(\"\")
else:
sygnał = control.get(&\"text_changed\")
if not sygnał:
sygnał = control.get(&\"value_changed\")
assert(not sygnał.is_null())
binds.append_array([control, setting])
sygnał.connect(callback.bindv(binds))
if sensitive and not setting in Data.sensitive_settings:
Data.sensitive_settings.append(setting)
func on_global_setting_changed(dummy, control: Control, setting: String):
Data.global_config[setting] = control.text
Data.queue_save_global_config()
func on_local_setting_changed(dummy, control: Control, setting: String):
Data.local_config[setting] = control.text
Data.queue_save_local_config()
func update_plugin_status():
var plugin_file := ConfigFile.new()
plugin_file.load(Data.get_res_path().path_join(\"addons/ProjectBuilder/plugin.cfg\"))
var current_version: String = plugin_file.get_value(\"plugin\", \"version\", \"0.0\")
if plugin_file.load(Data.project_path.path_join(\"addons/ProjectBuilder/plugin.cfg\")) == OK:
var project_version: String = plugin_file.get_value(\"plugin\", \"version\", \"0.0\")
var old: bool
for i in current_version.get_slice_count(\".\"):
if current_version.get_slice(\".\", i).to_int() > project_version.get_slice(\".\", i).to_int():
old = true
break
if old:
%PluginStatus.text = \"Plugin outdated.\"
%PluginStatus.modulate = Color.YELLOW
%InstallPlugin.disabled = false
else:
%PluginStatus.text = \"Plugin installed and up-to-date.\"
%PluginStatus.modulate = Color.GREEN
%InstallPlugin.disabled = true
else:
%PluginStatus.text = \"Plugin not installed.\"
%PluginStatus.modulate = Color.RED
%InstallPlugin.disabled = false
func _on_install_plugin_pressed() -> void:
var source_path := Data.get_res_path().path_join(\"addons/ProjectBuilder\")
var target_path := Data.project_path.path_join(\"addons/ProjectBuilder\")
DirAccess.make_dir_recursive_absolute(target_path)
for file in DirAccess.get_files_at(source_path):
DirAccess.copy_absolute(source_path.path_join(file), target_path.path_join(file))
update_plugin_status()
func config_path_updated():
var old_path := Data.local_config_file
var show_error = func(text: String):
%ConfigPath.text = old_path
%PathError.dialog_text = text
%PathError.popup_centered()
var new_path: String = %ConfigPath.text.path_join(\"project_builds_config.txt\")
var new_path_abs := Data.project_path.path_join(new_path)
if new_path == old_path:
return
%ConfigPath.text = new_path
var exists: bool
if FileAccess.file_exists(new_path_abs):
exists = true
var error := OK
var old_empty: bool
if exists:
old_empty = true
for value in Data.local_config.values():
if value is String or value is Array:
if not value.is_empty():
old_empty = false
break
if old_empty:
OS.move_to_trash(Data.project_path.path_join(old_path))
else:
error = DirAccess.rename_absolute(Data.project_path.path_join(old_path), new_path_abs)
if error == OK:
var settings_path := Data.project_path.path_join(\"project.godot\")
var settings := FileAccess.get_file_as_string(settings_path).split(\"\\n\")
const setting_line := \"_project_builder_config_path=\"
var replaced: bool
for i in settings.size():
if settings[i].begins_with(setting_line):
settings[i] = \"%s\\\"%s\\\"\" % [setting_line, new_path]
replaced = true
break
if not replaced:
settings.insert(10, \"%s\\\"%s\\\"\" % [setting_line, new_path])
var saver := FileAccess.open(settings_path, FileAccess.WRITE)
if not saver:
show_error.call(\"Failed to save project settings, error %d.\" % FileAccess.get_open_error())
return
else:
saver.store_string(\"\\n\".join(settings))
saver.close()
Data.local_config_file = new_path
%ConfigPath.validate()
if exists: # Reload config if changed to existing file.
%PathError.dialog_text = \"Config file already exists in that directory.\\nIt will be used instead and the Project Builder will reload.\"
if old_empty:
%PathError.dialog_text += \"\\n\" + \"The old config file was empty, it will be deleted.\"
%PathError.popup_centered()
await %PathError.visibility_changed
Data.load_project(Data.project_path)
get_tree().reload_current_scene()
else:
show_error.call(\"Failed to move config file, error %d.\" % error)
"
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1o4l0"]
content_margin_left = 4.0
content_margin_right = 4.0
[sub_resource type="GDScript" id="GDScript_fqp3h"]
script/source = "extends SpinBox
# ಠ_ಠ
var text: int:
set(v):
value = v
get:
return value
"
[sub_resource type="GDScript" id="GDScript_s2yn1"]
script/source = "extends Control
func _ready() -> void:
if Data.first_load:
get_tree().create_timer(0.4).timeout.connect(queue_free)
else:
queue_free()
"
[node name="Main" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_r1xkq")
metadata/_edit_lock_ = true
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_lock_ = true
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer"]
layout_mode = 2
theme_override_constants/margin_top = 8
theme_override_constants/margin_bottom = 8
[node name="Title" type="Label" parent="VBoxContainer/MarginContainer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 30
text = "Project Builder - %s"
horizontal_alignment = 1
[node name="TabContainer" type="TabContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_x81aq")
tab_alignment = 1
script = SubResource("GDScript_yepl2")
[node name="Routines" type="VBoxContainer" parent="VBoxContainer/TabContainer"]
layout_mode = 2
size_flags_horizontal = 3
metadata/_tab_index = 0
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/TabContainer/Routines"]
layout_mode = 2
theme_override_constants/margin_top = 8
theme_override_constants/margin_bottom = 8
[node name="Button" type="Button" parent="VBoxContainer/TabContainer/Routines/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 4
text = "Add Routine"
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/TabContainer/Routines"]
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_nuonq")
[node name="RoutineContainer" type="HFlowContainer" parent="VBoxContainer/TabContainer/Routines/ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
alignment = 1
[node name="Control2" type="Control" parent="VBoxContainer/TabContainer/Routines"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Preset Templates" type="VBoxContainer" parent="VBoxContainer/TabContainer"]
visible = false
layout_mode = 2
metadata/_tab_index = 1
[node name="MarginContainer" type="MarginContainer" parent="VBoxContainer/TabContainer/Preset Templates"]
auto_translate_mode = 1
layout_mode = 2
theme_override_constants/margin_top = 8
theme_override_constants/margin_bottom = 8
[node name="Button" type="Button" parent="VBoxContainer/TabContainer/Preset Templates/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
text = "Add Template"
[node name="MarginContainer2" type="MarginContainer" parent="VBoxContainer/TabContainer/Preset Templates"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/margin_left = 8
theme_override_constants/margin_right = 8
[node name="TemplateScroll" type="ScrollContainer" parent="VBoxContainer/TabContainer/Preset Templates/MarginContainer2"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_nuonq")
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Preset Templates/MarginContainer2/TemplateScroll"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="TemplateContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Preset Templates/MarginContainer2/TemplateScroll/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Tasks" type="MarginContainer" parent="VBoxContainer/TabContainer"]
visible = false
layout_mode = 2
theme_override_constants/margin_left = 8
theme_override_constants/margin_right = 8
metadata/_tab_index = 2
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/TabContainer/Tasks"]
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxEmpty_cy4oc")
[node name="TaskContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Tasks/ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="Config" type="HBoxContainer" parent="VBoxContainer/TabContainer"]
visible = false
layout_mode = 2
theme = SubResource("Theme_v1gob")
theme_override_constants/separation = 32
script = SubResource("GDScript_rk2qg")
metadata/_tab_index = 3
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/TabContainer/Config"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxEmpty_1o4l0")
vertical_scroll_mode = 4
script = SubResource("GDScript_yepl2")
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
alignment = 1
[node name="Global" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer"]
layout_mode = 2
theme_type_variation = &"HeaderMedium"
text = "Global Config"
horizontal_alignment = 1
[node name="Godot" type="FoldableContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer"]
auto_translate_mode = 2
layout_mode = 2
title = "Godot"
title_alignment = 1
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Godot"]
auto_translate_mode = 1
layout_mode = 2
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Godot/VBoxContainer"]
layout_mode = 2
text = "Godot Path"
horizontal_alignment = 1
[node name="GlobalGodot" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Godot/VBoxContainer" instance=ExtResource("2_ood4h")]
unique_name_in_owner = true
layout_mode = 2
mode = 1
missing_mode = 2
[node name="Steam" type="FoldableContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer"]
auto_translate_mode = 2
layout_mode = 2
title = "Steam"
title_alignment = 1
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam"]
auto_translate_mode = 1
layout_mode = 2
[node name="Label3" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer"]
layout_mode = 2
text = "Steam CMD Path"
horizontal_alignment = 1
[node name="SteamCMD" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer" instance=ExtResource("2_ood4h")]
unique_name_in_owner = true
layout_mode = 2
mode = 1
missing_mode = 2
[node name="Label4" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer"]
layout_mode = 2
text = "Username"
horizontal_alignment = 1
[node name="SteamUsername" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Label5" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer"]
layout_mode = 2
text = "Password"
horizontal_alignment = 1
[node name="SteamPassword" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Steam/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
secret = true
[node name="GOG" type="FoldableContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer"]
auto_translate_mode = 2
layout_mode = 2
title = "GOG"
title_alignment = 1
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG"]
auto_translate_mode = 1
layout_mode = 2
[node name="Label3" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Pipeline Builder Path"
horizontal_alignment = 1
[node name="PipelineBuilderPath" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer" instance=ExtResource("2_ood4h")]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
mode = 1
missing_mode = 2
[node name="Label4" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Username"
horizontal_alignment = 1
[node name="GOGUsername" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Label5" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Password"
horizontal_alignment = 1
[node name="GOGPassword" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/GOG/VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
secret = true
[node name="Epic" type="FoldableContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer"]
auto_translate_mode = 2
layout_mode = 2
title = "Epic"
title_alignment = 1
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic"]
auto_translate_mode = 1
layout_mode = 2
[node name="Label3" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Build Patch Tool Path"
horizontal_alignment = 1
[node name="BuildPatchToolPath" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer" instance=ExtResource("2_ood4h")]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
mode = 1
missing_mode = 2
[node name="Label4" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Organization ID"
horizontal_alignment = 1
[node name="EpicOrganizationID" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Label5" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Client ID"
horizontal_alignment = 1
[node name="EpicClientID" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Label6" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Client Secret"
horizontal_alignment = 1
[node name="EpicClientSecret" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
secret = true
[node name="Label7" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Client Secret Env Variable"
horizontal_alignment = 1
[node name="EpicClientSecretEnvVar" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Epic/VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Itch" type="FoldableContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer"]
auto_translate_mode = 2
layout_mode = 2
title = "Itch"
title_alignment = 1
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Itch"]
auto_translate_mode = 1
layout_mode = 2
[node name="Label10" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Itch/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Butler Path"
horizontal_alignment = 1
[node name="ItchButlerPath" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Itch/VBoxContainer" instance=ExtResource("2_ood4h")]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
mode = 1
missing_mode = 2
[node name="Label11" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Itch/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Username"
horizontal_alignment = 1
[node name="ItchUsername" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/Itch/VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer"]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="Label" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Execution Delay"
[node name="ExecutionDelay" type="SpinBox" parent="VBoxContainer/TabContainer/Config/ScrollContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
value = 2.0
suffix = "s"
script = SubResource("GDScript_fqp3h")
[node name="ScrollContainer2" type="ScrollContainer" parent="VBoxContainer/TabContainer/Config"]
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxEmpty_1o4l0")
vertical_scroll_mode = 4
script = SubResource("GDScript_yepl2")
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer2"]
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 3
alignment = 1
[node name="Local" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer"]
layout_mode = 2
theme_type_variation = &"HeaderMedium"
text = "Local Config"
horizontal_alignment = 1
[node name="Godot" type="FoldableContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer"]
auto_translate_mode = 2
layout_mode = 2
title = "Godot"
title_alignment = 1
[node name="Godot" type="VBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot"]
auto_translate_mode = 1
layout_mode = 2
[node name="Label2" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot"]
layout_mode = 2
text = "Project Builder Configuration Path"
horizontal_alignment = 1
[node name="ConfigPath" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot" instance=ExtResource("2_ood4h")]
unique_name_in_owner = true
layout_mode = 2
mode = 4
scope = 1
missing_mode = 1
[node name="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot/ConfigPath" index="0"]
editable = false
[node name="PathError" type="AcceptDialog" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot/ConfigPath"]
unique_name_in_owner = true
title = "Error!"
[node name="Label3" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot"]
layout_mode = 2
text = "Godot Exec For This Project"
horizontal_alignment = 1
[node name="LocalGodot" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot" instance=ExtResource("2_ood4h")]
unique_name_in_owner = true
layout_mode = 2
mode = 1
[node name="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot/LocalGodot" index="0"]
placeholder_text = "Leave empty to use global path."
caret_blink = true
caret_blink_interval = 0.5
[node name="Epic" type="FoldableContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer"]
auto_translate_mode = 2
layout_mode = 2
title = "Epic"
title_alignment = 1
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic"]
auto_translate_mode = 1
layout_mode = 2
[node name="Label9" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Product ID"
horizontal_alignment = 1
[node name="EpicProductID" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Label10" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Artifact ID"
horizontal_alignment = 1
[node name="EpicArtifactID" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Label11" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Cloud Directory"
horizontal_alignment = 1
[node name="EpicCloudDir" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Epic/VBoxContainer" instance=ExtResource("2_ood4h")]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
scope = 1
missing_mode = 2
[node name="Itch" type="FoldableContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer"]
auto_translate_mode = 2
layout_mode = 2
title = "Itch"
title_alignment = 1
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch"]
auto_translate_mode = 1
layout_mode = 2
[node name="Label8" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Game Name"
horizontal_alignment = 1
[node name="ItchGameName" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Label9" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Default Channel"
horizontal_alignment = 1
[node name="ItchDefaultChannel" type="LineEdit" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Label10" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer"]
auto_translate_mode = 1
layout_mode = 2
text = "Version File"
horizontal_alignment = 1
[node name="ItchVersionFile" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Itch/VBoxContainer" instance=ExtResource("2_ood4h")]
unique_name_in_owner = true
layout_mode = 2
mode = 1
scope = 1
filters = PackedStringArray("*.txt")
[node name="HSeparator" type="HSeparator" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer"]
layout_mode = 2
[node name="Button" type="Button" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 4
text = "Run Project Scan"
[node name="ScanFinished" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Button"]
unique_name_in_owner = true
visible = false
layout_mode = 1
anchors_preset = 9
anchor_bottom = 1.0
offset_left = 142.0
offset_right = 249.0
grow_vertical = 2
text = "Scan finished!"
vertical_alignment = 1
[node name="Control" type="Control" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer"]
custom_minimum_size = Vector2(0, 8)
layout_mode = 2
[node name="PluginStatus" type="Label" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Plugin Status"
horizontal_alignment = 1
[node name="InstallPlugin" type="Button" parent="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
text = "Instal Project Builder Plugin"
[node name="Back" type="Button" parent="."]
unique_name_in_owner = true
layout_mode = 0
shortcut = ExtResource("4_bgxb8")
shortcut_in_tooltip = false
text = "Back"
icon = ExtResource("2_fue2w")
[node name="OpenLogs" type="Button" parent="."]
auto_translate_mode = 1
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -57.0
offset_bottom = 31.0
grow_horizontal = 0
text = "Open Logs Folder"
icon = ExtResource("6_kgk8y")
[node name="InputKiller" type="Control" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = SubResource("GDScript_s2yn1")
metadata/_edit_lock_ = true
[connection signal="tab_changed" from="VBoxContainer/TabContainer" to="." method="tab_changed"]
[connection signal="pressed" from="VBoxContainer/TabContainer/Routines/MarginContainer/Button" to="." method="_add_routine_pressed"]
[connection signal="pressed" from="VBoxContainer/TabContainer/Preset Templates/MarginContainer/Button" to="." method="_add_template_pressed"]
[connection signal="pressed" from="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Button" to="." method="run_project_scan"]
[connection signal="pressed" from="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/InstallPlugin" to="VBoxContainer/TabContainer/Config" method="_on_install_plugin_pressed"]
[connection signal="pressed" from="Back" to="." method="go_back"]
[connection signal="pressed" from="OpenLogs" to="." method="open_logs"]
[editable path="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot/ConfigPath"]
[editable path="VBoxContainer/TabContainer/Config/ScrollContainer2/VBoxContainer/Godot/Godot/LocalGodot"]
================================================
FILE: Scenes/ProjectManager.gd
================================================
extends Control
@onready var custom_list: HBoxContainer = %CustomList
@onready var projects: VBoxContainer = %Projects
@onready var over: Label = %Over
var user_arguments := OS.get_cmdline_user_args()
func _ready() -> void:
var i := user_arguments.find("--open-project")
if i > -1:
if user_arguments.size() < i + 2:
printerr("No project path provided with --open-project.")
else:
var project_path := user_arguments[i + 1]
if DirAccess.dir_exists_absolute(project_path):
Data.from_plugin = true
load_project.call_deferred(project_path)
return
else:
printerr("The project provided for --open-project does not exist.")
i = user_arguments.find("--execute-routine")
if i > -1:
printerr("--execute-routine was provided, but no project was opened with --open-project.")
get_tree().quit(1)
return
i = user_arguments.find("--exit")
if i > -1:
printerr("--exit argument provided, but no --execute-routine. It will be ignored.")
custom_list.text = Data.global_config["custom_project_list"]
load_project_list()
func load_project_list():
for project in projects.get_children():
project.queue_free()
var projects_path: String = custom_list.text
var i := user_arguments.find("--projects-file-path")
if i > -1:
if user_arguments.size() < i + 2:
push_warning("--projects-file-path -- Missing projects file path.")
else:
over.show()
projects_path = user_arguments[i + 1]
if not FileAccess.file_exists(projects_path):
var editor_data := OS.get_user_data_dir().get_base_dir().get_base_dir()
projects_path = editor_data.path_join("projects.cfg")
var project_list := ConfigFile.new()
if project_list.load(projects_path) != OK:
return
for project in project_list.get_sections():
var project_entry := preload("res://Nodes/ProjectEntry.tscn").instantiate()
projects.add_child(project_entry)
project_entry.set_project(project, load_project)
func load_project(project: String):
Data.load_project(project)
var i := user_arguments.find("--execute-routine")
if i > -1:
var j := user_arguments.find("--exit")
if j > -1:
Data.auto_exit = true
if user_arguments.size() < i + 2:
print("No routine name provided for --execute-routine.")
print_routines_and_exit()
return
else:
var routine_name := user_arguments[i + 1]
for routine in Data.routines:
if routine["name"] == routine_name:
Data.current_routine = routine
get_tree().change_scene_to_file("res://Scenes/Execution.tscn")
return
printerr("The routine provided for --execute-routine does not exist.")
print_routines_and_exit()
return
i = user_arguments.find("--exit")
if i > -1:
printerr("--exit argument provided, but no --execute-routine. It will be ignored.")
get_tree().change_scene_to_packed(Data.main)
func print_routines_and_exit():
print("Available routines:")
for routine in Data.routines:
print(routine["name"])
if Data.auto_exit:
get_tree().quit(1)
func project_list_path_changed() -> void:
Data.global_config["custom_project_list"] = custom_list.text
Data.save_global_config()
load_project_list()
================================================
FILE: Scenes/ProjectManager.gd.uid
================================================
uid://drooidtlec0og
================================================
FILE: Scenes/ProjectManager.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://eu3eioilmxkb"]
[ext_resource type="Script" uid="uid://drooidtlec0og" path="res://Scenes/ProjectManager.gd" id="1_edpqn"]
[ext_resource type="PackedScene" uid="uid://cyl1d6reu3mk4" path="res://Nodes/GUI/DirectorySelector.tscn" id="2_8a1pu"]
[node name="ProjectManager" type="ScrollContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_edpqn")
metadata/_edit_lock_ = true
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
alignment = 1
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Custom Project List"
[node name="CustomList" parent="VBoxContainer/HBoxContainer" instance=ExtResource("2_8a1pu")]
unique_name_in_owner = true
custom_minimum_size = Vector2(250, 0)
layout_mode = 2
mode = 1
missing_mode = 2
filters = PackedStringArray("*.cfg")
empty_is_valid = true
[node name="Over" type="Label" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
visible = false
modulate = Color(1, 1, 0, 1)
layout_mode = 2
text = "Overridden by cmd argument."
[node name="Projects" type="VBoxContainer" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 6
size_flags_vertical = 3
[connection signal="path_changed" from="VBoxContainer/HBoxContainer/CustomList" to="." method="project_list_path_changed"]
================================================
FILE: Scenes/RoutineBuilder.gd
================================================
extends Control
@onready var task_list: VBoxContainer = %TaskList
@onready var add_task: MenuButton = %AddTask
var routine: Dictionary
var task_to_test: Task
var discard: bool
func _ready() -> void:
routine = Data.current_routine
%RoutineName.text = routine["name"]
%OnFail.selected = routine["on_fail"]
for task in Data.tasks.values():
add_task.get_popup().add_item(task["name"])
add_task.get_popup().set_item_metadata(-1, task["scene"])
for task: Dictionary in routine["tasks"]:
var task_instance := create_task(task["scene"])
task_instance.load_data(task["data"])
add_task.get_popup().index_pressed.connect(_create_task)
update_paste()
validate_routine_name()
func _create_task(idx: int):
create_task(add_task.get_popup().get_item_metadata(idx))
func create_task(scene: String) -> Task:
var container := preload("res://Nodes/TaskContainer.tscn").instantiate()
task_list.add_child(container)
container.copied.connect(update_paste)
container.owner = self
var task: Task = container.set_task_scene(scene)
task.owner = self
task._initialize()
return task
func _back_pressed() -> void:
get_tree().change_scene_to_packed(Data.main)
func _discard_pressed() -> void:
discard = true
get_tree().auto_accept_quit = true
get_tree().change_scene_to_packed(Data.main)
func test_task(task: Task):
task_to_test = task
get_tree().current_scene = null
get_parent().remove_child(self)
func _exit_tree() -> void:
Data.reset_current_routine()
if discard:
return
var routine_tasks: Array[Dictionary]
var test_data: Dictionary
for task: Task in task_list.get_children().map(func(container: Node) -> Task: return container.task):
var task_data := Dictionary()
task_data["scene"] = task.scene_file_path.get_file().get_basename()
task_data["data"] = task.store_data()
routine_tasks.append(task_data)
if task == task_to_test:
test_data = task_data
routine["name"] = %RoutineName.text
routine["tasks"] = routine_tasks
routine["on_fail"] = %OnFail.selected
Data.queue_save_local_config()
if task_to_test:
Data.current_routine = { "name": "Test", "tasks": [ test_data ], "on_fail": 0 }
queue_free()
get_tree().change_scene_to_file("res://Scenes/Execution.tscn")
func paste_task() -> void:
var task := Data.copied_task
if task.is_empty():
return
var task_instance := create_task(task["scene"])
task_instance.load_data(task["data"])
func update_paste():
%PasteTask.disabled = Data.copied_task.is_empty()
func validate_routine_name():
var valid := true
for rout in Data.routines:
if rout != routine and rout["name"] == %RoutineName.text:
valid = false
break
if valid:
get_tree().auto_accept_quit = true
%RoutineName.modulate = Color.WHITE
%Back.disabled = false
else:
get_tree().auto_accept_quit = false
%RoutineName.modulate = Color.RED
%Back.disabled = true
================================================
FILE: Scenes/RoutineBuilder.gd.uid
================================================
uid://bw6lvvotkx7nc
================================================
FILE: Scenes/RoutineBuilder.tscn
================================================
[gd_scene load_steps=5 format=3 uid="uid://dbb72q773nirj"]
[ext_resource type="Script" uid="uid://bw6lvvotkx7nc" path="res://Scenes/RoutineBuilder.gd" id="1_gekas"]
[ext_resource type="Texture2D" uid="uid://dt6drd8cw1dhl" path="res://Icons/Back.svg" id="2_0gvmh"]
[ext_resource type="Shortcut" uid="uid://d0olqx0bjlhp1" path="res://Nodes/GUI/ExitShortcut.tres" id="2_ghd1v"]
[ext_resource type="Texture2D" uid="uid://3i2umr3kmry6" path="res://Icons/Paste.svg" id="3_c5oe2"]
[node name="RoutineBuilder" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_gekas")
metadata/_edit_lock_ = true
[node name="HBoxContainer2" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Back" type="Button" parent="HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
shortcut = ExtResource("2_ghd1v")
shortcut_in_tooltip = false
text = "Back"
icon = ExtResource("2_0gvmh")
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer2"]
layout_mode = 2
size_flags_horizontal = 3
alignment = 1
[node name="Label" type="Label" parent="HBoxContainer2/HBoxContainer"]
layout_mode = 2
text = "Routine Name"
[node name="RoutineName" type="LineEdit" parent="HBoxContainer2/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(300, 0)
layout_mode = 2
theme_override_constants/minimum_character_width = 24
max_length = 24
[node name="Back2" type="Button" parent="HBoxContainer2"]
unique_name_in_owner = true
auto_translate_mode = 1
modulate = Color(1, 0, 0, 1)
layout_mode = 2
text = "Discard Changes"
icon = ExtResource("2_0gvmh")
icon_alignment = 2
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
alignment = 1
[node name="Label2" type="Label" parent="HBoxContainer"]
layout_mode = 2
text = "On Fail:"
[node name="OnFail" type="OptionButton" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
selected = 0
item_count = 2
popup/item_0/text = "Abort"
popup/item_0/id = 1
popup/item_1/text = "Continue"
popup/item_1/id = 0
[node name="ScrollContainer" type="ScrollContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
metadata/_edit_lock_ = true
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer"]
custom_minimum_size = Vector2(800, 0)
layout_mode = 2
size_flags_horizontal = 6
size_flags_vertical = 3
metadata/_edit_lock_ = true
[node name="TaskList" type="VBoxContainer" parent="ScrollContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="ScrollContainer/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="AddTask" type="MenuButton" parent="ScrollContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
text = "Add Task"
flat = false
[node name="PasteTask" type="Button" parent="ScrollContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 4
text = "Paste Task"
icon = ExtResource("3_c5oe2")
[connection signal="pressed" from="HBoxContainer2/Back" to="." method="_back_pressed"]
[connection signal="text_changed" from="HBoxContainer2/HBoxContainer/RoutineName" to="." method="validate_routine_name" unbinds=1]
[connection signal="pressed" from="HBoxContainer2/Back2" to="." method="_discard_pressed"]
[connection signal="pressed" from="ScrollContainer/VBoxContainer/HBoxContainer/PasteTask" to="." method="paste_task"]
================================================
FILE: Scripts/Data.gd
================================================
extends Node
const CONFIG_FILE = "project_builds_config.txt"
var local_config_file: String
var global_config: Dictionary
var local_config: Dictionary
var project_path: String
var from_plugin: bool
var auto_exit: bool
var first_load: bool
var initial_load: bool
var static_initialize_tasks: Array[Script]
var sensitive_settings: Array[String]
var tasks: Dictionary#[String, Dictionary]
var routines: Array[Dictionary]
var templates: Array[Dictionary]
var current_routine: Dictionary
var copied_task: Dictionary
var save_local_timer: Timer
var save_global_timer: Timer
var main: PackedScene
func _init() -> void:
var task_path := get_res_path().path_join("Tasks")
for task in DirAccess.get_files_at(task_path):
if task.get_extension() == "tscn":
register_task(task_path.path_join(task))
var global_defaults = {"project_builder_path": "", "project_builder_executable": "", "custom_project_list": ""}
var global_config_file := FileAccess.open("user://".path_join(CONFIG_FILE), FileAccess.READ)
if global_config_file:
global_config = str_to_var(global_config_file.get_as_text())
global_config.merge(global_defaults)
else:
global_config = global_defaults
save_local_timer = Timer.new()
save_local_timer.wait_time = 0.5
save_local_timer.one_shot = true
add_child(save_local_timer)
save_global_timer = save_local_timer.duplicate()
add_child(save_global_timer)
save_local_timer.timeout.connect(save_local_config)
save_global_timer.timeout.connect(save_global_config)
main = load("res://Scenes/Main.tscn")
func _ready() -> void:
if not OS.get_cmdline_user_args().is_empty():
return
var path := ProjectSettings.globalize_path("res://")
if path != global_config["project_builder_path"]:
global_config["project_builder_path"] = path
queue_save_global_config()
path = OS.get_executable_path()
if path != global_config["project_builder_executable"]:
global_config["project_builder_executable"] = path
queue_save_global_config()
func get_project_config_path(project: String, config_file: ConfigFile = null) -> String:
if not config_file:
config_file = ConfigFile.new()
config_file.load(project)
var config_path: String = config_file.get_value("addons", "project_builder/config_path", "") # compat
if config_path.is_empty():
config_path = config_file.get_value("", "_project_builder_config_path", CONFIG_FILE)
return config_path.trim_prefix("res://")
func load_project(path: String):
project_path = path
first_load = true
local_config_file = get_project_config_path(project_path.path_join("project.godot"))
var fa := FileAccess.open(project_path.path_join(local_config_file), FileAccess.READ)
if fa:
local_config = str_to_var(fa.get_as_text())
else:
local_config = {}
local_config["routines"] = Array([], TYPE_DICTIONARY, &"", null)
local_config["templates"] = Array([], TYPE_DICTIONARY, &"", null)
initial_load = true
routines = local_config["routines"]
templates = local_config["templates"]
func register_task(scene: String):
var data := Dictionary()
var scene_base := scene.get_file().get_basename()
data["scene"] = scene_base
data["scene_cache"] = load(scene)
var instance: Task = load(scene).instantiate()
data["name"] = instance._get_task_name()
if instance.has_static_configuration:
static_initialize_tasks.append(instance.get_script())
instance.free()
tasks[scene_base] = data
func create_routine() -> Dictionary:
var routine := Dictionary()
routine["name"] = get_unique_name(routines, "New Routine", "%d")
routine["on_fail"] = 0
routine["tasks"] = []
routines.append(routine)
return routine
func reset_current_routine() -> void:
current_routine = {}
func get_template(template_name: String) -> Dictionary:
for template in templates:
if template["name"] == template_name:
return template
return {}
func get_project_version() -> String:
var project := ConfigFile.new()
project.load(project_path.path_join("project.godot"))
return project.get_value("application", "config/version", "")
func get_godot_path() -> String:
var local_godot: String = local_config["godot_path"]
if local_godot.is_empty():
return global_config["godot_path"]
else:
return local_godot
func get_res_path() -> String:
if OS.has_feature("editor"):
return ProjectSettings.globalize_path("res://")
else:
return OS.get_executable_path().get_base_dir()
func resolve_path(path: String) -> String:
if path.is_absolute_path():
return path
else:
return Data.project_path.path_join(path)
func get_unique_name(dataset: Array[Dictionary], base: String, format_suffix: String, initial_count := 1) -> String:
var unique_name := base
var format_base := base + " " + format_suffix
var tries := initial_count
while dataset.any(func(data: Dictionary) -> bool: return data["name"] == unique_name):
tries += 1
unique_name = format_base % tries
return unique_name
func queue_save_local_config():
save_local_timer.start()
func save_local_config():
save_local_timer.stop()
var fa := FileAccess.open(project_path.path_join(local_config_file), FileAccess.WRITE)
fa.store_string(var_to_str(local_config))
func queue_save_global_config():
save_global_timer.start()
func save_global_config():
save_global_timer.stop()
var fa := FileAccess.open("user://".path_join(CONFIG_FILE), FileAccess.WRITE)
fa.store_string(var_to_str(global_config))
func _exit_tree() -> void:
if not project_path.is_empty():
save_local_config()
save_global_config()
================================================
FILE: Scripts/Data.gd.uid
================================================
uid://cxigukxxvepyb
================================================
FILE: Scripts/Templates/Task/EmptyTask.gd
================================================
extends Task
func _get_task_name() -> String:
return "Empty Task"
func _get_execute_string() -> String:
return _get_task_name()
static func _initialize_project() -> void:
pass
static func _begin_project_scan() -> void:
pass
static func _process_file(path: String) -> void:
pass
static func _end_project_scan() -> void:
pass
func _initialize() -> void:
pass
func _prevalidate() -> bool:
return true
func _validate() -> bool:
return true
func _get_command() -> String:
return ""
func _get_arguments() -> PackedStringArray:
return []
func _prepare() -> void:
pass
func _cleanup() -> void:
pass
func _load() -> void:
pass
func _store() -> void:
pass
func _get_task_info() -> PackedStringArray:
return [
"Task description",
"Argument Name|Description",
]
================================================
FILE: Scripts/Templates/Task/EmptyTask.gd.uid
================================================
uid://05ho57r0n7x
================================================
FILE: Tasks/ClearDirectory.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://baipfhiy5un08"]
[ext_resource type="PackedScene" uid="uid://cyl1d6reu3mk4" path="res://Nodes/GUI/DirectorySelector.tscn" id="1_63381"]
[sub_resource type="GDScript" id="GDScript_getv1"]
script/source = "extends \"ScriptTask.gd\"
@onready var target_directory: HBoxContainer = %TargetDirectory
@onready var file_filter: LineEdit = %FileFilter
@onready var file_filter2: LineEdit = %FileFilter2
func _get_task_name() -> String:
return \"Clear Directory Files\"
func _get_execute_string() -> String:
return \"Clear Files at %s\" % target_directory.text
func _initialize() -> void:
defaults[\"target_directory\"] = \"\"
defaults[\"include_files\"] = \"\"
defaults[\"exclude_files\"] = \"\"
func _validate() -> bool:
if not DirAccess.dir_exists_absolute(Data.resolve_path(target_directory.text)):
error_message = \"Target directory does not exist.\"
return false
return true
func _get_arguments() -> PackedStringArray:
var ret := super()
ret.append(Data.resolve_path(target_directory.text))
if not file_filter.text.is_empty():
ret.append(\"--include\")
ret.append_array(file_filter.text.split(\" \"))
if not file_filter2.text.is_empty():
ret.append(\"--exclude\")
ret.append_array(file_filter2.text.split(\" \"))
return ret
func _load() -> void:
target_directory.text = data[\"target_directory\"]
file_filter.text = data[\"include_files\"]
file_filter2.text = data[\"exclude_files\"]
func _store() -> void:
data[\"target_directory\"] = target_directory.text
data[\"include_files\"] = file_filter.text
data[\"exclude_files\"] = file_filter2.text
func _get_task_info() -> PackedStringArray:
return [
\"Clears all files in a directory. The files are not deleted, but moved to a special Trash directory in Project Builder's user data. The operation is not recursive.\",
\"Target Directory|The directory to be cleared.\",
\"Include Files|List of filters for files from the directory. Example: \\\"*.exe *.pck\\\". If empty, all files will be packed. Otherwise only files that match the filter will be included.\",
\"Exclude Files|Filters files from the directory. Files that match this filter will be excluded.\",
]
"
[node name="ClearDirectory" type="GridContainer"]
offset_right = 339.0
offset_bottom = 31.0
columns = 2
script = SubResource("GDScript_getv1")
script_name = "ClearDirectory.gd"
[node name="Label" type="Label" parent="."]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "Target Directory"
horizontal_alignment = 2
[node name="TargetDirectory" parent="." instance=ExtResource("1_63381")]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
scope = 1
missing_mode = 1
[node name="Label2" type="Label" parent="."]
layout_mode = 2
size_flags_horizontal = 8
text = "Include Files"
horizontal_alignment = 2
[node name="FileFilter" type="LineEdit" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Space-separated list of filters, e.g. \"*.exe\". Leave empty to include all."
[node name="Label4" type="Label" parent="."]
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 8
text = "Exclude Files"
horizontal_alignment = 2
[node name="FileFilter2" type="LineEdit" parent="."]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Space-separated list of filters. Leave empty to exclude none."
================================================
FILE: Tasks/CopyFiles.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://bfxyekjccdafx"]
[ext_resource type="PackedScene" uid="uid://cyl1d6reu3mk4" path="res://Nodes/GUI/DirectorySelector.tscn" id="1_nbxxd"]
[sub_resource type="GDScript" id="GDScript_6ruue"]
script/source = "extends \"ScriptTask.gd\"
@onready var source_path: Control = %SourcePath
@onready var target_path: Control = %TargetPath
@onready var recursive: CheckBox = %Recursive
var directory_mode: bool
func _get_task_name() -> String:
return \"Copy File(s)\"
func _get_execute_string() -> String:
return \"Copy %s to %s\" % [source_path.text, target_path.text]
func _initialize() -> void:
defaults[\"source_path\"] = \"\"
defaults[\"target_path\"] = \"\"
defaults[\"recursive\"] = true
func _prevalidate() -> bool:
if source_path.text == target_path.text:
error_message = \"Source and target paths are the same.\"
return false
else:
return true
func _validate() -> bool:
if DirAccess.dir_exists_absolute(Data.resolve_path(source_path.text)):
directory_mode = true
elif FileAccess.file_exists(Data.resolve_path(source_path.text)):
directory_mode = false
else:
error_message = \"Source path does not point to any file nor directory.\"
return error_message.is_empty()
func _get_arguments() -> PackedStringArray:
var ret := super()
ret.append(Data.resolve_path(source_path.text))
ret.append(Data.resolve_path(target_path.text))
if directory_mode:
if recursive.button_pressed:
ret.append(\"dir_recursive\")
else:
ret.append(\"dir\")
else:
ret.append(\"file\")
return ret
func _load() -> void:
source_path.text = data[\"source_path\"]
target_path.text = data[\"target_path\"]
recursive.button_pressed = data[\"recursive\"]
func _store() -> void:
data[\"source_path\"] = source_path.text
data[\"target_path\"] = target_path.text
data[\"recursive\"] = recursive.button_pressed
func _get_task_info() -> PackedStringArray:
return [
\"Copies the specified files or directories to a new location.\",
\"Source Path|File or directory path which is going to be copied.\",
\"Target Path|Target file/directory path where the files will be copied to..\",
\"Recursive|If source path is a directory, this option enables copying sub-directories.\",
]
"
[node name="CopyFiles" type="GridContainer"]
offset_right = 400.0
offset_bottom = 101.0
columns = 2
script = SubResource("GDScript_6ruue")
script_name = "CopyFiles.gd"
[node name="Label" type="Label" parent="."]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
size_flags_horizontal = 8
text = "Source Path"
horizontal_alignment = 2
[node name="SourcePath" parent="." instance=ExtResource("1_nbxxd")]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
mode = 3
scope = 1
missing_mode = 1
[node name="Label2" type="Label" parent="."]
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 8
text = "Target Path"
horizontal_alignment = 2
[node name="TargetPath" parent="." instance=ExtResource("1_nbxxd")]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 3
mode = 3
scope = 1
[node name="Control" type="Control" parent="."]
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 8
[node name="Recursive" type="CheckBox" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 0
button_pressed = true
text = "Recursive"
================================================
FILE: Tasks/CustomTask.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://2g67bchptwsm"]
[ext_resource type="PackedScene" uid="uid://bfbht01onlf1a" path="res://Nodes/GUI/StringContainer.tscn" id="1_siigy"]
[sub_resource type="GDScript" id="GDScript_3vtsj"]
script/source = "extends Task
@onready var command: LineEdit = %Command
@onready var arguments: Control = %Arguments
func _get_task_name() -> String:
return \"Custom Task\"
func _get_execute_string() -> String:
return \"Custom Task: %s\" % _get_command()
func _initialize() -> void:
defaults[\"command\"] = \"\"
defaults[\"arguments\"] = PackedStringArray()
func _get_command() -> String:
return process_string(command.text)
func _get_arguments() -> PackedStringArray:
return Array(arguments.get_strings()).map(process_string)
func _load() -> void:
command.text = data[\"command\"]
arguments.set_strings(data[\"arguments\"])
func _store() -> void:
data[\"command\"] = command.text
data[\"arguments\"] = arguments.get_strings()
func _get_task_info() -> PackedStringArray:
return [
\"Executes a custom command with the provided arguments.\",
\"Command|The command to execute (path to a program etc.).\",
\"Arguments|Launch arguments for the command.\",
]
func process_string(string: String) -> String:
if string == \"$godot\":
return OS.get_executable_path()
elif string == \"$local_godot\":
return Data.get_godot_path()
return string.replace(\"$project\", Data.project_path)
"
[node name="CustomTask" type="GridContainer"]
offset_right = 506.0
offset_bottom = 58.0
columns = 2
script = SubResource("GDScript_3vtsj")
[node name="Label" type="Label" parent="."]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "Command"
horizontal_alignment = 2
[node name="Command" type="LineEdit" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="Label2" type="Label" parent="."]
auto_translate_mode = 1
layout_mode = 2
text = "Arguments"
horizontal_alignment = 2
[node name="Arguments" parent="." instance=ExtResource("1_siigy")]
unique_name_in_owner = true
layout_mode = 2
================================================
FILE: Tasks/ExportProject.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://m4tjlhu18ode"]
[ext_resource type="PackedScene" uid="uid://cyl1d6reu3mk4" path="res://Nodes/GUI/DirectorySelector.tscn" id="1_ftqqe"]
[sub_resource type="GDScript" id="GDScript_mlhhy"]
script/source = "extends \"res://Tasks/ExportTask.gd\"
@onready var preset_list: OptionButton = %PresetList
@onready var debug: CheckBox = %Debug
@onready var custom_path: Control = $CustomPath
func _get_task_name() -> String:
return \"Export Project\"
func _get_execute_string() -> String:
return \"Export Project (%s)\" % get_preset_name()
func _initialize():
super()
defaults[\"preset\"] = \"\"
defaults[\"debug\"] = false
defaults[\"custom_path\"] = \"\"
setup_preset_list(preset_list)
func _prepare():
var path: String
if not custom_path.text.is_empty():
path = custom_path.text
override_path = true
else:
var export_presets := load_presets()
if export_presets:
var preset_name := get_preset_name()
for section in export_presets.get_sections():
if export_presets.get_value(section, \"name\", \"\") == preset_name:
path = export_presets.get_value(section, \"export_path\", \"\")
break
set_export_path(path)
export_debug = debug.button_pressed
export_preset = get_preset_name()
super()
func _load():
var preset_text: String = data[\"preset\"]
var preset_assigned: bool
for i in preset_list.item_count:
if preset_list.get_item_text(i) == preset_text:
preset_list.selected = i
preset_assigned = true
break
if not preset_assigned:
preset_list.selected = 0
custom_path.text = preset_text
debug.button_pressed = data[\"debug\"]
custom_path.text = data[\"custom_path\"]
func _store():
if preset_list.disabled:
data[\"preset\"] = \"\"
else:
data[\"preset\"] = preset_list.get_item_text(preset_list.selected)
data[\"debug\"] = debug.button_pressed
data[\"custom_path\"] = custom_path.text
func _get_task_info() -> PackedStringArray:
return [
\"Exports the project using one of defined export presets.\",
\"Preset|Preset name from the project's presets defined in the Export dialog.\",
\"Debug|If enabled, exports a debug build.\",
]
func get_preset_name() -> String:
return preset_list.get_item_text(preset_list.selected)
"
[node name="ExportProject" type="GridContainer"]
offset_right = 532.0
offset_bottom = 120.0
columns = 2
script = SubResource("GDScript_mlhhy")
[node name="Label2" type="Label" parent="."]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "Preset"
horizontal_alignment = 2
[node name="PresetList" type="OptionButton" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="."]
layout_mode = 2
text = "Custom Path"
horizontal_alignment = 2
[node name="CustomPath" parent="." instance=ExtResource("1_ftqqe")]
layout_mode = 2
size_flags_horizontal = 3
mode = 2
scope = 1
[node name="Control" type="Control" parent="."]
auto_translate_mode = 1
layout_mode = 2
[node name="Debug" type="CheckBox" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 0
text = "Debug"
================================================
FILE: Tasks/ExportProjectFromTemplate.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://doi6iht30rdxc"]
[sub_resource type="GDScript" id="GDScript_mlhhy"]
script/source = "extends \"res://Tasks/ExportTask.gd\"
const CUSTOM_PRESET = \"_Project_Builds_\"
@onready var preset_list: OptionButton = %PresetList
@onready var template_list: OptionButton = %TemplateList
@onready var path_suffix: LineEdit = %PathSuffix
@onready var debug: CheckBox = %Debug
var custom_preset: int
func _get_task_name() -> String:
return \"Export Project From Template\"
func _get_execute_string() -> String:
return \"Export Project From Template (preset %s, template %s)\" % [data[\"preset\"], data[\"template\"]]
func _initialize():
super()
defaults[\"preset\"] = \"\"
defaults[\"template\"] = \"\"
defaults[\"path_suffix\"] = \"\"
defaults[\"debug\"] = false
setup_preset_list(preset_list)
for template in Data.templates:
template_list.add_item(template[\"name\"])
if template_list.item_count == 0:
template_list.add_item(\"No template found. Define in Preset Templates tab.\")
template_list.disabled = true
func _prevalidate() -> bool:
if not super():
return false
var template_name: String = template_list.get_item_text(template_list.selected)
if Data.get_template(template_name).is_empty():
error_message = \"Invalid export preset template: %s.\" % template_name
return error_message.is_empty()
func _prepare() -> void:
var export_presets := load_presets()
var template := Data.get_template(template_list.get_item_text(template_list.selected))
var base_preset := preset_list.selected
custom_preset = preset_list.item_count
for section in export_presets.get_sections():
if export_presets.get_value(section, \"name\", \"\") == CUSTOM_PRESET:
custom_preset = section.get_slice(\".\", 1).to_int()
break
var new_section := \"preset.%d\" % custom_preset
var section := \"preset.%d\" % base_preset
for key in export_presets.get_section_keys(section):
export_presets.set_value(new_section, key, export_presets.get_value(section, key))
export_presets.set_value(new_section, \"name\", CUSTOM_PRESET)
export_presets.set_value(new_section, \"custom_features\", \", \".join(template[\"custom_features\"]))
export_presets.set_value(new_section, \"include_filter\", \", \".join(template[\"include_filters\"]))
export_presets.set_value(new_section, \"exclude_filter\", \", \".join(template[\"exclude_filters\"]))
if not template[\"export_path\"].is_empty():
set_export_path(template[\"export_path\"].path_join(path_suffix.text))
export_presets.set_value(new_section, \"export_path\", export_path)
else:
set_export_path(export_presets.get_value(new_section, \"export_path\", \"\"))
new_section = \"preset.%d.options\" % custom_preset
section = \"preset.%d.options\" % base_preset
for key in export_presets.get_section_keys(section):
export_presets.set_value(new_section, key, export_presets.get_value(section, key))
export_presets.save(preset_path)
export_debug = debug.button_pressed
export_preset = CUSTOM_PRESET
super()
func _cleanup() -> void:
var export_presets := load_presets()
if not export_presets:
return
export_presets.erase_section(\"preset.%d\" % custom_preset)
export_presets.erase_section(\"preset.%d.options\" % custom_preset)
export_presets.save(preset_path)
func _load():
var text: String = data[\"preset\"]
preset_list.selected = 0
for i in preset_list.item_count:
if preset_list.get_item_text(i) == text:
preset_list.selected = i
break
text = data[\"template\"]
template_list.selected = 0
for i in template_list.item_count:
if template_list.get_item_text(i) == text:
template_list.selected = i
break
path_suffix.text = data[\"path_suffix\"]
debug.button_pressed = data[\"debug\"]
func _store():
if preset_list.disabled:
data[\"preset\"] = \"\"
else:
data[\"preset\"] = preset_list.get_item_text(preset_list.selected)
if template_list.disabled:
data[\"template\"] = \"\"
else:
data[\"template\"] = template_list.get_item_text(template_list.selected)
data[\"path_suffix\"] = path_suffix.text
data[\"debug\"] = debug.button_pressed
func _get_task_info() -> PackedStringArray:
return [
\"Exports the project by creating a new preset using the selected template. An existing preset is used as a base.\",
\"Base Preset|Preset name from the project's presets defined in the Export dialog.\",
\"Preset Template|Template name from the templates defined in Project Builder.\",
\"Debug|If enabled, exports a debug build.\",
]
"
[node name="ExportProjectFromTemplate" type="GridContainer"]
offset_right = 532.0
offset_bottom = 120.0
columns = 2
script = SubResource("GDScript_mlhhy")
[node name="Label" type="Label" parent="."]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "Base Preset"
horizontal_alignment = 2
[node name="PresetList" type="OptionButton" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="Label2" type="Label" parent="."]
layout_mode = 2
text = "Preset Template"
horizontal_alignment = 2
[node name="TemplateList" type="OptionButton" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="Label3" type="Label" parent="."]
layout_mode = 2
text = "Path Suffix"
horizontal_alignment = 2
[node name="PathSuffix" type="LineEdit" parent="."]
unique_name_in_owner = true
layout_mode = 2
[node name="Control" type="Control" parent="."]
layout_mode = 2
[node name="Debug" type="CheckBox" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 0
text = "Debug"
================================================
FILE: Tasks/ExportTask.gd
================================================
extends Task
static var godot_path: String
static var is_godot_3: bool
var export_debug: bool
var override_path: bool
var preset_path: String
var export_path: String
var export_preset: String
func _init() -> void:
has_static_configuration = true
static func _initialize_project():
update_godot_version()
func _initialize():
preset_path = Data.project_path.path_join("export_presets.cfg")
if godot_path != Data.get_godot_path():
update_godot_version()
func _prevalidate() -> bool:
if OS.execute(godot_path, ["--version"], []) != OK:
error_message = "Godot executable (%s) is not valid." % godot_path
return false
var project := ConfigFile.new()
project.load(Data.project_path.path_join("project.godot"))
if project.get_value("", "config_version", 0) == 4 and not is_godot_3:
error_message = "Trying to export Godot 3 project using Godot 4 executable."
return false
var presets := load_presets()
if not presets:
error_message = "Export presets file does not exist."
return false
return true
func _get_command() -> String:
return Data.get_godot_path()
func _get_arguments() -> PackedStringArray:
var ret: PackedStringArray
if is_godot_3:
ret.append("--no-window")
else:
ret.append("--headless")
ret.append("--path")
ret.append(Data.project_path)
if export_debug:
ret.append("--export-debug")
elif is_godot_3:
ret.append("--export")
else:
ret.append("--export-release")
ret.append(export_preset)
if override_path:
ret.append(export_path)
return ret
func _prepare():
var base_dir := export_path.get_base_dir()
DirAccess.make_dir_recursive_absolute(base_dir)
static func update_godot_version():
godot_path = Data.get_godot_path()
var output: Array
if OS.execute(godot_path, ["--version"], output) == OK:
if output[0].begins_with("4"):
is_godot_3 = false
else:
is_godot_3 = true
func load_presets() -> ConfigFile:
var config_file := ConfigFile.new()
if config_file.load(preset_path) == OK:
return config_file
return null
func set_export_path(path: String):
if path.begins_with("res://"):
export_path = path.replace("res:/", Data.project_path)
else:
export_path = Data.project_path.path_join(path)
func setup_preset_list(list: OptionButton):
var export_presets := load_presets()
if export_presets:
for section in export_presets.get_sections():
if section.ends_with("options"):
continue
list.add_item(export_presets.get_value(section, "name"))
if list.item_count == 0:
list.add_item("No presets found in export_presets.cfg.")
list.disabled = true
================================================
FILE: Tasks/ExportTask.gd.uid
================================================
uid://ddyec862cgxyf
================================================
FILE: Tasks/PackZIP.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://dw4t3o5hj774w"]
[ext_resource type="PackedScene" uid="uid://cyl1d6reu3mk4" path="res://Nodes/GUI/DirectorySelector.tscn" id="1_smuq5"]
[sub_resource type="GDScript" id="GDScript_ess8h"]
script/source = "extends \"ScriptTask.gd\"
@onready var source_directory: Control = %SourceDirectory
@onready var target_path: Control = %TargetPath
@onready var file_filter: LineEdit = %FileFilter
@onready var file_filter2: LineEdit = %FileFilter2
func _get_task_name() -> String:
return \"Pack ZIP\"
func _get_execute_string() -> String:
return \"Pack folder \\\"%s\\\" into \\\"%s\\\"\" % [source_directory.text.get_file(), target_path.text.get_file()]
func _initialize() -> void:
defaults[\"source\"] = \"\"
defaults[\"destination\"] = \"\"
defaults[\"include_files\"] = \"\"
defaults[\"exclude_files\"] = \"\"
func _validate() -> bool:
var path := Data.resolve_path(source_directory.text)
if not DirAccess.dir_exists_absolute(path):
error_message = \"Source directory (%s) does not exist.\" % path
return false
return true
func _get_arguments() -> PackedStringArray:
var ret := super()
ret.append(Data.resolve_path(source_directory.text))
ret.append(Data.resolve_path(target_path.text))
if not file_filter.text.is_empty():
ret.append(\"--include\")
ret.append_array(file_filter.text.split(\" \"))
if not file_filter2.text.is_empty():
ret.append(\"--exclude\")
ret.append_array(file_filter2.text.split(\" \"))
return ret
func _load():
source_directory.text = data[\"source\"]
target_path.text = data[\"destination\"]
file_filter.text = data[\"include_files\"]
file_filter2.text = data[\"exclude_files\"]
func _store():
data[\"source\"] = source_directory.text
data[\"destination\"] = target_path.text
data[\"include_files\"] = file_filter.text
data[\"exclude_files\"] = file_filter2.text
func _get_task_info() -> PackedStringArray:
return [
\"Packs specified files in a ZIP archive.\",
\"Source Directory|Files from this directory will be packed.\",
\"Target File Path|Path of the target ZIP archive.\",
\"Include Files|List of filters for files from the directory. Example: \\\"*.exe *.pck\\\". If empty, all files will be packed. Otherwise only files that match the filter will be included.\",
\"Exclude Files|Filters files from the directory. Files that match this filter will be excluded.\",
]
"
[node name="MakeZip" type="VBoxContainer"]
offset_right = 734.0
offset_bottom = 101.0
script = SubResource("GDScript_ess8h")
script_name = "PackZIP.gd"
[node name="GridContainer" type="GridContainer" parent="."]
layout_mode = 2
columns = 2
[node name="Label2" type="Label" parent="GridContainer"]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
size_flags_horizontal = 8
text = "Source Directory"
horizontal_alignment = 2
[node name="SourceDirectory" parent="GridContainer" instance=ExtResource("1_smuq5")]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
scope = 1
missing_mode = 1
[node name="Label3" type="Label" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 8
text = "Target File Path"
horizontal_alignment = 2
[node name="TargetPath" parent="GridContainer" instance=ExtResource("1_smuq5")]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 3
mode = 2
scope = 1
filters = PackedStringArray("*.zip")
[node name="Label" type="Label" parent="GridContainer"]
layout_mode = 2
size_flags_horizontal = 8
text = "Include Files"
horizontal_alignment = 2
[node name="FileFilter" type="LineEdit" parent="GridContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Space-separated list of filters, e.g. \"*.exe\". Leave empty to include all."
[node name="Label4" type="Label" parent="GridContainer"]
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 8
text = "Exclude Files"
horizontal_alignment = 2
[node name="FileFilter2" type="LineEdit" parent="GridContainer"]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Space-separated list of filters. Leave empty to exclude none."
================================================
FILE: Tasks/ScriptTask/BaseScriptTask.gd
================================================
extends SceneTree
var argument_list: PackedStringArray
var arguments: Dictionary
func add_expected_argument(name: String, description: String):
argument_list.append("e|%s|%s" % [name, description])
func add_optional_argument(name: String, description: String):
argument_list.append("o|%s|%s" % [name, description])
func add_variadic_argument(name: String, description: String):
argument_list.append("v|%s|%s" % [name, description])
func fetch_arguments() -> bool:
var args := OS.get_cmdline_user_args()
if not args.is_empty() and args[0] == "--help":
print("Argument list:")
for argument in argument_list:
var type := argument.get_slice("|", 0)
print("%s (%s) - %s" % [
argument.get_slice("|", 1),
"expected" if type == "e" else "optional" if type == "o" else "variadic",
argument.get_slice("|", 2),
])
quit(OK)
return false
for i in argument_list.size():
var argument := argument_list[i]
var argument_name := argument.get_slice("|", 1)
if argument.begins_with("e") and i >= args.size():
printerr("Missing expected argument: %s" % argument_name)
quit(ERR_INVALID_PARAMETER)
return false
if i >= args.size():
if argument.begins_with("v"):
arguments[argument_name] = []
else:
arguments[argument_name] = ""
continue
if argument.begins_with("v"):
arguments[argument_name] = args.slice(i)
else:
arguments[argument_name] = args[i]
return true
================================================
FILE: Tasks/ScriptTask/BaseScriptTask.gd.uid
================================================
uid://dj2qru8dp0yi2
================================================
FILE: Tasks/ScriptTask/ClearDirectory.gd
================================================
extends "BaseScriptTask.gd"
func _init() -> void:
add_expected_argument("source", "Directory to clear.")
add_variadic_argument("filters", "List of filters.")
if not fetch_arguments():
return
var directory_path: String = arguments["source"]
var include_filters: PackedStringArray
var exclude_filters: PackedStringArray
var mode := 0
for arg: String in arguments["filters"]:
if arg == "--include":
mode = 1
elif arg == "--exclude":
mode = 2
else:
if mode == 1:
include_filters.append(arg)
elif mode == 2:
exclude_filters.append(arg)
DirAccess.make_dir_recursive_absolute("user://Trash")
var counter: int
var fail_counter: int
for file in DirAccess.get_files_at(directory_path):
var skip := not include_filters.is_empty()
for filter in include_filters:
if file.match(filter):
skip = false
break
if not skip:
for filter in exclude_filters:
if file.match(filter):
skip = true
break
if skip:
continue
print("Removing file: %s" % file)
var error := DirAccess.rename_absolute(directory_path.path_join(file), "user://Trash".path_join(file))
if error == OK:
counter += 1
else:
fail_counter += 1
printerr("Failed! Error: %d" % error)
if fail_counter > 0:
print("Cleanup finished. Cleared %d files, %d files failed." % [counter, fail_counter])
elif counter > 0:
print("Cleanup finished successfully. Cleared %d files." % counter)
else:
print("No files to cleanup.")
quit(OK)
================================================
FILE: Tasks/ScriptTask/ClearDirectory.gd.uid
================================================
uid://br3t3y5s843pj
================================================
FILE: Tasks/ScriptTask/CopyFiles.gd
================================================
extends "BaseScriptTask.gd"
func _init() -> void:
add_expected_argument("source", "Source directory for files.")
add_expected_argument("destination", "Destination directory for files.")
add_expected_argument("mode", "Either \"file\", \"dir\" or \"dir_recursive\".")
if not fetch_arguments():
return
var source_path: String = arguments["source"]
var target_path: String = arguments["destination"]
var folder_mode: bool = arguments["mode"] != "file"
var recursive: bool = arguments["mode"].ends_with("recursive")
var error: int = OK
if folder_mode:
error = copy_folder(source_path, target_path, recursive)
else:
if target_path.ends_with("/"):
error = copy_file(source_path, target_path.path_join(source_path.get_file()))
else:
error = copy_file(source_path, target_path)
if error == OK:
print("Copying finished successfully.")
else:
printerr("Copying failed, check error code.")
quit(error)
func copy_folder(source_path: String, target_path: String, recursive: bool) -> int:
for file in DirAccess.get_files_at(source_path):
var error := copy_file(source_path.path_join(file), target_path.path_join(file))
if error != OK:
return error
if recursive:
for dir in DirAccess.get_directories_at(source_path):
var error := copy_folder(source_path.path_join(dir), target_path.path_join(dir), true)
if error != OK:
return error
return OK
func copy_file(from: String, to: String) -> int:
if from == to:
printerr("Source and target file path are the same.")
return ERR_INVALID_PARAMETER
var error := DirAccess.make_dir_recursive_absolute(to.get_base_dir())
if error != OK:
return error
print("Copying %s to %s" % [from, to])
return DirAccess.copy_absolute(from, to)
================================================
FILE: Tasks/ScriptTask/CopyFiles.gd.uid
================================================
uid://dkrhktoogubcl
================================================
FILE: Tasks/ScriptTask/PackZIP.gd
================================================
extends "BaseScriptTask.gd"
var root_path: String
var include_filters: PackedStringArray
var exclude_filters: PackedStringArray
var quit_error: int
func _init() -> void:
add_expected_argument("source", "Source folder with files.")
add_expected_argument("destination", "Destination file path.")
add_variadic_argument("filters", "List of filters.")
if not fetch_arguments():
return
root_path = arguments["source"]
var target_path: String = arguments["destination"]
var mode := 0
for arg: String in arguments["filters"]:
if arg == "--include":
mode = 1
elif arg == "--exclude":
mode = 2
else:
if mode == 1:
include_filters.append(arg)
elif mode == 2:
exclude_filters.append(arg)
var zip := ZIPPacker.new()
print("Creating ZIP file: %s" % target_path)
var error := zip.open(target_path)
if error != OK:
printerr("Creating failed, error %d" % error)
quit(error)
return
pack_files(zip, root_path)
zip.close()
if quit_error == OK:
print("Packing finished successfully!")
else:
printerr("Packing failed, check error code.")
quit(quit_error)
func pack_files(zip: ZIPPacker, dir: String):
var da := DirAccess.open(dir)
if not da:
printerr("Error opening directory: %s" % dir)
quit_error = DirAccess.get_open_error()
return
da.include_hidden = true
for file in da.get_files():
var skip := not include_filters.is_empty()
for filter in include_filters:
if file.match(filter):
skip = false
break
if not skip:
for filter in exclude_filters:
if file.match(filter):
skip = true
break
if not skip:
pack(zip, dir.path_join(file))
if quit_error != OK:
return
for d in da.get_directories():
pack_files(zip, dir.path_join(d))
if quit_error != OK:
return
func pack(zip: ZIPPacker, file: String):
var target_file := file.trim_prefix(root_path + "/")
print("Packing file: %s" % target_file)
var data := FileAccess.get_file_as_bytes(file)
if data.is_empty():
var error := FileAccess.get_open_error()
if error != OK:
printerr("Error reading file: %d" % error)
quit_error = error
return
zip.start_file(target_file)
zip.write_file(data)
zip.close_file()
================================================
FILE: Tasks/ScriptTask/PackZIP.gd.uid
================================================
uid://ry5v70sx1vat
================================================
FILE: Tasks/ScriptTask.gd
================================================
extends Task
@export var script_name: String
func _get_command() -> String:
return OS.get_executable_path()
func _get_arguments() -> PackedStringArray:
var ret: PackedStringArray
ret.append("--headless")
if OS.has_feature("editor"):
ret.append("--path")
ret.append(Data.get_res_path())
ret.append("--script")
ret.append(get_script_path())
ret.append("--")
return ret
func _prevalidate() -> bool:
if script_name.is_empty():
error_message = "ScriptTask's script name is empty. Assign it in the scene."
return false
var script_path := get_script_path()
if not ResourceLoader.exists(script_path):
error_message = "The provided ScriptTask's script does not exist at expected path: \"%s\"." % script_path
return false
return true
func get_script_path() -> String:
var scr: Script = get_script()
while not scr.resource_path.get_file() == "ScriptTask.gd":
scr = scr.get_base_script()
return scr.resource_path.get_base_dir().path_join("ScriptTask/%s" % script_name)
================================================
FILE: Tasks/ScriptTask.gd.uid
================================================
uid://cu6cbkw8roupo
================================================
FILE: Tasks/SubRoutine.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://dh4e5n3xxf3n"]
[sub_resource type="GDScript" id="GDScript_boi7e"]
script/source = "extends Task
@onready var limbo: Node2D = %Limbo
@onready var sub_routine_list: OptionButton = %SubRoutine
var task_instances: Array[Task]
var task_index: int
var current_task: Task
func _get_task_name() -> String:
return \"Sub-Routine\"
func _get_execute_string() -> String:
if current_task:
return \"Sub-Routine: %s\" % current_task._get_task_name()
else:
return _get_task_name()
func _initialize():
if not is_node_ready():
await ready
for i in Data.routines.size():
var rout := Data.routines[i]
if rout == Data.current_routine:
continue
sub_routine_list.add_item(rout[\"name\"])
sub_routine_list.set_item_metadata(-1, rout[\"name\"])
if sub_routine_list.item_count == 0:
sub_routine_list.add_item(\"No other routine found. Create new routine.\")
sub_routine_list.set_item_metadata(-1, \"\")
sub_routine_list.disabled = true
func _prevalidate() -> bool:
if not task_instances.is_empty():
return true
var routine_name: String = data[\"routine\"]
var task_list: Array[Dictionary]
for rout in Data.routines:
if rout[\"name\"] == routine_name:
task_list = rout[\"tasks\"]
break
if task_list.is_empty():
error_message = \"Invalid routine name or no tasks.\"
return false
var current_routine_name: String = Data.current_routine[\"name\"]
var i := 0
while i < task_list.size():
var task := task_list[i]
if task[\"scene\"] == \"SubRoutine\":
var subroutine_name: String = task[\"data\"][\"routine\"]
if detect_loop([current_routine_name], subroutine_name):
error_message = \"Cyclic Sub-Routine detected.\"
return false
var subtask_list: Array[Dictionary]
for rout in Data.routines:
if rout[\"name\"] == subroutine_name:
subtask_list = rout[\"tasks\"]
break
task_list = task_list.slice(0, i) + subtask_list + task_list.slice(i + 1)
continue
var task_instance := Task.create_instance(task[\"scene\"])
limbo.add_child(task_instance)
task_instance._initialize()
task_instance.load_data(task[\"data\"])
task_instances.append(task_instance)
var result := task_instance._prevalidate()
if not result:
current_task = task_instance
error_message = task_instance.error_message
return false
i += 1
current_task = task_instances[0]
return true
func _validate() -> bool:
current_task = task_instances[task_index]
var result := current_task._validate()
error_message = current_task.error_message
return result
func _get_command() -> String:
return current_task._get_command()
func _get_arguments() -> PackedStringArray:
return current_task._get_arguments()
func _prepare() -> void:
current_task._prepare()
func _cleanup() -> void:
current_task._cleanup()
task_index += 1
if task_index < task_instances.size():
var next: Task = load(scene_file_path).instantiate()
next.task_instances = task_instances
next.task_index = task_index
get_parent().add_child(next)
get_parent().move_child(next, get_index() + 1)
func _load() -> void:
var routine_name: String = data[\"routine\"]
var found: bool
for i in sub_routine_list.item_count:
if sub_routine_list.get_item_metadata(i) == routine_name:
sub_routine_list.selected = i
found = true
break
if not found:
sub_routine_list.selected = -1
func _store() -> void:
data[\"routine\"] = sub_routine_list.get_selected_metadata()
if not data[\"routine\"]:
data[\"routine\"] = \"\"
func _get_task_info() -> PackedStringArray:
return [
\"Inlines another routine to allow running other routines as part of routines.\",
\"Routine|Name of the routine to run. Automatically lists all configured routines.\"
]
func detect_loop(base: PackedStringArray, routine: String) -> bool:
base.append(routine)
for rout in Data.routines:
if rout[\"name\"] == routine:
for task in rout[\"tasks\"]:
if task[\"scene\"] == \"SubRoutine\":
var routine2: String = task[\"data\"][\"routine\"]
if routine2 in base:
return true
if detect_loop(base, routine2):
return true
break
return false
"
[node name="SubRoutine" type="GridContainer"]
offset_right = 377.0
offset_bottom = 23.0
columns = 2
script = SubResource("GDScript_boi7e")
[node name="Label" type="Label" parent="."]
auto_translate_mode = 1
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "Routine"
horizontal_alignment = 2
[node name="SubRoutine" type="OptionButton" parent="."]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 3
[node name="Limbo" type="Node2D" parent="."]
unique_name_in_owner = true
visible = false
position = Vector2(0, 27)
================================================
FILE: Tasks/UploadEpic.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://228qq6rkrwx6"]
[ext_resource type="PackedScene" uid="uid://cyl1d6reu3mk4" path="res://Nodes/GUI/DirectorySelector.tscn" id="1_nh5oq"]
[sub_resource type="GDScript" id="GDScript_qbsc0"]
script/source = "extends Task
@onready var build_root: HBoxContainer = $BuildRoot
@onready var executable_name: LineEdit = $ExecutableName
@onready var version_prefix: LineEdit = $VersionPrefix
var project_version: String
func _get_task_name() -> String:
return \"Upload Epic\"
func _initialize() -> void:
defaults[\"build_root\"] = \"\"
defaults[\"executable_name\"] = \"\"
defaults[\"version_prefix\"] = \"\"
project_version = Data.get_project_version()
func _prevalidate() -> bool:
if not Data.global_config[\"epic_client_secret_env_var\"].is_empty():
var env := OS.get_environment(Data.global_config[\"epic_client_secret_env_var\"])
if env.is_empty():
error_message = \"The provided Epic client secret env variable does not exist.\"
return false
elif Data.global_config[\"epic_client_secret\"].is_empty():
error_message = \"Epic client secret is empty and no env variable was provided.\"
return false
if Data.global_config[\"build_patch_tool_path\"].is_empty():
error_message = \"Build Patch Tool path is empty.\"
elif not FileAccess.file_exists(Data.global_config[\"build_patch_tool_path\"]):
error_message = \"Build Patch Tool path does not point to any file.\"
elif Data.global_config[\"epic_organization_id\"].is_empty():
error_message = \"Epic organization ID is empty.\"
elif Data.global_config[\"epic_client_id\"].is_empty():
error_message = \"Epic client ID is empty.\"
elif Data.local_config[\"epic_product_id\"].is_empty():
error_message = \"Epic product ID is empty.\"
elif Data.local_config[\"epic_artifact_id\"].is_empty():
error_message = \"Epic artifact ID is empty.\"
elif executable_name.text.is_empty():
error_message = \"Executable name is empty.\"
elif project_version.length() < 1 or project_version.length() > 100:
error_message = \"Version string invalid (application/config/version). Length must be between 1 and 100 (inclusive).\"
else:
var reg := RegEx.create_from_string(r\"^[a-zA-Z0-9\\.\\+-_]*$\")
if not reg.search(project_version):
error_message = \"Version string invalid (application/config/version). Use only these characters: a-z, A-Z, 0-9, or .+-_\"
if not Data.global_config[\"epic_client_secret_env_var\"].is_empty():
var env := OS.get_environment(Data.global_config[\"epic_client_secret_env_var\"])
if env.is_empty():
error_message = \"The provided Epic client secret env variable does not exist.\"
elif Data.global_config[\"epic_client_secret\"].is_empty():
error_message = \"Epic client secret is empty and no env variable was provided.\"
return error_message.is_empty()
func _validate() -> bool:
if not DirAccess.dir_exists_absolute(Data.project_path.path_join(build_root.text)):
error_message = \"The provided build root folder does not exist.\"
return false
elif not FileAccess.file_exists(Data.project_path.path_join(build_root.text).path_join(executable_name.text)):
error_message = \"The executable does not exist in build root folder.\"
return false
return true
func _get_command() -> String:
return Data.global_config[\"build_patch_tool_path\"]
func _get_arguments() -> PackedStringArray:
var env_var: String = Data.global_config[\"epic_client_secret_env_var\"]
var ret: PackedStringArray
ret.append(\"-mode=UploadBinary\")
ret.append(\"-OrganizationId=\\\"%s\\\"\" % Data.global_config[\"epic_organization_id\"])
ret.append(\"-ClientId=\\\"%s\\\"\" % Data.global_config[\"epic_client_id\"])
if env_var.is_empty():
ret.append(\"-ClientSecret=\\\"%s\\\"\" % Data.global_config[\"epic_client_secret\"])
else:
ret.append(\"-ClientSecretEnvVar=\\\"%s\\\"\" % env_var)
ret.append(\"-ProductId=\\\"%s\\\"\" % Data.local_config[\"epic_product_id\"])
ret.append(\"-ArtifactId=\\\"%s\\\"\" % Data.local_config[\"epic_artifact_id\"])
ret.append(\"-BuildRoot=\\\"%s\\\"\" % build_root.text)
ret.append(\"-BuildVersion=\\\"%s-%s\\\"\" % [version_prefix.text, Data.get_project_version()])
ret.append(\"-AppLaunch=\\\"%s\\\"\" % executable_name.text)
#ret.append(\"-AppArgs=\\\"\\\"\")
ret.append(\"-CloudDir=\\\"%s\\\"\" % Data.project_path.path_join(Data.local_config[\"epic_cloud_dir\"]))
return ret
func _load():
build_root.text = data[\"build_root\"]
executable_name.text = data[\"executable_name\"]
version_prefix.text = data[\"version_prefix\"]
func _store():
data[\"build_root\"] = build_root.text
data[\"executable_name\"] = executable_name.text
data[\"version_prefix\"] = version_prefix.text
func _get_task_info() -> PackedStringArray:
return [
\"Uploads files to Epic Games using Build Patch Tool.\",
\"Build Root|The folder with your exported game content.\",
\"Executable Name|Name of the executable used when launching the game.\",
\"Version Prefix|Each upload needs unique version string, so use this option to add a (platform-specific etc.) prefix to your base version.\",
]
"
[node name="UploadEpic" type="GridContainer"]
offset_right = 422.0
offset_bottom = 101.0
columns = 2
script = SubResource("GDScript_qbsc0")
has_sensitive_data = true
[node name="Label" type="Label" parent="."]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "Build Root"
horizontal_alignment = 2
[node name="BuildRoot" parent="." instance=ExtResource("1_nh5oq")]
layout_mode = 2
size_flags_horizontal = 3
scope = 1
missing_mode = 1
[node name="Label2" type="Label" parent="."]
auto_translate_mode = 1
layout_mode = 2
text = "Executable Name"
horizontal_alignment = 2
[node name="ExecutableName" type="LineEdit" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label3" type="Label" parent="."]
auto_translate_mode = 1
layout_mode = 2
text = "Version Prefix"
horizontal_alignment = 2
[node name="VersionPrefix" type="LineEdit" parent="."]
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 3
================================================
FILE: Tasks/UploadGOG.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://7qw6mxp834ei"]
[sub_resource type="GDScript" id="GDScript_a0t4t"]
script/source = "extends Task
@onready var json_file: OptionButton = %JSONFile
@onready var branch: LineEdit = %Branch
@onready var branch_password: LineEdit = %BranchPassword
static var json_list: PackedStringArray
func _get_task_name() -> String:
return \"Upload GOG\"
func _get_execute_string() -> String:
return \"GOG Build (%s)\" % json_file.text
static func _initialize_project():
var cache_file := get_cache_file(\"GOGJSON\", FileAccess.READ)
if cache_file:
json_list = str_to_var(cache_file.get_as_text())
else:
json_list.clear()
static func _begin_project_scan() -> void:
json_list.clear()
static func _process_file(file: String):
if file.get_extension() != \"json\":
return
var json: JSON = load(file)
if not json.data is Dictionary:
return
var json_data: Dictionary = json.data
if not \"project\" in json_data or not \"baseProductId\" in json_data[\"project\"]:
return
json_list.append(file.trim_prefix(Data.project_path + \"/\"))
static func _end_project_scan() -> void:
var cache_file := get_cache_file(\"GOGJSON\", FileAccess.WRITE)
cache_file.store_string(var_to_str(json_list))
func _initialize() -> void:
defaults[\"json_path\"] = \"\"
defaults[\"branch\"] = \"\"
defaults[\"branch_password\"] = \"\"
if json_list.is_empty():
json_file.add_item(\"List empty. Run project scan from Config tab.\")
json_file.set_item_metadata(-1, \"\")
json_file.disabled = true
else:
for file in json_list:
json_file.add_item(file.get_file())
json_file.set_item_metadata(-1, file)
json_file.set_item_tooltip(-1, file)
func _prevalidate() -> bool:
if Data.global_config[\"pipeline_builder_path\"].is_empty():
error_message = \"Pipeline Builder path is empty.\"
elif not FileAccess.file_exists(Data.global_config[\"pipeline_builder_path\"]):
error_message = \"Pipeline Builder path does not point to any file.\"
elif Data.global_config[\"gog_username\"].is_empty():
error_message = \"GOG username is empty.\"
elif Data.global_config[\"gog_password\"].is_empty():
error_message = \"GOG password is empty.\"
elif Data.get_project_version().is_empty():
error_message = \"Project version (application/config/version) is empty.\"
return error_message.is_empty()
func _validate() -> bool:
if not FileAccess.file_exists(Data.project_path.path_join(get_json_path())):
error_message = \"The provided JSON file does not exist.\"
return false
return true
func _get_command() -> String:
return Data.global_config[\"pipeline_builder_path\"]
func _get_arguments() -> PackedStringArray:
var ret: PackedStringArray
ret.append(\"build-game\")
ret.append(Data.project_path.path_join(get_json_path()))
ret.append(\"--version\")
ret.append(Data.get_project_version())
ret.append(\"--username\")
ret.append(Data.global_config[\"gog_username\"])
ret.append(\"--password\")
ret.append(Data.global_config[\"gog_password\"])
if not branch.text.is_empty():
ret.append(\"--branch\")
ret.append(branch.text)
if not branch_password.text.is_empty():
ret.append(\"--branch_password\")
ret.append(branch_password.text)
#ret.append(\"--offline\")
return ret
func _load():
branch.text = data[\"branch\"]
branch.text = data[\"branch_password\"]
var file: String = data[\"json_path\"]
for i in json_file.item_count:
if json_file.get_item_metadata(i) == file:
json_file.selected = i
break
func _store():
data[\"json_path\"] = get_json_path()
data[\"branch\"] = branch.text
data[\"branch_password\"] = branch_password.text
func _get_task_info() -> PackedStringArray:
return [
\"Uploads files to GOG based on the given JSON file, using Pipeline Builder.\",
\"JSON File|File used as a base to upload.\",
\"Branch|Branch name to upload to.\",
\"Branch Password|Password for the branch if it's protected.\",
]
func get_json_path() -> String:
return json_file.get_selected_metadata()
"
[node name="UploadGog" type="GridContainer"]
offset_right = 520.0
offset_bottom = 23.0
columns = 2
script = SubResource("GDScript_a0t4t")
has_static_configuration = true
has_sensitive_data = true
[node name="Label" type="Label" parent="."]
auto_translate_mode = 1
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "JSON File"
horizontal_alignment = 2
[node name="JSONFile" type="OptionButton" parent="."]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 3
[node name="Label2" type="Label" parent="."]
layout_mode = 2
size_flags_horizontal = 8
text = "Branch"
horizontal_alignment = 2
[node name="Branch" type="LineEdit" parent="."]
unique_name_in_owner = true
layout_mode = 2
[node name="Label3" type="Label" parent="."]
auto_translate_mode = 1
layout_mode = 2
size_flags_horizontal = 8
text = "Branch Password"
horizontal_alignment = 2
[node name="BranchPassword" type="LineEdit" parent="."]
unique_name_in_owner = true
auto_translate_mode = 1
layout_mode = 2
================================================
FILE: Tasks/UploadItch.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://xt4wel0slqe4"]
[ext_resource type="PackedScene" uid="uid://cyl1d6reu3mk4" path="res://Nodes/GUI/DirectorySelector.tscn" id="1_qoq5m"]
[sub_resource type="GDScript" id="GDScript_5kvo4"]
script/source = "extends Task
@onready var source_folder: Control = %SourceFolder
@onready var channel: LineEdit = %Channel
@onready var project_version: CheckBox = %ProjectVersion
var version_file: String
func _get_task_name() -> String:
return \"Upload Itch\"
func _get_execute_string() -> String:
return \"Itch Upload (Channel: \\\"%s\\\")\" % channel.text
func _initialize() -> void:
defaults[\"source_folder\"] = \"\"
defaults[\"channel\"] = \"\"
defaults[\"use_project_version\"] = false
func _prevalidate() -> bool:
version_file = Data.local_config[\"itch_version_file\"]
if not version_file.is_empty():
version_file = Data.project_path.path_join(version_file)
if not FileAccess.file_exists(version_file):
error_message = \"The provided version file does not exist.\"
if project_version.button_pressed and Data.get_project_version().is_empty():
error_message = \"Use Project Version is enabled, but project version (application/config/version) is empty.\"
var butler_path: String = Data.global_config[\"itch_butler_path\"]
if butler_path.is_empty():
error_message = \"Butler path is empty.\"
elif not FileAccess.file_exists(butler_path):
error_message = \"Butler path does not point to any file.\"
else:
var pipe := OS.execute_with_pipe(butler_path, [\"login\"])
if pipe.is_empty():
error_message = \"Butler path does not point to a valid executable.\"
else:
var stdio: FileAccess = pipe[\"stdio\"]
OS.delay_msec(200)
var line := stdio.get_line()
if not line.contains(\"Your local credentials are valid!\"):
error_message = \"Butler credentials not valid. Please login before using.\"
OS.kill(pipe[\"pid\"])
return error_message.is_empty()
func _validate() -> bool:
if not DirAccess.dir_exists_absolute(Data.project_path.path_join(source_folder.text)):
error_message = \"The provided source directory does not exist.\"
return false
return true
func _get_command() -> String:
return Data.global_config[\"itch_butler_path\"]
func _get_arguments() -> PackedStringArray:
var ret: PackedStringArray
ret.append(\"push\")
ret.append(Data.project_path.path_join(source_folder.text))
var username: String = Data.global_config[\"itch_username\"]
var game_name: String = Data.local_config[\"itch_game_name\"]
var channel_name: String = channel.text
if channel_name.is_empty():
channel_name = Data.local_config[\"itch_default_channel\"]
ret.append(\"%s/%s:%s\" % [username, game_name, channel_name])
if project_version.button_pressed:
ret.append(\"--userversion\")
ret.append(Data.get_project_version())
elif not version_file.is_empty():
ret.append(\"--userversion-file\")
ret.append(version_file)
return ret
func _load():
source_folder.text = data[\"source_folder\"]
channel.text = data[\"channel\"]
project_version.button_pressed = data[\"use_project_version\"]
func _store():
data[\"source_folder\"] = source_folder.text
data[\"channel\"] = channel.text
data[\"use_project_version\"] = project_version.button_pressed
func _get_task_info() -> PackedStringArray:
return [
\"Uploads files to itch.io using butler. Remember to setup (login) butler before using this option.\",
\"Source Folder|The folder that will be provided for butler. It is automatically packed into ZIP before upload.\",
\"Channel|Channel to which the file will be uploaded. If empty, default channel from Local Config will be used.\",
\"Use Project Version|If enabled, provides userversion argument with version from \\\"application/config/version\\\".\",
]
"
[node name="UploadItch" type="GridContainer"]
offset_right = 536.0
offset_bottom = 66.0
columns = 2
script = SubResource("GDScript_5kvo4")
[node name="Label" type="Label" parent="."]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "Source Folder"
horizontal_alignment = 2
[node name="SourceFolder" parent="." instance=ExtResource("1_qoq5m")]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
scope = 1
missing_mode = 1
[node name="Label2" type="Label" parent="."]
auto_translate_mode = 1
layout_mode = 2
text = "Channel"
horizontal_alignment = 2
[node name="Channel" type="LineEdit" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="Control" type="Control" parent="."]
layout_mode = 2
[node name="ProjectVersion" type="CheckBox" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
text = "Use Project Version"
================================================
FILE: Tasks/UploadSteam.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://b7iec2eyiepn8"]
[sub_resource type="GDScript" id="GDScript_ftiq7"]
script/source = "extends Task
@onready var vdf_file: OptionButton = %VDFFile
static var vdf_list: PackedStringArray
func _get_task_name() -> String:
return \"Upload Steam\"
func _get_execute_string() -> String:
return \"Steam Build (%s)\" % vdf_file.text
static func _initialize_project():
var cache_file := get_cache_file(\"SteamVDF\", FileAccess.READ)
if cache_file:
vdf_list = str_to_var(cache_file.get_as_text())
else:
vdf_list.clear()
static func _begin_project_scan() -> void:
vdf_list.clear()
static func _process_file(file: String):
if file.get_extension() != \"vdf\":
return
var f := FileAccess.open(file, FileAccess.READ)
for line in f.get_as_text().split(\"\\n\"):
if line.contains(\"AppBuild\"):
vdf_list.append(file.trim_prefix(Data.project_path + \"/\"))
break
static func _end_project_scan() -> void:
var cache_file := get_cache_file(\"SteamVDF\", FileAccess.WRITE)
cache_file.store_string(var_to_str(vdf_list))
func _initialize() -> void:
defaults[\"vdf_path\"] = \"\"
if vdf_list.is_empty():
vdf_file.add_item(\"List empty. Run project scan from Config tab.\")
vdf_file.set_item_metadata(-1, \"\")
vdf_file.disabled = true
else:
for file in vdf_list:
vdf_file.add_item(file.get_file())
vdf_file.set_item_metadata(-1, file)
vdf_file.set_item_tooltip(-1, file)
func _prevalidate() -> bool:
if Data.global_config[\"steam_cmd_path\"].is_empty():
error_message = \"Steam CMD path is empty.\"
elif not FileAccess.file_exists(Data.global_config[\"steam_cmd_path\"]):
error_message = \"Steam CMD path does not point to any file.\"
elif Data.global_config[\"steam_username\"].is_empty():
error_message = \"Steam username is empty.\"
elif Data.global_config[\"steam_password\"].is_empty():
error_message = \"Steam password is empty.\"
return error_message.is_empty()
func _validate() -> bool:
if not FileAccess.file_exists(Data.project_path.path_join(get_vdf_path())):
error_message = \"The provided VDF file does not exist.\"
return false
return true
func _get_command() -> String:
return Data.global_config[\"steam_cmd_path\"]
func _get_arguments() -> PackedStringArray:
var ret: PackedStringArray
ret.append(\"+login\")
ret.append(Data.global_config[\"steam_username\"])
ret.append(Data.global_config[\"steam_password\"])
ret.append(\"+run_app_build\")
ret.append(Data.project_path.path_join(get_vdf_path()))
ret.append(\"+quit\")
return ret
func _load():
var file: String = data[\"vdf_path\"]
for i in vdf_file.item_count:
if vdf_file.get_item_metadata(i) == file:
vdf_file.selected = i
break
func _store():
data[\"vdf_path\"] = get_vdf_path()
func _get_task_info() -> PackedStringArray:
return [
\"Uploads files to Steam based on the given VDF file, using steamcmd.exe.\",
\"VDF File|File used as a base to upload. Only AppBuild VDF files are supported.\",
]
func get_vdf_path() -> String:
return vdf_file.get_selected_metadata()
"
[node name="UploadSteam" type="GridContainer"]
offset_right = 522.0
offset_bottom = 23.0
columns = 2
script = SubResource("GDScript_ftiq7")
has_static_configuration = true
has_sensitive_data = true
[node name="Label" type="Label" parent="."]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
text = "VDF File"
horizontal_alignment = 2
[node name="VDFFile" type="OptionButton" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
================================================
FILE: Tests/GutConfig.json
================================================
{
"background_color": "262626ff",
"compact_mode": false,
"configured_dirs": [
"res://Tests"
],
"dirs": [
"res://Tests"
],
"disable_colors": false,
"double_strategy": 1,
"errors_do_not_cause_failure": false,
"font_color": "ccccccff",
"font_name": "CourierPrime",
"font_size": 16,
"gut_on_top": true,
"hide_orphans": false,
"ignore_pause": false,
"include_subdirs": false,
"junit_xml_file": "",
"junit_xml_timestamp": false,
"log_level": 1,
"opacity": 100,
"paint_after": 0.1,
"post_run_script": "",
"pre_run_script": "",
"prefix": "Test",
"should_exit": false,
"should_exit_on_success": false,
"should_maximize": false,
"suffix": ".gd"
}
================================================
FILE: Tests/Projects/.gdignore
================================================
================================================
FILE: Tests/Projects/TestProject1/DeepDir/DirFile1.txt
================================================
DirFile1
================================================
FILE: Tests/Projects/TestProject1/DeepDir/DirFile2.txt
================================================
DirFile2
================================================
FILE: Tests/Projects/TestProject1/DeepDir/SubDir/SubDirFile1.txt
================================================
SubDirFile1
================================================
FILE: Tests/Projects/TestProject1/EmptyDir/.gdignore
================================================
================================================
FILE: Tests/Projects/TestProject1/File1.txt
================================================
File1
================================================
FILE: Tests/Projects/TestProject1/MixedDir/MdFile1.md
================================================
MdFile1
================================================
FILE: Tests/Projects/TestProject1/MixedDir/MdFile2.md
================================================
MdFile2
================================================
FILE: Tests/Projects/TestProject1/MixedDir/TxtFile1.txt
================================================
TxtFile1
================================================
FILE: Tests/Projects/TestProject1/MixedDir/TxtFile2.txt
================================================
TxtFile2
================================================
FILE: Tests/Projects/TestProject1/project.godot
================================================
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="TestProject1"
config/features=PackedStringArray("4.3", "Forward Plus")
================================================
FILE: Tests/Projects/TestProject1/project_builds_config.txt
================================================
{
"epic_artifact_id": "",
"epic_cloud_dir": "",
"epic_product_id": "",
"godot_path": "",
"itch_default_channel": "",
"itch_game_name": "",
"itch_version_file": "",
"routines": Array[Dictionary]([{
"name": "Copy Files Test",
"on_fail": 0,
"tasks": Array[Dictionary]([{
"data": {
"recursive": true,
"source_path": "File1.txt",
"target_path": "File1Copy.txt"
},
"scene": "CopyFiles"
}, {
"data": {
"recursive": true,
"source_path": "File1.txt",
"target_path": "EmptyDir/File1Copy.txt"
},
"scene": "CopyFiles"
}, {
"data": {
"recursive": true,
"source_path": "DeepDir",
"target_path": "EmptyDir/DirCopyRecursive"
},
"scene": "CopyFiles"
}, {
"data": {
"recursive": false,
"source_path": "DeepDir",
"target_path": "EmptyDir/DirCopyNotRecursive"
},
"scene": "CopyFiles"
}])
}, {
"name": "Clear Directory Files Test",
"on_fail": 0,
"tasks": Array[Dictionary]([{
"data": {
"exclude_files": "",
"include_files": "",
"target_directory": "DeepDir"
},
"scene": "ClearDirectory"
}])
}, {
"name": "Pack ZIP Test",
"on_fail": 0,
"tasks": Array[Dictionary]([{
"data": {
"destination": "DeepDir.zip",
"exclude_files": "",
"include_files": "",
"source": "DeepDir"
},
"scene": "PackZIP"
}, {
"data": {
"destination": "IncludeBlob.zip",
"exclude_files": "",
"include_files": "*.md",
"source": "MixedDir"
},
"scene": "PackZIP"
}, {
"data": {
"destination": "Exclude.zip",
"exclude_files": "SubDirFile1.txt",
"include_files": "",
"source": "DeepDir"
},
"scene": "PackZIP"
}, {
"data": {
"destination": "IncludeExclude.zip",
"exclude_files": "TxtFile1.txt",
"include_files": "*.txt",
"source": "MixedDir"
},
"scene": "PackZIP"
}])
}, {
"name": "Sub-Routine Test",
"on_fail": 0,
"tasks": Array[Dictionary]([{
"data": {
"routine": "Run Sub-Routines"
},
"scene": "SubRoutine"
}])
}, {
"name": "Run Sub-Routines",
"on_fail": 0,
"tasks": Array[Dictionary]([{
"data": {
"routine": "Copy 1 and 2"
},
"scene": "SubRoutine"
}, {
"data": {
"routine": "Copy 3 and 4"
},
"scene": "SubRoutine"
}])
}, {
"name": "Copy 1 and 2",
"on_fail": 0,
"tasks": Array[Dictionary]([{
"data": {
"recursive": true,
"source_path": "File1.txt",
"target_path": "Copy1.txt"
},
"scene": "CopyFiles"
}, {
"data": {
"recursive": true,
"source_path": "File1.txt",
"target_path": "Copy2.txt"
},
"scene": "CopyFiles"
}])
}, {
"name": "Copy 3 and 4",
"on_fail": 0,
"tasks": Array[Dictionary]([{
"data": {
"recursive": true,
"source_path": "File1.txt",
"target_path": "Copy3.txt"
},
"scene": "CopyFiles"
}, {
"data": {
"recursive": true,
"source_path": "File1.txt",
"target_path": "Copy4.txt"
},
"scene": "CopyFiles"
}])
}, {
"name": "Cyclic Sub-Routine 1",
"on_fail": 0,
"tasks": Array[Dictionary]([{
"data": {
"routine": "Cyclic Sub-Routine 2"
},
"scene": "SubRoutine"
}])
}, {
"name": "Cyclic Sub-Routine 2",
"on_fail": 0,
"tasks": Array[Dictionary]([{
"data": {
"routine": "Cyclic Sub-Routine 1"
},
"scene": "SubRoutine"
}])
}, {
"name": "Cyclic Sub-Routine 3",
"on_fail": 0,
"tasks": Array[Dictionary]([{
"data": {
"routine": "Cyclic Sub-Routine 1"
},
"scene": "SubRoutine"
}])
}]),
"templates": Array[Dictionary]([])
}
================================================
FILE: Tests/TestExecution.gd
================================================
extends GutTest
const PROJECTS := {
1: "res://Tests/Projects/TestProject1/",
}
const ROUTINES := {
# TestProject1
"copy_files": 0,
"clear_directory_files": 1,
"pack_zip": 2,
"sub_routine": 3,
"cyclic_sub_routine_1": 7,
"cyclic_sub_routine_2": 9,
}
const EXECUTION_TIMEOUT := 20.0
const Scene := preload("res://Scenes/Execution.tscn")
var scene: Node
var original_exec_delay
func before_all():
original_exec_delay = Data.global_config["execution_delay"]
Data.global_config["execution_delay"] = 0
func before_each():
scene = Scene.instantiate()
func after_each():
scene.free()
func after_all():
Data.global_config["execution_delay"] = original_exec_delay
func test_copy_files():
# Check assumptions
assert_true(FileAccess.file_exists(PROJECTS[1] + "File1.txt"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "File1Copy.txt"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "EmptyDir/File1Copy.txt"))
assert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + "DeepDir"))
assert_false(DirAccess.dir_exists_absolute(PROJECTS[1] + "EmptyDir/DirCopyRecursive"))
assert_false(DirAccess.dir_exists_absolute(PROJECTS[1] + "EmptyDir/DirCopyNotRecursive"))
# Setup
Data.load_project(PROJECTS[1])
Data.current_routine = Data.routines[ROUTINES.copy_files]
# Execute
add_child.call_deferred(scene)
await wait_for_signal(scene.finished, EXECUTION_TIMEOUT)
# Check results
assert_true(FileAccess.file_exists(PROJECTS[1] + "File1.txt"))
assert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + "DeepDir"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "File1Copy.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "EmptyDir/File1Copy.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "EmptyDir/DirCopyRecursive/DirFile1.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "EmptyDir/DirCopyRecursive/DirFile2.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "EmptyDir/DirCopyRecursive/SubDir/SubDirFile1.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "EmptyDir/DirCopyNotRecursive/DirFile1.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "EmptyDir/DirCopyNotRecursive/DirFile2.txt"))
assert_false(DirAccess.dir_exists_absolute(PROJECTS[1] + "EmptyDir/DirCopyNotRecursive/SubDir"))
# Cleanup
DirAccess.remove_absolute(PROJECTS[1] + "File1Copy.txt")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/File1Copy.txt")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/DirCopyRecursive/DirFile1.txt")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/DirCopyRecursive/DirFile2.txt")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/DirCopyRecursive/SubDir/SubDirFile1.txt")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/DirCopyRecursive/SubDir")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/DirCopyRecursive")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/DirCopyNotRecursive/DirFile1.txt")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/DirCopyNotRecursive/DirFile2.txt")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/DirCopyNotRecursive/SubDir/SubDirFile1.txt")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/DirCopyNotRecursive/SubDir")
DirAccess.remove_absolute(PROJECTS[1] + "EmptyDir/DirCopyNotRecursive")
func test_clear_directory_files():
# Check assumptions
assert_true(FileAccess.file_exists(PROJECTS[1] + "DeepDir/DirFile1.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "DeepDir/DirFile2.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "DeepDir/SubDir/SubDirFile1.txt"))
# Setup
DirAccess.copy_absolute(PROJECTS[1] + "DeepDir/DirFile1.txt", PROJECTS[1] + "EmptyDir/DirFile1.txt")
DirAccess.copy_absolute(PROJECTS[1] + "DeepDir/DirFile2.txt", PROJECTS[1] + "EmptyDir/DirFile2.txt")
Data.load_project(PROJECTS[1])
Data.current_routine = Data.routines[ROUTINES.clear_directory_files]
# Execute
add_child.call_deferred(scene)
await wait_for_signal(scene.finished, EXECUTION_TIMEOUT)
# Check results
assert_false(FileAccess.file_exists(PROJECTS[1] + "DeepDir/DirFile1.txt"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "DeepDir/DirFile2.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "DeepDir/SubDir/SubDirFile1.txt"))
# Cleanup
DirAccess.rename_absolute(PROJECTS[1] + "EmptyDir/DirFile1.txt", PROJECTS[1] + "DeepDir/DirFile1.txt")
DirAccess.rename_absolute(PROJECTS[1] + "EmptyDir/DirFile2.txt", PROJECTS[1] + "DeepDir/DirFile2.txt")
func test_pack_zip():
# Check assumptions
assert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + "DeepDir"))
assert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + "MixedDir"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "DeepDir.zip"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "IncludeBlob.zip"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "Exclude.zip"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "IncludeExclude.zip"))
# Setup
Data.load_project(PROJECTS[1])
Data.current_routine = Data.routines[ROUTINES.pack_zip]
# Execute
add_child.call_deferred(scene)
await wait_for_signal(scene.finished, EXECUTION_TIMEOUT)
# Check results
assert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + "DeepDir"))
assert_true(DirAccess.dir_exists_absolute(PROJECTS[1] + "MixedDir"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "DeepDir.zip"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "IncludeBlob.zip"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "Exclude.zip"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "IncludeExclude.zip"))
var reader := ZIPReader.new()
var error := reader.open(PROJECTS[1] + "DeepDir.zip")
var files := reader.get_files()
reader.close()
assert_true(error == OK)
assert_true(files.has("DirFile1.txt"))
assert_true(files.has("DirFile2.txt"))
assert_true(files.has("SubDir/SubDirFile1.txt"))
reader = ZIPReader.new()
error = reader.open(PROJECTS[1] + "IncludeBlob.zip")
files = reader.get_files()
reader.close()
assert_true(error == OK)
assert_false(files.has("TxtFile1.txt"))
assert_false(files.has("TxtFile2.txt"))
assert_true(files.has("MdFile1.md"))
assert_true(files.has("MdFile2.md"))
reader = ZIPReader.new()
error = reader.open(PROJECTS[1] + "Exclude.zip")
files = reader.get_files()
reader.close()
assert_true(error == OK)
assert_true(files.has("DirFile1.txt"))
assert_true(files.has("DirFile2.txt"))
assert_false(files.has("SubDir/SubDirFile1.txt"))
reader = ZIPReader.new()
error = reader.open(PROJECTS[1] + "IncludeExclude.zip")
files = reader.get_files()
reader.close()
assert_true(error == OK)
assert_false(files.has("TxtFile1.txt"))
assert_true(files.has("TxtFile2.txt"))
assert_false(files.has("MdFile1.md"))
assert_false(files.has("MdFile2.md"))
# Cleanup
DirAccess.remove_absolute(PROJECTS[1] + "DeepDir.zip")
DirAccess.remove_absolute(PROJECTS[1] + "IncludeBlob.zip")
DirAccess.remove_absolute(PROJECTS[1] + "Exclude.zip")
DirAccess.remove_absolute(PROJECTS[1] + "IncludeExclude.zip")
func test_sub_routine():
# Check assumptions
assert_true(FileAccess.file_exists(PROJECTS[1] + "File1.txt"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "Copy1.txt"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "Copy2.txt"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "Copy3.txt"))
assert_false(FileAccess.file_exists(PROJECTS[1] + "Copy4.txt"))
# Setup
Data.load_project(PROJECTS[1])
Data.current_routine = Data.routines[ROUTINES.sub_routine]
# Execute
add_child.call_deferred(scene)
await wait_for_signal(scene.finished, EXECUTION_TIMEOUT)
# Check results
assert_true(FileAccess.file_exists(PROJECTS[1] + "File1.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "Copy1.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "Copy2.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "Copy3.txt"))
assert_true(FileAccess.file_exists(PROJECTS[1] + "Copy4.txt"))
# Cleanup
DirAccess.remove_absolute(PROJECTS[1] + "Copy1.txt")
DirAccess.remove_absolute(PROJECTS[1] + "Copy2.txt")
DirAccess.remove_absolute(PROJECTS[1] + "Copy3.txt")
DirAccess.remove_absolute(PROJECTS[1] + "Copy4.txt")
func test_cyclic_sub_routine_1():
# Setup
Data.load_project(PROJECTS[1])
Data.current_routine = Data.routines[ROUTINES.cyclic_sub_routine_1]
# Execute
watch_signals(scene)
add_child.call_deferred(scene)
await wait_for_signal(scene.finished, EXECUTION_TIMEOUT)
# Check results
assert_signal_emitted(scene, "finished")
func test_cyclic_sub_routine_2():
# Setup
Data.load_project(PROJECTS[1])
Data.current_routine = Data.routines[ROUTINES.cyclic_sub_routine_2]
# Execute
watch_signals(scene)
add_child.call_deferred(scene)
await wait_for_signal(scene.finished, EXECUTION_TIMEOUT)
# Check results
assert_signal_emitted(scene, "finished")
================================================
FILE: Tests/TestExecution.gd.uid
================================================
uid://dbnpvtbcdms7l
================================================
FILE: addons/Prefab/Prefab.gd
================================================
extends PackedScene
class_name Prefab
static func create(node: Node, deferred_free := false) -> Prefab:
assert(node, "Invalid node provided.")
var to_check := node.get_children()
while not to_check.is_empty():
var sub: Node = to_check.pop_back()
if sub.owner == null:
continue
to_check.append_array(sub.get_children())
sub.owner = node
var prefab := Prefab.new()
prefab.pack(node)
if deferred_free:
node.queue_free()
else:
node.free()
return prefab
================================================
FILE: addons/Prefab/Prefab.gd.uid
================================================
uid://c3i8g8t027eyi
================================================
FILE: addons/ProjectBuilder/ProjectBuilderPlugin.gd
================================================
@tool
extends EditorPlugin
const CONFIG_SETTING = "_project_builder_config_path"
var popup: PopupMenu
var cached_routine_list: PackedStringArray
func _enter_tree() -> void:
popup = PopupMenu.new()
popup.index_pressed.connect(on_popup_action)
refresh_popup()
add_tool_submenu_item("Project Builder", popup)
EditorInterface.get_command_palette().add_command("Run Project Builder", "project_builder/run_project_builder", run_project_builder)
for routine in get_routine_list():
EditorInterface.get_command_palette().add_command("Execute: " + routine, "project_builder/" + routine, run_project_builder.bind(routine))
if ProjectSettings.has_setting("addons/project_builder/config_path"): # compat
ProjectSettings.set_setting(CONFIG_SETTING, ProjectSettings.get_setting("addons/project_builder/config_path"))
ProjectSettings.set_setting("addons/project_builder/config_path", null)
ProjectSettings.save()
elif not ProjectSettings.has_setting(CONFIG_SETTING):
ProjectSettings.set_setting(CONFIG_SETTING, "res://project_builds_config.txt")
ProjectSettings.set_as_internal(CONFIG_SETTING, true)
ProjectSettings.add_property_info({ "name": CONFIG_SETTING, "type": TYPE_STRING, "hint": PROPERTY_HINT_SAVE_FILE })
func _exit_tree() -> void:
remove_tool_menu_item("Project Builder")
EditorInterface.get_command_palette().remove_command("project_builder/run_project_builder")
for routine in get_routine_list():
EditorInterface.get_command_palette().remove_command("project_builder/" + routine)
func refresh_popup():
popup.clear()
cached_routine_list.clear()
popup.add_item("Run Project Builder")
popup.add_item("Refresh Routine List")
var routine_list := get_routine_list()
if routine_list.is_empty():
popup.add_separator("No Routines")
else:
popup.add_separator("Execute Routine")
for routine in routine_list:
popup.add_item(routine)
func get_routine_list() -> PackedStringArray:
if not cached_routine_list.is_empty():
return cached_routine_list
var routine_list: PackedStringArray
var project_builds_config := FileAccess.open(ProjectSettings.get_setting(CONFIG_SETTING), FileAccess.READ)
if project_builds_config:
var data: Dictionary = str_to_var(project_builds_config.get_as_text())
for routine in data["routines"]:
routine_list.append(routine["name"])
cached_routine_list = routine_list
return routine_list
func on_popup_action(idx: int):
match idx:
0:
run_project_builder()
1:
refresh_popup()
_:
run_project_builder(popup.get_item_text(idx))
func run_project_builder(routine := ""):
var project_builds_config := FileAccess.open(OS.get_user_data_dir().get_base_dir().path_join("Godot Project Builder/project_builds_config.txt"), FileAccess.READ)
if not project_builds_config:
OS.alert("Project Builder config file not found. Make sure you run Project Builder directly at least once.", "Something went wrong")
return
var data: Dictionary = str_to_var(project_builds_config.get_as_text())
var project_path: String = data["project_builder_path"]
if not DirAccess.dir_exists_absolute(project_path):
OS.alert("Project Builder project directory found. Make sure you run Project Builder directly at least once.", "Something went very wrong")
return
var arguments: PackedStringArray
if not project_path.is_empty():
arguments.append_array(["--path", project_path])
arguments.append_array(["--", "--open-project", ProjectSettings.globalize_path("res://").trim_suffix("/")])
if not routine.is_empty():
arguments.append_array(["--execute-routine", routine])
var executable_path: String = data["project_builder_executable"]
if FileAccess.file_exists(executable_path):
OS.create_process(executable_path, arguments)
else:
OS.create_instance(arguments)
================================================
FILE: addons/ProjectBuilder/ProjectBuilderPlugin.gd.uid
================================================
uid://sef5h7ohaqw0
================================================
FILE: addons/ProjectBuilder/plugin.cfg
================================================
[plugin]
name="Project Builder Plugin"
description="Helper addon to start Project Builder from within the project."
author="KoBeWi"
version="1.1.1"
script="ProjectBuilderPlugin.gd"
================================================
FILE: addons/gut/GutScene.gd
================================================
extends Node2D
# ##############################################################################
# This is a wrapper around the normal and compact gui controls and serves as
# the interface between gut.gd and the gui. The GutRunner creates an instance
# of this and then this takes care of managing the different GUI controls.
# ##############################################################################
@onready var _normal_gui = $Normal
@onready var _compact_gui = $Compact
var gut = null :
set(val):
gut = val
_set_gut(val)
func _ready():
_normal_gui.switch_modes.connect(use_compact_mode.bind(true))
_compact_gui.switch_modes.connect(use_compact_mode.bind(false))
_normal_gui.set_title("GUT")
_compact_gui.set_title("GUT")
_normal_gui.align_right()
_compact_gui.to_bottom_right()
use_compact_mode(false)
if(get_parent() == get_tree().root):
_test_running_setup()
func _test_running_setup():
set_font_size(100)
_normal_gui.get_textbox().text = "hello world, how are you doing?"
# ------------------------
# Private
# ------------------------
func _set_gut(val):
if(_normal_gui.get_gut() == val):
return
_normal_gui.set_gut(val)
_compact_gui.set_gut(val)
val.start_run.connect(_on_gut_start_run)
val.end_run.connect(_on_gut_end_run)
val.start_pause_before_teardown.connect(_on_gut_pause)
val.end_pause_before_teardown.connect(_on_pause_end)
func _set_both_titles(text):
_normal_gui.set_title(text)
_compact_gui.set_title(text)
# ------------------------
# Events
# ------------------------
func _on_gut_start_run():
_set_both_titles('Running')
func _on_gut_end_run():
_set_both_titles('Finished')
func _on_gut_pause():
_set_both_titles('-- Paused --')
func _on_pause_end():
_set_both_titles('Running')
# ------------------------
# Public
# ------------------------
func get_textbox():
return _normal_gui.get_textbox()
func set_font_size(new_size):
var rtl = _normal_gui.get_textbox()
rtl.set('theme_override_font_sizes/bold_italics_font_size', new_size)
rtl.set('theme_override_font_sizes/bold_font_size', new_size)
rtl.set('theme_override_font_sizes/italics_font_size', new_size)
rtl.set('theme_override_font_sizes/normal_font_size', new_size)
func set_font(font_name):
_set_all_fonts_in_rtl(_normal_gui.get_textbox(), font_name)
func _set_font(rtl, font_name, custom_name):
if(font_name == null):
rtl.remove_theme_font_override(custom_name)
else:
var dyn_font = FontFile.new()
dyn_font.load_dynamic_font('res://addons/gut/fonts/' + font_name + '.ttf')
rtl.add_theme_font_override(custom_name, dyn_font)
func _set_all_fonts_in_rtl(rtl, base_name):
if(base_name == 'Default'):
_set_font(rtl, null, 'normal_font')
_set_font(rtl, null, 'bold_font')
_set_font(rtl, null, 'italics_font')
_set_font(rtl, null, 'bold_italics_font')
else:
_set_font(rtl, base_name + '-Regular', 'normal_font')
_set_font(rtl, base_name + '-Bold', 'bold_font')
_set_font(rtl, base_name + '-Italic', 'italics_font')
_set_font(rtl, base_name + '-BoldItalic', 'bold_italics_font')
func set_default_font_color(color):
_normal_gui.get_textbox().set('custom_colors/default_color', color)
func set_background_color(color):
_normal_gui.set_bg_color(color)
func use_compact_mode(should=true):
_compact_gui.visible = should
_normal_gui.visible = !should
func set_opacity(val):
_normal_gui.modulate.a = val
_compact_gui.modulate.a = val
func set_title(text):
_set_both_titles(text)
================================================
FILE: addons/gut/GutScene.gd.uid
================================================
uid://dxo1rbug4pkeq
================================================
FILE: addons/gut/GutScene.tscn
================================================
[gd_scene load_steps=4 format=3 uid="uid://m28heqtswbuq"]
[ext_resource type="Script" uid="uid://dxo1rbug4pkeq" path="res://addons/gut/GutScene.gd" id="1_b4m8y"]
[ext_resource type="PackedScene" uid="uid://duxblir3vu8x7" path="res://addons/gut/gui/NormalGui.tscn" id="2_j6ywb"]
[ext_resource type="PackedScene" uid="uid://cnqqdfsn80ise" path="res://addons/gut/gui/MinGui.tscn" id="3_3glw1"]
[node name="GutScene" type="Node2D"]
script = ExtResource("1_b4m8y")
[node name="Normal" parent="." instance=ExtResource("2_j6ywb")]
[node name="Compact" parent="." instance=ExtResource("3_3glw1")]
offset_left = 5.0
offset_top = 273.0
offset_right = 265.0
offset_bottom = 403.0
================================================
FILE: addons/gut/LICENSE.md
================================================
The MIT License (MIT)
=====================
Copyright (c) 2018 Tom "Butch" Wesley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: addons/gut/UserFileViewer.gd
================================================
extends Window
@onready var rtl = $TextDisplay/RichTextLabel
func _get_file_as_text(path):
var to_return = null
var f = FileAccess.open(path, FileAccess.READ)
if(f != null):
to_return = f.get_as_text()
else:
to_return = str('ERROR: Could not open file. Error code ', FileAccess.get_open_error())
return to_return
func _ready():
rtl.clear()
func _on_OpenFile_pressed():
$FileDialog.popup_centered()
func _on_FileDialog_file_selected(path):
show_file(path)
func _on_Close_pressed():
self.hide()
func show_file(path):
var text = _get_file_as_text(path)
if(text == ''):
text = ''
rtl.set_text(text)
self.window_title = path
func show_open():
self.popup_centered()
$FileDialog.popup_centered()
func get_rich_text_label():
return $TextDisplay/RichTextLabel
func _on_Home_pressed():
rtl.scroll_to_line(0)
func _on_End_pressed():
rtl.scroll_to_line(rtl.get_line_count() -1)
func _on_Copy_pressed():
return
# OS.clipboard = rtl.text
func _on_file_dialog_visibility_changed():
if rtl.text.length() == 0 and not $FileDialog.visible:
self.hide()
================================================
FILE: addons/gut/UserFileViewer.gd.uid
================================================
uid://or7j8eak2s7t
================================================
FILE: addons/gut/UserFileViewer.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://bsm7wtt1gie4v"]
[ext_resource type="Script" uid="uid://or7j8eak2s7t" path="res://addons/gut/UserFileViewer.gd" id="1"]
[node name="UserFileViewer" type="Window"]
exclusive = true
script = ExtResource("1")
[node name="FileDialog" type="FileDialog" parent="."]
access = 1
show_hidden_files = true
[node name="TextDisplay" type="ColorRect" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_right = -10.0
offset_bottom = -65.0
color = Color(0.2, 0.188235, 0.188235, 1)
[node name="RichTextLabel" type="RichTextLabel" parent="TextDisplay"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
focus_mode = 2
text = "In publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used before final copy is available, but it may also be used to temporarily replace copy in a process called greeking, which allows designers to consider form without the meaning of the text influencing the design.
Lorem ipsum is typically a corrupted version of De finibus bonorum et malorum, a first-century BCE text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical, improper Latin.
Versions of the Lorem ipsum text have been used in typesetting at least since the 1960s, when it was popularized by advertisements for Letraset transfer sheets. Lorem ipsum was introduced to the digital world in the mid-1980s when Aldus employed it in graphic and word-processing templates for its desktop publishing program PageMaker. Other popular word processors including Pages and Microsoft Word have since adopted Lorem ipsum as well."
selection_enabled = true
[node name="OpenFile" type="Button" parent="."]
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -158.0
offset_top = -50.0
offset_right = -84.0
offset_bottom = -30.0
text = "Open File"
[node name="Home" type="Button" parent="."]
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -478.0
offset_top = -50.0
offset_right = -404.0
offset_bottom = -30.0
text = "Home"
[node name="Copy" type="Button" parent="."]
anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 160.0
offset_top = -50.0
offset_right = 234.0
offset_bottom = -30.0
text = "Copy"
[node name="End" type="Button" parent="."]
anchors_preset = 3
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -318.0
offset_top = -50.0
offset_right = -244.0
offset_bottom = -30.0
text = "End"
[node name="Close" type="Button" parent="."]
anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 10.0
offset_top = -50.0
offset_right = 80.0
offset_bottom = -30.0
text = "Close"
[connection signal="file_selected" from="FileDialog" to="." method="_on_FileDialog_file_selected"]
[connection signal="visibility_changed" from="FileDialog" to="." method="_on_file_dialog_visibility_changed"]
[connection signal="pressed" from="OpenFile" to="." method="_on_OpenFile_pressed"]
[connection signal="pressed" from="Home" to="." method="_on_Home_pressed"]
[connection signal="pressed" from="Copy" to="." method="_on_Copy_pressed"]
[connection signal="pressed" from="End" to="." method="_on_End_pressed"]
[connection signal="pressed" from="Close" to="." method="_on_Close_pressed"]
================================================
FILE: addons/gut/autofree.gd
================================================
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2020 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################
# Class used to keep track of objects to be freed and utilities to free them.
# ##############################################################################
var _to_free = []
var _to_queue_free = []
func add_free(thing):
if(typeof(thing) == TYPE_OBJECT):
if(!thing is RefCounted):
_to_free.append(thing)
func add_queue_free(thing):
_to_queue_free.append(thing)
func get_queue_free_count():
return _to_queue_free.size()
func get_free_count():
return _to_free.size()
func free_all():
for i in range(_to_free.size()):
if(is_instance_valid(_to_free[i])):
_to_free[i].free()
_to_free.clear()
for i in range(_to_queue_free.size()):
if(is_instance_valid(_to_queue_free[i])):
_to_queue_free[i].queue_free()
_to_queue_free.clear()
================================================
FILE: addons/gut/autofree.gd.uid
================================================
uid://u4sats0gvfeu
================================================
FILE: addons/gut/awaiter.gd
================================================
extends Node
signal timeout
signal wait_started
var _wait_time := 0.0
var _wait_frames := 0
var _signal_to_wait_on = null
var _predicate_function_waiting_to_be_true = null
var _predicate_time_between := 0.0
var _predicate_time_between_elpased := 0.0
var _did_last_wait_timeout = false
var did_last_wait_timeout = false :
get: return _did_last_wait_timeout
set(val): push_error("Cannot set did_last_wait_timeout")
var _elapsed_time := 0.0
var _elapsed_frames := 0
func _physics_process(delta):
if(_wait_time != 0.0):
_elapsed_time += delta
if(_elapsed_time >= _wait_time):
_end_wait()
if(_wait_frames != 0):
_elapsed_frames += 1
if(_elapsed_frames >= _wait_frames):
_end_wait()
if(_predicate_function_waiting_to_be_true != null):
_predicate_time_between_elpased += delta
if(_predicate_time_between_elpased >= _predicate_time_between):
_predicate_time_between_elpased = 0.0
var result = _predicate_function_waiting_to_be_true.call()
if(typeof(result) == TYPE_BOOL and result):
_end_wait()
func _end_wait():
# Check for time before checking for frames so that the extra frames added
# when waiting on a signal do not cause a false negative for timing out.
if(_wait_time > 0):
_did_last_wait_timeout = _elapsed_time >= _wait_time
elif(_wait_frames > 0):
_did_last_wait_timeout = _elapsed_frames >= _wait_frames
if(_signal_to_wait_on != null and _signal_to_wait_on.is_connected(_signal_callback)):
_signal_to_wait_on.disconnect(_signal_callback)
_wait_time = 0.0
_wait_frames = 0
_signal_to_wait_on = null
_predicate_function_waiting_to_be_true = null
_elapsed_time = 0.0
_elapsed_frames = 0
timeout.emit()
const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_'
func _signal_callback(
_arg1=ARG_NOT_SET, _arg2=ARG_NOT_SET, _arg3=ARG_NOT_SET,
_arg4=ARG_NOT_SET, _arg5=ARG_NOT_SET, _arg6=ARG_NOT_SET,
_arg7=ARG_NOT_SET, _arg8=ARG_NOT_SET, _arg9=ARG_NOT_SET):
_signal_to_wait_on.disconnect(_signal_callback)
# DO NOT _end_wait here. For other parts of the test to get the signal that
# was waited on, we have to wait for a couple more frames. For example, the
# signal_watcher doesn't get the signal in time if we don't do this.
_wait_frames = 2
func wait_seconds(x):
_did_last_wait_timeout = false
_wait_time = x
wait_started.emit()
func wait_frames(x):
_did_last_wait_timeout = false
_wait_frames = x
wait_started.emit()
func wait_for_signal(the_signal, max_time):
_did_last_wait_timeout = false
the_signal.connect(_signal_callback)
_signal_to_wait_on = the_signal
_wait_time = max_time
wait_started.emit()
func wait_until(predicate_function: Callable, max_time, time_between_calls:=0.0):
_predicate_time_between = time_between_calls
_predicate_function_waiting_to_be_true = predicate_function
_predicate_time_between_elpased = 0.0
_did_last_wait_timeout = false
_wait_time = max_time
wait_started.emit()
func is_waiting():
return _wait_time != 0.0 || _wait_frames != 0
================================================
FILE: addons/gut/awaiter.gd.uid
================================================
uid://cub0o1h1sbesy
================================================
FILE: addons/gut/cli/gut_cli.gd
================================================
extends Node
var Optparse = load('res://addons/gut/cli/optparse.gd')
var Gut = load('res://addons/gut/gut.gd')
var GutRunner = load('res://addons/gut/gui/GutRunner.tscn')
# ------------------------------------------------------------------------------
# Helper class to resolve the various different places where an option can
# be set. Using the get_value method will enforce the order of precedence of:
# 1. command line value
# 2. config file value
# 3. default value
#
# The idea is that you set the base_opts. That will get you a copies of the
# hash with null values for the other types of values. Lower precedented hashes
# will punch through null values of higher precedented hashes.
# ------------------------------------------------------------------------------
class OptionResolver:
var base_opts = {}
var cmd_opts = {}
var config_opts = {}
func get_value(key):
return _nvl(cmd_opts[key], _nvl(config_opts[key], base_opts[key]))
func set_base_opts(opts):
base_opts = opts
cmd_opts = _null_copy(opts)
config_opts = _null_copy(opts)
# creates a copy of a hash with all values null.
func _null_copy(h):
var new_hash = {}
for key in h:
new_hash[key] = null
return new_hash
func _nvl(a, b):
if(a == null):
return b
else:
return a
func _string_it(h):
var to_return = ''
for key in h:
to_return += str('(',key, ':', _nvl(h[key], 'NULL'), ')')
return to_return
func to_s():
return str("base:\n", _string_it(base_opts), "\n", \
"config:\n", _string_it(config_opts), "\n", \
"cmd:\n", _string_it(cmd_opts), "\n", \
"resolved:\n", _string_it(get_resolved_values()))
func get_resolved_values():
var to_return = {}
for key in base_opts:
to_return[key] = get_value(key)
return to_return
func to_s_verbose():
var to_return = ''
var resolved = get_resolved_values()
for key in base_opts:
to_return += str(key, "\n")
to_return += str(' default: ', _nvl(base_opts[key], 'NULL'), "\n")
to_return += str(' config: ', _nvl(config_opts[key], ' --'), "\n")
to_return += str(' cmd: ', _nvl(cmd_opts[key], ' --'), "\n")
to_return += str(' final: ', _nvl(resolved[key], 'NULL'), "\n")
return to_return
# ------------------------------------------------------------------------------
# Here starts the actual script that uses the Options class to kick off Gut
# and run your tests.
# ------------------------------------------------------------------------------
var _gut_config = load('res://addons/gut/gut_config.gd').new()
# array of command line options specified
var _final_opts = []
func setup_options(options, font_names):
var opts = Optparse.new()
opts.banner =\
"""
The GUT CLI
-----------
The default behavior for GUT is to load options from a res://.gutconfig.json if
it exists. Any options specified on the command line will take precedence over
options specified in the gutconfig file. You can specify a different gutconfig
file with the -gconfig option.
To generate a .gutconfig.json file you can use -gprint_gutconfig_sample
To see the effective values of a CLI command and a gutconfig use -gpo
Values for options can be supplied using:
option=value # no space around "="
option value # a space between option and value w/o =
Options whose values are lists/arrays can be specified multiple times:
-gdir=a,b
-gdir c,d
-gdir e
# results in -gdir equaling [a, b, c, d, e]
"""
opts.add_heading("Test Config:")
opts.add('-gdir', options.dirs, 'List of directories to search for test scripts in.')
opts.add('-ginclude_subdirs', false, 'Flag to include all subdirectories specified with -gdir.')
opts.add('-gtest', [], 'List of full paths to test scripts to run.')
opts.add('-gprefix', options.prefix, 'Prefix used to find tests when specifying -gdir. Default "[default]".')
opts.add('-gsuffix', options.suffix, 'Test script suffix, including .gd extension. Default "[default]".')
opts.add('-gconfig', 'res://.gutconfig.json', 'The config file to load options from. The default is [default]. Use "-gconfig=" to not use a config file.')
opts.add('-gpre_run_script', '', 'pre-run hook script path')
opts.add('-gpost_run_script', '', 'post-run hook script path')
opts.add('-gerrors_do_not_cause_failure', false, 'When an internal GUT error occurs tests will fail. With this option set, that does not happen.')
opts.add('-gdouble_strategy', 'SCRIPT_ONLY', 'Default strategy to use when doubling. Valid values are [INCLUDE_NATIVE, SCRIPT_ONLY]. Default "[default]"')
opts.add_heading("Run Options:")
opts.add('-gselect', '', 'All scripts that contain the specified string in their filename will be ran')
opts.add('-ginner_class', '', 'Only run inner classes that contain the specified string in their name.')
opts.add('-gunit_test_name', '', 'Any test that contains the specified text will be run, all others will be skipped.')
opts.add('-gexit', false, 'Exit after running tests. If not specified you have to manually close the window.')
opts.add('-gexit_on_success', false, 'Only exit if zero tests fail.')
opts.add('-gignore_pause', false, 'Ignores any calls to pause_before_teardown.')
opts.add_heading("Display Settings:")
opts.add('-glog', options.log_level, 'Log level [0-3]. Default [default]')
opts.add('-ghide_orphans', false, 'Display orphan counts for tests and scripts. Default [default].')
opts.add('-gmaximize', false, 'Maximizes test runner window to fit the viewport.')
opts.add('-gcompact_mode', false, 'The runner will be in compact mode. This overrides -gmaximize.')
opts.add('-gopacity', options.opacity, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.')
opts.add('-gdisable_colors', false, 'Disable command line colors.')
opts.add('-gfont_name', options.font_name, str('Valid values are: ', font_names, '. Default "[default]"'))
opts.add('-gfont_size', options.font_size, 'Font size, default "[default]"')
opts.add('-gbackground_color', options.background_color, 'Background color as an html color, default "[default]"')
opts.add('-gfont_color',options.font_color, 'Font color as an html color, default "[default]"')
opts.add('-gpaint_after', options.paint_after, 'Delay before GUT will add a 1 frame pause to paint the screen/GUI. default [default]')
opts.add_heading("Result Export:")
opts.add('-gjunit_xml_file', options.junit_xml_file, 'Export results of run to this file in the Junit XML format.')
opts.add('-gjunit_xml_timestamp', options.junit_xml_timestamp, 'Include a timestamp in the -gjunit_xml_file, default [default]')
opts.add_heading("Help:")
opts.add('-gh', false, 'Print this help. You did this to see this, so you probably understand.')
opts.add('-gpo', false, 'Print option values from all sources and the value used.')
opts.add('-gprint_gutconfig_sample', false, 'Print out json that can be used to make a gutconfig file.')
return opts
# Parses options, applying them to the _tester or setting values
# in the options struct.
func extract_command_line_options(from, to):
to.config_file = from.get_value_or_null('-gconfig')
to.dirs = from.get_value_or_null('-gdir')
to.disable_colors = from.get_value_or_null('-gdisable_colors')
to.double_strategy = from.get_value_or_null('-gdouble_strategy')
to.ignore_pause = from.get_value_or_null('-gignore_pause')
to.include_subdirs = from.get_value_or_null('-ginclude_subdirs')
to.inner_class = from.get_value_or_null('-ginner_class')
to.log_level = from.get_value_or_null('-glog')
to.opacity = from.get_value_or_null('-gopacity')
to.post_run_script = from.get_value_or_null('-gpost_run_script')
to.pre_run_script = from.get_value_or_null('-gpre_run_script')
to.prefix = from.get_value_or_null('-gprefix')
to.selected = from.get_value_or_null('-gselect')
to.should_exit = from.get_value_or_null('-gexit')
to.should_exit_on_success = from.get_value_or_null('-gexit_on_success')
to.should_maximize = from.get_value_or_null('-gmaximize')
to.compact_mode = from.get_value_or_null('-gcompact_mode')
to.hide_orphans = from.get_value_or_null('-ghide_orphans')
to.suffix = from.get_value_or_null('-gsuffix')
to.errors_do_not_cause_failure = from.get_value_or_null('-gerrors_do_not_cause_failure')
to.tests = from.get_value_or_null('-gtest')
to.unit_test_name = from.get_value_or_null('-gunit_test_name')
to.font_size = from.get_value_or_null('-gfont_size')
to.font_name = from.get_value_or_null('-gfont_name')
to.background_color = from.get_value_or_null('-gbackground_color')
to.font_color = from.get_value_or_null('-gfont_color')
to.paint_after = from.get_value_or_null('-gpaint_after')
to.junit_xml_file = from.get_value_or_null('-gjunit_xml_file')
to.junit_xml_timestamp = from.get_value_or_null('-gjunit_xml_timestamp')
func _print_gutconfigs(values):
var header = """Here is a sample of a full .gutconfig.json file.
You do not need to specify all values in your own file. The values supplied in
this sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample
option. Option priority is: command-line, .gutconfig, default)."""
print("\n", header.replace("\n", ' '), "\n")
var resolved = values
# remove_at some options that don't make sense to be in config
resolved.erase("config_file")
resolved.erase("show_help")
print(JSON.stringify(resolved, ' '))
for key in resolved:
resolved[key] = null
print("\n\nAnd here's an empty config for you fill in what you want.")
print(JSON.stringify(resolved, ' '))
func _run_tests(opt_resolver):
_final_opts = opt_resolver.get_resolved_values();
_gut_config.options = _final_opts
var runner = GutRunner.instantiate()
runner.ran_from_editor = false
runner.set_gut_config(_gut_config)
get_tree().root.add_child(runner)
runner.run_tests()
# parse options and run Gut
func main():
var opt_resolver = OptionResolver.new()
opt_resolver.set_base_opts(_gut_config.default_options)
var cli_opts = setup_options(_gut_config.default_options, _gut_config.valid_fonts)
cli_opts.parse()
var all_options_valid = cli_opts.unused.size() == 0
extract_command_line_options(cli_opts, opt_resolver.cmd_opts)
var config_path = opt_resolver.get_value('config_file')
var load_result = 1
# Checking for an empty config path allows us to not use a config file via
# the -gconfig_file option since using "-gconfig_file=" or -gconfig_file=''"
# will result in an empty string.
if(config_path != ''):
load_result = _gut_config.load_options_no_defaults(config_path)
# SHORTCIRCUIT
if(!all_options_valid):
print('Unknown arguments: ', cli_opts.unused)
get_tree().quit(1)
elif(load_result == -1):
print('Invalid gutconfig ', load_result)
get_tree().quit(1)
else:
opt_resolver.config_opts = _gut_config.options
if(cli_opts.get_value('-gh')):
print(GutUtils.version_numbers.get_version_text())
cli_opts.print_help()
get_tree().quit(0)
elif(cli_opts.get_value('-gpo')):
print('All config options and where they are specified. ' +
'The "final" value shows which value will actually be used ' +
'based on order of precedence (default < .gutconfig < cmd line).' + "\n")
print(opt_resolver.to_s_verbose())
get_tree().quit(0)
elif(cli_opts.get_value('-gprint_gutconfig_sample')):
_print_gutconfigs(opt_resolver.get_resolved_values())
get_tree().quit(0)
else:
_run_tests(opt_resolver)
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2023 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################
================================================
FILE: addons/gut/cli/gut_cli.gd.uid
================================================
uid://bvyb8bvwwhi67
================================================
FILE: addons/gut/cli/optparse.gd
================================================
# ##############################################################################
# Parses options from the command line, as one might expect. It can also
# generate help text that displays all the arguments your script accepts.
#
# This does alot, if you want to see it in action have a look at
# scratch/optparse_example.gd
#
#
# Godot Argument Lists
# -------------------------
# There are two sets of command line arguments that Godot populates:
# OS.get_cmdline_args
# OS.get_cmdline_user_args.
#
# OS.get_cmdline_args contains any arguments that are not used by the engine
# itself. This means options like --help and -d will never appear in this list
# since these are used by the engine. The one exception is the -s option which
# is always included as the first entry and the script path as the second.
# Optparse ignores these values for argument processing but can be accessed
# with my_optparse.options.script_option. This list does not contain any
# arguments that appear in OS.get_cmdline_user_args.
#
# OS.get_cmdline_user_args contains any arguments that appear on the command
# line AFTER " -- " or " ++ ". This list CAN contain options that the engine
# would otherwise use, and are ignored completely by the engine.
#
# The parse method, by default, includes arguments from OS.get_cmdline_args and
# OS.get_cmdline_user_args. You can optionally pass one of these to the parse
# method to limit which arguments are parsed. You can also conjure up your own
# array of arguments and pass that to parse.
#
# See Godot's documentation for get_cmdline_args and get_cmdline_user_args for
# more information.
#
#
# Adding Options
# --------------
# Use the following to add options to be parsed. These methods return the
# created Option instance. See that class above for more info. You can use
# the returned instance to get values, or use get_value/get_value_or_null.
# add("--name", "default", "Description goes here")
# add_required("--name", "default", "Description goes here")
# add_positional("--name", "default", "Description goes here")
# add_positional_required("--name", "default", "Description goes here")
#
# get_value will return the value of the option or the default if it was not
# set. get_value_or_null will return the value of the option or null if it was
# not set.
#
# The Datatype for an option is determined from the default value supplied to
# the various add methods. Supported types are
# String
# Int
# Float
# Array of strings
# Boolean
#
#
# Value Parsing
# -------------
# optparse uses option_name_prefix to differentiate between option names and
# values. Any argument that starts with this value will be treated as an
# argument name. The default is "-". Set this before calling parse if you want
# to change it.
#
# Values for options can be supplied on the command line with or without an "=":
# option=value # no space around "="
# option value # a space between option and value w/o =
# There is no way to escape "=" at this time.
#
# Array options can be specified multiple times and/or set from a comma delimited
# list.
# -gdir=a,b
# -gdir c,d
# -gdir e
# Results in -gdir equaling [a, b, c, d, e]. There is no way to escape commas
# at this time.
#
# To specify an empty list via the command line follow the option with an equal
# sign
# -gdir=
#
# Boolean options will have thier value set to !default when they are supplied
# on the command line. Boolean options cannot have a value on the command line.
# They are either supplied or not.
#
# If a value is not an array and is specified multiple times on the command line
# then the last entry will be used as the value.
#
# Positional argument values are parsed after all named arguments are parsed.
# This means that other options can appear before, between, and after positional
# arguments.
# --foo=bar positional_0_value --disabled --bar foo positional_1_value --a_flag
#
# Anything that is not used by named or positional arguments will appear in the
# unused property. You can use this to detect unrecognized arguments or treat
# everything else provided as a list of things, or whatever you want. You can
# use is_option on the elements of unused (or whatever you want really) to see
# if optparse would treat it as an option name.
#
# Use get_missing_required_options to get an array of Option with all required
# options that were not found when parsing.
#
# The parsed_args property holds the list of arguments that were parsed.
#
#
# Help Generation
# ---------------
# You can call get_help to generate help text, or you can just call print_help
# and this will print it for you.
#
# Set the banner property to any text you want to appear before the usage and
# options sections.
#
# Options are printed in the order they are added. You can add a heading for
# different options sections with add_heading.
# add("--asdf", 1, "This will have no heading")
# add_heading("foo")
# add("--foo", false, "This will have the foo heading")
# add("--another_foo", 1.5, "This too.")
# add_heading("This is after foo")
# add("--bar", true, "You probably get it by now.")
#
# If you include "[default]" in the description of a option, then the help will
# substitue it with the default value.
#
# ##############################################################################
#-------------------------------------------------------------------------------
# Holds all the properties of a command line option
#
# value will return the default when it has not been set.
#-------------------------------------------------------------------------------
class Option:
var _has_been_set = false
var _value = null
# REMEMBER that when this option is an array, you have to set the value
# before you alter the contents of the array (append etc) or has_been_set
# will return false and it might not be used right. For example
# get_value_or_null will return null when you've actually changed the value.
var value = _value:
get:
return _value
set(val):
_has_been_set = true
_value = val
var option_name = ''
var default = null
var description = ''
var required = false
func _init(name,default_value,desc=''):
option_name = name
default = default_value
description = desc
_value = default
func to_s(min_space=0):
var subbed_desc = description
subbed_desc = subbed_desc.replace('[default]', str(default))
return str(option_name.rpad(min_space), ' ', subbed_desc)
func has_been_set():
return _has_been_set
#-------------------------------------------------------------------------------
# A struct for organizing options by a heading
#-------------------------------------------------------------------------------
class OptionHeading:
var options = []
var display = 'default'
#-------------------------------------------------------------------------------
# Organizes options by order, heading, position. Also responsible for all
# help related text generation.
#-------------------------------------------------------------------------------
class Options:
var options = []
var positional = []
var default_heading = OptionHeading.new()
var script_option = Option.new('-s', '?', 'script option provided by Godot')
var _options_by_name = {}
var _options_by_heading = [default_heading]
var _cur_heading = default_heading
func add_heading(display):
var heading = OptionHeading.new()
heading.display = display
_cur_heading = heading
_options_by_heading.append(heading)
func add(option):
options.append(option)
_options_by_name[option.option_name] = option
_cur_heading.options.append(option)
func add_positional(option):
positional.append(option)
_options_by_name[option.option_name] = option
func get_by_name(option_name):
var found_param = null
if(option_name == script_option.option_name):
found_param = script_option
elif(_options_by_name.has(option_name)):
found_param = _options_by_name[option_name]
return found_param
func get_help_text():
var longest = 0
var text = ""
for i in range(options.size()):
if(options[i].option_name.length() > longest):
longest = options[i].option_name.length()
for heading in _options_by_heading:
if(heading != default_heading):
text += str("\n", heading.display, "\n")
for option in heading.options:
text += str(' ', option.to_s(longest + 2), "\n")
return text
func get_option_value_text():
var text = ""
var i = 0
for option in positional:
text += str(i, '. ', option.option_name, ' = ', option.value)
if(!option.has_been_set()):
text += " (default)"
text += "\n"
i += 1
for option in options:
text += str(option.option_name, ' = ', option.value)
if(!option.has_been_set()):
text += " (default)"
text += "\n"
return text
func print_option_values():
print(get_option_value_text())
func get_missing_required_options():
var to_return = []
for opt in options:
if(opt.required and !opt.has_been_set()):
to_return.append(opt)
for opt in positional:
if(opt.required and !opt.has_been_set()):
to_return.append(opt)
return to_return
func get_usage_text():
var pos_text = ""
for opt in positional:
pos_text += str("[", opt.description, "] ")
if(pos_text != ""):
pos_text += " [opts] "
return " -s " + script_option.value + " [opts] " + pos_text
#-------------------------------------------------------------------------------
#
# optarse
#
#-------------------------------------------------------------------------------
var options = Options.new()
var banner = ''
var option_name_prefix = '-'
var unused = []
var parsed_args = []
func _convert_value_to_array(raw_value):
var split = raw_value.split(',')
# This is what an empty set looks like from the command line. If we do
# not do this then we will always get back [''] which is not what it
# shoudl be.
if(split.size() == 1 and split[0] == ''):
split = []
return split
# REMEMBER raw_value not used for bools.
func _set_option_value(option, raw_value):
var t = typeof(option.default)
# only set values that were specified at the command line so that
# we can punch through default and config values correctly later.
# Without this check, you can't tell the difference between the
# defaults and what was specified, so you can't punch through
# higher level options.
if(t == TYPE_INT):
option.value = int(raw_value)
elif(t == TYPE_STRING):
option.value = str(raw_value)
elif(t == TYPE_ARRAY):
var values = _convert_value_to_array(raw_value)
if(!option.has_been_set()):
option.value = []
option.value.append_array(values)
elif(t == TYPE_BOOL):
option.value = !option.default
elif(t == TYPE_FLOAT):
option.value = float(raw_value)
elif(t == TYPE_NIL):
print(option.option_name + ' cannot be processed, it has a nil datatype')
else:
print(option.option_name + ' cannot be processed, it has unknown datatype:' + str(t))
func _parse_command_line_arguments(args):
var parsed_opts = args.duplicate()
var i = 0
var positional_index = 0
while i < parsed_opts.size():
var opt = ''
var value = ''
var entry = parsed_opts[i]
if(is_option(entry)):
if(entry.find('=') != -1):
var parts = entry.split('=')
opt = parts[0]
value = parts[1]
var the_option = options.get_by_name(opt)
if(the_option != null):
parsed_opts.remove_at(i)
_set_option_value(the_option, value)
else:
i += 1
else:
var the_option = options.get_by_name(entry)
if(the_option != null):
parsed_opts.remove_at(i)
if(typeof(the_option.default) == TYPE_BOOL):
_set_option_value(the_option, null)
elif(i < parsed_opts.size() and !is_option(parsed_opts[i])):
value = parsed_opts[i]
parsed_opts.remove_at(i)
_set_option_value(the_option, value)
else:
i += 1
else:
if(positional_index < options.positional.size()):
_set_option_value(options.positional[positional_index], entry)
parsed_opts.remove_at(i)
positional_index += 1
else:
i += 1
# this is the leftovers that were not extracted.
return parsed_opts
func is_option(arg):
return arg.begins_with(option_name_prefix)
func add(op_name, default, desc):
var new_op = null
if(options.get_by_name(op_name) != null):
push_error(str('Option [', op_name, '] already exists.'))
else:
new_op = Option.new(op_name, default, desc)
options.add(new_op)
return new_op
func add_required(op_name, default, desc):
var op = add(op_name, default, desc)
if(op != null):
op.required = true
return op
func add_positional(op_name, default, desc):
var new_op = null
if(options.get_by_name(op_name) != null):
push_error(str('Positional option [', op_name, '] already exists.'))
else:
new_op = Option.new(op_name, default, desc)
options.add_positional(new_op)
return new_op
func add_positional_required(op_name, default, desc):
var op = add_positional(op_name, default, desc)
if(op != null):
op.required = true
return op
func add_heading(display_text):
options.add_heading(display_text)
func get_value(name):
var found_param = options.get_by_name(name)
if(found_param != null):
return found_param.value
else:
print("COULD NOT FIND OPTION " + name)
return null
# This will return null instead of the default value if an option has not been
# specified. This can be useful when providing an order of precedence to your
# values. For example if
# default value < config file < command line
# then you do not want to get the default value for a command line option or it
# will overwrite the value in a config file.
func get_value_or_null(name):
var found_param = options.get_by_name(name)
if(found_param != null and found_param.has_been_set()):
return found_param.value
else:
return null
func get_help():
var sep = '---------------------------------------------------------'
var text = str(sep, "\n", banner, "\n\n")
text += "Usage\n-----------\n"
text += " " + options.get_usage_text() + "\n\n"
text += "\nOptions\n-----------\n"
text += options.get_help_text()
text += str(sep, "\n")
return text
func print_help():
print(get_help())
func parse(cli_args=null):
parsed_args = cli_args
if(parsed_args == null):
parsed_args = OS.get_cmdline_args()
parsed_args.append_array(OS.get_cmdline_user_args())
unused = _parse_command_line_arguments(parsed_args)
func get_missing_required_options():
return options.get_missing_required_options()
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2024 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################
================================================
FILE: addons/gut/cli/optparse.gd.uid
================================================
uid://c3h6h5q43socr
================================================
FILE: addons/gut/collected_script.gd
================================================
# ------------------------------------------------------------------------------
# This holds all the meta information for a test script. It contains the
# name of the inner class and an array of CollectedTests. This does not parse
# anything, it just holds the data about parsed scripts and tests. The
# TestCollector is responsible for populating this object.
#
# This class also facilitates all the exporting and importing of tests.
# ------------------------------------------------------------------------------
var CollectedTest = GutUtils.CollectedTest
var _lgr = null
# One entry per test found in the script. Added externally by TestCollector
var tests = []
# One entry for before_all and after_all (maybe add before_each and after_each).
# These are added by Gut when running before_all and after_all for the script.
var setup_teardown_tests = []
var inner_class_name:StringName
var path:String
# Set externally by test_collector after it can verify that the script was
# actually loaded. This could probably be changed to just hold the GutTest
# script that was loaded, cutting down on complexity elsewhere.
var is_loaded = false
# Set by Gut when it decides that a script should be skipped.
# Right now this is whenever the script has the variable skip_script declared.
# the value of skip_script is put into skip_reason.
var was_skipped = false
var skip_reason = ''
var was_run = false
var name = '' :
get: return path
set(val):pass
func _init(logger=null):
_lgr = logger
func get_new():
return load_script().new()
func load_script():
var to_return = load(path)
if(inner_class_name != null and inner_class_name != ''):
# If we wanted to do inner classes in inner classses
# then this would have to become some kind of loop or recursive
# call to go all the way down the chain or this class would
# have to change to hold onto the loaded class instead of
# just path information.
to_return = to_return.get(inner_class_name)
return to_return
# script.gd.InnerClass
func get_filename_and_inner():
var to_return = get_filename()
if(inner_class_name != ''):
to_return += '.' + String(inner_class_name)
return to_return
# res://foo/bar.gd.FooBar
func get_full_name():
var to_return = path
if(inner_class_name != ''):
to_return += '.' + String(inner_class_name)
return to_return
func get_filename():
return path.get_file()
func has_inner_class():
return inner_class_name != ''
# Note: although this no longer needs to export the inner_class names since
# they are pulled from metadata now, it is easier to leave that in
# so we don't have to cut the export down to unique script names.
func export_to(config_file, section):
config_file.set_value(section, 'path', path)
config_file.set_value(section, 'inner_class', inner_class_name)
var names = []
for i in range(tests.size()):
names.append(tests[i].name)
config_file.set_value(section, 'tests', names)
func _remap_path(source_path):
var to_return = source_path
if(!FileAccess.file_exists(source_path)):
_lgr.debug('Checking for remap for: ' + source_path)
var remap_path = source_path.get_basename() + '.gd.remap'
if(FileAccess.file_exists(remap_path)):
var cf = ConfigFile.new()
cf.load(remap_path)
to_return = cf.get_value('remap', 'path')
else:
_lgr.warn('Could not find remap file ' + remap_path)
return to_return
func import_from(config_file, section):
path = config_file.get_value(section, 'path')
path = _remap_path(path)
# Null is an acceptable value, but you can't pass null as a default to
# get_value since it thinks you didn't send a default...then it spits
# out red text. This works around that.
var inner_name = config_file.get_value(section, 'inner_class', 'Placeholder')
if(inner_name != 'Placeholder'):
inner_class_name = inner_name
else: # just being explicit
inner_class_name = StringName("")
func get_test_named(test_name):
return GutUtils.search_array(tests, 'name', test_name)
func get_ran_test_count():
var count = 0
for t in tests:
if(t.was_run):
count += 1
return count
func get_assert_count():
var count = 0
for t in tests:
count += t.pass_texts.size()
count += t.fail_texts.size()
for t in setup_teardown_tests:
count += t.pass_texts.size()
count += t.fail_texts.size()
return count
func get_pass_count():
var count = 0
for t in tests:
count += t.pass_texts.size()
for t in setup_teardown_tests:
count += t.pass_texts.size()
return count
func get_fail_count():
var count = 0
for t in tests:
count += t.fail_texts.size()
for t in setup_teardown_tests:
count += t.fail_texts.size()
return count
func get_pending_count():
var count = 0
for t in tests:
count += t.pending_texts.size()
return count
func get_passing_test_count():
var count = 0
for t in tests:
if(t.is_passing()):
count += 1
return count
func get_failing_test_count():
var count = 0
for t in tests:
if(t.is_failing()):
count += 1
return count
func get_risky_count():
var count = 0
if(was_skipped):
count = 1
else:
for t in tests:
if(t.is_risky()):
count += 1
return count
func to_s():
var to_return = path
if(inner_class_name != null):
to_return += str('.', inner_class_name)
to_return += "\n"
for i in range(tests.size()):
to_return += str(' ', tests[i].to_s())
return to_return
================================================
FILE: addons/gut/collected_script.gd.uid
================================================
uid://dsl71gqb5syn4
================================================
FILE: addons/gut/collected_test.gd
================================================
# ------------------------------------------------------------------------------
# Used to keep track of info about each test ran.
# ------------------------------------------------------------------------------
# the name of the function
var name = ""
# flag to know if the name has been printed yet. Used by the logger.
var has_printed_name = false
# the number of arguments the method has
var arg_count = 0
# the time it took to execute the test in seconds
var time_taken : float = 0
# The number of asserts in the test. Converted to a property for backwards
# compatibility. This now reflects the text sizes instead of being a value
# that can be altered externally.
var assert_count = 0 :
get: return pass_texts.size() + fail_texts.size()
set(val): pass
# Converted to propety for backwards compatibility. This now cannot be set
# externally
var pending = false :
get: return is_pending()
set(val): pass
# the line number when the test fails
var line_number = -1
# Set internally by Gut using whatever reason Gut wants to use to set this.
# Gut will skip these marked true and the test will be listed as risky.
var should_skip = false # -- Currently not used by GUT don't believe ^
var pass_texts = []
var fail_texts = []
var pending_texts = []
var orphans = 0
var was_run = false
func did_pass():
return is_passing()
func add_fail(fail_text):
fail_texts.append(fail_text)
func add_pending(pending_text):
pending_texts.append(pending_text)
func add_pass(passing_text):
pass_texts.append(passing_text)
# must have passed an assert and not have any other status to be passing
func is_passing():
return pass_texts.size() > 0 and fail_texts.size() == 0 and pending_texts.size() == 0
# failing takes precedence over everything else, so any failures makes the
# test a failure.
func is_failing():
return fail_texts.size() > 0
# test is only pending if pending was called and the test is not failing.
func is_pending():
return pending_texts.size() > 0 and fail_texts.size() == 0
func is_risky():
return should_skip or (was_run and !did_something())
func did_something():
return is_passing() or is_failing() or is_pending()
func get_status_text():
var to_return = GutUtils.TEST_STATUSES.NO_ASSERTS
if(should_skip):
to_return = GutUtils.TEST_STATUSES.SKIPPED
elif(!was_run):
to_return = GutUtils.TEST_STATUSES.NOT_RUN
elif(pending_texts.size() > 0):
to_return = GutUtils.TEST_STATUSES.PENDING
elif(fail_texts.size() > 0):
to_return = GutUtils.TEST_STATUSES.FAILED
elif(pass_texts.size() > 0):
to_return = GutUtils.TEST_STATUSES.PASSED
return to_return
# Deprecated
func get_status():
return get_status_text()
func to_s():
var pad = ' '
var to_return = str(name, "[", get_status_text(), "]\n")
for i in range(fail_texts.size()):
to_return += str(pad, 'Fail: ', fail_texts[i])
for i in range(pending_texts.size()):
to_return += str(pad, 'Pending: ', pending_texts[i], "\n")
for i in range(pass_texts.size()):
to_return += str(pad, 'Pass: ', pass_texts[i], "\n")
return to_return
================================================
FILE: addons/gut/collected_test.gd.uid
================================================
uid://b2edfqjiogifb
================================================
FILE: addons/gut/comparator.gd
================================================
var _strutils = GutUtils.Strutils.new()
var _max_length = 100
var _should_compare_int_to_float = true
const MISSING = '|__missing__gut__compare__value__|'
func _cannot_compare_text(v1, v2):
return str('Cannot compare ', _strutils.types[typeof(v1)], ' with ',
_strutils.types[typeof(v2)], '.')
func _make_missing_string(text):
return ''
func _create_missing_result(v1, v2, text):
var to_return = null
var v1_str = format_value(v1)
var v2_str = format_value(v2)
if(typeof(v1) == TYPE_STRING and v1 == MISSING):
v1_str = _make_missing_string(text)
to_return = GutUtils.CompareResult.new()
elif(typeof(v2) == TYPE_STRING and v2 == MISSING):
v2_str = _make_missing_string(text)
to_return = GutUtils.CompareResult.new()
if(to_return != null):
to_return.summary = str(v1_str, ' != ', v2_str)
to_return.are_equal = false
return to_return
func simple(v1, v2, missing_string=''):
var missing_result = _create_missing_result(v1, v2, missing_string)
if(missing_result != null):
return missing_result
var result = GutUtils.CompareResult.new()
var cmp_str = null
var extra = ''
var tv1 = typeof(v1)
var tv2 = typeof(v2)
# print(tv1, '::', tv2, ' ', _strutils.types[tv1], '::', _strutils.types[tv2])
if(_should_compare_int_to_float and [TYPE_INT, TYPE_FLOAT].has(tv1) and [TYPE_INT, TYPE_FLOAT].has(tv2)):
result.are_equal = v1 == v2
elif([TYPE_STRING, TYPE_STRING_NAME].has(tv1) and [TYPE_STRING, TYPE_STRING_NAME].has(tv2)):
result.are_equal = v1 == v2
elif(GutUtils.are_datatypes_same(v1, v2)):
result.are_equal = v1 == v2
if(typeof(v1) == TYPE_DICTIONARY or typeof(v1) == TYPE_ARRAY):
var sub_result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP)
result.summary = sub_result.get_short_summary()
if(!sub_result.are_equal):
extra = ".\n" + sub_result.get_short_summary()
else:
cmp_str = '!='
result.are_equal = false
extra = str('. ', _cannot_compare_text(v1, v2))
cmp_str = get_compare_symbol(result.are_equal)
result.summary = str(format_value(v1), ' ', cmp_str, ' ', format_value(v2), extra)
return result
func shallow(v1, v2):
var result = null
if(GutUtils.are_datatypes_same(v1, v2)):
if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]):
result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP)
else:
result = simple(v1, v2)
else:
result = simple(v1, v2)
return result
func deep(v1, v2):
var result = null
if(GutUtils.are_datatypes_same(v1, v2)):
if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]):
result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP)
else:
result = simple(v1, v2)
else:
result = simple(v1, v2)
return result
func format_value(val, max_val_length=_max_length):
return _strutils.truncate_string(_strutils.type2str(val), max_val_length)
func compare(v1, v2, diff_type=GutUtils.DIFF.SIMPLE):
var result = null
if(diff_type == GutUtils.DIFF.SIMPLE):
result = simple(v1, v2)
elif(diff_type == GutUtils.DIFF.DEEP):
result = deep(v1, v2)
return result
func get_should_compare_int_to_float():
return _should_compare_int_to_float
func set_should_compare_int_to_float(should_compare_int_float):
_should_compare_int_to_float = should_compare_int_float
func get_compare_symbol(is_equal):
if(is_equal):
return '=='
else:
return '!='
================================================
FILE: addons/gut/comparator.gd.uid
================================================
uid://3psq3ss4wepq
================================================
FILE: addons/gut/compare_result.gd
================================================
var _are_equal = false
var are_equal = false :
get:
return get_are_equal()
set(val):
set_are_equal(val)
var _summary = null
var summary = null :
get:
return get_summary()
set(val):
set_summary(val)
var _max_differences = 30
var max_differences = 30 :
get:
return get_max_differences()
set(val):
set_max_differences(val)
var _differences = {}
var differences :
get:
return get_differences()
set(val):
set_differences(val)
func _block_set(which, val):
push_error(str('cannot set ', which, ', value [', val, '] ignored.'))
func _to_string():
return str(get_summary()) # could be null, gotta str it.
func get_are_equal():
return _are_equal
func set_are_equal(r_eq):
_are_equal = r_eq
func get_summary():
return _summary
func set_summary(smry):
_summary = smry
func get_total_count():
pass
func get_different_count():
pass
func get_short_summary():
return summary
func get_max_differences():
return _max_differences
func set_max_differences(max_diff):
_max_differences = max_diff
func get_differences():
return _differences
func set_differences(diffs):
_block_set('differences', diffs)
func get_brackets():
return null
================================================
FILE: addons/gut/compare_result.gd.uid
================================================
uid://dscxpe8c6ytjo
================================================
FILE: addons/gut/diff_formatter.gd
================================================
var _strutils = GutUtils.Strutils.new()
const INDENT = ' '
var _max_to_display = 30
const ABSOLUTE_MAX_DISPLAYED = 10000
const UNLIMITED = -1
func _single_diff(diff, depth=0):
var to_return = ""
var brackets = diff.get_brackets()
if(brackets != null and !diff.are_equal):
to_return = ''
to_return += str(brackets.open, "\n",
_strutils.indent_text(differences_to_s(diff.differences, depth), depth+1, INDENT), "\n",
brackets.close)
else:
to_return = str(diff)
return to_return
func make_it(diff):
var to_return = ''
if(diff.are_equal):
to_return = diff.summary
else:
if(_max_to_display == ABSOLUTE_MAX_DISPLAYED):
to_return = str(diff.get_value_1(), ' != ', diff.get_value_2())
else:
to_return = diff.get_short_summary()
to_return += str("\n", _strutils.indent_text(_single_diff(diff, 0), 1, ' '))
return to_return
func differences_to_s(differences, depth=0):
var to_return = ''
var keys = differences.keys()
keys.sort()
var limit = min(_max_to_display, differences.size())
for i in range(limit):
var key = keys[i]
to_return += str(key, ": ", _single_diff(differences[key], depth))
if(i != limit -1):
to_return += "\n"
if(differences.size() > _max_to_display):
to_return += str("\n\n... ", differences.size() - _max_to_display, " more.")
return to_return
func get_max_to_display():
return _max_to_display
func set_max_to_display(max_to_display):
_max_to_display = max_to_display
if(_max_to_display == UNLIMITED):
_max_to_display = ABSOLUTE_MAX_DISPLAYED
================================================
FILE: addons/gut/diff_formatter.gd.uid
================================================
uid://be6htaq8rkbd0
================================================
FILE: addons/gut/diff_tool.gd
================================================
extends 'res://addons/gut/compare_result.gd'
const INDENT = ' '
enum {
DEEP,
SIMPLE
}
var _strutils = GutUtils.Strutils.new()
var _compare = GutUtils.Comparator.new()
var DiffTool = load('res://addons/gut/diff_tool.gd')
var _value_1 = null
var _value_2 = null
var _total_count = 0
var _diff_type = null
var _brackets = null
var _valid = true
var _desc_things = 'somethings'
# -------- comapre_result.gd "interface" ---------------------
func set_are_equal(val):
_block_set('are_equal', val)
func get_are_equal():
if(!_valid):
return null
else:
return differences.size() == 0
func set_summary(val):
_block_set('summary', val)
func get_summary():
return summarize()
func get_different_count():
return differences.size()
func get_total_count():
return _total_count
func get_short_summary():
var text = str(_strutils.truncate_string(str(_value_1), 50),
' ', _compare.get_compare_symbol(are_equal), ' ',
_strutils.truncate_string(str(_value_2), 50))
if(!are_equal):
text += str(' ', get_different_count(), ' of ', get_total_count(),
' ', _desc_things, ' do not match.')
return text
func get_brackets():
return _brackets
# -------- comapre_result.gd "interface" ---------------------
func _invalidate():
_valid = false
differences = null
func _init(v1,v2,diff_type=DEEP):
_value_1 = v1
_value_2 = v2
_diff_type = diff_type
_compare.set_should_compare_int_to_float(false)
_find_differences(_value_1, _value_2)
func _find_differences(v1, v2):
if(GutUtils.are_datatypes_same(v1, v2)):
if(typeof(v1) == TYPE_ARRAY):
_brackets = {'open':'[', 'close':']'}
_desc_things = 'indexes'
_diff_array(v1, v2)
elif(typeof(v2) == TYPE_DICTIONARY):
_brackets = {'open':'{', 'close':'}'}
_desc_things = 'keys'
_diff_dictionary(v1, v2)
else:
_invalidate()
GutUtils.get_logger().error('Only Arrays and Dictionaries are supported.')
else:
_invalidate()
GutUtils.get_logger().error('Only Arrays and Dictionaries are supported.')
func _diff_array(a1, a2):
_total_count = max(a1.size(), a2.size())
for i in range(a1.size()):
var result = null
if(i < a2.size()):
if(_diff_type == DEEP):
result = _compare.deep(a1[i], a2[i])
else:
result = _compare.simple(a1[i], a2[i])
else:
result = _compare.simple(a1[i], _compare.MISSING, 'index')
if(!result.are_equal):
differences[i] = result
if(a1.size() < a2.size()):
for i in range(a1.size(), a2.size()):
differences[i] = _compare.simple(_compare.MISSING, a2[i], 'index')
func _diff_dictionary(d1, d2):
var d1_keys = d1.keys()
var d2_keys = d2.keys()
# Process all the keys in d1
_total_count += d1_keys.size()
for key in d1_keys:
if(!d2.has(key)):
differences[key] = _compare.simple(d1[key], _compare.MISSING, 'key')
else:
d2_keys.remove_at(d2_keys.find(key))
var result = null
if(_diff_type == DEEP):
result = _compare.deep(d1[key], d2[key])
else:
result = _compare.simple(d1[key], d2[key])
if(!result.are_equal):
differences[key] = result
# Process all the keys in d2 that didn't exist in d1
_total_count += d2_keys.size()
for i in range(d2_keys.size()):
differences[d2_keys[i]] = _compare.simple(_compare.MISSING, d2[d2_keys[i]], 'key')
func summarize():
var summary = ''
if(are_equal):
summary = get_short_summary()
else:
var formatter = load('res://addons/gut/diff_formatter.gd').new()
formatter.set_max_to_display(max_differences)
summary = formatter.make_it(self)
return summary
func get_diff_type():
return _diff_type
func get_value_1():
return _value_1
func get_value_2():
return _value_2
================================================
FILE: addons/gut/diff_tool.gd.uid
================================================
uid://c5u6dl5cwn26w
================================================
FILE: addons/gut/double_templates/function_template.txt
================================================
{func_decleration}
{vararg_warning}__gutdbl.spy_on('{method_name}', {param_array})
if(__gutdbl.is_stubbed_to_call_super('{method_name}', {param_array})):
return {super_call}
else:
return await __gutdbl.handle_other_stubs('{method_name}', {param_array})
================================================
FILE: addons/gut/double_templates/init_template.txt
================================================
{func_decleration}:
super({super_params})
__gutdbl.spy_on('{method_name}', {param_array})
================================================
FILE: addons/gut/double_templates/script_template.txt
================================================
# ##############################################################################
# Gut Doubled Script
# ##############################################################################
{extends}
{constants}
{properties}
# ------------------------------------------------------------------------------
# GUT stuff
# ------------------------------------------------------------------------------
var __gutdbl_values = {
double = self,
thepath = '{path}',
subpath = '{subpath}',
stubber = {stubber_id},
spy = {spy_id},
gut = {gut_id},
from_singleton = '{singleton_name}',
is_partial = {is_partial},
doubled_methods = {doubled_methods},
}
var __gutdbl = load('res://addons/gut/double_tools.gd').new(__gutdbl_values)
# Here so other things can check for a method to know if this is a double.
func __gutdbl_check_method__():
pass
# ------------------------------------------------------------------------------
# Doubled Methods
# ------------------------------------------------------------------------------
================================================
FILE: addons/gut/double_tools.gd
================================================
var thepath = ''
var subpath = ''
var stubber = null
var spy = null
var gut = null
var from_singleton = null
var is_partial = null
var double = null
const NO_DEFAULT_VALUE = '!__gut__no__default__value__!'
func _init(values=null):
if(values != null):
double = values.double
thepath = values.thepath
subpath = values.subpath
stubber = from_id(values.stubber)
spy = from_id(values.spy)
gut = from_id(values.gut)
from_singleton = values.from_singleton
is_partial = values.is_partial
if(gut != null):
gut.get_autofree().add_free(double)
func _get_stubbed_method_to_call(method_name, called_with):
var method = stubber.get_call_this(double, method_name, called_with)
if(method != null):
method = method.bindv(called_with)
return method
return method
func from_id(inst_id):
if(inst_id == -1):
return null
else:
return instance_from_id(inst_id)
func is_stubbed_to_call_super(method_name, called_with):
if(stubber != null):
return stubber.should_call_super(double, method_name, called_with)
else:
return false
func handle_other_stubs(method_name, called_with):
if(stubber == null):
return
var method = _get_stubbed_method_to_call(method_name, called_with)
if(method != null):
return await method.call()
else:
return stubber.get_return(double, method_name, called_with)
func spy_on(method_name, called_with):
if(spy != null):
spy.add_call(double, method_name, called_with)
func default_val(method_name, p_index, default_val=NO_DEFAULT_VALUE):
if(stubber != null):
return stubber.get_default_value(double, method_name, p_index)
else:
return null
func vararg_warning():
if(gut != null):
gut.get_logger().warn(
"This method contains a vararg argument and the paramter count was not stubbed. " + \
"GUT adds extra parameters to this method which should fill most needs. " + \
"It is recommended that you stub param_count for this object's class to ensure " + \
"that there are not any parameter count mismatch errors.")
================================================
FILE: addons/gut/double_tools.gd.uid
================================================
uid://b2qjwou7pm0tr
================================================
FILE: addons/gut/doubler.gd
================================================
# ------------------------------------------------------------------------------
# A stroke of genius if I do say so. This allows for doubling a scene without
# having to write any files. By overloading the "instantiate" method we can
# make whatever we want.
# ------------------------------------------------------------------------------
class PackedSceneDouble:
extends PackedScene
var _script = null
var _scene = null
func set_script_obj(obj):
_script = obj
@warning_ignore("native_method_override")
func instantiate(edit_state=0):
var inst = _scene.instantiate(edit_state)
var export_props = []
var script_export_flag = (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_SCRIPT_VARIABLE)
if(_script != null):
if(inst.get_script() != null):
# Get all the exported props and values so we can set them again
for prop in inst.get_property_list():
var is_export = prop.usage & (script_export_flag) == script_export_flag
if(is_export):
export_props.append([prop.name, inst.get(prop.name)])
inst.set_script(_script)
for exported_value in export_props:
inst.set(exported_value[0], exported_value[1])
return inst
func load_scene(path):
_scene = load(path)
# ------------------------------------------------------------------------------
# START Doubler
# ------------------------------------------------------------------------------
var _base_script_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/script_template.txt')
var _script_collector = GutUtils.ScriptCollector.new()
# used by tests for debugging purposes.
var print_source = false
var inner_class_registry = GutUtils.InnerClassRegistry.new()
# ###############
# Properties
# ###############
var _stubber = GutUtils.Stubber.new()
func get_stubber():
return _stubber
func set_stubber(stubber):
_stubber = stubber
var _lgr = GutUtils.get_logger()
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger
_method_maker.set_logger(logger)
var _spy = null
func get_spy():
return _spy
func set_spy(spy):
_spy = spy
var _gut = null
func get_gut():
return _gut
func set_gut(gut):
_gut = gut
var _strategy = null
func get_strategy():
return _strategy
func set_strategy(strategy):
if(GutUtils.DOUBLE_STRATEGY.values().has(strategy)):
_strategy = strategy
else:
_lgr.error(str('doubler.gd: invalid double strategy ', strategy))
var _method_maker = GutUtils.MethodMaker.new()
func get_method_maker():
return _method_maker
var _ignored_methods = GutUtils.OneToMany.new()
func get_ignored_methods():
return _ignored_methods
# ###############
# Private
# ###############
func _init(strategy=GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY):
set_logger(GutUtils.get_logger())
_strategy = strategy
func _get_indented_line(indents, text):
var to_return = ''
for _i in range(indents):
to_return += "\t"
return str(to_return, text, "\n")
func _stub_to_call_super(parsed, method_name):
if(!parsed.get_method(method_name).is_eligible_for_doubling()):
return
var params = GutUtils.StubParams.new(parsed.script_path, method_name, parsed.subpath)
params.to_call_super()
_stubber.add_stub(params)
func _get_base_script_text(parsed, override_path, partial, included_methods):
var path = parsed.script_path
if(override_path != null):
path = override_path
var stubber_id = -1
if(_stubber != null):
stubber_id = _stubber.get_instance_id()
var spy_id = -1
if(_spy != null):
spy_id = _spy.get_instance_id()
var gut_id = -1
if(_gut != null):
gut_id = _gut.get_instance_id()
var extends_text = parsed.get_extends_text()
var values = {
# Top sections
"extends":extends_text,
"constants":'',#obj_info.get_constants_text(),
"properties":'',#obj_info.get_properties_text(),
# metadata values
"path":path,
"subpath":GutUtils.nvl(parsed.subpath, ''),
"stubber_id":stubber_id,
"spy_id":spy_id,
"gut_id":gut_id,
"singleton_name":'',#GutUtils.nvl(obj_info.get_singleton_name(), ''),
"is_partial":partial,
"doubled_methods":included_methods,
}
return _base_script_text.format(values)
func _is_method_eligible_for_doubling(parsed_script, parsed_method):
return !parsed_method.is_accessor() and \
parsed_method.is_eligible_for_doubling() and \
!_ignored_methods.has(parsed_script.resource, parsed_method.meta.name)
# Disable the native_method_override setting so that doubles do not generate
# errors or warnings when doubling with INCLUDE_NATIVE or when a method has
# been added because of param_count stub.
func _create_script_no_warnings(src):
var prev_native_override_value = null
var native_method_override = 'debug/gdscript/warnings/native_method_override'
prev_native_override_value = ProjectSettings.get_setting(native_method_override)
ProjectSettings.set_setting(native_method_override, 0)
var DblClass = GutUtils.create_script_from_source(src)
ProjectSettings.set_setting(native_method_override, prev_native_override_value)
return DblClass
func _create_double(parsed, strategy, override_path, partial):
var path = ""
path = parsed.script_path
var dbl_src = ""
var included_methods = []
for method in parsed.get_local_methods():
if(_is_method_eligible_for_doubling(parsed, method)):
included_methods.append(method.meta.name)
var mthd = parsed.get_local_method(method.meta.name)
if(parsed.is_native):
dbl_src += _get_func_text(method.meta, parsed.resource)
else:
dbl_src += _get_func_text(method.meta, path)
if(strategy == GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE):
for method in parsed.get_super_methods():
if(_is_method_eligible_for_doubling(parsed, method)):
included_methods.append(method.meta.name)
_stub_to_call_super(parsed, method.meta.name)
if(parsed.is_native):
dbl_src += _get_func_text(method.meta, parsed.resource)
else:
dbl_src += _get_func_text(method.meta, path)
var base_script = _get_base_script_text(parsed, override_path, partial, included_methods)
dbl_src = base_script + "\n\n" + dbl_src
if(print_source):
print(GutUtils.add_line_numbers(dbl_src))
var DblClass = _create_script_no_warnings(dbl_src)
if(_stubber != null):
_stub_method_default_values(DblClass, parsed, strategy)
return DblClass
func _stub_method_default_values(which, parsed, strategy):
for method in parsed.get_local_methods():
if(method.is_eligible_for_doubling() and !_ignored_methods.has(parsed.resource, method.meta.name)):
_stubber.stub_defaults_from_meta(parsed.script_path, method.meta)
func _double_scene_and_script(scene, strategy, partial):
var to_return = PackedSceneDouble.new()
to_return.load_scene(scene.get_path())
var script_obj = GutUtils.get_scene_script_object(scene)
if(script_obj != null):
var script_dbl = null
if(partial):
script_dbl = _partial_double(script_obj, strategy, scene.get_path())
else:
script_dbl = _double(script_obj, strategy, scene.get_path())
to_return.set_script_obj(script_dbl)
return to_return
func _get_inst_id_ref_str(inst):
var ref_str = 'null'
if(inst):
ref_str = str('instance_from_id(', inst.get_instance_id(),')')
return ref_str
func _get_func_text(method_hash, path):
var override_count = null;
if(_stubber != null):
override_count = _stubber.get_parameter_count(path, method_hash.name)
var text = _method_maker.get_function_text(method_hash, override_count) + "\n"
return text
func _parse_script(obj):
var parsed = null
if(GutUtils.is_inner_class(obj)):
if(inner_class_registry.has(obj)):
parsed = _script_collector.parse(inner_class_registry.get_base_resource(obj), obj)
else:
_lgr.error('Doubling Inner Classes requires you register them first. Call register_inner_classes passing the script that contains the inner class.')
else:
parsed = _script_collector.parse(obj)
return parsed
# Override path is used with scenes.
func _double(obj, strategy, override_path=null):
var parsed = _parse_script(obj)
if(parsed != null):
return _create_double(parsed, strategy, override_path, false)
func _partial_double(obj, strategy, override_path=null):
var parsed = _parse_script(obj)
if(parsed != null):
return _create_double(parsed, strategy, override_path, true)
# -------------------------
# Public
# -------------------------
# double a script/object
func double(obj, strategy=_strategy):
return _double(obj, strategy)
func partial_double(obj, strategy=_strategy):
return _partial_double(obj, strategy)
# double a scene
func double_scene(scene, strategy=_strategy):
return _double_scene_and_script(scene, strategy, false)
func partial_double_scene(scene, strategy=_strategy):
return _double_scene_and_script(scene, strategy, true)
func double_gdnative(which):
return _double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE)
func partial_double_gdnative(which):
return _partial_double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE)
func double_inner(parent, inner, strategy=_strategy):
var parsed = _script_collector.parse(parent, inner)
return _create_double(parsed, strategy, null, false)
func partial_double_inner(parent, inner, strategy=_strategy):
var parsed = _script_collector.parse(parent, inner)
return _create_double(parsed, strategy, null, true)
func add_ignored_method(obj, method_name):
_ignored_methods.add(obj, method_name)
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2024 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################
================================================
FILE: addons/gut/doubler.gd.uid
================================================
uid://1a6kkk2dlt2i
================================================
FILE: addons/gut/dynamic_gdscript.gd
================================================
@tool
var default_script_name_no_extension = 'gut_dynamic_script'
var default_script_resource_path = 'res://addons/gut/not_a_real_file/'
var _created_script_count = 0
# Creates a loaded script from the passed in source. This loaded script is
# returned unless there is an error. When an error occcurs the error number
# is returned instead.
func create_script_from_source(source, override_path=null):
_created_script_count += 1
var r_path = str(default_script_resource_path, default_script_name_no_extension, '_', _created_script_count)
if(override_path != null):
r_path = override_path
var DynamicScript = GDScript.new()
DynamicScript.source_code = source.dedent()
# The resource_path must be unique or Godot thinks it is trying
# to load something it has already loaded and generates an error like
# ERROR: Another resource is loaded from path 'workaround for godot
# issue #65263' (possible cyclic resource inclusion).
DynamicScript.resource_path = r_path
var result = DynamicScript.reload()
if(result != OK):
DynamicScript = result
return DynamicScript
================================================
FILE: addons/gut/dynamic_gdscript.gd.uid
================================================
uid://rh3x2vl3tmb2
================================================
FILE: addons/gut/fonts/AnonymousPro-Bold.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://c8axnpxc0nrk4"
path="res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60afbe608aa49.fontdata"
[deps]
source_file="res://addons/gut/fonts/AnonymousPro-Bold.ttf"
dest_files=["res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60afbe608aa49.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://msst1l2s2s"
path="res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd32c4f0754d8c83d.fontdata"
[deps]
source_file="res://addons/gut/fonts/AnonymousPro-BoldItalic.ttf"
dest_files=["res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd32c4f0754d8c83d.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/AnonymousPro-Italic.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://hf5rdg67jcwc"
path="res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d570de2a42c1.fontdata"
[deps]
source_file="res://addons/gut/fonts/AnonymousPro-Italic.ttf"
dest_files=["res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d570de2a42c1.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/AnonymousPro-Regular.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://c6c7gnx36opr0"
path="res://.godot/imported/AnonymousPro-Regular.ttf-856c843fd6f89964d2ca8d8ff1724f13.fontdata"
[deps]
source_file="res://addons/gut/fonts/AnonymousPro-Regular.ttf"
dest_files=["res://.godot/imported/AnonymousPro-Regular.ttf-856c843fd6f89964d2ca8d8ff1724f13.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/CourierPrime-Bold.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://bhjgpy1dovmyq"
path="res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e7756f4fa235.fontdata"
[deps]
source_file="res://addons/gut/fonts/CourierPrime-Bold.ttf"
dest_files=["res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e7756f4fa235.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/CourierPrime-BoldItalic.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://n6mxiov5sbgc"
path="res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8f96313da4ad7019.fontdata"
[deps]
source_file="res://addons/gut/fonts/CourierPrime-BoldItalic.ttf"
dest_files=["res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8f96313da4ad7019.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/CourierPrime-Italic.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://mcht266g817e"
path="res://.godot/imported/CourierPrime-Italic.ttf-baa9156a73770735a0f72fb20b907112.fontdata"
[deps]
source_file="res://addons/gut/fonts/CourierPrime-Italic.ttf"
dest_files=["res://.godot/imported/CourierPrime-Italic.ttf-baa9156a73770735a0f72fb20b907112.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/CourierPrime-Regular.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://bnh0lslf4yh87"
path="res://.godot/imported/CourierPrime-Regular.ttf-3babe7e4a7a588dfc9a84c14b4f1fe23.fontdata"
[deps]
source_file="res://addons/gut/fonts/CourierPrime-Regular.ttf"
dest_files=["res://.godot/imported/CourierPrime-Regular.ttf-3babe7e4a7a588dfc9a84c14b4f1fe23.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/LobsterTwo-Bold.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://cmiuntu71oyl3"
path="res://.godot/imported/LobsterTwo-Bold.ttf-7c7f734103b58a32491a4788186f3dcb.fontdata"
[deps]
source_file="res://addons/gut/fonts/LobsterTwo-Bold.ttf"
dest_files=["res://.godot/imported/LobsterTwo-Bold.ttf-7c7f734103b58a32491a4788186f3dcb.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://bll38n2ct6qme"
path="res://.godot/imported/LobsterTwo-BoldItalic.ttf-227406a33e84448e6aa974176016de19.fontdata"
[deps]
source_file="res://addons/gut/fonts/LobsterTwo-BoldItalic.ttf"
dest_files=["res://.godot/imported/LobsterTwo-BoldItalic.ttf-227406a33e84448e6aa974176016de19.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/LobsterTwo-Italic.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://dis65h8wxc3f2"
path="res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c4ee75159e.fontdata"
[deps]
source_file="res://addons/gut/fonts/LobsterTwo-Italic.ttf"
dest_files=["res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c4ee75159e.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/LobsterTwo-Regular.ttf.import
================================================
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://5e8msj0ih2pv"
path="res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd875d0fe04b.fontdata"
[deps]
source_file="res://addons/gut/fonts/LobsterTwo-Regular.ttf"
dest_files=["res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd875d0fe04b.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}
================================================
FILE: addons/gut/fonts/OFL.txt
================================================
Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com),
with Reserved Font Name Anonymous Pro.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
================================================
FILE: addons/gut/gui/BottomPanelShortcuts.gd
================================================
@tool
extends Window
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
var default_path = GutEditorGlobals.editor_shortcuts_path
@onready var _ctrls = {
run_all = $Layout/CRunAll/ShortcutButton,
run_current_script = $Layout/CRunCurrentScript/ShortcutButton,
run_current_inner = $Layout/CRunCurrentInner/ShortcutButton,
run_current_test = $Layout/CRunCurrentTest/ShortcutButton,
panel_button = $Layout/CPanelButton/ShortcutButton,
}
var _user_prefs = GutEditorGlobals.user_prefs
func _ready():
for key in _ctrls:
var sc_button = _ctrls[key]
sc_button.connect('start_edit', _on_edit_start.bind(sc_button))
sc_button.connect('end_edit', _on_edit_end)
# show dialog when running scene from editor.
if(get_parent() == get_tree().root):
popup_centered()
func _cancel_all():
_ctrls.run_all.cancel()
_ctrls.run_current_script.cancel()
_ctrls.run_current_inner.cancel()
_ctrls.run_current_test.cancel()
_ctrls.panel_button.cancel()
# ------------
# Events
# ------------
func _on_Hide_pressed():
hide()
func _on_edit_start(which):
for key in _ctrls:
var sc_button = _ctrls[key]
if(sc_button != which):
sc_button.disable_set(true)
sc_button.disable_clear(true)
func _on_edit_end():
for key in _ctrls:
var sc_button = _ctrls[key]
sc_button.disable_set(false)
sc_button.disable_clear(false)
func _on_popup_hide():
_cancel_all()
# ------------
# Public
# ------------
func get_run_all():
return _ctrls.run_all.get_shortcut()
func get_run_current_script():
return _ctrls.run_current_script.get_shortcut()
func get_run_current_inner():
return _ctrls.run_current_inner.get_shortcut()
func get_run_current_test():
return _ctrls.run_current_test.get_shortcut()
func get_panel_button():
return _ctrls.panel_button.get_shortcut()
func _set_pref_value(pref, button):
pref.value = {shortcut = button.get_shortcut().events}
func save_shortcuts():
save_shortcuts_to_file(default_path)
func save_shortcuts_to_editor_settings():
_set_pref_value(_user_prefs.shortcut_run_all, _ctrls.run_all)
_set_pref_value(_user_prefs.shortcut_run_current_script, _ctrls.run_current_script)
_set_pref_value(_user_prefs.shortcut_run_current_inner, _ctrls.run_current_inner)
_set_pref_value(_user_prefs.shortcut_run_current_test, _ctrls.run_current_test)
_set_pref_value(_user_prefs.shortcut_panel_button, _ctrls.panel_button)
_user_prefs.save_it()
func save_shortcuts_to_file(path):
var f = ConfigFile.new()
f.set_value('main', 'run_all', _ctrls.run_all.get_shortcut())
f.set_value('main', 'run_current_script', _ctrls.run_current_script.get_shortcut())
f.set_value('main', 'run_current_inner', _ctrls.run_current_inner.get_shortcut())
f.set_value('main', 'run_current_test', _ctrls.run_current_test.get_shortcut())
f.set_value('main', 'panel_button', _ctrls.panel_button.get_shortcut())
f.save(path)
func _load_shortcut_from_pref(user_pref):
var to_return = Shortcut.new()
# value with be _user_prefs.EMPTY which is a string when the value
# has not been set.
if(typeof(user_pref.value) == TYPE_DICTIONARY):
to_return.events.append(user_pref.value.shortcut[0])
# to_return = user_pref.value
return to_return
func load_shortcuts():
load_shortcuts_from_file(default_path)
func load_shortcuts_from_editor_settings():
var empty = Shortcut.new()
_ctrls.run_all.set_shortcut(_load_shortcut_from_pref(_user_prefs.shortcut_run_all))
_ctrls.run_current_script.set_shortcut(_load_shortcut_from_pref(_user_prefs.shortcut_run_current_script))
_ctrls.run_current_inner.set_shortcut(_load_shortcut_from_pref(_user_prefs.shortcut_run_current_inner))
_ctrls.run_current_test.set_shortcut(_load_shortcut_from_pref(_user_prefs.shortcut_run_current_test))
_ctrls.panel_button.set_shortcut(_load_shortcut_from_pref(_user_prefs.shortcut_panel_button))
func load_shortcuts_from_file(path):
var f = ConfigFile.new()
var empty = Shortcut.new()
f.load(path)
_ctrls.run_all.set_shortcut(f.get_value('main', 'run_all', empty))
_ctrls.run_current_script.set_shortcut(f.get_value('main', 'run_current_script', empty))
_ctrls.run_current_inner.set_shortcut(f.get_value('main', 'run_current_inner', empty))
_ctrls.run_current_test.set_shortcut(f.get_value('main', 'run_current_test', empty))
_ctrls.panel_button.set_shortcut(f.get_value('main', 'panel_button', empty))
================================================
FILE: addons/gut/gui/BottomPanelShortcuts.gd.uid
================================================
uid://ck68kf4ewmrrf
================================================
FILE: addons/gut/gui/BottomPanelShortcuts.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://bsk32dh41b4gs"]
[ext_resource type="PackedScene" uid="uid://sfb1fw8j6ufu" path="res://addons/gut/gui/ShortcutButton.tscn" id="1"]
[ext_resource type="Script" uid="uid://ck68kf4ewmrrf" path="res://addons/gut/gui/BottomPanelShortcuts.gd" id="2"]
[node name="BottomPanelShortcuts" type="Popup"]
title = "Shortcuts"
size = Vector2i(500, 350)
visible = true
exclusive = true
unresizable = false
borderless = false
script = ExtResource("2")
[node name="Layout" type="VBoxContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 5.0
offset_right = -5.0
offset_bottom = 2.0
[node name="TopPad" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="Label2" type="Label" parent="Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
text = "Always Active"
[node name="ColorRect" type="ColorRect" parent="Layout/Label2"]
show_behind_parent = true
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.196078)
[node name="CPanelButton" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CPanelButton"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Show/Hide GUT Panel"
[node name="ShortcutButton" parent="Layout/CPanelButton" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="GutPanelPad" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="Label" type="Label" parent="Layout"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
text = "Only Active When GUT Panel Shown"
[node name="ColorRect2" type="ColorRect" parent="Layout/Label"]
show_behind_parent = true
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0, 0, 0, 0.196078)
[node name="TopPad2" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="CRunAll" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunAll"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run All"
[node name="ShortcutButton" parent="Layout/CRunAll" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CRunCurrentScript" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunCurrentScript"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Script"
[node name="ShortcutButton" parent="Layout/CRunCurrentScript" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CRunCurrentInner" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunCurrentInner"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Inner Class"
[node name="ShortcutButton" parent="Layout/CRunCurrentInner" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CRunCurrentTest" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunCurrentTest"]
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Test"
[node name="ShortcutButton" parent="Layout/CRunCurrentTest" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CenterContainer2" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ShiftDisclaimer" type="Label" parent="Layout"]
layout_mode = 2
text = "\"Shift\" cannot be the only modifier for a shortcut."
[node name="HBoxContainer" type="HBoxContainer" parent="Layout"]
layout_mode = 2
[node name="CenterContainer" type="CenterContainer" parent="Layout/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Hide" type="Button" parent="Layout/HBoxContainer"]
custom_minimum_size = Vector2(60, 30)
layout_mode = 2
text = "Close"
[node name="BottomPad" type="CenterContainer" parent="Layout"]
custom_minimum_size = Vector2(0, 10)
layout_mode = 2
size_flags_horizontal = 3
[connection signal="popup_hide" from="." to="." method="_on_popup_hide"]
[connection signal="pressed" from="Layout/HBoxContainer/Hide" to="." method="_on_Hide_pressed"]
================================================
FILE: addons/gut/gui/GutBottomPanel.gd
================================================
@tool
extends Control
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
var TestScript = load('res://addons/gut/test.gd')
var GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd')
var ScriptTextEditors = load('res://addons/gut/gui/script_text_editor_controls.gd')
var _interface = null;
var _is_running = false;
var _gut_config = load('res://addons/gut/gut_config.gd').new()
var _gut_config_gui = null
var _gut_plugin = null
var _light_color = Color(0, 0, 0, .5)
var _panel_button = null
var _last_selected_path = null
var _user_prefs = null
@onready var _ctrls = {
output = $layout/RSplit/CResults/TabBar/OutputText.get_rich_text_edit(),
output_ctrl = $layout/RSplit/CResults/TabBar/OutputText,
run_button = $layout/ControlBar/RunAll,
shortcuts_button = $layout/ControlBar/Shortcuts,
settings_button = $layout/ControlBar/Settings,
run_results_button = $layout/ControlBar/RunResultsBtn,
output_button = $layout/ControlBar/OutputBtn,
settings = $layout/RSplit/sc/Settings,
shortcut_dialog = $BottomPanelShortcuts,
light = $layout/RSplit/CResults/ControlBar/Light3D,
results = {
bar = $layout/RSplit/CResults/ControlBar,
passing = $layout/RSplit/CResults/ControlBar/Passing/value,
failing = $layout/RSplit/CResults/ControlBar/Failing/value,
pending = $layout/RSplit/CResults/ControlBar/Pending/value,
errors = $layout/RSplit/CResults/ControlBar/Errors/value,
warnings = $layout/RSplit/CResults/ControlBar/Warnings/value,
orphans = $layout/RSplit/CResults/ControlBar/Orphans/value
},
run_at_cursor = $layout/ControlBar/RunAtCursor,
run_results = $layout/RSplit/CResults/TabBar/RunResults
}
func _init():
pass
func _ready():
GutEditorGlobals.create_temp_directory()
_user_prefs = GutEditorGlobals.user_prefs
_gut_config_gui = GutConfigGui.new(_ctrls.settings)
_ctrls.results.bar.connect('draw', _on_results_bar_draw.bind(_ctrls.results.bar))
hide_settings(!_ctrls.settings_button.button_pressed)
_gut_config.load_options(GutEditorGlobals.editor_run_gut_config_path)
_gut_config_gui.set_options(_gut_config.options)
_apply_options_to_controls()
_ctrls.shortcuts_button.icon = get_theme_icon('Shortcut', 'EditorIcons')
_ctrls.settings_button.icon = get_theme_icon('Tools', 'EditorIcons')
_ctrls.run_results_button.icon = get_theme_icon('AnimationTrackGroup', 'EditorIcons') # Tree
_ctrls.output_button.icon = get_theme_icon('Font', 'EditorIcons')
_ctrls.run_results.set_output_control(_ctrls.output_ctrl)
var check_import = load('res://addons/gut/images/red.png')
if(check_import == null):
_ctrls.run_results.add_centered_text("GUT got some new images that are not imported yet. Please restart Godot.")
print('GUT got some new images that are not imported yet. Please restart Godot.')
else:
_ctrls.run_results.add_centered_text("Let's run some tests!")
func _apply_options_to_controls():
hide_settings(_user_prefs.hide_settings.value)
hide_result_tree(_user_prefs.hide_result_tree.value)
hide_output_text(_user_prefs.hide_output_text.value)
_ctrls.run_results.set_show_orphans(!_gut_config.options.hide_orphans)
func _process(delta):
if(_is_running):
if(!_interface.is_playing_scene()):
_is_running = false
_ctrls.output_ctrl.add_text("\ndone")
load_result_output()
_gut_plugin.make_bottom_panel_item_visible(self)
# ---------------
# Private
# ---------------
func load_shortcuts():
_ctrls.shortcut_dialog.load_shortcuts()
_apply_shortcuts()
func _is_test_script(script):
var from = script.get_base_script()
while(from and from.resource_path != 'res://addons/gut/test.gd'):
from = from.get_base_script()
return from != null
func _show_errors(errs):
_ctrls.output_ctrl.clear()
var text = "Cannot run tests, you have a configuration error:\n"
for e in errs:
text += str('* ', e, "\n")
text += "Check your settings ----->"
_ctrls.output_ctrl.add_text(text)
hide_output_text(false)
hide_settings(false)
func _save_config():
_user_prefs.hide_settings.value = !_ctrls.settings_button.button_pressed
_user_prefs.hide_result_tree.value = !_ctrls.run_results_button.button_pressed
_user_prefs.hide_output_text.value = !_ctrls.output_button.button_pressed
_user_prefs.save_it()
_gut_config.options = _gut_config_gui.get_options(_gut_config.options)
var w_result = _gut_config.write_options(GutEditorGlobals.editor_run_gut_config_path)
if(w_result != OK):
push_error(str('Could not write options to ', GutEditorGlobals.editor_run_gut_config_path, ': ', w_result))
else:
_gut_config_gui.mark_saved()
func _run_tests():
GutEditorGlobals.create_temp_directory()
var issues = _gut_config_gui.get_config_issues()
if(issues.size() > 0):
_show_errors(issues)
return
write_file(GutEditorGlobals.editor_run_bbcode_results_path, 'Run in progress')
_save_config()
_apply_options_to_controls()
_ctrls.output_ctrl.clear()
_ctrls.run_results.clear()
_ctrls.run_results.add_centered_text('Running...')
_interface.play_custom_scene('res://addons/gut/gui/GutRunner.tscn')
_is_running = true
_ctrls.output_ctrl.add_text('Running...')
func _apply_shortcuts():
_ctrls.run_button.shortcut = _ctrls.shortcut_dialog.get_run_all()
_ctrls.run_at_cursor.get_script_button().shortcut = \
_ctrls.shortcut_dialog.get_run_current_script()
_ctrls.run_at_cursor.get_inner_button().shortcut = \
_ctrls.shortcut_dialog.get_run_current_inner()
_ctrls.run_at_cursor.get_test_button().shortcut = \
_ctrls.shortcut_dialog.get_run_current_test()
_panel_button.shortcut = _ctrls.shortcut_dialog.get_panel_button()
func _run_all():
_gut_config.options.selected = null
_gut_config.options.inner_class = null
_gut_config.options.unit_test_name = null
_run_tests()
# ---------------
# Events
# ---------------
func _on_results_bar_draw(bar):
bar.draw_rect(Rect2(Vector2(0, 0), bar.size), Color(0, 0, 0, .2))
func _on_Light_draw():
var l = _ctrls.light
l.draw_circle(Vector2(l.size.x / 2, l.size.y / 2), l.size.x / 2, _light_color)
func _on_editor_script_changed(script):
if(script):
set_current_script(script)
func _on_RunAll_pressed():
_run_all()
func _on_Shortcuts_pressed():
_ctrls.shortcut_dialog.popup_centered()
func _on_bottom_panel_shortcuts_visibility_changed():
_apply_shortcuts()
_ctrls.shortcut_dialog.save_shortcuts()
func _on_RunAtCursor_run_tests(what):
_gut_config.options.selected = what.script
_gut_config.options.inner_class = what.inner_class
_gut_config.options.unit_test_name = what.test_method
_run_tests()
func _on_Settings_pressed():
hide_settings(!_ctrls.settings_button.button_pressed)
_save_config()
func _on_OutputBtn_pressed():
hide_output_text(!_ctrls.output_button.button_pressed)
_save_config()
func _on_RunResultsBtn_pressed():
hide_result_tree(! _ctrls.run_results_button.button_pressed)
_save_config()
# Currently not used, but will be when I figure out how to put
# colors into the text results
func _on_UseColors_pressed():
pass
# ---------------
# Public
# ---------------
func hide_result_tree(should):
_ctrls.run_results.visible = !should
_ctrls.run_results_button.button_pressed = !should
func hide_settings(should):
var s_scroll = _ctrls.settings.get_parent()
s_scroll.visible = !should
# collapse only collapses the first control, so we move
# settings around to be the collapsed one
if(should):
s_scroll.get_parent().move_child(s_scroll, 0)
else:
s_scroll.get_parent().move_child(s_scroll, 1)
$layout/RSplit.collapsed = should
_ctrls.settings_button.button_pressed = !should
func hide_output_text(should):
$layout/RSplit/CResults/TabBar/OutputText.visible = !should
_ctrls.output_button.button_pressed = !should
func load_result_output():
_ctrls.output_ctrl.load_file(GutEditorGlobals.editor_run_bbcode_results_path)
var summary = get_file_as_text(GutEditorGlobals.editor_run_json_results_path)
var test_json_conv = JSON.new()
if (test_json_conv.parse(summary) != OK):
return
var results = test_json_conv.get_data()
_ctrls.run_results.load_json_results(results)
var summary_json = results['test_scripts']['props']
_ctrls.results.passing.text = str(summary_json.passing)
_ctrls.results.passing.get_parent().visible = true
_ctrls.results.failing.text = str(summary_json.failures)
_ctrls.results.failing.get_parent().visible = true
_ctrls.results.pending.text = str(summary_json.pending)
_ctrls.results.pending.get_parent().visible = _ctrls.results.pending.text != '0'
_ctrls.results.errors.text = str(summary_json.errors)
_ctrls.results.errors.get_parent().visible = _ctrls.results.errors.text != '0'
_ctrls.results.warnings.text = str(summary_json.warnings)
_ctrls.results.warnings.get_parent().visible = _ctrls.results.warnings.text != '0'
_ctrls.results.orphans.text = str(summary_json.orphans)
_ctrls.results.orphans.get_parent().visible = _ctrls.results.orphans.text != '0' and !_gut_config.options.hide_orphans
if(summary_json.tests == 0):
_light_color = Color(1, 0, 0, .75)
elif(summary_json.failures != 0):
_light_color = Color(1, 0, 0, .75)
elif(summary_json.pending != 0):
_light_color = Color(1, 1, 0, .75)
else:
_light_color = Color(0, 1, 0, .75)
_ctrls.light.visible = true
_ctrls.light.queue_redraw()
func set_current_script(script):
if(script):
if(_is_test_script(script)):
var file = script.resource_path.get_file()
_last_selected_path = script.resource_path.get_file()
_ctrls.run_at_cursor.activate_for_script(script.resource_path)
func set_interface(value):
_interface = value
_interface.get_script_editor().connect("editor_script_changed",Callable(self,'_on_editor_script_changed'))
var ste = ScriptTextEditors.new(_interface.get_script_editor())
_ctrls.run_results.set_interface(_interface)
_ctrls.run_results.set_script_text_editors(ste)
_ctrls.run_at_cursor.set_script_text_editors(ste)
set_current_script(_interface.get_script_editor().get_current_script())
func set_plugin(value):
_gut_plugin = value
func set_panel_button(value):
_panel_button = value
# ------------------------------------------------------------------------------
# Write a file.
# ------------------------------------------------------------------------------
func write_file(path, content):
var f = FileAccess.open(path, FileAccess.WRITE)
if(f != null):
f.store_string(content)
f = null;
return FileAccess.get_open_error()
# ------------------------------------------------------------------------------
# Returns the text of a file or an empty string if the file could not be opened.
# ------------------------------------------------------------------------------
func get_file_as_text(path):
var to_return = ''
var f = FileAccess.open(path, FileAccess.READ)
if(f != null):
to_return = f.get_as_text()
f = null
return to_return
# ------------------------------------------------------------------------------
# return if_null if value is null otherwise return value
# ------------------------------------------------------------------------------
func nvl(value, if_null):
if(value == null):
return if_null
else:
return value
================================================
FILE: addons/gut/gui/GutBottomPanel.gd.uid
================================================
uid://bhjmhcm3b7dv2
================================================
FILE: addons/gut/gui/GutBottomPanel.tscn
================================================
[gd_scene load_steps=10 format=3 uid="uid://b3bostcslstem"]
[ext_resource type="Script" uid="uid://bhjmhcm3b7dv2" path="res://addons/gut/gui/GutBottomPanel.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://bsk32dh41b4gs" path="res://addons/gut/gui/BottomPanelShortcuts.tscn" id="2"]
[ext_resource type="PackedScene" uid="uid://0yunjxtaa8iw" path="res://addons/gut/gui/RunAtCursor.tscn" id="3"]
[ext_resource type="Texture2D" uid="uid://cr6tvdv0ve6cv" path="res://addons/gut/gui/play.png" id="4"]
[ext_resource type="PackedScene" uid="uid://4gyyn12um08h" path="res://addons/gut/gui/RunResults.tscn" id="5"]
[ext_resource type="PackedScene" uid="uid://bqmo4dj64c7yl" path="res://addons/gut/gui/OutputText.tscn" id="6"]
[sub_resource type="Shortcut" id="9"]
[sub_resource type="Image" id="Image_p7oqn"]
data = {
"data": PackedByteArray(255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 255, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 92, 92, 127, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 231, 255, 90, 90, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 90, 90, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 42, 255, 90, 90, 0, 255, 94, 94, 0, 255, 91, 91, 42, 255, 93, 93, 233, 255, 92, 92, 232, 255, 93, 93, 41, 255, 90, 90, 0, 255, 94, 94, 0, 255, 91, 91, 42, 255, 93, 93, 233, 255, 92, 92, 232, 255, 92, 92, 0, 255, 92, 92, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 45, 255, 93, 93, 44, 255, 91, 91, 0, 255, 91, 91, 42, 255, 91, 91, 42, 255, 93, 93, 0, 255, 91, 91, 45, 255, 93, 93, 44, 255, 91, 91, 0, 255, 91, 91, 42, 255, 91, 91, 42, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 45, 255, 92, 92, 235, 255, 92, 92, 234, 255, 89, 89, 43, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 45, 255, 92, 92, 235, 255, 92, 92, 234, 255, 89, 89, 43, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 91, 91, 0, 255, 92, 92, 0, 255, 92, 92, 0, 255, 92, 92, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 91, 91, 59, 255, 92, 92, 61, 255, 92, 92, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 91, 91, 59, 255, 92, 92, 61, 255, 92, 92, 0, 255, 92, 92, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0, 255, 93, 93, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_srqj5"]
image = SubResource("Image_p7oqn")
[node name="GutBottomPanel" type="Control"]
custom_minimum_size = Vector2(250, 250)
layout_mode = 3
anchor_left = -0.0025866
anchor_top = -0.00176575
anchor_right = 0.997413
anchor_bottom = 0.998234
offset_left = 2.64868
offset_top = 1.05945
offset_right = 2.64862
offset_bottom = 1.05945
script = ExtResource("1")
[node name="layout" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="ControlBar" type="HBoxContainer" parent="layout"]
layout_mode = 2
[node name="RunAll" type="Button" parent="layout/ControlBar"]
layout_mode = 2
size_flags_vertical = 11
shortcut = SubResource("9")
text = "Run All"
icon = ExtResource("4")
[node name="Label" type="Label" parent="layout/ControlBar"]
layout_mode = 2
mouse_filter = 1
text = "Current: "
[node name="RunAtCursor" parent="layout/ControlBar" instance=ExtResource("3")]
layout_mode = 2
[node name="CenterContainer2" type="CenterContainer" parent="layout/ControlBar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Sep1" type="ColorRect" parent="layout/ControlBar"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="RunResultsBtn" type="Button" parent="layout/ControlBar"]
layout_mode = 2
toggle_mode = true
button_pressed = true
icon = SubResource("ImageTexture_srqj5")
[node name="OutputBtn" type="Button" parent="layout/ControlBar"]
layout_mode = 2
toggle_mode = true
button_pressed = true
icon = SubResource("ImageTexture_srqj5")
[node name="Settings" type="Button" parent="layout/ControlBar"]
layout_mode = 2
toggle_mode = true
button_pressed = true
icon = SubResource("ImageTexture_srqj5")
[node name="Sep2" type="ColorRect" parent="layout/ControlBar"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="Shortcuts" type="Button" parent="layout/ControlBar"]
layout_mode = 2
size_flags_vertical = 11
icon = SubResource("ImageTexture_srqj5")
[node name="RSplit" type="HSplitContainer" parent="layout"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="CResults" type="VBoxContainer" parent="layout/RSplit"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ControlBar" type="HBoxContainer" parent="layout/RSplit/CResults"]
layout_mode = 2
[node name="Sep2" type="ColorRect" parent="layout/RSplit/CResults/ControlBar"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="Light3D" type="Control" parent="layout/RSplit/CResults/ControlBar"]
custom_minimum_size = Vector2(30, 30)
layout_mode = 2
[node name="Passing" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Passing"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Passing"]
layout_mode = 2
text = "Passing"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Passing"]
layout_mode = 2
text = "---"
[node name="Failing" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Failing"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Failing"]
layout_mode = 2
text = "Failing"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Failing"]
layout_mode = 2
text = "---"
[node name="Pending" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Pending"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Pending"]
layout_mode = 2
text = "Pending"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Pending"]
layout_mode = 2
text = "---"
[node name="Orphans" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Orphans"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Orphans"]
layout_mode = 2
text = "Orphans"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Orphans"]
layout_mode = 2
text = "---"
[node name="Errors" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Errors"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Errors"]
layout_mode = 2
text = "Errors"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Errors"]
layout_mode = 2
text = "---"
[node name="Warnings" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Warnings"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Warnings"]
layout_mode = 2
text = "Warnings"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Warnings"]
layout_mode = 2
text = "---"
[node name="CenterContainer" type="CenterContainer" parent="layout/RSplit/CResults/ControlBar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TabBar" type="HSplitContainer" parent="layout/RSplit/CResults"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="RunResults" parent="layout/RSplit/CResults/TabBar" instance=ExtResource("5")]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="OutputText" parent="layout/RSplit/CResults/TabBar" instance=ExtResource("6")]
layout_mode = 2
[node name="sc" type="ScrollContainer" parent="layout/RSplit"]
custom_minimum_size = Vector2(500, 2.08165e-12)
layout_mode = 2
size_flags_vertical = 3
[node name="Settings" type="VBoxContainer" parent="layout/RSplit/sc"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="BottomPanelShortcuts" parent="." instance=ExtResource("2")]
visible = false
[connection signal="pressed" from="layout/ControlBar/RunAll" to="." method="_on_RunAll_pressed"]
[connection signal="run_tests" from="layout/ControlBar/RunAtCursor" to="." method="_on_RunAtCursor_run_tests"]
[connection signal="pressed" from="layout/ControlBar/RunResultsBtn" to="." method="_on_RunResultsBtn_pressed"]
[connection signal="pressed" from="layout/ControlBar/OutputBtn" to="." method="_on_OutputBtn_pressed"]
[connection signal="pressed" from="layout/ControlBar/Settings" to="." method="_on_Settings_pressed"]
[connection signal="pressed" from="layout/ControlBar/Shortcuts" to="." method="_on_Shortcuts_pressed"]
[connection signal="draw" from="layout/RSplit/CResults/ControlBar/Light3D" to="." method="_on_Light_draw"]
[connection signal="visibility_changed" from="BottomPanelShortcuts" to="." method="_on_bottom_panel_shortcuts_visibility_changed"]
================================================
FILE: addons/gut/gui/GutControl.gd
================================================
@tool
extends Control
const RUNNER_JSON_PATH = 'res://.gut_editor_config.json'
var GutConfig = load('res://addons/gut/gut_config.gd')
var GutRunnerScene = load('res://addons/gut/gui/GutRunner.tscn')
var GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd')
var _config = GutConfig.new()
var _config_gui = null
var _gut_runner = GutRunnerScene.instantiate()
var _has_connected = false
var _tree_root : TreeItem = null
var _script_icon = load('res://addons/gut/images/Script.svg')
var _folder_icon = load('res://addons/gut/images/Folder.svg')
var _tree_scripts = {}
var _tree_directories = {}
const TREE_SCRIPT = 'Script'
const TREE_DIR = 'Directory'
@onready var _ctrls = {
run_tests_button = $VBox/Buttons/RunTests,
run_selected = $VBox/Buttons/RunSelected,
test_tree = $VBox/Tabs/Tests,
settings_vbox = $VBox/Tabs/SettingsScroll/Settings,
tabs = $VBox/Tabs,
bg = $Bg
}
@export var bg_color : Color = Color(.36, .36, .36) :
get: return bg_color
set(val):
bg_color = val
if(is_inside_tree()):
$Bg.color = bg_color
func _ready():
if Engine.is_editor_hint():
return
$Bg.color = bg_color
_ctrls.tabs.set_tab_title(0, 'Tests')
_ctrls.tabs.set_tab_title(1, 'Settings')
_config_gui = GutConfigGui.new(_ctrls.settings_vbox)
_ctrls.test_tree.hide_root = true
# Stop tests from kicking off when the runner is "ready" and prevents it
# from writing results file that is used by the panel.
_gut_runner.ran_from_editor = false
add_child(_gut_runner)
# TODO This might not need to be called deferred after changing GutUtils to
# an all static class.
call_deferred('_post_ready')
func _draw():
if Engine.is_editor_hint():
return
var gut = _gut_runner.get_gut()
if(!gut.is_running()):
var r = Rect2(Vector2(0, 0), get_rect().size)
draw_rect(r, Color.BLACK, false, 2)
func _post_ready():
var gut = _gut_runner.get_gut()
gut.start_run.connect(_on_gut_run_started)
gut.end_run.connect(_on_gut_run_ended)
_refresh_tree_and_settings()
func _set_meta_for_script_tree_item(item, script, test=null):
var meta = {
type = TREE_SCRIPT,
script = script.path,
inner_class = script.inner_class_name,
test = ''
}
if(test != null):
meta.test = test.name
item.set_metadata(0, meta)
func _set_meta_for_directory_tree_item(item, path, temp_item):
var meta = {
type = TREE_DIR,
path = path,
temp_item = temp_item
}
item.set_metadata(0, meta)
func _get_script_tree_item(script, parent_item):
if(!_tree_scripts.has(script.path)):
var item = _ctrls.test_tree.create_item(parent_item)
item.set_text(0, script.path.get_file())
item.set_icon(0, _script_icon)
_tree_scripts[script.path] = item
_set_meta_for_script_tree_item(item, script)
return _tree_scripts[script.path]
func _get_directory_tree_item(path):
var parent = _tree_root
if(!_tree_directories.has(path)):
var item : TreeItem = null
if(parent != _tree_root):
item = parent.create_child(0)
else:
item = parent.create_child()
_tree_directories[path] = item
item.collapsed = false
item.set_text(0, path)
item.set_icon(0, _folder_icon)
item.set_icon_modulate(0, Color.ROYAL_BLUE)
# temp_item is used in calls with move_before since you must use
# move_before or move_after to reparent tree items. This ensures that
# there is an item on all directories. These are deleted later.
var temp_item = item.create_child()
temp_item.set_text(0, '')
_set_meta_for_directory_tree_item(item, path, temp_item)
return _tree_directories[path]
func _find_dir_item_to_move_before(path):
var max_matching_len = 0
var best_parent = null
# Go through all the directory items finding the one that has the longest
# path that contains our path.
for key in _tree_directories.keys():
if(path != key and path.begins_with(key) and key.length() > max_matching_len):
max_matching_len = key.length()
best_parent = _tree_directories[key]
var to_return = null
if(best_parent != null):
to_return = best_parent.get_metadata(0).temp_item
return to_return
func _reorder_dir_items():
var the_keys = _tree_directories.keys()
the_keys.sort()
for key in _tree_directories.keys():
var to_move = _tree_directories[key]
to_move.collapsed = false
var move_before = _find_dir_item_to_move_before(key)
if(move_before != null):
to_move.move_before(move_before)
var new_text = key.substr(move_before.get_parent().get_metadata(0).path.length())
to_move.set_text(0, new_text)
func _remove_dir_temp_items():
for key in _tree_directories.keys():
var item = _tree_directories[key].get_metadata(0).temp_item
_tree_directories[key].remove_child(item)
func _add_dir_and_script_tree_items():
var tree : Tree = _ctrls.test_tree
tree.clear()
_tree_root = _ctrls.test_tree.create_item()
var scripts = _gut_runner.get_gut().get_test_collector().scripts
for script in scripts:
var dir_item = _get_directory_tree_item(script.path.get_base_dir())
var item = _get_script_tree_item(script, dir_item)
if(script.inner_class_name != ''):
var inner_item = tree.create_item(item)
inner_item.set_text(0, script.inner_class_name)
_set_meta_for_script_tree_item(inner_item, script)
item = inner_item
for test in script.tests:
var test_item = tree.create_item(item)
test_item.set_text(0, test.name)
_set_meta_for_script_tree_item(test_item, script, test)
func _populate_tree():
_add_dir_and_script_tree_items()
_tree_root.set_collapsed_recursive(true)
_tree_root.set_collapsed(false)
_reorder_dir_items()
_remove_dir_temp_items()
func _refresh_tree_and_settings():
_config.apply_options(_gut_runner.get_gut())
_gut_runner.set_gut_config(_config)
_populate_tree()
# ---------------------------
# Events
# ---------------------------
func _on_gut_run_started():
_ctrls.run_tests_button.disabled = true
_ctrls.run_selected.visible = false
_ctrls.tabs.visible = false
_ctrls.bg.visible = false
_ctrls.run_tests_button.text = 'Running'
queue_redraw()
func _on_gut_run_ended():
_ctrls.run_tests_button.disabled = false
_ctrls.run_selected.visible = true
_ctrls.tabs.visible = true
_ctrls.bg.visible = true
_ctrls.run_tests_button.text = 'Run All'
queue_redraw()
func _on_run_tests_pressed():
run_all()
func _on_run_selected_pressed():
run_selected()
func _on_tests_item_activated():
run_selected()
# ---------------------------
# Public
# ---------------------------
func get_gut():
return _gut_runner.get_gut()
func get_config():
return _config
func run_all():
_config.options.selected = ''
_config.options.inner_class_name = ''
_config.options.unit_test_name = ''
run_tests()
func run_tests(options = null):
if(options == null):
_config.options = _config_gui.get_options(_config.options)
else:
_config.options = options
_gut_runner.get_gut().get_test_collector().clear()
_gut_runner.set_gut_config(_config)
_gut_runner.run_tests()
func run_selected():
var sel_item = _ctrls.test_tree.get_selected()
if(sel_item == null):
return
var options = _config_gui.get_options(_config.options)
var meta = sel_item.get_metadata(0)
if(meta.type == TREE_SCRIPT):
options.selected = meta.script.get_file()
options.inner_class_name = meta.inner_class
options.unit_test_name = meta.test
elif(meta.type == TREE_DIR):
options.dirs = [meta.path]
options.include_subdirectories = true
options.selected = ''
options.inner_class_name = ''
options.unit_test_name = ''
run_tests(options)
func load_config_file(path):
_config.load_options(path)
_config.options.selected = ''
_config.options.inner_class_name = ''
_config.options.unit_test_name = ''
_config_gui.load_file(path)
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2023 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################
================================================
FILE: addons/gut/gui/GutControl.gd.uid
================================================
uid://4d6efbk6plxa
================================================
FILE: addons/gut/gui/GutControl.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://4jb53yqktyfg"]
[ext_resource type="Script" uid="uid://4d6efbk6plxa" path="res://addons/gut/gui/GutControl.gd" id="1_eprql"]
[node name="GutControl" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 295.0
offset_bottom = 419.0
script = ExtResource("1_eprql")
[node name="Bg" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.36, 0.36, 0.36, 1)
[node name="VBox" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Tabs" type="TabContainer" parent="VBox"]
layout_mode = 2
size_flags_vertical = 3
current_tab = 0
[node name="Tests" type="Tree" parent="VBox/Tabs"]
layout_mode = 2
size_flags_vertical = 3
hide_root = true
metadata/_tab_index = 0
[node name="SettingsScroll" type="ScrollContainer" parent="VBox/Tabs"]
visible = false
layout_mode = 2
size_flags_vertical = 3
metadata/_tab_index = 1
[node name="Settings" type="VBoxContainer" parent="VBox/Tabs/SettingsScroll"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Buttons" type="HBoxContainer" parent="VBox"]
layout_mode = 2
[node name="RunTests" type="Button" parent="VBox/Buttons"]
layout_mode = 2
size_flags_horizontal = 3
text = "Run All"
[node name="RunSelected" type="Button" parent="VBox/Buttons"]
layout_mode = 2
size_flags_horizontal = 3
text = "Run Selected"
[connection signal="item_activated" from="VBox/Tabs/Tests" to="." method="_on_tests_item_activated"]
[connection signal="pressed" from="VBox/Buttons/RunTests" to="." method="_on_run_tests_pressed"]
[connection signal="pressed" from="VBox/Buttons/RunSelected" to="." method="_on_run_selected_pressed"]
================================================
FILE: addons/gut/gui/GutRunner.gd
================================================
# ##############################################################################
# This class joins together GUT, GUT Gui, GutConfig and is THE way to kick off a
# run of a test suite.
#
# This creates its own instance of gut.gd that it manages. You can set the
# gut.gd instance if you need to for testing.
#
# Set gut_config to an instance of a configured gut_config.gd instance prior to
# running tests.
#
# This will create a GUI and wire it up and apply gut_config.gd options.
#
# Running tests:
# By default, this will run tests once this control has been added to the tree.
# You can override this by setting ran_from_editor to false before adding
# this to the tree. To run tests manually, call run_tests.
#
# ##############################################################################
extends Node2D
const EXIT_OK = 0
const EXIT_ERROR = 1
var Gut = load('res://addons/gut/gut.gd')
var ResultExporter = load('res://addons/gut/result_exporter.gd')
var GutConfig = load('res://addons/gut/gut_config.gd')
var runner_json_path = null
var result_bbcode_path = null
var result_json_path = null
var lgr = GutUtils.get_logger()
var gut_config = null
var _hid_gut = null;
# Lazy loaded gut instance. Settable for testing purposes.
var gut = _hid_gut :
get:
if(_hid_gut == null):
_hid_gut = Gut.new()
return _hid_gut
set(val):
_hid_gut = val
var _wrote_results = false
# The editor runs this scene using play_custom_scene, which means we cannot
# pass any info directly to the scene. Whenever this is being used from
# somewhere else, you probably want to set this to false before adding this
# to the tree.
var ran_from_editor = true
@onready var _gut_layer = $GutLayer
@onready var _gui = $GutLayer/GutScene
func _ready():
GutUtils.WarningsManager.apply_warnings_dictionary(
GutUtils.warnings_at_start)
GutUtils.LazyLoader.load_all()
# When used from the panel we have to kick off the tests ourselves b/c
# there's no way I know of to interact with the scene that was run via
# play_custom_scene.
if(ran_from_editor):
_run_from_editor()
func _exit_tree():
if(!_wrote_results and ran_from_editor):
_write_results_for_gut_panel()
func _run_from_editor():
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
runner_json_path = GutUtils.nvl(runner_json_path, GutEditorGlobals.editor_run_gut_config_path)
result_bbcode_path = GutUtils.nvl(result_bbcode_path, GutEditorGlobals.editor_run_bbcode_results_path)
result_json_path = GutUtils.nvl(result_json_path, GutEditorGlobals.editor_run_json_results_path)
if(gut_config == null):
gut_config = GutConfig.new()
gut_config.load_options(runner_json_path)
call_deferred('run_tests')
func _setup_gui(show_gui):
if(show_gui):
_gui.gut = gut
var printer = gut.logger.get_printer('gui')
printer.set_textbox(_gui.get_textbox())
else:
gut.logger.disable_printer('gui', true)
_gui.visible = false
var opts = gut_config.options
_gui.set_font_size(opts.font_size)
_gui.set_font(opts.font_name)
if(opts.font_color != null and opts.font_color.is_valid_html_color()):
_gui.set_default_font_color(Color(opts.font_color))
if(opts.background_color != null and opts.background_color.is_valid_html_color()):
_gui.set_background_color(Color(opts.background_color))
_gui.set_opacity(min(1.0, float(opts.opacity) / 100))
_gui.use_compact_mode(opts.compact_mode)
func _write_results_for_gut_panel():
var content = _gui.get_textbox().get_parsed_text() #_gut.logger.get_gui_bbcode()
var f = FileAccess.open(result_bbcode_path, FileAccess.WRITE)
if(f != null):
f.store_string(content)
f = null # closes file
else:
push_error('Could not save bbcode, result = ', FileAccess.get_open_error())
var exporter = ResultExporter.new()
# TODO this should be checked and _wrote_results should maybe not be set, or
# maybe we do not care. Whichever, it should be clear.
var _f_result = exporter.write_json_file(gut, result_json_path)
_wrote_results = true
func _handle_quit(should_exit, should_exit_on_success, override_exit_code=EXIT_OK):
var quitting_time = should_exit or \
(should_exit_on_success and gut.get_fail_count() == 0)
if(!quitting_time):
if(should_exit_on_success):
lgr.log("There are failing tests, exit manually.")
_gui.use_compact_mode(false)
return
# For some reason, tests fail asserting that quit was called with 0 if we
# do not do this, but everything is defaulted so I don't know why it gets
# null.
var exit_code = GutUtils.nvl(override_exit_code, EXIT_OK)
if(gut.get_fail_count() > 0):
exit_code = EXIT_ERROR
# Overwrite the exit code with the post_script's exit code if it is set
var post_hook_inst = gut.get_post_run_script_instance()
if(post_hook_inst != null and post_hook_inst.get_exit_code() != null):
exit_code = post_hook_inst.get_exit_code()
quit(exit_code)
func _end_run(override_exit_code=EXIT_OK):
if(ran_from_editor):
_write_results_for_gut_panel()
_handle_quit(gut_config.options.should_exit,
gut_config.options.should_exit_on_success,
override_exit_code)
# -------------
# Events
# -------------
func _on_tests_finished():
_end_run()
# -------------
# Public
# -------------
func run_tests(show_gui=true):
_setup_gui(show_gui)
if(gut_config.options.dirs.size() + gut_config.options.tests.size() == 0):
var err_text = "You do not have any directories configrued, so GUT doesn't know where to find the tests. Tell GUT where to find the tests and GUT shall run the tests."
lgr.error(err_text)
push_error(err_text)
_end_run(EXIT_ERROR)
return
var install_check_text = GutUtils.make_install_check_text()
if(install_check_text != GutUtils.INSTALL_OK_TEXT):
print("\n\n", GutUtils.version_numbers.get_version_text())
lgr.error(install_check_text)
push_error(install_check_text)
_end_run(EXIT_ERROR)
return
gut.add_children_to = self
if(gut.get_parent() == null):
if(gut_config.options.gut_on_top):
_gut_layer.add_child(gut)
else:
add_child(gut)
gut.end_run.connect(_on_tests_finished)
gut_config.apply_options(gut)
var run_rest_of_scripts = gut_config.options.unit_test_name == ''
gut.test_scripts(run_rest_of_scripts)
func set_gut_config(which):
gut_config = which
# for backwards compatibility
func get_gut():
return gut
func quit(exit_code):
# Sometimes quitting takes a few seconds. This gives some indicator
# of what is going on.
_gui.set_title("Exiting")
await get_tree().process_frame
lgr.info(str('Exiting with code ', exit_code))
get_tree().quit(exit_code)
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2023 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################
================================================
FILE: addons/gut/gui/GutRunner.gd.uid
================================================
uid://dcxarkn8adlju
================================================
FILE: addons/gut/gui/GutRunner.tscn
================================================
[gd_scene load_steps=3 format=3 uid="uid://bqy3ikt6vu4b5"]
[ext_resource type="Script" uid="uid://dcxarkn8adlju" path="res://addons/gut/gui/GutRunner.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://m28heqtswbuq" path="res://addons/gut/GutScene.tscn" id="2_6ruxb"]
[node name="GutRunner" type="Node2D"]
script = ExtResource("1")
[node name="GutLayer" type="CanvasLayer" parent="."]
layer = 128
[node name="GutScene" parent="GutLayer" instance=ExtResource("2_6ruxb")]
================================================
FILE: addons/gut/gui/GutSceneTheme.tres
================================================
[gd_resource type="Theme" load_steps=2 format=3 uid="uid://cstkhwkpajvqu"]
[ext_resource type="FontFile" uid="uid://c6c7gnx36opr0" path="res://addons/gut/fonts/AnonymousPro-Regular.ttf" id="1_df57p"]
[resource]
default_font = ExtResource("1_df57p")
Label/fonts/font = ExtResource("1_df57p")
================================================
FILE: addons/gut/gui/MinGui.tscn
================================================
[gd_scene load_steps=5 format=3 uid="uid://cnqqdfsn80ise"]
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_farmq"]
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_a2e2l"]
[ext_resource type="Script" uid="uid://duu660pjff8cv" path="res://addons/gut/gui/gut_gui.gd" id="2_eokrf"]
[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_xrhva"]
[node name="Min" type="Panel"]
clip_contents = true
custom_minimum_size = Vector2(280, 145)
offset_right = 280.0
offset_bottom = 145.0
theme = ExtResource("1_farmq")
script = ExtResource("2_eokrf")
[node name="MainBox" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="TitleBar" type="Panel" parent="MainBox"]
custom_minimum_size = Vector2(0, 25)
layout_mode = 2
[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 2.0
offset_bottom = 3.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "Title"
[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "0.000s"
[node name="Body" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="LeftMargin" type="CenterContainer" parent="MainBox/Body"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="BodyRows" type="VBoxContainer" parent="MainBox/Body"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ProgressBars" type="HBoxContainer" parent="MainBox/Body/BodyRows"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
layout_mode = 2
text = "T:"
[node name="ProgressTest" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_horizontal = 3
value = 25.0
[node name="HBoxContainer2" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
layout_mode = 2
text = "S:"
[node name="ProgressScript" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_horizontal = 3
value = 75.0
[node name="PathDisplay" type="VBoxContainer" parent="MainBox/Body/BodyRows"]
clip_contents = true
layout_mode = 2
size_flags_vertical = 3
[node name="Path" type="Label" parent="MainBox/Body/BodyRows/PathDisplay"]
layout_mode = 2
theme_override_fonts/font = ExtResource("2_a2e2l")
theme_override_font_sizes/font_size = 14
text = "res://test/integration/whatever"
clip_text = true
text_overrun_behavior = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/PathDisplay"]
clip_contents = true
layout_mode = 2
[node name="S3" type="CenterContainer" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="File" type="Label" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_a2e2l")
theme_override_font_sizes/font_size = 14
text = "test_this_thing.gd"
text_overrun_behavior = 3
[node name="Footer" type="HBoxContainer" parent="MainBox/Body/BodyRows"]
layout_mode = 2
[node name="HandleLeft" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")]
layout_mode = 2
orientation = 0
resize_control = NodePath("../../../../..")
vertical_resize = false
[node name="SwitchModes" type="Button" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
text = "Expand"
[node name="CenterContainer" type="CenterContainer" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Continue" type="Button" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
text = "Continue
"
[node name="HandleRight" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")]
layout_mode = 2
resize_control = NodePath("../../../../..")
vertical_resize = false
[node name="RightMargin" type="CenterContainer" parent="MainBox/Body"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="CenterContainer" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(2.08165e-12, 2)
layout_mode = 2
================================================
FILE: addons/gut/gui/NormalGui.tscn
================================================
[gd_scene load_steps=5 format=3 uid="uid://duxblir3vu8x7"]
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_5hlsm"]
[ext_resource type="Script" uid="uid://duu660pjff8cv" path="res://addons/gut/gui/gut_gui.gd" id="2_fue6q"]
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_u5uc1"]
[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_2r8a8"]
[node name="Large" type="Panel"]
custom_minimum_size = Vector2(500, 150)
offset_right = 632.0
offset_bottom = 260.0
theme = ExtResource("1_5hlsm")
script = ExtResource("2_fue6q")
[node name="MainBox" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="TitleBar" type="Panel" parent="MainBox"]
custom_minimum_size = Vector2(0, 25)
layout_mode = 2
[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 2.0
offset_bottom = 3.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "Title"
[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"]
custom_minimum_size = Vector2(90, 0)
layout_mode = 2
text = "999.999s"
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="MainBox/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="OutputBG" type="ColorRect" parent="MainBox/HBoxContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
color = Color(0.0745098, 0.0705882, 0.0784314, 1)
metadata/_edit_layout_mode = 1
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="S2" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="TestOutput" type="RichTextLabel" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
focus_mode = 2
bbcode_enabled = true
scroll_following = true
autowrap_mode = 0
selection_enabled = true
[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="ControlBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer"]
layout_mode = 2
[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="ProgressBars" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12)
layout_mode = 2
[node name="TestBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
layout_mode = 2
[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
size_flags_horizontal = 3
text = "Tests"
[node name="ProgressTest" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
value = 25.0
[node name="ScriptBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
layout_mode = 2
[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
size_flags_horizontal = 3
text = "Scripts"
[node name="ProgressScript" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
value = 75.0
[node name="PathDisplay" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Path" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
layout_mode = 2
size_flags_vertical = 6
theme_override_fonts/font = ExtResource("2_u5uc1")
theme_override_font_sizes/font_size = 14
text = "res://test/integration/whatever"
text_overrun_behavior = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
layout_mode = 2
size_flags_vertical = 3
[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="File" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_u5uc1")
theme_override_font_sizes/font_size = 14
text = "test_this_thing.gd"
text_overrun_behavior = 3
[node name="Spacer1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
visible = false
layout_mode = 2
size_flags_horizontal = 10
[node name="Continue" type="Button" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
layout_mode = 2
size_flags_vertical = 4
text = "Continue
"
[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="BottomPad" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="Footer" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
[node name="SidePad1" type="CenterContainer" parent="MainBox/Footer"]
custom_minimum_size = Vector2(2, 2.08165e-12)
layout_mode = 2
[node name="ResizeHandle3" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")]
custom_minimum_size = Vector2(25, 25)
layout_mode = 2
orientation = 0
resize_control = NodePath("../../..")
[node name="SwitchModes" type="Button" parent="MainBox/Footer"]
layout_mode = 2
text = "Compact
"
[node name="CenterContainer" type="CenterContainer" parent="MainBox/Footer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ResizeHandle2" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")]
custom_minimum_size = Vector2(25, 25)
layout_mode = 2
resize_control = NodePath("../../..")
[node name="SidePad2" type="CenterContainer" parent="MainBox/Footer"]
custom_minimum_size = Vector2(2, 2.08165e-12)
layout_mode = 2
[node name="BottomPad2" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(2.08165e-12, 2)
layout_mode = 2
================================================
FILE: addons/gut/gui/OutputText.gd
================================================
@tool
extends VBoxContainer
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
var PanelControls = load('res://addons/gut/gui/panel_controls.gd')
# ##############################################################################
# Keeps search results from the TextEdit
# ##############################################################################
class TextEditSearcher:
var te : TextEdit
var _last_term = ''
var _last_pos = Vector2(-1, -1)
var _ignore_caret_change = false
func set_text_edit(which):
te = which
te.caret_changed.connect(_on_caret_changed)
func _on_caret_changed():
if(_ignore_caret_change):
_ignore_caret_change = false
else:
_last_pos = _get_caret();
func _get_caret():
return Vector2(te.get_caret_column(), te.get_caret_line())
func _set_caret_and_sel(pos, len):
te.set_caret_line(pos.y)
te.set_caret_column(pos.x)
if(len > 0):
te.select(pos.y, pos.x, pos.y, pos.x + len)
func _find(term, search_flags):
var pos = _get_caret()
if(term == _last_term):
if(search_flags == 0):
pos = _last_pos
pos.x += 1
else:
pos = _last_pos
pos.x -= 1
var result = te.search(term, search_flags, pos.y, pos.x)
# print('searching from ', pos, ' for "', term, '" = ', result)
if(result.y != -1):
_ignore_caret_change = true
_set_caret_and_sel(result, term.length())
_last_pos = result
_last_term = term
func find_next(term):
_find(term, 0)
func find_prev(term):
_find(term, te.SEARCH_BACKWARDS)
# ##############################################################################
# Start OutputText control code
# ##############################################################################
@onready var _ctrls = {
output = $Output,
settings_bar = $Settings,
use_colors = $Settings/UseColors,
word_wrap = $Settings/WordWrap,
copy_button = $Toolbar/CopyButton,
clear_button = $Toolbar/ClearButton,
show_search = $Toolbar/ShowSearch,
caret_position = $Toolbar/LblPosition,
search_bar = {
bar = $Search,
search_term = $Search/SearchTerm,
}
}
var _sr = TextEditSearcher.new()
var _highlighter : CodeHighlighter
var _font_name = null
var _user_prefs = GutEditorGlobals.user_prefs
var _font_name_pctrl = null
var _font_size_pctrl = null
# Automatically used when running the OutputText scene from the editor. Changes
# to this method only affect test-running the control through the editor.
func _test_running_setup():
_ctrls.use_colors.text = 'use colors'
_ctrls.show_search.text = 'search'
_ctrls.word_wrap.text = 'ww'
set_all_fonts("CourierPrime")
set_font_size(30)
_ctrls.output.queue_redraw()
load_file('user://.gut_editor.bbcode')
await get_tree().process_frame
show_search(true)
_ctrls.output.set_caret_line(0)
_ctrls.output.scroll_vertical = 0
_ctrls.output.caret_changed.connect(_on_caret_changed)
func _ready():
_sr.set_text_edit(_ctrls.output)
_ctrls.use_colors.icon = get_theme_icon('RichTextEffect', 'EditorIcons')
_ctrls.show_search.icon = get_theme_icon('Search', 'EditorIcons')
_ctrls.word_wrap.icon = get_theme_icon('Loop', 'EditorIcons')
_setup_colors()
_ctrls.use_colors.button_pressed = true
_use_highlighting(true)
if(get_parent() == get_tree().root):
_test_running_setup()
_ctrls.settings_bar.visible = false
_add_other_ctrls()
func _add_other_ctrls():
var fname = 'CourierNew'
if(_user_prefs != null):
fname = _user_prefs.output_font_name.value
_font_name_pctrl = PanelControls.SelectControl.new('Font', fname, GutUtils.avail_fonts,
"The font, you know, for the text below. Change it, see what it does.")
_font_name_pctrl.changed.connect(_on_font_name_changed)
_font_name_pctrl.label.size_flags_horizontal = SIZE_SHRINK_BEGIN
_ctrls.settings_bar.add_child(_font_name_pctrl)
set_all_fonts(fname)
var fsize = 30
if(_user_prefs != null):
fsize = _user_prefs.output_font_size.value
_font_size_pctrl = PanelControls.NumberControl.new('Font Size', fsize , 5, 100,
"The size of 'The Font'.")
_font_size_pctrl.changed.connect(_on_font_size_changed)
_font_size_pctrl.label.size_flags_horizontal = SIZE_SHRINK_BEGIN
_ctrls.settings_bar.add_child(_font_size_pctrl)
set_font_size(fsize)
# ------------------
# Private
# ------------------
# Call this after changes in colors and the like to get them to apply. reloads
# the text of the output control.
func _refresh_output():
var orig_pos = _ctrls.output.scroll_vertical
var text = _ctrls.output.text
_ctrls.output.text = text
_ctrls.output.scroll_vertical = orig_pos
func _create_highlighter(default_color=Color(1, 1, 1, 1)):
var to_return = CodeHighlighter.new()
to_return.function_color = default_color
to_return.number_color = default_color
to_return.symbol_color = default_color
to_return.member_variable_color = default_color
var keywords = [
['Failed', Color.RED],
['Passed', Color.GREEN],
['Pending', Color.YELLOW],
['Orphans', Color.YELLOW],
['WARNING', Color.YELLOW],
['ERROR', Color.RED]
]
for keyword in keywords:
to_return.add_keyword_color(keyword[0], keyword[1])
return to_return
func _setup_colors():
_ctrls.output.clear()
var f_color = null
if (_ctrls.output.theme == null) :
f_color = get_theme_color("font_color")
else :
f_color = _ctrls.output.theme.font_color
_highlighter = _create_highlighter()
_ctrls.output.queue_redraw()
func _use_highlighting(should):
if(should):
_ctrls.output.syntax_highlighter = _highlighter
else:
_ctrls.output.syntax_highlighter = null
_refresh_output()
# ------------------
# Events
# ------------------
func _on_caret_changed():
var txt = str("line:",_ctrls.output.get_caret_line(), ' col:', _ctrls.output.get_caret_column())
_ctrls.caret_position.text = str(txt)
func _on_font_size_changed():
set_font_size(_font_size_pctrl.value)
if(_user_prefs != null):
_user_prefs.output_font_size.value = _font_size_pctrl.value
_user_prefs.output_font_size.save_it()
func _on_font_name_changed():
set_all_fonts(_font_name_pctrl.text)
if(_user_prefs != null):
_user_prefs.output_font_name.value = _font_name_pctrl.text
_user_prefs.output_font_name.save_it()
func _on_CopyButton_pressed():
copy_to_clipboard()
func _on_UseColors_pressed():
_use_highlighting(_ctrls.use_colors.button_pressed)
func _on_ClearButton_pressed():
clear()
func _on_ShowSearch_pressed():
show_search(_ctrls.show_search.button_pressed)
func _on_SearchTerm_focus_entered():
_ctrls.search_bar.search_term.call_deferred('select_all')
func _on_SearchNext_pressed():
_sr.find_next(_ctrls.search_bar.search_term.text)
func _on_SearchPrev_pressed():
_sr.find_prev(_ctrls.search_bar.search_term.text)
func _on_SearchTerm_text_changed(new_text):
if(new_text == ''):
_ctrls.output.deselect()
else:
_sr.find_next(new_text)
func _on_SearchTerm_text_entered(new_text):
if(Input.is_physical_key_pressed(KEY_SHIFT)):
_sr.find_prev(new_text)
else:
_sr.find_next(new_text)
func _on_SearchTerm_gui_input(event):
if(event is InputEventKey and !event.pressed and event.keycode == KEY_ESCAPE):
show_search(false)
func _on_WordWrap_pressed():
if(_ctrls.word_wrap.button_pressed):
_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
else:
_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_NONE
_ctrls.output.queue_redraw()
func _on_settings_pressed():
_ctrls.settings_bar.visible = $Toolbar/ShowSettings.button_pressed
# ------------------
# Public
# ------------------
func show_search(should):
_ctrls.search_bar.bar.visible = should
if(should):
_ctrls.search_bar.search_term.grab_focus()
_ctrls.search_bar.search_term.select_all()
_ctrls.show_search.button_pressed = should
func search(text, start_pos, highlight=true):
return _sr.find_next(text)
func copy_to_clipboard():
var selected = _ctrls.output.get_selected_text()
if(selected != ''):
DisplayServer.clipboard_set(selected)
else:
DisplayServer.clipboard_set(_ctrls.output.text)
func clear():
_ctrls.output.text = ''
func _set_font(font_name, custom_name):
var rtl = _ctrls.output
if(font_name == null):
rtl.remove_theme_font_override(custom_name)
else:
var dyn_font = FontFile.new()
dyn_font.load_dynamic_font('res://addons/gut/fonts/' + font_name + '.ttf')
rtl.add_theme_font_override(custom_name, dyn_font)
func set_all_fonts(base_name):
_font_name = GutUtils.nvl(base_name, 'Default')
if(base_name == 'Default'):
_set_font(null, 'font')
_set_font(null, 'normal_font')
_set_font(null, 'bold_font')
_set_font(null, 'italics_font')
_set_font(null, 'bold_italics_font')
else:
_set_font(base_name + '-Regular', 'font')
_set_font(base_name + '-Regular', 'normal_font')
_set_font(base_name + '-Bold', 'bold_font')
_set_font(base_name + '-Italic', 'italics_font')
_set_font(base_name + '-BoldItalic', 'bold_italics_font')
func set_font_size(new_size):
_ctrls.output.set("theme_override_font_sizes/font_size", new_size)
func set_use_colors(value):
pass
func get_use_colors():
return false;
func get_rich_text_edit():
return _ctrls.output
func load_file(path):
var f = FileAccess.open(path, FileAccess.READ)
if(f == null):
return
var t = f.get_as_text()
f = null # closes file
_ctrls.output.text = t
_ctrls.output.scroll_vertical = _ctrls.output.get_line_count()
_ctrls.output.set_deferred('scroll_vertical', _ctrls.output.get_line_count())
func add_text(text):
if(is_inside_tree()):
_ctrls.output.text += text
func scroll_to_line(line):
_ctrls.output.scroll_vertical = line
_ctrls.output.set_caret_line(line)
================================================
FILE: addons/gut/gui/OutputText.gd.uid
================================================
uid://e7xrdfvw3g7c
================================================
FILE: addons/gut/gui/OutputText.tscn
================================================
[gd_scene load_steps=6 format=4 uid="uid://bqmo4dj64c7yl"]
[ext_resource type="Script" uid="uid://e7xrdfvw3g7c" path="res://addons/gut/gui/OutputText.gd" id="1"]
[sub_resource type="Image" id="Image_p7oqn"]
data = {
"data": PackedByteArray("/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9cXAD/XFwA/1xcAP9cXAD/XFwA/1xcAP9dXQD/XV0A/11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXQD/XFx//1xcAP9cXAD/XFwA/1xcAP9cXAD/XV0A/11dAP9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV0A/11d//9cXH//XFwA/1xcAP9cXAD/XFwA/11dAP9dXQD/XV3//11d//9dXf//XV3//11d//9dXf//XV3//11dAP9dXf//XV3//1xcf/9cXAD/XFwA/1xcAP9dXQD/XV0A/11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXQD/XV3//11d//9dXf//XFx//1xcAP9cXAD/XV0A/11dAP9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV0A/11dAP9dXQD/XV0A/1xcAP9cXAD/XFwA/11dAP9dXQD/XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV0A/11dAP9dXQD/XV0A/11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11dAP9dXQD/XV0A/11dAP9dXef/Wlo2/15eOf9dXen/XV3//11d//9dXef/Wlo2/15eOf9dXen/XV3//11d//9dXQD/XV0A/1tbAP9bWwD/W1sq/1paAP9eXgD/W1sq/11d6f9cXOj/XV0p/1paAP9eXgD/W1sq/11d6f9cXOj/XFwA/1xcAP9bWwD/W1sA/1tbAP9bWy3/XV0s/1tbAP9bWyr/W1sq/11dAP9bWy3/XV0s/1tbAP9bWyr/W1sq/1tbAP9bWwD/W1sA/1tbAP9bWy3/XFzr/1xc6v9ZWSv/W1sA/1tbAP9bWy3/XFzr/1xc6v9ZWSv/W1sA/1tbAP9bWwD/W1sA/1xcAP9cXAD/XFzr/11d//9dXf//XV3p/1tbO/9cXD3/XFzr/11d//9dXf//XV3p/1tbO/9cXD3/XFwA/1xcAP9dXQD/XV0A/11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11dAP9dXQD/XV0A/11dAP9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXf//XV3//11d//9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAP9dXQD/XV0A/11dAA=="),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_srqj5"]
image = SubResource("Image_p7oqn")
[sub_resource type="FontFile" id="FontFile_lygvu"]
data = PackedByteArray("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BMwAAATMAekEzAD/BMwAPATMALMEzAAyBMwAkwTMAeUEzAEpBMwBawTMAL8EzACqBMwBdATMAKoEzAHfBMwAxgTMALwEzADcBMwAuwTMAL8EzACTBMwAvATMAMkEzADlBMwAzgTMAMAEzAHfBMwBiATMAH0EzACqBMwArwTMAP4EzAAyBMwACgTMAFAEzAB2BMwAUATMAFAEzABQBMwAYgTMAFAEzADIBMwAcwTMAFAEzABQBMwAHgTMAEYEzABYBMwAUATMAFgEzABQBMwAngTMAHoEzABGBMwACgTM//YEzAAyBMwAPATMAKcEzAFKBMwAxgTMAWYEzADoBMz/5ATMATkEzACHBMwAJQTMAH8EzAB/BMwAkgTMALsEzAB/BMwATQTMALsEzAC+BMwAQwTMALEEzP/8BMwATQTMAIQEzAAlBMwAfwTMAJMEzACzBMwAdQTMAEgEzAAyBMz/9gTMAEYEzAAyBMwAsgTMALEEzAIbBMwA7gTMAJYEzAHpBMwAfwTMAMsEzABvBMwAPATMAhsEzACzBMwBNQTMACQEzADyBMwAjATMAIQEzAEiBMwA9gTMAKoEzAEgBMwBLgTMAaEEzADaBMwAgwTMAd8EzAFfBMwBNATMAOsEzP/7BMz/+wTMABQEzAEGBMwACgTMAAoEzAAKBMwACgTMAAoEzAAKBMwAAATMAHYEzABQBMwAUATMAFAEzABQBMwAyATMAMgEzADIBMwAyATMADIEzABGBMwAWATMAFgEzABYBMwAWATMAFgEzADpBMwAWATMAEYEzABGBMwARgTMAEYEzAA8BMwAbgTMACgEzACHBMwAhwTMAIcEzACHBMwAhwTMAIcEzABLBMwAfwTMAJIEzACSBMwAkgTMAJIEzAC7BMwAuwTMALsEzAC7BMwAjATMAE0EzACEBMwAhATMAIQEzACEBMwAhATMAKoEzABUBMwASATMAEgEzABIBMwASATMADIEzAAlBMwAMgTMAAoEzACHBMwACgTMAIcEzAAKBMwAhwTMAHYEzAB/BMwAdgTMAH8EzAB2BMwAfwTMAHYEzAB/BMwAUATMAH8EzABQBMwAlQTMAFAEzACSBMwAUATMAJIEzABQBMwAkgTMAFAEzACSBMwAYgTMAH8EzABiBMwAfwTMAGIEzAB/BMwAYgTMAH8EzABQBMwATQTMAB4EzAAU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AS4AAwABBAkABQAaAVgAAwABBAkABgAoAXIAAwABBAkACAAkAZoAAwABBAkACQBKAb4AAwABBAkACwA2AggAAwABBAkADAAsAj4AAwABBAkADQEgAmoAAwABBAkADgA0A4oAQwBvAHAAeQByAGkAZwBoAHQAIAAyADAAMQA1ACAAVABoAGUAIABDAG8AdQByAGkAZQByACAAUAByAGkAbQBlACAAUAByAG8AagBlAGMAdAAgAEEAdQB0AGgAbwByAHMAIAAoAGgAdAB0AHAAcwA6AC8ALwBnAGkAdABoAHUAYgAuAGMAbwBtAC8AcQB1AG8AdABlAHUAbgBxAHUAbwB0AGUAYQBwAHAAcwAvAEMAbwB1AHIAaQBlAHIAUAByAGkAbQBlACkALgBDAG8AdQByAGkAZQByACAAUAByAGkAbQBlAFIAZQBnAHUAbABhAHIAMwAuADAAMQA4ADsAUQBVAFEAQQA7AEMAbwB1AHIAaQBlAHIAUAByAGkAbQBlAC0AUgBlAGcAdQBsAGEAcgBDAG8AdQByAGkAZQByACAAUAByAGkAbQBlACAAUgBlAGcAdQBsAGEAcgBWAGUAcgBzAGkAbwBuACAAMwAuADAAMQA4AEMAbwB1AHIAaQBlAHIAUAByAGkAbQBlAC0AUgBlAGcAdQBsAGEAcgBRAHUAbwB0AGUALQBVAG4AcQB1AG8AdABlACAAQQBwAHAAcwBBAGwAYQBuACAARABhAGcAdQBlAC0ARwByAGUAZQBuAGUALAAgAFEAdQBvAHQAZQAtAFUAbgBxAHUAbwB0AGUAIABBAHAAcABzAGgAdAB0AHAAOgAvAC8AcQB1AG8AdABlAHUAbgBxAHUAbwB0AGUAYQBwAHAAcwAuAGMAbwBtAGgAdAB0AHAAOgAvAC8AYgBhAHMAaQBjAHIAZQBjAGkAcABlAC4AYwBvAG0AVABoAGkAcwAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUAIABpAHMAIABsAGkAYwBlAG4AcwBlAGQAIAB1AG4AZABlAHIAIAB0AGgAZQAgAFMASQBMACAATwBwAGUAbgAgAEYAbwBuAHQAIABMAGkAYwBlAG4AcwBlACwAIABWAGUAcgBzAGkAbwBuACAAMQAuADEALgAgAFQAaABpAHMAIABsAGkAYwBlAG4AcwBlACAAaQBzACAAYQB2AGEAaQBsAGEAYgBsAGUAIAB3AGkAdABoACAAYQAgAEYAQQBRACAAYQB0ADoAIABoAHQAdABwADoALwAvAHMAYwByAGkAcAB0AHMALgBzAGkAbAAuAG8AcgBnAC8ATwBGAEwAaAB0AHQAcAA6AC8ALwBzAGMAcgBpAHAAdABzAC4AcwBpAGwALgBvAHIAZwAvAE8ARgBMAAIAAAAAAAD/RwCCAAAAAQAAAAAAAAAAAAAAAAAAAAABkAAAAQIAAgADAAQABQAGAAcACAAJAAoACwAMAA0ADgAPABAAEQASABMAFAAVABYAFwAYABkAGgAbABwAHQAeAB8AIAAhACIAIwAkACUAJgAnACgAKQAqACsALAAtAC4ALwAwADEAMgAzADQANQA2ADcAOAA5ADoAOwA8AD0APgA/AEAAQQBCAEMARABFAEYARwBIAEkASgBLAEwATQBOAE8AUABRAFIAUwBUAFUAVgBXAFgAWQBaAFsAXABdAF4AXwBgAGEAowCEAIUAvQCWAOgAhgCOAIsAnQCkAIoA2gCDAJMBAwEEAI0BBQCIAMMA3gEGAJ4A9QD0APYAogCtAMkAxwCuAGIAYwCQAGQAywBlAMgAygDPAMwAzQDOAOkAZgDTANAA0QCvAGcA8ACRANYA1ADVAGgA6wDtAIkAagBpAGsAbQBsAG4AoABvAHEAcAByAHMAdQB0AHYAdwDqAHgAegB5AHsAfQB8ALgAoQB/AH4AgACBAOwA7gC6AQcBCAEJAQoBCwEMAP0A/gENAQ4BDwEQAP8BAAERARIBEwEBARQBFQEWARcBGAEZARoBGwEcAR0A+AD5AR4BHwEgASEBIgEjASQBJQEmAScBKAEpAPoBKgErASwBLQEuAS8BMAExATIBMwE0ATUA4gDjATYBNwE4ATkBOgE7ATwBPQE+AT8AsACxAUABQQFCAUMBRAFFAUYBRwFIAUkA+wD8AOQA5QFKAUsBTAFNAU4BTwFQAVEBUgFTAVQBVQFWAVcAuwFYAVkBWgFbAOYA5wCmAVwBXQFeANgA4QDbANwA3QDgANkA3wCbALIAswC2ALcAxAC0ALUAxQCCAMIAhwCrAMYAvgC/ALwAjAFfAJgBYACaAJkA7wClAJIAnACnAI8AlACVALkAwADBAWEBYgFjAWQBZQFmAWcBaAFpAWoBawFsAW0BbgFvAXABcQDXAXIBcwF0AXUBdgF3AXgBeQF6AXsBfAF9AX4BfwGAAYEAqQCqAYIBgwD3AYQBhQGGAYcBiAGJAYoBiwGMAY0BjgGPAZABkQGSAZMBlAGVAZYBlwGYBE5VTEwHdW5pMDBCMgd1bmkwMEIzB3VuaTAzQkMHdW5pMDBCOQdBbWFjcm9uB2FtYWNyb24GQWJyZXZlBmFicmV2ZQdBb2dvbmVrB2FvZ29uZWsLQ2NpcmN1bWZsZXgLY2NpcmN1bWZsZXgKQ2RvdGFjY2VudApjZG90YWNjZW50BkRjYXJvbgZkY2Fyb24GRGNyb2F0B0VtYWNyb24HZW1hY3JvbgpFZG90YWNjZW50CmVkb3RhY2NlbnQHRW9nb25lawdlb2dvbmVrBkVjYXJvbgZlY2Fyb24LR2NpcmN1bWZsZXgLZ2NpcmN1bWZsZXgKR2RvdGFjY2VudApnZG90YWNjZW50B3VuaTAxMjIHdW5pMDEyMwtIY2lyY3VtZmxleAtoY2lyY3VtZmxleARIYmFyBGhiYXIHSW1hY3JvbgdpbWFjcm9uB0lvZ29uZWsHaW9nb25lawJJSgJpagtKY2lyY3VtZmxleAtqY2lyY3VtZmxleAd1bmkwMTM2B3VuaTAxMzcGTGFjdXRlBmxhY3V0ZQd1bmkwMTNCB3VuaTAxM0MGTGNhcm9uBmxjYXJvbgZOYWN1dGUGbmFjdXRlB3VuaTAxNDUHdW5pMDE0NgZOY2Fyb24GbmNhcm9uB09tYWNyb24Hb21hY3Jvbg1PaHVuZ2FydW1sYXV0DW9odW5nYXJ1bWxhdXQGUmFjdXRlBnJhY3V0ZQd1bmkwMTU2B3VuaTAxNTcGUmNhcm9uBnJjYXJvbgZTYWN1dGUGc2FjdXRlC1NjaXJjdW1mbGV4C3NjaXJjdW1mbGV4B3VuaTAyMUEHdW5pMDIxQgZUY2Fyb24GdGNhcm9uB1VtYWNyb24HdW1hY3JvbgZVYnJldmUGdWJyZXZlBVVyaW5nBXVyaW5nDVVodW5nYXJ1bWxhdXQNdWh1bmdhcnVtbGF1dAdVb2dvbmVrB3VvZ29uZWsGWmFjdXRlBnphY3V0ZQpaZG90YWNjZW50Cnpkb3RhY2NlbnQHdW5pMDIxOAd1bmkwMjE5B3VuaTAyMzcHdW5pMDNBOQd1bmkwMzk0B3VuaTAxNjIHdW5pMDE2MwZFYnJldmUGSWJyZXZlBkl0aWxkZQRMZG90A0VuZwZPYnJldmUEVGJhcgZVdGlsZGUGV2FjdXRlC1djaXJjdW1mbGV4CVdkaWVyZXNpcwZXZ3JhdmULWWNpcmN1bWZsZXgGWWdyYXZlBmVicmV2ZQZpYnJldmUGaXRpbGRlBGxkb3QDZW5nBm9icmV2ZQR0YmFyBnV0aWxkZQZ3YWN1dGULd2NpcmN1bWZsZXgJd2RpZXJlc2lzBndncmF2ZQt5Y2lyY3VtZmxleAZ5Z3JhdmUHdW5pMjA3NAd1bmkwMEFEB3VuaTIwMTEHdW5pMDBBMARFdXJvB3VuaTIwQTkHdW5pMjIxNQd1bmkwMEI1Bm1pbnV0ZQZzZWNvbmQHdW5pMDMyNgx1bmkwMzI2LmNhc2UKYWN1dGUuY2FzZQpicmV2ZS5jYXNlCWNhcm9uLmFsdApjYXJvbi5jYXNlDGNlZGlsbGEuY2FzZQ9jaXJjdW1mbGV4LmNhc2UNZGllcmVzaXMuY2FzZQ5kb3RhY2NlbnQuY2FzZQpncmF2ZS5jYXNlEWh1bmdhcnVtbGF1dC5jYXNlC21hY3Jvbi5jYXNlC29nb25lay5jYXNlCXJpbmcuY2FzZQp0aWxkZS5jYXNlAAABAAH//wAPAAEAAAAMAAAAHAAAAAIAAgFSAVMAAgGAAYEAAwAIAAIAEAAYAAEAAgFSAVMAAQAEAAECAQABAAQAAQIrAAEAAAAKAKIBmgADREZMVAAUZ3JlawAobGF0bgA8AAQAAAAA//8ABQAAAAYADAAVABsABAAAAAD//wAFAAEABwANABYAHAAWAANDQVQgACZNT0wgADhST00gAEoAAP//AAUAAgAIAA4AFwAdAAD//wAGAAMACQAPABIAGAAeAAD//wAGAAQACgAQABMAGQAfAAD//wAGAAUACwARABQAGgAgACFhYWx0AMhhYWx0AMhhYWx0AMhhYWx0AMhhYWx0AMhhYWx0AMhjYXNlAM5jYXNlAM5jYXNlAM5jYXNlAM5jYXNlAM5jYXNlAM5mcmFjANRmcmFjANRmcmFjANRmcmFjANRmcmFjANRmcmFjANRsb2NsANpsb2NsAOBsb2NsAOZvcmRuAOxvcmRuAOxvcmRuAOxvcmRuAOxvcmRuAOxvcmRuAOxzdXBzAPJzdXBzAPJzdXBzAPJzdXBzAPJzdXBzAPJzdXBzAPIAAAABAAAAAAABAAcAAAABAAUAAAABAAMAAAABAAIAAAABAAEAAAABAAYAAAABAAQACgAWAJAAkACyAPYBFgFSAZoB5AISAAEAAAABAAgAAgA6ABoAeABxAHIBcwBrAHkBigBrAHkBiAGMAYIBhgEnASgBhwGFAYMBiQGOAY0BjwGLAREBEgGBAAEAGgAUABUAFgAXACQAMgBDAEQAUgBpAG4AcwB3AQ0BDgEqASsBLAEtAS4BLwEwATEBVAFVAYAAAQAAAAEACAACAA4ABAEnASgBEQESAAEABAENAQ4BVAFVAAYAAAACAAoAJAADAAAAAgAUAC4AAQAUAAEAAAAIAAEAAQBPAAMAAAACABoAFAABABoAAQAAAAgAAQABAHYAAQABAC8AAQAAAAEACAACAA4ABAB4AHEAcgFzAAIAAQAUABcAAAAEAAAAAQAIAAEALAACAAoAIAACAAYADgB7AAMAEgAVAHoAAwASABcAAQAEAHwAAwASABcAAQACABQAFgAGAAAAAgAKACQAAwABACwAAQASAAAAAQAAAAkAAQACACQARAADAAEAEgABABwAAAABAAAACQACAAEAEwAcAAAAAQACADIAUgABAAAAAQAIAAIAIgAOAYoBiAGMAYIBhgGHAYUBgwGJAY4BjQGPAYsBgQABAA4AQwBpAG4AcwB3ASoBKwEsAS0BLgEvATABMQGAAAQAAAABAAgAAQAeAAIACgAUAAEABAFZAAIAdgABAAQBaAACAHYAAQACAC8ATwABAAAAAQAIAAIADgAEAGsAeQBrAHkAAQAEACQAMgBEAFIAAA==")
font_name = "Courier Prime"
style_name = "Regular"
font_style = 4
subpixel_positioning = 0
msdf_pixel_range = 14
msdf_size = 128
cache/0/25/0/ascent = 20.0
cache/0/25/0/descent = 9.0
cache/0/25/0/underline_position = 3.04688
cache/0/25/0/underline_thickness = 1.59375
cache/0/25/0/scale = 1.0
cache/0/25/0/glyphs/3/advance = Vector2(15, 28)
cache/0/25/0/glyphs/3/offset = Vector2(0, 0)
cache/0/25/0/glyphs/3/size = Vector2(0, 0)
cache/0/25/0/glyphs/3/uv_rect = Rect2(0, 0, 0, 0)
cache/0/25/0/glyphs/3/texture_idx = -1
cache/0/30/0/ascent = 24.0
cache/0/30/0/descent = 11.0
cache/0/30/0/underline_position = 3.65625
cache/0/30/0/underline_thickness = 1.90625
cache/0/30/0/scale = 1.0
cache/0/30/0/glyphs/3/advance = Vector2(18, 34)
cache/0/30/0/glyphs/3/offset = Vector2(0, 0)
cache/0/30/0/glyphs/3/size = Vector2(0, 0)
cache/0/30/0/glyphs/3/uv_rect = Rect2(0, 0, 0, 0)
cache/0/30/0/glyphs/3/texture_idx = -1
cache/0/16/0/ascent = 13.0
cache/0/16/0/descent = 6.0
cache/0/16/0/underline_position = 1.95313
cache/0/16/0/underline_thickness = 1.01563
cache/0/16/0/scale = 1.0
[sub_resource type="CodeHighlighter" id="CodeHighlighter_b4xqv"]
number_color = Color(1, 1, 1, 1)
symbol_color = Color(1, 1, 1, 1)
function_color = Color(1, 1, 1, 1)
member_variable_color = Color(1, 1, 1, 1)
keyword_colors = {
"ERROR": Color(1, 0, 0, 1),
"Failed": Color(1, 0, 0, 1),
"Orphans": Color(1, 1, 0, 1),
"Passed": Color(0, 1, 0, 1),
"Pending": Color(1, 1, 0, 1),
"WARNING": Color(1, 1, 0, 1)
}
[node name="OutputText" type="VBoxContainer"]
offset_right = 862.0
offset_bottom = 523.0
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1")
[node name="Toolbar" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="ShowSearch" type="Button" parent="Toolbar"]
layout_mode = 2
tooltip_text = "Search"
toggle_mode = true
icon = SubResource("ImageTexture_srqj5")
[node name="ShowSettings" type="Button" parent="Toolbar"]
layout_mode = 2
tooltip_text = "Settings"
toggle_mode = true
text = "..."
[node name="CenterContainer" type="CenterContainer" parent="Toolbar"]
layout_mode = 2
size_flags_horizontal = 3
[node name="LblPosition" type="Label" parent="Toolbar"]
layout_mode = 2
[node name="CopyButton" type="Button" parent="Toolbar"]
layout_mode = 2
text = " Copy "
[node name="ClearButton" type="Button" parent="Toolbar"]
layout_mode = 2
text = " Clear "
[node name="Settings" type="HBoxContainer" parent="."]
visible = false
layout_mode = 2
[node name="WordWrap" type="Button" parent="Settings"]
layout_mode = 2
tooltip_text = "Word Wrap"
toggle_mode = true
icon = SubResource("ImageTexture_srqj5")
[node name="UseColors" type="Button" parent="Settings"]
layout_mode = 2
tooltip_text = "Colorized Text"
toggle_mode = true
button_pressed = true
icon = SubResource("ImageTexture_srqj5")
[node name="Output" type="TextEdit" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_fonts/font = SubResource("FontFile_lygvu")
theme_override_font_sizes/font_size = 30
deselect_on_focus_loss_enabled = false
virtual_keyboard_enabled = false
middle_mouse_paste_enabled = false
scroll_smooth = true
syntax_highlighter = SubResource("CodeHighlighter_b4xqv")
highlight_all_occurrences = true
highlight_current_line = true
[node name="Search" type="HBoxContainer" parent="."]
visible = false
layout_mode = 2
[node name="SearchTerm" type="LineEdit" parent="Search"]
layout_mode = 2
size_flags_horizontal = 3
[node name="SearchNext" type="Button" parent="Search"]
layout_mode = 2
text = "Next"
[node name="SearchPrev" type="Button" parent="Search"]
layout_mode = 2
text = "Prev"
[connection signal="pressed" from="Toolbar/ShowSearch" to="." method="_on_ShowSearch_pressed"]
[connection signal="pressed" from="Toolbar/ShowSettings" to="." method="_on_settings_pressed"]
[connection signal="pressed" from="Toolbar/CopyButton" to="." method="_on_CopyButton_pressed"]
[connection signal="pressed" from="Toolbar/ClearButton" to="." method="_on_ClearButton_pressed"]
[connection signal="pressed" from="Settings/WordWrap" to="." method="_on_WordWrap_pressed"]
[connection signal="pressed" from="Settings/UseColors" to="." method="_on_UseColors_pressed"]
[connection signal="focus_entered" from="Search/SearchTerm" to="." method="_on_SearchTerm_focus_entered"]
[connection signal="gui_input" from="Search/SearchTerm" to="." method="_on_SearchTerm_gui_input"]
[connection signal="text_changed" from="Search/SearchTerm" to="." method="_on_SearchTerm_text_changed"]
[connection signal="text_submitted" from="Search/SearchTerm" to="." method="_on_SearchTerm_text_entered"]
[connection signal="pressed" from="Search/SearchNext" to="." method="_on_SearchNext_pressed"]
[connection signal="pressed" from="Search/SearchPrev" to="." method="_on_SearchPrev_pressed"]
================================================
FILE: addons/gut/gui/ResizeHandle.gd
================================================
@tool
extends ColorRect
# #############################################################################
# Resize Handle control. Place onto a control. Set the orientation, then
# set the control that this should resize. Then you can resize the control
# by dragging this thing around. It's pretty neat.
# #############################################################################
enum ORIENTATION {
LEFT,
RIGHT
}
@export var orientation := ORIENTATION.RIGHT :
get: return orientation
set(val):
orientation = val
queue_redraw()
@export var resize_control : Control = null
@export var vertical_resize := true
var _line_width = .5
var _line_color = Color(.4, .4, .4)
var _active_line_color = Color(.3, .3, .3)
var _invalid_line_color = Color(1, 0, 0)
var _line_space = 3
var _num_lines = 8
var _mouse_down = false
# Called when the node enters the scene tree for the first time.
func _draw():
var c = _line_color
if(resize_control == null):
c = _invalid_line_color
elif(_mouse_down):
c = _active_line_color
if(orientation == ORIENTATION.LEFT):
_draw_resize_handle_left(c)
else:
_draw_resize_handle_right(c)
func _gui_input(event):
if(resize_control == null):
return
if(orientation == ORIENTATION.LEFT):
_handle_left_input(event)
else:
_handle_right_input(event)
# Draw the lines in the corner to show where you can
# drag to resize the dialog
func _draw_resize_handle_right(draw_color):
var br = size
for i in range(_num_lines):
var start = br - Vector2(i * _line_space, 0)
var end = br - Vector2(0, i * _line_space)
draw_line(start, end, draw_color, _line_width, true)
func _draw_resize_handle_left(draw_color):
var bl = Vector2(0, size.y)
for i in range(_num_lines):
var start = bl + Vector2(i * _line_space, 0)
var end = bl - Vector2(0, i * _line_space)
draw_line(start, end, draw_color, _line_width, true)
func _handle_right_input(event : InputEvent):
if(event is InputEventMouseMotion):
if(_mouse_down and
event.global_position.x > 0 and
event.global_position.y < DisplayServer.window_get_size().y):
if(vertical_resize):
resize_control.size.y += event.relative.y
resize_control.size.x += event.relative.x
elif(event is InputEventMouseButton):
if(event.button_index == MOUSE_BUTTON_LEFT):
_mouse_down = event.pressed
queue_redraw()
func _handle_left_input(event : InputEvent):
if(event is InputEventMouseMotion):
if(_mouse_down and
event.global_position.x > 0 and
event.global_position.y < DisplayServer.window_get_size().y):
var start_size = resize_control.size
resize_control.size.x -= event.relative.x
if(resize_control.size.x != start_size.x):
resize_control.global_position.x += event.relative.x
if(vertical_resize):
resize_control.size.y += event.relative.y
elif(event is InputEventMouseButton):
if(event.button_index == MOUSE_BUTTON_LEFT):
_mouse_down = event.pressed
queue_redraw()
================================================
FILE: addons/gut/gui/ResizeHandle.gd.uid
================================================
uid://dahg0ssw8tdx3
================================================
FILE: addons/gut/gui/ResizeHandle.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://bvrqqgjpyouse"]
[ext_resource type="Script" uid="uid://dahg0ssw8tdx3" path="res://addons/gut/gui/ResizeHandle.gd" id="1_oi5ed"]
[node name="ResizeHandle" type="ColorRect"]
custom_minimum_size = Vector2(20, 20)
color = Color(1, 1, 1, 0)
script = ExtResource("1_oi5ed")
================================================
FILE: addons/gut/gui/ResultsTree.gd
================================================
@tool
extends Control
var _show_orphans = true
var show_orphans = true :
get: return _show_orphans
set(val): _show_orphans = val
var _hide_passing = true
var hide_passing = true :
get: return _hide_passing
set(val): _hide_passing = val
var _icons = {
red = load('res://addons/gut/images/red.png'),
green = load('res://addons/gut/images/green.png'),
yellow = load('res://addons/gut/images/yellow.png'),
}
const _col_1_bg_color = Color(0, 0, 0, .1)
var _max_icon_width = 10
var _root : TreeItem
@onready var _ctrls = {
tree = $Tree,
lbl_overlay = $Tree/TextOverlay
}
signal item_selected(script_path, inner_class, test_name, line_number)
# -------------------
# Private
# -------------------
func _ready():
_root = _ctrls.tree.create_item()
_root = _ctrls.tree.create_item()
_ctrls.tree.set_hide_root(true)
_ctrls.tree.columns = 2
_ctrls.tree.set_column_expand(0, true)
_ctrls.tree.set_column_expand(1, false)
_ctrls.tree.set_column_clip_content(0, true)
$Tree.item_selected.connect(_on_tree_item_selected)
if(get_parent() == get_tree().root):
_test_running_setup()
func _test_running_setup():
load_json_file('user://.gut_editor.json')
func _on_tree_item_selected():
var item = _ctrls.tree.get_selected()
var item_meta = item.get_metadata(0)
var item_type = null
# Only select the left side of the tree item, cause I like that better.
# you can still click the right, but only the left gets highlighted.
if(item.is_selected(1)):
item.deselect(1)
item.select(0)
if(item_meta == null):
return
else:
item_type = item_meta.type
var script_path = '';
var line = -1;
var test_name = ''
var inner_class = ''
if(item_type == 'test'):
var s_item = item.get_parent()
script_path = s_item.get_metadata(0)['path']
inner_class = s_item.get_metadata(0)['inner_class']
line = -1
test_name = item.get_text(0)
elif(item_type == 'assert'):
var s_item = item.get_parent().get_parent()
script_path = s_item.get_metadata(0)['path']
inner_class = s_item.get_metadata(0)['inner_class']
line = _get_line_number_from_assert_msg(item.get_text(0))
test_name = item.get_parent().get_text(0)
elif(item_type == 'script'):
script_path = item.get_metadata(0)['path']
if(item.get_parent() != _root):
inner_class = item.get_text(0)
line = -1
test_name = ''
else:
return
item_selected.emit(script_path, inner_class, test_name, line)
func _get_line_number_from_assert_msg(msg):
var line = -1
if(msg.find('at line') > 0):
line = msg.split("at line")[-1].split(" ")[-1].to_int()
return line
func _get_path_and_inner_class_name_from_test_path(path):
var to_return = {
path = '',
inner_class = ''
}
to_return.path = path
if !path.ends_with('.gd'):
var loc = path.find('.gd')
to_return.inner_class = path.split('.')[-1]
to_return.path = path.substr(0, loc + 3)
return to_return
func _find_script_item_with_path(path):
var items = _root.get_children()
var to_return = null
var idx = 0
while(idx < items.size() and to_return == null):
var item = items[idx]
if(item.get_metadata(0).path == path):
to_return = item
else:
idx += 1
return to_return
func _add_script_tree_item(script_path, script_json):
var path_info = _get_path_and_inner_class_name_from_test_path(script_path)
var item_text = script_path
var parent = _root
if(path_info.inner_class != ''):
parent = _find_script_item_with_path(path_info.path)
item_text = path_info.inner_class
if(parent == null):
parent = _add_script_tree_item(path_info.path, {})
parent.get_metadata(0).inner_tests += script_json['props']['tests']
parent.get_metadata(0).inner_passing += script_json['props']['tests']
parent.get_metadata(0).inner_passing -= script_json['props']['failures']
parent.get_metadata(0).inner_passing -= script_json['props']['pending']
var total_text = str("All ", parent.get_metadata(0).inner_tests, " passed")
if(parent.get_metadata(0).inner_passing != parent.get_metadata(0).inner_tests):
total_text = str(parent.get_metadata(0).inner_passing, '/', parent.get_metadata(0).inner_tests, ' passed.')
parent.set_text(1, total_text)
var item = _ctrls.tree.create_item(parent)
item.set_text(0, item_text)
var meta = {
"type":"script",
"path":path_info.path,
"inner_class":path_info.inner_class,
"json":script_json,
"inner_passing":0,
"inner_tests":0
}
item.set_metadata(0, meta)
item.set_custom_bg_color(1, _col_1_bg_color)
return item
func _add_assert_item(text, icon, parent_item):
# print(' * adding assert')
var assert_item = _ctrls.tree.create_item(parent_item)
assert_item.set_icon_max_width(0, _max_icon_width)
assert_item.set_text(0, text)
assert_item.set_metadata(0, {"type":"assert"})
assert_item.set_icon(0, icon)
assert_item.set_custom_bg_color(1, _col_1_bg_color)
return assert_item
func _add_test_tree_item(test_name, test_json, script_item):
# print(' * adding test ', test_name)
var no_orphans_to_show = !_show_orphans or (_show_orphans and test_json.orphans == 0)
if(_hide_passing and test_json['status'] == 'pass' and no_orphans_to_show):
return
var item = _ctrls.tree.create_item(script_item)
var status = test_json['status']
var meta = {"type":"test", "json":test_json}
item.set_text(0, test_name)
item.set_text(1, status)
item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
item.set_custom_bg_color(1, _col_1_bg_color)
item.set_metadata(0, meta)
item.set_icon_max_width(0, _max_icon_width)
var orphan_text = 'orphans'
if(test_json.orphans == 1):
orphan_text = 'orphan'
orphan_text = str(test_json.orphans, ' ', orphan_text)
if(status == 'pass' and no_orphans_to_show):
item.set_icon(0, _icons.green)
elif(status == 'pass' and !no_orphans_to_show):
item.set_icon(0, _icons.yellow)
item.set_text(1, orphan_text)
elif(status == 'fail'):
item.set_icon(0, _icons.red)
else:
item.set_icon(0, _icons.yellow)
if(!_hide_passing):
for passing in test_json.passing:
_add_assert_item('pass: ' + passing, _icons.green, item)
for failure in test_json.failing:
_add_assert_item("fail: " + failure.replace("\n", ''), _icons.red, item)
for pending in test_json.pending:
_add_assert_item("pending: " + pending.replace("\n", ''), _icons.yellow, item)
if(status != 'pass' and !no_orphans_to_show):
_add_assert_item(orphan_text, _icons.yellow, item)
return item
func _add_script_to_tree(key, script_json):
var tests = script_json['tests']
var test_keys = tests.keys()
var s_item = _add_script_tree_item(key, script_json)
var bad_count = 0
for test_key in test_keys:
var t_item = _add_test_tree_item(test_key, tests[test_key], s_item)
if(tests[test_key].status != 'pass'):
bad_count += 1
elif(t_item != null):
t_item.collapsed = true
if(s_item.get_children().size() == 0):
s_item.free()
else:
var total_text = str('All ', test_keys.size(), ' passed')
if(bad_count == 0):
s_item.collapsed = true
else:
total_text = str(test_keys.size() - bad_count, '/', test_keys.size(), ' passed')
s_item.set_text(1, total_text)
func _free_childless_scripts():
var items = _root.get_children()
for item in items:
var next_item = item.get_next()
if(item.get_children().size() == 0):
item.free()
item = next_item
func _show_all_passed():
if(_root.get_children().size() == 0):
add_centered_text('Everything passed!')
func _load_result_tree(j):
var scripts = j['test_scripts']['scripts']
var script_keys = scripts.keys()
# if we made it here, the json is valid and we did something, otherwise the
# 'nothing to see here' should be visible.
clear_centered_text()
var add_count = 0
for key in script_keys:
if(scripts[key]['props']['tests'] > 0):
add_count += 1
_add_script_to_tree(key, scripts[key])
_free_childless_scripts()
if(add_count == 0):
add_centered_text('Nothing was run')
else:
_show_all_passed()
# -------------------
# Public
# -------------------
func load_json_file(path):
var file = FileAccess.open(path, FileAccess.READ)
var text = ''
if(file != null):
text = file.get_as_text()
if(text != ''):
var test_json_conv = JSON.new()
var result = test_json_conv.parse(text)
if(result != OK):
add_centered_text(str(path, " has invalid json in it \n",
'Error ', result, "@", test_json_conv.get_error_line(), "\n",
test_json_conv.get_error_message()))
return
var data = test_json_conv.get_data()
load_json_results(data)
else:
add_centered_text(str(path, ' was empty or does not exist.'))
func load_json_results(j):
clear()
_load_result_tree(j)
func clear():
_ctrls.tree.clear()
_root = _ctrls.tree.create_item()
func set_summary_min_width(width):
_ctrls.tree.set_column_custom_minimum_width(1, width)
func add_centered_text(t):
_ctrls.lbl_overlay.visible = true
_ctrls.lbl_overlay.text = t
func clear_centered_text():
_ctrls.lbl_overlay.visible = false
_ctrls.lbl_overlay.text = ''
func collapse_all():
set_collapsed_on_all(_root, true)
func expand_all():
set_collapsed_on_all(_root, false)
func set_collapsed_on_all(item, value):
item.set_collapsed_recursive(value)
if(item == _root and value):
item.set_collapsed(false)
func get_selected():
return _ctrls.tree.get_selected()
================================================
FILE: addons/gut/gui/ResultsTree.gd.uid
================================================
uid://b6e871qdu7x1w
================================================
FILE: addons/gut/gui/ResultsTree.tscn
================================================
[gd_scene load_steps=2 format=3 uid="uid://dls5r5f6157nq"]
[ext_resource type="Script" uid="uid://b6e871qdu7x1w" path="res://addons/gut/gui/ResultsTree.gd" id="1_b4uub"]
[node name="ResultsTree" type="VBoxContainer"]
custom_minimum_size = Vector2(10, 10)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = -70.0
offset_bottom = -104.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_b4uub")
[node name="Tree" type="Tree" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 2
hide_root = true
[node name="TextOverlay" type="Label" parent="Tree"]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
================================================
FILE: addons/gut/gui/RunAtCursor.gd
================================================
@tool
extends Control
var ScriptTextEditors = load('res://addons/gut/gui/script_text_editor_controls.gd')
@onready var _ctrls = {
btn_script = $HBox/BtnRunScript,
btn_inner = $HBox/BtnRunInnerClass,
btn_method = $HBox/BtnRunMethod,
lbl_none = $HBox/LblNoneSelected,
arrow_1 = $HBox/Arrow1,
arrow_2 = $HBox/Arrow2
}
var _editors = null
var _cur_editor = null
var _last_line = -1
var _cur_script_path = null
var _last_info = {
script = null,
inner_class = null,
test_method = null
}
signal run_tests(what)
func _ready():
_ctrls.lbl_none.visible = true
_ctrls.btn_script.visible = false
_ctrls.btn_inner.visible = false
_ctrls.btn_method.visible = false
_ctrls.arrow_1.visible = false
_ctrls.arrow_2.visible = false
# ----------------
# Private
# ----------------
func _set_editor(which):
_last_line = -1
if(_cur_editor != null and _cur_editor.get_ref()):
# _cur_editor.get_ref().disconnect('cursor_changed',Callable(self,'_on_cursor_changed'))
_cur_editor.get_ref().caret_changed.disconnect(_on_cursor_changed)
if(which != null):
_cur_editor = weakref(which)
which.caret_changed.connect(_on_cursor_changed.bind(which))
# which.connect('cursor_changed',Callable(self,'_on_cursor_changed'),[which])
_last_line = which.get_caret_line()
_last_info = _editors.get_line_info()
_update_buttons(_last_info)
func _update_buttons(info):
_ctrls.lbl_none.visible = _cur_script_path == null
_ctrls.btn_script.visible = _cur_script_path != null
_ctrls.btn_inner.visible = info.inner_class != null
_ctrls.arrow_1.visible = info.inner_class != null
_ctrls.btn_inner.text = str(info.inner_class)
_ctrls.btn_inner.tooltip_text = str("Run all tests in Inner-Test-Class ", info.inner_class)
_ctrls.btn_method.visible = info.test_method != null
_ctrls.arrow_2.visible = info.test_method != null
_ctrls.btn_method.text = str(info.test_method)
_ctrls.btn_method.tooltip_text = str("Run test ", info.test_method)
# The button's new size won't take effect until the next frame.
# This appears to be what was causing the button to not be clickable the
# first time.
call_deferred("_update_size")
func _update_size():
custom_minimum_size.x = _ctrls.btn_method.size.x + _ctrls.btn_method.position.x
# ----------------
# Events
# ----------------
func _on_cursor_changed(which):
if(which.get_caret_line() != _last_line):
_last_line = which.get_caret_line()
_last_info = _editors.get_line_info()
_update_buttons(_last_info)
func _on_BtnRunScript_pressed():
var info = _last_info.duplicate()
info.script = _cur_script_path.get_file()
info.inner_class = null
info.test_method = null
emit_signal("run_tests", info)
func _on_BtnRunInnerClass_pressed():
var info = _last_info.duplicate()
info.script = _cur_script_path.get_file()
info.test_method = null
emit_signal("run_tests", info)
func _on_BtnRunMethod_pressed():
var info = _last_info.duplicate()
info.script = _cur_script_path.get_file()
emit_signal("run_tests", info)
# ----------------
# Public
# ----------------
func set_script_text_editors(value):
_editors = value
func activate_for_script(path):
_ctrls.btn_script.visible = true
_ctrls.btn_script.text = path.get_file()
_ctrls.btn_script.tooltip_text = str("Run all tests in script ", path)
_cur_script_path = path
_editors.refresh()
# We have to wait a beat for the visibility to change on
# the editors, otherwise we always get the first one.
await get_tree().process_frame
_set_editor(_editors.get_current_text_edit())
func get_script_button():
return _ctrls.btn_script
func get_inner_button():
return _ctrls.btn_inner
func get_test_button():
return _ctrls.btn_method
# not used, thought was configurable but it's just the script prefix
func set_method_prefix(value):
_editors.set_method_prefix(value)
# not used, thought was configurable but it's just the script prefix
func set_inner_class_prefix(value):
_editors.set_inner_class_prefix(value)
# Mashed this function in here b/c it has _editors. Probably should be
# somewhere else (possibly in script_text_editor_controls).
func search_current_editor_for_text(txt):
var te = _editors.get_current_text_edit()
var result = te.search(txt, 0, 0, 0)
var to_return = -1
return to_return
================================================
FILE: addons/gut/gui/RunAtCursor.gd.uid
================================================
uid://djglbxlh2shog
================================================
FILE: addons/gut/gui/RunAtCursor.tscn
================================================
[gd_scene load_steps=4 format=3 uid="uid://0yunjxtaa8iw"]
[ext_resource type="Script" uid="uid://djglbxlh2shog" path="res://addons/gut/gui/RunAtCursor.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://cr6tvdv0ve6cv" path="res://addons/gut/gui/play.png" id="2"]
[ext_resource type="Texture2D" uid="uid://6wra5rxmfsrl" path="res://addons/gut/gui/arrow.png" id="3"]
[node name="RunAtCursor" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 1.0
offset_bottom = -527.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1")
[node name="HBox" type="HBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="LblNoneSelected" type="Label" parent="HBox"]
layout_mode = 2
text = ""
[node name="BtnRunScript" type="Button" parent="HBox"]
visible = false
layout_mode = 2
text = "