#requires -version 5.1
# RTV Mod Updater -- WPF GUI for checking/updating Road to Vostok mods from ModWorkshop.
# ============================================================================
# DEBUG LOG
# ============================================================================
$_scriptDir = if ($MyInvocation.MyCommand.Path) {
[System.IO.Path]::GetDirectoryName($MyInvocation.MyCommand.Path)
} else {
$env:USERPROFILE
}
$script:LogPath = [System.IO.Path]::Combine($_scriptDir, "ModUpdater-debug.log")
if ($script:LogPath -and (Test-Path $script:LogPath)) { Remove-Item $script:LogPath -Force -ErrorAction SilentlyContinue }
function Write-Log {
param([string]$Msg)
$line = "[$(Get-Date -Format 'HH:mm:ss.fff')] $Msg"
Add-Content -Path $script:LogPath -Value $line -ErrorAction SilentlyContinue
Write-Host $line
}
# ============================================================================
# BACKEND FUNCTIONS
# ============================================================================
function Compare-SemVer {
param([string]$V1, [string]$V2)
$parse = {
param([string]$v)
$v = $v.TrimStart('v','V').Trim('"',"'")
$parts = $v.Split('.')
$major = if ($parts[0] -match '^\d+$') { [int]$parts[0] } else { 0 }
$minor = if ($parts.Count -gt 1 -and $parts[1] -match '^\d+$') { [int]$parts[1] } else { 0 }
$patch = if ($parts.Count -gt 2 -and $parts[2] -match '^\d+$') { [int]$parts[2] } else { 0 }
return ($major * 10000) + ($minor * 100) + $patch
}
return ([int](& $parse $V1)) - ([int](& $parse $V2))
}
function Expand-VmzFile {
param([string]$VmzPath, [string]$Destination)
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
try {
$zip = [System.IO.Compression.ZipFile]::OpenRead($VmzPath)
if (-not (Test-Path $Destination)) { New-Item -ItemType Directory -Path $Destination -Force | Out-Null }
foreach ($entry in $zip.Entries) {
$destPath = [System.IO.Path]::Combine($Destination, $entry.FullName)
$destDir = [System.IO.Path]::GetDirectoryName($destPath)
if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null }
if ($entry.Length -gt 0) { [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $destPath, $true) }
}
$zip.Dispose(); return $true
} catch { return $false }
}
function Get-VmzModTxt {
param([string]$VmzPath)
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue
try {
$zip = [System.IO.Compression.ZipFile]::OpenRead($VmzPath)
$entry = $zip.GetEntry("mod.txt")
if ($entry) {
$reader = New-Object System.IO.StreamReader($entry.Open())
$content = $reader.ReadToEnd()
$reader.Close()
$zip.Dispose()
return $content
}
$zip.Dispose()
} catch { }
return $null
}
function Get-ModInfoFromVmz {
param([string]$VmzPath)
$modTxt = Get-VmzModTxt $VmzPath
if (-not $modTxt) { return $null }
$info = @{ FilePath = $VmzPath; FileName = [System.IO.Path]::GetFileName($VmzPath)
Name = ""; Id = ""; Version = "0.0.0"; ModworkshopId = $null }
foreach ($line in $modTxt -split "`n") {
$line = $line.Trim()
if ($line -match '^name\s*=\s*"?([^"]+)"?\s*$') { $info.Name = $Matches[1].Trim() }
if ($line -match '^id\s*=\s*"?([^"]+)"?\s*$') { $info.Id = $Matches[1].Trim() }
if ($line -match '^version\s*=\s*"?([^"]+)"?\s*$') { $info.Version = $Matches[1].Trim() }
if ($line -match '^modworkshop\s*=\s*(\d+)') { $info.ModworkshopId = [int]$Matches[1] }
}
if (-not $info.Id) { $info.Id = [System.IO.Path]::GetFileNameWithoutExtension($VmzPath) }
return $info
}
function Get-ApiModInfo {
param([int]$ModId)
try {
return Invoke-RestMethod -Uri "https://api.modworkshop.net/mods/$ModId" `
-Method Get -Headers @{"Accept"="application/json"} -TimeoutSec 15
} catch { return $null }
}
function Get-ApiLatestFile {
param([int]$ModId)
try {
$resp = Invoke-RestMethod -Uri "https://api.modworkshop.net/mods/$ModId/files" `
-Method Get -Headers @{"Accept"="application/json"} -TimeoutSec 15
if ($resp.data -and $resp.data.Count -gt 0) {
return $resp.data | Sort-Object -Property { [int]$_.id } -Descending | Select-Object -First 1
}
} catch { }
return $null
}
function Get-LocalModFileHashes {
param([string]$ExtractedDir)
$hashes = @{}
Get-ChildItem -Path $ExtractedDir -Recurse -File | Where-Object { $_.Name -ne "mod.txt" } | ForEach-Object {
$rel = $_.FullName.Substring($ExtractedDir.Length).TrimStart([char]'/',[char]'\').Replace('\','/')
$hashes[$rel] = (Get-FileHash $_.FullName -Algorithm MD5).Hash
}
return $hashes
}
function Invoke-Download {
param([string]$Url, [string]$OutFile, [int]$MinBytes = 1024)
try {
Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing -TimeoutSec 90 -ErrorAction Stop
} catch {
Write-Log "Invoke-Download: IWR failed for $Url : $_"
}
if (-not (Test-Path $OutFile) -or (Get-Item $OutFile).Length -lt $MinBytes) {
try {
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
$wc.DownloadFile($Url, $OutFile)
$wc.Dispose()
} catch {
Write-Log "Invoke-Download: WebClient failed for $Url : $_"
}
}
if (Test-Path $OutFile) {
$sz = (Get-Item $OutFile).Length
if ($sz -ge $MinBytes) { return $true }
Write-Log "Invoke-Download: $OutFile too small ($sz bytes), removing"
Remove-Item $OutFile -Force -ErrorAction SilentlyContinue
}
return $false
}
function Get-7zaExe {
$tmp = [System.IO.Path]::GetTempPath()
$cached = [System.IO.Path]::Combine($tmp, "7za.exe")
if (Test-Path $cached) {
$sz = (Get-Item $cached).Length
if ($sz -gt 200000) {
Write-Log "Get-7zaExe: Using cached $cached ($sz bytes)"
return $cached
}
Write-Log "Get-7zaExe: Cached 7za.exe too small ($sz bytes), rebuilding"
Remove-Item $cached -Force -ErrorAction SilentlyContinue
}
# Step 1: get 7zr.exe
$szrPath = [System.IO.Path]::Combine($tmp, "7zr.exe")
$szrOk = $false
if (Test-Path $szrPath) {
$szrSz = (Get-Item $szrPath).Length
if ($szrSz -gt 200000) { $szrOk = $true; Write-Log "Get-7zaExe: 7zr.exe already cached ($szrSz bytes)" }
else { Remove-Item $szrPath -Force -ErrorAction SilentlyContinue }
}
if (-not $szrOk) {
Write-Log "Get-7zaExe: Downloading 7zr.exe ..."
$szrOk = Invoke-Download "https://github.com/ip7z/7zip/releases/download/26.01/7zr.exe" $szrPath 200000
if (-not $szrOk) {
Write-Log "Get-7zaExe: Failed to download 7zr.exe -- cannot bootstrap 7za.exe"
return $null
}
Write-Log "Get-7zaExe: 7zr.exe downloaded OK ($((Get-Item $szrPath).Length) bytes)"
}
# Step 2: download 7z2601-extra.7z
$extraPath = [System.IO.Path]::Combine($tmp, "7z-extra.7z")
$extraOk = $false
if (Test-Path $extraPath) {
$exSz = (Get-Item $extraPath).Length
if ($exSz -gt 500000) { $extraOk = $true; Write-Log "Get-7zaExe: 7z-extra.7z already cached ($exSz bytes)" }
else { Remove-Item $extraPath -Force -ErrorAction SilentlyContinue }
}
if (-not $extraOk) {
Write-Log "Get-7zaExe: Downloading 7z2601-extra.7z ..."
$extraOk = Invoke-Download "https://github.com/ip7z/7zip/releases/download/26.01/7z2601-extra.7z" $extraPath 500000
if (-not $extraOk) {
Write-Log "Get-7zaExe: Failed to download 7z-extra.7z -- cannot bootstrap 7za.exe"
return $null
}
Write-Log "Get-7zaExe: 7z-extra.7z downloaded OK ($((Get-Item $extraPath).Length) bytes)"
}
# Step 3: extract 7za.exe
Write-Log "Get-7zaExe: Extracting 7za.exe from 7z-extra.7z using 7zr.exe ..."
$extractDir = [System.IO.Path]::Combine($tmp, "7z-extra-extracted")
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue }
New-Item -ItemType Directory -Path $extractDir -Force | Out-Null
$outArg = "-o" + $extractDir
try {
$proc = Start-Process -FilePath $szrPath `
-ArgumentList "x", "-y", $outArg, $extraPath `
-Wait -PassThru -WindowStyle Hidden -ErrorAction Stop
Write-Log "Get-7zaExe: 7zr.exe extract exit=$($proc.ExitCode)"
} catch {
Write-Log "Get-7zaExe: 7zr.exe extract threw: $_"
return $null
}
$found = Get-ChildItem -Path $extractDir -Recurse -Filter "7za.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.Length -gt 200000 } | Select-Object -First 1
if ($found) {
Copy-Item -Path $found.FullName -Destination $cached -Force
Write-Log "Get-7zaExe: 7za.exe extracted and cached at $cached ($((Get-Item $cached).Length) bytes)"
Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue
return $cached
}
Write-Log "Get-7zaExe: 7za.exe not found in extracted archive contents"
return $null
}
function Expand-RarFile {
param([string]$RarPath, [string]$Destination)
if (-not (Test-Path $Destination)) { New-Item -ItemType Directory -Path $Destination -Force | Out-Null }
# 1. 7za.exe
$szr = Get-7zaExe
if ($szr -and (Test-Path $szr)) {
Write-Log "Expand-RarFile: Trying 7za.exe at $szr"
$stdoutLog = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "7za-stdout.txt")
$stderrLog = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "7za-stderr.txt")
$outArg = "-o" + $Destination
Write-Log "Expand-RarFile: 7za args: x -y `"$outArg`" `"$RarPath`""
try {
$proc = Start-Process -FilePath $szr `
-ArgumentList "x", "-y", $outArg, $RarPath `
-Wait -PassThru -WindowStyle Hidden `
-RedirectStandardOutput $stdoutLog `
-RedirectStandardError $stderrLog `
-ErrorAction Stop
$exitCode = $proc.ExitCode
$stdout = if (Test-Path $stdoutLog) { Get-Content $stdoutLog -Raw -ErrorAction SilentlyContinue } else { "" }
$stderr = if (Test-Path $stderrLog) { Get-Content $stderrLog -Raw -ErrorAction SilentlyContinue } else { "" }
Write-Log "Expand-RarFile: 7za.exe exit=$exitCode"
if ($stdout) { Write-Log "Expand-RarFile: 7za stdout: $($stdout.Trim())" }
if ($stderr) { Write-Log "Expand-RarFile: 7za stderr: $($stderr.Trim())" }
} catch {
Write-Log "Expand-RarFile: 7za.exe Start-Process threw: $_"
try {
$outArg2 = '-o"' + $Destination + '"'
$result2 = & $szr x -y $outArg2 $RarPath 2>&1
Write-Log "Expand-RarFile: 7za.exe direct invoke result: $result2"
} catch {
Write-Log "Expand-RarFile: 7za.exe direct invoke also failed: $_"
}
}
$extractedFiles = @(Get-ChildItem -Path $Destination -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -notin @('.rar', '.zip', '.7z') })
$fileCount = $extractedFiles.Count
Write-Log "Expand-RarFile: After 7za.exe, extracted file count (excl. archives) = $fileCount"
if ($fileCount -gt 0) {
Write-Log "Expand-RarFile: 7za.exe succeeded"
return $true
}
Write-Log "Expand-RarFile: 7za.exe produced no extracted files"
}
# 2. WinRAR
foreach ($winrar in @(
"$env:ProgramFiles\WinRAR\WinRAR.exe",
"${env:ProgramFiles(x86)}\WinRAR\WinRAR.exe",
"$env:LOCALAPPDATA\Programs\WinRAR\WinRAR.exe"
)) {
if (Test-Path $winrar) {
Write-Log "Expand-RarFile: Trying WinRAR at $winrar"
& $winrar x -y -o+ $RarPath $Destination | Out-Null
$wxCount = @(Get-ChildItem -Path $Destination -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -notin @('.rar', '.zip', '.7z') }).Count
if ($wxCount -gt 0) {
Write-Log "Expand-RarFile: WinRAR succeeded ($wxCount extracted files)"
return $true
}
}
}
# 3. 7-Zip installed
foreach ($sz in @(
"$env:ProgramFiles\7-Zip\7z.exe",
"${env:ProgramFiles(x86)}\7-Zip\7z.exe",
"$env:LOCALAPPDATA\Programs\7-Zip\7z.exe"
)) {
if (Test-Path $sz) {
Write-Log "Expand-RarFile: Trying installed 7-Zip at $sz"
& $sz x -y "-o$Destination" $RarPath | Out-Null
$szCount = @(Get-ChildItem -Path $Destination -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -notin @('.rar', '.zip', '.7z') }).Count
if ($szCount -gt 0) {
Write-Log "Expand-RarFile: Installed 7-Zip succeeded ($szCount extracted files)"
return $true
}
}
}
# 4. unrar
$unrarCmd = Get-Command unrar -ErrorAction SilentlyContinue
if ($unrarCmd) {
Write-Log "Expand-RarFile: Trying unrar from PATH ($($unrarCmd.Source))"
& unrar x -o+ $RarPath $Destination | Out-Null
$urCount = @(Get-ChildItem -Path $Destination -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -notin @('.rar', '.zip', '.7z') }).Count
if ($urCount -gt 0) {
Write-Log "Expand-RarFile: unrar succeeded ($urCount extracted files)"
return $true
}
}
# 5. tar.exe
$tarCmd = Get-Command tar -ErrorAction SilentlyContinue
if ($tarCmd) {
Write-Log "Expand-RarFile: Trying tar ($($tarCmd.Source))"
try {
& tar -xf $RarPath -C $Destination 2>$null | Out-Null
$tarCount = @(Get-ChildItem -Path $Destination -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -notin @('.rar', '.zip', '.7z') }).Count
if ($tarCount -gt 0) {
Write-Log "Expand-RarFile: tar succeeded ($tarCount extracted files)"
return $true
}
} catch { Write-Log "Expand-RarFile: tar threw exception: $_" }
}
Write-Log "Expand-RarFile: ALL methods failed - no extraction tool available"
return $false
}
function Get-ModpackSubModDetails {
param([int]$ModId)
$result = @{}
$tempDir = $null
Write-Log "GetModpackSubModDetails: START ModId=$ModId"
try {
$latestFile = Get-ApiLatestFile $ModId
if (-not $latestFile) {
Write-Log "GetModpackSubModDetails: No latest file returned from API for $ModId"
return $result
}
$mpReleaseDate = if ($latestFile.created_at) { $latestFile.created_at } else { "" }
Write-Log "GetModpackSubModDetails: API file id=$($latestFile.id) type=$($latestFile.type) ext=$($latestFile.extension) name=$($latestFile.file) created_at=$mpReleaseDate"
$downloadUrl = if ($latestFile.download_url) { $latestFile.download_url } elseif ($latestFile.url) { $latestFile.url } else { "" }
Write-Log "GetModpackSubModDetails: downloadUrl=$downloadUrl"
if (-not $downloadUrl) {
Write-Log "GetModpackSubModDetails: No download URL found, aborting"
return $result
}
$tempDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "rtv-modpack-$ModId")
if (Test-Path $tempDir) { Remove-Item $tempDir -Recurse -Force }
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
Write-Log "GetModpackSubModDetails: tempDir=$tempDir"
$ext = if ($latestFile.type) { $latestFile.type.ToLower() } elseif ($latestFile.extension) { $latestFile.extension.ToLower() } else { 'zip' }
$archivePath = [System.IO.Path]::Combine($tempDir, "modpack.$ext")
Write-Log "GetModpackSubModDetails: Downloading to $archivePath (ext=$ext)..."
Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath -TimeoutSec 120
$archiveSize = if (Test-Path $archivePath) { (Get-Item $archivePath).Length } else { 0 }
Write-Log "GetModpackSubModDetails: Download complete, archive size=$archiveSize bytes"
Write-Log "GetModpackSubModDetails: Trying Expand-VmzFile (ZIP)..."
$extracted = Expand-VmzFile $archivePath $tempDir
$vmzCountAfterZip = @(Get-ChildItem -Path $tempDir -Recurse -Filter "*.vmz" -ErrorAction SilentlyContinue).Count
Write-Log "GetModpackSubModDetails: Expand-VmzFile result=$extracted, vmz files found=$vmzCountAfterZip"
if (-not $extracted -or $vmzCountAfterZip -eq 0) {
Write-Log "GetModpackSubModDetails: ZIP failed or no vmz found, trying Expand-RarFile..."
$extracted = Expand-RarFile $archivePath $tempDir
$vmzCountAfterRar = @(Get-ChildItem -Path $tempDir -Recurse -Filter "*.vmz" -ErrorAction SilentlyContinue).Count
Write-Log "GetModpackSubModDetails: Expand-RarFile result=$extracted, vmz files found=$vmzCountAfterRar"
}
$allFiles = @(Get-ChildItem -Path $tempDir -Recurse -File -ErrorAction SilentlyContinue)
Write-Log "GetModpackSubModDetails: All files in tempDir after extraction ($($allFiles.Count) total):"
foreach ($f in $allFiles) { Write-Log " $($f.FullName) ($($f.Length) bytes)" }
$vmzFiles = @(Get-ChildItem -Path $tempDir -Recurse -Filter "*.vmz" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -ne $archivePath })
Write-Log "GetModpackSubModDetails: Processing $($vmzFiles.Count) vmz file(s)..."
foreach ($vmz in $vmzFiles) {
Write-Log "GetModpackSubModDetails: Reading mod.txt from $($vmz.Name)..."
$sub = Get-ModInfoFromVmz $vmz.FullName
if ($sub) {
Write-Log "GetModpackSubModDetails: -> Id='$($sub.Id)' Name='$($sub.Name)' Version='$($sub.Version)'"
if ($sub.Id) {
$innerDir = [System.IO.Path]::Combine($tempDir, "inner-$($sub.Id)")
Expand-VmzFile $vmz.FullName $innerDir | Out-Null
$hashes = @{}
if (Test-Path $innerDir) { $hashes = Get-LocalModFileHashes $innerDir }
$result[$sub.Id] = @{
Name = $sub.Name
Version = $sub.Version
FileHashes = $hashes
VmzPath = $vmz.FullName
ReleaseDate = $mpReleaseDate
}
Write-Log "GetModpackSubModDetails: -> Stored in result[$($sub.Id)], hashes=$($hashes.Count)"
} else {
Write-Log "GetModpackSubModDetails: -> SKIPPED (empty Id)"
}
} else {
Write-Log "GetModpackSubModDetails: -> mod.txt not found or unreadable in $($vmz.Name)"
}
}
Write-Log "GetModpackSubModDetails: result keys: $($result.Keys -join ', ')"
$script:ModpackTempDirs[$ModId] = $tempDir
} catch {
Write-Log "GetModpackSubModDetails: EXCEPTION: $_"
if ($tempDir -and (Test-Path $tempDir)) {
Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
Write-Log "GetModpackSubModDetails: END, returning $($result.Count) sub-mod(s)"
return $result
}
function Resolve-ModsInDirectory {
param([string]$Directory)
$mods = @()
foreach ($vmz in (Get-ChildItem -Path $Directory -Filter "*.vmz" -File)) {
$info = Get-ModInfoFromVmz $vmz.FullName
if ($info) { $info.FileSize = $vmz.Length; $mods += $info }
}
return $mods
}
function Find-ModworkshopIdByName {
param([string]$ModName, [string]$ModId)
$gameId = 864
try {
$query = $ModName -replace '\s+','+'
$resp = Invoke-RestMethod -Uri "https://api.modworkshop.net/games/$gameId/mods?query=$query&limit=10" `
-Method Get -Headers @{"Accept"="application/json"} -TimeoutSec 15
if ($resp.data -and $resp.data.Count -gt 0) {
foreach ($m in $resp.data) {
if ($m.name -and ($m.name -replace '\s+','').ToLower() -eq ($ModName -replace '\s+','').ToLower()) { return [int]$m.id }
}
return [int]($resp.data[0].id)
}
} catch { }
try {
$terms = @($ModName.ToLower(), ($ModId -replace '-',' ').ToLower(), ($ModId -replace '-','').ToLower())
$page = 1
do {
$resp = Invoke-RestMethod -Uri "https://api.modworkshop.net/games/$gameId/mods?limit=100&page=$page" `
-Method Get -Headers @{"Accept"="application/json"} -TimeoutSec 30
if ($resp.data) {
foreach ($m in $resp.data) {
$desc = (if ($m.desc) { $m.desc } else { "" }).ToLower()
$mName = (if ($m.name) { $m.name } else { "" }).ToLower()
foreach ($term in $terms) {
if ($desc -match [regex]::Escape($term) -or $mName -match [regex]::Escape($term)) { return [int]$m.id }
}
}
}
$lastPage = if ($resp.meta -and $resp.meta.last_page) { [int]$resp.meta.last_page } else { 1 }
$page++
} while ($page -le $lastPage)
} catch { }
return $null
}
function Format-FileSize {
param([long]$Bytes)
if ($Bytes -ge 1MB) { return "{0:N1} MB" -f ($Bytes / 1MB) }
if ($Bytes -ge 1KB) { return "{0:N1} KB" -f ($Bytes / 1KB) }
return "$Bytes B"
}
function Format-ReleaseDate {
param([string]$IsoDate)
if (-not $IsoDate) { return "--" }
try {
$dt = [System.DateTime]::Parse($IsoDate, [System.Globalization.CultureInfo]::InvariantCulture,
[System.Globalization.DateTimeStyles]::RoundtripKind)
return $dt.ToLocalTime().ToString("MM/dd/yyyy")
} catch { return "--" }
}
# ============================================================================
# WPF ASSEMBLIES
# ============================================================================
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Drawing, System.Windows.Forms
# ============================================================================
# DATA MODEL -- ModItem class for ListView binding + sorting
# ============================================================================
Add-Type -TypeDefinition @"
using System;
using System.ComponentModel;
public class RtvModItem2 : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private void Notify(string n) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(n)); }
private bool _checked;
private string _statusText;
private string _statusColor;
private bool _isBold;
private string _fileName;
private string _modName;
private string _modworkshopId;
private string _installedVersion;
private string _latestVersion;
private string _installedVersionColor;
private string _latestVersionColor;
private string _fileSize;
private string _filePath;
private string _internalId;
private string _releaseDate;
public bool Checked { get { return _checked; } set { _checked = value; Notify("Checked"); } }
public string StatusText { get { return _statusText; } set { _statusText = value; Notify("StatusText"); } }
public string StatusColor { get { return _statusColor; } set { _statusColor = value; Notify("StatusColor"); } }
public bool IsBold { get { return _isBold; } set { _isBold = value; Notify("IsBold"); Notify("FontWeight"); } }
public string FontWeight { get { return _isBold ? "Bold" : "Normal"; } }
public string FileName { get { return _fileName; } set { _fileName = value; Notify("FileName"); } }
public string ModName { get { return _modName; } set { _modName = value; Notify("ModName"); } }
public string ModworkshopId { get { return _modworkshopId; } set { _modworkshopId = value; Notify("ModworkshopId"); } }
public string InstalledVersion { get { return _installedVersion; } set { _installedVersion = value; Notify("InstalledVersion"); } }
public string LatestVersion { get { return _latestVersion; } set { _latestVersion = value; Notify("LatestVersion"); } }
public string InstalledVersionColor { get { return _installedVersionColor; } set { _installedVersionColor = value; Notify("InstalledVersionColor"); } }
public string LatestVersionColor { get { return _latestVersionColor; } set { _latestVersionColor = value; Notify("LatestVersionColor"); } }
public string FileSize { get { return _fileSize; } set { _fileSize = value; Notify("FileSize"); } }
public string FilePath { get { return _filePath; } set { _filePath = value; Notify("FilePath"); } }
public string InternalId { get { return _internalId; } set { _internalId = value; Notify("InternalId"); } }
public string ReleaseDate { get { return _releaseDate; } set { _releaseDate = value; Notify("ReleaseDate"); } }
public int RawModworkshopId { get; set; }
public string RawStatus { get; set; }
public string RawVersion { get; set; }
public string RawLatestVersion { get; set; }
public string RawReleaseDate { get; set; }
public bool IsModpack { get; set; }
}
"@ -ReferencedAssemblies PresentationCore, PresentationFramework, WindowsBase
# ============================================================================
# WPF XAML
# ============================================================================
$xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="RTV Mod Updater"
Height="660" Width="1220"
WindowStartupLocation="CenterScreen"
Background="#1E1E2E"
WindowStyle="SingleBorderWindow"
ResizeMode="CanResizeWithGrip"
FontFamily="Segoe UI"
FontSize="13">
<Window.Resources>
<!-- Button Styles -->
<Style x:Key="PrimaryButton" TargetType="Button">
<Setter Property="Background" Value="#89B4FA"/>
<Setter Property="Foreground" Value="#1E1E2E"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="18,8"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="7" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="#B4BEFE"/> </Trigger>
<Trigger Property="IsEnabled" Value="False"> <Setter Property="Background" Value="#45475A"/> <Setter Property="Foreground" Value="#6C7086"/> </Trigger>
</Style.Triggers>
</Style>
<Style x:Key="SuccessButton" TargetType="Button">
<Setter Property="Background" Value="#A6E3A1"/>
<Setter Property="Foreground" Value="#1E1E2E"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="18,8"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="7" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="#B8F0B3"/> </Trigger>
<Trigger Property="IsEnabled" Value="False"> <Setter Property="Background" Value="#45475A"/> <Setter Property="Foreground" Value="#6C7086"/> </Trigger>
</Style.Triggers>
</Style>
<Style x:Key="WarnButton" TargetType="Button">
<Setter Property="Background" Value="#F9E2AF"/>
<Setter Property="Foreground" Value="#1E1E2E"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="18,8"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="7" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="#FAF0C8"/> </Trigger>
<Trigger Property="IsEnabled" Value="False"> <Setter Property="Background" Value="#45475A"/> <Setter Property="Foreground" Value="#6C7086"/> </Trigger>
</Style.Triggers>
</Style>
<Style x:Key="DangerButton" TargetType="Button">
<Setter Property="Background" Value="#F38BA8"/>
<Setter Property="Foreground" Value="#1E1E2E"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="18,8"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="7" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="#F5A3BA"/> </Trigger>
<Trigger Property="IsEnabled" Value="False"> <Setter Property="Background" Value="#45475A"/> <Setter Property="Foreground" Value="#6C7086"/> </Trigger>
</Style.Triggers>
</Style>
<Style x:Key="GhostButton" TargetType="Button">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="#BAC2DE"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush" Value="#45475A"/>
<Setter Property="Padding" Value="14,7"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="6" Padding="{TemplateBinding Padding}"
BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="#313244"/> <Setter Property="Foreground" Value="#CDD6F4"/> </Trigger>
</Style.Triggers>
</Style>
<!-- ListView dark theme -->
<Style TargetType="ListView">
<Setter Property="Background" Value="#1E1E2E"/>
<Setter Property="Foreground" Value="#CDD6F4"/>
<Setter Property="BorderBrush" Value="#313244"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<!-- GridViewColumn header style -->
<Style TargetType="GridViewColumnHeader">
<Setter Property="Background" Value="#181825"/>
<Setter Property="Foreground" Value="#A6ADC8"/>
<Setter Property="BorderBrush" Value="#313244"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="Padding" Value="8,7"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewColumnHeader">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{TemplateBinding Content}"
Foreground="{TemplateBinding Foreground}"
FontWeight="{TemplateBinding FontWeight}"
FontSize="{TemplateBinding FontSize}"
VerticalAlignment="Center"/>
<TextBlock x:Name="SortArrow" Grid.Column="1"
Text="" FontSize="10" Foreground="#585B70"
VerticalAlignment="Center" Margin="4,0,0,0"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#1E1E2E"/>
<Setter Property="Foreground" Value="#CDD6F4"/>
</Trigger>
<Trigger Property="Tag" Value="Asc">
<Setter TargetName="SortArrow" Property="Text" Value=" ^"/>
<Setter TargetName="SortArrow" Property="Foreground" Value="#89B4FA"/>
<Setter Property="Foreground" Value="#B4BEFE"/>
</Trigger>
<Trigger Property="Tag" Value="Desc">
<Setter TargetName="SortArrow" Property="Text" Value=" v"/>
<Setter TargetName="SortArrow" Property="Foreground" Value="#89B4FA"/>
<Setter Property="Foreground" Value="#B4BEFE"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ScrollBar minimal dark style -->
<Style TargetType="ScrollBar">
<Setter Property="Background" Value="#181825"/>
</Style>
<!-- CheckBox dark style -->
<Style TargetType="CheckBox">
<Setter Property="Foreground" Value="#CDD6F4"/>
<Setter Property="BorderBrush" Value="#585B70"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Directory Bar -->
<Border Grid.Row="0" Background="#181825" Padding="16,9"
BorderBrush="#313244" BorderThickness="0,0,0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Mods Directory:" FontSize="12"
Foreground="#A6ADC8" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox x:Name="DirTextBox" Grid.Column="1"
Background="#313244" Foreground="#CDD6F4"
BorderBrush="#45475A" BorderThickness="1"
Padding="8,5" FontSize="12" VerticalContentAlignment="Center"
CaretBrush="#CDD6F4"/>
<Button x:Name="BrowseButton" Grid.Column="2" Content="Browse"
Style="{StaticResource GhostButton}" Margin="10,0,0,0"/>
</Grid>
</Border>
<!-- Toolbar -->
<Border Grid.Row="1" Background="#1E1E2E" Padding="16,8"
BorderBrush="#313244" BorderThickness="0,0,0,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<CheckBox x:Name="SelectAllCheckBox" Grid.Column="0"
Content="Select All" FontSize="12"
Foreground="#A6ADC8" VerticalAlignment="Center"/>
<Button x:Name="CheckButton" Grid.Column="1" Content="Check for Updates"
Style="{StaticResource PrimaryButton}" Margin="0,0,8,0"/>
<Button x:Name="UpdateButton" Grid.Column="2" Content="Update Mod(s)"
Style="{StaticResource SuccessButton}" Margin="0,0,8,0"/>
<Button x:Name="BackupButton" Grid.Column="3" Content="Backup Mods"
Style="{StaticResource WarnButton}" Margin="0,0,8,0"/>
<Button x:Name="DeleteButton" Grid.Column="4" Content="Delete Mod(s)"
Style="{StaticResource DangerButton}"/>
</Grid>
</Border>
<!-- Mod List Area -->
<Grid Grid.Row="2">
<!-- Empty / loading states -->
<Border x:Name="LoadingPanel" Background="#1E1E2E" Visibility="Collapsed">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="Loading mods and checking for updates..."
FontSize="17" FontWeight="Medium" Foreground="#CDD6F4"
HorizontalAlignment="Center"/>
<TextBlock x:Name="LoadingText" Text="Scanning mods directory..."
FontSize="13" Foreground="#6C7086"
HorizontalAlignment="Center" Margin="0,10,0,0"/>
</StackPanel>
</Border>
<Border x:Name="EmptyPanel" Background="#1E1E2E" Visibility="Visible">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="No mods loaded" FontSize="19" FontWeight="Medium"
Foreground="#585B70" HorizontalAlignment="Center"/>
<TextBlock Text="Set your mods directory and click Check for Updates"
FontSize="12" Foreground="#45475A"
HorizontalAlignment="Center" Margin="0,8,0,0"/>
</StackPanel>
</Border>
<!-- ListView with sortable GridView -->
<ListView x:Name="ModListView" Visibility="Collapsed"
VirtualizingPanel.IsVirtualizing="True"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectionMode="Single"
Focusable="False">
<ListView.View>
<GridView>
<!-- Col 0: Checkbox (non-sortable) -->
<GridViewColumn Width="36">
<GridViewColumn.Header>
<GridViewColumnHeader x:Name="CheckHeader" Tag="NoSort" Cursor="Arrow"/>
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Checked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<!-- Col 1: Status -->
<GridViewColumn Width="130">
<GridViewColumn.Header>
<GridViewColumnHeader x:Name="StatusHeader" Content="Status" Tag="None"/>
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Border Background="{Binding StatusColor}" CornerRadius="5"
Padding="8,4" HorizontalAlignment="Left"
VerticalAlignment="Center">
<TextBlock Text="{Binding StatusText}"
FontWeight="{Binding FontWeight}"
FontSize="12" Foreground="#1E1E2E"/>
</Border>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<!-- Col 2: File Name -->
<GridViewColumn Width="230">
<GridViewColumn.Header>
<GridViewColumnHeader x:Name="FileNameHeader" Content="File Name" Tag="None"/>
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel VerticalAlignment="Center" Margin="0,2,0,2">
<TextBlock Text="{Binding ModName}"
FontWeight="{Binding FontWeight}"
Foreground="#CDD6F4" FontSize="13"
TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding FileName}"
Foreground="#6C7086" FontSize="11"
Margin="0,2,0,0"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<!-- Col 3: Mod ID -->
<GridViewColumn Width="100">
<GridViewColumn.Header>
<GridViewColumnHeader x:Name="ModIdHeader" Content="Mod ID" Tag="None"/>
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock x:Name="ModIdBlock"
Text="{Binding ModworkshopId}"
Foreground="#89B4FA" FontSize="12"
TextDecorations="Underline" Cursor="Hand"
VerticalAlignment="Center"
ToolTip="{Binding ModworkshopId}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<!-- Col 4: Installed Version -->
<GridViewColumn Width="130">
<GridViewColumn.Header>
<GridViewColumnHeader x:Name="InstalledVerHeader" Content="Installed Ver" Tag="None"/>
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding InstalledVersion}"
Foreground="{Binding InstalledVersionColor}"
FontWeight="{Binding FontWeight}"
FontSize="12"
VerticalAlignment="Center"
FontFamily="Cascadia Code, Consolas, Courier New"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<!-- Col 5: Latest Version -->
<GridViewColumn Width="130">
<GridViewColumn.Header>
<GridViewColumnHeader x:Name="LatestVerHeader" Content="Latest Ver" Tag="None"/>
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding LatestVersion}"
Foreground="{Binding LatestVersionColor}"
FontWeight="{Binding FontWeight}"
FontSize="12"
VerticalAlignment="Center"
FontFamily="Cascadia Code, Consolas, Courier New"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<!-- Col 6: Release Date -->
<GridViewColumn Width="110">
<GridViewColumn.Header>
<GridViewColumnHeader x:Name="ReleaseDateHeader" Content="Released" Tag="None"/>
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding ReleaseDate}"
Foreground="#7C849E" FontSize="12"
VerticalAlignment="Center"
FontFamily="Cascadia Code, Consolas, Courier New"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<!-- Col 7: Size -->
<GridViewColumn Width="90">
<GridViewColumn.Header>
<GridViewColumnHeader x:Name="SizeHeader" Content="Size" Tag="None"/>
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding FileSize}"
Foreground="#585B70" FontSize="12"
HorizontalAlignment="Left"
VerticalAlignment="Center"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
<!-- Row style: no hover, no selection highlight -->
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="#CDD6F4"/>
<Setter Property="BorderBrush" Value="#313244"/>
<Setter Property="BorderThickness" Value="0,0,0,1"/>
<Setter Property="Padding" Value="4,8"/>
<Setter Property="MinHeight" Value="48"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<Border x:Name="RowBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
MinHeight="{TemplateBinding MinHeight}">
<GridViewRowPresenter
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
<ListView.ContextMenu>
<ContextMenu x:Name="RowContextMenu"
Background="#313244" Foreground="#CDD6F4"
BorderBrush="#45475A">
<MenuItem x:Name="CtxOpenPage" Header="Open ModWorkshop Page" Foreground="#CDD6F4"/>
<MenuItem x:Name="CtxOpenFolder" Header="Open Containing Folder" Foreground="#CDD6F4"/>
</ContextMenu>
</ListView.ContextMenu>
</ListView>
</Grid>
<!-- Status Bar -->
<Border Grid.Row="3" Background="#11111B" Padding="16,5"
BorderBrush="#313244" BorderThickness="0,1,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="StatusText" Grid.Column="0" Text="Ready"
FontSize="12" Foreground="#45475A"/>
<TextBlock x:Name="ModCountText" Grid.Column="1" Text="0 mods"
FontSize="12" Foreground="#45475A"/>
</Grid>
</Border>
</Grid>
</Window>
"@
# ============================================================================
# WINDOW ICON (Base64-encoded ICO, 7 sizes: 16/24/32/48/64/128/256px)
# ============================================================================
$IconBase64 = @'
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
'@
# ============================================================================
# PARSE XAML
# ============================================================================
$reader = [System.Xml.XmlReader]::Create([System.IO.StringReader]::new($xaml))
$script:MainWindow = [Windows.Markup.XamlReader]::Load($reader)
$reader.Close()
try {
$iconBytes = [Convert]::FromBase64String($IconBase64)
$iconStream = [System.IO.MemoryStream]::new($iconBytes, 0, $iconBytes.Length)
$script:MainWindow.Icon = [System.Windows.Media.Imaging.BitmapFrame]::Create(
$iconStream,
[System.Windows.Media.Imaging.BitmapCreateOptions]::None,
[System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad
)
} catch {
}
$C = @{
DirTextBox = $script:MainWindow.FindName("DirTextBox")
BrowseButton = $script:MainWindow.FindName("BrowseButton")
SelectAllCheckBox = $script:MainWindow.FindName("SelectAllCheckBox")
CheckButton = $script:MainWindow.FindName("CheckButton")
UpdateButton = $script:MainWindow.FindName("UpdateButton")
BackupButton = $script:MainWindow.FindName("BackupButton")
DeleteButton = $script:MainWindow.FindName("DeleteButton")
ModListView = $script:MainWindow.FindName("ModListView")
LoadingPanel = $script:MainWindow.FindName("LoadingPanel")
LoadingText = $script:MainWindow.FindName("LoadingText")
EmptyPanel = $script:MainWindow.FindName("EmptyPanel")
StatusText = $script:MainWindow.FindName("StatusText")
ModCountText = $script:MainWindow.FindName("ModCountText")
StatusHeader = $script:MainWindow.FindName("StatusHeader")
FileNameHeader = $script:MainWindow.FindName("FileNameHeader")
ModIdHeader = $script:MainWindow.FindName("ModIdHeader")
InstalledVerHeader = $script:MainWindow.FindName("InstalledVerHeader")
LatestVerHeader = $script:MainWindow.FindName("LatestVerHeader")
ReleaseDateHeader = $script:MainWindow.FindName("ReleaseDateHeader")
SizeHeader = $script:MainWindow.FindName("SizeHeader")
CtxOpenPage = $script:MainWindow.FindName("CtxOpenPage")
CtxOpenFolder = $script:MainWindow.FindName("CtxOpenFolder")
}
# ============================================================================
# OBSERVABLE COLLECTION (data source)
# ============================================================================
Add-Type -AssemblyName PresentationCore
$script:ModCollection = New-Object System.Collections.ObjectModel.ObservableCollection[RtvModItem2]
$script:ModpackTempDirs = @{} # ModworkshopId -> temp dir path (kept alive for Update button)
$C.ModListView.ItemsSource = $script:ModCollection
# ============================================================================
# HELPERS
# ============================================================================
function Set-Status {
param([string]$Text)
$C.StatusText.Text = $Text
}
function Set-LoadingText {
param([string]$Text)
$C.LoadingText.Text = $Text
}
function Show-Panel {
param([string]$Name)
$C.LoadingPanel.Visibility = "Collapsed"
$C.EmptyPanel.Visibility = "Collapsed"
$C.ModListView.Visibility = "Collapsed"
switch ($Name) {
"Loading" { $C.LoadingPanel.Visibility = "Visible" }
"Empty" { $C.EmptyPanel.Visibility = "Visible" }
"Mods" { $C.ModListView.Visibility = "Visible" }
}
}
function Get-StatusProps {
param([string]$Status)
switch ($Status) {
"Current" { return @{ Text = "Up to Date"; Color = "#A6E3A1"; InstalledColor = "#A6E3A1"; LatestColor = "#A6E3A1"; Bold = $false } }
"Update" { return @{ Text = "Out of Date"; Color = "#F9E2AF"; InstalledColor = "#F38BA8"; LatestColor = "#A6E3A1"; Bold = $true } }
"NoId" { return @{ Text = "Unknown"; Color = "#585B70"; InstalledColor = "#6C7086"; LatestColor = "#585B70"; Bold = $false } }
"Error" { return @{ Text = "Error"; Color = "#F38BA8"; InstalledColor = "#F38BA8"; LatestColor = "#F38BA8"; Bold = $false } }
default { return @{ Text = "Pending..."; Color = "#45475A"; InstalledColor = "#6C7086"; LatestColor = "#45475A"; Bold = $false } }
}
}
function New-ModItem {
param([hashtable]$Mod)
$props = Get-StatusProps ($Mod.Status)
$item = New-Object RtvModItem2
$item.Checked = $false
$item.StatusText = $props.Text
$item.StatusColor = $props.Color
$item.IsBold = $props.Bold
$item.FileName = $Mod.FileName
$item.ModName = $Mod.Name
$item.ModworkshopId = if ($Mod.ModworkshopId) { "$($Mod.ModworkshopId)" } else { "--" }
$item.RawModworkshopId = if ($Mod.ModworkshopId) { [int]$Mod.ModworkshopId } else { 0 }
$cleanVer = $Mod.Version.Trim('"').Trim("'")
$cleanApi = if ($Mod.ApiVersion) { $Mod.ApiVersion.ToString().Trim('"').Trim("'") } else { "" }
$item.InstalledVersion = "v$cleanVer"
$_latestDisp = if ($cleanApi) { "v$cleanApi" } else { "--" }
$item.LatestVersion = $_latestDisp
$item.InstalledVersionColor = $props.InstalledColor
$item.LatestVersionColor = $props.LatestColor
$item.RawVersion = $cleanVer
$item.RawLatestVersion = $cleanApi
$item.RawStatus = if ($Mod.Status) { $Mod.Status } else { "Pending" }
$_rdRaw = if ($Mod.ReleaseDate) { $Mod.ReleaseDate } else { "" }
$item.ReleaseDate = Format-ReleaseDate $_rdRaw
$item.RawReleaseDate = $_rdRaw
$item.FileSize = Format-FileSize $Mod.FileSize
$item.FilePath = $Mod.FilePath
$item.InternalId = $Mod.Id
$item.IsModpack = if ($Mod.IsModpack) { $true } else { $false }
return $item
}
function Update-ModItem {
param([RtvModItem2]$Item, [hashtable]$Mod)
$props = Get-StatusProps ($Mod.Status)
$Item.StatusText = $props.Text
$Item.StatusColor = $props.Color
$Item.IsBold = $props.Bold
$cleanVer = $Mod.Version.Trim('"').Trim("'")
$cleanApi = if ($Mod.ApiVersion) { $Mod.ApiVersion.ToString().Trim('"').Trim("'") } else { "" }
$Item.InstalledVersion = "v$cleanVer"
$Item.LatestVersion = if ($cleanApi) { "v$cleanApi" } else { "--" }
$Item.InstalledVersionColor = $props.InstalledColor
$Item.LatestVersionColor = $props.LatestColor
$Item.RawVersion = $cleanVer
$Item.RawLatestVersion = $cleanApi
$Item.RawStatus = $Mod.Status
if ($Mod.ReleaseDate) {
$Item.ReleaseDate = Format-ReleaseDate $Mod.ReleaseDate
$Item.RawReleaseDate = $Mod.ReleaseDate
}
if ($Mod.ModworkshopId) {
$Item.ModworkshopId = "$($Mod.ModworkshopId)"
$Item.RawModworkshopId = [int]$Mod.ModworkshopId
}
if ($Mod.IsModpack) { $Item.IsModpack = $true }
}
# ============================================================================
# SORTING
# ============================================================================
$script:SortColumn = "StatusText"
$script:SortDirection = "Asc"
$script:LastHeader = $null
function Apply-Sort {
$col = $script:SortColumn
$dir = $script:SortDirection
$items = @($script:ModCollection)
$sorted = switch ($col) {
"StatusText" { if ($dir -eq "Asc") { $items | Sort-Object { @("Out of Date","Pending...","Unknown","Error","Up to Date").IndexOf($_.StatusText) } }
else { $items | Sort-Object { @("Out of Date","Pending...","Unknown","Error","Up to Date").IndexOf($_.StatusText) } -Descending } }
"FileName" { if ($dir -eq "Asc") { $items | Sort-Object FileName } else { $items | Sort-Object FileName -Descending } }
"ModworkshopId" { if ($dir -eq "Asc") { $items | Sort-Object RawModworkshopId } else { $items | Sort-Object RawModworkshopId -Descending } }
"InstalledVersion" { if ($dir -eq "Asc") { $items | Sort-Object { Compare-SemVer $_.RawVersion "0.0.0" } } else { $items | Sort-Object { Compare-SemVer $_.RawVersion "0.0.0" } -Descending } }
"LatestVersion" { if ($dir -eq "Asc") { $items | Sort-Object { Compare-SemVer $_.RawLatestVersion "0.0.0" } } else { $items | Sort-Object { Compare-SemVer $_.RawLatestVersion "0.0.0" } -Descending } }
"ReleaseDate" { if ($dir -eq "Asc") { $items | Sort-Object { $_.RawReleaseDate } } else { $items | Sort-Object { $_.RawReleaseDate } -Descending } }
"FileSize" { if ($dir -eq "Asc") { $items | Sort-Object { (Get-Item $_.FilePath -ErrorAction SilentlyContinue).Length } }
else { $items | Sort-Object { (Get-Item $_.FilePath -ErrorAction SilentlyContinue).Length } -Descending } }
default { $items }
}
$script:ModCollection.Clear()
foreach ($i in $sorted) { $script:ModCollection.Add($i) | Out-Null }
}
function On-ColumnHeaderClick {
param($Header, $ColName)
if ($script:LastHeader -and $script:LastHeader -ne $Header) {
$script:LastHeader.Tag = "None"
}
if ($script:SortColumn -eq $ColName -and $script:SortDirection -eq "Asc") {
$script:SortDirection = "Desc"
$Header.Tag = "Desc"
} else {
$script:SortColumn = $ColName
$script:SortDirection = "Asc"
$Header.Tag = "Asc"
}
$script:LastHeader = $Header
Apply-Sort
}
$C.StatusHeader.Add_Click({ On-ColumnHeaderClick $C.StatusHeader "StatusText" })
$C.FileNameHeader.Add_Click({ On-ColumnHeaderClick $C.FileNameHeader "FileName" })
$C.ModIdHeader.Add_Click({ On-ColumnHeaderClick $C.ModIdHeader "ModworkshopId" })
$C.InstalledVerHeader.Add_Click({ On-ColumnHeaderClick $C.InstalledVerHeader "InstalledVersion" })
$C.LatestVerHeader.Add_Click({ On-ColumnHeaderClick $C.LatestVerHeader "LatestVersion" })
$C.ReleaseDateHeader.Add_Click({ On-ColumnHeaderClick $C.ReleaseDateHeader "ReleaseDate" })
$C.SizeHeader.Add_Click({ On-ColumnHeaderClick $C.SizeHeader "FileSize" })
# ============================================================================
# RECYCLE BIN
# ============================================================================
if ([System.Environment]::OSVersion.Platform -eq "Win32NT" -and -not ([System.Management.Automation.PSTypeName]'RecycleBin').Type) {
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class RecycleBin {
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
public static extern int SHFileOperation(ref SHFILEOPSTRUCT lpFileOp);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct SHFILEOPSTRUCT {
public IntPtr hwnd;
public int wFunc;
[MarshalAs(UnmanagedType.LPTStr)] public string pFrom;
[MarshalAs(UnmanagedType.LPTStr)] public string pTo;
public int fFlags;
public int fAnyOperationsAborted;
public IntPtr hNameMappings;
[MarshalAs(UnmanagedType.LPTStr)] public string lpszProgressTitle;
}
public const int FO_DELETE = 0x0003;
public const int FOF_ALLOWUNDO = 0x0040;
public const int FOF_NOCONFIRMATION = 0x0010;
public const int FOF_SILENT = 0x0004;
public static void Send(string path) {
SHFILEOPSTRUCT op = new SHFILEOPSTRUCT();
op.wFunc = FO_DELETE;
op.pFrom = path + "\0\0";
op.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT;
SHFileOperation(ref op);
}
}
"@
}
# ============================================================================
# EVENT HANDLERS
# ============================================================================
# Browse
$C.BrowseButton.Add_Click({
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
$dlg.Description = "Select your Road to Vostok mods directory"
if ($C.DirTextBox.Text -and (Test-Path $C.DirTextBox.Text)) {
$dlg.SelectedPath = $C.DirTextBox.Text
}
if ($dlg.ShowDialog() -eq "OK") {
$C.DirTextBox.Text = $dlg.SelectedPath
Save-ModsDir $dlg.SelectedPath
$C.CheckButton.RaiseEvent(
[System.Windows.RoutedEventArgs]::new([System.Windows.Controls.Button]::ClickEvent)
)
}
})
# Allow pressing Enter in the DirTextBox to trigger a check
$C.DirTextBox.Add_KeyDown({
param($s, $e)
if ($e.Key -eq [System.Windows.Input.Key]::Return -or
$e.Key -eq [System.Windows.Input.Key]::Enter) {
$typed = $C.DirTextBox.Text.Trim()
if ($typed -and (Test-Path $typed)) { Save-ModsDir $typed }
$C.CheckButton.RaiseEvent(
[System.Windows.RoutedEventArgs]::new([System.Windows.Controls.Button]::ClickEvent)
)
}
})
# Select All
$C.SelectAllCheckBox.Add_Checked({
foreach ($item in $script:ModCollection) { $item.Checked = $true }
})
$C.SelectAllCheckBox.Add_Unchecked({
foreach ($item in $script:ModCollection) { $item.Checked = $false }
})
# Mod ID click -- open browser
$C.ModListView.Add_MouseLeftButtonUp({
param($s, $e)
$el = $e.OriginalSource
$tb = $el
while ($tb -and $tb -isnot [System.Windows.Controls.TextBlock]) { $tb = $tb.Parent }
if ($tb -and $tb.Name -eq "ModIdBlock" -and $tb.Text -match '^\d+$') {
Start-Process "https://modworkshop.net/mod/$($tb.Text)"
}
})
# Context menu
$C.CtxOpenPage.Add_Click({
$item = $C.ModListView.SelectedItem
if ($item -and $item.RawModworkshopId -gt 0) {
Start-Process "https://modworkshop.net/mod/$($item.RawModworkshopId)"
}
})
$C.CtxOpenFolder.Add_Click({
$item = $C.ModListView.SelectedItem
if ($item -and $item.FilePath) {
Start-Process ([System.IO.Path]::GetDirectoryName($item.FilePath))
}
})
# ============================================================================
# CHECK FOR UPDATES
# ============================================================================
$C.CheckButton.Add_Click({
$modsDir = $C.DirTextBox.Text
if (-not $modsDir -or -not (Test-Path $modsDir)) {
[System.Windows.MessageBox]::Show("Please select a valid mods directory first.", "No Directory", "OK", "Warning")
return
}
$script:ModCollection.Clear()
$script:RawMods = @()
$script:ItemMap = @{}
foreach ($td in $script:ModpackTempDirs.Values) {
if (Test-Path $td) { Remove-Item $td -Recurse -Force -ErrorAction SilentlyContinue }
}
$script:ModpackTempDirs = @{}
Show-Panel "Loading"
Set-LoadingText "Scanning mods directory..."
Set-Status "Scanning $modsDir..."
$rawMods = Resolve-ModsInDirectory $modsDir
if ($rawMods.Count -eq 0) {
Show-Panel "Empty"
Set-Status "No .vmz mods found."
$C.ModCountText.Text = "0 mods"
return
}
$script:RawMods = $rawMods
Write-Log "Scan: Found $($rawMods.Count) local vmz file(s):"
foreach ($mod in $rawMods) {
Write-Log "Scan: File='$($mod.FileName)' Id='$($mod.Id)' Name='$($mod.Name)' Version='$($mod.Version)' ModworkshopId='$($mod.ModworkshopId)'"
$mod.Status = "Pending"
$item = New-ModItem -Mod $mod
$script:ModCollection.Add($item) | Out-Null
$script:ItemMap[$mod.Id] = $item
}
$C.ModCountText.Text = "$($rawMods.Count) mods"
Set-Status "Found $($rawMods.Count) mod(s). Resolving IDs..."
foreach ($mod in $rawMods) {
if (-not $mod.ModworkshopId) {
Set-Status "Looking up: $($mod.Name)..."
$found = Find-ModworkshopIdByName -ModName $mod.Name -ModId $mod.Id
if ($found) {
$mod.ModworkshopId = $found
$key = $mod.Id
if ($script:ItemMap[$key]) { Update-ModItem -Item $script:ItemMap[$key] -Mod $mod }
}
}
}
$groups = @{}
foreach ($mod in $rawMods) {
if ($mod.ModworkshopId) {
$gkey = [string]$mod.ModworkshopId
if (-not $groups[$gkey]) { $groups[$gkey] = @() }
$groups[$gkey] += $mod
}
}
$standaloneIds = @(); $modpackIds = @()
foreach ($id in $groups.Keys) {
if ($groups[$id].Count -gt 1) { $modpackIds += [string]$id } else { $standaloneIds += [string]$id }
}
Write-Log "Groups: standaloneIds=[$($standaloneIds -join ', ')] modpackIds=[$($modpackIds -join ', ')]"
$saIdx = 0
foreach ($mpId in $standaloneIds) {
$saIdx++
$mod = $groups[$mpId][0]
Set-Status "Checking ($saIdx/$($standaloneIds.Count)): $($mod.Name)..."
Write-Log "CheckStandalone: [$saIdx/$($standaloneIds.Count)] Id=$mpId Name='$($mod.Name)' LocalVer='$($mod.Version)'"
try {
$apiMod = Get-ApiModInfo ([int]$mpId)
$apiFile = Get-ApiLatestFile ([int]$mpId)
if ($apiMod) {
$apiVer = if ($apiMod.version) { $apiMod.version } else { "0.0.0" }
$apiDate = if ($apiFile -and $apiFile.created_at) { $apiFile.created_at } else { "" }
Write-Log "CheckStandalone: API version=$apiVer created_at=$apiDate local=$($mod.Version) cmp=$(Compare-SemVer $apiVer $mod.Version)"
$mod.ReleaseDate = $apiDate
$mod.ApiVersion = $apiVer
if ((Compare-SemVer $apiVer $mod.Version) -gt 0) {
$mod.Status = "Update"
Write-Log "CheckStandalone: -> OUT OF DATE (local=$($mod.Version) api=$apiVer)"
} else {
$mod.Status = "Current"
Write-Log "CheckStandalone: -> UP TO DATE (local=$($mod.Version) api=$apiVer)"
}
} else {
$mod.Status = "Error"
Write-Log "CheckStandalone: -> ERROR (no API response)"
}
} catch {
$mod.Status = "Error"
Write-Log "CheckStandalone: -> EXCEPTION: $_"
}
$key = $mod.Id
if ($script:ItemMap[$key]) { Update-ModItem -Item $script:ItemMap[$key] -Mod $mod }
}
$mpIdx = 0
foreach ($mpId in $modpackIds) {
$mpIdx++
Set-Status "Downloading modpack $mpId ($mpIdx/$($modpackIds.Count))..."
Set-LoadingText "Downloading modpack $mpId..."
Write-Log "CheckModpack: [$mpIdx/$($modpackIds.Count)] ModWorkshop ID=$mpId has $($groups[$mpId].Count) local mod(s)"
foreach ($mod in $groups[$mpId]) {
Write-Log "CheckModpack: local mod Id='$($mod.Id)' Name='$($mod.Name)' Version='$($mod.Version)' File='$($mod.FileName)'"
}
foreach ($mod in $groups[$mpId]) { $mod.IsModpack = $true }
try {
$remote = Get-ModpackSubModDetails ([int]$mpId)
Write-Log "CheckModpack: remote result has $($remote.Count) key(s): $($remote.Keys -join ', ')"
Set-Status "Comparing modpack $mpId ($mpIdx/$($modpackIds.Count))..."
foreach ($mod in $groups[$mpId]) {
try {
Write-Log "CheckModpack: Looking up remote[$($mod.Id)]..."
$rem = $remote[$mod.Id]
if ($rem) {
$remVer = if ($rem.Version) { $rem.Version } else { "" }
Write-Log "CheckModpack: Found remote entry for '$($mod.Id)': RemoteVer='$remVer' LocalVer='$($mod.Version)' ReleaseDate=$($rem.ReleaseDate)"
$mod.ReleaseDate = if ($rem.ReleaseDate) { $rem.ReleaseDate } else { "" }
$semCmp = if ($remVer) { Compare-SemVer $remVer $mod.Version } else { 0 }
Write-Log "CheckModpack: semCmp=$semCmp"
$mod.ApiVersion = if ($remVer) { $remVer } else { $mod.Version }
if ($semCmp -gt 0) {
$mod.Status = "Update"
} else {
$localDir = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "rtv-local-$($mod.Id)")
if (Test-Path $localDir) { Remove-Item $localDir -Recurse -Force -ErrorAction SilentlyContinue }
$expandOk = Expand-VmzFile $mod.FilePath $localDir
Write-Log "CheckModpack: local vmz expand ok=$expandOk localDir exists=$(Test-Path $localDir)"
if ($expandOk -and (Test-Path $localDir)) {
$localH = Get-LocalModFileHashes $localDir
$remH = if ($rem.FileHashes) { $rem.FileHashes } else { @{} }
Write-Log "CheckModpack: localH=$($localH.Count) keys, remH=$($remH.Count) keys"
Remove-Item $localDir -Recurse -Force -ErrorAction SilentlyContinue
$changed = $false
foreach ($k in $remH.Keys) {
if (-not $localH[$k] -or $localH[$k] -ne $remH[$k]) { $changed = $true; break }
}
if (-not $changed) {
foreach ($k in $localH.Keys) {
if (-not $remH[$k]) { $changed = $true; break }
}
}
if ($changed) {
$mod.Status = "Update"
$mod.ApiVersion = if ($remVer) { "$remVer (content)" } else { "$($mod.Version) (content)" }
} else {
$mod.Status = "Current"
if ($remVer) { $mod.ApiVersion = $remVer }
}
} else {
Write-Log "CheckModpack: Could not expand local vmz, assuming Current"
$mod.Status = "Current"
if ($remVer) { $mod.ApiVersion = $remVer }
}
}
} else {
Write-Log "CheckModpack: No remote entry for '$($mod.Id)' -- marking NoId. Available keys: $($remote.Keys -join ', ')"
$mod.Status = "NoId"
}
} catch {
Write-Log "CheckModpack: EXCEPTION processing '$($mod.Id)': $_"
$mod.Status = "Error"
}
Write-Log "CheckModpack: '$($mod.Id)' final status=$($mod.Status)"
$mod.IsModpack = $true
$key = $mod.Id
if ($script:ItemMap[$key]) { Update-ModItem -Item $script:ItemMap[$key] -Mod $mod }
}
} catch {
Write-Log "CheckModpack: OUTER EXCEPTION for modpack $mpId : $_"
foreach ($mod in $groups[$mpId]) {
$mod.Status = "Error"; $mod.IsModpack = $true
$key = $mod.Id
if ($script:ItemMap[$key]) { Update-ModItem -Item $script:ItemMap[$key] -Mod $mod }
}
}
}
foreach ($mod in $rawMods) {
if ($mod.Status -eq "Pending") {
$mod.Status = "NoId"
$key = $mod.Id
if ($script:ItemMap[$key]) { Update-ModItem -Item $script:ItemMap[$key] -Mod $mod }
}
}
$script:SortColumn = "StatusText"
$script:SortDirection = "Asc"
$C.StatusHeader.Tag = "Asc"
$script:LastHeader = $C.StatusHeader
Apply-Sort
$upd = ($rawMods | Where-Object { $_.Status -eq "Update" }).Count
$ok = ($rawMods | Where-Object { $_.Status -eq "Current" }).Count
$total = $rawMods.Count
Set-Status "$total mod(s) checked, $ok mod(s) up to date, $upd mod update(s) available."
Show-Panel "Mods"
})
# ============================================================================
# UPDATE MOD(S)
# ============================================================================
$C.UpdateButton.Add_Click({
$selected = @($script:ModCollection | Where-Object { $_.Checked -eq $true })
if ($selected.Count -eq 0) {
[System.Windows.MessageBox]::Show("Check the boxes next to mods you want to update.", "No Selection", "OK", "Information")
return
}
$dl = 0; $fail = 0; $skipNotReady = 0; $skipNoId = 0
$total = @($selected | Where-Object { $_.RawModworkshopId -gt 0 -and $_.RawStatus -eq "Update" }).Count
$current = 0
foreach ($item in $selected) {
if ($item.RawModworkshopId -le 0) { $skipNoId++; continue }
if ($item.RawStatus -ne "Update") { $skipNotReady++; continue }
$current++
$label = if ($item.ModName -and $item.ModName -ne $item.FileName) { $item.ModName } else { $item.FileName }
Set-Status "Updating $label... ($current of $total)"
$script:MainWindow.Dispatcher.Invoke(
[System.Windows.Threading.DispatcherPriority]::Render,
[System.Action]{}
)
try {
if ($item.IsModpack) {
$mpId = $item.RawModworkshopId
$subId = $item.InternalId
$tempDir = $script:ModpackTempDirs[$mpId]
if (-not $tempDir -or -not (Test-Path $tempDir)) {
Set-Status "Re-downloading modpack $mpId for $label... ($current of $total)"
$remote = Get-ModpackSubModDetails $mpId # re-downloads and stores in ModpackTempDirs
$tempDir = $script:ModpackTempDirs[$mpId]
}
$subVmzPath = $null
if ($tempDir -and (Test-Path $tempDir)) {
foreach ($vmz in (Get-ChildItem -Path $tempDir -Recurse -Filter "*.vmz")) {
$sub = Get-ModInfoFromVmz $vmz.FullName
if ($sub -and $sub.Id -eq $subId) {
$subVmzPath = $vmz.FullName
break
}
}
}
if ($subVmzPath -and (Test-Path $subVmzPath)) {
Copy-Item -Path $subVmzPath -Destination $item.FilePath -Force
$newVer = $item.RawLatestVersion
$newVer = $newVer -replace '\s*\(content\)', ''
if (-not $newVer) { $newVer = $item.RawVersion }
$item.InstalledVersion = "v$newVer"
$item.LatestVersion = "v$newVer"
$item.InstalledVersionColor = "#A6E3A1"
$item.LatestVersionColor = "#A6E3A1"
$item.RawVersion = $newVer
$item.RawLatestVersion = $newVer
$item.StatusText = "Up to Date"
$item.StatusColor = "#A6E3A1"
$item.IsBold = $false
$item.RawStatus = "Current"
$dl++
} else {
$item.StatusText = "DL Failed"
$item.StatusColor = "#F38BA8"
$fail++
}
} else {
$lf = Get-ApiLatestFile $item.RawModworkshopId
if ($lf) {
$url = if ($lf.download_url) { $lf.download_url } elseif ($lf.url) { $lf.url } else { "" }
if ($url) {
$tempFile = [System.IO.Path]::Combine(
[System.IO.Path]::GetTempPath(),
"rtv-update-$($item.InternalId).vmz"
)
Invoke-WebRequest -Uri $url -OutFile $tempFile -TimeoutSec 120
if (Test-Path $tempFile) {
Copy-Item -Path $tempFile -Destination $item.FilePath -Force
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
$newVer = $item.RawLatestVersion
if (-not $newVer) { $newVer = $item.RawVersion }
$item.InstalledVersion = "v$newVer"
$item.LatestVersion = "v$newVer"
$item.InstalledVersionColor = "#A6E3A1"
$item.LatestVersionColor = "#A6E3A1"
$item.RawVersion = $newVer
$item.RawLatestVersion = $newVer
$item.StatusText = "Up to Date"
$item.StatusColor = "#A6E3A1"
$item.IsBold = $false
$item.RawStatus = "Current"
$dl++
} else { $fail++ }
} else { $fail++ }
} else { $fail++ }
}
} catch {
$item.StatusText = "DL Failed"
$item.StatusColor = "#F38BA8"
$fail++
}
}
if ($dl -gt 0 -and $fail -eq 0) {
$msg = "All $dl mod(s) updated successfully."
} elseif ($dl -gt 0) {
$msg = "$dl updated"
if ($fail -gt 0) { $msg += ", $fail failed" }
$msg += "."
} elseif ($fail -gt 0) {
$msg = "$fail update(s) failed."
} else {
$msg = "All selected mod(s) are up to date."
}
if ($skipNoId -gt 0) { $msg += " $skipNoId skipped (no ModWorkshop ID)." }
Set-Status $msg
})
# ============================================================================
# BACKUP MODS
# ============================================================================
$C.BackupButton.Add_Click({
$modsDir = $C.DirTextBox.Text
if (-not $modsDir -or -not (Test-Path $modsDir)) {
[System.Windows.MessageBox]::Show("Please select a valid mods directory first.", "No Directory", "OK", "Warning")
return
}
$vmzFiles = @(Get-ChildItem -Path $modsDir -Filter "*.vmz" -File)
if ($vmzFiles.Count -eq 0) {
[System.Windows.MessageBox]::Show("No .vmz mod files found in the mods directory.", "Nothing to Backup", "OK", "Information")
return
}
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$zipName = "mods-backup-$timestamp.zip"
$zipPath = [System.IO.Path]::Combine($modsDir, $zipName)
Set-Status "Backing up $($vmzFiles.Count) mod(s) to $zipName..."
try {
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop
$zip = [System.IO.Compression.ZipFile]::Open($zipPath, [System.IO.Compression.ZipArchiveMode]::Create)
foreach ($file in $vmzFiles) {
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$zip, $file.FullName, $file.Name,
[System.IO.Compression.CompressionLevel]::Optimal
) | Out-Null
}
$zip.Dispose()
$zipSize = Format-FileSize ((Get-Item $zipPath).Length)
Set-Status "Backup complete. $zipName ($zipSize, $($vmzFiles.Count) mods)"
[System.Windows.MessageBox]::Show(
"Backup saved successfully.`n`nFile: $zipName`nSize: $zipSize`nMods: $($vmzFiles.Count)`nLocation: $modsDir",
"Backup Complete", "OK", "None"
)
} catch {
if (Test-Path $zipPath) { Remove-Item $zipPath -Force -ErrorAction SilentlyContinue }
Set-Status "Backup failed: $_"
[System.Windows.MessageBox]::Show("Backup failed:`n$_", "Backup Error", "OK", "Error")
}
})
# ============================================================================
# DELETE MOD(S)
# ============================================================================
$C.DeleteButton.Add_Click({
$selected = @($script:ModCollection | Where-Object { $_.Checked -eq $true })
if ($selected.Count -eq 0) {
[System.Windows.MessageBox]::Show("Check the boxes next to mods you want to delete.", "No Selection", "OK", "Information")
return
}
$names = ($selected | ForEach-Object { $_.FileName }) -join "`n"
$result = [System.Windows.MessageBox]::Show("Move these mods to Recycle Bin?`n`n$names", "Confirm Delete", "YesNo", "Question")
if ($result -ne "Yes") { return }
$del = 0
foreach ($item in $selected) {
try {
if ([System.Environment]::OSVersion.Platform -eq "Win32NT") { [RecycleBin]::Send($item.FilePath) }
else { Remove-Item $item.FilePath -Force }
$script:ModCollection.Remove($item) | Out-Null
$del++
} catch {
[System.Windows.MessageBox]::Show("Failed to delete: $($item.FileName)`n$_", "Delete Error", "OK", "Error")
}
}
$C.ModCountText.Text = "$($script:ModCollection.Count) mods"
Set-Status "Moved $del mod(s) to Recycle Bin."
if ($script:ModCollection.Count -eq 0) { Show-Panel "Empty" }
})
# ============================================================================
# INIT
# ============================================================================
# REGISTRY PERSISTENCE (HKCU\Software\RTVModUpdater)
# ============================================================================
$script:RegPath = "HKCU:\Software\RTVModUpdater"
function Save-ModsDir {
param([string]$Path)
if (-not $Path) { return }
try {
if (-not (Test-Path $script:RegPath)) {
New-Item -Path $script:RegPath -Force | Out-Null
}
Set-ItemProperty -Path $script:RegPath -Name "ModsDirectory" -Value $Path -ErrorAction Stop
} catch { }
}
function Load-ModsDir {
try {
$val = (Get-ItemProperty -Path $script:RegPath -Name "ModsDirectory" -ErrorAction Stop).ModsDirectory
if ($val -and (Test-Path $val)) { return $val }
} catch { }
return ""
}
# ============================================================================
$initialDir = ""
if ((Load-ModsDir) -ne "") {
$initialDir = Load-ModsDir
} else {
$steamPath = $null
try {
$steamPath = (Get-ItemProperty -Path "HKCU:\Software\Valve\Steam" -Name "SteamPath" -ErrorAction Stop).SteamPath
$steamPath = $steamPath.Replace('/', '\')
} catch { }
$candidates = @()
if ($steamPath) {
$candidates += "$steamPath\steamapps\common\Road to Vostok\mods"
$vdf = "$steamPath\steamapps\libraryfolders.vdf"
if (Test-Path $vdf) {
Select-String -Path $vdf -Pattern '"path"\s+"([^"]+)"' | ForEach-Object {
$lib = $_.Matches[0].Groups[1].Value.Replace('\\','\').Replace('/','\')
$candidates += "$lib\steamapps\common\Road to Vostok\mods"
}
}
}
foreach ($drive in 'C','D','E','F','G','H') {
$candidates += "${drive}:\Program Files\Steam\steamapps\common\Road to Vostok\mods"
$candidates += "${drive}:\Program Files (x86)\Steam\steamapps\common\Road to Vostok\mods"
$candidates += "${drive}:\Steam\steamapps\common\Road to Vostok\mods"
$candidates += "${drive}:\SteamLibrary\steamapps\common\Road to Vostok\mods"
}
$candidates += "$env:ProgramFiles\Steam\steamapps\common\Road to Vostok\mods"
$candidates += "${env:ProgramFiles(x86)}\Steam\steamapps\common\Road to Vostok\mods"
foreach ($p in $candidates) { if (Test-Path $p) { $initialDir = $p; break } }
if (-not $initialDir) {
$scriptDir = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName) }
foreach ($p in @($scriptDir, (Get-Location).Path)) {
if ($p -and (Test-Path $p) -and @(Get-ChildItem -Path $p -Filter "*.vmz" -File -ErrorAction SilentlyContinue).Count -gt 0) {
$initialDir = $p; break
}
}
}
}
$C.DirTextBox.Text = $initialDir
Set-Status "Ready. Select your mods directory and click Check for Updates."
if ($initialDir) {
Show-Panel "Loading"
Set-LoadingText "Scanning mods directory..."
Set-Status "Starting up..."
$script:MainWindow.Add_Loaded({
$script:MainWindow.Dispatcher.Invoke(
[System.Windows.Threading.DispatcherPriority]::Render,
[System.Action]{}
)
$C.CheckButton.RaiseEvent(
[System.Windows.RoutedEventArgs]::new([System.Windows.Controls.Button]::ClickEvent)
)
})
}
$script:MainWindow.Add_Closing({
$dir = $C.DirTextBox.Text.Trim()
if ($dir) { Save-ModsDir $dir }
})
try {
$script:MainWindow.ShowDialog() | Out-Null
} catch {
[System.Windows.MessageBox]::Show("Fatal error:`n$_", "Error", "OK", "Error")
}