Showing preview only (902K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
# <img src="Icons/Icon.png" width="64" height="64"> 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 <project>`: Starts Project Builder and opens the provided `project`.
- `--execute-routine <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).
<a href='https://ko-fi.com/W7W7AD4W4' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://cdn.ko-fi.com/cdn/kofi1.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
================================================
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.globa
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
Condensed preview — 277 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (921K chars).
[
{
"path": ".gitattributes",
"chars": 176,
"preview": ".gitattributes export-ignore\r\n.gitignore export-ignore\r\nLICENSE.txt export-ignore\r\nREADME.md export-ignore\r\nMedia export"
},
{
"path": ".gitignore",
"chars": 42,
"preview": ".godot/\r\n.export/\r\n.vscode/\r\n.editorconfig"
},
{
"path": "Icons/Add.svg.import",
"chars": 846,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://da1w5je87d6kc\"\npath=\"res://.godot/imported/Add.svg-465"
},
{
"path": "Icons/ArrowDown.svg.import",
"chars": 864,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bidwkeg0fiqcn\"\npath=\"res://.godot/imported/ArrowDown.s"
},
{
"path": "Icons/ArrowLeft.svg.import",
"chars": 864,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cp6em75230mi1\"\npath=\"res://.godot/imported/ArrowLeft.s"
},
{
"path": "Icons/ArrowRight.svg.import",
"chars": 867,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bv4tqa6ixyi1h\"\npath=\"res://.godot/imported/ArrowRight."
},
{
"path": "Icons/ArrowUp.svg.import",
"chars": 857,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://6orgtpcieuls\"\npath=\"res://.godot/imported/ArrowUp.svg-"
},
{
"path": "Icons/Back.svg.import",
"chars": 849,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dt6drd8cw1dhl\"\npath=\"res://.godot/imported/Back.svg-c3"
},
{
"path": "Icons/Copy.svg.import",
"chars": 849,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dgcqwwuv1m48b\"\npath=\"res://.godot/imported/Copy.svg-e2"
},
{
"path": "Icons/Duplicate.svg.import",
"chars": 864,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dnlqsyyx311xx\"\npath=\"res://.godot/imported/Duplicate.s"
},
{
"path": "Icons/Edit.svg.import",
"chars": 849,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cfbagouqtgqfs\"\npath=\"res://.godot/imported/Edit.svg-bb"
},
{
"path": "Icons/Folder.svg.import",
"chars": 855,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dhuukef617r6s\"\npath=\"res://.godot/imported/Folder.svg-"
},
{
"path": "Icons/Icon.png.import",
"chars": 752,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bwbaksun3rqh4\"\npath=\"res://.godot/imported/Icon.png-3c"
},
{
"path": "Icons/Inherit.svg.import",
"chars": 858,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://csjjbw7dnc51a\"\npath=\"res://.godot/imported/Inherit.svg"
},
{
"path": "Icons/MissingIcon.svg.import",
"chars": 870,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dwlbcdps7ydkj\"\npath=\"res://.godot/imported/MissingIcon"
},
{
"path": "Icons/Paste.svg.import",
"chars": 851,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://3i2umr3kmry6\"\npath=\"res://.godot/imported/Paste.svg-c1"
},
{
"path": "Icons/Play.svg.import",
"chars": 849,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://bwtchwpfrbqsw\"\npath=\"res://.godot/imported/Play.svg-a9"
},
{
"path": "Icons/Remove.svg.import",
"chars": 855,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://byn071xu1q5op\"\npath=\"res://.godot/imported/Remove.svg-"
},
{
"path": "Icons/Script.svg.import",
"chars": 855,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://ckqqfrho1pqcd\"\npath=\"res://.godot/imported/Script.svg-"
},
{
"path": "LICENSE.txt",
"chars": 1062,
"preview": "MIT License\n\nCopyright (c) 2024 Tomek\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
},
{
"path": "Media/.gdignore",
"chars": 0,
"preview": ""
},
{
"path": "Nodes/Command.gd",
"chars": 4575,
"preview": "extends Control\n\n@onready var time: Label = %Time\n@onready var output_label: RichTextLabel = %OutputLabel\n\nvar task_text"
},
{
"path": "Nodes/Command.gd.uid",
"chars": 20,
"preview": "uid://c5deg6k04uij2\n"
},
{
"path": "Nodes/Command.tscn",
"chars": 8547,
"preview": "[gd_scene load_steps=24 format=3 uid=\"uid://b6hrktoise2ji\"]\n\n[ext_resource type=\"Script\" uid=\"uid://c5deg6k04uij2\" path="
},
{
"path": "Nodes/GUI/DeleteButton.tscn",
"chars": 1202,
"preview": "[gd_scene load_steps=4 format=3 uid=\"uid://m6a3ajkud85h\"]\n\n[ext_resource type=\"Texture2D\" uid=\"uid://byn071xu1q5op\" path"
},
{
"path": "Nodes/GUI/DirectorySelector.tscn",
"chars": 3114,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://cyl1d6reu3mk4\"]\n\n[ext_resource type=\"Texture2D\" uid=\"uid://dhuukef617r6s\" pat"
},
{
"path": "Nodes/GUI/Disablabler.gd",
"chars": 372,
"preview": "extends Control\n\n@onready var initial_mouse := mouse_filter\n@onready var initial_focus := focus_mode\n\nfunc set_noexist(n"
},
{
"path": "Nodes/GUI/Disablabler.gd.uid",
"chars": 20,
"preview": "uid://dlv30xan3u6wy\n"
},
{
"path": "Nodes/GUI/ExitShortcut.tres",
"chars": 229,
"preview": "[gd_resource type=\"Shortcut\" load_steps=2 format=3 uid=\"uid://d0olqx0bjlhp1\"]\n\n[sub_resource type=\"InputEventAction\" id="
},
{
"path": "Nodes/GUI/StringContainer.gd",
"chars": 966,
"preview": "extends ScrollContainer\n\n@onready var strings: HBoxContainer = %Strings\n\nvar string_prefab: PackedScene\n\nsignal changed\n"
},
{
"path": "Nodes/GUI/StringContainer.gd.uid",
"chars": 20,
"preview": "uid://b640rv8k8mmlp\n"
},
{
"path": "Nodes/GUI/StringContainer.tscn",
"chars": 1135,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://bfbht01onlf1a\"]\n\n[ext_resource type=\"Texture2D\" uid=\"uid://da1w5je87d6kc\" pat"
},
{
"path": "Nodes/Hourglass.png.import",
"chars": 767,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://dno0spmwh4kov\"\npath=\"res://.godot/imported/Hourglass.p"
},
{
"path": "Nodes/PresetTemplate.tscn",
"chars": 7188,
"preview": "[gd_scene load_steps=8 format=3 uid=\"uid://bsgg4mwgvd5vi\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://bfbht01onlf1a\" p"
},
{
"path": "Nodes/ProjectEntry.tscn",
"chars": 3604,
"preview": "[gd_scene load_steps=2 format=3 uid=\"uid://cc3kmk1iwkf5p\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_8e84h\"]\nscript/so"
},
{
"path": "Nodes/RoutinePreview.tscn",
"chars": 5131,
"preview": "[gd_scene load_steps=9 format=3 uid=\"uid://cctfsldanqcig\"]\n\n[ext_resource type=\"Texture2D\" uid=\"uid://dnlqsyyx311xx\" pat"
},
{
"path": "Nodes/Task.gd",
"chars": 4566,
"preview": "extends Control\nclass_name Task\n\n## If [code]true[/code], the script will receive [method _initialize_project] and [meth"
},
{
"path": "Nodes/Task.gd.uid",
"chars": 19,
"preview": "uid://qg3dm7um8fj4\n"
},
{
"path": "Nodes/TaskContainer.tscn",
"chars": 4128,
"preview": "[gd_scene load_steps=7 format=3 uid=\"uid://fktnevmh7mia\"]\n\n[ext_resource type=\"Texture2D\" uid=\"uid://bwtchwpfrbqsw\" path"
},
{
"path": "Nodes/TaskPreview.tscn",
"chars": 2374,
"preview": "[gd_scene load_steps=4 format=3 uid=\"uid://dv8u0se3f7m7s\"]\n\n[sub_resource type=\"StyleBoxFlat\" id=\"StyleBoxFlat_75d6r\"]\nc"
},
{
"path": "README.md",
"chars": 24810,
"preview": "# <img src=\"Icons/Icon.png\" width=\"64\" height=\"64\"> Godot Project Builder\r\n\r\nProject Builder is an automation tool made "
},
{
"path": "Scenes/Execution.gd",
"chars": 4110,
"preview": "extends Control\n\nsignal finished\n\n@onready var task_limbo: Node2D = %TaskLimbo\n@onready var commands_container: VBoxCont"
},
{
"path": "Scenes/Execution.gd.uid",
"chars": 19,
"preview": "uid://gj2kvs6p5asi\n"
},
{
"path": "Scenes/Execution.tscn",
"chars": 2964,
"preview": "[gd_scene load_steps=6 format=3 uid=\"uid://dqm1wdopgkdkp\"]\n\n[ext_resource type=\"Script\" uid=\"uid://gj2kvs6p5asi\" path=\"r"
},
{
"path": "Scenes/Main.gd",
"chars": 5528,
"preview": "extends Control\n\n@onready var template_container: Control = %TemplateContainer\n@onready var routine_container: Control ="
},
{
"path": "Scenes/Main.gd.uid",
"chars": 20,
"preview": "uid://bjg08mlxtbkx5\n"
},
{
"path": "Scenes/Main.tscn",
"chars": 32983,
"preview": "[gd_scene load_steps=19 format=3 uid=\"uid://ba3dlg1fj2hxv\"]\n\n[ext_resource type=\"Script\" uid=\"uid://bjg08mlxtbkx5\" path="
},
{
"path": "Scenes/ProjectManager.gd",
"chars": 3122,
"preview": "extends Control\n\n@onready var custom_list: HBoxContainer = %CustomList\n@onready var projects: VBoxContainer = %Projects\n"
},
{
"path": "Scenes/ProjectManager.gd.uid",
"chars": 20,
"preview": "uid://drooidtlec0og\n"
},
{
"path": "Scenes/ProjectManager.tscn",
"chars": 1610,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://eu3eioilmxkb\"]\n\n[ext_resource type=\"Script\" uid=\"uid://drooidtlec0og\" path=\"r"
},
{
"path": "Scenes/RoutineBuilder.gd",
"chars": 2862,
"preview": "extends Control\n\n@onready var task_list: VBoxContainer = %TaskList\n@onready var add_task: MenuButton = %AddTask\n\nvar rou"
},
{
"path": "Scenes/RoutineBuilder.gd.uid",
"chars": 20,
"preview": "uid://bw6lvvotkx7nc\n"
},
{
"path": "Scenes/RoutineBuilder.tscn",
"chars": 3537,
"preview": "[gd_scene load_steps=5 format=3 uid=\"uid://dbb72q773nirj\"]\n\n[ext_resource type=\"Script\" uid=\"uid://bw6lvvotkx7nc\" path=\""
},
{
"path": "Scripts/Data.gd",
"chars": 5467,
"preview": "extends Node\n\nconst CONFIG_FILE = \"project_builds_config.txt\"\nvar local_config_file: String\n\nvar global_config: Dictiona"
},
{
"path": "Scripts/Data.gd.uid",
"chars": 20,
"preview": "uid://cxigukxxvepyb\n"
},
{
"path": "Scripts/Templates/Task/EmptyTask.gd",
"chars": 787,
"preview": "extends Task\n\nfunc _get_task_name() -> String:\n\treturn \"Empty Task\"\n\nfunc _get_execute_string() -> String:\n\treturn _get_"
},
{
"path": "Scripts/Templates/Task/EmptyTask.gd.uid",
"chars": 18,
"preview": "uid://05ho57r0n7x\n"
},
{
"path": "Tasks/ClearDirectory.tscn",
"chars": 3457,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://baipfhiy5un08\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" p"
},
{
"path": "Tasks/CopyFiles.tscn",
"chars": 3378,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://bfxyekjccdafx\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" p"
},
{
"path": "Tasks/CustomTask.tscn",
"chars": 2084,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://2g67bchptwsm\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://bfbht01onlf1a\" pa"
},
{
"path": "Tasks/ExportProject.tscn",
"chars": 3121,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://m4tjlhu18ode\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" pa"
},
{
"path": "Tasks/ExportProjectFromTemplate.tscn",
"chars": 5601,
"preview": "[gd_scene load_steps=2 format=3 uid=\"uid://doi6iht30rdxc\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_mlhhy\"]\nscript/so"
},
{
"path": "Tasks/ExportTask.gd",
"chars": 2573,
"preview": "extends Task\n\nstatic var godot_path: String\nstatic var is_godot_3: bool\n\nvar export_debug: bool\nvar override_path: bool\n"
},
{
"path": "Tasks/ExportTask.gd.uid",
"chars": 20,
"preview": "uid://ddyec862cgxyf\n"
},
{
"path": "Tasks/PackZIP.tscn",
"chars": 4190,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://dw4t3o5hj774w\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" p"
},
{
"path": "Tasks/ScriptTask/BaseScriptTask.gd",
"chars": 1442,
"preview": "extends SceneTree\n\nvar argument_list: PackedStringArray\nvar arguments: Dictionary\n\nfunc add_expected_argument(name: Stri"
},
{
"path": "Tasks/ScriptTask/BaseScriptTask.gd.uid",
"chars": 20,
"preview": "uid://dj2qru8dp0yi2\n"
},
{
"path": "Tasks/ScriptTask/ClearDirectory.gd",
"chars": 1493,
"preview": "extends \"BaseScriptTask.gd\"\n\nfunc _init() -> void:\n\tadd_expected_argument(\"source\", \"Directory to clear.\")\n\tadd_variadic"
},
{
"path": "Tasks/ScriptTask/ClearDirectory.gd.uid",
"chars": 20,
"preview": "uid://br3t3y5s843pj\n"
},
{
"path": "Tasks/ScriptTask/CopyFiles.gd",
"chars": 1738,
"preview": "extends \"BaseScriptTask.gd\"\n\nfunc _init() -> void:\n\tadd_expected_argument(\"source\", \"Source directory for files.\")\n\tadd_"
},
{
"path": "Tasks/ScriptTask/CopyFiles.gd.uid",
"chars": 20,
"preview": "uid://dkrhktoogubcl\n"
},
{
"path": "Tasks/ScriptTask/PackZIP.gd",
"chars": 2205,
"preview": "extends \"BaseScriptTask.gd\"\n\nvar root_path: String\nvar include_filters: PackedStringArray\nvar exclude_filters: PackedStr"
},
{
"path": "Tasks/ScriptTask/PackZIP.gd.uid",
"chars": 19,
"preview": "uid://ry5v70sx1vat\n"
},
{
"path": "Tasks/ScriptTask.gd",
"chars": 1003,
"preview": "extends Task\n\n@export var script_name: String\n\nfunc _get_command() -> String:\n\treturn OS.get_executable_path()\n\nfunc _ge"
},
{
"path": "Tasks/ScriptTask.gd.uid",
"chars": 20,
"preview": "uid://cu6cbkw8roupo\n"
},
{
"path": "Tasks/SubRoutine.tscn",
"chars": 4752,
"preview": "[gd_scene load_steps=2 format=3 uid=\"uid://dh4e5n3xxf3n\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_boi7e\"]\nscript/sou"
},
{
"path": "Tasks/UploadEpic.tscn",
"chars": 6035,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://228qq6rkrwx6\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" pa"
},
{
"path": "Tasks/UploadGOG.tscn",
"chars": 5037,
"preview": "[gd_scene load_steps=2 format=3 uid=\"uid://7qw6mxp834ei\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_a0t4t\"]\nscript/sou"
},
{
"path": "Tasks/UploadItch.tscn",
"chars": 4714,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://xt4wel0slqe4\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://cyl1d6reu3mk4\" pa"
},
{
"path": "Tasks/UploadSteam.tscn",
"chars": 3544,
"preview": "[gd_scene load_steps=2 format=3 uid=\"uid://b7iec2eyiepn8\"]\n\n[sub_resource type=\"GDScript\" id=\"GDScript_ftiq7\"]\nscript/so"
},
{
"path": "Tests/GutConfig.json",
"chars": 668,
"preview": "{\n \"background_color\": \"262626ff\",\n \"compact_mode\": false,\n \"configured_dirs\": [\n \"res://Tests\"\n ],\n \"dirs\": [\n \"res:/"
},
{
"path": "Tests/Projects/.gdignore",
"chars": 0,
"preview": ""
},
{
"path": "Tests/Projects/TestProject1/DeepDir/DirFile1.txt",
"chars": 9,
"preview": "DirFile1\n"
},
{
"path": "Tests/Projects/TestProject1/DeepDir/DirFile2.txt",
"chars": 9,
"preview": "DirFile2\n"
},
{
"path": "Tests/Projects/TestProject1/DeepDir/SubDir/SubDirFile1.txt",
"chars": 12,
"preview": "SubDirFile1\n"
},
{
"path": "Tests/Projects/TestProject1/EmptyDir/.gdignore",
"chars": 0,
"preview": ""
},
{
"path": "Tests/Projects/TestProject1/File1.txt",
"chars": 6,
"preview": "File1\n"
},
{
"path": "Tests/Projects/TestProject1/MixedDir/MdFile1.md",
"chars": 8,
"preview": "MdFile1\n"
},
{
"path": "Tests/Projects/TestProject1/MixedDir/MdFile2.md",
"chars": 8,
"preview": "MdFile2\n"
},
{
"path": "Tests/Projects/TestProject1/MixedDir/TxtFile1.txt",
"chars": 9,
"preview": "TxtFile1\n"
},
{
"path": "Tests/Projects/TestProject1/MixedDir/TxtFile2.txt",
"chars": 9,
"preview": "TxtFile2\n"
},
{
"path": "Tests/Projects/TestProject1/project.godot",
"chars": 359,
"preview": "; Engine configuration file.\n; It's best edited using the editor UI and not directly,\n; since the parameters that go her"
},
{
"path": "Tests/Projects/TestProject1/project_builds_config.txt",
"chars": 3061,
"preview": "{\n\"epic_artifact_id\": \"\",\n\"epic_cloud_dir\": \"\",\n\"epic_product_id\": \"\",\n\"godot_path\": \"\",\n\"itch_default_channel\": \"\",\n\"it"
},
{
"path": "Tests/TestExecution.gd",
"chars": 8766,
"preview": "extends GutTest\n\nconst PROJECTS := {\n\t1: \"res://Tests/Projects/TestProject1/\",\n}\nconst ROUTINES := {\n\t# TestProject1\n\t\"c"
},
{
"path": "Tests/TestExecution.gd.uid",
"chars": 20,
"preview": "uid://dbnpvtbcdms7l\n"
},
{
"path": "addons/Prefab/Prefab.gd",
"chars": 484,
"preview": "extends PackedScene\nclass_name Prefab\n\nstatic func create(node: Node, deferred_free := false) -> Prefab:\n\tassert(node, \""
},
{
"path": "addons/Prefab/Prefab.gd.uid",
"chars": 20,
"preview": "uid://c3i8g8t027eyi\n"
},
{
"path": "addons/ProjectBuilder/ProjectBuilderPlugin.gd",
"chars": 3763,
"preview": "@tool\nextends EditorPlugin\n\nconst CONFIG_SETTING = \"_project_builder_config_path\"\n\nvar popup: PopupMenu\nvar cached_routi"
},
{
"path": "addons/ProjectBuilder/ProjectBuilderPlugin.gd.uid",
"chars": 19,
"preview": "uid://sef5h7ohaqw0\n"
},
{
"path": "addons/ProjectBuilder/plugin.cfg",
"chars": 182,
"preview": "[plugin]\n\nname=\"Project Builder Plugin\"\ndescription=\"Helper addon to start Project Builder from within the project.\"\naut"
},
{
"path": "addons/gut/GutScene.gd",
"chars": 3461,
"preview": "extends Node2D\n# ##############################################################################\n# This is a wrapper arou"
},
{
"path": "addons/gut/GutScene.gd.uid",
"chars": 20,
"preview": "uid://dxo1rbug4pkeq\n"
},
{
"path": "addons/gut/GutScene.tscn",
"chars": 673,
"preview": "[gd_scene load_steps=4 format=3 uid=\"uid://m28heqtswbuq\"]\n\n[ext_resource type=\"Script\" uid=\"uid://dxo1rbug4pkeq\" path=\"r"
},
{
"path": "addons/gut/LICENSE.md",
"chars": 1107,
"preview": "The MIT License (MIT)\n=====================\n\nCopyright (c) 2018 Tom \"Butch\" Wesley\n\nPermission is hereby granted, free o"
},
{
"path": "addons/gut/UserFileViewer.gd",
"chars": 1090,
"preview": "extends Window\n\n@onready var rtl = $TextDisplay/RichTextLabel\n\nfunc _get_file_as_text(path):\n\tvar to_return = null\n\tvar "
},
{
"path": "addons/gut/UserFileViewer.gd.uid",
"chars": 19,
"preview": "uid://or7j8eak2s7t\n"
},
{
"path": "addons/gut/UserFileViewer.tscn",
"chars": 3521,
"preview": "[gd_scene load_steps=2 format=3 uid=\"uid://bsm7wtt1gie4v\"]\n\n[ext_resource type=\"Script\" uid=\"uid://or7j8eak2s7t\" path=\"r"
},
{
"path": "addons/gut/autofree.gd",
"chars": 2176,
"preview": "# ##############################################################################\n#(G)odot (U)nit (T)est class\n#\n# ######"
},
{
"path": "addons/gut/autofree.gd.uid",
"chars": 19,
"preview": "uid://u4sats0gvfeu\n"
},
{
"path": "addons/gut/awaiter.gd",
"chars": 2971,
"preview": "extends Node\n\nsignal timeout\nsignal wait_started\n\nvar _wait_time := 0.0\nvar _wait_frames := 0\nvar _signal_to_wait_on = n"
},
{
"path": "addons/gut/awaiter.gd.uid",
"chars": 20,
"preview": "uid://cub0o1h1sbesy\n"
},
{
"path": "addons/gut/cli/gut_cli.gd",
"chars": 12790,
"preview": "extends Node\n\nvar Optparse = load('res://addons/gut/cli/optparse.gd')\nvar Gut = load('res://addons/gut/gut.gd')\nvar GutR"
},
{
"path": "addons/gut/cli/gut_cli.gd.uid",
"chars": 20,
"preview": "uid://bvyb8bvwwhi67\n"
},
{
"path": "addons/gut/cli/optparse.gd",
"chars": 15780,
"preview": "# ##############################################################################\n# Parses options from the command line,"
},
{
"path": "addons/gut/cli/optparse.gd.uid",
"chars": 20,
"preview": "uid://c3h6h5q43socr\n"
},
{
"path": "addons/gut/collected_script.gd",
"chars": 5352,
"preview": "# ------------------------------------------------------------------------------\n# This holds all the meta information f"
},
{
"path": "addons/gut/collected_script.gd.uid",
"chars": 20,
"preview": "uid://dsl71gqb5syn4\n"
},
{
"path": "addons/gut/collected_test.gd",
"chars": 3066,
"preview": "# ------------------------------------------------------------------------------\n# Used to keep track of info about each"
},
{
"path": "addons/gut/collected_test.gd.uid",
"chars": 20,
"preview": "uid://b2edfqjiogifb\n"
},
{
"path": "addons/gut/comparator.gd",
"chars": 3315,
"preview": "var _strutils = GutUtils.Strutils.new()\nvar _max_length = 100\nvar _should_compare_int_to_float = true\n\nconst MISSING = '"
},
{
"path": "addons/gut/comparator.gd.uid",
"chars": 19,
"preview": "uid://3psq3ss4wepq\n"
},
{
"path": "addons/gut/compare_result.gd",
"chars": 1170,
"preview": "var _are_equal = false\nvar are_equal = false :\n\tget:\n\t\treturn get_are_equal()\n\tset(val):\n\t\tset_are_equal(val)\n\nvar _summ"
},
{
"path": "addons/gut/compare_result.gd.uid",
"chars": 20,
"preview": "uid://dscxpe8c6ytjo\n"
},
{
"path": "addons/gut/diff_formatter.gd",
"chars": 1534,
"preview": "var _strutils = GutUtils.Strutils.new()\nconst INDENT = ' '\nvar _max_to_display = 30\nconst ABSOLUTE_MAX_DISPLAYED = 10"
},
{
"path": "addons/gut/diff_formatter.gd.uid",
"chars": 20,
"preview": "uid://be6htaq8rkbd0\n"
},
{
"path": "addons/gut/diff_tool.gd",
"chars": 3615,
"preview": "extends 'res://addons/gut/compare_result.gd'\nconst INDENT = ' '\nenum {\n\tDEEP,\n\tSIMPLE\n}\n\nvar _strutils = GutUtils.Str"
},
{
"path": "addons/gut/diff_tool.gd.uid",
"chars": 20,
"preview": "uid://c5u6dl5cwn26w\n"
},
{
"path": "addons/gut/double_templates/function_template.txt",
"chars": 260,
"preview": "{func_decleration}\n\t{vararg_warning}__gutdbl.spy_on('{method_name}', {param_array})\n\tif(__gutdbl.is_stubbed_to_call_supe"
},
{
"path": "addons/gut/double_templates/init_template.txt",
"chars": 93,
"preview": "{func_decleration}:\n\tsuper({super_params})\n\t__gutdbl.spy_on('{method_name}', {param_array})\n\n"
},
{
"path": "addons/gut/double_templates/script_template.txt",
"chars": 1017,
"preview": "# ##############################################################################\n# Gut Doubled Script\n# ################"
},
{
"path": "addons/gut/double_tools.gd",
"chars": 2000,
"preview": "var thepath = ''\nvar subpath = ''\nvar stubber = null\nvar spy = null\nvar gut = null\nvar from_singleton = null\nvar is_part"
},
{
"path": "addons/gut/double_tools.gd.uid",
"chars": 20,
"preview": "uid://b2qjwou7pm0tr\n"
},
{
"path": "addons/gut/doubler.gd",
"chars": 10734,
"preview": "# ------------------------------------------------------------------------------\n# A stroke of genius if I do say so. T"
},
{
"path": "addons/gut/doubler.gd.uid",
"chars": 19,
"preview": "uid://1a6kkk2dlt2i\n"
},
{
"path": "addons/gut/dynamic_gdscript.gd",
"chars": 1082,
"preview": "@tool\nvar default_script_name_no_extension = 'gut_dynamic_script'\nvar default_script_resource_path = 'res://addons/gut/n"
},
{
"path": "addons/gut/dynamic_gdscript.gd.uid",
"chars": 19,
"preview": "uid://rh3x2vl3tmb2\n"
},
{
"path": "addons/gut/fonts/AnonymousPro-Bold.ttf.import",
"chars": 765,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://c8axnpxc0nrk4\"\npath=\"res://.godot/imported/AnonymousPro"
},
{
"path": "addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import",
"chars": 780,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://msst1l2s2s\"\npath=\"res://.godot/imported/AnonymousPro-Bo"
},
{
"path": "addons/gut/fonts/AnonymousPro-Italic.ttf.import",
"chars": 770,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://hf5rdg67jcwc\"\npath=\"res://.godot/imported/AnonymousPro-"
},
{
"path": "addons/gut/fonts/AnonymousPro-Regular.ttf.import",
"chars": 774,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://c6c7gnx36opr0\"\npath=\"res://.godot/imported/AnonymousPro"
},
{
"path": "addons/gut/fonts/CourierPrime-Bold.ttf.import",
"chars": 765,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://bhjgpy1dovmyq\"\npath=\"res://.godot/imported/CourierPrime"
},
{
"path": "addons/gut/fonts/CourierPrime-BoldItalic.ttf.import",
"chars": 782,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://n6mxiov5sbgc\"\npath=\"res://.godot/imported/CourierPrime-"
},
{
"path": "addons/gut/fonts/CourierPrime-Italic.ttf.import",
"chars": 770,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://mcht266g817e\"\npath=\"res://.godot/imported/CourierPrime-"
},
{
"path": "addons/gut/fonts/CourierPrime-Regular.ttf.import",
"chars": 774,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://bnh0lslf4yh87\"\npath=\"res://.godot/imported/CourierPrime"
},
{
"path": "addons/gut/fonts/LobsterTwo-Bold.ttf.import",
"chars": 759,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://cmiuntu71oyl3\"\npath=\"res://.godot/imported/LobsterTwo-B"
},
{
"path": "addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import",
"chars": 777,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://bll38n2ct6qme\"\npath=\"res://.godot/imported/LobsterTwo-B"
},
{
"path": "addons/gut/fonts/LobsterTwo-Italic.ttf.import",
"chars": 765,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://dis65h8wxc3f2\"\npath=\"res://.godot/imported/LobsterTwo-I"
},
{
"path": "addons/gut/fonts/LobsterTwo-Regular.ttf.import",
"chars": 767,
"preview": "[remap]\n\nimporter=\"font_data_dynamic\"\ntype=\"FontFile\"\nuid=\"uid://5e8msj0ih2pv\"\npath=\"res://.godot/imported/LobsterTwo-Re"
},
{
"path": "addons/gut/fonts/OFL.txt",
"chars": 4426,
"preview": "Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com),\nwith Reserved Font Name Anonymous P"
},
{
"path": "addons/gut/gui/BottomPanelShortcuts.gd",
"chars": 4340,
"preview": "@tool\nextends Window\n\nvar GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')\nvar default_path = GutEditor"
},
{
"path": "addons/gut/gui/BottomPanelShortcuts.gd.uid",
"chars": 20,
"preview": "uid://ck68kf4ewmrrf\n"
},
{
"path": "addons/gut/gui/BottomPanelShortcuts.tscn",
"chars": 4510,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://bsk32dh41b4gs\"]\n\n[ext_resource type=\"PackedScene\" uid=\"uid://sfb1fw8j6ufu\" pa"
},
{
"path": "addons/gut/gui/GutBottomPanel.gd",
"chars": 11076,
"preview": "@tool\nextends Control\n\nvar GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')\nvar TestScript = load('res:"
},
{
"path": "addons/gut/gui/GutBottomPanel.gd.uid",
"chars": 20,
"preview": "uid://bhjmhcm3b7dv2\n"
},
{
"path": "addons/gut/gui/GutBottomPanel.tscn",
"chars": 12685,
"preview": "[gd_scene load_steps=10 format=3 uid=\"uid://b3bostcslstem\"]\n\n[ext_resource type=\"Script\" uid=\"uid://bhjmhcm3b7dv2\" path="
},
{
"path": "addons/gut/gui/GutControl.gd",
"chars": 8953,
"preview": "@tool\nextends Control\n\nconst RUNNER_JSON_PATH = 'res://.gut_editor_config.json'\n\nvar GutConfig = load('res://addons/gut/"
},
{
"path": "addons/gut/gui/GutControl.gd.uid",
"chars": 19,
"preview": "uid://4d6efbk6plxa\n"
},
{
"path": "addons/gut/gui/GutControl.tscn",
"chars": 1840,
"preview": "[gd_scene load_steps=2 format=3 uid=\"uid://4jb53yqktyfg\"]\n\n[ext_resource type=\"Script\" uid=\"uid://4d6efbk6plxa\" path=\"re"
},
{
"path": "addons/gut/gui/GutRunner.gd",
"chars": 7848,
"preview": "# ##############################################################################\n# This class joins together GUT, GUT Gu"
},
{
"path": "addons/gut/gui/GutRunner.gd.uid",
"chars": 20,
"preview": "uid://dcxarkn8adlju\n"
},
{
"path": "addons/gut/gui/GutRunner.tscn",
"chars": 478,
"preview": "[gd_scene load_steps=3 format=3 uid=\"uid://bqy3ikt6vu4b5\"]\n\n[ext_resource type=\"Script\" uid=\"uid://dcxarkn8adlju\" path=\""
},
{
"path": "addons/gut/gui/GutSceneTheme.tres",
"chars": 293,
"preview": "[gd_resource type=\"Theme\" load_steps=2 format=3 uid=\"uid://cstkhwkpajvqu\"]\n\n[ext_resource type=\"FontFile\" uid=\"uid://c6c"
},
{
"path": "addons/gut/gui/MinGui.tscn",
"chars": 5218,
"preview": "[gd_scene load_steps=5 format=3 uid=\"uid://cnqqdfsn80ise\"]\n\n[ext_resource type=\"Theme\" uid=\"uid://cstkhwkpajvqu\" path=\"r"
},
{
"path": "addons/gut/gui/NormalGui.tscn",
"chars": 7287,
"preview": "[gd_scene load_steps=5 format=3 uid=\"uid://duxblir3vu8x7\"]\n\n[ext_resource type=\"Theme\" uid=\"uid://cstkhwkpajvqu\" path=\"r"
},
{
"path": "addons/gut/gui/OutputText.gd",
"chars": 9520,
"preview": "@tool\nextends VBoxContainer\n\nvar GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')\nvar PanelControls = l"
},
{
"path": "addons/gut/gui/OutputText.gd.uid",
"chars": 19,
"preview": "uid://e7xrdfvw3g7c\n"
},
{
"path": "addons/gut/gui/OutputText.tscn",
"chars": 97518,
"preview": "[gd_scene load_steps=6 format=4 uid=\"uid://bqmo4dj64c7yl\"]\n\n[ext_resource type=\"Script\" uid=\"uid://e7xrdfvw3g7c\" path=\"r"
},
{
"path": "addons/gut/gui/ResizeHandle.gd",
"chars": 2934,
"preview": "@tool\nextends ColorRect\n# #############################################################################\n# Resize Handle "
},
{
"path": "addons/gut/gui/ResizeHandle.gd.uid",
"chars": 20,
"preview": "uid://dahg0ssw8tdx3\n"
},
{
"path": "addons/gut/gui/ResizeHandle.tscn",
"chars": 313,
"preview": "[gd_scene load_steps=2 format=3 uid=\"uid://bvrqqgjpyouse\"]\n\n[ext_resource type=\"Script\" uid=\"uid://dahg0ssw8tdx3\" path=\""
},
{
"path": "addons/gut/gui/ResultsTree.gd",
"chars": 9212,
"preview": "@tool\nextends Control\n\nvar _show_orphans = true\nvar show_orphans = true :\n\tget: return _show_orphans\n\tset(val): _show_or"
},
{
"path": "addons/gut/gui/ResultsTree.gd.uid",
"chars": 20,
"preview": "uid://b6e871qdu7x1w\n"
},
{
"path": "addons/gut/gui/ResultsTree.tscn",
"chars": 801,
"preview": "[gd_scene load_steps=2 format=3 uid=\"uid://dls5r5f6157nq\"]\n\n[ext_resource type=\"Script\" uid=\"uid://b6e871qdu7x1w\" path=\""
},
{
"path": "addons/gut/gui/RunAtCursor.gd",
"chars": 4241,
"preview": "@tool\nextends Control\n\n\nvar ScriptTextEditors = load('res://addons/gut/gui/script_text_editor_controls.gd')\n\n@onready va"
},
{
"path": "addons/gut/gui/RunAtCursor.gd.uid",
"chars": 20,
"preview": "uid://djglbxlh2shog\n"
},
{
"path": "addons/gut/gui/RunAtCursor.tscn",
"chars": 1938,
"preview": "[gd_scene load_steps=4 format=3 uid=\"uid://0yunjxtaa8iw\"]\n\n[ext_resource type=\"Script\" uid=\"uid://djglbxlh2shog\" path=\"r"
},
{
"path": "addons/gut/gui/RunResults.gd",
"chars": 6138,
"preview": "@tool\nextends Control\n\nvar GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')\n\nvar _interface = null\nvar "
},
{
"path": "addons/gut/gui/RunResults.gd.uid",
"chars": 20,
"preview": "uid://dq5y5rt75836n\n"
},
{
"path": "addons/gut/gui/RunResults.tscn",
"chars": 7763,
"preview": "[gd_scene load_steps=5 format=3 uid=\"uid://4gyyn12um08h\"]\n\n[ext_resource type=\"Script\" uid=\"uid://dq5y5rt75836n\" path=\"r"
},
{
"path": "addons/gut/gui/Settings.tscn",
"chars": 184,
"preview": "[gd_scene format=3 uid=\"uid://cvvvtsah38l0e\"]\n\n[node name=\"Settings\" type=\"VBoxContainer\"]\noffset_right = 388.0\noffset_b"
},
{
"path": "addons/gut/gui/ShortcutButton.gd",
"chars": 3266,
"preview": "@tool\nextends Control\n\n\n@onready var _ctrls = {\n\tshortcut_label = $Layout/lblShortcut,\n\tset_button = $Layout/SetButton,\n"
},
{
"path": "addons/gut/gui/ShortcutButton.gd.uid",
"chars": 19,
"preview": "uid://slt6m4nclynh\n"
},
{
"path": "addons/gut/gui/ShortcutButton.tscn",
"chars": 1681,
"preview": "[gd_scene load_steps=2 format=3 uid=\"uid://sfb1fw8j6ufu\"]\n\n[ext_resource type=\"Script\" uid=\"uid://slt6m4nclynh\" path=\"re"
},
{
"path": "addons/gut/gui/arrow.png.import",
"chars": 763,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://6wra5rxmfsrl\"\npath=\"res://.godot/imported/arrow.png-2b"
},
{
"path": "addons/gut/gui/editor_globals.gd",
"chars": 1787,
"preview": "@tool\n\nstatic var GutUserPreferences = load(\"res://addons/gut/gui/gut_user_preferences.gd\")\nstatic var temp_directory = "
},
{
"path": "addons/gut/gui/editor_globals.gd.uid",
"chars": 19,
"preview": "uid://f5ak18bwmj26\n"
},
{
"path": "addons/gut/gui/gut_config_gui.gd",
"chars": 11599,
"preview": "var PanelControls = load(\"res://addons/gut/gui/panel_controls.gd\")\nvar GutConfig = load('res://addons/gut/gut_config.gd'"
},
{
"path": "addons/gut/gui/gut_config_gui.gd.uid",
"chars": 20,
"preview": "uid://brrhd3umxj5ua\n"
},
{
"path": "addons/gut/gui/gut_gui.gd",
"chars": 5682,
"preview": "extends Control\n# ##############################################################################\n# This is the decoupled"
},
{
"path": "addons/gut/gui/gut_gui.gd.uid",
"chars": 20,
"preview": "uid://duu660pjff8cv\n"
},
{
"path": "addons/gut/gui/gut_user_preferences.gd",
"chars": 2350,
"preview": "class GutEditorPref:\n\tvar gut_pref_prefix = 'gut/'\n\tvar pname = '__not_set__'\n\tvar default = null\n\tvar value = '__not_se"
},
{
"path": "addons/gut/gui/gut_user_preferences.gd.uid",
"chars": 20,
"preview": "uid://ce34uxa87kx45\n"
},
{
"path": "addons/gut/gui/panel_controls.gd",
"chars": 11179,
"preview": "# ------------------------------------------------------------------------------\n# -------------------------------------"
},
{
"path": "addons/gut/gui/panel_controls.gd.uid",
"chars": 20,
"preview": "uid://bqf8edf4yh5l2\n"
},
{
"path": "addons/gut/gui/play.png.import",
"chars": 761,
"preview": "[remap]\n\nimporter=\"texture\"\ntype=\"CompressedTexture2D\"\nuid=\"uid://cr6tvdv0ve6cv\"\npath=\"res://.godot/imported/play.png-5c"
},
{
"path": "addons/gut/gui/script_text_editor_controls.gd",
"chars": 6722,
"preview": "# Holds weakrefs to a ScriptTextEditor and related children nodes\n# that might be useful. Though the CodeEdit is really"
},
{
"path": "addons/gut/gui/script_text_editor_controls.gd.uid",
"chars": 19,
"preview": "uid://6xa0oyal4g4j\n"
},
{
"path": "addons/gut/gut.gd",
"chars": 42362,
"preview": "extends 'res://addons/gut/gut_to_move.gd'\nclass_name GutMain\n\n# ########################################################"
},
{
"path": "addons/gut/gut.gd.uid",
"chars": 20,
"preview": "uid://dr1jwb3s6t8cc\n"
},
{
"path": "addons/gut/gut_cmdln.gd",
"chars": 2438,
"preview": "# ------------------------------------------------------------------------------\n# Description\n# -----------\n# Command l"
}
]
// ... and 77 more files (download for full content)
About this extraction
This page contains the full source code of the KoBeWi/Godot-Project-Builds GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 277 files (832.2 KB), approximately 259.3k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.