#requires -Version 5.1 # Build: 2026-07-20-cod-witten-v3 <# .SYNOPSIS Downloads an IW4x patch package and installs it into a legitimate Steam installation of Call of Duty: Modern Warfare 2 (2009). .EXAMPLE .\Install-IW4x.ps1 .EXAMPLE .\Install-IW4x.ps1 -ServerAddress "cod.witten.se:28960" .EXAMPLE .\Install-IW4x.ps1 -GamePath "D:\SteamLibrary\steamapps\common\Call of Duty Modern Warfare 2" .EXAMPLE .\Install-IW4x.ps1 -DisplayMode Fullscreen .EXAMPLE .\Install-IW4x.ps1 -Fov 100 -FpsLimit 300 -Sensitivity 1 #> [CmdletBinding()] param( [Parameter()] [ValidateNotNullOrEmpty()] [string]$PackageUrl = "https://files.dayv.se/mw4.zip", [Parameter()] [string]$GamePath, [Parameter()] [ValidatePattern('^[^:\s]+(?::\d{1,5})?$')] [string]$ServerAddress = "cod.witten.se:28960", [Parameter()] [ValidateSet("Borderless", "Fullscreen", "Windowed")] [string]$DisplayMode = "Borderless", [Parameter()] [ValidateRange(65, 120)] [int]$Fov = 90, [Parameter()] [ValidateRange(0, 1000)] [int]$FpsLimit = 300, [Parameter()] [ValidateRange(0.01, 100)] [double]$Sensitivity = 1, [Parameter()] [switch]$SkipShortcuts ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $ProgressPreference = "Continue" function Write-Step { param([Parameter(Mandatory)][string]$Message) Write-Host "" Write-Host "==> $Message" -ForegroundColor Cyan } function Test-IsAdministrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } function Quote-ProcessArgument { param([Parameter(Mandatory)][string]$Value) return '"' + ($Value -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1') + '"' } function Restart-Elevated { if (-not $PSCommandPath) { throw "Save this script as a .ps1 file and run it again so it can request administrator access." } $arguments = @( "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", (Quote-ProcessArgument $PSCommandPath), "-PackageUrl", (Quote-ProcessArgument $PackageUrl), "-ServerAddress", (Quote-ProcessArgument $ServerAddress), "-DisplayMode", (Quote-ProcessArgument $DisplayMode), "-Fov", $Fov.ToString([Globalization.CultureInfo]::InvariantCulture), "-FpsLimit", $FpsLimit.ToString([Globalization.CultureInfo]::InvariantCulture), "-Sensitivity", $Sensitivity.ToString([Globalization.CultureInfo]::InvariantCulture) ) if ($GamePath) { $arguments += @("-GamePath", (Quote-ProcessArgument $GamePath)) } if ($SkipShortcuts) { $arguments += "-SkipShortcuts" } Start-Process -FilePath "powershell.exe" ` -Verb RunAs ` -ArgumentList ($arguments -join " ") exit } function Get-SteamRootCandidates { $roots = [System.Collections.Generic.List[string]]::new() $registryLocations = @( @{ Path = "HKCU:\Software\Valve\Steam"; Name = "SteamPath" }, @{ Path = "HKLM:\SOFTWARE\WOW6432Node\Valve\Steam"; Name = "InstallPath" }, @{ Path = "HKLM:\SOFTWARE\Valve\Steam"; Name = "InstallPath" } ) foreach ($entry in $registryLocations) { try { $value = (Get-ItemProperty -LiteralPath $entry.Path -Name $entry.Name -ErrorAction Stop).($entry.Name) if ($value) { $roots.Add(([IO.Path]::GetFullPath($value))) } } catch { # Registry location is optional. } } if (${env:ProgramFiles(x86)}) { $roots.Add((Join-Path ${env:ProgramFiles(x86)} "Steam")) } if ($env:ProgramFiles) { $roots.Add((Join-Path $env:ProgramFiles "Steam")) } return $roots | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -Unique } function Get-SteamLibraries { $libraries = [System.Collections.Generic.List[string]]::new() foreach ($steamRoot in Get-SteamRootCandidates) { $libraries.Add($steamRoot) $vdf = Join-Path $steamRoot "steamapps\libraryfolders.vdf" if (-not (Test-Path -LiteralPath $vdf)) { continue } $content = Get-Content -LiteralPath $vdf -Raw $matches = [regex]::Matches($content, '"path"\s+"([^"]+)"') foreach ($match in $matches) { $path = $match.Groups[1].Value -replace '\\\\', '\' if ($path -and (Test-Path -LiteralPath $path)) { $libraries.Add(([IO.Path]::GetFullPath($path))) } } } return $libraries | Select-Object -Unique } function Find-MW2GamePath { foreach ($library in Get-SteamLibraries) { $steamApps = Join-Path $library "steamapps" foreach ($appId in @("10190", "10180")) { $manifest = Join-Path $steamApps "appmanifest_$appId.acf" if (-not (Test-Path -LiteralPath $manifest)) { continue } $manifestText = Get-Content -LiteralPath $manifest -Raw $installDirMatch = [regex]::Match($manifestText, '"installdir"\s+"([^"]+)"') if ($installDirMatch.Success) { $candidate = Join-Path $steamApps ("common\" + $installDirMatch.Groups[1].Value) if (Test-Path -LiteralPath (Join-Path $candidate "iw4mp.exe")) { return [IO.Path]::GetFullPath($candidate) } } } $fallback = Join-Path $steamApps "common\Call of Duty Modern Warfare 2" if (Test-Path -LiteralPath (Join-Path $fallback "iw4mp.exe")) { return [IO.Path]::GetFullPath($fallback) } } return $null } function Download-File { param( [Parameter(Mandatory)][uri]$Uri, [Parameter(Mandatory)][string]$Destination ) $bits = Get-Command Start-BitsTransfer -ErrorAction SilentlyContinue if ($bits) { Start-BitsTransfer -Source $Uri.AbsoluteUri -Destination $Destination } else { Invoke-WebRequest -Uri $Uri -OutFile $Destination -UseBasicParsing } } function New-Shortcut { param( [Parameter(Mandatory)][string]$ShortcutPath, [Parameter(Mandatory)][string]$TargetPath, [Parameter(Mandatory)][string]$WorkingDirectory, [string]$Arguments = "" ) $shell = New-Object -ComObject WScript.Shell $shortcut = $shell.CreateShortcut($ShortcutPath) $shortcut.TargetPath = $TargetPath $shortcut.WorkingDirectory = $WorkingDirectory $shortcut.Arguments = $Arguments $shortcut.IconLocation = "$TargetPath,0" $shortcut.Save() } function ConvertTo-LauncherArguments { param( [Parameter(Mandatory)] [string[]]$GameArguments ) return (($GameArguments | ForEach-Object { '--game-args "{0}"' -f ($_ -replace '"', '\"') }) -join " ") } function Write-DayvClientConfig { param( [Parameter(Mandatory)] [string]$InstallPath, [Parameter(Mandatory)] [ValidateSet("Borderless", "Fullscreen", "Windowed")] [string]$Mode, [Parameter(Mandatory)] [ValidateRange(65, 120)] [int]$ConfiguredFov, [Parameter(Mandatory)] [ValidateRange(0, 1000)] [int]$ConfiguredFpsLimit, [Parameter(Mandatory)] [ValidateRange(0.01, 100)] [double]$ConfiguredSensitivity ) $mainPath = Join-Path $InstallPath "main" $configPath = Join-Path $mainPath "dayv_client.cfg" New-Item -ItemType Directory -Path $mainPath -Force | Out-Null $lines = [System.Collections.Generic.List[string]]::new() $lines.Add("// Managed by the dayv.se IW4x installer") $lines.Add("// Delete this file to stop applying these defaults.") $lines.Add("") $lines.Add(('seta com_maxfps "{0}"' -f $ConfiguredFpsLimit)) $lines.Add(('seta cg_fov "{0}"' -f $ConfiguredFov)) $lines.Add(('seta sensitivity "{0}"' -f $ConfiguredSensitivity.ToString( [Globalization.CultureInfo]::InvariantCulture ))) $lines.Add("") $lines.Add('// Explicitly restore the normal in-game crosshair.') $lines.Add('seta cg_drawCrosshair "1"') $lines.Add('seta cg_crosshairAlpha "1"') $lines.Add("") $lines.Add('bind CTRL "gocrouch"') $lines.Add('bind C "goprone"') $lines.Add("") switch ($Mode) { "Borderless" { $lines.Add('seta r_fullscreen "0"') $lines.Add('seta r_noborder "1"') $lines.Add('seta vid_xpos "0"') $lines.Add('seta vid_ypos "0"') } "Fullscreen" { $lines.Add('seta r_fullscreen "1"') $lines.Add('seta r_noborder "0"') } "Windowed" { $lines.Add('seta r_fullscreen "0"') $lines.Add('seta r_noborder "0"') } } [IO.File]::WriteAllLines( $configPath, $lines.ToArray(), [Text.Encoding]::ASCII ) return $configPath } function Write-DayvAutoConnectConfig { param( [Parameter(Mandatory)] [string]$InstallPath, [Parameter(Mandatory)] [ValidatePattern('^[^:\s]+(?::\d{1,5})?$')] [string]$Address, [ValidateRange(0, 2000)] [int]$DelayFrames = 300 ) $mainPath = Join-Path $InstallPath "main" $configPath = Join-Path $mainPath "dayv_autoconnect.cfg" New-Item -ItemType Directory -Path $mainPath -Force | Out-Null $lines = [System.Collections.Generic.List[string]]::new() $lines.Add("// Managed by the dayv.se IW4x installer") $lines.Add("// Apply client settings, wait for initialization, then connect.") $lines.Add("exec dayv_client.cfg") $lines.Add("") for ($index = 0; $index -lt $DelayFrames; $index++) { $lines.Add("wait") } $lines.Add("") $lines.Add("connect $Address") [IO.File]::WriteAllLines( $configPath, $lines.ToArray(), [Text.Encoding]::ASCII ) return $configPath } if (-not (Test-IsAdministrator)) { Restart-Elevated } [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 Write-Step "Locating the Steam MW2 installation" if ($GamePath) { $GamePath = [IO.Path]::GetFullPath($GamePath) } else { $GamePath = Find-MW2GamePath } if (-not $GamePath -or -not (Test-Path -LiteralPath (Join-Path $GamePath "iw4mp.exe"))) { throw @" A valid Steam installation of Modern Warfare 2 (2009) was not found. Install or verify "Call of Duty: Modern Warfare 2 (2009) - Multiplayer" in Steam, then run this script again. You can also specify: -GamePath "D:\SteamLibrary\steamapps\common\Call of Duty Modern Warfare 2" "@ } Write-Host "Game directory: $GamePath" $tempRoot = Join-Path $env:TEMP ("IW4x-Install-" + [guid]::NewGuid().ToString("N")) $zipPath = Join-Path $tempRoot "mw4.zip" $extractPath = Join-Path $tempRoot "extracted" $hashPath = Join-Path $tempRoot "mw4.zip.sha256" New-Item -ItemType Directory -Path $tempRoot, $extractPath -Force | Out-Null try { Write-Step "Downloading the IW4x package" Download-File -Uri ([uri]$PackageUrl) -Destination $zipPath if ((Get-Item -LiteralPath $zipPath).Length -lt 1MB) { throw "The downloaded file is unexpectedly small. Check $PackageUrl." } Write-Step "Checking the package hash" $hashUrl = "$PackageUrl.sha256" $hashDownloaded = $false try { Invoke-WebRequest -Uri $hashUrl -OutFile $hashPath -UseBasicParsing $hashDownloaded = $true } catch { Write-Warning "The optional checksum could not be downloaded from $hashUrl. Installation will continue over HTTPS." } if ($hashDownloaded) { $hashText = Get-Content -LiteralPath $hashPath -Raw $expectedMatch = [regex]::Match($hashText, '(?i)\b[0-9a-f]{64}\b') if (-not $expectedMatch.Success) { throw "The downloaded checksum file did not contain a valid SHA-256 value." } $expected = $expectedMatch.Value.ToUpperInvariant() $actual = (Get-FileHash -LiteralPath $zipPath -Algorithm SHA256).Hash.ToUpperInvariant() if ($actual -ne $expected) { throw "SHA-256 mismatch. Expected $expected but downloaded $actual. Installation was stopped." } Write-Host "SHA-256 verified: $actual" -ForegroundColor Green } Write-Step "Extracting and validating the package" Expand-Archive -LiteralPath $zipPath -DestinationPath $extractPath -Force $requiredItems = @( (Join-Path $extractPath "iw4x.exe"), (Join-Path $extractPath "iw4x.dll"), (Join-Path $extractPath "iw4x"), (Join-Path $extractPath "iw4x-launcher.exe") ) foreach ($requiredItem in $requiredItems) { if (-not (Test-Path -LiteralPath $requiredItem)) { throw "The package is missing required item: $requiredItem" } } Write-Step "Backing up an existing IW4x installation" $backupRoot = Join-Path $GamePath ("IW4x-backup-" + (Get-Date -Format "yyyyMMdd-HHmmss")) $backupCandidates = @( "iw4x.exe", "iw4x.dll", "iw4x-launcher.exe", "iw4x", "zone\patch" ) $createdBackup = $false foreach ($relativePath in $backupCandidates) { $source = Join-Path $GamePath $relativePath if (-not (Test-Path -LiteralPath $source)) { continue } $destination = Join-Path $backupRoot $relativePath $destinationParent = Split-Path -Parent $destination New-Item -ItemType Directory -Path $destinationParent -Force | Out-Null Move-Item -LiteralPath $source -Destination $destination -Force $createdBackup = $true } if ($createdBackup) { Write-Host "Previous IW4x files moved to: $backupRoot" } else { Remove-Item -LiteralPath $backupRoot -Recurse -Force -ErrorAction SilentlyContinue } Write-Step "Installing IW4x into the MW2 directory" Get-ChildItem -LiteralPath $extractPath -Force | ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination $GamePath -Recurse -Force } Get-ChildItem -LiteralPath $GamePath -Filter "iw4x*.exe" -File -ErrorAction SilentlyContinue | Unblock-File -ErrorAction SilentlyContinue Unblock-File -LiteralPath (Join-Path $GamePath "iw4x.dll") -ErrorAction SilentlyContinue Write-Step "Writing the client defaults" $clientConfigPath = Write-DayvClientConfig ` -InstallPath $GamePath ` -Mode $DisplayMode ` -ConfiguredFov $Fov ` -ConfiguredFpsLimit $FpsLimit ` -ConfiguredSensitivity $Sensitivity $autoConnectConfigPath = $null if ($ServerAddress) { $autoConnectConfigPath = Write-DayvAutoConnectConfig ` -InstallPath $GamePath ` -Address $ServerAddress ` -DelayFrames 300 } if (-not $SkipShortcuts) { Write-Step "Creating desktop shortcuts" $desktop = [Environment]::GetFolderPath("Desktop") $launcher = Join-Path $GamePath "iw4x-launcher.exe" # Apply the client settings without automatically joining a server. $browserShortcutArguments = ConvertTo-LauncherArguments ` -GameArguments @("+exec dayv_client.cfg") New-Shortcut ` -ShortcutPath (Join-Path $desktop "IW4x - Server Browser.lnk") ` -TargetPath $launcher ` -WorkingDirectory $GamePath ` -Arguments $browserShortcutArguments # No settings and no auto-connect. Useful for troubleshooting. New-Shortcut ` -ShortcutPath (Join-Path $desktop "IW4x - Safe Launch.lnk") ` -TargetPath $launcher ` -WorkingDirectory $GamePath if ($ServerAddress) { $autoConnectShortcutArguments = ConvertTo-LauncherArguments ` -GameArguments @("+exec dayv_autoconnect.cfg") New-Shortcut ` -ShortcutPath (Join-Path $desktop "IW4x.lnk") ` -TargetPath $launcher ` -WorkingDirectory $GamePath ` -Arguments $autoConnectShortcutArguments New-Shortcut ` -ShortcutPath (Join-Path $desktop "IW4x - Witten Server.lnk") ` -TargetPath $launcher ` -WorkingDirectory $GamePath ` -Arguments $autoConnectShortcutArguments } else { New-Shortcut ` -ShortcutPath (Join-Path $desktop "IW4x.lnk") ` -TargetPath $launcher ` -WorkingDirectory $GamePath ` -Arguments $browserShortcutArguments } } Write-Step "Installation complete" Write-Host "IW4x directory: $GamePath" -ForegroundColor Green Write-Host "Client config: $clientConfigPath" -ForegroundColor Green Write-Host "Display mode: $DisplayMode" -ForegroundColor Green Write-Host "FPS limit: $FpsLimit" -ForegroundColor Green Write-Host "Field of view: $Fov" -ForegroundColor Green Write-Host "Mouse sensitivity: $Sensitivity" -ForegroundColor Green Write-Host "Crosshair: enabled" -ForegroundColor Green Write-Host "Left Ctrl: crouch" -ForegroundColor Green Write-Host "C: prone" -ForegroundColor Green Write-Host "Auto-connect server: $ServerAddress" -ForegroundColor Green if ($autoConnectConfigPath) { Write-Host "Auto-connect config: $autoConnectConfigPath" -ForegroundColor Green } Write-Host "" Write-Host "Use IW4x.lnk to auto-connect." Write-Host "Use IW4x - Server Browser.lnk to launch without connecting." Write-Host "Use IW4x - Safe Launch.lnk if custom settings cause a startup problem." } finally { Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue }