<# deploy-mw4.ps1 - assemble a runnable copy of BattleTech:FireStorm at c:\VWE\firestorm\MW4 RULES-BASED deployment, sourced entirely from the working directory (c:\VWE\firestorm). No external/image paths are read. Sources: Runtime = c:\VWE\firestorm\Gameleap\mw4 (dev superset: runtime DLLs, resources, config) BinDir = c:\VWE\firestorm\Gameleap\code\rel.bin (our freshly built binaries) The dev runtime tree is bloated (~6 GB) with source content and dev-only data. A clean runnable deployment is ~900 MB. These rules select the runtime subset: KEEP (whole): Resource\, hsh\, Assets\, Stats\ (compiled .mw4 packages etc.) KEEP (subset): Content\shellscripts\files\ ONLY (runtime *_music.wav). The loose .script/.h sources are redundant -- they're packed into props.mw4 and the engine loads packed resources before loose disk files (s_DiskFirst defaults to false). KEEP (root): *.dll *.ini *.options *.txt *.rtf *.ico *.m4c *.sem *.016 *.256 plus clokspl.exe (redistributable loader). (.lnk/.h dropped; no runtime use.) DROP: Content\* (except shellscripts\files), Movies\, Notes\, fonts\, tool exes, Thumbs.db, dead .lnk shortcuts, source headers, test/variant .ini, editor EULA. OPTIMIZE: hsh\mechs\*.bmp reduced from 24-bit to 256-color (8bpp) in the deployed copy (matches production; ~3x smaller, no runtime quality loss; source untouched). OVERLAY: fresh build binaries from rel.bin (MW4.exe + the DLLs/EXEs we compile) Re-runnable: overlay always runs last so deployed binaries are the fresh build. RESOURCE PACKING: by default this first repacks resource\*.mw4 from the Content\ source tree (via build-resources.ps1) so the deployed packages reflect current source -- including loose edits like Content\ShellScripts\*.script that would otherwise be shadowed by stale .mw4 files (the runtime loads packed resources before loose disk files; s_DiskFirst defaults to false). Pass -SkipResourceBuild to deploy the existing prebuilt packages as-is. #> param( [string]$Runtime = "c:\VWE\firestorm\Gameleap\mw4", [string]$BinDir = "c:\VWE\firestorm\Gameleap\code\rel.bin", [string]$Dest = "c:\VWE\firestorm\MW4", [switch]$SkipResourceBuild, [int] $ResourceBuildTimeoutSec = 1800 ) $ErrorActionPreference = "Stop" Add-Type -AssemblyName PresentationCore # WPF imaging, for the 256-color BMP optimization function Robo($src,$dst,$extra) { # /XF Thumbs.db: never copy Windows thumbnail caches that litter the dev tree. $a = @("`"$src`"","`"$dst`"","/E","/R:1","/W:1","/NFL","/NDL","/NP","/NJH","/XF","Thumbs.db") + $extra $p = Start-Process robocopy -ArgumentList $a -Wait -PassThru -NoNewWindow if ($p.ExitCode -ge 8) { throw "robocopy '$src' failed (code $($p.ExitCode))" } } # Reduce a 24-bit BMP to 256-color (8bpp indexed) in place, matching production's hsh\mechs art. # Returns bytes saved, or -1 if skipped (already 8bpp) / failed. ~3x size reduction, no visible # loss on these small mech thumbnails. Uses WPF FormatConvertedBitmap with an optimal palette. function Convert-BmpTo8bit($path) { try { $fs = [System.IO.File]::OpenRead($path) $dec = New-Object System.Windows.Media.Imaging.BmpBitmapDecoder($fs, [System.Windows.Media.Imaging.BitmapCreateOptions]::PreservePixelFormat, [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad) $src = $dec.Frames[0]; $fs.Close() # OnLoad caches in memory; file now free to overwrite if ($src.Format -eq [System.Windows.Media.PixelFormats]::Indexed8) { return -1 } # already optimized $before = (Get-Item $path).Length $pal = New-Object System.Windows.Media.Imaging.BitmapPalette($src,256) $conv = New-Object System.Windows.Media.Imaging.FormatConvertedBitmap $conv.BeginInit(); $conv.Source=$src $conv.DestinationFormat=[System.Windows.Media.PixelFormats]::Indexed8 $conv.DestinationPalette=$pal; $conv.EndInit() $enc = New-Object System.Windows.Media.Imaging.BmpBitmapEncoder $enc.Frames.Add([System.Windows.Media.Imaging.BitmapFrame]::Create($conv)) $os = [System.IO.File]::Create($path); $enc.Save($os); $os.Close() return ($before - (Get-Item $path).Length) } catch { Write-Warning " BMP convert failed: $path ($($_.Exception.Message))"; return -1 } } # Return the canonical set of game resource packages (lowercased "resource\...\X.mw4" keys) by # parsing the build manifest tree (Content\MechWarrior4.build -> child .build files -> each one's # [resource\X.mw4] page). The shared dev resource\ also holds EDITOR-only extras (extra maps, # _backup sets); this lets the game deploy ship only the packages the game actually references. function Get-GamePackages($contentDir) { $set = New-Object 'System.Collections.Generic.HashSet[string]' $master = Join-Path $contentDir "MechWarrior4.build" if (-not (Test-Path $master)) { return $set } $refs = [regex]::Matches((Get-Content $master -Raw), '([^\s={}]+\.build)') | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique foreach ($ref in $refs) { $bf = Join-Path $contentDir $ref if (-not (Test-Path $bf)) { continue } foreach ($ln in (Get-Content $bf)) { if ($ln -match '\[\s*(resource\\[^\]]+\.mw4)\s*\]') { # -match is case-insensitive [void]$set.Add(($Matches[1] -replace '/','\').ToLower()); break } } } return $set } Write-Host "== MW4 rules-based deployment ==" -ForegroundColor Cyan Write-Host " runtime: $Runtime" Write-Host " build : $BinDir" Write-Host " dest : $Dest`n" if (-not (Test-Path $Runtime)) { throw "Runtime source not found: $Runtime" } if (-not (Test-Path $BinDir)) { throw "Build output not found: $BinDir (build MW4Application first)" } if (Test-Path $Dest) { Write-Host " (dest exists, refreshing in place)`n" } New-Item -ItemType Directory -Force -Path $Dest | Out-Null # 0) Pack resources from source BEFORE copying, so the deployed resource\*.mw4 are current. if ($SkipResourceBuild) { Write-Host "[0/5] Resource packing SKIPPED (-SkipResourceBuild); deploying prebuilt packages.`n" -ForegroundColor DarkYellow } else { Write-Host "[0/5] Packing resources from source (resource\*.mw4) ..." -ForegroundColor Yellow & (Join-Path $PSScriptRoot "build-resources.ps1") -Runtime $Runtime -BinDir $BinDir -TimeoutSec $ResourceBuildTimeoutSec Write-Host "" } # 1) Runtime dirs. Resource is copied SELECTIVELY -- only the canonical game packages from the # build manifest (+ each one's .dep, + non-package metadata .nfo/.tga/.txt). The shared dev # resource\ also holds editor-only extras (extra maps, _backup sets) that must NOT ship. # hsh/Assets/Stats copy whole (Thumbs.db excluded by Robo). Write-Host "[1/5] Resource (game packages only), hsh, Assets, Stats ..." -ForegroundColor Yellow $gamePkgs = Get-GamePackages (Join-Path $Runtime "Content") if ($gamePkgs.Count -eq 0) { throw "Manifest parse returned 0 game packages -- check Content\MechWarrior4.build" } $srcRes = Join-Path $Runtime "resource"; $dstRes = Join-Path $Dest "Resource" $rc = 0; $rs = 0 if (Test-Path $srcRes) { Get-ChildItem $srcRes -Recurse -File | ForEach-Object { $rel = $_.FullName.Substring($srcRes.Length).TrimStart('\') if ($_.Name -ieq "Thumbs.db") { $rs++; return } if ($rel -imatch '^Pilots\\') { return } # player profiles -> copied verbatim below (not package-filtered) $ext = $_.Extension.ToLower() if ($ext -eq ".mw4" -or $ext -eq ".dep") { $key = ("resource\" + ($rel -replace '\.dep$','.mw4')).ToLower() if (-not $gamePkgs.Contains($key)) { $rs++; return } # editor-only package -> skip } $dst = Join-Path $dstRes $rel $dd = Split-Path $dst -Parent if (-not (Test-Path $dd)) { New-Item -ItemType Directory -Force $dd | Out-Null } Copy-Item $_.FullName $dst -Force; $rc++ } } Write-Host (" Resource: {0} files copied ({1} game packages + metadata), {2} skipped (editor-only/cruft)" -f $rc,$gamePkgs.Count,$rs) # Pilot profiles (Resource\Pilots\*) are player data, NOT content packages -- copy verbatim # (each pilot's options.mw4 has a .mw4 extension but isn't in the manifest, so the package # filter above would drop it). Robo preserves the per-pilot Games\ subtree too. $srcPilots = Join-Path $srcRes "Pilots" if (Test-Path $srcPilots) { Robo $srcPilots (Join-Path $dstRes "Pilots") @() Write-Host " Resource\Pilots copied verbatim (player profiles)" } foreach ($d in "hsh","Assets","Stats") { if (Test-Path (Join-Path $Runtime $d)) { Robo (Join-Path $Runtime $d) (Join-Path $Dest $d) @() } } # 2) Content subset: ONLY shellscripts\files (runtime audio: *_music.wav). The loose # .script/.h/.hpp sources are redundant -- they're packed into props.mw4 and the engine reads # packed resources before loose disk files (s_DiskFirst=false), so they'd never be loaded. # Production ships only files\ as well. Write-Host "[2/5] Content\shellscripts\files (runtime audio only) ..." -ForegroundColor Yellow $ssf = Join-Path $Runtime "Content\shellscripts\files" if (Test-Path $ssf) { Robo $ssf (Join-Path $Dest "Content\shellscripts\files") @() } # 3) Root runtime files by extension whitelist (.lnk/.h excluded -- shortcuts & source headers # have no runtime use). Plus a name-based skip list of dev-only files absent from a production # install: editor EULA, alternate icon, empty dev logs, and test/variant config .ini files. Write-Host "[3/5] Root runtime files (dlls, config, localization) ..." -ForegroundColor Yellow $keepExt = ".dll",".ini",".options",".txt",".rtf",".ico",".m4c",".sem",".016",".256" # Dev-only DLLs that must NOT be deployed: dbghelp.dll/imagehlp.dll are SYSTEM dlls # whose 2003 copies shadow Windows' and break DirectDraw init (DDERR_NODIRECTDRAWHW); # the rest are tool/optional dlls the runtime game (per known-good) does not use. # ddraw.dll is DDrawCompat -- the editor installs it into the shared data tree for its windowed # viewport, but the fullscreen game must keep native DirectDraw, so never ship it here. $skipDll = "dbghelp.dll","imagehlp.dll","ijl10.dll","ctcl.dll","Language.oeg.dll","Resources.dll","ddraw.dll" $skipName = "edeula.rtf","mech4.ico","debuglog.txt","erfviewer.txt","servercyclex.txt","tctd.ini", "options-ext.ini","options-cam-var.ini","options-game-var.ini","options-mr-var.ini", "ctcl-camera-test.ini","ctcl-game-test.ini","ctcl-mr-test.ini" Get-ChildItem $Runtime -File | Where-Object { ($keepExt -contains $_.Extension.ToLower()) -and ($skipDll -notcontains $_.Name) -and ($skipName -notcontains $_.Name) } | ForEach-Object { Copy-Item $_.FullName (Join-Path $Dest $_.Name) -Force } # clokspl.exe is a runtime loader (the only non-built exe the clean deploy keeps) if (Test-Path (Join-Path $Runtime "clokspl.exe")) { Copy-Item (Join-Path $Runtime "clokspl.exe") $Dest -Force } # 4) Overlay freshly built binaries Write-Host "[4/5] Overlaying fresh build artifacts from rel.bin ..." -ForegroundColor Yellow $artifacts = "MW4.exe","MW4pro.exe","Launcher.exe","autoconfig.exe","mw4print.exe","MissionLang.dll","ScriptStrings.dll","ctcls.dll" foreach ($a in $artifacts) { $src = Join-Path $BinDir $a if (Test-Path $src) { Copy-Item $src (Join-Path $Dest $a) -Force; Write-Host (" + {0,-18} {1:N0} bytes" -f $a,(Get-Item $src).Length) } else { Write-Warning " build artifact missing: $a" } } # 4b) Apply the DirectDraw 16-bit compatibility shim (REQUIRED on Win10/11). # MW4 runs at bitdepth=16; without DWM8And16BitMitigation it fails graphics init # with DDERR_NODIRECTDRAWHW. Per-user (HKCU) needs no admin. The matching all-users # (HKLM) entry is in build-env\mw4-compat-hklm.reg (import elevated for full parity). Write-Host "[4b ] Applying DirectDraw compat shim (HKCU) for $Dest\MW4.exe ..." -ForegroundColor Yellow $layers = "HKCU:\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" if (-not (Test-Path $layers)) { New-Item -Path $layers -Force | Out-Null } Set-ItemProperty -Path $layers -Name "$Dest\MW4.exe" -Value "DWM8And16BitMitigation HIGHDPIAWARE" -Type String Write-Host " set: $Dest\MW4.exe = DWM8And16BitMitigation HIGHDPIAWARE" # 4c) Optimize hsh\mechs preview BMPs to 256-color (8bpp). The dev tree stores them as 24-bit # (~600 KB each, ~23 MB total); production ships 256-color (~200 KB). Converts the DEPLOYED # copies only -- source 24-bit originals are untouched. Idempotent (skips already-8bpp). Write-Host "[4c ] Optimizing hsh\mechs BMPs to 256-color ..." -ForegroundColor Yellow $mechDir = Join-Path $Dest "hsh\mechs" $saved = 0L; $cnt = 0 if (Test-Path $mechDir) { Get-ChildItem $mechDir -Filter *.bmp | ForEach-Object { $d = Convert-BmpTo8bit $_.FullName if ($d -ge 0) { $saved += $d; $cnt++ } } Write-Host (" converted {0} BMP(s), saved {1:N1} MB" -f $cnt,($saved/1MB)) } else { Write-Host " (no hsh\mechs dir)" } # 5) Report Write-Host "[5/5] Deployment summary:" -ForegroundColor Yellow $exe = Join-Path $Dest "MW4.exe" if (-not (Test-Path $exe)) { throw "MW4.exe missing from $Dest" } $files = Get-ChildItem $Dest -Recurse -File $mb = ($files | Measure-Object Length -Sum).Sum/1MB Write-Host (" {0} : {1:N0} files, {2:N0} MB" -f $Dest, $files.Count, $mb) -ForegroundColor Green Write-Host (" MW4.exe {0:N0} bytes ({1})" -f (Get-Item $exe).Length,(Get-Item $exe).LastWriteTime) -ForegroundColor Green " per top-level:" Get-ChildItem $Dest -Directory | ForEach-Object { $m=(Get-ChildItem $_.FullName -Recurse -File -EA SilentlyContinue|Measure-Object Length -Sum).Sum/1MB " {0,8:N1} MB {1}" -f $m,$_.Name } Write-Host "`nDone. Runnable game deployed to $Dest" -ForegroundColor Cyan