├── .deploy_to_steamos
├── devices.json
└── settings.json
├── .gitattributes
├── .idea
└── .idea.Godot4-DeployToSteamOS
│ └── .idea
│ ├── encodings.xml
│ ├── vcs.xml
│ ├── indexLayout.xml
│ └── .gitignore
├── addons
└── deploy_to_steamos
│ ├── plugin.cfg
│ ├── folder.svg
│ ├── SettingsFile.cs
│ ├── icon.svg
│ ├── deploy_window
│ ├── DeployWindow.PrepareUpload.cs
│ ├── DeployWindow.Upload.cs
│ ├── DeployWindow.CreateShortcut.cs
│ ├── DeployWindow.Build.cs
│ ├── DeployWindow.Init.cs
│ ├── DeployWindow.cs
│ └── deploy_window.tscn
│ ├── icon.svg.import
│ ├── folder.svg.import
│ ├── Plugin.cs
│ ├── add_device_window
│ ├── DeviceItemPrefab.cs
│ ├── device_item_prefab.tscn
│ ├── AddDeviceWindow.cs
│ └── add_device_window.tscn
│ ├── GodotExportManager.cs
│ ├── deploy_dock
│ ├── deploy_dock.tscn
│ └── DeployDock.cs
│ ├── SettingsManager.cs
│ ├── settings_panel
│ ├── SettingsPanel.cs
│ └── settings_panel.tscn
│ └── SteamOSDevkitManager.cs
├── demo
├── DemoMovement.cs
└── demo.tscn
├── .gitignore
├── Godot4-DeployToSteamOS.csproj
├── logo_dark.svg
├── logo_bright.svg
├── project.godot
├── icon.svg
├── icon.svg.import
├── logo_dark.svg.import
├── logo_bright.svg.import
├── LICENSE
├── Godot4-DeployToSteamOS.sln
└── README.md
/.deploy_to_steamos/devices.json:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------
/.deploy_to_steamos/settings.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Normalize EOL for all files that Git considers text files.
2 | * text=auto eol=lf
3 |
--------------------------------------------------------------------------------
/.idea/.idea.Godot4-DeployToSteamOS/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/.idea.Godot4-DeployToSteamOS/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="Deploy to SteamOS"
4 | description="A Godot plugin integrating a direct deployment workflow to SteamOS"
5 | author="Laura Sofia Heimann"
6 | version="1.1.0"
7 | script="Plugin.cs"
8 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/folder.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.idea/.idea.Godot4-DeployToSteamOS/.idea/indexLayout.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/demo/DemoMovement.cs:
--------------------------------------------------------------------------------
1 | using Godot;
2 |
3 | public partial class DemoMovement : CharacterBody2D
4 | {
5 | public const float Speed = 300.0f;
6 |
7 | public override void _PhysicsProcess(double delta)
8 | {
9 | var direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
10 |
11 | Velocity = direction * Speed;
12 | MoveAndSlide();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.idea/.idea.Godot4-DeployToSteamOS/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Rider ignored files
5 | /.idea.Godot4-DeployToSteamOS.iml
6 | /modules.xml
7 | /contentModel.xml
8 | /projectSettingsUpdater.xml
9 | # Editor-based HTTP Client requests
10 | /httpRequests/
11 | # Datasource local storage ignored files
12 | /dataSources/
13 | /dataSources.local.xml
14 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/SettingsFile.cs:
--------------------------------------------------------------------------------
1 | public class SettingsFile
2 | {
3 | public enum UploadMethods
4 | {
5 | Differential,
6 | Incremental,
7 | CleanReplace
8 | }
9 |
10 | public string BuildPath { get; set; } = "";
11 | public string StartParameters { get; set; } = "";
12 | public UploadMethods UploadMethod { get; set; } = UploadMethods.Differential;
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Godot 4+ specific ignores
2 | .godot/
3 |
4 | # Godot-specific ignores
5 | .import/
6 | export.cfg
7 | export_presets.cfg
8 | # Dummy HTML5 export presets file for continuous integration
9 | !.github/dist/export_presets.cfg
10 |
11 | # Imported translations (automatically generated from CSV files)
12 | *.translation
13 |
14 | # Mono-specific ignores
15 | .mono/
16 | data_*/
17 | mono_crash.*.json
18 |
19 | # System/tool-specific ignores
20 | .directory
21 | .DS_Store
22 | *~
23 | *.blend1
--------------------------------------------------------------------------------
/Godot4-DeployToSteamOS.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0
4 | true
5 | Godot4DeployToSteamOS
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/logo_dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/logo_bright.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/project.godot:
--------------------------------------------------------------------------------
1 | ; Engine configuration file.
2 | ; It's best edited using the editor UI and not directly,
3 | ; since the parameters that go here are not all obvious.
4 | ;
5 | ; Format:
6 | ; [section] ; section goes between []
7 | ; param=value ; assign values to parameters
8 |
9 | config_version=5
10 |
11 | [application]
12 |
13 | config/name="Godot4-DeployToSteamOS"
14 | run/main_scene="res://demo/demo.tscn"
15 | config/features=PackedStringArray("4.2", "C#", "Forward Plus")
16 | config/icon="res://icon.svg"
17 |
18 | [autoload]
19 |
20 | SettingsManager="*res://addons/deploy_to_steamos/SettingsManager.cs"
21 |
22 | [dotnet]
23 |
24 | project/assembly_name="Godot4-DeployToSteamOS"
25 |
26 | [editor_plugins]
27 |
28 | enabled=PackedStringArray("res://addons/deploy_to_steamos/plugin.cfg")
29 |
--------------------------------------------------------------------------------
/demo/demo.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=4 format=3 uid="uid://bvc31agshb0el"]
2 |
3 | [ext_resource type="Texture2D" uid="uid://pi1k2s1q78mt" path="res://icon.svg" id="1_8o1ex"]
4 | [ext_resource type="Script" path="res://demo/DemoMovement.cs" id="1_vnpjs"]
5 |
6 | [sub_resource type="RectangleShape2D" id="RectangleShape2D_kxjvv"]
7 | size = Vector2(64, 64)
8 |
9 | [node name="Demo" type="Node2D"]
10 |
11 | [node name="CharacterBody2D" type="CharacterBody2D" parent="."]
12 | script = ExtResource("1_vnpjs")
13 |
14 | [node name="Sprite2D" type="Sprite2D" parent="CharacterBody2D"]
15 | scale = Vector2(0.5, 0.5)
16 | texture = ExtResource("1_8o1ex")
17 |
18 | [node name="CollisionShape2D" type="CollisionShape2D" parent="CharacterBody2D"]
19 | shape = SubResource("RectangleShape2D_kxjvv")
20 |
21 | [node name="Camera2D" type="Camera2D" parent="."]
22 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/deploy_window/DeployWindow.PrepareUpload.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Godot;
3 |
4 | namespace Laura.DeployToSteamOS;
5 |
6 | public partial class DeployWindow
7 | {
8 | private async Task DeployPrepareUpload(Callable logCallable)
9 | {
10 | CurrentStep = DeployStep.PrepareUpload;
11 | CurrentProgress = StepProgress.Running;
12 | UpdateUI();
13 |
14 | _prepareUploadResult = await SteamOSDevkitManager.PrepareUpload(
15 | _device,
16 | _gameId,
17 | logCallable
18 | );
19 |
20 | AddToConsole(DeployStep.PrepareUpload, $"User: {_prepareUploadResult.User}");
21 | AddToConsole(DeployStep.PrepareUpload, $"Directory: {_prepareUploadResult.Directory}");
22 |
23 | CurrentProgress = StepProgress.Succeeded;
24 | UpdateUI();
25 | }
26 | }
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/deploy_window/DeployWindow.Upload.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Godot;
4 |
5 | namespace Laura.DeployToSteamOS;
6 |
7 | public partial class DeployWindow
8 | {
9 | private async Task DeployUpload(Callable logCallable)
10 | {
11 | CurrentStep = DeployStep.Uploading;
12 | CurrentProgress = StepProgress.Running;
13 | UpdateUI();
14 |
15 | try
16 | {
17 | await SteamOSDevkitManager.CopyFiles(
18 | _device,
19 | _localPath,
20 | _prepareUploadResult.Directory,
21 | logCallable
22 | );
23 | }
24 | catch (Exception e)
25 | {
26 | AddToConsole(DeployStep.Uploading, e.Message);
27 | UpdateUIToFail();
28 | }
29 |
30 | CurrentProgress = StepProgress.Succeeded;
31 | UpdateUI();
32 | }
33 | }
--------------------------------------------------------------------------------
/icon.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://pi1k2s1q78mt"
6 | path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://icon.svg"
14 | dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/logo_dark.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://dpr1rtge0xy5v"
6 | path="res://.godot/imported/logo_dark.svg-0c05ef3109fb7de9ee85ce1a9d19bc9c.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://logo_dark.svg"
14 | dest_files=["res://.godot/imported/logo_dark.svg-0c05ef3109fb7de9ee85ce1a9d19bc9c.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/logo_bright.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://bjlqpiaahmp37"
6 | path="res://.godot/imported/logo_bright.svg-47959f1b08e8558c95e0c938aba01ca7.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://logo_bright.svg"
14 | dest_files=["res://.godot/imported/logo_bright.svg-47959f1b08e8558c95e0c938aba01ca7.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/icon.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://s1tcpal4iir4"
6 | path="res://.godot/imported/icon.svg-3fd3169593c3fd142e77475bb0bb8a61.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/deploy_to_steamos/icon.svg"
14 | dest_files=["res://.godot/imported/icon.svg-3fd3169593c3fd142e77475bb0bb8a61.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/folder.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="CompressedTexture2D"
5 | uid="uid://du661vtsmqc7h"
6 | path="res://.godot/imported/folder.svg-3209c1ff45131a205cbeb3923822ebc7.ctex"
7 | metadata={
8 | "vram_texture": false
9 | }
10 |
11 | [deps]
12 |
13 | source_file="res://addons/deploy_to_steamos/folder.svg"
14 | dest_files=["res://.godot/imported/folder.svg-3209c1ff45131a205cbeb3923822ebc7.ctex"]
15 |
16 | [params]
17 |
18 | compress/mode=0
19 | compress/high_quality=false
20 | compress/lossy_quality=0.7
21 | compress/hdr_compression=1
22 | compress/normal_map=0
23 | compress/channel_pack=0
24 | mipmaps/generate=false
25 | mipmaps/limit=-1
26 | roughness/mode=0
27 | roughness/src_normal=""
28 | process/fix_alpha_border=true
29 | process/premult_alpha=false
30 | process/normal_map_invert_y=false
31 | process/hdr_as_srgb=false
32 | process/hdr_clamp_exposure=false
33 | process/size_limit=0
34 | detect_3d/compress_to=1
35 | svg/scale=1.0
36 | editor/scale_with_editor_scale=false
37 | editor/convert_colors_with_editor_theme=false
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Laura Sofia Heimann
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Godot4-DeployToSteamOS.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio 2012
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Godot4-DeployToSteamOS", "Godot4-DeployToSteamOS.csproj", "{5687FD30-1FDF-475D-B3F5-B203D0656BAB}"
4 | EndProject
5 | Global
6 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
7 | Debug|Any CPU = Debug|Any CPU
8 | ExportDebug|Any CPU = ExportDebug|Any CPU
9 | ExportRelease|Any CPU = ExportRelease|Any CPU
10 | EndGlobalSection
11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
12 | {5687FD30-1FDF-475D-B3F5-B203D0656BAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
13 | {5687FD30-1FDF-475D-B3F5-B203D0656BAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
14 | {5687FD30-1FDF-475D-B3F5-B203D0656BAB}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
15 | {5687FD30-1FDF-475D-B3F5-B203D0656BAB}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
16 | {5687FD30-1FDF-475D-B3F5-B203D0656BAB}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
17 | {5687FD30-1FDF-475D-B3F5-B203D0656BAB}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
18 | EndGlobalSection
19 | EndGlobal
20 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/Plugin.cs:
--------------------------------------------------------------------------------
1 | #if TOOLS
2 | using Godot;
3 |
4 | namespace Laura.DeployToSteamOS;
5 |
6 | [Tool]
7 | public partial class Plugin : EditorPlugin
8 | {
9 | private Control _dock;
10 | private Control _settingsPanel;
11 |
12 | public override void _EnterTree()
13 | {
14 | AddAutoloadSingleton("SettingsManager", "res://addons/deploy_to_steamos/SettingsManager.cs");
15 |
16 | _dock = GD.Load("res://addons/deploy_to_steamos/deploy_dock/deploy_dock.tscn").Instantiate();
17 | _settingsPanel = GD.Load("res://addons/deploy_to_steamos/settings_panel/settings_panel.tscn").Instantiate();
18 |
19 | AddControlToContainer(CustomControlContainer.Toolbar, _dock);
20 | AddControlToContainer(CustomControlContainer.ProjectSettingTabRight, _settingsPanel);
21 | }
22 |
23 | public override void _ExitTree()
24 | {
25 | RemoveControlFromContainer(CustomControlContainer.Toolbar, _dock);
26 | RemoveControlFromContainer(CustomControlContainer.ProjectSettingTabRight, _settingsPanel);
27 |
28 | RemoveAutoloadSingleton("SettingsManager");
29 |
30 | _dock.Free();
31 | }
32 | }
33 | #endif
34 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/add_device_window/DeviceItemPrefab.cs:
--------------------------------------------------------------------------------
1 | using Godot;
2 |
3 | namespace Laura.DeployToSteamOS;
4 |
5 | [Tool]
6 | public partial class DeviceItemPrefab : PanelContainer
7 | {
8 | private SteamOSDevkitManager.Device _device;
9 |
10 | public delegate void DeviceDelegate(SteamOSDevkitManager.Device device);
11 | public event DeviceDelegate OnDevicePair;
12 | public event DeviceDelegate OnDeviceUnpair;
13 |
14 | [ExportGroup("References")]
15 | [Export] private Label _deviceNameLabel;
16 | [Export] private Label _deviceConnectionLabel;
17 | [Export] private Button _devicePairButton;
18 | [Export] private Button _deviceUnpairButton;
19 |
20 | public void SetUI(SteamOSDevkitManager.Device device)
21 | {
22 | _device = device;
23 | _deviceNameLabel.Text = device.DisplayName;
24 | _deviceConnectionLabel.Text = $"{device.Login}@{device.IPAdress}";
25 |
26 | if (SettingsManager.Instance.Devices.Exists(x => x.IPAdress == device.IPAdress && x.Login == device.Login))
27 | {
28 | _devicePairButton.Visible = false;
29 | _deviceUnpairButton.Visible = true;
30 | }
31 | }
32 |
33 | public void Pair()
34 | {
35 | OnDevicePair?.Invoke(_device);
36 | }
37 |
38 | public void Unpair()
39 | {
40 | OnDeviceUnpair?.Invoke(_device);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/deploy_window/DeployWindow.CreateShortcut.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Threading.Tasks;
3 | using Godot;
4 |
5 | namespace Laura.DeployToSteamOS;
6 |
7 | public partial class DeployWindow
8 | {
9 | private async Task DeployCreateShortcut(Callable logCallable)
10 | {
11 | CurrentStep = DeployStep.CreateShortcut;
12 | CurrentProgress = StepProgress.Running;
13 | UpdateUI();
14 |
15 | var createShortcutParameters = new SteamOSDevkitManager.CreateShortcutParameters
16 | {
17 | gameid = _gameId,
18 | directory = _prepareUploadResult.Directory,
19 | argv = new[] { "game.x86_64", SettingsManager.Instance.Settings.StartParameters },
20 | settings = new Dictionary
21 | {
22 | { "steam_play", "0" }
23 | },
24 | };
25 |
26 | // TODO: Fix Result, success/error are not filled in response but exist/dont
27 | _createShortcutResult = await SteamOSDevkitManager.CreateShortcut(
28 | _device,
29 | createShortcutParameters,
30 | logCallable
31 | );
32 |
33 | CurrentProgress = StepProgress.Succeeded;
34 | UpdateUI();
35 | }
36 | }
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/deploy_window/DeployWindow.Build.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Threading.Tasks;
3 | using Godot;
4 |
5 | namespace Laura.DeployToSteamOS;
6 |
7 | public partial class DeployWindow
8 | {
9 | private async Task DeployBuild(Callable logCallable)
10 | {
11 | CurrentStep = DeployStep.Building;
12 | CurrentProgress = StepProgress.Running;
13 | UpdateUI();
14 |
15 | // Adding a 2 second delay so the UI can update
16 | await ToSignal(GetTree().CreateTimer(1), "timeout");
17 | var buildTask = new TaskCompletionSource();
18 |
19 | if (SettingsManager.Instance.Settings.UploadMethod == SettingsFile.UploadMethods.CleanReplace)
20 | {
21 | AddToConsole(DeployStep.Building, "Removing previous build as upload method is set to CleanReplace");
22 | await SteamOSDevkitManager.RunSSHCommand(_device, "python3 ~/devkit-utils/steamos-delete --delete-title " + _gameId, logCallable);
23 | }
24 |
25 | GodotExportManager.ExportProject(
26 | ProjectSettings.GlobalizePath("res://"),
27 | Path.Join(_localPath, "game.x86_64"),
28 | false,
29 | logCallable,
30 | () => { buildTask.SetResult(true); }
31 | );
32 |
33 | await buildTask.Task;
34 |
35 | CurrentProgress = StepProgress.Succeeded;
36 | UpdateUI();
37 | }
38 | }
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/deploy_window/DeployWindow.Init.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text.RegularExpressions;
4 | using System.Threading.Tasks;
5 | using Godot;
6 |
7 | namespace Laura.DeployToSteamOS;
8 |
9 | public partial class DeployWindow
10 | {
11 | private async Task DeployInit()
12 | {
13 | CurrentStep = DeployStep.Init;
14 | CurrentProgress = StepProgress.Running;
15 | UpdateUI();
16 |
17 | await Task.Delay(0);
18 |
19 | _gameId = ProjectSettings.GetSetting("application/config/name", "game").AsString();
20 | _gameId = Regex.Replace(_gameId, @"[^a-zA-Z0-9]", string.Empty);
21 |
22 | // Add current timestamp to gameid for incremental builds
23 | if (SettingsManager.Instance.Settings.UploadMethod == SettingsFile.UploadMethods.Incremental)
24 | {
25 | _gameId += "_" + DateTimeOffset.Now.ToUnixTimeSeconds();
26 | }
27 |
28 | GD.Print($"[DeployToSteamOS] Deploying '{_gameId}' to '{_device.DisplayName} ({_device.Login}@{_device.IPAdress})'");
29 |
30 | _localPath = SettingsManager.Instance.Settings.BuildPath;
31 | if (DirAccess.Open(_localPath) == null)
32 | {
33 | GD.PrintErr($"[DeployToSteamOS] Build path '{_localPath}' does not exist.");
34 | UpdateUIToFail();
35 | return;
36 | }
37 |
38 | CurrentProgress = StepProgress.Succeeded;
39 | UpdateUI();
40 | }
41 | }
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/GodotExportManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using Godot;
4 |
5 | namespace Laura.DeployToSteamOS;
6 |
7 | public class GodotExportManager
8 | {
9 | public static void ExportProject(
10 | string projectPath,
11 | string outputPath,
12 | bool isReleaseBuild,
13 | Callable logCallable,
14 | Action OnProcessExited = null)
15 | {
16 | logCallable.CallDeferred("Starting project export, this may take a while.");
17 | logCallable.CallDeferred($"Output path: {outputPath}");
18 | logCallable.CallDeferred($"Is Release Build: {(isReleaseBuild ? "Yes" : "No")}");
19 |
20 | Process exportProcess = new Process();
21 |
22 | exportProcess.StartInfo.FileName = OS.GetExecutablePath();
23 |
24 | var arguments = $"--headless --path \"{projectPath}\" ";
25 | arguments += isReleaseBuild ? "--export-release " : "--export-debug ";
26 | arguments += "\"Steamdeck\" ";
27 | arguments += $"\"{outputPath}\"";
28 |
29 | exportProcess.StartInfo.Arguments = arguments;
30 | exportProcess.StartInfo.UseShellExecute = false;
31 | exportProcess.StartInfo.RedirectStandardOutput = true;
32 | exportProcess.EnableRaisingEvents = true;
33 |
34 | exportProcess.ErrorDataReceived += (sender, args) =>
35 | {
36 | throw new Exception("Error while building project: " + args.Data);
37 | };
38 |
39 | exportProcess.OutputDataReceived += (sender, args) =>
40 | {
41 | logCallable.CallDeferred(args.Data);
42 | };
43 |
44 | exportProcess.Exited += (sender, args) =>
45 | {
46 | OnProcessExited?.Invoke();
47 | exportProcess.WaitForExit();
48 | };
49 |
50 | exportProcess.Start();
51 | exportProcess.BeginOutputReadLine();
52 | }
53 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Godot 4 DeployToSteamOS
2 | A Godot 4 addon to deploy your game to your SteamOS devkit devices with one click
3 |
4 | ## Setup
5 | **Due to external dependencies, this addon is only compatible with Godot 4 C# for the time being**
6 |
7 | - Install the following NuGet packages
8 | - SSH.NET (Version 2023.0.0)
9 | - Zeroconf (Version 3.6.11)
10 | - Add the `deploy_to_steamos` folder to your project
11 | - Build your project
12 | - Enable the addon and define a build path in the `Deploy to SteamOS` project settings panel
13 | - Create a new `Linux/X11` export preset and name it `Steamdeck`
14 | - **Optional:** You can exclude this addon in your export by selecting `Export all resources in the project except resources checked below` under `Resources` -> `Export Mode` and checking the `deploy_to_steamos` folder
15 | - **Optional:** You can add the created `.deploy_to_steamos` folder to your `.gitignore` if you do not want to commit changes to the settings to your git repo
16 |
17 | ## Usage
18 | After the initial setup, you can open the dropdown on the top right and click on `Add devkit device` to pair with a new SteamOS devkit device.
19 |
20 | **Please keep in mind that you need to connect to the device at least once before you can pair it**
21 |
22 | ## FAQ
23 | ### Why do I need to connect through the official SteamOS devkit client once?
24 | Only the official SteamOS devkit client can establish the initial connection, install the devkit tools on your devkit device and create the necessary connection keys for the time being.
25 |
26 | ### Why is this addon only compatible with the C# build of Godot?
27 | This project uses external libraries for establishing SSH connections and scanning for devices on your network that aren't accessible through GDScript only.
28 |
29 | ### The deployment stops after "Uploading files"
30 | The most common cause for this problem is a build error of your project or your project missing the "Steamdeck" Linux export preset.
31 |
32 | ## Donations
33 | If you would like to support my open source projects, feel free to [drop me a coffee (or rather, an energy drink)](https://ko-fi.com/laurasofiaheimann) or check out my [release games](https://indiegesindel.itch.io) on itch.
34 |
35 | ## License
36 | This project is licensed under the (MIT License)[LICENSE].
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/deploy_dock/deploy_dock.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=6 format=3 uid="uid://kdbpdei4v1ub"]
2 |
3 | [ext_resource type="Script" path="res://addons/deploy_to_steamos/deploy_dock/DeployDock.cs" id="1_atc6e"]
4 | [ext_resource type="Texture2D" uid="uid://s1tcpal4iir4" path="res://addons/deploy_to_steamos/icon.svg" id="2_rpj28"]
5 | [ext_resource type="PackedScene" uid="uid://6be5afncp3nr" path="res://addons/deploy_to_steamos/add_device_window/add_device_window.tscn" id="3_qfyb3"]
6 | [ext_resource type="PackedScene" uid="uid://ds2umdqybfls8" path="res://addons/deploy_to_steamos/deploy_window/deploy_window.tscn" id="4_8y8co"]
7 |
8 | [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ebqha"]
9 | content_margin_left = 10.0
10 |
11 | [node name="DeployDock" type="PanelContainer" node_paths=PackedStringArray("_deployTargetButton", "_deployButton", "_addDeviceWindow", "_deployWindow")]
12 | offset_right = 337.0
13 | offset_bottom = 32.0
14 | size_flags_horizontal = 3
15 | size_flags_vertical = 3
16 | theme_override_styles/panel = SubResource("StyleBoxEmpty_ebqha")
17 | script = ExtResource("1_atc6e")
18 | _deployTargetButton = NodePath("HBoxContainer/DeployTargetButton")
19 | _deployButton = NodePath("HBoxContainer/DeployButton")
20 | _addDeviceWindow = NodePath("AddDeviceWindow")
21 | _deployWindow = NodePath("DeployWindow")
22 |
23 | [node name="HBoxContainer" type="HBoxContainer" parent="."]
24 | layout_mode = 2
25 |
26 | [node name="DeployTargetButton" type="OptionButton" parent="HBoxContainer"]
27 | layout_mode = 2
28 | size_flags_horizontal = 3
29 | item_count = 3
30 | selected = 0
31 | popup/item_0/text = "None"
32 | popup/item_0/id = 10000
33 | popup/item_1/text = ""
34 | popup/item_1/id = -1
35 | popup/item_1/separator = true
36 | popup/item_2/text = "Add devkit device"
37 | popup/item_2/id = 10001
38 |
39 | [node name="DeployButton" type="Button" parent="HBoxContainer"]
40 | custom_minimum_size = Vector2(32, 32)
41 | layout_mode = 2
42 | disabled = true
43 | icon = ExtResource("2_rpj28")
44 | icon_alignment = 1
45 | expand_icon = true
46 |
47 | [node name="AddDeviceWindow" parent="." instance=ExtResource("3_qfyb3")]
48 | visible = false
49 |
50 | [node name="DeployWindow" parent="." instance=ExtResource("4_8y8co")]
51 | visible = false
52 |
53 | [connection signal="item_selected" from="HBoxContainer/DeployTargetButton" to="." method="OnDeployTargetItemSelected"]
54 | [connection signal="pressed" from="HBoxContainer/DeployButton" to="." method="Deploy"]
55 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/add_device_window/device_item_prefab.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=3 format=3 uid="uid://p64nkj5ii2xt"]
2 |
3 | [ext_resource type="Script" path="res://addons/deploy_to_steamos/add_device_window/DeviceItemPrefab.cs" id="1_q77lw"]
4 |
5 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6g2hd"]
6 | content_margin_left = 15.0
7 | content_margin_top = 10.0
8 | content_margin_right = 15.0
9 | content_margin_bottom = 10.0
10 | bg_color = Color(0.133333, 0.133333, 0.133333, 1)
11 | corner_radius_top_left = 4
12 | corner_radius_top_right = 4
13 | corner_radius_bottom_right = 4
14 | corner_radius_bottom_left = 4
15 |
16 | [node name="DeviceItemPrefab" type="PanelContainer" node_paths=PackedStringArray("_deviceNameLabel", "_deviceConnectionLabel", "_devicePairButton", "_deviceUnpairButton")]
17 | offset_right = 532.0
18 | offset_bottom = 51.0
19 | theme_override_styles/panel = SubResource("StyleBoxFlat_6g2hd")
20 | script = ExtResource("1_q77lw")
21 | _deviceNameLabel = NodePath("HBoxContainer/VBoxContainer/DeviceNameLabel")
22 | _deviceConnectionLabel = NodePath("HBoxContainer/VBoxContainer/DeviceConnectionLabel")
23 | _devicePairButton = NodePath("HBoxContainer/PairButton")
24 | _deviceUnpairButton = NodePath("HBoxContainer/UnpairButton")
25 |
26 | [node name="HBoxContainer" type="HBoxContainer" parent="."]
27 | layout_mode = 2
28 | size_flags_horizontal = 3
29 | theme_override_constants/separation = 5
30 | alignment = 1
31 |
32 | [node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"]
33 | layout_mode = 2
34 | size_flags_horizontal = 3
35 | theme_override_constants/separation = -5
36 |
37 | [node name="DeviceNameLabel" type="Label" parent="HBoxContainer/VBoxContainer"]
38 | layout_mode = 2
39 | size_flags_horizontal = 3
40 | text = "steamdeck"
41 |
42 | [node name="DeviceConnectionLabel" type="Label" parent="HBoxContainer/VBoxContainer"]
43 | layout_mode = 2
44 | size_flags_horizontal = 3
45 | theme_override_colors/font_color = Color(1, 1, 1, 0.490196)
46 | theme_override_font_sizes/font_size = 12
47 | text = "deck@127.0.0.1"
48 |
49 | [node name="PairButton" type="Button" parent="HBoxContainer"]
50 | layout_mode = 2
51 | size_flags_vertical = 4
52 | theme_override_constants/icon_max_width = 20
53 | text = "Pair"
54 |
55 | [node name="UnpairButton" type="Button" parent="HBoxContainer"]
56 | visible = false
57 | layout_mode = 2
58 | size_flags_vertical = 4
59 | theme_override_constants/icon_max_width = 20
60 | text = "Unpair"
61 |
62 | [connection signal="pressed" from="HBoxContainer/PairButton" to="." method="Pair"]
63 | [connection signal="pressed" from="HBoxContainer/UnpairButton" to="." method="Unpair"]
64 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/SettingsManager.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Text.Json;
3 | using Godot;
4 |
5 | namespace Laura.DeployToSteamOS;
6 |
7 | [Tool]
8 | public partial class SettingsManager : Node
9 | {
10 | public static SettingsManager Instance;
11 |
12 | private const string FolderPath = "res://.deploy_to_steamos";
13 | private const string SettingsPath = FolderPath + "/settings.json";
14 | private const string DevicesPath = FolderPath + "/devices.json";
15 |
16 | public bool IsDirty = false;
17 | public SettingsFile Settings = new();
18 | public List Devices = new();
19 |
20 | public override void _EnterTree()
21 | {
22 | Instance = this;
23 |
24 | Load();
25 | }
26 |
27 | public void Load()
28 | {
29 | if (!FileAccess.FileExists(SettingsPath))
30 | {
31 | Settings = new SettingsFile();
32 | IsDirty = true;
33 | }
34 | else
35 | {
36 | using var settingsFile = FileAccess.Open(SettingsPath, FileAccess.ModeFlags.Read);
37 | var settingsFileContent = settingsFile.GetAsText();
38 |
39 | Settings = JsonSerializer.Deserialize(settingsFileContent, DefaultSerializerOptions);
40 |
41 | // Failsafe if settings file is corrupt
42 | if (Settings == null)
43 | {
44 | Settings = new SettingsFile();
45 | IsDirty = true;
46 | }
47 | }
48 |
49 | if (!FileAccess.FileExists(SettingsPath))
50 | {
51 | Devices = new List();
52 | IsDirty = true;
53 | }
54 | else
55 | {
56 | using var devicesFile = FileAccess.Open(DevicesPath, FileAccess.ModeFlags.Read);
57 | var devicesFileContent = devicesFile.GetAsText();
58 |
59 | Devices = JsonSerializer.Deserialize>(devicesFileContent, DefaultSerializerOptions);
60 |
61 | // Failsafe if device file is corrupt
62 | if (Devices == null)
63 | {
64 | Devices = new List();
65 | IsDirty = true;
66 | }
67 | }
68 |
69 | if (IsDirty)
70 | {
71 | Save();
72 | }
73 | }
74 |
75 | public void Save()
76 | {
77 | // Check if directory exists and create if it doesn't
78 | var dirAccess = DirAccess.Open(FolderPath);
79 | if (dirAccess == null)
80 | {
81 | DirAccess.MakeDirRecursiveAbsolute(FolderPath);
82 | }
83 |
84 | // Save Settings
85 | var jsonSettings = JsonSerializer.Serialize(Settings);
86 | using var settingsFile = FileAccess.Open(SettingsPath, FileAccess.ModeFlags.Write);
87 | settingsFile.StoreString(jsonSettings);
88 |
89 | // Save Devices
90 | var jsonDevices = JsonSerializer.Serialize(Devices);
91 | using var devicesFile = FileAccess.Open(DevicesPath, FileAccess.ModeFlags.Write);
92 | devicesFile.StoreString(jsonDevices);
93 |
94 | IsDirty = false;
95 | }
96 |
97 | public static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions
98 | {
99 | PropertyNameCaseInsensitive = true,
100 | };
101 | }
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/settings_panel/SettingsPanel.cs:
--------------------------------------------------------------------------------
1 | using Godot;
2 |
3 | namespace Laura.DeployToSteamOS;
4 |
5 | [Tool]
6 | public partial class SettingsPanel : PanelContainer
7 | {
8 | [ExportGroup("References")]
9 | [Export] private Label _versionLabel;
10 | [Export] private AddDeviceWindow _addDeviceWindow;
11 | [Export] private LineEdit _buildPathLineEdit;
12 | [Export] private FileDialog _buildPathFileDialog;
13 | [Export] private LineEdit _startParametersLineEdit;
14 | [Export] private OptionButton _uploadMethodOptionButton;
15 | [Export] private Label _uploadMethodDifferentialHintLabel;
16 | [Export] private Label _uploadMethodIncrementalHintLabel;
17 | [Export] private Label _uploadMethodCleanReplaceHintLabel;
18 |
19 | private float _saveCooldown = 2f;
20 |
21 | public override void _Process(double delta)
22 | {
23 | if (SettingsManager.Instance.IsDirty && _saveCooldown < 0f)
24 | {
25 | _saveCooldown = 2f;
26 | SettingsManager.Instance.Save();
27 | }
28 |
29 | if (SettingsManager.Instance.IsDirty && Visible)
30 | {
31 | _saveCooldown -= (float)delta;
32 | }
33 | }
34 |
35 | public void OnVisibilityChanged()
36 | {
37 | if (Visible)
38 | {
39 | ShowPanel();
40 | }
41 | else
42 | {
43 | HidePanel();
44 | }
45 | }
46 |
47 | private void ShowPanel()
48 | {
49 | _buildPathLineEdit.Text = SettingsManager.Instance.Settings.BuildPath;
50 | _startParametersLineEdit.Text = SettingsManager.Instance.Settings.StartParameters;
51 | _uploadMethodOptionButton.Selected = (int)SettingsManager.Instance.Settings.UploadMethod;
52 |
53 | _saveCooldown = 2f;
54 | }
55 |
56 | private void HidePanel()
57 | {
58 | }
59 |
60 | public void BuildPathTextChanged(string newBuildPath)
61 | {
62 | SettingsManager.Instance.Settings.BuildPath = newBuildPath;
63 | _buildPathLineEdit.Text = newBuildPath;
64 |
65 | _saveCooldown = 2f;
66 | SettingsManager.Instance.IsDirty = true;
67 | }
68 |
69 | public void BuildPathOpenFileDialog()
70 | {
71 | _buildPathFileDialog.Show();
72 | }
73 |
74 | public void StartParametersTextChanged(string newStartParameters)
75 | {
76 | SettingsManager.Instance.Settings.StartParameters = newStartParameters;
77 | _startParametersLineEdit.Text = newStartParameters;
78 |
79 | _saveCooldown = 2f;
80 | SettingsManager.Instance.IsDirty = true;
81 | }
82 |
83 | public void UploadMethodItemSelected(int newItemIndex)
84 | {
85 | var newUploadMethod = (SettingsFile.UploadMethods)newItemIndex;
86 | SettingsManager.Instance.Settings.UploadMethod = newUploadMethod;
87 | _uploadMethodOptionButton.Selected = newItemIndex;
88 |
89 | _saveCooldown = 2f;
90 | SettingsManager.Instance.IsDirty = true;
91 |
92 | _uploadMethodDifferentialHintLabel.Visible = newUploadMethod == SettingsFile.UploadMethods.Differential;
93 | _uploadMethodIncrementalHintLabel.Visible = newUploadMethod == SettingsFile.UploadMethods.Incremental;
94 | _uploadMethodCleanReplaceHintLabel.Visible = newUploadMethod == SettingsFile.UploadMethods.CleanReplace;
95 | }
96 |
97 | public void PairDevices()
98 | {
99 | _addDeviceWindow.Show();
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/add_device_window/AddDeviceWindow.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Godot;
3 |
4 | namespace Laura.DeployToSteamOS;
5 |
6 | [Tool]
7 | public partial class AddDeviceWindow : Window
8 | {
9 | private bool _isVisible;
10 | private bool _isUpdatingDevices;
11 | private float _scanCooldown;
12 | private List _scannedDevices = new();
13 | private List _pairedDevices = new();
14 |
15 | public delegate void DevicesChangedDelegate(List devices);
16 | public event DevicesChangedDelegate OnDevicesChanged;
17 |
18 | [ExportGroup("References")]
19 | [Export] private VBoxContainer _devicesContainer;
20 | [Export] private PackedScene _deviceItemPrefab;
21 | [Export] private Button _refreshButton;
22 |
23 | public override void _Process(double delta)
24 | {
25 | if (!Visible) return;
26 |
27 | _scanCooldown -= (float)delta;
28 | if (_scanCooldown < 0f)
29 | {
30 | _scanCooldown = 10f;
31 | UpdateDevices();
32 | UpdateDeviceList();
33 | }
34 |
35 | _refreshButton.Disabled = _isUpdatingDevices;
36 | }
37 |
38 | private async void UpdateDevices()
39 | {
40 | if (!Visible || _isUpdatingDevices) return;
41 |
42 | _isUpdatingDevices = true;
43 |
44 | var devices = await SteamOSDevkitManager.ScanDevices();
45 | if (devices != _scannedDevices)
46 | {
47 | foreach (var device in devices)
48 | {
49 | if (
50 | _scannedDevices.Exists(x => x.IPAdress == device.IPAdress && x.Login == device.Login)
51 | || _pairedDevices.Exists(x => x.IPAdress == device.IPAdress && x.Login == device.Login)
52 | ) continue;
53 |
54 | _scannedDevices.Add(device);
55 | }
56 |
57 | UpdateDeviceList();
58 | }
59 |
60 | _isUpdatingDevices = false;
61 | }
62 |
63 | public void UpdateDeviceList()
64 | {
65 | // Clear List
66 | foreach (var childNode in _devicesContainer.GetChildren())
67 | {
68 | childNode.QueueFree();
69 | }
70 |
71 | var devices = new List();
72 | devices.AddRange(_pairedDevices);
73 | foreach (var scannedDevice in _scannedDevices)
74 | {
75 | if (devices.Exists(x => x.IPAdress == scannedDevice.IPAdress && x.Login == scannedDevice.Login))
76 | {
77 | continue;
78 | }
79 |
80 | devices.Add(scannedDevice);
81 | }
82 |
83 | foreach (var scannedDevice in devices)
84 | {
85 | var deviceItem = _deviceItemPrefab.Instantiate();
86 | deviceItem.SetUI(scannedDevice);
87 | deviceItem.OnDevicePair += device =>
88 | {
89 | // TODO: Connect to device and run a random ssh command to check communication
90 | if (!_pairedDevices.Exists(x => x.IPAdress == device.IPAdress && x.Login == device.Login))
91 | {
92 | _pairedDevices.Add(device);
93 | }
94 | UpdateDeviceList();
95 | };
96 | deviceItem.OnDeviceUnpair += device =>
97 | {
98 | _pairedDevices.Remove(device);
99 | UpdateDeviceList();
100 | };
101 | _devicesContainer.AddChild(deviceItem);
102 | }
103 | }
104 |
105 | public void Close()
106 | {
107 | Hide();
108 |
109 | if (_pairedDevices != SettingsManager.Instance.Devices)
110 | {
111 | OnDevicesChanged?.Invoke(_pairedDevices);
112 | }
113 | }
114 |
115 | public void OnVisibilityChanged()
116 | {
117 | if (Visible)
118 | {
119 | _isVisible = true;
120 |
121 | // Prepopulate device list with saved devices
122 | _pairedDevices = new List(SettingsManager.Instance.Devices);
123 | UpdateDeviceList();
124 | }
125 | else
126 | {
127 | _isVisible = false;
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/deploy_dock/DeployDock.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Godot;
5 |
6 | namespace Laura.DeployToSteamOS;
7 |
8 | [Tool]
9 | public partial class DeployDock : PanelContainer
10 | {
11 | private int _selectedId = 10000;
12 |
13 | [Export] private OptionButton _deployTargetButton;
14 | [Export] private Button _deployButton;
15 | [Export] private AddDeviceWindow _addDeviceWindow;
16 | [Export] private DeployWindow _deployWindow;
17 |
18 | public override void _EnterTree()
19 | {
20 | _addDeviceWindow.OnDevicesChanged += OnDevicesChanged;
21 | }
22 |
23 | public override void _ExitTree()
24 | {
25 | _addDeviceWindow.OnDevicesChanged -= OnDevicesChanged;
26 | }
27 |
28 | public override void _Ready()
29 | {
30 | UpdateDropdown();
31 | }
32 |
33 | public void OnDeployTargetItemSelected(int index)
34 | {
35 | var itemId = _deployTargetButton.GetItemId(index);
36 | if (_selectedId == itemId) return;
37 |
38 | if (itemId <= 10000)
39 | {
40 | _selectedId = itemId;
41 | }
42 | else if (itemId == 10001)
43 | {
44 | _addDeviceWindow.Show();
45 | }
46 |
47 | _deployTargetButton.Select(_deployTargetButton.GetItemIndex(_selectedId));
48 | _deployButton.Disabled = _selectedId >= 10000;
49 | }
50 |
51 | public void Deploy()
52 | {
53 | var device = SettingsManager.Instance.Devices.ElementAtOrDefault(_selectedId);
54 | if (device == null)
55 | {
56 | GD.PrintErr("[DeployToSteamOS] Unknown deploy target.");
57 | return;
58 | }
59 |
60 | // TODO: Detect Export Presets and stop if no Linux preset named "Steamdeck" exists
61 |
62 | _deployWindow.Deploy(device);
63 | }
64 |
65 | private void OnDevicesChanged(List devices)
66 | {
67 | // Adding Devices
68 | var devicesToAdd = devices.Where(device => !SettingsManager.Instance.Devices.Exists(x => x.IPAdress == device.IPAdress && x.Login == device.Login)).ToList();
69 | foreach (var device in devicesToAdd)
70 | {
71 | SettingsManager.Instance.Devices.Add(device);
72 | }
73 |
74 | // Removing Devices
75 | var devicesToRemove = SettingsManager.Instance.Devices.Where(savedDevice => !devices.Exists(x => x.IPAdress == savedDevice.IPAdress && x.Login == savedDevice.Login)).ToList();
76 | foreach (var savedDevice in devicesToRemove)
77 | {
78 | SettingsManager.Instance.Devices.Remove(savedDevice);
79 | }
80 |
81 | SettingsManager.Instance.Save();
82 | UpdateDropdown();
83 | }
84 |
85 | private async void UpdateDropdown()
86 | {
87 | // Hack to prevent console error
88 | // Godot apparently calls this before the settings are loaded
89 | while (SettingsManager.Instance == null)
90 | {
91 | await Task.Delay(10);
92 | }
93 |
94 | _deployTargetButton.Clear();
95 | _deployTargetButton.AddItem("None", 10000);
96 |
97 | for (var index = 0; index < SettingsManager.Instance.Devices.Count; index++)
98 | {
99 | var savedDevice = SettingsManager.Instance.Devices.ElementAtOrDefault(index);
100 | if(savedDevice == null) continue;
101 |
102 | _deployTargetButton.AddItem($"{savedDevice.DisplayName} ({savedDevice.Login}@{savedDevice.IPAdress})", index);
103 | }
104 |
105 | _deployTargetButton.AddSeparator();
106 | _deployTargetButton.AddItem("Add devkit device", 10001);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/add_device_window/add_device_window.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=7 format=3 uid="uid://6be5afncp3nr"]
2 |
3 | [ext_resource type="Script" path="res://addons/deploy_to_steamos/add_device_window/AddDeviceWindow.cs" id="1_3tepc"]
4 | [ext_resource type="PackedScene" uid="uid://p64nkj5ii2xt" path="res://addons/deploy_to_steamos/add_device_window/device_item_prefab.tscn" id="2_dka1k"]
5 |
6 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mam6h"]
7 | content_margin_left = 0.0
8 | content_margin_top = 0.0
9 | content_margin_right = 0.0
10 | content_margin_bottom = 0.0
11 | bg_color = Color(0.0666667, 0.0666667, 0.0666667, 1)
12 |
13 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_imdn0"]
14 | content_margin_left = 30.0
15 | content_margin_top = 10.0
16 | content_margin_right = 30.0
17 | content_margin_bottom = 10.0
18 | bg_color = Color(0.2, 0.0823529, 0.121569, 1)
19 |
20 | [sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_74135"]
21 | content_margin_left = 15.0
22 | content_margin_top = 15.0
23 | content_margin_right = 15.0
24 | content_margin_bottom = 15.0
25 |
26 | [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ne8al"]
27 | content_margin_left = 30.0
28 | content_margin_top = 10.0
29 | content_margin_right = 30.0
30 | content_margin_bottom = 10.0
31 | bg_color = Color(0.133333, 0.133333, 0.133333, 1)
32 |
33 | [node name="AddDeviceWindow" type="Window" node_paths=PackedStringArray("_devicesContainer", "_refreshButton")]
34 | title = "Add devkit device"
35 | initial_position = 1
36 | size = Vector2i(650, 500)
37 | transient = true
38 | exclusive = true
39 | min_size = Vector2i(650, 500)
40 | script = ExtResource("1_3tepc")
41 | _devicesContainer = NodePath("PanelContainer/VBoxContainer/ScrollContainer/ContentContainer/DevicesContainer")
42 | _deviceItemPrefab = ExtResource("2_dka1k")
43 | _refreshButton = NodePath("PanelContainer/VBoxContainer/ActionsContainer/HBoxContainer/RefreshButton")
44 |
45 | [node name="PanelContainer" type="PanelContainer" parent="."]
46 | anchors_preset = 15
47 | anchor_right = 1.0
48 | anchor_bottom = 1.0
49 | grow_horizontal = 2
50 | grow_vertical = 2
51 | size_flags_horizontal = 3
52 | size_flags_vertical = 3
53 | theme_override_styles/panel = SubResource("StyleBoxFlat_mam6h")
54 |
55 | [node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer"]
56 | layout_mode = 2
57 | size_flags_horizontal = 3
58 | size_flags_vertical = 3
59 | theme_override_constants/separation = 0
60 |
61 | [node name="InfoContainer" type="PanelContainer" parent="PanelContainer/VBoxContainer"]
62 | layout_mode = 2
63 | theme_override_styles/panel = SubResource("StyleBoxFlat_imdn0")
64 |
65 | [node name="Label" type="Label" parent="PanelContainer/VBoxContainer/InfoContainer"]
66 | custom_minimum_size = Vector2(0, 10)
67 | layout_mode = 2
68 | theme_override_colors/font_color = Color(1, 0.788235, 0.858824, 1)
69 | theme_override_font_sizes/font_size = 11
70 | text = "Please keep in mind that you need to connect to your devkit device at least once through the official SteamOS devkit client to install the devkit tools to your device and create the necessary authentication keys."
71 | autowrap_mode = 3
72 |
73 | [node name="ScrollContainer" type="ScrollContainer" parent="PanelContainer/VBoxContainer"]
74 | layout_mode = 2
75 | size_flags_vertical = 3
76 |
77 | [node name="ContentContainer" type="PanelContainer" parent="PanelContainer/VBoxContainer/ScrollContainer"]
78 | layout_mode = 2
79 | size_flags_horizontal = 3
80 | size_flags_vertical = 0
81 | theme_override_styles/panel = SubResource("StyleBoxEmpty_74135")
82 |
83 | [node name="DevicesContainer" type="VBoxContainer" parent="PanelContainer/VBoxContainer/ScrollContainer/ContentContainer"]
84 | layout_mode = 2
85 | theme_override_constants/separation = 10
86 |
87 | [node name="ActionsContainer" type="PanelContainer" parent="PanelContainer/VBoxContainer"]
88 | layout_mode = 2
89 | theme_override_styles/panel = SubResource("StyleBoxFlat_ne8al")
90 |
91 | [node name="HBoxContainer" type="HBoxContainer" parent="PanelContainer/VBoxContainer/ActionsContainer"]
92 | layout_mode = 2
93 | theme_override_constants/separation = 10
94 | alignment = 2
95 |
96 | [node name="RefreshButton" type="Button" parent="PanelContainer/VBoxContainer/ActionsContainer/HBoxContainer"]
97 | layout_mode = 2
98 | text = "Refresh"
99 |
100 | [node name="CloseButton" type="Button" parent="PanelContainer/VBoxContainer/ActionsContainer/HBoxContainer"]
101 | layout_mode = 2
102 | text = "Close"
103 |
104 | [connection signal="close_requested" from="." to="." method="Close"]
105 | [connection signal="visibility_changed" from="." to="." method="OnVisibilityChanged"]
106 | [connection signal="pressed" from="PanelContainer/VBoxContainer/ActionsContainer/HBoxContainer/RefreshButton" to="." method="UpdateDevices"]
107 | [connection signal="pressed" from="PanelContainer/VBoxContainer/ActionsContainer/HBoxContainer/CloseButton" to="." method="Close"]
108 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/SteamOSDevkitManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Runtime.InteropServices;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Text.Json;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 | using Godot;
10 | using Renci.SshNet;
11 | using Zeroconf;
12 |
13 | namespace Laura.DeployToSteamOS;
14 |
15 | ///
16 | /// Scans the network for SteamOS Devkit devices via the ZeroConf / Bonjour protocol
17 | ///
18 | public class SteamOSDevkitManager
19 | {
20 | public const string SteamOSProtocol = "_steamos-devkit._tcp.local.";
21 | public const string CompatibleTextVersion = "1";
22 |
23 | ///
24 | /// Scans the network for valid SteamOS devkit devices
25 | ///
26 | /// A list of valid SteamOS devkit devices
27 | public static async Task> ScanDevices()
28 | {
29 | var networkDevices = await ZeroconfResolver.ResolveAsync(SteamOSProtocol);
30 | List devices = new();
31 |
32 | // Iterate through all network devices and request further connection info from the service
33 | foreach (var networkDevice in networkDevices)
34 | {
35 | var device = new Device
36 | {
37 | DisplayName = networkDevice.DisplayName,
38 | IPAdress = networkDevice.IPAddress,
39 | ServiceName = networkDevice.DisplayName + "." + SteamOSProtocol,
40 | };
41 |
42 | var hasServiceData = networkDevice.Services.TryGetValue(device.ServiceName, out var serviceData);
43 |
44 | // This device is not a proper SteamOS device
45 | if(!hasServiceData) continue;
46 |
47 | var properties = serviceData.Properties.FirstOrDefault();
48 | if (properties == null) continue;
49 |
50 | // Device is not compatible (version mismatch)
51 | // String"1" is for some reason not equal to String"1"
52 | //if (properties["txtvers"] != CompatibleTextVersion) continue;
53 |
54 | device.Settings = properties["settings"];
55 | device.Login = properties["login"];
56 | device.Devkit1 = properties["devkit1"];
57 |
58 | devices.Add(device);
59 | }
60 |
61 | return devices;
62 | }
63 |
64 | ///
65 | /// Creates an SSH connection and runs a command
66 | ///
67 | /// A SteamOS devkit device
68 | /// The SSH command to run
69 | /// A callable for logging
70 | /// The SSH CLI output
71 | public static async Task RunSSHCommand(Device device, string command, Callable logCallable)
72 | {
73 | logCallable.CallDeferred($"Connecting to {device.Login}@{device.IPAdress}");
74 | using var client = new SshClient(GetSSHConnectionInfo(device));
75 | await client.ConnectAsync(CancellationToken.None);
76 |
77 | logCallable.CallDeferred($"Command: '{command}'");
78 | var sshCommand = client.CreateCommand(command);
79 | var result = await Task.Factory.FromAsync(sshCommand.BeginExecute(), sshCommand.EndExecute);
80 |
81 | client.Disconnect();
82 |
83 | return result;
84 | }
85 |
86 | ///
87 | /// Creates a new SCP connection and copies all local files to a remote path
88 | ///
89 | /// A SteamOS devkit device
90 | /// The path on the host
91 | /// The path on the device
92 | /// A callable for logging
93 | public static async Task CopyFiles(Device device, string localPath, string remotePath, Callable logCallable)
94 | {
95 | logCallable.CallDeferred($"Connecting to {device.Login}@{device.IPAdress}");
96 | using var client = new ScpClient(GetSSHConnectionInfo(device));
97 | await client.ConnectAsync(CancellationToken.None);
98 |
99 | logCallable.CallDeferred($"Uploading files");
100 |
101 | // Run async method until upload is done
102 | // TODO: Set Progress based on files
103 | var lastUploadedFilename = "";
104 | var uploadProgress = 0;
105 | var taskCompletion = new TaskCompletionSource();
106 | client.Uploading += (sender, e) =>
107 | {
108 | if (e.Filename != lastUploadedFilename)
109 | {
110 | lastUploadedFilename = e.Filename;
111 | uploadProgress = 0;
112 | }
113 | var progressPercentage = Mathf.CeilToInt((double)e.Uploaded / e.Size * 100);
114 |
115 | if (progressPercentage != uploadProgress)
116 | {
117 | uploadProgress = progressPercentage;
118 | logCallable.CallDeferred($"Uploading {lastUploadedFilename} ({progressPercentage}%)");
119 | }
120 |
121 | if (e.Uploaded == e.Size)
122 | {
123 | taskCompletion.TrySetResult(true);
124 | }
125 | };
126 | client.ErrorOccurred += (sender, args) => throw new Exception("Error while uploading build.");
127 |
128 | await Task.Run(() => client.Upload(new DirectoryInfo(localPath), remotePath));
129 | await taskCompletion.Task;
130 | client.Disconnect();
131 |
132 | logCallable.CallDeferred($"Fixing file permissions");
133 | await RunSSHCommand(device, $"chmod +x -R {remotePath}", logCallable);
134 | }
135 |
136 | ///
137 | /// Runs an SSH command on the device that runs the steamos-prepare-upload script
138 | ///
139 | /// A SteamOS devkit device
140 | /// An ID for the game
141 | /// A callable for logging
142 | /// The CLI result
143 | public static async Task PrepareUpload(Device device, string gameId, Callable logCallable)
144 | {
145 | logCallable.CallDeferred("Preparing upload");
146 |
147 | var resultRaw = await RunSSHCommand(device, "python3 ~/devkit-utils/steamos-prepare-upload --gameid " + gameId, logCallable);
148 | var result = JsonSerializer.Deserialize(resultRaw, DefaultSerializerOptions);
149 |
150 | return result;
151 | }
152 |
153 | ///
154 | /// Runs an SSH command on the device that runs the steamos-create-shortcut script
155 | ///
156 | /// A SteamOS devkit device
157 | /// Parameters for the shortcut
158 | /// A callable for logging
159 | /// The CLI result
160 | public static async Task CreateShortcut(Device device, CreateShortcutParameters parameters, Callable logCallable)
161 | {
162 | var parametersJson = JsonSerializer.Serialize(parameters);
163 | var command = $"python3 ~/devkit-utils/steam-client-create-shortcut --parms '{parametersJson}'";
164 |
165 | var resultRaw = await RunSSHCommand(device, command, logCallable);
166 | var result = JsonSerializer.Deserialize(resultRaw, DefaultSerializerOptions);
167 |
168 | return result;
169 | }
170 |
171 | ///
172 | /// A SteamOS devkit device
173 | ///
174 | public class Device
175 | {
176 | public string DisplayName { get; set; }
177 | public string IPAdress { get; set; }
178 | public int Port { get; set; }
179 | public string ServiceName { get; set; }
180 | public string Settings { get; set; }
181 | public string Login { get; set; }
182 | public string Devkit1 { get; set; }
183 | }
184 |
185 | public class PrepareUploadResult
186 | {
187 | public string User { get; set; }
188 | public string Directory { get; set; }
189 | }
190 |
191 | public class CreateShortcutResult
192 | {
193 | public string Error { get; set; }
194 | public string Success { get; set; }
195 | }
196 |
197 | ///
198 | /// Parameters for the CreateShortcut method
199 | ///
200 | public struct CreateShortcutParameters
201 | {
202 | public string gameid { get; set; }
203 | public string directory { get; set; }
204 | public string[] argv { get; set; }
205 | public Dictionary settings { get; set; }
206 | }
207 |
208 | ///
209 | /// Returns the path to the devkit_rsa key generated by the official SteamOS devkit client
210 | ///
211 | /// The path to the "devkit_rsa" key
212 | public static string GetPrivateKeyPath()
213 | {
214 | string applicationDataPath = "";
215 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
216 | applicationDataPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData), "steamos-devkit");
217 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
218 | applicationDataPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "Library", "Application Support");
219 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
220 | applicationDataPath = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".config");
221 |
222 | var keyFolder = Path.Combine(applicationDataPath, "steamos-devkit");
223 | return Path.Combine(keyFolder, "devkit_rsa");
224 | }
225 |
226 | ///
227 | /// Creates a SSH Connection Info for a SteamOS devkit device
228 | ///
229 | /// A SteamOS devkit device
230 | /// An SSH ConnectionInfo
231 | /// Throws if there is no private key present
232 | public static ConnectionInfo GetSSHConnectionInfo(Device device)
233 | {
234 | var privateKeyPath = GetPrivateKeyPath();
235 | if (!File.Exists(privateKeyPath)) throw new Exception("devkit_rsa key is missing. Have you connected to your device via the official devkit UI yet?");
236 |
237 | var privateKeyFile = new PrivateKeyFile(privateKeyPath);
238 | return new ConnectionInfo(device.IPAdress, device.Login, new PrivateKeyAuthenticationMethod(device.Login, privateKeyFile));
239 | }
240 |
241 | public static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions
242 | {
243 | PropertyNameCaseInsensitive = true,
244 | };
245 | }
246 |
--------------------------------------------------------------------------------
/addons/deploy_to_steamos/deploy_window/DeployWindow.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Godot;
3 |
4 | namespace Laura.DeployToSteamOS;
5 |
6 | [Tool]
7 | public partial class DeployWindow : Window
8 | {
9 | public enum DeployStep
10 | {
11 | Init,
12 | Building,
13 | PrepareUpload,
14 | Uploading,
15 | CreateShortcut,
16 | Done
17 | }
18 | public DeployStep CurrentStep = DeployStep.Init;
19 |
20 | public enum StepProgress
21 | {
22 | Queued,
23 | Running,
24 | Succeeded,
25 | Failed
26 | }
27 | public StepProgress CurrentProgress = StepProgress.Queued;
28 |
29 | private string _localPath;
30 | private string _gameId;
31 | private SteamOSDevkitManager.Device _device;
32 | private SteamOSDevkitManager.PrepareUploadResult _prepareUploadResult;
33 | private SteamOSDevkitManager.CreateShortcutResult _createShortcutResult;
34 |
35 | private Dictionary _colorsBright = new()
36 | {
37 | { StepProgress.Queued, new Color("#6a6a6a") },
38 | { StepProgress.Running, new Color("#ffffff") },
39 | { StepProgress.Succeeded, new Color("#00f294") },
40 | { StepProgress.Failed, new Color("#ff4245") },
41 | };
42 | private Dictionary _colorsDim = new()
43 | {
44 | { StepProgress.Queued, new Color("#1d1d1d") },
45 | { StepProgress.Running, new Color("#222222") },
46 | { StepProgress.Succeeded, new Color("#13241d") },
47 | { StepProgress.Failed, new Color("#241313") },
48 | };
49 |
50 | [ExportGroup("References")]
51 | [Export] private VBoxContainer _buildingContainer;
52 | [Export] private VBoxContainer _prepareUploadContainer;
53 | [Export] private VBoxContainer _uploadingContainer;
54 | [Export] private VBoxContainer _createShortcutContainer;
55 |
56 | public override void _Process(double delta)
57 | {
58 | if (CurrentStep != DeployStep.Done)
59 | {
60 | var container = CurrentStep switch
61 | {
62 | DeployStep.Building => _buildingContainer,
63 | DeployStep.PrepareUpload => _prepareUploadContainer,
64 | DeployStep.Uploading => _uploadingContainer,
65 | DeployStep.CreateShortcut => _createShortcutContainer,
66 | _ => null
67 | };
68 | if (container != null)
69 | {
70 | var progressBar = container.GetNode("Progressbar");
71 | if (progressBar.Value < 95f)
72 | {
73 | var shouldDoubleSpeed = CurrentStep is DeployStep.PrepareUpload or DeployStep.CreateShortcut;
74 | progressBar.Value += (float)delta * (shouldDoubleSpeed ? 2f : 1f);
75 | }
76 | }
77 | }
78 | }
79 |
80 | public async void Deploy(SteamOSDevkitManager.Device device)
81 | {
82 | if (device == null)
83 | {
84 | GD.PrintErr("[DeployToSteamOS] Device is not available.");
85 | return;
86 | }
87 |
88 | _device = device;
89 | _prepareUploadResult = null;
90 | _createShortcutResult = null;
91 | ResetUI();
92 |
93 | Title = $"Deploying to {_device.DisplayName} ({_device.Login}@{_device.IPAdress})";
94 | Show();
95 |
96 | await DeployInit();
97 | await DeployBuild(Callable.From((Variant log) => AddToConsole(DeployStep.Building, log.AsString())));
98 | await DeployPrepareUpload(Callable.From((Variant log) => AddToConsole(DeployStep.PrepareUpload, log.AsString())));
99 | await DeployUpload(Callable.From((Variant log) => AddToConsole(DeployStep.Uploading, log.AsString())));
100 | await DeployCreateShortcut(Callable.From((Variant log) => AddToConsole(DeployStep.CreateShortcut, log.AsString())));
101 |
102 | CurrentStep = DeployStep.Done;
103 | CurrentProgress = StepProgress.Succeeded;
104 | UpdateUI();
105 | }
106 |
107 | public void Close()
108 | {
109 | if (CurrentStep != DeployStep.Init && CurrentStep != DeployStep.Done) return;
110 | Hide();
111 | }
112 |
113 | private void UpdateUI()
114 | {
115 | UpdateContainer(CurrentStep, CurrentProgress);
116 | if (CurrentStep == DeployStep.Done)
117 | {
118 | SetConsoleVisibility(DeployStep.Building, false, false);
119 | SetConsoleVisibility(DeployStep.PrepareUpload, false, false);
120 | SetConsoleVisibility(DeployStep.Uploading, false, false);
121 | SetConsoleVisibility(DeployStep.CreateShortcut, false, false);
122 | }
123 | }
124 |
125 | private void ResetUI()
126 | {
127 | // Clear Consoles
128 | List consoleNodes = new();
129 | consoleNodes.AddRange(_buildingContainer.GetNode("ConsoleContainer/ScrollContainer/VBoxContainer").GetChildren());
130 | consoleNodes.AddRange(_prepareUploadContainer.GetNode("ConsoleContainer/ScrollContainer/VBoxContainer").GetChildren());
131 | consoleNodes.AddRange(_uploadingContainer.GetNode("ConsoleContainer/ScrollContainer/VBoxContainer").GetChildren());
132 | consoleNodes.AddRange(_createShortcutContainer.GetNode("ConsoleContainer/ScrollContainer/VBoxContainer").GetChildren());
133 |
134 | foreach (var consoleNode in consoleNodes)
135 | {
136 | consoleNode.QueueFree();
137 | }
138 |
139 | // Clear States
140 | UpdateContainer(DeployStep.Building, StepProgress.Queued);
141 | UpdateContainer(DeployStep.PrepareUpload, StepProgress.Queued);
142 | UpdateContainer(DeployStep.Uploading, StepProgress.Queued);
143 | UpdateContainer(DeployStep.CreateShortcut, StepProgress.Queued);
144 |
145 | CurrentStep = DeployStep.Init;
146 | CurrentProgress = StepProgress.Queued;
147 | }
148 |
149 | private void UpdateContainer(DeployStep step, StepProgress progress)
150 | {
151 | var container = step switch
152 | {
153 | DeployStep.Building => _buildingContainer,
154 | DeployStep.PrepareUpload => _prepareUploadContainer,
155 | DeployStep.Uploading => _uploadingContainer,
156 | DeployStep.CreateShortcut => _createShortcutContainer,
157 | _ => null
158 | };
159 | if (container == null) return;
160 |
161 | var headerContainer = container.GetNode("HeaderContainer");
162 | var label = headerContainer.GetNode