# Valtaren - Windows one-line installer (served at /mmo/install.ps1 on valtaren.com and arminefe.com) # # Run it with (Win + R, or in any PowerShell window): # powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol=3072; irm https://valtaren.com/mmo/install.ps1 | iex" # or, in an open PowerShell window: irm https://valtaren.com/mmo/install.ps1 | iex # (3072 = TLS 1.2: Windows PowerShell 5.1 offers only TLS 1.0 by default on many Windows 10/11 setups, and the server # speaks TLS 1.2 and newer only, so without it the very first "irm" can fail before this script ever runs.) # The old address (https://arminefe.com/mmo/install.ps1) serves this same script. # # What it does, step by step: # 1. asks https://valtaren.com/mmo/client/latest?platform=win64 (falling back to arminefe.com) for the current build # 2. downloads that zip (only from the server that answered, no redirects) into %TEMP%\OldKnightsSetup, resuming # if the line drops # 3. checks the zip's SHA-256 against the one the server published; stops if they differ # 4. asks a running game (Valtaren, or Old Knights before the rename) to close and waits for it # 5. unpacks the game into %LOCALAPPDATA%\Programs\Valtaren; an older install there, or the pre-rename # %LOCALAPPDATA%\Programs\Old Knights, is replaced (settings live elsewhere and are kept) # 6. puts "Valtaren" shortcuts on the desktop and in the Start menu (old "Old Knights" ones are removed) and moves # the Settings > Apps entry of an Old Knights install to the new folder # 7. starts the game # No administrator rights are needed and nothing outside those folders is touched. Afterwards the game keeps itself # up to date (it checks for a new version every time it starts). # # Runs on Windows PowerShell 5.1 (Windows 10 and 11). Everything is inside one script block so that, run with # "irm | iex" from an open PowerShell window, it leaves no settings behind and never closes that window. & { $ErrorActionPreference = 'Stop' $ProgressPreference = 'Continue' $Sites = @('https://valtaren.com/mmo', 'https://arminefe.com/mmo') $Site = $Sites[0] $GameName = 'Valtaren' $LegacyName = 'Old Knights' # the game's name on disk before the rename $ExeName = 'Valtaren.exe' # a zip from before the rename holds 'Old Knights.exe' instead $InstallDir = Join-Path $env:LOCALAPPDATA "Programs\$GameName" $LegacyDir = Join-Path $env:LOCALAPPDATA "Programs\$LegacyName" $UninstKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Valtaren' $LegacyKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\OldKnights' $WorkDir = Join-Path $env:TEMP 'OldKnightsSetup' $Steps = 6 # ---------------- Output: a Turkish line, then the same in English ---------------- function Step([int]$n, [string]$tr, [string]$en) { Write-Host '' Write-Host "[$n/$Steps] $tr" -ForegroundColor Cyan Write-Host " $en" -ForegroundColor DarkGray } function Say([string]$tr, [string]$en, [string]$color = 'Gray') { Write-Host " $tr" -ForegroundColor $color Write-Host " $en" -ForegroundColor DarkGray } # A failure with a message for the player (the catch at the bottom prints it and keeps the window open). function Fail([string]$tr, [string]$en) { throw (New-Object System.Exception ("$tr`n$en")) } function MB([long]$bytes) { '{0:N0} MB' -f ($bytes / 1MB) } # ---------------- Download: streamed to disk with progress, resumed after a dropped line ---------------- function Get-GameZip([string]$url, [string]$path, [long]$size) { $buffer = New-Object byte[] (1MB) for ($try = 1; $try -le 5; $try++) { $have = 0 if (Test-Path -LiteralPath $path) { $have = (Get-Item -LiteralPath $path).Length } if ($have -gt $size) { Remove-Item -LiteralPath $path -Force; $have = 0 } if ($have -eq $size) { return } try { $request = [System.Net.HttpWebRequest]::Create($url) $request.AllowAutoRedirect = $false # only the server that answered: never follow a redirect elsewhere $request.UserAgent = 'ValtarenInstaller/2' $request.Timeout = 30000 $request.ReadWriteTimeout = 60000 if ($have -gt 0) { $request.AddRange([long]$have) } $response = $request.GetResponse() try { $code = [int]$response.StatusCode if ($code -ne 200 -and $code -ne 206) { throw "HTTP $code" } if ($code -eq 200) { $have = 0 } # the server sent the whole file: start over $mode = if ($have -gt 0) { [System.IO.FileMode]::Append } else { [System.IO.FileMode]::Create } $in = $response.GetResponseStream() $out = New-Object System.IO.FileStream($path, $mode, [System.IO.FileAccess]::Write) try { $done = $have $clock = [System.Diagnostics.Stopwatch]::StartNew() $shown = -1000 while (($read = $in.Read($buffer, 0, $buffer.Length)) -gt 0) { $out.Write($buffer, 0, $read) $done += $read if ($clock.ElapsedMilliseconds - $shown -ge 500) { $shown = $clock.ElapsedMilliseconds $speed = ($done - $have) / [Math]::Max(0.5, $clock.Elapsed.TotalSeconds) $left = if ($speed -gt 0) { [int](($size - $done) / $speed) } else { -1 } Write-Progress -Activity "Valtaren indiriliyor / Downloading Valtaren" ` -Status ("{0} / {1} ({2:N1} MB/s)" -f (MB $done), (MB $size), ($speed / 1MB)) ` -PercentComplete ([Math]::Min(100, [int](100 * $done / $size))) -SecondsRemaining $left } } } finally { $out.Dispose() $in.Dispose() } } finally { $response.Close() } } catch { Write-Progress -Activity "Valtaren indiriliyor / Downloading Valtaren" -Completed if ($try -lt 5) { Say "Bağlantı koptu, kaldığı yerden devam ediliyor ($try/5)..." ` "The connection dropped, resuming where it stopped ($try/5)..." 'Yellow' Start-Sleep -Seconds (3 * $try) continue } Fail "İndirme tamamlanamadı: $($_.Exception.Message). İnternet bağlantını kontrol edip tekrar dene." ` "The download could not be finished: $($_.Exception.Message). Check the internet connection and try again." } Write-Progress -Activity "Valtaren indiriliyor / Downloading Valtaren" -Completed } if ((Get-Item -LiteralPath $path).Length -ne $size) { Fail 'İndirilen dosya eksik kaldı. Tekrar dene.' 'The downloaded file is incomplete. Please try again.' } } # Free bytes on the disk holding $path (a network folder cannot be asked: counted as enough). function Get-FreeSpace([string]$path) { try { (New-Object System.IO.DriveInfo([System.IO.Path]::GetPathRoot($path))).AvailableFreeSpace } catch { [long]::MaxValue } } # The game and its crash reporter (UnityCrashHandler64.exe from the install folder, which outlives the game by a # moment): while either runs, the install folder cannot be renamed. function Get-GameProcesses { @(Get-Process -Name @($GameName, $LegacyName, 'UnityCrashHandler64') -ErrorAction SilentlyContinue | Where-Object { if ($_.ProcessName -eq $GameName -or $_.ProcessName -eq $LegacyName) { return $true } $path = $null try { $path = $_.Path } catch { } [bool]$path -and ($path.StartsWith($InstallDir + '\', [StringComparison]::OrdinalIgnoreCase) -or $path.StartsWith($LegacyDir + '\', [StringComparison]::OrdinalIgnoreCase)) }) } # Renames/moves a folder on the same disk, retrying for a few seconds: right after unpacking, antivirus software # (Windows Defender too) briefly holds the new .exe and .dll files open, and a closed game's files can linger. function Move-Folder([string]$from, [string]$to) { for ($try = 1; ; $try++) { try { [System.IO.Directory]::Move($from, $to); return } catch { if ($try -ge 30) { throw } ; Start-Sleep -Milliseconds 500 } } } try { Write-Host '' Write-Host ' VALTAREN - Kolay kurulum / Easy install' -ForegroundColor Yellow Write-Host " $InstallDir" -ForegroundColor DarkGray if (-not [Environment]::Is64BitOperatingSystem) { Fail 'Valtaren 64 bit Windows ister; bu bilgisayar 32 bit.' 'Valtaren needs 64-bit Windows; this computer is 32-bit.' } # The server speaks TLS 1.2; older Windows PowerShell setups do not offer it on their own (left alone when # Windows already chooses the protocol itself). $protocols = [System.Net.ServicePointManager]::SecurityProtocol if ([int]$protocols -ne 0) { [System.Net.ServicePointManager]::SecurityProtocol = $protocols -bor [System.Net.SecurityProtocolType]::Tls12 } # ---------------- 1. Which version ---------------- Step 1 'Son sürüm soruluyor...' 'Checking the latest version...' # valtaren.com first; the old address (same server) when it cannot be reached. The download comes from the # address that answered. $release = $null $problem = '' foreach ($candidate in $Sites) { try { $release = Invoke-RestMethod -Uri "$candidate/client/latest?platform=win64" -MaximumRedirection 0 -TimeoutSec 30 $Site = $candidate break } catch { $problem = $_.Exception.Message } } if ($null -eq $release) { Fail "Sunucuya ulaşılamadı ($problem). İnternet bağlantını kontrol edip tekrar dene." ` "Could not reach the server ($problem). Check the internet connection and try again." } $file = [string]$release.File $hash = [string]$release.Sha256 $size = [long]$release.Size if ($file -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*\.zip$' -or $hash -notmatch '^[0-9a-fA-F]{64}$' -or $size -le 0) { Fail 'Sunucu şu an indirilebilir bir Windows sürümü sunmuyor. Biraz sonra tekrar dene.' ` 'The server offers no Windows build to download right now. Please try again later.' } Say "Sürüm $($release.Version), $(MB $size)" "Version $($release.Version), $(MB $size)" # Room on the disks: the zip in %TEMP%, the unpacked game (about twice the zip) next to the old one. New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null $zip = Join-Path $WorkDir $file $have = 0 if (Test-Path -LiteralPath $zip) { $have = [Math]::Min((Get-Item -LiteralPath $zip).Length, $size) } $need = @{} foreach ($entry in @(@($WorkDir, ($size - $have)), @($env:LOCALAPPDATA, (2.5 * $size)))) { $root = [System.IO.Path]::GetPathRoot($entry[0]) $need[$root] = [long]$need[$root] + [long]$entry[1] + 200MB } foreach ($root in $need.Keys) { $free = Get-FreeSpace $root if ($free -lt $need[$root]) { Fail "$root sürücüsünde yer yetmiyor: $(MB $need[$root]) gerekli, $(MB $free) boş. Biraz yer açıp tekrar dene." ` "Not enough room on drive ${root}: $(MB $need[$root]) needed, $(MB $free) free. Free some space and try again." } } # Earlier downloads of other versions are of no use any more. Get-ChildItem -LiteralPath $WorkDir -Filter '*.zip' | Where-Object { $_.Name -ne $file } | Remove-Item -Force -ErrorAction SilentlyContinue # ---------------- 2. Download ---------------- Step 2 'Oyun indiriliyor (internetine göre birkaç dakika sürebilir)...' 'Downloading the game (a few minutes, depending on the connection)...' Get-GameZip "$Site/client/files/$([Uri]::EscapeDataString($file))" $zip $size # ---------------- 3. Check ---------------- Step 3 'Dosya doğrulanıyor...' 'Verifying the file...' $actual = (Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash if ($actual -ne $hash) { Remove-Item -LiteralPath $zip -Force -ErrorAction SilentlyContinue Fail 'İndirilen dosya bozuk çıktı (SHA-256 tutmuyor) ve silindi. Kurulum durduruldu; tekrar dene.' ` 'The downloaded file is damaged (its SHA-256 does not match) and was deleted. Install stopped; please try again.' } Say 'Dosya sağlam.' 'The file is intact.' 'Green' # ---------------- 4. Close a running game, unpack, replace ---------------- Step 4 'Oyun kuruluyor...' 'Installing the game...' $running = @(Get-GameProcesses) if ($running.Count -gt 0) { Say 'Valtaren açık; kapanması isteniyor...' 'Valtaren is running; asking it to close...' 'Yellow' foreach ($process in $running) { if ($process.ProcessName -eq $GameName -or $process.ProcessName -eq $LegacyName) { [void]$process.CloseMainWindow() } } $clock = [System.Diagnostics.Stopwatch]::StartNew() $asked = $false while (@(Get-GameProcesses).Count -gt 0) { if (-not $asked -and $clock.Elapsed.TotalSeconds -ge 10) { Say 'Lütfen oyunu kapat; kurulum onu bekliyor (en çok 2 dakika).' ` 'Please close the game; the installer is waiting for it (up to 2 minutes).' 'Yellow' $asked = $true } if ($clock.Elapsed.TotalSeconds -ge 120) { Fail 'Oyun kapanmadı. Valtaren''i kapatıp bu satırı yeniden çalıştır.' ` 'The game did not close. Close Valtaren and run the line again.' } Start-Sleep -Seconds 1 } Start-Sleep -Milliseconds 500 } # Unpacked beside the install folder (same disk, so the swap is a rename), then swapped in: an older install # is set aside until the new one is in place and put back if anything goes wrong. $parent = Split-Path -Parent $InstallDir $fresh = "$InstallDir.new" $old = "$InstallDir.old" New-Item -ItemType Directory -Force -Path $parent | Out-Null foreach ($leftover in @($fresh, $old)) { if (Test-Path -LiteralPath $leftover) { Remove-Item -LiteralPath $leftover -Recurse -Force } } Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $fresh) # The zip holds "Valtaren.exe" and "Valtaren_Data" at its root (one from before the rename "Old Knights.exe" and # "Old Knights_Data"); accept them inside one top folder too. $source = $fresh $hasGame = { param($dir) (Test-Path -LiteralPath (Join-Path $dir 'Valtaren.exe')) -or (Test-Path -LiteralPath (Join-Path $dir "$LegacyName.exe")) } if (-not (& $hasGame $source)) { $top = @(Get-ChildItem -LiteralPath $source) if ($top.Count -eq 1 -and $top[0].PSIsContainer) { $source = $top[0].FullName } } if (-not (Test-Path -LiteralPath (Join-Path $source 'Valtaren.exe'))) { $ExeName = "$LegacyName.exe" } if (-not (Test-Path -LiteralPath (Join-Path $source $ExeName))) { Remove-Item -LiteralPath $fresh -Recurse -Force -ErrorAction SilentlyContinue Fail "Zip'in içinde $ExeName bulunamadı. Kurulum durduruldu." "$ExeName was not found in the zip. Install stopped." } # The install being replaced: the Valtaren folder, or (once, after the rename) the Old Knights one. $previous = if (Test-Path -LiteralPath $InstallDir) { $InstallDir } elseif (Test-Path -LiteralPath (Join-Path $LegacyDir "$LegacyName.exe")) { $LegacyDir } else { $null } if ($previous) { try { Move-Folder $previous $old } catch { Remove-Item -LiteralPath $fresh -Recurse -Force -ErrorAction SilentlyContinue Fail 'Eski kurulum değiştirilemedi (bir dosyası açık olabilir). Oyunu ve klasörünü kapatıp tekrar dene.' ` 'The older install could not be replaced (one of its files may be open). Close the game and its folder, then try again.' } } try { Move-Folder $source $InstallDir if ($source -ne $fresh) { Remove-Item -LiteralPath $fresh -Recurse -Force -ErrorAction SilentlyContinue } } catch { if ((Test-Path -LiteralPath $old) -and -not (Test-Path -LiteralPath $previous)) { try { [System.IO.Directory]::Move($old, $previous) } catch { } } Fail 'Yeni sürüm yerine konamadı (bir dosyası açık olabilir); eski kurulum yerinde bırakıldı. Bilgisayarı yeniden başlatıp tekrar dene.' ` 'The new version could not be put in place (one of its files may be open); the older install was left as it was. Restart the computer and try again.' } # An install made by a setup program keeps a working uninstaller (Settings > Apps runs it from the game's # folder): the zip's own "Uninstall Valtaren.exe", else the older one. foreach ($name in @('Uninstall Valtaren.exe', "Uninstall $LegacyName.exe")) { $uninstaller = Join-Path $old $name if ((Test-Path -LiteralPath $uninstaller) -and -not (Test-Path -LiteralPath (Join-Path $InstallDir 'Uninstall Valtaren.exe'))) { try { Copy-Item -LiteralPath $uninstaller -Destination $InstallDir -Force } catch { } } } if (Test-Path -LiteralPath $old) { Remove-Item -LiteralPath $old -Recurse -Force -ErrorAction SilentlyContinue } # Files from the internet carry a "downloaded" mark that makes Windows ask before starting them: cleared. Get-ChildItem -LiteralPath $InstallDir -Recurse -File | Unblock-File -ErrorAction SilentlyContinue Remove-Item -LiteralPath $zip -Force -ErrorAction SilentlyContinue Say 'Kuruldu.' 'Installed.' 'Green' # ---------------- 5. Shortcuts ---------------- Step 5 'Masaüstü ve Başlat menüsü kısayolları ekleniyor...' 'Adding desktop and Start menu shortcuts...' $exe = Join-Path $InstallDir $ExeName $shell = New-Object -ComObject WScript.Shell foreach ($folder in @([Environment]::GetFolderPath('Desktop'), [Environment]::GetFolderPath('Programs'))) { if (-not $folder -or -not (Test-Path -LiteralPath $folder)) { continue } Remove-Item -LiteralPath (Join-Path $folder "$LegacyName.lnk") -Force -ErrorAction SilentlyContinue $link = $shell.CreateShortcut((Join-Path $folder "$GameName.lnk")) $link.TargetPath = $exe $link.WorkingDirectory = $InstallDir $link.IconLocation = "$exe,0" $link.Description = $GameName $link.Save() } # Settings > Apps: an entry of the Old Knights setup moves to the new folder and name. try { $un = @('Uninstall Valtaren.exe', "Uninstall $LegacyName.exe") | ForEach-Object { Join-Path $InstallDir $_ } | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1 if ($un -and ((Test-Path $LegacyKey) -or (Test-Path $UninstKey))) { if (-not (Test-Path $UninstKey)) { New-Item -Path $UninstKey -Force | Out-Null } foreach ($pair in @(@('DisplayName', $GameName), @('Publisher', $GameName), @('DisplayIcon', "`"$exe`",0"), @('InstallLocation', $InstallDir), @('UninstallString', "`"$un`""), @('QuietUninstallString', "`"$un`" /S"), @('URLInfoAbout', 'https://valtaren.com/'), @('HelpLink', 'https://valtaren.com/download/'))) { Set-ItemProperty -Path $UninstKey -Name $pair[0] -Value $pair[1] } foreach ($flag in @('NoModify', 'NoRepair')) { Set-ItemProperty -Path $UninstKey -Name $flag -Value 1 -Type DWord } if (Test-Path $LegacyKey) { Remove-Item -Path $LegacyKey -Recurse -Force } } } catch { } # ---------------- 6. Play ---------------- Step 6 'Oyun açılıyor. İyi eğlenceler!' 'Starting the game. Have fun!' # Started through .NET rather than Start-Process: that one reads [ and ] in a path (a user name may hold them) # as wildcards. $start = New-Object System.Diagnostics.ProcessStartInfo $exe $start.WorkingDirectory = $InstallDir $start.UseShellExecute = $true [void][System.Diagnostics.Process]::Start($start) Write-Host '' Say 'Bundan sonra oyunu masaüstündeki "Valtaren" simgesiyle aç. Yeni sürümleri oyun kendisi indirir.' ` 'From now on, open the game with the "Valtaren" icon on the desktop. The game fetches new versions by itself.' 'Green' Write-Host '' Write-Host ' Bu pencere birazdan kendiliğinden kapanır. / This window closes by itself in a moment.' -ForegroundColor DarkGray Start-Sleep -Seconds 8 } catch { Write-Progress -Activity "Valtaren indiriliyor / Downloading Valtaren" -Completed Write-Host '' Write-Host ' KURULUM DURDU / INSTALL STOPPED' -ForegroundColor Red foreach ($line in ($_.Exception.Message -split "`n")) { Write-Host " $line" -ForegroundColor Red } Write-Host '' Write-Host ' Yardım / Help: https://valtaren.com/download/' -ForegroundColor DarkGray try { [void](Read-Host ' Kapatmak için Enter''a bas / Press Enter to close') } catch { Start-Sleep -Seconds 30 } } }