├── .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 | 3 | 4 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /logo_bright.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /addons/deploy_to_steamos/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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