param( [Parameter(Mandatory=$false)] [ValidateSet("production", "ptr", "local")] [string]$Env = "local" ) # Stop on errors $ErrorActionPreference = "Stop" # --- Resolve project root (folder where script is located) $ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path . (Join-Path $ROOT 'pipeline-layout.ps1') # --- Load .env if WOW_HOME not already set if (-not $env:WOW_HOME) { $envFile = Join-Path $ROOT ".env" if (Test-Path $envFile) { Write-Host "Loading .env file..." Get-Content $envFile | ForEach-Object { if ($_ -match "^\s*([^#][^=]+)=(.+)$") { $name = $matches[1].Trim() $value = $matches[2].Trim() [System.Environment]::SetEnvironmentVariable($name, $value) Set-Item -Path "Env:$name" -Value $value } } } } # --- Validate WOW_HOME if (-not $env:WOW_HOME) { Write-Error "WOW_HOME is not set (env or .env)" exit 1 } $WOW_HOME = $env:WOW_HOME Write-Host "WOW_HOME = $WOW_HOME" function Test-ExistingFileWriteAccess { param([Parameter(Mandatory=$true)][string]$Path) try { $stream = [System.IO.File]::Open( $Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite ) $stream.Close() return $true } catch [System.UnauthorizedAccessException] { return $false } catch { # Sharing violations are handled later by Stop-WowRuntime. They do not # indicate that the whole script needs an elevated token. return $true } } $installedWarcraftXL = Join-Path $WOW_HOME "WarcraftXL.dll" $isAdministrator = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator ) if ((Test-Path -LiteralPath $installedWarcraftXL -PathType Leaf) -and -not (Test-ExistingFileWriteAccess -Path $installedWarcraftXL) -and -not $isAdministrator) { Write-Host "WarcraftXL.dll requires administrator access. Requesting elevation..." $elevatedProcess = Start-Process -FilePath "powershell.exe" -Verb RunAs -PassThru -Wait ` -WorkingDirectory $ROOT ` -ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env exit $elevatedProcess.ExitCode } # --- Paths $SRC_DIR = Join-Path $ROOT "src" $DIST_DIR = Join-Path $ROOT "dist" $TOOL = Join-Path $ROOT "tool\target\release\tool.exe" $RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat" $WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1" Assert-SafePackagePath ` -PackagePath $DIST_DIR ` -RepositoryPath $ROOT ` -ClientPath $WOW_HOME function Stop-WowRuntime { param([Parameter(Mandatory=$true)][string]$ClientPath) $clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\') + '\' $runtimeProcesses = @(Get-Process -Name "Wow" -ErrorAction SilentlyContinue | Where-Object { try { $_.Path -and $_.Path.StartsWith($clientRoot, [System.StringComparison]::OrdinalIgnoreCase) } catch { $false } }) if (-not $runtimeProcesses.Count) { return } Write-Host "Stopping the running WoW client..." $runtimeProcesses | Stop-Process -Force try { $runtimeProcesses | Wait-Process -Timeout 10 -ErrorAction Stop } catch { $stillRunning = @($runtimeProcesses | Where-Object { -not $_.HasExited }) if ($stillRunning.Count) { $processNames = ($stillRunning | ForEach-Object { "$($_.ProcessName) (PID $($_.Id))" }) -join ", " throw "Could not stop processes that lock WarcraftXL files: $processNames" } } } # --- Ensure tool exists if (!(Test-Path $TOOL)) { Write-Host "Building Rust tool..." Push-Location (Join-Path $ROOT "tool") cargo build --release Pop-Location } # --- Build MPQ archives into a clean dist/ $previousPackageFiles = @( Get-RelativeFilePaths -Root $DIST_DIR ) Reset-MoonWellPackageStaging ` -PackagePath $DIST_DIR ` -RepositoryPath $ROOT ` -ClientPath $WOW_HOME Write-Host "Building MPQ archives from src/ -> dist/..." & $TOOL $SRC_DIR $DIST_DIR if ($LASTEXITCODE -ne 0) { Write-Error "MPQ build failed!" exit 1 } # --- Verify result if (!(Test-Path (Join-Path $DIST_DIR "Data"))) { Write-Error "dist\Data not found after build!" exit 1 } # --- Build/package/deploy WarcraftXL before the dist sync. The packaged files # are uploaded by upload_to_s3.py and consumed by the launcher manifest. Stop-WowRuntime -ClientPath $WOW_HOME Write-Host "Building and packaging WarcraftXL..." & $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME ` -PackagePath $DIST_DIR -Deploy if ($LASTEXITCODE -ne 0) { Write-Error "WarcraftXL build/deploy failed!" exit 1 } # --- Sync dist/ -> WOW_HOME Write-Host "Syncing dist/ -> WOW_HOME..." $python = Get-Command python -ErrorAction SilentlyContinue if (-not $python) { Write-Error "Python is required for package validation." exit 1 } & $python.Source (Join-Path $ROOT 'validate_package.py') --dir $DIST_DIR if ($LASTEXITCODE -ne 0) { Write-Error "Package validation failed!" exit 1 } $currentPackageFiles = @(Get-RelativeFilePaths -Root $DIST_DIR) Remove-StalePackageFilesFromClient ` -ClientPath $WOW_HOME ` -PreviousFiles $previousPackageFiles ` -CurrentFiles $currentPackageFiles Remove-MoonWellRuntimeGarbage -Destination $WOW_HOME robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL # robocopy exit codes 0-7 are success/warnings; 8+ are errors if ($LASTEXITCODE -ge 8) { Write-Error "robocopy failed with exit code $LASTEXITCODE" exit 1 } Write-Host "MPQ archives deployed to $WOW_HOME" # --- Write realmlist.wtf based on selected environment $realmlist = switch ($Env) { "production" { $env:PRODUCTION_REALMLIST } "ptr" { $env:PTR_REALMLIST } "local" { $env:LOCAL_REALMLIST } } if ($realmlist) { $realmlistPath = Join-Path $WOW_HOME "Data\ruRU\realmlist.wtf" Set-Content -Path $realmlistPath -Value "set realmlist $realmlist" -Encoding ASCII Write-Host "Realmlist ($Env): $realmlist" } else { Write-Warning "REALMLIST for '$Env' is not set in .env -- skipping realmlist.wtf" } # --- Run WoW reload script Write-Host "Launching WoW..." cmd /c $RELOAD_SCRIPT