diff --git a/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs b/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs
index 5c345585..007bdfaa 100644
--- a/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs
+++ b/LANCommander.SDK/Extensions/IServiceCollectionExtensions.cs
@@ -5,6 +5,9 @@ using LANCommander.SDK.PowerShell;
using LANCommander.SDK.Providers;
using LANCommander.SDK.Rpc.Client;
using LANCommander.SDK.Services;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Extensions;
+using LANCommander.Steam.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using RpcSubscriber = LANCommander.SDK.Rpc.Clients.RpcSubscriber;
diff --git a/LANCommander.SDK/LANCommander.SDK.csproj b/LANCommander.SDK/LANCommander.SDK.csproj
index b6ef3335..144e1235 100644
--- a/LANCommander.SDK/LANCommander.SDK.csproj
+++ b/LANCommander.SDK/LANCommander.SDK.csproj
@@ -32,6 +32,7 @@
+
@@ -50,5 +51,9 @@
\
+
+
+
+
diff --git a/LANCommander.SDK/Models/Settings/Settings.cs b/LANCommander.SDK/Models/Settings/Settings.cs
index f6381278..2bc9fe84 100644
--- a/LANCommander.SDK/Models/Settings/Settings.cs
+++ b/LANCommander.SDK/Models/Settings/Settings.cs
@@ -15,6 +15,7 @@ public class Settings
public DebugSettings Debug { get; set; } = new();
public UpdateSettings Updates { get; set; } = new();
public IPXRelaySettings IPXRelay { get; set; } = new();
+ public SteamSettings Steam { get; set; } = new();
public string Culture { get; set; } = "en-US";
}
\ No newline at end of file
diff --git a/LANCommander.SDK/Models/Settings/SteamSettings.cs b/LANCommander.SDK/Models/Settings/SteamSettings.cs
new file mode 100644
index 00000000..285dd92e
--- /dev/null
+++ b/LANCommander.SDK/Models/Settings/SteamSettings.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using LANCommander.Steam.Models;
+
+namespace LANCommander.SDK.Models;
+
+public class SteamSettings
+{
+ public string Path { get; set; } = string.Empty;
+ public string InstallDirectory { get; set; } = "";
+ public ICollection Profiles { get; set; } = [];
+}
\ No newline at end of file
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Connect-SteamCmd.cs b/LANCommander.SDK/PowerShell/Cmdlets/Connect-SteamCmd.cs
new file mode 100644
index 00000000..8bedd856
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Connect-SteamCmd.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Management.Automation;
+using System.Security;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Enums;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommunications.Connect, "SteamCmd")]
+[OutputType(typeof(SteamCmdStatus))]
+[GenerateBindings]
+public partial class ConnectSteamCmdCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Username { get; set; } = string.Empty;
+
+ [Parameter(Mandatory = false)]
+ public SecureString? Password { get; set; }
+
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken token)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ string? password = null;
+ if (Password != null)
+ {
+ var ptr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(Password);
+ try
+ {
+ password = System.Runtime.InteropServices.Marshal.PtrToStringBSTR(ptr);
+ }
+ finally
+ {
+ System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(ptr);
+ }
+ }
+
+ var status = await _steamCmdService.LoginToSteamAsync(Username, password);
+
+ WriteObject(status);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "LoginError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Convert-AspectRatio.cs b/LANCommander.SDK/PowerShell/Cmdlets/Convert-AspectRatio.cs
index f4651e2d..19284047 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Convert-AspectRatio.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Convert-AspectRatio.cs
@@ -11,7 +11,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
[Cmdlet(VerbsData.Convert, "AspectRatio")]
[OutputType(typeof(string))]
- public class ConvertAspectRatioCmdlet : BaseCmdlet
+ public class ConvertAspectRatioCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0)]
public int Width { get; set; }
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs b/LANCommander.SDK/PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs
index b40172e2..2d6f1b76 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/ConvertFrom-SerializedBase64.cs
@@ -11,7 +11,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsData.ConvertFrom, "SerializedBase64")]
[OutputType(typeof(object))]
- public class ConvertFromSerializedBase64Cmdlet : BaseCmdlet
+ public class ConvertFromSerializedBase64Cmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Input { get; set; }
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs b/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs
index b4a94e25..20540080 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-SerializedBase64.cs
@@ -12,7 +12,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsData.ConvertTo, "SerializedBase64")]
[OutputType(typeof(object))]
- public class ConvertToSerializedBase64Cmdlet : BaseCmdlet
+ public class ConvertToSerializedBase64Cmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public object Input { get; set; }
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-StringBytes.cs b/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-StringBytes.cs
index cccf4e47..f5425fca 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-StringBytes.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/ConvertTo-StringBytes.cs
@@ -6,7 +6,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsData.ConvertTo, "StringBytes")]
[OutputType(typeof(byte[]))]
- public class ConvertToStringBytesCmdlet : BaseCmdlet
+ public class ConvertToStringBytesCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Input { get; set; }
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Disconnect-SteamCmd.cs b/LANCommander.SDK/PowerShell/Cmdlets/Disconnect-SteamCmd.cs
new file mode 100644
index 00000000..a64a6deb
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Disconnect-SteamCmd.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Enums;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommunications.Disconnect, "SteamCmd")]
+[OutputType(typeof(SteamCmdStatus))]
+[GenerateBindings]
+public partial class DisconnectSteamCmdCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Username { get; set; } = string.Empty;
+
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var status = await _steamCmdService.LogoutAsync(Username);
+
+ WriteObject(status);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "LogoutError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchBinary.cs b/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchBinary.cs
index d1a40028..e716dc39 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchBinary.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchBinary.cs
@@ -5,7 +5,7 @@ using System.Management.Automation;
namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsData.Edit, "PatchBinary")]
- public class EditPatchBinaryCmdlet : BaseCmdlet
+ public class EditPatchBinaryCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0)]
public long Offset { get; set; }
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchGameSpy.cs b/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchGameSpy.cs
index df2f8f57..17ea61d2 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchGameSpy.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchGameSpy.cs
@@ -12,7 +12,7 @@ using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
namespace LANCommander.SDK.PowerShell.Cmdlets;
[Cmdlet(VerbsData.Edit, "PatchGameSpy")]
-public class EditPatchGameSpy : BaseCmdlet
+public class EditPatchGameSpy : Cmdlet
{
private const string GAMESPY_HOSTNAME = "gamespy.com";
private const string GAMESPY_PUBLICKEY = "BF05D63E93751AD4A59A4A7389CF0BE8A22CCDEEA1E7F12C062D6E194472EFDA5184CCECEB4FBADF5EB1D7ABFE91181453972AA971F624AF9BA8F0F82E2869FB7D44BDE8D56EE50977898F3FEE75869622C4981F07506248BD3D092E8EA05C12B2FA37881176084C8F8B8756C4722CDC57D2AD28ACD3AD85934FB48D6B2D2027";
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-GameManifest.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-GameManifest.cs
index 4c31bb2f..bdd9fcb6 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Get-GameManifest.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-GameManifest.cs
@@ -7,7 +7,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "GameManifest")]
[OutputType(typeof(SDK.Models.Manifest.Game))]
- public class GetGameManifestCmdlet : BaseCmdlet
+ public class GetGameManifestCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Path { get; set; }
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-HorizontalFov.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-HorizontalFov.cs
index bea55755..1dfe0420 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Get-HorizontalFov.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-HorizontalFov.cs
@@ -9,7 +9,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "VerticalFov")]
[OutputType(typeof(string))]
- public class GetHorizontalFovCmdlet : BaseCmdlet
+ public class GetHorizontalFovCmdlet : Cmdlet
{
[Parameter] public int Width { get; set; } = 0;
[Parameter] public int Height { get; set; } = 0;
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-PrimaryDisplay.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-PrimaryDisplay.cs
index 2af029e9..ffa2abcf 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Get-PrimaryDisplay.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-PrimaryDisplay.cs
@@ -8,7 +8,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "PrimaryDisplay")]
[OutputType(typeof(string))]
- public class GetPrimaryDisplayCmdlet : BaseCmdlet
+ public class GetPrimaryDisplayCmdlet : Cmdlet
{
protected override void ProcessRecord()
{
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdConnectionStatus.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdConnectionStatus.cs
new file mode 100644
index 00000000..86bd6791
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdConnectionStatus.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Enums;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Get, "SteamCmdConnectionStatus")]
+[OutputType(typeof(SteamCmdConnectionStatus))]
+[GenerateBindings]
+public partial class GetSteamCmdConnectionStatusCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Username { get; set; } = string.Empty;
+
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var status = await _steamCmdService.GetConnectionStatusAsync(Username);
+ WriteObject(status);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "GetConnectionStatusError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdPath.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdPath.cs
new file mode 100644
index 00000000..dda05aa2
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdPath.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Get, "SteamCmdPath")]
+[OutputType(typeof(string))]
+[GenerateBindings]
+public partial class GetSteamCmdPathCmdlet : DependencyCmdlet
+{
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var path = await _steamCmdService.AutoDetectSteamCmdPathAsync();
+ if (!string.IsNullOrEmpty(path))
+ {
+ WriteObject(path);
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "AutoDetectPathError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfile.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfile.cs
new file mode 100644
index 00000000..d2486772
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfile.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Models;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Get, "SteamCmdProfile")]
+[OutputType(typeof(SteamCmdProfile))]
+[GenerateBindings]
+public partial class GetSteamCmdProfileCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Username { get; set; } = string.Empty;
+
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var profile = await _steamCmdService.GetProfileAsync(Username);
+ if (profile != null)
+ {
+ WriteObject(profile);
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "GetProfileError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfiles.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfiles.cs
new file mode 100644
index 00000000..dfbe7c55
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamCmdProfiles.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Models;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Get, "SteamCmdProfiles")]
+[OutputType(typeof(SteamCmdProfile))]
+[GenerateBindings]
+public partial class GetSteamCmdProfilesCmdlet : DependencyCmdlet
+{
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var profiles = await _steamCmdService.GetProfilesAsync();
+ foreach (var profile in profiles)
+ {
+ WriteObject(profile);
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "GetProfilesError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJob.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJob.cs
new file mode 100644
index 00000000..316a80c8
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJob.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Models;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Get, "SteamInstallJob")]
+[OutputType(typeof(SteamCmdInstallJob))]
+[GenerateBindings]
+public partial class GetSteamInstallJobCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public Guid JobId { get; set; }
+
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var job = _steamCmdService.GetInstallJob(JobId);
+ if (job != null)
+ {
+ WriteObject(job);
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "GetInstallJobError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJobs.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJobs.cs
new file mode 100644
index 00000000..b51d2832
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamInstallJobs.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Models;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Get, "SteamInstallJobs")]
+[OutputType(typeof(SteamCmdInstallJob))]
+[GenerateBindings]
+public partial class GetSteamInstallJobsCmdlet : DependencyCmdlet
+{
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var jobs = _steamCmdService.GetInstallJobs();
+ foreach (var job in jobs)
+ {
+ WriteObject(job);
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "GetInstallJobsError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManual.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManual.cs
new file mode 100644
index 00000000..ac5ce244
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManual.cs
@@ -0,0 +1,62 @@
+using System;
+using System.IO;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Services;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Get, "SteamManual")]
+[OutputType(typeof(byte[]))]
+[GenerateBindings]
+public partial class GetSteamManualCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public int AppId { get; set; }
+
+ [Parameter(Mandatory = false)]
+ public string? OutputPath { get; set; }
+
+ [ServiceDependency]
+ private SteamStoreService _steamStoreService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamStoreService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamStoreService is not available in the PowerShell session"),
+ "SteamStoreServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var manualData = await _steamStoreService.DownloadManualAsync(AppId);
+
+ if (manualData == null || manualData.Length == 0)
+ {
+ WriteWarning($"No manual found for App ID {AppId}");
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(OutputPath))
+ {
+ await File.WriteAllBytesAsync(OutputPath, manualData, cancellationToken);
+ WriteObject(OutputPath);
+ }
+ else
+ {
+ WriteObject(manualData);
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "DownloadManualError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManualUri.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManualUri.cs
new file mode 100644
index 00000000..db93106e
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamManualUri.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Services;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Get, "SteamManualUri")]
+[OutputType(typeof(Uri))]
+[GenerateBindings]
+public partial class GetSteamManualUriCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public int AppId { get; set; }
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var uri = SteamStoreService.GetManualUri(AppId);
+ WriteObject(uri);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "GetManualUriError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamWebAssetUri.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamWebAssetUri.cs
new file mode 100644
index 00000000..3154abfc
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-SteamWebAssetUri.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam;
+using LANCommander.Steam.Services;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Get, "SteamWebAssetUri")]
+[OutputType(typeof(Uri))]
+[GenerateBindings]
+public partial class GetSteamWebAssetUriCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public int AppId { get; set; }
+
+ [Parameter(Mandatory = true, Position = 1)]
+ public WebAssetType WebAssetType { get; set; }
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var uri = SteamStoreService.GetWebAssetUri(AppId, WebAssetType);
+ WriteObject(uri);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "GetWebAssetUriError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs
index 1a971089..da014b65 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-UserCustomField.cs
@@ -8,7 +8,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "UserCustomField")]
[OutputType(typeof(string))]
- public class GetUserCustomFieldCmdlet(ProfileClient profileClient) : BaseCmdlet
+ public class GetUserCustomFieldCmdlet(ProfileClient profileClient) : Cmdlet
{
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; }
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Get-VerticalFov.cs b/LANCommander.SDK/PowerShell/Cmdlets/Get-VerticalFov.cs
index 8f029817..1bb26049 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Get-VerticalFov.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Get-VerticalFov.cs
@@ -9,7 +9,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsCommon.Get, "VerticalFov")]
[OutputType(typeof(string))]
- public class GetVerticalFovCmdlet : BaseCmdlet
+ public class GetVerticalFovCmdlet : Cmdlet
{
[Parameter] public int Width { get; set; } = 0;
[Parameter] public int Height { get; set; } = 0;
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Install-SteamContent.cs b/LANCommander.SDK/PowerShell/Cmdlets/Install-SteamContent.cs
new file mode 100644
index 00000000..c54ed785
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Install-SteamContent.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Models;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsLifecycle.Install, "SteamContent")]
+[OutputType(typeof(SteamCmdInstallJob))]
+[GenerateBindings]
+public partial class InstallSteamContentCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public uint AppId { get; set; }
+
+ [Parameter(Mandatory = true, Position = 1)]
+ public string InstallDirectory { get; set; } = string.Empty;
+
+ [Parameter(Mandatory = false)]
+ public string? Username { get; set; }
+
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var job = await _steamCmdService.InstallContentAsync(AppId, InstallDirectory, Username);
+ WriteObject(job);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "InstallContentError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs b/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs
index 91348d3a..cfe7cad4 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Out-PlayerAvatar.cs
@@ -5,7 +5,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsData.Out, "PlayerAvatar")]
[OutputType(typeof(string))]
- public class OutPlayerAvatarCmdlet(ProfileClient profileClient) : BaseCmdlet
+ public class OutPlayerAvatarCmdlet(ProfileClient profileClient) : Cmdlet
{
protected override void ProcessRecord()
{
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamCmdProfile.cs b/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamCmdProfile.cs
new file mode 100644
index 00000000..00c615d9
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamCmdProfile.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Remove, "SteamCmdProfile")]
+[GenerateBindings]
+public partial class RemoveSteamCmdProfileCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Username { get; set; } = string.Empty;
+
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ await _steamCmdService.DeleteProfileAsync(Username);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "DeleteProfileError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamContent.cs b/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamContent.cs
new file mode 100644
index 00000000..cc8995d0
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Remove-SteamContent.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Enums;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Remove, "SteamContent")]
+[OutputType(typeof(SteamCmdStatus))]
+[GenerateBindings]
+public partial class RemoveSteamContentCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public string InstallDirectory { get; set; } = string.Empty;
+
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var status = await _steamCmdService.RemoveContentAsync(InstallDirectory);
+ WriteObject(status);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "RemoveContentError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Search-SteamGames.cs b/LANCommander.SDK/PowerShell/Cmdlets/Search-SteamGames.cs
new file mode 100644
index 00000000..8690d29c
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Search-SteamGames.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam;
+using LANCommander.Steam.Services;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Search, "SteamGames")]
+[OutputType(typeof(GameSearchResult))]
+[GenerateBindings]
+public partial class SearchSteamGamesCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Keyword { get; set; } = string.Empty;
+
+ [ServiceDependency]
+ private SteamStoreService _steamStoreService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamStoreService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamStoreService is not available in the PowerShell session"),
+ "SteamStoreServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var results = await _steamStoreService.SearchGamesAsync(Keyword);
+ foreach (var result in results)
+ {
+ WriteObject(result);
+ }
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "SearchGamesError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Set-SteamCmdProfile.cs b/LANCommander.SDK/PowerShell/Cmdlets/Set-SteamCmdProfile.cs
new file mode 100644
index 00000000..52cf33e9
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Set-SteamCmdProfile.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Models;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsCommon.Set, "SteamCmdProfile")]
+[GenerateBindings]
+public partial class SetSteamCmdProfileCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public string Username { get; set; } = string.Empty;
+
+ [Parameter(Mandatory = true, Position = 1)]
+ public string InstallDirectory { get; set; } = string.Empty;
+
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var profile = new SteamCmdProfile
+ {
+ Username = Username,
+ InstallDirectory = InstallDirectory
+ };
+
+ await _steamCmdService.SaveProfileAsync(profile);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "SaveProfileError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Stop-SteamInstallJob.cs b/LANCommander.SDK/PowerShell/Cmdlets/Stop-SteamInstallJob.cs
new file mode 100644
index 00000000..840f8d21
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Stop-SteamInstallJob.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsLifecycle.Stop, "SteamInstallJob")]
+[OutputType(typeof(bool))]
+[GenerateBindings]
+public partial class StopSteamInstallJobCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public Guid JobId { get; set; }
+
+ [ServiceDependency]
+ private ISteamCmdService _steamCmdService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamCmdService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamCmdService is not available in the PowerShell session"),
+ "SteamCmdServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var cancelled = await _steamCmdService.CancelInstallJobAsync(JobId);
+ WriteObject(cancelled);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "CancelInstallJobError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamManual.cs b/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamManual.cs
new file mode 100644
index 00000000..4e830406
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamManual.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam.Services;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsDiagnostic.Test, "SteamManual")]
+[OutputType(typeof(bool))]
+[GenerateBindings]
+public partial class TestSteamManualCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public int AppId { get; set; }
+
+ [ServiceDependency]
+ private SteamStoreService _steamStoreService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamStoreService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamStoreService is not available in the PowerShell session"),
+ "SteamStoreServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var exists = await _steamStoreService.HasManualAsync(AppId);
+ WriteObject(exists);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "HasManualError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamWebAsset.cs b/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamWebAsset.cs
new file mode 100644
index 00000000..ab6211b7
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Test-SteamWebAsset.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Management.Automation;
+using System.Threading;
+using System.Threading.Tasks;
+using LANCommander.Steam;
+using LANCommander.Steam.Services;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell.Cmdlets;
+
+[Cmdlet(VerbsDiagnostic.Test, "SteamWebAsset")]
+[OutputType(typeof(bool))]
+[GenerateBindings]
+public partial class TestSteamWebAssetCmdlet : DependencyCmdlet
+{
+ [Parameter(Mandatory = true, Position = 0)]
+ public int AppId { get; set; }
+
+ [Parameter(Mandatory = true, Position = 1)]
+ public WebAssetType WebAssetType { get; set; }
+
+ [ServiceDependency]
+ private SteamStoreService _steamStoreService;
+
+ public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
+ {
+ if (_steamStoreService == null)
+ {
+ WriteError(new ErrorRecord(
+ new InvalidOperationException("SteamStoreService is not available in the PowerShell session"),
+ "SteamStoreServiceNotAvailable",
+ ErrorCategory.InvalidOperation,
+ null));
+ return;
+ }
+
+ try
+ {
+ var (exists, _) = await _steamStoreService.HasWebAssetAsync(AppId, WebAssetType);
+ WriteObject(exists);
+ }
+ catch (Exception ex)
+ {
+ WriteError(new ErrorRecord(ex, "HasWebAssetError", ErrorCategory.OperationStopped, null));
+ }
+ }
+}
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Update-IniValue.cs b/LANCommander.SDK/PowerShell/Cmdlets/Update-IniValue.cs
index 734ccdd1..8f9ea1ea 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Update-IniValue.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Update-IniValue.cs
@@ -13,7 +13,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
///
[Cmdlet(VerbsData.Update, "IniValue")]
[OutputType(typeof(string))]
- public class UpdateIniValueCmdlet : BaseCmdlet
+ public class UpdateIniValueCmdlet : Cmdlet
{
///
/// Gets or sets the section in the INI file that contains the key to be updated.
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs b/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs
index bd93f5f9..5a0eca4c 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Update-UserCustomField.cs
@@ -5,7 +5,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsData.Update, "UserCustomField")]
[OutputType(typeof(string))]
- public class UpdateUserCustomFieldCmdlet(ProfileClient profileClient) : BaseCmdlet
+ public class UpdateUserCustomFieldCmdlet(ProfileClient profileClient) : Cmdlet
{
[Parameter(Mandatory = true, Position = 0)]
public string Name { get; set; }
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Write-GameManifest.cs b/LANCommander.SDK/PowerShell/Cmdlets/Write-GameManifest.cs
index 0d0b884c..c4e773b9 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Write-GameManifest.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Write-GameManifest.cs
@@ -6,7 +6,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
{
[Cmdlet(VerbsCommunications.Write, "GameManifest")]
[OutputType(typeof(string))]
- public class WriteGameManifestCmdlet : BaseCmdlet
+ public class WriteGameManifestCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Path { get; set; }
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/Write-ReplaceContentInFile.cs b/LANCommander.SDK/PowerShell/Cmdlets/Write-ReplaceContentInFile.cs
index bda0904b..9c7565fc 100644
--- a/LANCommander.SDK/PowerShell/Cmdlets/Write-ReplaceContentInFile.cs
+++ b/LANCommander.SDK/PowerShell/Cmdlets/Write-ReplaceContentInFile.cs
@@ -6,7 +6,7 @@ namespace LANCommander.SDK.PowerShell.Cmdlets
[Cmdlet(VerbsCommunications.Write, "ReplaceContentInFile")]
[OutputType(typeof(string))]
- public class ReplaceContentInFileCmdlet : BaseCmdlet
+ public class ReplaceContentInFileCmdlet : Cmdlet
{
[Parameter(Mandatory = true, Position = 0)]
public string Pattern { get; set; }
diff --git a/LANCommander.SDK/PowerShell/Cmdlets/_BaseCmdlet.cs b/LANCommander.SDK/PowerShell/Cmdlets/_BaseCmdlet.cs
deleted file mode 100644
index fc3684ff..00000000
--- a/LANCommander.SDK/PowerShell/Cmdlets/_BaseCmdlet.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using System.Management.Automation;
-
-namespace LANCommander.SDK.PowerShell.Cmdlets
-{
- public abstract class BaseCmdlet : Cmdlet
- {
- }
-}
diff --git a/LANCommander.SDK/PowerShell/Extensions/InitialSessionStateExtensions.cs b/LANCommander.SDK/PowerShell/Extensions/InitialSessionStateExtensions.cs
index 10fe1a56..6de22c6f 100644
--- a/LANCommander.SDK/PowerShell/Extensions/InitialSessionStateExtensions.cs
+++ b/LANCommander.SDK/PowerShell/Extensions/InitialSessionStateExtensions.cs
@@ -27,5 +27,28 @@ public static class InitialSessionStateExtensions
initialSessionState.Commands.Add(new SessionStateCmdletEntry("Update-UserCustomField", typeof(UpdateUserCustomFieldCmdlet), null));
initialSessionState.Commands.Add(new SessionStateCmdletEntry("Write-GameManifest", typeof(WriteGameManifestCmdlet), null));
initialSessionState.Commands.Add(new SessionStateCmdletEntry("Write-ReplaceContentInFile", typeof(ReplaceContentInFileCmdlet), null));
+
+ // SteamCMD cmdlets
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Connect-SteamCmd", typeof(ConnectSteamCmdCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Disconnect-SteamCmd", typeof(DisconnectSteamCmdCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamCmdConnectionStatus", typeof(GetSteamCmdConnectionStatusCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamCmdPath", typeof(GetSteamCmdPathCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamCmdProfile", typeof(GetSteamCmdProfileCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamCmdProfiles", typeof(GetSteamCmdProfilesCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamInstallJob", typeof(GetSteamInstallJobCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamInstallJobs", typeof(GetSteamInstallJobsCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Install-SteamContent", typeof(InstallSteamContentCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Remove-SteamContent", typeof(RemoveSteamContentCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Remove-SteamCmdProfile", typeof(RemoveSteamCmdProfileCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Set-SteamCmdProfile", typeof(SetSteamCmdProfileCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Stop-SteamInstallJob", typeof(StopSteamInstallJobCmdlet), null));
+
+ // Steam Store cmdlets
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamManual", typeof(GetSteamManualCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamManualUri", typeof(GetSteamManualUriCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Get-SteamWebAssetUri", typeof(GetSteamWebAssetUriCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Search-SteamGames", typeof(SearchSteamGamesCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Test-SteamManual", typeof(TestSteamManualCmdlet), null));
+ initialSessionState.Commands.Add(new SessionStateCmdletEntry("Test-SteamWebAsset", typeof(TestSteamWebAssetCmdlet), null));
}
}
\ No newline at end of file
diff --git a/LANCommander.SDK/PowerShell/PowerShellScript.cs b/LANCommander.SDK/PowerShell/PowerShellScript.cs
index 2f643cf2..f03d50ac 100644
--- a/LANCommander.SDK/PowerShell/PowerShellScript.cs
+++ b/LANCommander.SDK/PowerShell/PowerShellScript.cs
@@ -1,4 +1,4 @@
-using LANCommander.SDK.Enums;
+using LANCommander.SDK.Enums;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
diff --git a/LANCommander.SDK/PowerShell/PowerShellStartup.cs b/LANCommander.SDK/PowerShell/PowerShellStartup.cs
new file mode 100644
index 00000000..556374db
--- /dev/null
+++ b/LANCommander.SDK/PowerShell/PowerShellStartup.cs
@@ -0,0 +1,18 @@
+using LANCommander.SDK.Extensions;
+using LANCommander.SDK.Models;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Svrooij.PowerShell.DI;
+
+namespace LANCommander.SDK.PowerShell;
+
+public class PowerShellStartup : PsStartup
+{
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ services.AddLANCommanderClient();
+ services.AddScoped();
+ services.AddScoped();
+ }
+}
\ No newline at end of file
diff --git a/LANCommander.SDK/Providers/SteamCmdProfileStore.cs b/LANCommander.SDK/Providers/SteamCmdProfileStore.cs
new file mode 100644
index 00000000..530d2333
--- /dev/null
+++ b/LANCommander.SDK/Providers/SteamCmdProfileStore.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using LANCommander.SDK.Abstractions;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Models;
+
+namespace LANCommander.SDK.Providers;
+
+public class SteamCmdProfileStore(ISettingsProvider settingsProvider) : ISteamCmdProfileStore
+{
+ public async Task> GetAllAsync()
+ => settingsProvider.CurrentValue.Steam.Profiles;
+
+ public async Task GetByUsernameAsync(string username)
+ => settingsProvider.CurrentValue.Steam.Profiles.FirstOrDefault(p => p.Username == username);
+
+ public async Task SaveAsync(SteamCmdProfile profile)
+ {
+ settingsProvider.Update(s =>
+ {
+ var existing = s.Steam.Profiles.FirstOrDefault(p => p.Username == profile.Username);
+
+ if (existing != null)
+ {
+ // Update existing profile
+ existing.InstallDirectory = profile.InstallDirectory;
+ }
+ else
+ {
+ // Add new profile
+ s.Steam.Profiles.Add(profile);
+ }
+ });
+ }
+
+ public async Task DeleteAsync(string username)
+ {
+ settingsProvider.Update(s =>
+ {
+ var existing = s.Steam.Profiles.FirstOrDefault(p => p.Username == username);
+
+ if (existing != null)
+ {
+ s.Steam.Profiles.Remove(existing);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/LANCommander.Server.Services/Extensions/IServiceCollectionExtensions.cs b/LANCommander.Server.Services/Extensions/IServiceCollectionExtensions.cs
index 4c15f5bc..1352d503 100644
--- a/LANCommander.Server.Services/Extensions/IServiceCollectionExtensions.cs
+++ b/LANCommander.Server.Services/Extensions/IServiceCollectionExtensions.cs
@@ -55,7 +55,6 @@ public static class IServiceCollectionExtensions
services.AddScoped();
services.AddScoped();
services.AddScoped();
- services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
diff --git a/LANCommander.Server.Services/SteamCMDService.cs b/LANCommander.Server.Services/SteamCMDService.cs
deleted file mode 100644
index e5b224ed..00000000
--- a/LANCommander.Server.Services/SteamCMDService.cs
+++ /dev/null
@@ -1,441 +0,0 @@
-using LANCommander.Server.Services.Enums;
-using Microsoft.Extensions.Logging;
-using System.Diagnostics;
-using System.Text;
-using System.Text.RegularExpressions;
-
-namespace LANCommander.Server.Services;
-
-public class SteamCMDService(
- ILogger logger,
- SettingsProvider settingsProvider) : BaseService(logger, settingsProvider)
-{
- public async Task GetConnectionStatusAsync(string username)
- {
- try
- {
- if (!IsValidUsername(username))
- throw new ArgumentException("Invalid username", nameof(username));
-
- // Auto-populate SteamCMD path if not configured
- if (string.IsNullOrWhiteSpace(_settingsProvider.CurrentValue.Server.SteamCMD.Path))
- {
- var detectedPath = await AutoDetectSteamCmdPathAsync();
-
- if (!string.IsNullOrWhiteSpace(detectedPath))
- {
- _settingsProvider.Update(s =>
- {
- s.Server.SteamCMD.Path = detectedPath;
- });
-
- _logger.LogInformation("Auto-detected SteamCMD at: {Path}", detectedPath);
- }
- else
- {
- _logger.LogWarning("SteamCMD path is not configured and could not be auto-detected");
- return SteamCmdConnectionStatus.NotInstalled;
- }
- }
-
- if (!File.Exists(_settingsProvider.CurrentValue.Server.SteamCMD.Path))
- {
- _logger.LogWarning("SteamCMD executable not found at configured path: {Path}", _settingsProvider.CurrentValue.Server.SteamCMD.Path);
- return SteamCmdConnectionStatus.NotInstalled;
- }
-
- // Try to run steamcmd with +quit to check if it's working
- var result = await ExecuteSteamCmdCommandAsync("+quit");
-
- if (result.Success)
- {
- // Check if we're logged in by trying to get user info
- var loginResult = await ExecuteSteamCmdCommandAsync($"+login {username} +quit", TimeSpan.FromSeconds(30));
- return loginResult.Success ? SteamCmdConnectionStatus.Authenticated : SteamCmdConnectionStatus.Unauthenticated;
- }
-
- return SteamCmdConnectionStatus.NotInstalled;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error checking SteamCMD connection status");
- return SteamCmdConnectionStatus.NotInstalled;
- }
- }
-
- public async Task AutoDetectSteamCmdPathAsync()
- {
- var possiblePaths = new List();
-
- // Windows paths
- if (OperatingSystem.IsWindows())
- {
- // Common Steam installation directories
- var steamPaths = new[]
- {
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Steam", "steamcmd.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Steam", "steamcmd.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Steam", "steamcmd.exe"),
- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Steam", "steamcmd.exe"),
- "C:\\Steam\\steamcmd.exe",
- "C:\\Program Files\\Steam\\steamcmd.exe",
- "C:\\Program Files (x86)\\Steam\\steamcmd.exe"
- };
-
- possiblePaths.AddRange(steamPaths);
-
- // Check PATH environment variable
- var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? [];
-
- foreach (var dir in pathDirs)
- {
- if (!string.IsNullOrWhiteSpace(dir))
- {
- possiblePaths.Add(Path.Combine(dir, "steamcmd.exe"));
- }
- }
- }
- // Linux paths
- else if (OperatingSystem.IsLinux())
- {
- possiblePaths.AddRange(new[]
- {
- "/app/Data/Steam/steamcmd.sh",
- "/usr/local/bin/steamcmd",
- "/usr/bin/steamcmd",
- "/home/steam/steamcmd/steamcmd.sh",
- "/opt/steamcmd/steamcmd.sh",
- "/var/lib/steam/steamcmd/steamcmd.sh"
- });
-
- // Check PATH environment variable
- var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(':') ?? [];
-
- foreach (var dir in pathDirs)
- {
- if (!string.IsNullOrWhiteSpace(dir))
- {
- possiblePaths.Add(Path.Combine(dir, "steamcmd"));
- }
- }
- }
- // macOS paths
- else if (OperatingSystem.IsMacOS())
- {
- possiblePaths.AddRange(new[]
- {
- "/usr/local/bin/steamcmd",
- "/opt/homebrew/bin/steamcmd",
- "/Applications/Steam.app/Contents/MacOS/steamcmd"
- });
-
- // Check PATH environment variable
- var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(':') ?? [];
-
- foreach (var dir in pathDirs)
- {
- if (!string.IsNullOrWhiteSpace(dir))
- {
- possiblePaths.Add(Path.Combine(dir, "steamcmd"));
- }
- }
- }
-
- // Check each possible path
- foreach (var path in possiblePaths)
- {
- if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
- {
- try
- {
- // Verify it's actually SteamCMD by checking if it responds to --version or +quit
- var testResult = await ExecuteSteamCmdCommandAsync("+quit", TimeSpan.FromSeconds(30), path);
-
- if (testResult.Success)
- {
- _logger.LogDebug("Found SteamCMD at: {Path}", path);
- return path;
- }
- }
- catch (Exception ex)
- {
- _logger.LogDebug(ex, "Failed to verify SteamCMD at: {Path}", path);
- }
- }
- }
-
- _logger.LogWarning("SteamCMD not found in common installation locations");
-
- return String.Empty;
- }
-
- public async Task LoginToSteamAsync(string username, string? password = null)
- {
- try
- {
- if (!IsValidUsername(username))
- throw new ArgumentException("Invalid username", nameof(username));
-
- if (string.IsNullOrWhiteSpace(_settingsProvider.CurrentValue.Server.SteamCMD.Path))
- {
- _logger.LogError("SteamCMD path is not configured");
- return SteamCmdStatus.PathNotConfigured;
- }
-
- if (!File.Exists(_settingsProvider.CurrentValue.Server.SteamCMD.Path))
- {
- _logger.LogError("SteamCMD executable not found at configured path: {Path}", _settingsProvider.CurrentValue.Server.SteamCMD.Path);
- return SteamCmdStatus.ExecutableNotFound;
- }
-
- var loginCommand = string.IsNullOrWhiteSpace(password)
- ? $"+login {username}"
- : $"+login {username} {password}";
-
- var result = await ExecuteSteamCmdCommandAsync($"{loginCommand} +quit", TimeSpan.FromSeconds(30));
-
- if (result.Output.Contains("Invalid Password"))
- return SteamCmdStatus.InvalidPassword;
-
- if (result.Success)
- {
- _logger.LogInformation("Successfully logged into Steam as {Username}", username);
- return SteamCmdStatus.Success;
- }
-
- _logger.LogError("Failed to log into Steam: {Error}", result.ErrorOutput);
- return SteamCmdStatus.Error;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error logging into Steam");
- return SteamCmdStatus.UnknownError;
- }
- }
-
- public async Task LogoutAsync(string username)
- {
- try
- {
- if (!IsValidUsername(username))
- return SteamCmdStatus.InvalidUsername;
-
- if (string.IsNullOrWhiteSpace(_settingsProvider.CurrentValue.Server.SteamCMD.Path))
- {
- _logger.LogError("SteamCMD path is not configured");
- return SteamCmdStatus.PathNotConfigured;
- }
-
- if (!File.Exists(_settingsProvider.CurrentValue.Server.SteamCMD.Path))
- {
- _logger.LogError("SteamCMD executable not found at configured path: {Path}", _settingsProvider.CurrentValue.Server.SteamCMD.Path);
- return SteamCmdStatus.ExecutableNotFound;
- }
-
- var logoutCommand = $"+logout {username}";
-
- var result = await ExecuteSteamCmdCommandAsync($"{logoutCommand} +quit");
-
- if (result.Success)
- {
- _logger.LogInformation("Successfully logged out of the Steam account {Username}", username);
- return SteamCmdStatus.Success;
- }
-
- _logger.LogError("Failed to log out of Steam: {Error}", result.ErrorOutput);
- return SteamCmdStatus.Error;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error logging out of Steam");
- return SteamCmdStatus.UnknownError;
- }
- }
-
- public async Task InstallContentAsync(uint appId, string installDirectory, string username)
- {
- try
- {
- if (!IsValidUsername(username))
- throw new ArgumentException("Invalid username", nameof(username));
-
- if (string.IsNullOrWhiteSpace(_settingsProvider.CurrentValue.Server.SteamCMD.Path))
- {
- _logger.LogError("SteamCMD path is not configured");
- return SteamCmdStatus.PathNotConfigured;
- }
-
- if (!File.Exists(_settingsProvider.CurrentValue.Server.SteamCMD.Path))
- {
- _logger.LogError("SteamCMD executable not found at configured path: {Path}", _settingsProvider.CurrentValue.Server.SteamCMD.Path);
- return SteamCmdStatus.ExecutableNotFound;
- }
-
- // Ensure install directory exists
- Directory.CreateDirectory(installDirectory);
-
- var commands = new List();
-
- // Add login command if credentials provided
- if (!string.IsNullOrWhiteSpace(username))
- {
- var loginCommand = $"+login {username}";
-
- commands.Add(loginCommand);
- }
- else
- {
- commands.Add("+login anonymous");
- }
-
- // Add install commands
- commands.Add($"+force_install_dir \"{installDirectory}\"");
- commands.Add($"+app_update {appId} validate");
- commands.Add("+quit");
-
- var commandString = string.Join(" ", commands);
- var result = await ExecuteSteamCmdCommandAsync(commandString);
-
- if (result.Success)
- {
- _logger.LogInformation("Successfully installed Steam app {AppId} to {InstallDirectory}", appId, installDirectory);
- return SteamCmdStatus.Success;
- }
-
- _logger.LogError("Failed to install Steam app {AppId}: {Error}", appId, result.ErrorOutput);
- return SteamCmdStatus.Error;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error installing Steam content for app {AppId}", appId);
- return SteamCmdStatus.UnknownError;
- }
- }
-
- public async Task RemoveContentAsync(string installDirectory)
- {
- try
- {
- if (string.IsNullOrWhiteSpace(installDirectory))
- {
- _logger.LogError("Install directory is not specified");
- return SteamCmdStatus.InstallDirectoryNotFound;
- }
-
- if (!Directory.Exists(installDirectory))
- {
- _logger.LogWarning("Install directory does not exist: {InstallDirectory}", installDirectory);
- return SteamCmdStatus.Success; // Consider it already removed
- }
-
- // Remove the directory and all its contents
- Directory.Delete(installDirectory, true);
-
- _logger.LogInformation("Successfully removed content from {InstallDirectory}", installDirectory);
- return SteamCmdStatus.Success;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error removing content from {InstallDirectory}", installDirectory);
- return SteamCmdStatus.UnknownError;
- }
- }
-
- private async Task ExecuteSteamCmdCommandAsync(string arguments, TimeSpan? timeout = null, string steamCmdPath = "")
- {
- try
- {
- var executablePath = String.IsNullOrWhiteSpace(steamCmdPath) ? _settingsProvider.CurrentValue.Server.SteamCMD.Path : steamCmdPath;
-
- var processStartInfo = new ProcessStartInfo
- {
- FileName = executablePath,
- Arguments = arguments,
- UseShellExecute = false,
- RedirectStandardOutput = true,
- RedirectStandardError = true,
- CreateNoWindow = true,
- StandardOutputEncoding = Encoding.UTF8,
- StandardErrorEncoding = Encoding.UTF8
- };
-
- using var process = new Process { StartInfo = processStartInfo };
- var output = new StringBuilder();
- var error = new StringBuilder();
-
- process.OutputDataReceived += (sender, e) =>
- {
- if (!string.IsNullOrEmpty(e.Data))
- {
- output.AppendLine(e.Data);
- _logger.LogDebug("SteamCMD Output: {Output}", e.Data);
- }
- };
-
- process.ErrorDataReceived += (sender, e) =>
- {
- if (!string.IsNullOrEmpty(e.Data))
- {
- error.AppendLine(e.Data);
- _logger.LogDebug("SteamCMD Error: {Error}", e.Data);
- }
- };
-
- process.Start();
-
- process.BeginOutputReadLine();
- process.BeginErrorReadLine();
-
- try
- {
- if (timeout.HasValue)
- await process.WaitForExitAsync().WaitAsync(timeout.Value);
- else
- await process.WaitForExitAsync();
- }
- catch (TimeoutException)
- {
- _logger.LogWarning("SteamCMD timed out");
-
- process.Kill();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error while executing SteamCMD");
- }
-
- return new SteamCmdResult
- {
- Success = process.ExitCode == 0,
- ExitCode = process.ExitCode,
- Output = output.ToString(),
- ErrorOutput = error.ToString()
- };
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error executing SteamCMD command: {Arguments}", arguments);
-
- return new SteamCmdResult
- {
- Success = false,
- ExitCode = -1,
- ErrorOutput = ex.Message
- };
- }
- }
-
- private static bool IsValidUsername(string value)
- {
- return !string.IsNullOrEmpty(value) &&
- Regex.IsMatch(value, @"^[a-zA-Z0-9_]+$");
- }
-
- private class SteamCmdResult
- {
- public bool Success { get; set; }
- public int ExitCode { get; set; }
- public string Output { get; set; } = string.Empty;
- public string ErrorOutput { get; set; } = string.Empty;
- }
-}
\ No newline at end of file
diff --git a/LANCommander.Server.Settings/Models/ServerSettings.cs b/LANCommander.Server.Settings/Models/ServerSettings.cs
index d9438616..806c4f25 100644
--- a/LANCommander.Server.Settings/Models/ServerSettings.cs
+++ b/LANCommander.Server.Settings/Models/ServerSettings.cs
@@ -17,7 +17,6 @@ public class ServerSettings
public RoleSettings Roles { get; set; } = new();
public ScriptSettings Scripts { get; set; } = new();
public GameServerSettings GameServers { get; set; } = new();
- public SteamCmdSettings SteamCMD { get; set; } = new();
public UpdateSettings Update { get; set; } = new();
public UserSaveSettings UserSaves { get; set; } = new();
}
\ No newline at end of file
diff --git a/LANCommander.Server.Settings/Models/SteamCmdProfile.cs b/LANCommander.Server.Settings/Models/SteamCmdProfile.cs
deleted file mode 100644
index bad12b96..00000000
--- a/LANCommander.Server.Settings/Models/SteamCmdProfile.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace LANCommander.Server.Settings.Models;
-
-public class SteamCmdProfile
-{
- public string Username { get; set; } = String.Empty;
- public string InstallDirectory { get; set; } = String.Empty;
-}
\ No newline at end of file
diff --git a/LANCommander.Server.Settings/Models/SteamCmdSettings.cs b/LANCommander.Server.Settings/Models/SteamCmdSettings.cs
deleted file mode 100644
index 67b00369..00000000
--- a/LANCommander.Server.Settings/Models/SteamCmdSettings.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace LANCommander.Server.Settings.Models;
-
-public class SteamCmdSettings
-{
- public string Path { get; set; } = String.Empty;
- public string InstallDirectory { get; set; } = "";
- public ICollection Profiles { get; set; } = [];
-}
\ No newline at end of file
diff --git a/LANCommander.Server/Program.cs b/LANCommander.Server/Program.cs
index d721d839..a1a69e4a 100644
--- a/LANCommander.Server/Program.cs
+++ b/LANCommander.Server/Program.cs
@@ -23,6 +23,7 @@ builder.AddServerProcessStatusMonitor();
builder.AddLANCommanderServices();
builder.AddMigrations();
builder.AddDatabase(args);
+builder.UseSteam();
builder.Services.AddHealthChecks();
diff --git a/LANCommander.Server/Startup/Steam.cs b/LANCommander.Server/Startup/Steam.cs
new file mode 100644
index 00000000..d409dd45
--- /dev/null
+++ b/LANCommander.Server/Startup/Steam.cs
@@ -0,0 +1,18 @@
+using LANCommander.Steam.Extensions;
+
+namespace LANCommander.Server.Startup;
+
+public static class Steam
+{
+ public static WebApplicationBuilder UseSteam(this WebApplicationBuilder builder)
+ {
+ var settings = builder.Configuration.Get();
+
+ builder.Services.AddSteamCmd(o =>
+ {
+ o.ExecutablePath = settings?.Steam.Path;
+ });
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/LANCommander.Server/UI/Pages/Settings/Integrations/Steam.razor b/LANCommander.Server/UI/Pages/Settings/Integrations/Steam.razor
index 3d43f867..f0def24e 100644
--- a/LANCommander.Server/UI/Pages/Settings/Integrations/Steam.razor
+++ b/LANCommander.Server/UI/Pages/Settings/Integrations/Steam.razor
@@ -1,10 +1,12 @@
@page "/Settings/Integrations/Steam"
-@using LANCommander.Server.Services.Enums
@using LANCommander.Server.Settings
-@using LANCommander.Server.Settings.Models
@using LANCommander.Server.UI.Pages.Settings.Components
+@using LANCommander.Steam.Abstractions
+@using LANCommander.Steam.Enums
+@using LANCommander.Steam.Models
@using Microsoft.Extensions.Options
-@inject SteamCMDService SteamCmdService
+@inject ISteamCmdService SteamCmdService
+@inject ISteamCmdProfileStore SteamCmdProfileStore
@inject IOptions Settings
@inject SettingsProvider SettingsProvider
@inject IMessageService MessageService
@@ -20,7 +22,7 @@
@@ -28,7 +30,7 @@
- @foreach (var profile in Settings.Value.Server.SteamCMD.Profiles)
+ @foreach (var profile in _profiles)
{
@@ -57,6 +59,8 @@
SteamCmdConnectionStatus _connectionStatus;
+ List _profiles = [];
+
string _username;
string _password;
@@ -64,35 +68,20 @@
protected override async Task OnInitializedAsync()
{
- if (!Settings.Value.Server.SteamCMD.Profiles.Any())
+ _profiles = (await SteamCmdService.GetProfilesAsync()).ToList();
+
+ if (!_profiles.Any())
AddProfile();
-
- if (string.IsNullOrWhiteSpace(Settings.Value.Server.SteamCMD.Path))
- {
- var detectedPath = await SteamCmdService.AutoDetectSteamCmdPathAsync();
-
- if (!string.IsNullOrWhiteSpace(detectedPath))
- {
- Settings.Value.Server.SteamCMD.Path = detectedPath;
-
- SettingsProvider.Update(s =>
- {
- s.Server.SteamCMD.Path = detectedPath;
- });
- }
- }
}
void AddProfile()
{
- Settings.Value.Server.SteamCMD.Profiles.Add(new SteamCmdProfile());
+ _profiles.Add(new SteamCmdProfile());
}
async Task RemoveProfile(SteamCmdProfile profile)
{
- Settings.Value.Server.SteamCMD.Profiles.Remove(profile);
-
- if (!String.IsNullOrWhiteSpace(profile.Username))
+ if (!string.IsNullOrWhiteSpace(profile.Username))
{
var logoutMessage = new MessageConfig
{
@@ -104,14 +93,17 @@
MessageService.Loading(logoutMessage);
await SteamCmdService.LogoutAsync(profile.Username);
+ await SteamCmdService.DeleteProfileAsync(profile.Username);
logoutMessage.Content = "Successfully logged out from Steam!";
logoutMessage.Duration = 3;
+ _profiles = (await SteamCmdService.GetProfilesAsync()).ToList();
+
MessageService.Success(logoutMessage);
}
- if (!Settings.Value.Server.SteamCMD.Profiles.Any())
+ if (!_profiles.Any())
AddProfile();
}
@@ -119,12 +111,10 @@
{
try
{
- Settings.Value.Server.SteamCMD.Profiles = Settings.Value.Server.SteamCMD.Profiles.Where(p => !String.IsNullOrWhiteSpace(p.Username)).ToList();
-
- SettingsProvider.Update(s =>
+ foreach (var profile in _profiles)
{
- s.Server.SteamCMD = Settings.Value.Server.SteamCMD;
- });
+ await SteamCmdService.SaveProfileAsync(profile);
+ }
MessageService.Success("Settings saved!");
}
diff --git a/LANCommander.Steam/Abstractions/ISteamCmdProfileStore.cs b/LANCommander.Steam/Abstractions/ISteamCmdProfileStore.cs
new file mode 100644
index 00000000..dcf8733f
--- /dev/null
+++ b/LANCommander.Steam/Abstractions/ISteamCmdProfileStore.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using LANCommander.Steam.Models;
+
+namespace LANCommander.Steam.Abstractions;
+
+///
+/// Interface for storing and retrieving SteamCMD profiles
+/// Allows consumers to implement their own storage mechanism
+///
+public interface ISteamCmdProfileStore
+{
+ ///
+ /// Get all profiles
+ ///
+ Task> GetAllAsync();
+
+ ///
+ /// Get a profile by username
+ ///
+ Task GetByUsernameAsync(string username);
+
+ ///
+ /// Save or update a profile
+ ///
+ Task SaveAsync(SteamCmdProfile profile);
+
+ ///
+ /// Delete a profile by username
+ ///
+ Task DeleteAsync(string username);
+}
diff --git a/LANCommander.Steam/Abstractions/ISteamCmdService.cs b/LANCommander.Steam/Abstractions/ISteamCmdService.cs
new file mode 100644
index 00000000..39bf9fb1
--- /dev/null
+++ b/LANCommander.Steam/Abstractions/ISteamCmdService.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using LANCommander.Steam.Enums;
+using LANCommander.Steam.Events;
+using LANCommander.Steam.Models;
+
+namespace LANCommander.Steam.Abstractions;
+
+///
+/// Interface for SteamCMD service operations
+///
+public interface ISteamCmdService
+{
+ ///
+ /// Get or set the SteamCMD executable path
+ ///
+ string? ExecutablePath { get; set; }
+
+ ///
+ /// Event fired when an install job status changes (started, completed, failed)
+ ///
+ event EventHandler? InstallStatusChanged;
+
+ ///
+ /// Event fired when install progress is updated
+ ///
+ event EventHandler? InstallProgress;
+
+ ///
+ /// Check the connection status for a username
+ ///
+ Task GetConnectionStatusAsync(string username);
+
+ ///
+ /// Auto-detect the SteamCMD executable path
+ ///
+ Task AutoDetectSteamCmdPathAsync();
+
+ ///
+ /// Login to Steam with username and optional password
+ ///
+ Task LoginToSteamAsync(string username, string? password = null);
+
+ ///
+ /// Logout from Steam
+ ///
+ Task LogoutAsync(string username);
+
+ ///
+ /// Queue an installation job for Steam content
+ /// Returns a job ID that can be used to track progress
+ ///
+ Task InstallContentAsync(uint appId, string installDirectory, string? username = null);
+
+ ///
+ /// Get an install job by ID
+ ///
+ SteamCmdInstallJob? GetInstallJob(Guid jobId);
+
+ ///
+ /// Get all install jobs
+ ///
+ IEnumerable GetInstallJobs();
+
+ ///
+ /// Cancel an install job
+ ///
+ Task CancelInstallJobAsync(Guid jobId);
+
+ ///
+ /// Remove installed content from a directory
+ ///
+ Task RemoveContentAsync(string installDirectory);
+
+ ///
+ /// Get all profiles (requires profile store to be configured)
+ ///
+ Task> GetProfilesAsync();
+
+ ///
+ /// Get a profile by username (requires profile store to be configured)
+ ///
+ Task GetProfileAsync(string username);
+
+ ///
+ /// Save a profile (requires profile store to be configured)
+ ///
+ Task SaveProfileAsync(SteamCmdProfile profile);
+
+ ///
+ /// Delete a profile by username (requires profile store to be configured)
+ ///
+ Task DeleteProfileAsync(string username);
+}
diff --git a/LANCommander.Server.Services/Enums/SteamCMDConnectionStatus.cs b/LANCommander.Steam/Enums/SteamCmdConnectionStatus.cs
similarity index 70%
rename from LANCommander.Server.Services/Enums/SteamCMDConnectionStatus.cs
rename to LANCommander.Steam/Enums/SteamCmdConnectionStatus.cs
index bf122348..4ca549a7 100644
--- a/LANCommander.Server.Services/Enums/SteamCMDConnectionStatus.cs
+++ b/LANCommander.Steam/Enums/SteamCmdConnectionStatus.cs
@@ -1,4 +1,4 @@
-namespace LANCommander.Server.Services.Enums;
+namespace LANCommander.Steam.Enums;
public enum SteamCmdConnectionStatus
{
diff --git a/LANCommander.Steam/Enums/SteamCmdInstallStatus.cs b/LANCommander.Steam/Enums/SteamCmdInstallStatus.cs
new file mode 100644
index 00000000..6c7a9799
--- /dev/null
+++ b/LANCommander.Steam/Enums/SteamCmdInstallStatus.cs
@@ -0,0 +1,32 @@
+namespace LANCommander.Steam.Enums;
+
+///
+/// Status of an installation job
+///
+public enum SteamCmdInstallStatus
+{
+ ///
+ /// Job is queued and waiting to start
+ ///
+ Queued,
+
+ ///
+ /// Job is currently being processed
+ ///
+ InProgress,
+
+ ///
+ /// Job completed successfully
+ ///
+ Completed,
+
+ ///
+ /// Job failed
+ ///
+ Failed,
+
+ ///
+ /// Job was cancelled
+ ///
+ Cancelled
+}
diff --git a/LANCommander.Server.Services/Enums/SteamCmdResult.cs b/LANCommander.Steam/Enums/SteamCmdResult.cs
similarity index 80%
rename from LANCommander.Server.Services/Enums/SteamCmdResult.cs
rename to LANCommander.Steam/Enums/SteamCmdResult.cs
index db338b61..813ef040 100644
--- a/LANCommander.Server.Services/Enums/SteamCmdResult.cs
+++ b/LANCommander.Steam/Enums/SteamCmdResult.cs
@@ -1,4 +1,4 @@
-namespace LANCommander.Server.Services.Enums;
+namespace LANCommander.Steam.Enums;
public enum SteamCmdStatus
{
diff --git a/LANCommander.Steam/Events/SteamCmdInstallProgressEventArgs.cs b/LANCommander.Steam/Events/SteamCmdInstallProgressEventArgs.cs
new file mode 100644
index 00000000..568acff0
--- /dev/null
+++ b/LANCommander.Steam/Events/SteamCmdInstallProgressEventArgs.cs
@@ -0,0 +1,56 @@
+using System;
+using LANCommander.Steam.Models;
+
+namespace LANCommander.Steam.Events;
+
+///
+/// Event arguments for install progress updates
+///
+public class SteamCmdInstallProgressEventArgs : EventArgs
+{
+ ///
+ /// The install job this progress update is for
+ ///
+ public SteamCmdInstallJob Job { get; }
+
+ ///
+ /// Progress percentage (0-100)
+ ///
+ public double Progress { get; }
+
+ ///
+ /// Status message
+ ///
+ public string StatusMessage { get; }
+
+ ///
+ /// Bytes downloaded
+ ///
+ public long BytesDownloaded { get; }
+
+ ///
+ /// Total bytes to download
+ ///
+ public long BytesTotal { get; }
+
+ ///
+ /// Download speed in bytes per second
+ ///
+ public long BytesPerSecond { get; }
+
+ public SteamCmdInstallProgressEventArgs(
+ SteamCmdInstallJob job,
+ double progress,
+ string statusMessage,
+ long bytesDownloaded = 0,
+ long bytesTotal = 0,
+ long bytesPerSecond = 0)
+ {
+ Job = job;
+ Progress = progress;
+ StatusMessage = statusMessage;
+ BytesDownloaded = bytesDownloaded;
+ BytesTotal = bytesTotal;
+ BytesPerSecond = bytesPerSecond;
+ }
+}
diff --git a/LANCommander.Steam/Events/SteamCmdInstallStatusEventArgs.cs b/LANCommander.Steam/Events/SteamCmdInstallStatusEventArgs.cs
new file mode 100644
index 00000000..93465c31
--- /dev/null
+++ b/LANCommander.Steam/Events/SteamCmdInstallStatusEventArgs.cs
@@ -0,0 +1,33 @@
+using System;
+using LANCommander.Steam.Enums;
+using LANCommander.Steam.Models;
+
+namespace LANCommander.Steam.Events;
+
+///
+/// Event arguments for install status changes (started, completed, failed)
+///
+public class SteamCmdInstallStatusEventArgs : EventArgs
+{
+ ///
+ /// The install job this status change is for
+ ///
+ public SteamCmdInstallJob Job { get; }
+
+ ///
+ /// The new status
+ ///
+ public SteamCmdInstallStatus Status { get; }
+
+ ///
+ /// Error message if the status is Failed
+ ///
+ public string? ErrorMessage { get; }
+
+ public SteamCmdInstallStatusEventArgs(SteamCmdInstallJob job, SteamCmdInstallStatus status, string? errorMessage = null)
+ {
+ Job = job;
+ Status = status;
+ ErrorMessage = errorMessage;
+ }
+}
diff --git a/LANCommander.Steam/Extensions/ServiceCollectionExtensions.cs b/LANCommander.Steam/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 00000000..9f64573d
--- /dev/null
+++ b/LANCommander.Steam/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,81 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Implementations;
+using LANCommander.Steam.Options;
+using LANCommander.Steam.Services;
+
+namespace LANCommander.Steam.Extensions;
+
+///
+/// Extension methods for registering SteamCMD services
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Add SteamCMD service with default in-memory profile store
+ ///
+ public static IServiceCollection AddSteamCmd(this IServiceCollection services, Action? configure = null)
+ {
+ if (configure != null)
+ {
+ services.Configure(configure);
+ }
+ else
+ {
+ services.Configure(_ => { });
+ }
+
+ services.AddSingleton();
+ services.AddScoped();
+
+ return services;
+ }
+
+ ///
+ /// Add SteamCMD service with custom profile store implementation
+ ///
+ public static IServiceCollection AddSteamCmd(
+ this IServiceCollection services,
+ Action? configure = null)
+ where TProfileStore : class, ISteamCmdProfileStore
+ {
+ if (configure != null)
+ {
+ services.Configure(configure);
+ }
+ else
+ {
+ services.Configure(_ => { });
+ }
+
+ services.AddSingleton();
+ services.AddScoped();
+
+ return services;
+ }
+
+ ///
+ /// Add SteamCMD service with existing profile store instance
+ ///
+ public static IServiceCollection AddSteamCmd(
+ this IServiceCollection services,
+ ISteamCmdProfileStore profileStore,
+ Action? configure = null)
+ {
+ if (configure != null)
+ {
+ services.Configure(configure);
+ }
+ else
+ {
+ services.Configure(_ => { });
+ }
+
+ services.AddSingleton(profileStore);
+ services.AddScoped();
+
+ return services;
+ }
+}
diff --git a/LANCommander.Steam/Implementations/InMemorySteamCmdProfileStore.cs b/LANCommander.Steam/Implementations/InMemorySteamCmdProfileStore.cs
new file mode 100644
index 00000000..f87283e6
--- /dev/null
+++ b/LANCommander.Steam/Implementations/InMemorySteamCmdProfileStore.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Models;
+
+namespace LANCommander.Steam.Implementations;
+
+///
+/// In-memory implementation of ISteamCmdProfileStore
+/// Useful for testing or simple scenarios
+///
+public class InMemorySteamCmdProfileStore : ISteamCmdProfileStore
+{
+ private readonly Dictionary _profiles = new();
+
+ public Task> GetAllAsync()
+ {
+ return Task.FromResult>(_profiles.Values.ToList());
+ }
+
+ public Task GetByUsernameAsync(string username)
+ {
+ _profiles.TryGetValue(username, out var profile);
+ return Task.FromResult(profile);
+ }
+
+ public Task SaveAsync(SteamCmdProfile profile)
+ {
+ _profiles[profile.Username] = profile;
+ return Task.CompletedTask;
+ }
+
+ public Task DeleteAsync(string username)
+ {
+ _profiles.Remove(username);
+ return Task.CompletedTask;
+ }
+}
diff --git a/LANCommander.Steam/LANCommander.Steam.csproj b/LANCommander.Steam/LANCommander.Steam.csproj
index 7f24897d..83631051 100644
--- a/LANCommander.Steam/LANCommander.Steam.csproj
+++ b/LANCommander.Steam/LANCommander.Steam.csproj
@@ -1,13 +1,21 @@
- netstandard2.0
+ net9.0
+ latestmajor
+
+
+
+
+
+
+
diff --git a/LANCommander.Steam/Models/SteamCmdInstallJob.cs b/LANCommander.Steam/Models/SteamCmdInstallJob.cs
new file mode 100644
index 00000000..2226c917
--- /dev/null
+++ b/LANCommander.Steam/Models/SteamCmdInstallJob.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Threading.Tasks;
+using LANCommander.Steam.Enums;
+
+namespace LANCommander.Steam.Models;
+
+///
+/// Represents an installation job in the queue
+///
+public class SteamCmdInstallJob
+{
+ ///
+ /// Unique identifier for this install job
+ ///
+ public Guid Id { get; set; } = Guid.NewGuid();
+
+ ///
+ /// Steam App ID to install
+ ///
+ public uint AppId { get; set; }
+
+ ///
+ /// Installation directory
+ ///
+ public string InstallDirectory { get; set; } = string.Empty;
+
+ ///
+ /// Username for Steam login (null for anonymous)
+ ///
+ public string? Username { get; set; }
+
+ ///
+ /// Current status of the installation
+ ///
+ public SteamCmdInstallStatus Status { get; set; } = SteamCmdInstallStatus.Queued;
+
+ ///
+ /// Progress percentage (0-100)
+ ///
+ public double Progress { get; set; }
+
+ ///
+ /// Current status message
+ ///
+ public string StatusMessage { get; set; } = string.Empty;
+
+ ///
+ /// Bytes downloaded
+ ///
+ public long BytesDownloaded { get; set; }
+
+ ///
+ /// Total bytes to download
+ ///
+ public long BytesTotal { get; set; }
+
+ ///
+ /// Download speed in bytes per second
+ ///
+ public long BytesPerSecond { get; set; }
+
+ ///
+ /// When the job was created
+ ///
+ public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+
+ ///
+ /// When the job started processing
+ ///
+ public DateTime? StartedAt { get; set; }
+
+ ///
+ /// When the job completed
+ ///
+ public DateTime? CompletedAt { get; set; }
+
+ ///
+ /// Error message if the job failed
+ ///
+ public string? ErrorMessage { get; set; }
+
+ ///
+ /// Task completion source for awaiting the job
+ ///
+ internal TaskCompletionSource? CompletionSource { get; set; }
+}
diff --git a/LANCommander.Steam/Models/SteamCmdProfile.cs b/LANCommander.Steam/Models/SteamCmdProfile.cs
new file mode 100644
index 00000000..e10aa810
--- /dev/null
+++ b/LANCommander.Steam/Models/SteamCmdProfile.cs
@@ -0,0 +1,17 @@
+namespace LANCommander.Steam.Models;
+
+///
+/// Represents a SteamCMD profile with username and install directory
+///
+public class SteamCmdProfile
+{
+ ///
+ /// Steam username for this profile
+ ///
+ public string Username { get; set; } = string.Empty;
+
+ ///
+ /// Default install directory for this profile
+ ///
+ public string InstallDirectory { get; set; } = string.Empty;
+}
diff --git a/LANCommander.Steam/Options/SteamCmdOptions.cs b/LANCommander.Steam/Options/SteamCmdOptions.cs
new file mode 100644
index 00000000..efeb2db3
--- /dev/null
+++ b/LANCommander.Steam/Options/SteamCmdOptions.cs
@@ -0,0 +1,22 @@
+namespace LANCommander.Steam.Options;
+
+///
+/// Configuration options for SteamCMD service
+///
+public class SteamCmdOptions
+{
+ ///
+ /// Path to the SteamCMD executable
+ ///
+ public string? ExecutablePath { get; set; }
+
+ ///
+ /// Default install directory for Steam content
+ ///
+ public string? DefaultInstallDirectory { get; set; }
+
+ ///
+ /// Whether to auto-detect the SteamCMD path if not configured
+ ///
+ public bool AutoDetectPath { get; set; } = true;
+}
diff --git a/LANCommander.Steam/Services/SteamCmdService.cs b/LANCommander.Steam/Services/SteamCmdService.cs
new file mode 100644
index 00000000..cda82d77
--- /dev/null
+++ b/LANCommander.Steam/Services/SteamCmdService.cs
@@ -0,0 +1,860 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using LANCommander.Steam.Abstractions;
+using LANCommander.Steam.Enums;
+using LANCommander.Steam.Events;
+using LANCommander.Steam.Models;
+using LANCommander.Steam.Options;
+
+namespace LANCommander.Steam.Services;
+
+///
+/// Service for interacting with SteamCMD
+///
+public class SteamCmdService : ISteamCmdService
+{
+ private readonly ILogger _logger;
+ private readonly ISteamCmdProfileStore? _profileStore;
+ private readonly SteamCmdOptions _options;
+ private readonly ConcurrentDictionary _installJobs = new();
+ private readonly SemaphoreSlim _queueSemaphore = new(1, 1);
+ private readonly CancellationTokenSource _cancellationTokenSource = new();
+ private Task? _queueProcessorTask;
+
+ private string? _executablePath;
+
+ ///
+ /// Event fired when an install job status changes
+ ///
+ public event EventHandler? InstallStatusChanged;
+
+ ///
+ /// Event fired when install progress is updated
+ ///
+ public event EventHandler? InstallProgress;
+
+ ///
+ /// Get or set the SteamCMD executable path
+ ///
+ public string? ExecutablePath
+ {
+ get => _executablePath ?? _options.ExecutablePath;
+ set => _executablePath = value;
+ }
+
+ public SteamCmdService(
+ ILogger logger,
+ IOptions? options = null,
+ ISteamCmdProfileStore? profileStore = null)
+ {
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ _options = options?.Value ?? new SteamCmdOptions();
+ _profileStore = profileStore;
+
+ // Start the queue processor
+ _queueProcessorTask = Task.Run(ProcessInstallQueueAsync, _cancellationTokenSource.Token);
+ }
+ public async Task GetConnectionStatusAsync(string username)
+ {
+ try
+ {
+ if (!IsValidUsername(username))
+ throw new ArgumentException("Invalid username", nameof(username));
+
+ // Auto-populate SteamCMD path if not configured
+ if (string.IsNullOrWhiteSpace(ExecutablePath))
+ {
+ if (_options.AutoDetectPath)
+ {
+ var detectedPath = await AutoDetectSteamCmdPathAsync();
+
+ if (!string.IsNullOrWhiteSpace(detectedPath))
+ {
+ ExecutablePath = detectedPath;
+ _logger.LogInformation("Auto-detected SteamCMD at: {Path}", detectedPath);
+ }
+ else
+ {
+ _logger.LogWarning("SteamCMD path is not configured and could not be auto-detected");
+ return SteamCmdConnectionStatus.NotInstalled;
+ }
+ }
+ else
+ {
+ _logger.LogWarning("SteamCMD path is not configured and auto-detection is disabled");
+ return SteamCmdConnectionStatus.NotInstalled;
+ }
+ }
+
+ if (!File.Exists(ExecutablePath))
+ {
+ _logger.LogWarning("SteamCMD executable not found at configured path: {Path}", ExecutablePath);
+ return SteamCmdConnectionStatus.NotInstalled;
+ }
+
+ // Try to run steamcmd with +quit to check if it's working
+ var result = await ExecuteSteamCmdCommandAsync("+quit");
+
+ if (result.Success)
+ {
+ // Check if we're logged in by trying to get user info
+ var loginResult = await ExecuteSteamCmdCommandAsync($"+login {username} +quit", TimeSpan.FromSeconds(30));
+ return loginResult.Success ? SteamCmdConnectionStatus.Authenticated : SteamCmdConnectionStatus.Unauthenticated;
+ }
+
+ return SteamCmdConnectionStatus.NotInstalled;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error checking SteamCMD connection status");
+ return SteamCmdConnectionStatus.NotInstalled;
+ }
+ }
+
+ public async Task AutoDetectSteamCmdPathAsync()
+ {
+ var possiblePaths = new List();
+
+ // Windows paths
+ if (OperatingSystem.IsWindows())
+ {
+ // Common Steam installation directories
+ var steamPaths = new[]
+ {
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Steam", "steamcmd.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Steam", "steamcmd.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Steam", "steamcmd.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Steam", "steamcmd.exe"),
+ "C:\\Steam\\steamcmd.exe",
+ "C:\\Program Files\\Steam\\steamcmd.exe",
+ "C:\\Program Files (x86)\\Steam\\steamcmd.exe"
+ };
+
+ possiblePaths.AddRange(steamPaths);
+
+ // Check PATH environment variable
+ var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? [];
+
+ foreach (var dir in pathDirs)
+ {
+ if (!string.IsNullOrWhiteSpace(dir))
+ {
+ possiblePaths.Add(Path.Combine(dir, "steamcmd.exe"));
+ }
+ }
+ }
+ // Linux paths
+ else if (OperatingSystem.IsLinux())
+ {
+ possiblePaths.AddRange(new[]
+ {
+ "/app/Data/Steam/steamcmd.sh",
+ "/usr/local/bin/steamcmd",
+ "/usr/bin/steamcmd",
+ "/home/steam/steamcmd/steamcmd.sh",
+ "/opt/steamcmd/steamcmd.sh",
+ "/var/lib/steam/steamcmd/steamcmd.sh"
+ });
+
+ // Check PATH environment variable
+ var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(':') ?? [];
+
+ foreach (var dir in pathDirs)
+ {
+ if (!string.IsNullOrWhiteSpace(dir))
+ {
+ possiblePaths.Add(Path.Combine(dir, "steamcmd"));
+ }
+ }
+ }
+ // macOS paths
+ else if (OperatingSystem.IsMacOS())
+ {
+ possiblePaths.AddRange(new[]
+ {
+ "/usr/local/bin/steamcmd",
+ "/opt/homebrew/bin/steamcmd",
+ "/Applications/Steam.app/Contents/MacOS/steamcmd"
+ });
+
+ // Check PATH environment variable
+ var pathDirs = Environment.GetEnvironmentVariable("PATH")?.Split(':') ?? [];
+
+ foreach (var dir in pathDirs)
+ {
+ if (!string.IsNullOrWhiteSpace(dir))
+ {
+ possiblePaths.Add(Path.Combine(dir, "steamcmd"));
+ }
+ }
+ }
+
+ // Check each possible path
+ foreach (var path in possiblePaths)
+ {
+ if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
+ {
+ try
+ {
+ // Verify it's actually SteamCMD by checking if it responds to --version or +quit
+ var testResult = await ExecuteSteamCmdCommandAsync("+quit", TimeSpan.FromSeconds(30), path);
+
+ if (testResult.Success)
+ {
+ _logger.LogDebug("Found SteamCMD at: {Path}", path);
+ return path;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Failed to verify SteamCMD at: {Path}", path);
+ }
+ }
+ }
+
+ _logger.LogWarning("SteamCMD not found in common installation locations");
+
+ return string.Empty;
+ }
+
+ public async Task LoginToSteamAsync(string username, string? password = null)
+ {
+ try
+ {
+ if (!IsValidUsername(username))
+ throw new ArgumentException("Invalid username", nameof(username));
+
+ if (string.IsNullOrWhiteSpace(ExecutablePath))
+ {
+ _logger.LogError("SteamCMD path is not configured");
+ return SteamCmdStatus.PathNotConfigured;
+ }
+
+ if (!File.Exists(ExecutablePath))
+ {
+ _logger.LogError("SteamCMD executable not found at configured path: {Path}", ExecutablePath);
+ return SteamCmdStatus.ExecutableNotFound;
+ }
+
+ var loginCommand = string.IsNullOrWhiteSpace(password)
+ ? $"+login {username}"
+ : $"+login {username} {password}";
+
+ var result = await ExecuteSteamCmdCommandAsync($"{loginCommand} +quit", TimeSpan.FromSeconds(30));
+
+ if (result.Output.Contains("Invalid Password"))
+ return SteamCmdStatus.InvalidPassword;
+
+ if (result.Success)
+ {
+ _logger.LogInformation("Successfully logged into Steam as {Username}", username);
+ return SteamCmdStatus.Success;
+ }
+
+ _logger.LogError("Failed to log into Steam: {Error}", result.ErrorOutput);
+ return SteamCmdStatus.Error;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error logging into Steam");
+ return SteamCmdStatus.UnknownError;
+ }
+ }
+
+ public async Task LogoutAsync(string username)
+ {
+ try
+ {
+ if (!IsValidUsername(username))
+ return SteamCmdStatus.InvalidUsername;
+
+ if (string.IsNullOrWhiteSpace(ExecutablePath))
+ {
+ _logger.LogError("SteamCMD path is not configured");
+ return SteamCmdStatus.PathNotConfigured;
+ }
+
+ if (!File.Exists(ExecutablePath))
+ {
+ _logger.LogError("SteamCMD executable not found at configured path: {Path}", ExecutablePath);
+ return SteamCmdStatus.ExecutableNotFound;
+ }
+
+ var logoutCommand = $"+logout {username}";
+
+ var result = await ExecuteSteamCmdCommandAsync($"{logoutCommand} +quit");
+
+ if (result.Success)
+ {
+ _logger.LogInformation("Successfully logged out of the Steam account {Username}", username);
+ return SteamCmdStatus.Success;
+ }
+
+ _logger.LogError("Failed to log out of Steam: {Error}", result.ErrorOutput);
+ return SteamCmdStatus.Error;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error logging out of Steam");
+ return SteamCmdStatus.UnknownError;
+ }
+ }
+
+ public async Task InstallContentAsync(uint appId, string installDirectory, string? username = null)
+ {
+ if (!string.IsNullOrWhiteSpace(username) && !IsValidUsername(username))
+ throw new ArgumentException("Invalid username", nameof(username));
+
+ if (string.IsNullOrWhiteSpace(ExecutablePath))
+ {
+ _logger.LogError("SteamCMD path is not configured");
+ throw new InvalidOperationException("SteamCMD path is not configured");
+ }
+
+ if (!File.Exists(ExecutablePath))
+ {
+ _logger.LogError("SteamCMD executable not found at configured path: {Path}", ExecutablePath);
+ throw new FileNotFoundException("SteamCMD executable not found", ExecutablePath);
+ }
+
+ // Create install job
+ var job = new SteamCmdInstallJob
+ {
+ AppId = appId,
+ InstallDirectory = installDirectory,
+ Username = username,
+ Status = SteamCmdInstallStatus.Queued,
+ StatusMessage = "Queued for installation",
+ CompletionSource = new TaskCompletionSource()
+ };
+
+ _installJobs[job.Id] = job;
+
+ _logger.LogInformation("Queued installation job {JobId} for app {AppId} to {InstallDirectory}", job.Id, appId, installDirectory);
+
+ // Queue processor will pick it up automatically
+ return job;
+ }
+
+ public SteamCmdInstallJob? GetInstallJob(Guid jobId)
+ {
+ _installJobs.TryGetValue(jobId, out var job);
+ return job;
+ }
+
+ public IEnumerable GetInstallJobs()
+ {
+ return _installJobs.Values.ToList();
+ }
+
+ public async Task CancelInstallJobAsync(Guid jobId)
+ {
+ if (!_installJobs.TryGetValue(jobId, out var job))
+ return false;
+
+ if (job.Status == SteamCmdInstallStatus.Completed || job.Status == SteamCmdInstallStatus.Failed)
+ return false;
+
+ job.Status = SteamCmdInstallStatus.Cancelled;
+ job.StatusMessage = "Cancelled by user";
+ job.CompletedAt = DateTime.UtcNow;
+ job.CompletionSource?.TrySetCanceled();
+
+ OnInstallStatusChanged(job, SteamCmdInstallStatus.Cancelled);
+
+ _logger.LogInformation("Cancelled install job {JobId}", jobId);
+
+ return true;
+ }
+
+ public async Task RemoveContentAsync(string installDirectory)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(installDirectory))
+ {
+ _logger.LogError("Install directory is not specified");
+ return SteamCmdStatus.InstallDirectoryNotFound;
+ }
+
+ if (!Directory.Exists(installDirectory))
+ {
+ _logger.LogWarning("Install directory does not exist: {InstallDirectory}", installDirectory);
+ return SteamCmdStatus.Success; // Consider it already removed
+ }
+
+ // Remove the directory and all its contents
+ Directory.Delete(installDirectory, true);
+
+ _logger.LogInformation("Successfully removed content from {InstallDirectory}", installDirectory);
+ return SteamCmdStatus.Success;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error removing content from {InstallDirectory}", installDirectory);
+ return SteamCmdStatus.UnknownError;
+ }
+ }
+
+ public async Task> GetProfilesAsync()
+ {
+ if (_profileStore == null)
+ {
+ throw new InvalidOperationException("Profile store is not configured. Provide an ISteamCmdProfileStore implementation.");
+ }
+
+ return await _profileStore.GetAllAsync();
+ }
+
+ public async Task GetProfileAsync(string username)
+ {
+ if (_profileStore == null)
+ {
+ throw new InvalidOperationException("Profile store is not configured. Provide an ISteamCmdProfileStore implementation.");
+ }
+
+ return await _profileStore.GetByUsernameAsync(username);
+ }
+
+ public async Task SaveProfileAsync(SteamCmdProfile profile)
+ {
+ if (_profileStore == null)
+ {
+ throw new InvalidOperationException("Profile store is not configured. Provide an ISteamCmdProfileStore implementation.");
+ }
+
+ if (profile == null)
+ {
+ throw new ArgumentNullException(nameof(profile));
+ }
+
+ if (string.IsNullOrWhiteSpace(profile.Username))
+ {
+ throw new ArgumentException("Profile username cannot be empty", nameof(profile));
+ }
+
+ await _profileStore.SaveAsync(profile);
+ _logger.LogInformation("Saved profile for username: {Username}", profile.Username);
+ }
+
+ public async Task DeleteProfileAsync(string username)
+ {
+ if (_profileStore == null)
+ {
+ throw new InvalidOperationException("Profile store is not configured. Provide an ISteamCmdProfileStore implementation.");
+ }
+
+ if (string.IsNullOrWhiteSpace(username))
+ {
+ throw new ArgumentException("Username cannot be empty", nameof(username));
+ }
+
+ await _profileStore.DeleteAsync(username);
+ _logger.LogInformation("Deleted profile for username: {Username}", username);
+ }
+
+ private async Task ProcessInstallQueueAsync()
+ {
+ while (!_cancellationTokenSource.Token.IsCancellationRequested)
+ {
+ try
+ {
+ var queuedJob = _installJobs.Values
+ .FirstOrDefault(j => j.Status == SteamCmdInstallStatus.Queued);
+
+ if (queuedJob == null)
+ {
+ await Task.Delay(1000, _cancellationTokenSource.Token);
+ continue;
+ }
+
+ // Process the job (semaphore is acquired inside ProcessInstallJobAsync)
+ await ProcessInstallJobAsync(queuedJob);
+ }
+ catch (OperationCanceledException)
+ {
+ // Service is being disposed
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error processing install queue");
+ await Task.Delay(5000, _cancellationTokenSource.Token);
+ }
+ }
+ }
+
+ private async Task ProcessInstallJobAsync(SteamCmdInstallJob job)
+ {
+ await _queueSemaphore.WaitAsync(_cancellationTokenSource.Token);
+
+ try
+ {
+ job.Status = SteamCmdInstallStatus.InProgress;
+ job.StartedAt = DateTime.UtcNow;
+ job.StatusMessage = "Starting installation...";
+ OnInstallStatusChanged(job, SteamCmdInstallStatus.InProgress);
+
+ // Ensure install directory exists
+ Directory.CreateDirectory(job.InstallDirectory);
+
+ var commands = new List();
+
+ // Add login command if credentials provided
+ if (!string.IsNullOrWhiteSpace(job.Username))
+ {
+ commands.Add($"+login {job.Username}");
+ }
+ else
+ {
+ commands.Add("+login anonymous");
+ }
+
+ // Add install commands
+ commands.Add($"+force_install_dir \"{job.InstallDirectory}\"");
+ commands.Add($"+app_update {job.AppId} validate");
+ commands.Add("+quit");
+
+ var commandString = string.Join(" ", commands);
+
+ var result = await ExecuteSteamCmdCommandWithProgressAsync(
+ commandString,
+ job,
+ _cancellationTokenSource.Token);
+
+ if (result.Success)
+ {
+ job.Status = SteamCmdInstallStatus.Completed;
+ job.StatusMessage = "Installation completed successfully";
+ job.Progress = 100;
+ job.CompletedAt = DateTime.UtcNow;
+ job.CompletionSource?.TrySetResult(SteamCmdStatus.Success);
+
+ OnInstallStatusChanged(job, SteamCmdInstallStatus.Completed);
+ OnInstallProgress(job, 100, "Installation completed successfully");
+
+ _logger.LogInformation("Successfully installed Steam app {AppId} to {InstallDirectory}", job.AppId, job.InstallDirectory);
+ }
+ else
+ {
+ job.Status = SteamCmdInstallStatus.Failed;
+ job.StatusMessage = $"Installation failed: {result.ErrorOutput}";
+ job.ErrorMessage = result.ErrorOutput;
+ job.CompletedAt = DateTime.UtcNow;
+ job.CompletionSource?.TrySetResult(SteamCmdStatus.Error);
+
+ OnInstallStatusChanged(job, SteamCmdInstallStatus.Failed, result.ErrorOutput);
+
+ _logger.LogError("Failed to install Steam app {AppId}: {Error}", job.AppId, result.ErrorOutput);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ job.Status = SteamCmdInstallStatus.Cancelled;
+ job.StatusMessage = "Installation cancelled";
+ job.CompletedAt = DateTime.UtcNow;
+ job.CompletionSource?.TrySetCanceled();
+
+ OnInstallStatusChanged(job, SteamCmdInstallStatus.Cancelled);
+ }
+ catch (Exception ex)
+ {
+ job.Status = SteamCmdInstallStatus.Failed;
+ job.StatusMessage = $"Installation error: {ex.Message}";
+ job.ErrorMessage = ex.Message;
+ job.CompletedAt = DateTime.UtcNow;
+ job.CompletionSource?.TrySetException(ex);
+
+ OnInstallStatusChanged(job, SteamCmdInstallStatus.Failed, ex.Message);
+
+ _logger.LogError(ex, "Error installing Steam content for app {AppId}", job.AppId);
+ }
+ finally
+ {
+ _queueSemaphore.Release();
+ }
+ }
+
+ private async Task ExecuteSteamCmdCommandWithProgressAsync(
+ string arguments,
+ SteamCmdInstallJob? job = null,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(ExecutablePath))
+ {
+ throw new InvalidOperationException("SteamCMD executable path is not configured");
+ }
+
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = ExecutablePath,
+ Arguments = arguments,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ StandardOutputEncoding = Encoding.UTF8,
+ StandardErrorEncoding = Encoding.UTF8
+ };
+
+ using var process = new Process { StartInfo = processStartInfo };
+ var output = new StringBuilder();
+ var error = new StringBuilder();
+
+ process.OutputDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ output.AppendLine(e.Data);
+ _logger.LogDebug("SteamCMD Output: {Output}", e.Data);
+
+ // Parse progress if job is provided
+ if (job != null)
+ {
+ ParseProgressFromOutput(e.Data, job);
+ }
+ }
+ };
+
+ process.ErrorDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ error.AppendLine(e.Data);
+ _logger.LogDebug("SteamCMD Error: {Error}", e.Data);
+ }
+ };
+
+ process.Start();
+
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ try
+ {
+ await process.WaitForExitAsync(cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ _logger.LogWarning("SteamCMD operation was cancelled");
+ process.Kill();
+ throw;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while executing SteamCMD");
+ }
+
+ return new SteamCmdResult
+ {
+ Success = process.ExitCode == 0,
+ ExitCode = process.ExitCode,
+ Output = output.ToString(),
+ ErrorOutput = error.ToString()
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error executing SteamCMD command: {Arguments}", arguments);
+
+ return new SteamCmdResult
+ {
+ Success = false,
+ ExitCode = -1,
+ ErrorOutput = ex.Message
+ };
+ }
+ }
+
+ private void ParseProgressFromOutput(string line, SteamCmdInstallJob job)
+ {
+ // SteamCMD progress patterns:
+ // "Downloading update (X of Y MB)"
+ // "Update state (X% at Y MB/s) : downloading update"
+ // "Success. App 'X' fully installed."
+ // "Installing update..."
+
+ var progressMatch = Regex.Match(line, @"Update state \(([\d.]+)%", RegexOptions.IgnoreCase);
+ if (progressMatch.Success && double.TryParse(progressMatch.Groups[1].Value, out var progress))
+ {
+ job.Progress = Math.Min(100, Math.Max(0, progress));
+
+ // Try to extract download speed
+ var speedMatch = Regex.Match(line, @"at ([\d.]+) (MB|KB)/s", RegexOptions.IgnoreCase);
+ if (speedMatch.Success && double.TryParse(speedMatch.Groups[1].Value, out var speed))
+ {
+ var unit = speedMatch.Groups[2].Value.ToUpper();
+ job.BytesPerSecond = (long)(speed * (unit == "MB" ? 1048576 : 1024));
+ }
+
+ OnInstallProgress(job, job.Progress, line, job.BytesDownloaded, job.BytesTotal, job.BytesPerSecond);
+ }
+
+ // Extract download progress (X of Y MB)
+ var downloadMatch = Regex.Match(line, @"Downloading update \(([\d.]+) of ([\d.]+) (MB|KB)\)", RegexOptions.IgnoreCase);
+ if (downloadMatch.Success)
+ {
+ if (double.TryParse(downloadMatch.Groups[1].Value, out var downloaded) &&
+ double.TryParse(downloadMatch.Groups[2].Value, out var total))
+ {
+ var unit = downloadMatch.Groups[3].Value.ToUpper();
+ var multiplier = unit == "MB" ? 1048576 : 1024;
+
+ job.BytesDownloaded = (long)(downloaded * multiplier);
+ job.BytesTotal = (long)(total * multiplier);
+
+ if (job.BytesTotal > 0)
+ {
+ job.Progress = (job.BytesDownloaded * 100.0) / job.BytesTotal;
+ }
+
+ OnInstallProgress(job, job.Progress, line, job.BytesDownloaded, job.BytesTotal, job.BytesPerSecond);
+ }
+ }
+
+ // Update status message
+ if (!string.IsNullOrWhiteSpace(line) && !line.Contains("Update state"))
+ {
+ job.StatusMessage = line.Trim();
+ }
+ }
+
+ private void OnInstallStatusChanged(SteamCmdInstallJob job, SteamCmdInstallStatus status, string? errorMessage = null)
+ {
+ try
+ {
+ InstallStatusChanged?.Invoke(this, new SteamCmdInstallStatusEventArgs(job, status, errorMessage));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error invoking InstallStatusChanged event");
+ }
+ }
+
+ private void OnInstallProgress(SteamCmdInstallJob job, double progress, string statusMessage, long bytesDownloaded = 0, long bytesTotal = 0, long bytesPerSecond = 0)
+ {
+ try
+ {
+ InstallProgress?.Invoke(this, new SteamCmdInstallProgressEventArgs(job, progress, statusMessage, bytesDownloaded, bytesTotal, bytesPerSecond));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error invoking InstallProgress event");
+ }
+ }
+
+ private async Task ExecuteSteamCmdCommandAsync(string arguments, TimeSpan? timeout = null, string? steamCmdPath = null)
+ {
+ try
+ {
+ var executablePath = string.IsNullOrWhiteSpace(steamCmdPath) ? ExecutablePath : steamCmdPath;
+
+ if (string.IsNullOrWhiteSpace(executablePath))
+ {
+ throw new InvalidOperationException("SteamCMD executable path is not configured");
+ }
+
+ var processStartInfo = new ProcessStartInfo
+ {
+ FileName = executablePath,
+ Arguments = arguments,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ StandardOutputEncoding = Encoding.UTF8,
+ StandardErrorEncoding = Encoding.UTF8
+ };
+
+ using var process = new Process { StartInfo = processStartInfo };
+ var output = new StringBuilder();
+ var error = new StringBuilder();
+
+ process.OutputDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ output.AppendLine(e.Data);
+ _logger.LogDebug("SteamCMD Output: {Output}", e.Data);
+ }
+ };
+
+ process.ErrorDataReceived += (sender, e) =>
+ {
+ if (!string.IsNullOrEmpty(e.Data))
+ {
+ error.AppendLine(e.Data);
+ _logger.LogDebug("SteamCMD Error: {Error}", e.Data);
+ }
+ };
+
+ process.Start();
+
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ try
+ {
+ if (timeout.HasValue)
+ await process.WaitForExitAsync().WaitAsync(timeout.Value);
+ else
+ await process.WaitForExitAsync();
+ }
+ catch (TimeoutException)
+ {
+ _logger.LogWarning("SteamCMD timed out");
+
+ process.Kill();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error while executing SteamCMD");
+ }
+
+ return new SteamCmdResult
+ {
+ Success = process.ExitCode == 0,
+ ExitCode = process.ExitCode,
+ Output = output.ToString(),
+ ErrorOutput = error.ToString()
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error executing SteamCMD command: {Arguments}", arguments);
+
+ return new SteamCmdResult
+ {
+ Success = false,
+ ExitCode = -1,
+ ErrorOutput = ex.Message
+ };
+ }
+ }
+
+ private static bool IsValidUsername(string value)
+ {
+ return !string.IsNullOrEmpty(value) &&
+ Regex.IsMatch(value, @"^[a-zA-Z0-9_]+$");
+ }
+
+ private class SteamCmdResult
+ {
+ public bool Success { get; set; }
+ public int ExitCode { get; set; }
+ public string Output { get; set; } = string.Empty;
+ public string ErrorOutput { get; set; } = string.Empty;
+ }
+}
\ No newline at end of file
diff --git a/LANCommander.Steam/Services/SteamStoreService.cs b/LANCommander.Steam/Services/SteamStoreService.cs
new file mode 100644
index 00000000..5d9e0118
--- /dev/null
+++ b/LANCommander.Steam/Services/SteamStoreService.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Mime;
+using System.Threading.Tasks;
+using HtmlAgilityPack;
+
+namespace LANCommander.Steam.Services;
+
+public class SteamStoreService
+{
+ private readonly HttpClient HttpClient;
+
+ public SteamStoreService()
+ {
+ HttpClient = new HttpClient();
+ HttpClient.BaseAddress = new Uri("https://store.steampowered.com");
+ }
+
+ public async Task> SearchGamesAsync(string keyword)
+ {
+ HtmlWeb web = new HtmlWeb();
+ HtmlDocument dom = await web.LoadFromWebAsync($"https://store.steampowered.com/search/suggest?term={keyword}&f=games&cc=US");
+
+ var results = new List();
+ var matches = dom.DocumentNode.SelectNodes("//a[@data-ds-appid]");
+
+ if (matches == null || matches.Count == 0)
+ return [];
+
+ foreach (var match in matches)
+ {
+ try
+ {
+ var appId = match.Attributes["data-ds-appid"].Value;
+ var matchNameElement = match.SelectSingleNode(".//div[@class = 'match_name']");
+
+ appId = appId.Split(',').First();
+
+ if (matchNameElement != null)
+ {
+ results.Add(new GameSearchResult
+ {
+ Name = matchNameElement.InnerText,
+ AppId = Convert.ToInt32(appId)
+ });
+ }
+ }
+ catch (Exception ex) { }
+ }
+
+ return results;
+ }
+
+ public async Task<(bool Exists, string MimeType)> HasWebAssetAsync(int appId, WebAssetType webAssetType)
+ {
+ var webAssetUri = GetWebAssetUri(appId, webAssetType);
+ var response = await HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, webAssetUri));
+
+ var exists = response.Content.Headers.ContentType.MediaType == MediaTypeNames.Image.Jpeg || response.Content.Headers.ContentType.MediaType == "image/png";
+
+ return (exists, response.Content.Headers.ContentType.MediaType);
+ }
+
+ public async Task HasManualAsync(int appId)
+ {
+ var manualUri = GetManualUri(appId);
+ var response = await HttpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, manualUri));
+
+ return response.Content.Headers.ContentType.MediaType == MediaTypeNames.Application.Pdf;
+ }
+
+ public async Task DownloadManualAsync(int appId)
+ {
+ var manualUri = GetManualUri(appId);
+ var response = await HttpClient.GetAsync(manualUri);
+
+ if (!response.IsSuccessStatusCode)
+ return null;
+
+ using (var ms = new MemoryStream())
+ {
+ await response.Content.CopyToAsync(ms);
+
+ return ms.ToArray();
+ }
+ }
+
+ public static Uri GetManualUri(int appId)
+ {
+ return new Uri($"https://store.steampowered.com/manual/{appId}");
+ }
+
+ public static Uri GetWebAssetUri(int appId, WebAssetType type)
+ {
+ Dictionary webAssetTypeMap = new Dictionary()
+ {
+ { WebAssetType.Capsule, "capsule_231x87.jpg" },
+ { WebAssetType.CapsuleLarge, "capsule_616x353.jpg" },
+ { WebAssetType.Header, "header.jpg" },
+ { WebAssetType.HeroCapsule, "hero_capsule.jpg" },
+ { WebAssetType.LibraryCover, "library_600x900.jpg" },
+ { WebAssetType.LibraryHeader, "library_header.jpg" },
+ { WebAssetType.LibraryHero, "library_hero.jpg" },
+ { WebAssetType.Logo, "logo.png" }
+ };
+
+ return new Uri($"https://shared.cloudflare.steamstatic.com/store_item_assets/steam/apps/{appId}/{webAssetTypeMap[type]}");
+ }
+}
\ No newline at end of file