From 676898608bc31c854bba70ca90dec196782e1c3a Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Tue, 20 Jan 2026 00:48:13 -0600 Subject: [PATCH] Add basic PowerShell module for SteamCMD --- .../Modules/SteamCMD/SteamCMD.psd1 | 14 ++ .../Modules/SteamCMD/SteamCMD.psm1 | 200 ++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 LANCommander.Server/Modules/SteamCMD/SteamCMD.psd1 create mode 100644 LANCommander.Server/Modules/SteamCMD/SteamCMD.psm1 diff --git a/LANCommander.Server/Modules/SteamCMD/SteamCMD.psd1 b/LANCommander.Server/Modules/SteamCMD/SteamCMD.psd1 new file mode 100644 index 00000000..4665b327 --- /dev/null +++ b/LANCommander.Server/Modules/SteamCMD/SteamCMD.psd1 @@ -0,0 +1,14 @@ +@{ + RootModule = 'SteamCMD.psm1' + ModuleVersion = '1.0.0' + GUID = 'd9e2b2b7-7c4a-4c1c-9c33-93d5f65a4a21' + Author = 'Pat Hartl' + CompanyName = 'LANCommander' + Description = 'Install and update Steam games using SteamCMD' + PowerShellVersion = '7.0' + + FunctionsToExport = @('Install-SteamGame') + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() +} \ No newline at end of file diff --git a/LANCommander.Server/Modules/SteamCMD/SteamCMD.psm1 b/LANCommander.Server/Modules/SteamCMD/SteamCMD.psm1 new file mode 100644 index 00000000..294727a5 --- /dev/null +++ b/LANCommander.Server/Modules/SteamCMD/SteamCMD.psm1 @@ -0,0 +1,200 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +function Invoke-SteamCmd { + [CmdletBinding()] + param( + [Parameter(Mandatory)] [string] $SteamCmdPath, + [Parameter(Mandatory)] [string[]] $Arguments, + [Parameter()] [string] $LogPath = "" + ) + + $psi = [System.Diagnostics.ProcessStartInfo]::new() + $psi.FileName = $SteamCmdPath + $psi.UseShellExecute = $false + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + + foreach ($a in $Arguments) { [void]$psi.ArgumentList.Add($a) } + + $proc = [System.Diagnostics.Process]::new() + $proc.StartInfo = $psi + + [void]$proc.Start() + + $stdout = $proc.StandardOutput.ReadToEnd() + $stderr = $proc.StandardError.ReadToEnd() + + $proc.WaitForExit() + + $combined = @( + "===== SteamCMD STDOUT =====" + $stdout + "===== SteamCMD STDERR =====" + $stderr + ) -join "`n" + + if ($LogPath) { + $dir = Split-Path -Parent $LogPath + if ($dir) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } + Set-Content -LiteralPath $LogPath -Value $combined -Encoding UTF8 + } + + [PSCustomObject]@{ + ExitCode = $proc.ExitCode + StdOut = $stdout + StdErr = $stderr + Combined = $combined + } +} + +function Install-SteamGame { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [int] $AppId, + + [Parameter(Mandatory)] + [string] $GameName, + + [Parameter(Mandatory)] + [string] $InstallRoot, + + [Parameter()] + [string] $SteamCmdPath = "steamcmd", + + [Parameter()] + [string] $Username = "anonymous", + + [Parameter()] + [string] $Password = "", + + [Parameter()] + [string] $Branch = "", + + [Parameter()] + [string] $BranchPassword = "", + + [Parameter()] + [switch] $Validate, + + [Parameter()] + [switch] $IncludeSteamNews = $true + ) + + function Join-PathPortable([string] $A, [string] $B) { + if ($A.EndsWith("/") -or $A.EndsWith("\")) { return "$A$B" } + if ($A -match "^[a-zA-Z]:\\") { return "$A\$B" } + return "$A/$B" + } + + function Find-AppManifest([string] $InstallDir, [int] $AppIdValue) { + $expected = Join-PathPortable $InstallDir ("steamapps/appmanifest_{0}.acf" -f $AppIdValue) + if (Test-Path -LiteralPath $expected) { return $expected } + + Get-ChildItem -LiteralPath $InstallDir -Recurse -File ` + -Filter ("appmanifest_{0}.acf" -f $AppIdValue) ` + -ErrorAction SilentlyContinue | + Select-Object -First 1 | + ForEach-Object FullName + } + + function Parse-AppManifestBuildInfo([string] $ManifestPath) { + $content = Get-Content -LiteralPath $ManifestPath -Raw + + $buildId = [regex]::Match($content, '"buildid"\s+"(\d+)"').Groups[1].Value + $lastUpdated = [regex]::Match($content, '"LastUpdated"\s+"(\d+)"').Groups[1].Value + + [PSCustomObject]@{ + BuildId = $buildId + LastUpdatedUtc = if ($lastUpdated) { + [DateTimeOffset]::FromUnixTimeSeconds([int64]$lastUpdated).UtcDateTime + } + } + } + + function Get-SteamNewsChangelog([int] $AppIdValue) { + try { + $uri = "https://api.steampowered.com/ISteamNews/GetNewsForApp/v2/?appid=$AppIdValue&count=3&maxlength=800&format=json" + $resp = Invoke-RestMethod -Uri $uri -TimeoutSec 15 + $resp.appnews.newsitems | + ForEach-Object { + "[{0}] {1}`n{2}" -f ` + ([DateTimeOffset]::FromUnixTimeSeconds($_.date).ToString("u")), + $_.title, + $_.contents + } -join "`n`n" + } + catch { + "" + } + } + + # ---- Install ---- + + $installDir = Join-PathPortable $InstallRoot $GameName + New-Item -ItemType Directory -Path $installDir -Force | Out-Null + + $args = @( + "+force_install_dir", $installDir, + "+login", $Username + ) + + if ($Username -ne "anonymous" -and $Password) { + $args += $Password + } + + $args += "+app_update", $AppId + + if ($Branch) { + $args += "-beta", $Branch + if ($BranchPassword) { + $args += "-betapassword", $BranchPassword + } + } + + if ($Validate) { + $args += "validate" + } + + $args += "+quit" + +$logPath = Join-Path -Path $installDir -ChildPath "steamcmd.log" + +$result = Invoke-SteamCmd -SteamCmdPath $SteamCmdPath -Arguments $args -LogPath $logPath + +if ($result.ExitCode -ne 0) { + $tail = ($result.Combined -split "`r?`n" | Select-Object -Last 80) -join "`n" + throw @( + "SteamCMD failed with exit code $($result.ExitCode)." + "Log: $logPath" + "" + "Last 80 lines:" + $tail + ) -join "`n" +} + + + # ---- Version / Changelog ---- + + $manifest = Find-AppManifest -InstallDir $installDir -AppIdValue $AppId + $build = if ($manifest) { Parse-AppManifestBuildInfo $manifest } + + $version = $build?.BuildId ?? "unknown" + + $changelog = if ($IncludeSteamNews) { + Get-SteamNewsChangelog $AppId + } + + if (-not $changelog) { + $changelog = "BuildID: $version" + } + + return [PSCustomObject]@{ + Path = $installDir + Version = $version + Changelog = $changelog + } +} + +Export-ModuleMember -Function Install-SteamGame \ No newline at end of file