@echo off
rem ===========================================================================
rem  MEGA MAN MAGNETIC SHOCKWAVE v4 -- Marvel vs. Capcom (CPS-2, mvsc)
rem
rem  DRAG mvsc.zip ONTO THIS FILE
rem       or run:  MVSC-MEGAMAN-MAGNETIC-SHOCKWAVE-v4.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:MVSCMAGV4_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 v4 -- Marvel vs. Capcom (CPS-2)'
Write-Host '  =========================================================='
Write-Host '  Adds ground-only QCB + any two punches; costs one level.'
Write-Host '  V4 uses the native two-punch parser and accepts a released final BACK.'
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-v4.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 = '00615a03004a0c66b94e010050a1f94e0600ca10754efa450801ee47c801b94e3f00ec122a6700702e1002002d0138c00c662e0c0100740204640070754e7c1d04004d007c1d1c00e600f94e0600f0170070754e2e0c1c00e6000c6700702e10e600f94e06000a2d2e3008000c67400c020000676000006098006e540800b94e01003476b94e0000b0a40070402d1400402d1800402d1c00402d20007c3d3c004e007c1d460080006e428400ee50a1032e4a020006666d5226c004606d5228c014700472b94e0100e635b94e0b00205ef84ed81db84ed81d6e534e003c676e0c2c004e0028666e5408002e429a0300616001f870cf72b94e0100fe48b94e0200ee7100610e01fa412800b84efc1d754eb84ed81d6e534e00f466f94e0100ba6f030000801c00000004000500010070000000ff7f3100f6be009000a0000006000000000000000061f601004a0e666e2054002e300400f94e09004c60754e0000000000000000000000000000e748f2c00061400008202c674e2448266d224e5a09201467cab30a67492c00612600cbb1126769222c00e8602d4ab7790e664b2c00610001df4c034ff84e9e45df4c034f754e6e0c2400240054666e0c220052004c662e4a26000e6b2e0c1c0027003e666e2054002a606e20540008203067680c240024002866680c220052002066284a26001a6b280c1c00270012666820540008200a67680c200052000266754ec891754e00726d204e5a08202a67680c240024001c66680c220052001466284a26000e6be8bd540008667c111c002700015268202c00d260014a046600615600754e2e0ca500ce0144676e20c8022e42cf012e4a77020a677c1d0100cf01fcd06000e8414000ee43d0017c241c0044bc0570d022da20c851faffee4300020170d022da20c851faff7c1da500ce01f94e0000b0a42e0ca500ce0140666e20c8022e4acf010467fcd06000e8414000ee43d0010570d920a942fcffc851f8ffee4300020170d920a942fcffc851f8ff2e42ce012e42cf01f94e0000b0a4754eb94e01001097754e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070402dc801403dcc01754e006172fe08201267e7487e7f48264b2cb94e0200de72df4cfe7e0070754e2e3010006eb0c4021c662e10b0000055106300510c6700590c62714e6e4a08000466006096fc0061aeff0070754ee748c2802f201000800c0100aecc0867800c010016cd1c664e224c2c00610afec9b11066ac0c0f0072788c000666df4c0143754edf4c01432e4ae8000667f94e010026cdf94e010030cd4020fcb10f0072780e666d0c000a00c00666f94e01002224280805000400f94e01001424'
$Payload = ConvertFrom-Hex $PayloadHex
if ($Payload.Length -ne 1016) { Fail 'internal payload length mismatch.' 8 }
if ((Get-Sha256 $Payload) -ne 'e191b13801cfec8664afaeb5081b266622a773216eef8f3d418b3004ffc22bb8') { 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 '13cd13867172c1ccc17e4b12e91224520f4192d8f9bffdc315474557062d4bed') {
    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'] = '812c2fa196eaff12b6835d2a090a6dfb99018b6825b87505fbd784692302e1f6'
$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) 'mvsc_magnetic_shockwave_v4.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."
}
$ReceiptPath = "$Destination.receipt.json"
$ReceiptTemporary = "$ReceiptPath.part"
if (Test-Path -LiteralPath $ReceiptPath) {
    Fail "receipt already exists: $ReceiptPath`n         Delete or move that old receipt yourself, then run this installer again."
}
if (Test-Path -LiteralPath $ReceiptTemporary) {
    Fail "stale temporary receipt exists: $ReceiptTemporary`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
}

$ReceiptInputMembers = @(
    foreach ($Record in $Vanilla) {
        [pscustomobject][ordered]@{
            name = [string]$Record.Name
            bytes = [int]$Record.Size
            sha256 = [string]$Record.Sha256
        }
    }
)
$ReceiptOutputMembers = @(
    foreach ($Record in $Vanilla) {
        $Name = [string]$Record.Name
        [pscustomobject][ordered]@{
            name = $Name
            bytes = ([byte[]]$Entries[$Name]).Length
            sha256 = [string]$Patched[$Name]
        }
    }
)
$ReceiptRuns = @(
    foreach ($Hook in $Patches) {
        [pscustomobject][ordered]@{
            kind = 'hook-or-native-data'
            member = [string]$Hook.File
            offset = ('0x{0:X6}' -f [int]$Hook.Offset)
            bytes = (ConvertFrom-Hex ([string]$Hook.After)).Length
            beforeSha256 = Get-Sha256 (ConvertFrom-Hex ([string]$Hook.Before))
            afterSha256 = Get-Sha256 (ConvertFrom-Hex ([string]$Hook.After))
            purpose = [string]$Hook.Purpose
        }
    }
)
$Receipt = [pscustomobject][ordered]@{
    format = 1
    status = 'installed'
    feature = 'Mega Man Magnetic Shockwave'
    componentRevision = 4
    cleanBaseOnly = $true
    stackingSupported = $false
    inputPolicy = 'exact 21 case-sensitive names/order/sizes/member SHA-256; container metadata/compression and outer hash are non-semantic'
    inputArchive = [IO.Path]::GetFileName($Source)
    inputArchiveSha256 = Get-Sha256 ([IO.File]::ReadAllBytes($Source))
    outputArchive = [IO.Path]::GetFileName($Destination)
    outputArchiveSha256 = Get-Sha256 ([IO.File]::ReadAllBytes($Temporary))
    payloadBytes = 1016
    payloadSha256 = '13cd13867172c1ccc17e4b12e91224520f4192d8f9bffdc315474557062d4bed'
    sourceSha256 = '442a0b38b028abc2d2d8a77b28f1a480cd762090d7bfd614267a5c4ae72a40f6'
    resourceSha256 = '7c508975370d03c11a1451d592b443e88ed2a10c376abf0970eae3ba3b7123c9'
    patchFingerprint = 'd5325ee80cbbff481fae271aa562babd5f621af6e5fffbf38d3a1fdeb272427b'
    romExpansion = $false
    customGraphicsWrites = $false
    command = 'ground QCB + any simultaneous two punches; costs one level'
    inputMembers = $ReceiptInputMembers
    outputMembers = $ReceiptOutputMembers
    changedRuns = $ReceiptRuns
    payload = [pscustomobject][ordered]@{
        member = 'mvc.10'
        offset = '0x071000'
        cpuAddress = '$3F1000'
        bytes = 1016
        plainSha256 = '13cd13867172c1ccc17e4b12e91224520f4192d8f9bffdc315474557062d4bed'
        diskSha256 = 'e191b13801cfec8664afaeb5081b266622a773216eef8f3d418b3004ffc22bb8'
    }
}
try {
    $ReceiptJson = ($Receipt | ConvertTo-Json -Depth 8) + [Environment]::NewLine
    $Utf8NoBom = New-Object System.Text.UTF8Encoding -ArgumentList $false
    [IO.File]::WriteAllText($ReceiptTemporary, $ReceiptJson, $Utf8NoBom)
    $ReceiptCheck = Get-Content -LiteralPath $ReceiptTemporary -Raw | ConvertFrom-Json
    if ([int]$ReceiptCheck.componentRevision -ne 4) { throw 'receipt revision mismatch.' }
    if ([int]$ReceiptCheck.inputMembers.Count -ne $Vanilla.Count) { throw 'receipt input manifest count mismatch.' }
    if ([int]$ReceiptCheck.outputMembers.Count -ne $Vanilla.Count) { throw 'receipt output manifest count mismatch.' }
    if ([string]$ReceiptCheck.payloadSha256 -ne '13cd13867172c1ccc17e4b12e91224520f4192d8f9bffdc315474557062d4bed') { throw 'receipt payload hash mismatch.' }
    if ([string]$ReceiptCheck.patchFingerprint -ne 'd5325ee80cbbff481fae271aa562babd5f621af6e5fffbf38d3a1fdeb272427b') { throw 'receipt patch fingerprint mismatch.' }
} catch {
    $ReceiptError = $_.Exception.Message
    if (Test-Path -LiteralPath $ReceiptTemporary) { Remove-Item -LiteralPath $ReceiptTemporary -Force }
    if (Test-Path -LiteralPath $Temporary) { Remove-Item -LiteralPath $Temporary -Force }
    Fail "could not write or verify receipt: $ReceiptError" 8
}

try {
    Move-Item -LiteralPath $Temporary -Destination $Destination
    try {
        Move-Item -LiteralPath $ReceiptTemporary -Destination $ReceiptPath
    } catch {
        if (Test-Path -LiteralPath $Destination) { Remove-Item -LiteralPath $Destination -Force }
        throw
    }
} catch {
    $PromotionError = $_.Exception.Message
    if (Test-Path -LiteralPath $Temporary) { Remove-Item -LiteralPath $Temporary -Force }
    if (Test-Path -LiteralPath $ReceiptTemporary) { Remove-Item -LiteralPath $ReceiptTemporary -Force }
    Fail "could not promote verified output and receipt: $PromotionError" 8
}

Write-Host ''
Write-Host '  DONE. Wrote and re-verified:' -ForegroundColor Green
Write-Host "    $Destination"
Write-Host "    $ReceiptPath"
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   13cd13867172c1ccc17e4b12e91224520f4192d8f9bffdc315474557062d4bed'
Write-Host '    disk payload SHA    e191b13801cfec8664afaeb5081b266622a773216eef8f3d418b3004ffc22bb8'
Write-Host '    patch fingerprint   d5325ee80cbbff481fae271aa562babd5f621af6e5fffbf38d3a1fdeb272427b'
Write-Host '    native title token  $5A'
Write-Host '    title record SHA    f1088827140a3b3fc63e5da69b2ff63d5ea65ac3a5e57f7ce148397498808c1f'
Write-Host '    stock GFX members   unchanged'
Write-Host ''
Pause-IfConsole
exit 0
