Last active
September 25, 2025 15:46
-
-
Save kiler129/ddc62e359b0fa7be10b88e6da8bb7928 to your computer and use it in GitHub Desktop.
Mass-validate all Steam games on Windows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
######################################################################################################################## | |
# Mass Steam Game Validator | |
# | |
# This script, meant to be used on Windows, validates whole Steam libraries. By default, Steam client can only do it | |
# one-by-one which is cumbersome in larger libraries. This tool uses the Valve's official steamCMD. | |
# The tool, by default, will attempt to validate all games in all libraries found, using the default account. Use -Help | |
# to see available options. | |
# | |
# To run this tool, save it as ".ps1" file, start Windows PowerShell/Windows Terminal, and type | |
# .\Validate-Steam-Games.ps1 in the console. You may need to enable PS scripts (https://superuser.com/a/106362) first. | |
# | |
# License: MIT; (c) Gregory House (https://github.com/kiler129) | |
param ( | |
[switch]$DryRun, | |
[string]$Skip, | |
[switch]$Help, | |
[string]$LibraryPath, | |
[string]$User, | |
[switch]$Verbose | |
) | |
# --- Help --- | |
if ($Help) { | |
$scriptName = $MyInvocation.MyCommand.Name | |
Write-Host "Mass Steam Game Validator" -ForegroundColor Yellow | |
Write-Host "" | |
Write-Host "USAGE:" -ForegroundColor Cyan | |
Write-Host " .\$scriptName [-DryRun] [-Skip <AppIDs>] [-LibraryPath <Path>] [-User <Name>] [-Verbose] [-Help]" -ForegroundColor White | |
Write-Host "" | |
Write-Host "OPTIONS:" -ForegroundColor Cyan | |
Write-Host " -DryRun Verifies Steam login and lists games, but does not validate them." -ForegroundColor White | |
Write-Host " -Skip <IDs> Comma-separated AppIDs to skip (e.g. -Skip 346110,123456)." -ForegroundColor White | |
Write-Host " -LibraryPath <Path> Use a specific Steam library root (folder containing 'steamapps')." -ForegroundColor White | |
Write-Host " If omitted, libraries are auto-detected from registry and libraryfolders.vdf." -ForegroundColor White | |
Write-Host " -User <Name> Steam username to use. If omitted, auto-detected from loginusers.vdf (or prompted)." -ForegroundColor White | |
Write-Host " -Verbose Echo the exact steamcmd command and stream its live output." -ForegroundColor White | |
Write-Host " -Help Show this help screen and exit." -ForegroundColor White | |
exit | |
} | |
# --- Parse Skip list --- | |
$skipList = @() | |
if ($Skip) { | |
$skipList = $Skip.Split(",") | ForEach-Object { $_.Trim() } | Where-Object { $_ } | |
if ($skipList.Count) { Write-Host "[INFO] Will skip AppIDs: $($skipList -join ', ')" -ForegroundColor Yellow } | |
} | |
# --- Steam install path --- | |
$steamReg = Get-ItemProperty -Path "HKCU:\Software\Valve\Steam" -ErrorAction SilentlyContinue | |
if (-not $steamReg -or -not $steamReg.SteamPath) { | |
Write-Host "[ERROR] Could not find Steam in registry." -ForegroundColor Red | |
$steamPath = Read-Host "Enter your Steam install path (e.g. C:\Program Files (x86)\Steam)" | |
} else { | |
$steamPath = $steamReg.SteamPath | |
} | |
$libraryFile = Join-Path $steamPath "steamapps\libraryfolders.vdf" | |
$loginUsersFile = Join-Path $steamPath "config\loginusers.vdf" | |
# --- Detect usernames from loginusers.vdf --- | |
function Detect-SteamUsers { | |
$users = @() | |
if (Test-Path -LiteralPath $loginUsersFile) { | |
$content = Get-Content -LiteralPath $loginUsersFile -Raw | |
$rx = '"AccountName"\s*"([^"]+)"' | |
$matches = [regex]::Matches($content, $rx) | |
foreach ($m in $matches) { $users += $m.Groups[1].Value } | |
} | |
$users | Where-Object { $_ -and $_.Trim() -ne '' } | Select-Object -Unique | |
} | |
$detectedUsers = Detect-SteamUsers | |
# --- Resolve steamcmd --- | |
function Resolve-SteamCmd { | |
$cmd = Get-Command steamcmd -ErrorAction SilentlyContinue | |
if ($cmd) { | |
Write-Host "[INFO] Found steamcmd in PATH." -ForegroundColor Green | |
return "steamcmd" | |
} | |
Write-Host "[ERROR] steamcmd not found in PATH." -ForegroundColor Red | |
if (Get-Command winget -ErrorAction SilentlyContinue) { | |
Write-Host "Install it with:" -ForegroundColor Cyan | |
Write-Host " winget install steamcmd" -ForegroundColor White | |
} else { | |
Write-Host "Download: https://developer.valvesoftware.com/wiki/SteamCMD" -ForegroundColor Cyan | |
} | |
$choice = Read-Host "Do you want to manually enter the path now? (Y/N)" | |
if ($choice -notmatch '^[Yy]$') { throw "steamcmd is required but not installed or provided." } | |
while ($true) { | |
$candidate = Read-Host "Enter full path to steamcmd.exe (or Ctrl+C to cancel)" | |
if ([string]::IsNullOrWhiteSpace($candidate)) { Write-Host "[WARN] Path cannot be empty." -ForegroundColor Yellow; continue } | |
$candidate = $candidate.Trim('"') | |
if (-not (Test-Path -LiteralPath $candidate)) { Write-Host "[ERROR] File not found: $candidate" -ForegroundColor Red; continue } | |
try { | |
& $candidate +quit | Out-Null | |
if ($LASTEXITCODE -eq 0) { Write-Host "[INFO] steamcmd verified at: $candidate" -ForegroundColor Green; return $candidate } | |
Write-Host "[ERROR] steamcmd exited with code $LASTEXITCODE when tested." -ForegroundColor Red | |
} catch { Write-Host "[ERROR] Failed to run steamcmd: $($_.Exception.Message)" -ForegroundColor Red } | |
} | |
} | |
# --- Resolve username --- | |
function Resolve-SteamUser { | |
param([string]$UserOverride, [string[]]$Detected) | |
if (-not [string]::IsNullOrWhiteSpace($UserOverride)) { | |
Write-Host "[INFO] Using specified Steam user: $UserOverride" -ForegroundColor Green | |
return $UserOverride | |
} | |
if ($Detected.Count -eq 1) { | |
Write-Host "[INFO] Auto-detected Steam user: $($Detected[0])" -ForegroundColor Green | |
return $Detected[0] | |
} | |
if ($Detected.Count -gt 1) { | |
Write-Host "[INFO] Multiple Steam users detected:" -ForegroundColor Yellow | |
for ($i = 0; $i -lt $Detected.Count; $i++) { | |
Write-Host " [$i] $($Detected[$i])" | |
} | |
while ($true) { | |
$choice = Read-Host "Select user number" | |
if ($choice -match '^\d+$' -and [int]$choice -ge 0 -and [int]$choice -lt $Detected.Count) { | |
$selected = $Detected[[int]$choice] | |
Write-Host "[INFO] Selected Steam user: $selected" -ForegroundColor Green | |
return $selected | |
} | |
Write-Host "[WARN] Invalid choice, try again." -ForegroundColor Yellow | |
} | |
} | |
Write-Host "[WARN] No Steam usernames detected in loginusers.vdf." -ForegroundColor Yellow | |
$manual = Read-Host "Enter your Steam username" | |
Write-Host "[INFO] Using manually entered Steam user: $manual" -ForegroundColor Green | |
return $manual | |
} | |
# --- Test login (interactive always) --- | |
function Test-SteamLogin { | |
param([string]$SteamCmdPath, [string]$SteamUser, [switch]$Verbose) | |
Write-Host "[INFO] Testing Steam login for user '$SteamUser'..." -ForegroundColor Cyan | |
Write-Host " If cached credentials are missing, SteamCMD will ask for your password/Steam Guard." -ForegroundColor Yellow | |
Write-Host " This prompt is from the official Steam tool, not from this script." -ForegroundColor Yellow | |
$args = @("+login", $SteamUser, "+quit") | |
if ($Verbose) { Write-Host "[VERBOSE] Executing: $SteamCmdPath $($args -join ' ')" -ForegroundColor Magenta } | |
& $SteamCmdPath @args | |
$exit = $LASTEXITCODE | |
if ($exit -eq 0) { | |
Write-Host "[INFO] Steam login successful." -ForegroundColor Green | |
} else { | |
Write-Host "[ERROR] Steam login failed with exit code $exit." -ForegroundColor Red | |
throw "Steam login failed. Cannot proceed." | |
} | |
} | |
# --- Steam libraries (return roots, not steamapps) --- | |
function Get-SteamLibraries { | |
param([string]$ManualPath) | |
if ($ManualPath) { return ,$ManualPath } | |
$libs = New-Object System.Collections.Generic.List[string] | |
$libs.Add($steamPath) | |
if (Test-Path -LiteralPath $libraryFile) { | |
$content = Get-Content -LiteralPath $libraryFile | |
foreach ($line in $content) { | |
if ($line -match '"path"\s+"(.+)"') { | |
$raw = $matches[1] -replace '\\\\','\' | |
try { | |
$resolved = (Resolve-Path $raw -ErrorAction SilentlyContinue) | |
if ($resolved) { | |
$libs.Add($resolved.Path) | |
} else { | |
$libs.Add($raw) | |
} | |
} catch { $libs.Add($raw) } | |
} | |
} | |
} | |
$libs | ForEach-Object { $_.ToLowerInvariant() } | Sort-Object -Unique | ForEach-Object { | |
try { (Resolve-Path $_).Path } catch { $_ } | |
} | |
} | |
# --- Parse manifest for name and installdir --- | |
function Get-AppInfoFromManifest($manifestPath) { | |
$name = "<Unknown>" | |
$installdir = $null | |
try { | |
$hitName = Select-String -Path $manifestPath -Pattern '"name"\s+"([^"]+)"' -ErrorAction SilentlyContinue | Select-Object -First 1 | |
if ($hitName -and $hitName.Matches.Count -gt 0) { $name = $hitName.Matches[0].Groups[1].Value } | |
$hitDir = Select-String -Path $manifestPath -Pattern '"installdir"\s+"([^"]+)"' -ErrorAction SilentlyContinue | Select-Object -First 1 | |
if ($hitDir -and $hitDir.Matches.Count -gt 0) { $installdir = $hitDir.Matches[0].Groups[1].Value } | |
} catch {} | |
[PSCustomObject]@{ Name = $name; InstallDir = $installdir } | |
} | |
# --- Validate one game at its real install path --- | |
function Validate-App([string]$appId, [string]$gameName, [string]$installPath) { | |
if ($skipList -contains $appId) { Write-Host "[SKIP] $appId ($gameName)" -ForegroundColor DarkYellow; return } | |
if ($DryRun) { Write-Host "[DRYRUN] Would validate $appId ($gameName) at [$installPath]" -ForegroundColor DarkCyan; return } | |
if (-not (Test-Path -LiteralPath $installPath)) { | |
Write-Host "[WARN] Install path not found, creating (SteamCMD may re-download): $installPath" -ForegroundColor Yellow | |
New-Item -ItemType Directory -Force -Path $installPath | Out-Null | |
} | |
$args = @( | |
"+force_install_dir", $installPath, | |
"+login", $steamUser, | |
"+app_update", $appId, "validate", | |
"+quit" | |
) | |
Write-Host "[INFO] Validating $appId ($gameName) at [$installPath]..." -ForegroundColor Cyan | |
if ($Verbose) { Write-Host "[VERBOSE] Executing: $steamCmdPath $($args -join ' ')" -ForegroundColor Magenta } | |
& $steamCmdPath @args | |
if ($LASTEXITCODE -eq 0) { Write-Host "[SUCCESS] $gameName validated." -ForegroundColor Green } | |
else { Write-Host "[ERROR] $gameName failed with code $LASTEXITCODE." -ForegroundColor Red } | |
} | |
# --- Main --- | |
Write-Host "=======================================" -ForegroundColor Yellow | |
Write-Host ("Mass Steam Game Validation {0} Started: {1}" -f ($(if ($DryRun) {"(DRY RUN)"} else {""}), (Get-Date))) -ForegroundColor Yellow | |
Write-Host "=======================================" -ForegroundColor Yellow | |
Write-Host "NOTE: Validation checks files and only re-downloads missing/corrupted ones, not full reinstall." -ForegroundColor Yellow | |
$steamCmdPath = Resolve-SteamCmd | |
$steamUser = Resolve-SteamUser -UserOverride $User -Detected $detectedUsers | |
Test-SteamLogin -SteamCmdPath $steamCmdPath -SteamUser $steamUser -Verbose:$Verbose | |
$libraries = Get-SteamLibraries -ManualPath $LibraryPath | |
foreach ($libRoot in $libraries) { | |
$steamapps = Join-Path $libRoot "steamapps" | |
if (-not (Test-Path -LiteralPath $steamapps)) { | |
Write-Host "[WARN] No 'steamapps' in library: $libRoot" -ForegroundColor Yellow | |
continue | |
} | |
Write-Host "[LIB] $libRoot" -ForegroundColor DarkYellow | |
Get-ChildItem -LiteralPath $steamapps -Filter "appmanifest_*.acf" -ErrorAction SilentlyContinue | | |
ForEach-Object { | |
if ($_.Name -match "appmanifest_(\d+).acf") { | |
$appId = $matches[1] | |
$info = Get-AppInfoFromManifest $_.FullName | |
$gameName = $info.Name | |
$installdir = $info.InstallDir | |
if ([string]::IsNullOrWhiteSpace($installdir)) { | |
Write-Host "[WARN] Manifest missing 'installdir' for AppID $appId; skipping to avoid mis-install." -ForegroundColor Yellow | |
return | |
} | |
$installPath = Join-Path (Join-Path $libRoot "steamapps\common") $installdir | |
Validate-App $appId $gameName $installPath | |
} | |
} | |
} | |
Write-Host "=======================================" -ForegroundColor Yellow | |
Write-Host "Validation Completed: $(Get-Date)" -ForegroundColor Yellow | |
Write-Host "=======================================" -ForegroundColor Yellow |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment