Files

228 lines
14 KiB
PowerShell

<#
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