@echo off
rem ===========================================================================
rem  GOLD WAR MACHINE MEGA OPTIC BLAST v4 -- Marvel vs. Capcom (CPS-2, mvsc)
rem
rem  DRAG mvsc.zip ONTO THIS FILE
rem       or run:  MVSC-GOLD-WAR-MACHINE-MEGA-OPTIC-BLAST-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:MVSCGWMV4_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 '  GOLD WAR MACHINE MEGA OPTIC BLAST v4 -- Marvel vs. Capcom (CPS-2)'
Write-Host '  =========================================================='
Write-Host '  Adds Gold War Machine air-only QCF + any two punches; costs one level.'
Write-Host '  V4 enters stock unlit air-MP cooldown on the first post-beam frame.'
Write-Host '  Works in normal jumps and super jumps; ground Proton Cannon remains intact.'

$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-GOLD-WAR-MACHINE-MEGA-OPTIC-BLAST-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)
    }
}
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=0x00A4C6; Before='ab7daecec156ef83'; After='ce7ab586a5031ece'; Purpose='scoped common-palette reload/repaint gate' }
    @{ File='mvce.03a'; Offset=0x01CD1E; Before='b7e6ba813793ed6b'; After='59a8a5a9823ac4af'; Purpose='marked Gold class-$B4 hit/guard meter-add suppression' }
    @{ File='mvce.03a'; Offset=0x02896A; Before='140985fcc3c6'; After='9ac65ab03140'; Purpose='Gold War Machine airborne QCF+PP recognizer (normal/super jump)' }
    @{ File='mvce.03a'; Offset=0x02A41E; Before='ff7be8697134'; After='6a8530b84426'; Purpose='Gold/War Machine special dispatcher interpose' }
    @{ File='mvce.04a'; Offset=0x0254F4; Before='2fd01a6efe19'; After='f2f6e06b1939'; Purpose='class-$B4 native/custom owner gate and cleanup' }
    @{ File='mvce.04a'; Offset=0x02558E; Before='b8d5aefd1994'; After='2a50f3cb355e'; Purpose='class-$B4 subtype-0 Onslaught identity tail' }
    @{ File='mvce.04a'; Offset=0x0255D0; Before='77ce50ea9ea5'; After='aeea45fd79d9'; Purpose='class-$B4 subtype-0 forward/vertical calibration' }
    @{ File='mvce.04a'; Offset=0x0257C4; Before='f4439fcdfdc7'; After='58a8e06c86fa'; Purpose='class-$B4 subtype-2 Onslaught identity tail' }
    @{ File='mvce.04a'; Offset=0x02593C; Before='b7bb8047b87f'; After='ac2b79b21d78'; Purpose='class-$B4 subtype-4 Onslaught identity tail' }
    @{ File='mvce.04a'; Offset=0x025AAC; Before='d651d637798f'; After='e7aaa7738167'; Purpose='class-$B4 subtype-6 Onslaught identity tail' }
    @{ File='mvce.04a'; Offset=0x025AB8; Before='1aca3265bbf1'; After='4f4cde874687'; Purpose='class-$B4 subtype-6 forward/vertical calibration' }
)
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 = '6e0c28005200086600610e00004a0666f94e0200b68b754e2e4ace012466fa454e00ee47c80100614e04714e146700702e1002002d0138c00c662e0c0100740204640070754eb94e0100889a7c1d04004d007c1d1a00e6006e4206006e1db000b1007c1d2a00b000f94e02002c8c030000a01a00000004000600020070006e0c2800520008662e0c1a00e6000c6700702e10e600f94e020024a46e0c28005200006620002e3008001e67400c020000677a00400c04000067ba00400c060000670e01f94e0100b6776e5408002e42cf010070402d1400402d1800402d1c00402d20002e30c402400420006eb01000046c403d10007c1d0d0080002e4284002e4286002e423c007c3d3c004e002e4a020006666d5226c004606d5228c018700272b94e0100e635f84ed81d0061d400b84ed81d2e4284002e428600ae0800003c000c666e534e002866f94e0100b6776e5408002e4284002e4286003c309eff3c329fff0076b94e020058a47c3da0004e00754e00618c002d4ab77908677c1d0100cf01ec602e4acf0110672e42cf012e0ca500ce01046600619e0200612803714e714e2e4ace011466b94e0a006e5408646e424e00006012000061f8016e4a4e0006676e534e00a8667c3d06000800006130002d4ab77998662e4284002e4286000061aa01004a886600610e036e4a4e0008676e534e00006678fff94e0100b6770070402d1400402d1800402d1c00402d2000754e6e205400680c220052000a66680c2c02600000678200280ca500ce010467006070002d4ab77900666000680c2800520000663e00280c2a00b00000663400280c1a00e60000662a00680c0400080000662000082fb94e0a000c555f2600618202004a0a660061b002714e714e714e754e082fb94e0a00345b5f260061fe00004aec6600612e01754e7c110100cf01754ef94e0a00345bf94e0a000c5500612600f94e0a009a5500611c00f94e0a00d05700611200f94e0a00485900610800f94e0a00b85a680c280052002666280ca500ce011e667c1d040047007c3d220052007c2d0f00fcd768007c2d3a0000886c00754e682d68006800682d6c006c00754e6e205400680c280052001466280ca500ce010c667c3dafff1c003c30ebff0a607c3d7bff1c003c30c9ff2e4a4b0002674044403d1400f94e0a00e6556e205400680c280052001466280ca500ce010c667c3daeff1c003c30ecff0a607c3d7aff1c003c30caff2e4a4b0002674044403d1400f94e0a00ce5a4e266d224e5a09201867690cb40024000666e9b75400066769222c00e8600170754e0070754e2e0ca500ce010a6700617a007c1da500ce01754e0e2f4b2c04615f2c754e2e0ca500ce010e662e42ce012e42cf01b94e0000b0a4754e2d4ab77940662e0ca500ce013a666e0c2800520032662e0c2a00b0002a662e0c1a00e60022663c3017001122810200f000f09082c1221822810000f000f0c124c851e8ff0861754ef94e0000cea46e208802e84120007c241c00e4bd07701022810200f000f09a82c120c851f2ff6e208c02e84120007c241c00e4bd07701a22810000f000f0c120c851f4ff754e2e0c2a00b0004267b94e01001097004a3267e74880c000722e12020041d2ed414cc4c1d0103002722e4ab3000267017240c21267400270000032415341c00667df4c0301754edf4c03010070754e2e4284002e428600ae0c160060fe34001c642e304e00400203001266b84ed81d2e4284002e428600ae0800003c00754e006098fe0061d6fe2e0c0200600008662e0c120061000a6702701272b94e0100e635b84ed81d2e4284002e428600ae0800003c00754e0e2f4b2c7c3d060008000061c2ff5f2c754effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff970c0100aecc0867970c010016cd42666e0c280052003a662e0ca500ce0132666c0cb40024002a666c0c0200260008676c0c040026001a66ecbd54001466ac0c0f00d2788c001c67ac0c0f00f2788c0012672e4ae8000667f94e010026cdf94e010030cd754e'
$Payload = ConvertFrom-Hex $PayloadHex
if ($Payload.Length -ne 1478) { Fail 'internal payload length mismatch.' 8 }
if ((Get-Sha256 $Payload) -ne 'e396d12df0a20980806d0210f7e20206a1df65ba0923cdd86871208eee8bbd49') { 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 '688119ee0eb753f206e60fa2d08bbffb01d94f210722653ca7f26fe96febfb7f') {
    Fail 'embedded payload is not the declared CPS-2 word-swap.' 8
}
$Body = [byte[]]$Entries['mvc.10']
$BodyOffset = 0x072000
$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 $3F2000' -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'] = '4636026b99b5eb34dd735f259d68c36cd5c4b2eac3bf57a8c4c5bfc7683cfd65'
$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'] = 'd5c346a1438bbace02d44ed0c67c3d72cf4afe2bce26c8b7a0a19f7edf15394a'
$Patched['mvce.04a'] = '864eea7d84cb88123be1a8a92a59f09db631de27770c7fb421c1a5a9983edb9c'
$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_gold_war_machine_mega_optic_blast_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
            size = [int]$Record.Size
            sha256 = [string]$Record.Sha256
        }
    }
)
$ReceiptOutputMembers = @(
    foreach ($Record in $Vanilla) {
        $Name = [string]$Record.Name
        [pscustomobject][ordered]@{
            name = $Name
            size = ([byte[]]$Entries[$Name]).Length
            sha256 = [string]$Patched[$Name]
        }
    }
)
$ReceiptHooks = @(
    foreach ($Hook in $Patches) {
        [pscustomobject][ordered]@{
            member = [string]$Hook.File
            offset = ('0x{0:X6}' -f [int]$Hook.Offset)
            before = [string]$Hook.Before
            after = [string]$Hook.After
            purpose = [string]$Hook.Purpose
        }
    }
)
$Receipt = [pscustomobject][ordered]@{
    format = 1
    feature = 'Gold War Machine Airborne Mega Optic Blast'
    componentRevision = 4
    variant = 'mvsc'
    variantTitle = 'Marvel vs. Capcom (Euro 980123, encrypted parent)'
    command = 'Gold War Machine only: airborne QCF + any two punches in a normal or super jump; costs one level'
    romExpansion = $false
    customGraphicsWrites = $false
    stockAssetsOnly = $true
    containerPolicy = 'exact 21 member names, order, sizes and decompressed bytes; ZIP metadata/compression may differ'
    inputArchive = [IO.Path]::GetFileName($Source)
    inputArchiveSha256 = Get-Sha256 ([IO.File]::ReadAllBytes($Source))
    outputArchive = [IO.Path]::GetFileName($Destination)
    outputArchiveSha256 = Get-Sha256 ([IO.File]::ReadAllBytes($Temporary))
    inputMembers = $ReceiptInputMembers
    outputMembers = $ReceiptOutputMembers
    hooks = $ReceiptHooks
    payload = [pscustomobject][ordered]@{
        member = 'mvc.10'
        offset = '0x072000'
        cpuAddress = '$3F2000'
        bytes = 1478
        plainSha256 = '688119ee0eb753f206e60fa2d08bbffb01d94f210722653ca7f26fe96febfb7f'
        diskSha256 = 'e396d12df0a20980806d0210f7e20206a1df65ba0923cdd86871208eee8bbd49'
    }
    preservationClaims = @(
        'regular War Machine is unchanged'
        'grounded Gold War Machine Proton Cannon is unchanged'
        'plain Gold War Machine air MP is unchanged'
        'the native Onslaught class-$B4 path is preserved'
    )
    patchFingerprint = 'f7694640f3ab99c82b9683813b1317baeec7985f9e21edc21558331653b9829f'
    sourceSha256 = '08b43a7b9f7678241c53d46471ce6d67bb073165fbcf9598a7f7a9c5545be6de'
    resourceSha256 = '52785ce5cd6c688e6806daaa9c97c30f065cf80de0aa78e63cf3822769b14de3'
    cooldown = 'stock unlit group $02/subaction $12 on first post-beam frame'
}
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 ([int]$ReceiptCheck.hooks.Count -ne $Patches.Count) { throw 'receipt hook count mismatch.' }
    if ([string]$ReceiptCheck.payload.plainSha256 -ne '688119ee0eb753f206e60fa2d08bbffb01d94f210722653ca7f26fe96febfb7f') { throw 'receipt payload hash mismatch.' }
    if ([string]$ReceiptCheck.patchFingerprint -ne 'f7694640f3ab99c82b9683813b1317baeec7985f9e21edc21558331653b9829f') { 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: Gold War Machine airborne QCF + any two punch buttons (one super level; normal or super jump).'
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       1478'
Write-Host '    plain payload SHA   688119ee0eb753f206e60fa2d08bbffb01d94f210722653ca7f26fe96febfb7f'
Write-Host '    disk payload SHA    e396d12df0a20980806d0210f7e20206a1df65ba0923cdd86871208eee8bbd49'
Write-Host '    patch fingerprint   f7694640f3ab99c82b9683813b1317baeec7985f9e21edc21558331653b9829f'
Write-Host '    stock GFX/audio     unchanged'
Write-Host ''
Pause-IfConsole
exit 0
