@echo off
rem ===========================================================================
rem  MEGA MAN MAGNETIC SHOCKWAVE -- Marvel vs. Capcom (CPS-2, mvsc)
rem
rem  DRAG mvsc.zip ONTO THIS FILE
rem       or run:  MVSC-MEGAMAN-MAGNETIC-SHOCKWAVE-v3.cmd "C:\path\mvsc.zip"
rem
rem  It writes a NEW zip beside the input.  The original is never modified.
rem  Nothing to install: Windows PowerShell ships with Windows.
rem
rem  This installer contains patch code and checksums only.  It does not
rem  contain or download Capcom ROM data.  Supply your own clean Euro parent
rem  mvsc.zip (the Fightcade-style set containing mvce.03a and mvce.04a).
rem ===========================================================================

setlocal
set "ROMPATH=%~1"
set "SELFPATH=%~f0"

where powershell.exe >nul 2>&1
if errorlevel 1 (
  echo.
  echo   ERROR: Windows PowerShell was not found.
  echo   It normally ships with Windows. Try:
  echo   %%SystemRoot%%\System32\WindowsPowerShell\v1.0\powershell.exe
  echo.
  pause
  exit /b 1
)

rem The marker is split here so this launcher cannot match its own command.
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$t=[IO.File]::ReadAllText($env:SELFPATH);$m='#=PS'+'BEGIN=#';$i=$t.IndexOf($m);if($i -lt 0){Write-Host '  ERROR: installer marker missing; re-download this file.' -ForegroundColor Red;exit 9};& ([ScriptBlock]::Create($t.Substring($i+$m.Length)))"
set "RC=%errorlevel%"
endlocal & exit /b %RC%

#=PSBEGIN=#
$ErrorActionPreference = 'Stop'

function Pause-IfConsole {
    if ($Host.Name -eq 'ConsoleHost' -and -not $env:MVSCMAG_NOPAUSE) {
        try { Read-Host '  Press Enter to close' | Out-Null } catch {}
    }
}

function Fail([string]$Message, [int]$Code = 1) {
    Write-Host ''
    Write-Host "  ERROR: $Message" -ForegroundColor Red
    Write-Host ''
    Pause-IfConsole
    exit $Code
}

function ConvertFrom-Hex([string]$Hex) {
    if (($Hex.Length % 2) -ne 0) { Fail 'internal hex string has odd length.' 8 }
    $Bytes = New-Object byte[] ($Hex.Length / 2)
    for ($i = 0; $i -lt $Bytes.Length; $i++) {
        $Bytes[$i] = [Convert]::ToByte($Hex.Substring($i * 2, 2), 16)
    }
    return ,$Bytes
}

function Get-Sha256([byte[]]$Bytes) {
    $Hash = [System.Security.Cryptography.SHA256]::Create()
    try {
        return (([BitConverter]::ToString($Hash.ComputeHash($Bytes))).Replace('-', '').ToLowerInvariant())
    } finally {
        $Hash.Dispose()
    }
}

function Test-Bytes([byte[]]$Buffer, [int]$Offset, [byte[]]$Expected) {
    if ($Offset -lt 0 -or $Offset + $Expected.Length -gt $Buffer.Length) { return $false }
    for ($i = 0; $i -lt $Expected.Length; $i++) {
        if ($Buffer[$Offset + $i] -ne $Expected[$i]) { return $false }
    }
    return $true
}

function Read-ZipMembers([string]$Path) {
    $Entries = New-Object 'System.Collections.Generic.Dictionary[string,byte[]]'
    $Metadata = New-Object 'System.Collections.Generic.Dictionary[string,object]'
    $Order = New-Object 'System.Collections.Generic.List[string]'
    try { $Zip = [System.IO.Compression.ZipFile]::OpenRead($Path) }
    catch { throw "not a readable ZIP archive: $Path. Give it mvsc.zip, not a directory or .7z file." }
    try {
        foreach ($Entry in $Zip.Entries) {
            if ($Entries.ContainsKey($Entry.FullName)) {
                throw "archive has a duplicate member named $($Entry.FullName)."
            }
            $Memory = New-Object System.IO.MemoryStream
            $Stream = $Entry.Open()
            try { $Stream.CopyTo($Memory) } finally { $Stream.Dispose() }
            $Entries.Add($Entry.FullName, $Memory.ToArray())
            $Metadata.Add($Entry.FullName, [pscustomobject]@{
                LastWriteTime = $Entry.LastWriteTime
                ExternalAttributes = $Entry.ExternalAttributes
            })
            [void]$Order.Add($Entry.FullName)
            $Memory.Dispose()
        }
    } finally {
        $Zip.Dispose()
    }
    return [pscustomobject]@{ Entries=$Entries; Metadata=$Metadata; Order=$Order }
}

Write-Host ''
Write-Host '  MEGA MAN MAGNETIC SHOCKWAVE -- Marvel vs. Capcom (CPS-2)'
Write-Host '  =========================================================='
Write-Host '  Adds ground-only QCB + any two punches; costs one level.'
Write-Host '  Hyper Mega Man, Rush Drill, and Beat Plane remain intact.'
Write-Host '  A final-team KO shows MAGNETIC / SHOCKWAVE through the stock title renderer.'

$Source = $env:ROMPATH
if ([string]::IsNullOrWhiteSpace($Source)) {
    $Candidates = @(
        (Join-Path ([Environment]::CurrentDirectory) 'mvsc.zip'),
        (Join-Path (Split-Path -Parent $env:SELFPATH) 'mvsc.zip')
    ) | Select-Object -Unique
    $Source = $Candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
}
if ([string]::IsNullOrWhiteSpace($Source)) {
    Write-Host ''
    Write-Host '  Drag your clean mvsc.zip onto this file, or run:'
    Write-Host '    .\MVSC-MEGAMAN-MAGNETIC-SHOCKWAVE-v3.cmd "C:\path\mvsc.zip"'
    Fail 'no ROM archive was supplied and mvsc.zip was not found here.'
}
try { $Source = (Resolve-Path -LiteralPath $Source -ErrorAction Stop).ProviderPath }
catch { Fail "file not found: $Source" }
if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { Fail "not a file: $Source" }

Add-Type -AssemblyName System.IO.Compression -ErrorAction SilentlyContinue
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue

$Vanilla = @(
    @{ Name='mvc.01'; Size=131072; Sha256='565f2c1befc6dbcd727b65c8954d3263b1e8105cd16be3ff7399c0ff513d144e' }
    @{ Name='mvc.02'; Size=131072; Sha256='b460ea0d95c967e5276d0f1f6e0a68d3bcc6008ce7952b970069a211e460ec63' }
    @{ Name='mvc.05a'; Size=524288; Sha256='1dfd50972c8796624051857e16ddc2296932c39fb05ecfcd1bae6180a369285d' }
    @{ Name='mvc.06a'; Size=524288; Sha256='132316e99d02d197cd8b4b9227dccd9215f5f741bcd21d5abf5c3f07fb64354b' }
    @{ Name='mvc.07'; Size=524288; Sha256='8b29a492800ec41c9818f1cf880697b298af69c75d0dd6864ed06e65223930eb' }
    @{ Name='mvc.08'; Size=524288; Sha256='402a848e3860f20cdcf3ec80c640f27eaef36f4e7cd78a330913baedcd5356d8' }
    @{ Name='mvc.09'; Size=524288; Sha256='fd0e680e0e1b14a5d1e913ddaef37076dd80203326e305b1fe1f8070be0cd6b2' }
    @{ Name='mvc.10'; Size=524288; Sha256='e24119140931bc861bf791758af8eb375892260d8ffb633369ebedcbe21f172a' }
    @{ Name='mvc.11m'; Size=4194304; Sha256='5587602fb97953d0cd951392b3bbaef2d317ae943ff2f6608c46cb627b7b16ca' }
    @{ Name='mvc.12m'; Size=4194304; Sha256='971b949ca3ad2471bf5ddc5c87c10286eacba51c30b4cc2261328ec46732b920' }
    @{ Name='mvc.13m'; Size=4194304; Sha256='689b9f2e2f9105f6b6a456e836a289c65caf642d9c7d246ea95c2db256f2ce0d' }
    @{ Name='mvc.14m'; Size=4194304; Sha256='1be6ba60b3cc0e6b647c2a54ca06b948dbc72eb32efdead84bcaa1d2efd69db5' }
    @{ Name='mvc.15m'; Size=4194304; Sha256='37365288e38a8c41fb5d2eb72808038478def8c8aa368d85816b70a89f6a6f55' }
    @{ Name='mvc.16m'; Size=4194304; Sha256='c94231770a7377bf6c7817427f28b87db79f3be03287279e8a19bea34cca85be' }
    @{ Name='mvc.17m'; Size=4194304; Sha256='dc29f2f0bf56d60408e9b9a2fcfae6c684f8be97712c94f146386c9bbb9ab46f' }
    @{ Name='mvc.18m'; Size=4194304; Sha256='4f3ceefa11958fd92f88047f876e37a5a9a226ca9c28e3b9915a03f39a268947' }
    @{ Name='mvc.19m'; Size=4194304; Sha256='d245405b8fa57b315300d915b9b5f96859bc31b9a97f653e370c7f0f28a1fc7f' }
    @{ Name='mvc.20m'; Size=4194304; Sha256='6cb08b23e33cc97bd14c6bf0ed179c99952146328f0dc0f01e8135c2783466bd' }
    @{ Name='mvce.03a'; Size=524288; Sha256='48128b6f98c5171a0e734e99f01a273f526f2da74f08635732068c60c891d757' }
    @{ Name='mvce.04a'; Size=524288; Sha256='71ded7040f279d0ddea1326fc062465159ccd832a327230098e5740d2a747d67' }
    @{ Name='mvsc.key'; Size=20; Sha256='9ba914d01fdb50c562fef4d4a5c7ff4779f92340322272696a46025d33f7155c' }
)
$VanillaLookup = New-Object 'System.Collections.Generic.Dictionary[string,object]'
foreach ($Record in $Vanilla) { $VanillaLookup.Add([string]$Record.Name, $Record) }

try { $Read = Read-ZipMembers $Source }
catch { Fail $_.Exception.Message }
$Entries = $Read.Entries
$Metadata = $Read.Metadata
$Order = $Read.Order

if ($Entries.Count -ne $Vanilla.Count) {
    Fail "wrong archive member count: got $($Entries.Count), expected $($Vanilla.Count).`n         This patch accepts only the clean Euro parent mvsc.zip."
}
for ($i = 0; $i -lt $Vanilla.Count; $i++) {
    if ($Order[$i] -cne [string]$Vanilla[$i].Name) {
        Fail ("archive member order mismatch at index {0}: got '{1}', expected '{2}'.`n         Repacking is allowed only when all 21 member names, order, and bytes remain exact." -f $i,$Order[$i],$Vanilla[$i].Name)
    }
}
foreach ($Name in $Order) {
    if (-not $VanillaLookup.ContainsKey($Name)) {
        Fail "unexpected archive member '$Name'. This is not the exact Euro parent set."
    }
}
foreach ($Record in $Vanilla) {
    $Name = [string]$Record.Name
    if (-not $Entries.ContainsKey($Name)) { Fail "required archive member '$Name' is missing." }
    $Bytes = [byte[]]$Entries[$Name]
    if ($Bytes.Length -ne [int]$Record.Size) {
        Fail "$Name size is $($Bytes.Length), expected $($Record.Size)."
    }
    $Got = Get-Sha256 $Bytes
    if ($Got -ne [string]$Record.Sha256) {
        Fail "$Name SHA-256 mismatch.`n         expected $($Record.Sha256)`n         got      $Got`n         Start from an unmodified Euro parent mvsc.zip."
    }
}

Write-Host ''
Write-Host "  Input : $Source"
Write-Host '  Set   : mvsc Euro 980123 encrypted parent -- all 21 members SHA-256 verified'

$Patches = @(
    @{ File='mvce.03a'; Offset=0x01240C; Before='8556cda558da31f5'; After='552243378da2e4d5'; Purpose='exact-row stock final-team Hyper-title eligibility gate' }
    @{ File='mvce.03a'; Offset=0x01CD1E; Before='b7e6ba813793ed6b'; After='59a8a5a9309dc4af'; Purpose='Magnetic-only attacker meter-adder filter' }
    @{ File='mvce.03a'; Offset=0x0610C4; Before='c36825e7add9'; After='b1ca59e65ca0'; Purpose='ground command hook' }
    @{ File='mvce.03a'; Offset=0x062D04; Before='925c9362484a'; After='9623014d8b83'; Purpose='Mega Man dispatcher hook' }
    @{ File='mvce.04a'; Offset=0x016044; Before='f6d4debdb3a893ff'; After='ddf2452db7578840'; Purpose='Magnetic Shockwave palette-lifetime hook' }
    @{ File='mvce.04a'; Offset=0x0162C0; Before='767552890694'; After='7675b5c15945'; Purpose='Magnetic Shockwave final-object palette restore' }
    @{ File='mvce.04a'; Offset=0x043C90; Before='ee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6cee6c'; After='ee6cee6cee6cee6cec6a2e6be46a406b086be86a206b026b286bee6cee6cee6cee6cee6cee6cee6ce86a046b2a6bec6a0e6b006be46a0c6bee6cee6cee6cee6c'; Purpose='stock class-$C0 MAGNETIC / SHOCKWAVE glyph record (token $5A)' }
    @{ File='mvce.04a'; Offset=0x07788A; Before='0000'; After='5a00'; Purpose='assign native finish-title token $5A to the exact Magnetic row' }
)
foreach ($Patch in $Patches) {
    $Buffer = [byte[]]$Entries[[string]$Patch.File]
    $Before = ConvertFrom-Hex ([string]$Patch.Before)
    $After = ConvertFrom-Hex ([string]$Patch.After)
    if ($Before.Length -ne $After.Length) { Fail 'internal hook length mismatch.' 8 }
    if (-not (Test-Bytes $Buffer ([int]$Patch.Offset) $Before)) {
        Fail ("{0} stock-byte proof failed at +0x{1:X6}; nothing was written." -f $Patch.File,$Patch.Offset)
    }
    [Array]::Copy($After, 0, $Buffer, [int]$Patch.Offset, $After.Length)
    Write-Host ("  patch : {0} +0x{1:X6} ({2} bytes) -- {3}" -f $Patch.File,$Patch.Offset,$After.Length,$Patch.Purpose)
}

$PayloadHex = '00615a03004a0c66b94e010050a1f94e0600ca10754efa450801ee47c801b94e3f00ec122a6700702e1002002d0138c00c662e0c0100740204640070754e7c1d04004d007c1d1c00e600f94e0600f0170070754e2e0c1c00e6000c6700702e10e600f94e06000a2d2e3008000c67400c020000676000006098006e540800b94e01003476b94e0000b0a40070402d1400402d1800402d1c00402d20007c3d3c004e007c1d460080006e428400ee50a1032e4a020006666d5226c004606d5228c014700472b94e0100e635b94e0b00205ef84ed81db84ed81d6e534e003c676e0c2c004e0028666e5408002e429a0300616001f870cf72b94e0100fe48b94e0200ee7100610e01fa412800b84efc1d754eb84ed81d6e534e00f466f94e0100ba6f030000101c00000004000500010070000000ff7f3100f6be009000a0000006000000000000000061f601004a0e666e2054002e300400f94e09004c60754e0000000000000000000000000000e748f2c00061400008202c674e2448266d224e5a09201467cab30a67492c00612600cbb1126769222c00e8602d4ab7790e664b2c00610001df4c034ff84e9e45df4c034f754e6e0c2400240054666e0c220052004c662e4a26000e6b2e0c1c0027003e666e2054002a606e20540008203067680c240024002866680c220052002066284a26001a6b280c1c00270012666820540008200a67680c200052000266754ec891754e00726d204e5a08202a67680c240024001c66680c220052001466284a26000e6be8bd540008667c111c002700015268202c00d260014a046600615600754e2e0ca500ce0144676e20c8022e42cf012e4a77020a677c1d0100cf01fcd06000e8414000ee43d0017c241c0044bc0570d022da20c851faffee4300020170d022da20c851faff7c1da500ce01f94e0000b0a42e0ca500ce0140666e20c8022e4acf010467fcd06000e8414000ee43d0010570d920a942fcffc851f8ffee4300020170d920a942fcffc851f8ff2e42ce012e42cf01f94e0000b0a4754eb94e01001097004a3267e74880c000722e12020041d2ed414cc4c1d0103001722e4ab3000267027240c21267400270000032415341c00667df4c0301754edf4c03010070754e0070402dc801403dcc01754e006172fe08201267e7487e7f48264b2cb94e0200de72df4cfe7e0070754e2e3010006eb0c4021c662e10b0001267000c0a000c67000c0c000a666e4a08000466006096fc0061aeff0070754ee748c2802f201000800c0100aecc0867800c010016cd1c664e224c2c00610afec9b11066ac0c0f0072788c000666df4c0143754edf4c01432e4ae8000667f94e010026cdf94e010030cd4020fcb10f0072780e666d0c000a00c00666f94e01002224280805000400f94e01001424'
$Payload = ConvertFrom-Hex $PayloadHex
if ($Payload.Length -ne 1016) { Fail 'internal payload length mismatch.' 8 }
if ((Get-Sha256 $Payload) -ne '8efc777e3313e0f69b33d6117f0d4711e5cabcc9cdd7c34c1ef2a95d0b41048d') { Fail 'internal payload SHA-256 mismatch.' 8 }
$PlainPayload = New-Object byte[] $Payload.Length
for ($i = 0; $i -lt $Payload.Length; $i += 2) {
    $PlainPayload[$i] = $Payload[$i + 1]
    $PlainPayload[$i + 1] = $Payload[$i]
}
if ((Get-Sha256 $PlainPayload) -ne '1ab0d5821438356ca3bc248244c6d71153fcda3c464e089038b0db5202c278c2') {
    Fail 'embedded payload is not the declared CPS-2 word-swap.' 8
}
$Body = [byte[]]$Entries['mvc.10']
$BodyOffset = 0x071000
$Empty = New-Object byte[] $Payload.Length
for ($i = 0; $i -lt $Empty.Length; $i++) { $Empty[$i] = 0xFF }
if (-not (Test-Bytes $Body $BodyOffset $Empty)) {
    Fail ('mvc.10 payload window +0x{0:X6} is not untouched stock 0xFF; nothing was written.' -f $BodyOffset)
}
[Array]::Copy($Payload, 0, $Body, $BodyOffset, $Payload.Length)
Write-Host ('  patch : mvc.10 +0x{0:X6} ({1} bytes) -- word-swapped payload at CPU $3F1000' -f $BodyOffset,$Payload.Length)

$Patched = New-Object 'System.Collections.Generic.Dictionary[string,string]'
$Patched['mvc.01'] = '565f2c1befc6dbcd727b65c8954d3263b1e8105cd16be3ff7399c0ff513d144e'
$Patched['mvc.02'] = 'b460ea0d95c967e5276d0f1f6e0a68d3bcc6008ce7952b970069a211e460ec63'
$Patched['mvc.05a'] = '1dfd50972c8796624051857e16ddc2296932c39fb05ecfcd1bae6180a369285d'
$Patched['mvc.06a'] = '132316e99d02d197cd8b4b9227dccd9215f5f741bcd21d5abf5c3f07fb64354b'
$Patched['mvc.07'] = '8b29a492800ec41c9818f1cf880697b298af69c75d0dd6864ed06e65223930eb'
$Patched['mvc.08'] = '402a848e3860f20cdcf3ec80c640f27eaef36f4e7cd78a330913baedcd5356d8'
$Patched['mvc.09'] = 'fd0e680e0e1b14a5d1e913ddaef37076dd80203326e305b1fe1f8070be0cd6b2'
$Patched['mvc.10'] = 'b78e1e7327b2657265dbe86999e5f38a795f8df6b29dc3640f9edb7358c69eeb'
$Patched['mvc.11m'] = '5587602fb97953d0cd951392b3bbaef2d317ae943ff2f6608c46cb627b7b16ca'
$Patched['mvc.12m'] = '971b949ca3ad2471bf5ddc5c87c10286eacba51c30b4cc2261328ec46732b920'
$Patched['mvc.13m'] = '689b9f2e2f9105f6b6a456e836a289c65caf642d9c7d246ea95c2db256f2ce0d'
$Patched['mvc.14m'] = '1be6ba60b3cc0e6b647c2a54ca06b948dbc72eb32efdead84bcaa1d2efd69db5'
$Patched['mvc.15m'] = '37365288e38a8c41fb5d2eb72808038478def8c8aa368d85816b70a89f6a6f55'
$Patched['mvc.16m'] = 'c94231770a7377bf6c7817427f28b87db79f3be03287279e8a19bea34cca85be'
$Patched['mvc.17m'] = 'dc29f2f0bf56d60408e9b9a2fcfae6c684f8be97712c94f146386c9bbb9ab46f'
$Patched['mvc.18m'] = '4f3ceefa11958fd92f88047f876e37a5a9a226ca9c28e3b9915a03f39a268947'
$Patched['mvc.19m'] = 'd245405b8fa57b315300d915b9b5f96859bc31b9a97f653e370c7f0f28a1fc7f'
$Patched['mvc.20m'] = '6cb08b23e33cc97bd14c6bf0ed179c99952146328f0dc0f01e8135c2783466bd'
$Patched['mvce.03a'] = 'd1afc005b653c95535ea234f740d993053946f8d5f8f959a243985a7358ed7db'
$Patched['mvce.04a'] = 'fc112a3a35b4f11ebc75a446c0ce0bece2dd4a0b196cef0ff89d4b3f05fbcbf0'
$Patched['mvsc.key'] = '9ba914d01fdb50c562fef4d4a5c7ff4779f92340322272696a46025d33f7155c'
foreach ($Record in $Vanilla) {
    $Name = [string]$Record.Name
    $Got = Get-Sha256 ([byte[]]$Entries[$Name])
    if ($Got -ne $Patched[$Name]) {
        Fail "internal patched-member proof failed for $Name; nothing was written." 8
    }
}

$Destination = Join-Path (Split-Path -Parent $Source) (([IO.Path]::GetFileNameWithoutExtension($Source)) + '_megaman_magnetic_shockwave_v3.zip')
$Destination = [IO.Path]::GetFullPath($Destination)
if ($Destination -eq [IO.Path]::GetFullPath($Source)) { Fail 'refusing to overwrite the input archive.' }
if (Test-Path -LiteralPath $Destination) {
    Fail "output already exists: $Destination`n         Delete or move that old output yourself, then run this installer again."
}
$Temporary = "$Destination.part"
if (Test-Path -LiteralPath $Temporary) {
    Fail "stale temporary output exists: $Temporary`n         Delete it yourself after confirming it is not needed."
}

$File = [IO.File]::Open($Temporary, [IO.FileMode]::CreateNew, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None)
try {
    $Output = New-Object System.IO.Compression.ZipArchive($File, [IO.Compression.ZipArchiveMode]::Create, $true)
    try {
        foreach ($Name in $Order) {
            $NewEntry = $Output.CreateEntry($Name, [IO.Compression.CompressionLevel]::Optimal)
            try {
                $NewEntry.LastWriteTime = $Metadata[$Name].LastWriteTime
                $NewEntry.ExternalAttributes = $Metadata[$Name].ExternalAttributes
            } catch {
                # Member data/order are authoritative; metadata preservation is best effort.
            }
            $Stream = $NewEntry.Open()
            try {
                $Bytes = [byte[]]$Entries[$Name]
                $Stream.Write($Bytes, 0, $Bytes.Length)
            } finally { $Stream.Dispose() }
        }
    } finally { $Output.Dispose() }
} catch {
    $WriteError = $_.Exception.Message
    $File.Dispose()
    if (Test-Path -LiteralPath $Temporary) { Remove-Item -LiteralPath $Temporary -Force }
    Fail "could not write output ZIP: $WriteError" 8
} finally {
    $File.Dispose()
}

# Re-open the actual bytes on disk. Verify member order, names, sizes and every
# patched SHA-256 before the .part file is promoted to the final output name.
try {
    $Written = Read-ZipMembers $Temporary
    if ($Written.Order.Count -ne $Order.Count) { throw 'output member count changed during ZIP write.' }
    for ($i = 0; $i -lt $Order.Count; $i++) {
        if ($Written.Order[$i] -cne $Order[$i]) { throw "output member order changed at index $i." }
    }
    foreach ($Record in $Vanilla) {
        $Name = [string]$Record.Name
        if (-not $Written.Entries.ContainsKey($Name)) { throw "output is missing $Name." }
        $Bytes = [byte[]]$Written.Entries[$Name]
        if ($Bytes.Length -ne [int]$Record.Size) { throw "output size check failed for $Name." }
        $Got = Get-Sha256 $Bytes
        if ($Got -ne $Patched[$Name]) { throw "output SHA-256 check failed for $Name." }
    }
} catch {
    $VerificationError = $_.Exception.Message
    if (Test-Path -LiteralPath $Temporary) { Remove-Item -LiteralPath $Temporary -Force }
    Fail "output verification failed: $VerificationError" 8
}

Move-Item -LiteralPath $Temporary -Destination $Destination

Write-Host ''
Write-Host '  DONE. Wrote and re-verified:' -ForegroundColor Green
Write-Host "    $Destination"
Write-Host ''
Write-Host '  Input command: ground QCB + any two punch buttons (one super level).'
Write-Host '  The original ROM was not changed. To undo the patch, delete the new ZIP.'
Write-Host ''
Write-Host '  NETPLAY / CRC NOTE' -ForegroundColor Yellow
Write-Host '  This is an unofficial modified ROM. An emulator or frontend may show CRC'
Write-Host '  warnings or refuse the set. For any peer-to-peer session, both players'
Write-Host '  need the identical patched ROM and an emulator/session that accepts it;'
Write-Host '  mismatched program bytes can desync. This is not official Fightcade support.'
Write-Host ''
Write-Host '  Emulators normally identify this set by the filename mvsc.zip. Keep your'
Write-Host '  clean original safe; copy/rename the new ZIP only in the ROM folder where'
Write-Host '  you deliberately want to test the mod.'
Write-Host ''
Write-Host '  Embedded build:'
Write-Host '    payload bytes       1016'
Write-Host '    plain payload SHA   1ab0d5821438356ca3bc248244c6d71153fcda3c464e089038b0db5202c278c2'
Write-Host '    disk payload SHA    8efc777e3313e0f69b33d6117f0d4711e5cabcc9cdd7c34c1ef2a95d0b41048d'
Write-Host '    native title token  $5A'
Write-Host '    title record SHA    f1088827140a3b3fc63e5da69b2ff63d5ea65ac3a5e57f7ce148397498808c1f'
Write-Host '    stock GFX members   unchanged'
Write-Host ''
Pause-IfConsole
exit 0
