@echo off
rem ===========================================================================
rem  MVC1 four-player Cross Fever with player markers v2.1-preview
rem  *** EXPERIMENTAL PREVIEW -- see included scope and evidence ***
rem  Generated from a patch spec by gen_cmd.py -- do not hand-edit.
rem  Required base: Stock MVC1 Euro 980123 (mvsc). Drag your mvsc.zip onto this file, or run:
rem      MVC1-Cross-Fever-v2.1-preview.cmd "C:\path\to\mvsc.zip"
rem  This installer contains patch deltas and integrity metadata, not a ROM.
rem  It makes no network requests, never overwrites the clean input, and
rem  refuses patched/noncanonical sets.
rem ===========================================================================
setlocal
set "ROMPATH=%~f1"
set "SELFPATH=%~f0"
where powershell.exe >nul 2>&1
if errorlevel 1 (
  echo.
  echo   ERROR: Windows PowerShell was not found.
  echo.
  exit /b 1
)
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.' -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:MVSC_DHC_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]'
    $Ignored = New-Object 'System.Collections.Generic.List[string]'
    $Seen = New-Object 'System.Collections.Generic.HashSet[string]'
    try { $Zip = [System.IO.Compression.ZipFile]::OpenRead($Path) }
    catch { throw "not a readable ZIP archive: $Path" }
    try {
        foreach ($Entry in $Zip.Entries) {
            if (-not $Seen.Add($Entry.FullName)) {
                throw "archive has a duplicate member named $($Entry.FullName)."
            }
            $Name = [string]$Entry.FullName
            if ($Name.StartsWith('/') -or $Name.StartsWith('\') -or $Name.Contains(':') -or (($Name.Replace('\','/').Split('/')) -contains '..')) {
                throw "unsafe archive member name: $Name"
            }
            if (-not $VanillaLookup.ContainsKey($Name)) {
                if ($VanillaLookup.ContainsKey($Name.ToLowerInvariant())) {
                    throw "required member has incorrect capitalization: $Name"
                }
                [void]$Ignored.Add($Name)
                continue
            }
            $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; Ignored=$Ignored }
}

Write-Host ''
Write-Host '  MVC1 four-player Cross Fever with player markers v2.1-preview'
Write-Host '  ================================================================'
    Write-Host '  *** EXPERIMENTAL PREVIEW -- not an accepted stable release. ***' -ForegroundColor Yellow
Write-Host '  The clean input is only read. A new patched ZIP is written.'

$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)) {
    Fail 'drag the required mvsc.zip onto this CMD or put it beside this file.'
}
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='mvce.04a'; Size=524288; Sha256='71ded7040f279d0ddea1326fc062465159ccd832a327230098e5740d2a747d67' }
    @{ Name='mvc.06a'; Size=524288; Sha256='132316e99d02d197cd8b4b9227dccd9215f5f741bcd21d5abf5c3f07fb64354b' }
    @{ Name='mvc.05a'; Size=524288; Sha256='1dfd50972c8796624051857e16ddc2296932c39fb05ecfcd1bae6180a369285d' }
    @{ Name='mvc.12m'; Size=4194304; Sha256='971b949ca3ad2471bf5ddc5c87c10286eacba51c30b4cc2261328ec46732b920' }
    @{ Name='mvc.18m'; Size=4194304; Sha256='4f3ceefa11958fd92f88047f876e37a5a9a226ca9c28e3b9915a03f39a268947' }
    @{ Name='mvc.11m'; Size=4194304; Sha256='5587602fb97953d0cd951392b3bbaef2d317ae943ff2f6608c46cb627b7b16ca' }
    @{ Name='mvc.15m'; Size=4194304; Sha256='37365288e38a8c41fb5d2eb72808038478def8c8aa368d85816b70a89f6a6f55' }
    @{ Name='mvc.16m'; Size=4194304; Sha256='c94231770a7377bf6c7817427f28b87db79f3be03287279e8a19bea34cca85be' }
    @{ Name='mvc.07'; Size=524288; Sha256='8b29a492800ec41c9818f1cf880697b298af69c75d0dd6864ed06e65223930eb' }
    @{ Name='mvc.09'; Size=524288; Sha256='fd0e680e0e1b14a5d1e913ddaef37076dd80203326e305b1fe1f8070be0cd6b2' }
    @{ Name='mvce.03a'; Size=524288; Sha256='48128b6f98c5171a0e734e99f01a273f526f2da74f08635732068c60c891d757' }
    @{ Name='mvc.08'; Size=524288; Sha256='402a848e3860f20cdcf3ec80c640f27eaef36f4e7cd78a330913baedcd5356d8' }
    @{ Name='mvc.01'; Size=131072; Sha256='565f2c1befc6dbcd727b65c8954d3263b1e8105cd16be3ff7399c0ff513d144e' }
    @{ Name='mvc.20m'; Size=4194304; Sha256='6cb08b23e33cc97bd14c6bf0ed179c99952146328f0dc0f01e8135c2783466bd' }
    @{ Name='mvc.10'; Size=524288; Sha256='e24119140931bc861bf791758af8eb375892260d8ffb633369ebedcbe21f172a' }
    @{ Name='mvc.17m'; Size=4194304; Sha256='dc29f2f0bf56d60408e9b9a2fcfae6c684f8be97712c94f146386c9bbb9ab46f' }
    @{ Name='mvc.14m'; Size=4194304; Sha256='1be6ba60b3cc0e6b647c2a54ca06b948dbc72eb32efdead84bcaa1d2efd69db5' }
    @{ Name='mvc.13m'; Size=4194304; Sha256='689b9f2e2f9105f6b6a456e836a289c65caf642d9c7d246ea95c2db256f2ce0d' }
    @{ Name='mvsc.key'; Size=20; Sha256='9ba914d01fdb50c562fef4d4a5c7ff4779f92340322272696a46025d33f7155c' }
    @{ Name='mvc.19m'; Size=4194304; Sha256='d245405b8fa57b315300d915b9b5f96859bc31b9a97f653e370c7f0f28a1fc7f' }
    @{ Name='mvc.02'; Size=131072; Sha256='b460ea0d95c967e5276d0f1f6e0a68d3bcc6008ce7952b970069a211e460ec63' }
)
$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
$IgnoredArchiveMembers = $Read.Ignored

$OptionalMembers = @('mvsc.key')
$PresentOptional = @($OptionalMembers | Where-Object { $Entries.ContainsKey($_) })
foreach ($Record in $Vanilla) {
    $Name = [string]$Record.Name
    if (-not $Entries.ContainsKey($Name)) {
        if ($OptionalMembers -contains $Name) { continue }
        Fail "required Euro 980123 member '$Name' is missing. The complete Euro parent must be at the archive root; regional clone files do not replace it."
    }
    $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. Required base: Stock MVC1 Euro 980123 (mvsc). Patched and wrong-revision sets are rejected."
    }
}

Write-Host ''
Write-Host "  Input : $Source"
Write-Host '  Base  : Stock MVC1 Euro 980123 (mvsc)'
Write-Host "  Set   : $($Entries.Count) exact members present"
Write-Host "  Merged: $($IgnoredArchiveMembers.Count) non-parent members left out of output."

$Patches = @(
    @{ File='mvc.15m'; Offset=0x2AE9E7; After='e0' }
    @{ File='mvc.15m'; Offset=0x2AE9EB; After='f0' }
    @{ File='mvc.15m'; Offset=0x2AE9EF; After='f8' }
    @{ File='mvc.15m'; Offset=0x2AE9F3; After='fc' }
    @{ File='mvc.15m'; Offset=0x2AE9F6; After='fee7' }
    @{ File='mvc.15m'; Offset=0x2AE9FB; After='f3' }
    @{ File='mvc.15m'; Offset=0x2AE9FF; After='f9' }
    @{ File='mvc.15m'; Offset=0x2AEA16; After='7f' }
    @{ File='mvc.15m'; Offset=0x2AEA1A; After='3f' }
    @{ File='mvc.15m'; Offset=0x2AEA1E; After='80' }
    @{ File='mvc.15m'; Offset=0x2AEA22; After='be' }
    @{ File='mvc.15m'; Offset=0x2AEA26; After='b3' }
    @{ File='mvc.15m'; Offset=0x2AEA2A; After='33' }
    @{ File='mvc.15m'; Offset=0x2AEA2E; After='b3' }
    @{ File='mvc.15m'; Offset=0x2AEA32; After='be' }
    @{ File='mvc.15m'; Offset=0x2AEA36; After='b0' }
    @{ File='mvc.15m'; Offset=0x2AEA3A; After='35fa' }
    @{ File='mvc.15m'; Offset=0x2AEA3E; After='00ff' }
    @{ File='mvc.15m'; Offset=0x2AEA67; After='e0' }
    @{ File='mvc.15m'; Offset=0x2AEA6B; After='f0' }
    @{ File='mvc.15m'; Offset=0x2AEA6F; After='f8' }
    @{ File='mvc.15m'; Offset=0x2AEA73; After='fc' }
    @{ File='mvc.15m'; Offset=0x2AEA76; After='fee7' }
    @{ File='mvc.15m'; Offset=0x2AEA7B; After='f3' }
    @{ File='mvc.15m'; Offset=0x2AEA7F; After='f9' }
    @{ File='mvc.15m'; Offset=0x2AEA96; After='7f' }
    @{ File='mvc.15m'; Offset=0x2AEA9A; After='3f' }
    @{ File='mvc.15m'; Offset=0x2AEA9E; After='00' }
    @{ File='mvc.15m'; Offset=0x2AEAA2; After='00' }
    @{ File='mvc.15m'; Offset=0x2AEABA; After='05fa' }
    @{ File='mvc.15m'; Offset=0x2AEE02; After='01ff' }
    @{ File='mvc.15m'; Offset=0x2AEE06; After='03ff' }
    @{ File='mvc.15m'; Offset=0x2AEE0A; After='07fe' }
    @{ File='mvc.15m'; Offset=0x2AEE0E; After='0c' }
    @{ File='mvc.15m'; Offset=0x2AEE12; After='19' }
    @{ File='mvc.15m'; Offset=0x2AEE16; After='33' }
    @{ File='mvc.15m'; Offset=0x2AEE1A; After='66' }
    @{ File='mvc.15m'; Offset=0x2AEE1E; After='cc' }
    @{ File='mvc.15m'; Offset=0x2AEE22; After='98' }
    @{ File='mvc.15m'; Offset=0x2AEE82; After='01ff' }
    @{ File='mvc.15m'; Offset=0x2AEE86; After='03ff' }
    @{ File='mvc.15m'; Offset=0x2AEE8A; After='07fe' }
    @{ File='mvc.15m'; Offset=0x2AEE8E; After='0c' }
    @{ File='mvc.15m'; Offset=0x2AEE92; After='19' }
    @{ File='mvc.15m'; Offset=0x2AEE96; After='33' }
    @{ File='mvc.15m'; Offset=0x2AEE9A; After='66' }
    @{ File='mvc.15m'; Offset=0x2AEE9E; After='cc' }
    @{ File='mvc.15m'; Offset=0x2AEEA2; After='98' }
    @{ File='mvce.03a'; Offset=0x000D12; After='ea31082e406194a675d1' }
    @{ File='mvce.03a'; Offset=0x000D1D; After='8e2d176d1f46deb0473f833428af78d3136f6d619417d19ae97fbb77f35fb954fefa701c1145e9d61044a186d147c344d6f42adc1f506e6c7d7249658dae0e3cb851b3227e183498dcdd87e3542cd9399df031' }
    @{ File='mvce.03a'; Offset=0x007EF4; After='ef3b' }
    @{ File='mvce.03a'; Offset=0x007F38; After='cd26' }
    @{ File='mvce.03a'; Offset=0x01368E; After='3ab111a49465cbece9d5956dc7e6314c' }
    @{ File='mvce.03a'; Offset=0x015F58; After='09943c7e582e' }
    @{ File='mvce.03a'; Offset=0x016E8C; After='83fc38d3c321' }
    @{ File='mvce.03a'; Offset=0x024AB4; After='990878b24b41' }
    @{ File='mvc.10'; Offset=0x071000; After='e748003f2d4250c4791b8000214051c42d4651c4007000616e000038017000616600003a027000615e00003c037000615600003e790c0400' }
    @{ File='mvc.10'; Offset=0x071039; After='0004411866390c0100' }
    @{ File='mvc.10'; Offset=0x071043; After='00834102660638390c0100' }
    @{ File='mvc.10'; Offset=0x07104F; After='00b3410266073a443b4cc4453b4ec46d1b50c458c4df4cfc00f84e700d0e208004' }
    @{ File='mvc.10'; Offset=0x071071; After='00003088e088e44002030000610800f94e01009e36e7488070003600344ae24ae9f94180000140c2d040020100c09000701010004600324102800049e340027f004180393280002040414603344206080001050467400000024258010504674000000439080000' }
    @{ File='mvc.10'; Offset=0x0710D9; After='009b4400679a00390c0800' }
    @{ File='mvc.10'; Offset=0x0710E5; After='0000401e66394a' }
    @{ File='mvc.10'; Offset=0x0710ED; After='00004116673912' }
    @{ File='mvc.10'; Offset=0x0710F5; After='000141010203000a67f9080100' }
    @{ File='mvc.10'; Offset=0x071103; After='009b441260b9080100' }
    @{ File='mvc.10'; Offset=0x07110D; After='009b4408673900f000' }
    @{ File='mvc.10'; Offset=0x071117; After='009b44033442580032410240043905' }
    @{ File='mvc.10'; Offset=0x071127; After='009b440c67414a4466b905' }
    @{ File='mvc.10'; Offset=0x071133; After='009b444060390c0800' }
    @{ File='mvc.10'; Offset=0x07113D; After='0000401666394a' }
    @{ File='mvc.10'; Offset=0x071145; After='0000410e670332410201003903' }
    @{ File='mvc.10'; Offset=0x071153; After='0001412066003241024004410c40041466f905' }
    @{ File='mvc.10'; Offset=0x071167; After='009b44033241020100ed0350c440023f03df4c0e01754e6d4a8cc40a672d125cc4410280000e662d08000089c40667f94e0100685ff94e01006c5fe748e0fc390c0800' }
    @{ File='mvc.10'; Offset=0x0711AB; After='00004000669e00fcb97000d09f00629400f941' }
    @{ File='mvc.10'; Offset=0x0711BF; After='000030007a104a00677800684a70020067700000702810300000676600f94502003a56722400000478284a03000267053844d8fa436a00313800402832520049e2410c17000265007241d2fa435c00313200102834100041d46a9404004202' }
    @{ File='mvc.10'; Offset=0x07121F; After='1f4200002028360c005296430410004302' }
    @{ File='mvc.10'; Offset=0x071231; After='1f430000c0c338c238c438fc381211e84100044552450c0400006578' }
    @{ File='mvc.10'; Offset=0x07124E; After='df4c3f07fc2800800080f94e0200ba4aab6bad6ba76ba96bbc6c90' }
    @{ File='mvc.10'; Offset=0x07126A; After='80' }
    @{ File='mvc.10'; Offset=0x07126C; After='70' }
    @{ File='mvc.10'; Offset=0x07126E; After='80' }
    @{ File='mvc.10'; Offset=0x071270; After='a0' }
    @{ File='mvc.10'; Offset=0x071272; After='80' }
    @{ File='mvc.10'; Offset=0x071274; After='80' }
    @{ File='mvc.10'; Offset=0x071276; After='90' }
    @{ File='mvc.10'; Offset=0x071278; After='a0' }
    @{ File='mvc.10'; Offset=0x07127A; After='90' }
    @{ File='mvc.10'; Offset=0x07127C; After='80' }
    @{ File='mvc.10'; Offset=0x07127E; After='90' }
    @{ File='mvc.10'; Offset=0x071280; After='80' }
    @{ File='mvc.10'; Offset=0x071282; After='70' }
    @{ File='mvc.10'; Offset=0x071284; After='80' }
    @{ File='mvc.10'; Offset=0x071286; After='80' }
    @{ File='mvc.10'; Offset=0x071288; After='90' }
    @{ File='mvc.10'; Offset=0x07128A; After='a0' }
    @{ File='mvc.10'; Offset=0x07128C; After='80' }
    @{ File='mvc.10'; Offset=0x07128E; After='80' }
    @{ File='mvc.10'; Offset=0x071290; After='80' }
    @{ File='mvc.10'; Offset=0x071292; After='90' }
    @{ File='mvc.10'; Offset=0x071294; After='80' }
    @{ File='mvc.17m'; Offset=0x2AE9D6; After='c0' }
    @{ File='mvc.17m'; Offset=0x2AE9DA; After='9fe0' }
    @{ File='mvc.17m'; Offset=0x2AE9DE; After='bfc0' }
    @{ File='mvc.17m'; Offset=0x2AE9E2; After='3bc4' }
    @{ File='mvc.17m'; Offset=0x2AE9E6; After='83fc' }
    @{ File='mvc.17m'; Offset=0x2AE9EA; After='eff0' }
    @{ File='mvc.17m'; Offset=0x2AE9EE; After='c3fc' }
    @{ File='mvc.17m'; Offset=0x2AE9F2; After='bbc4' }
    @{ File='mvc.17m'; Offset=0x2AE9F6; After='bfc0' }
    @{ File='mvc.17m'; Offset=0x2AE9FA; After='1f60' }
    @{ File='mvc.17m'; Offset=0x2AE9FE; After='00ff' }
    @{ File='mvc.17m'; Offset=0x2AEA22; After='01' }
    @{ File='mvc.17m'; Offset=0x2AEA26; After='7c' }
    @{ File='mvc.17m'; Offset=0x2AEA2A; After='79' }
    @{ File='mvc.17m'; Offset=0x2AEA2E; After='73' }
    @{ File='mvc.17m'; Offset=0x2AEA32; After='61' }
    @{ File='mvc.17m'; Offset=0x2AEA36; After='8cbf' }
    @{ File='mvc.17m'; Offset=0x2AEA3A; After='197f' }
    @{ File='mvc.17m'; Offset=0x2AEA3E; After='33' }
    @{ File='mvc.17m'; Offset=0x2AEA56; After='c4' }
    @{ File='mvc.17m'; Offset=0x2AEA5A; After='bb' }
    @{ File='mvc.17m'; Offset=0x2AEA5E; After='bb' }
    @{ File='mvc.17m'; Offset=0x2AEA62; After='3b7f' }
    @{ File='mvc.17m'; Offset=0x2AEA6A; After='bf' }
    @{ File='mvc.17m'; Offset=0x2AEA72; After='fbff' }
    @{ File='mvc.17m'; Offset=0x2AEA76; After='abaf' }
    @{ File='mvc.17m'; Offset=0x2AEA7A; After='5357' }
    @{ File='mvc.17m'; Offset=0x2AEA7E; After='0007' }
    @{ File='mvc.17m'; Offset=0x2AEAA2; After='0181' }
    @{ File='mvc.17m'; Offset=0x2AEAA6; After='7cfc' }
    @{ File='mvc.17m'; Offset=0x2AEAAA; After='79f9' }
    @{ File='mvc.17m'; Offset=0x2AEAAE; After='73f3' }
    @{ File='mvc.17m'; Offset=0x2AEAB2; After='61e1' }
    @{ File='mvc.17m'; Offset=0x2AEAB6; After='8c8c' }
    @{ File='mvc.17m'; Offset=0x2AEABA; After='1919' }
    @{ File='mvc.17m'; Offset=0x2AEABE; After='3333' }
    @{ File='mvc.17m'; Offset=0x2AEDC2; After='00c0' }
    @{ File='mvc.17m'; Offset=0x2AEDC7; After='e0' }
    @{ File='mvc.17m'; Offset=0x2AEDCA; After='4570' }
    @{ File='mvc.17m'; Offset=0x2AEDCE; After='0298' }
    @{ File='mvc.17m'; Offset=0x2AEDD2; After='03cc' }
    @{ File='mvc.17m'; Offset=0x2AEDD6; After='01e6' }
    @{ File='mvc.17m'; Offset=0x2AEDDA; After='04f3' }
    @{ File='mvc.17m'; Offset=0x2AEDDF; After='f9' }
    @{ File='mvc.17m'; Offset=0x2AEE02; After='63' }
    @{ File='mvc.17m'; Offset=0x2AEE42; After='0000' }
    @{ File='mvc.17m'; Offset=0x2AEE47; After='80' }
    @{ File='mvc.17m'; Offset=0x2AEE4A; After='4540' }
    @{ File='mvc.17m'; Offset=0x2AEE4E; After='02' }
    @{ File='mvc.17m'; Offset=0x2AEE52; After='03' }
    @{ File='mvc.17m'; Offset=0x2AEE56; After='01' }
    @{ File='mvc.17m'; Offset=0x2AEE5A; After='04' }
    @{ File='mvc.17m'; Offset=0x2AEE82; After='6367' }
    @{ File='mvc.13m'; Offset=0x2AE9E2; After='c0' }
    @{ File='mvc.13m'; Offset=0x2AE9E6; After='9f' }
    @{ File='mvc.13m'; Offset=0x2AE9EA; After='cf' }
    @{ File='mvc.13m'; Offset=0x2AE9EE; After='e7' }
    @{ File='mvc.13m'; Offset=0x2AE9F2; After='c3' }
    @{ File='mvc.13m'; Offset=0x2AE9F6; After='98fe' }
    @{ File='mvc.13m'; Offset=0x2AE9FA; After='cc' }
    @{ File='mvc.13m'; Offset=0x2AE9FE; After='e6' }
    @{ File='mvc.13m'; Offset=0x2AEA16; After='7f' }
    @{ File='mvc.13m'; Offset=0x2AEA1A; After='3f' }
    @{ File='mvc.13m'; Offset=0x2AEA1E; After='807f' }
    @{ File='mvc.13m'; Offset=0x2AEA22; After='be41' }
    @{ File='mvc.13m'; Offset=0x2AEA26; After='b34c' }
    @{ File='mvc.13m'; Offset=0x2AEA2A; After='33cc' }
    @{ File='mvc.13m'; Offset=0x2AEA2E; After='b34c' }
    @{ File='mvc.13m'; Offset=0x2AEA32; After='be41' }
    @{ File='mvc.13m'; Offset=0x2AEA36; After='b04f' }
    @{ File='mvc.13m'; Offset=0x2AEA3A; After='35cd' }
    @{ File='mvc.13m'; Offset=0x2AEA3E; After='00f8' }
    @{ File='mvc.13m'; Offset=0x2AEA62; After='c0c0' }
    @{ File='mvc.13m'; Offset=0x2AEA66; After='9f9f' }
    @{ File='mvc.13m'; Offset=0x2AEA6A; After='cfcf' }
    @{ File='mvc.13m'; Offset=0x2AEA6E; After='e7e7' }
    @{ File='mvc.13m'; Offset=0x2AEA72; After='c3c3' }
    @{ File='mvc.13m'; Offset=0x2AEA76; After='9898' }
    @{ File='mvc.13m'; Offset=0x2AEA7A; After='cccc' }
    @{ File='mvc.13m'; Offset=0x2AEA7E; After='e6e6' }
    @{ File='mvc.13m'; Offset=0x2AEA96; After='7f' }
    @{ File='mvc.13m'; Offset=0x2AEA9A; After='bf' }
    @{ File='mvc.13m'; Offset=0x2AEA9E; After='80' }
    @{ File='mvc.13m'; Offset=0x2AEAA2; After='be' }
    @{ File='mvc.13m'; Offset=0x2AEAA6; After='b3' }
    @{ File='mvc.13m'; Offset=0x2AEAAA; After='b3' }
    @{ File='mvc.13m'; Offset=0x2AEAB2; After='be' }
    @{ File='mvc.13m'; Offset=0x2AEAB6; After='b0' }
    @{ File='mvc.13m'; Offset=0x2AEABA; After='b5fd' }
    @{ File='mvc.13m'; Offset=0x2AEABE; After='00f8' }
    @{ File='mvc.13m'; Offset=0x2AEE02; After='0001' }
    @{ File='mvc.13m'; Offset=0x2AEE06; After='0003' }
    @{ File='mvc.13m'; Offset=0x2AEE0A; After='5107' }
    @{ File='mvc.13m'; Offset=0x2AEE0E; After='a00c' }
    @{ File='mvc.13m'; Offset=0x2AEE12; After='e019' }
    @{ File='mvc.13m'; Offset=0x2AEE16; After='c033' }
    @{ File='mvc.13m'; Offset=0x2AEE1A; After='9067' }
    @{ File='mvc.13m'; Offset=0x2AEE1E; After='20cf' }
    @{ File='mvc.13m'; Offset=0x2AEE22; After='609f' }
    @{ File='mvc.13m'; Offset=0x2AEE82; After='0000' }
    @{ File='mvc.13m'; Offset=0x2AEE86; After='0000' }
    @{ File='mvc.13m'; Offset=0x2AEE8A; After='5101' }
    @{ File='mvc.13m'; Offset=0x2AEE8E; After='a0' }
    @{ File='mvc.13m'; Offset=0x2AEE92; After='e0' }
    @{ File='mvc.13m'; Offset=0x2AEE96; After='c0' }
    @{ File='mvc.13m'; Offset=0x2AEE9A; After='90' }
    @{ File='mvc.13m'; Offset=0x2AEE9E; After='20' }
    @{ File='mvc.13m'; Offset=0x2AEEA2; After='60' }
    @{ File='mvc.19m'; Offset=0x2AE9D6; After='c0' }
    @{ File='mvc.19m'; Offset=0x2AE9DA; After='9f' }
    @{ File='mvc.19m'; Offset=0x2AE9DE; After='bf' }
    @{ File='mvc.19m'; Offset=0x2AE9E2; After='bb' }
    @{ File='mvc.19m'; Offset=0x2AE9E6; After='837f' }
    @{ File='mvc.19m'; Offset=0x2AE9EA; After='ef1f' }
    @{ File='mvc.19m'; Offset=0x2AE9EE; After='c33f' }
    @{ File='mvc.19m'; Offset=0x2AE9F2; After='bb7f' }
    @{ File='mvc.19m'; Offset=0x2AE9F6; After='bf7f' }
    @{ File='mvc.19m'; Offset=0x2AE9FA; After='1fff' }
    @{ File='mvc.19m'; Offset=0x2AE9FE; After='80ff' }
    @{ File='mvc.19m'; Offset=0x2AEA22; After='7f' }
    @{ File='mvc.19m'; Offset=0x2AEA26; After='7f83' }
    @{ File='mvc.19m'; Offset=0x2AEA2A; After='7f87' }
    @{ File='mvc.19m'; Offset=0x2AEA2E; After='7f8f' }
    @{ File='mvc.19m'; Offset=0x2AEA32; After='7f9f' }
    @{ File='mvc.19m'; Offset=0x2AEA36; After='bf73' }
    @{ File='mvc.19m'; Offset=0x2AEA3A; After='7fe7' }
    @{ File='mvc.19m'; Offset=0x2AEA3F; After='cf' }
    @{ File='mvc.19m'; Offset=0x2AEA56; After='c4' }
    @{ File='mvc.19m'; Offset=0x2AEA5A; After='80' }
    @{ File='mvc.19m'; Offset=0x2AEA5E; After='80' }
    @{ File='mvc.19m'; Offset=0x2AEA67; After='7f' }
    @{ File='mvc.19m'; Offset=0x2AEA6B; After='7f' }
    @{ File='mvc.19m'; Offset=0x2AEA6F; After='7f' }
    @{ File='mvc.19m'; Offset=0x2AEA72; After='f807' }
    @{ File='mvc.19m'; Offset=0x2AEA76; After='a857' }
    @{ File='mvc.19m'; Offset=0x2AEA7A; After='50af' }
    @{ File='mvc.19m'; Offset=0x2AEA7F; After='ff' }
    @{ File='mvc.19m'; Offset=0x2AEAA2; After='7f' }
    @{ File='mvc.19m'; Offset=0x2AEAA6; After='7f83' }
    @{ File='mvc.19m'; Offset=0x2AEAAB; After='87' }
    @{ File='mvc.19m'; Offset=0x2AEAAF; After='8f' }
    @{ File='mvc.19m'; Offset=0x2AEAB3; After='9f' }
    @{ File='mvc.19m'; Offset=0x2AEAB6; After='bf73' }
    @{ File='mvc.19m'; Offset=0x2AEABB; After='e7' }
    @{ File='mvc.19m'; Offset=0x2AEABF; After='cf' }
    @{ File='mvc.19m'; Offset=0x2AEDC2; After='c0ff' }
    @{ File='mvc.19m'; Offset=0x2AEDC6; After='e0' }
    @{ File='mvc.19m'; Offset=0x2AEDCA; After='70bf' }
    @{ File='mvc.19m'; Offset=0x2AEDCE; After='98' }
    @{ File='mvc.19m'; Offset=0x2AEDD2; After='cc' }
    @{ File='mvc.19m'; Offset=0x2AEDD6; After='66' }
    @{ File='mvc.19m'; Offset=0x2AEDDA; After='33' }
    @{ File='mvc.19m'; Offset=0x2AEDDE; After='19' }
    @{ File='mvc.19m'; Offset=0x2AEE03; After='9f' }
    @{ File='mvc.19m'; Offset=0x2AEE42; After='c0ff' }
    @{ File='mvc.19m'; Offset=0x2AEE46; After='e0' }
    @{ File='mvc.19m'; Offset=0x2AEE4A; After='70bf' }
    @{ File='mvc.19m'; Offset=0x2AEE4E; After='98' }
    @{ File='mvc.19m'; Offset=0x2AEE52; After='cc' }
    @{ File='mvc.19m'; Offset=0x2AEE56; After='66' }
    @{ File='mvc.19m'; Offset=0x2AEE5A; After='33' }
    @{ File='mvc.19m'; Offset=0x2AEE5E; After='19' }
    @{ File='mvc.19m'; Offset=0x2AEE83; After='9f' }
)
$HasBeforeProof = $false

$FingerprintBuilder = New-Object System.Text.StringBuilder
foreach ($Patch in $Patches) {
    if ($HasBeforeProof) {
        [void]$FingerprintBuilder.Append(('{0}|{1:x6}|{2}|{3}' -f [string]$Patch.File, [int]$Patch.Offset, [string]$Patch.Before, [string]$Patch.After))
    } else {
        [void]$FingerprintBuilder.Append(('{0}|{1:x6}|{2}' -f [string]$Patch.File, [int]$Patch.Offset, [string]$Patch.After))
    }
    [void]$FingerprintBuilder.Append([char]10)
}
$Fingerprint = Get-Sha256 ([System.Text.Encoding]::ASCII.GetBytes($FingerprintBuilder.ToString()))
if ($Fingerprint -ne 'd24b6526f637a67cb48edf8ada94626d6e103a9f33a9cac328c59d88ed09908a') {
    Fail 'internal patch fingerprint mismatch; re-download this installer.' 8
}

$ByFile = New-Object 'System.Collections.Generic.Dictionary[string,object]'
foreach ($Patch in $Patches) {
    $Name = [string]$Patch.File
    if (-not $Entries.ContainsKey($Name)) { Fail "internal patch references unknown member '$Name'." 8 }
    $Buffer = [byte[]]$Entries[$Name]
    $After = ConvertFrom-Hex ([string]$Patch.After)
    if ([int]$Patch.Offset -lt 0 -or [int]$Patch.Offset + $After.Length -gt $Buffer.Length) {
        Fail 'internal patch range is outside its member.' 8
    }
    if ($HasBeforeProof) {
        $Before = ConvertFrom-Hex ([string]$Patch.Before)
        if ($Before.Length -ne $After.Length) { Fail 'internal patch 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 $Name,[int]$Patch.Offset)
        }
    }
    [Array]::Copy($After, 0, $Buffer, [int]$Patch.Offset, $After.Length)
    if (-not $ByFile.ContainsKey($Name)) { $ByFile[$Name] = [pscustomobject]@{ Hunks=0; Bytes=0 } }
    $ByFile[$Name].Hunks = $ByFile[$Name].Hunks + 1
    $ByFile[$Name].Bytes = $ByFile[$Name].Bytes + $After.Length
}
foreach ($Name in ($ByFile.Keys | Sort-Object)) {
    Write-Host ("  patch : {0,-9} {1,4} hunks, {2,6} embedded bytes" -f $Name,$ByFile[$Name].Hunks,$ByFile[$Name].Bytes)
}

$Patched = New-Object 'System.Collections.Generic.Dictionary[string,string]'
$Patched['mvce.04a'] = '71ded7040f279d0ddea1326fc062465159ccd832a327230098e5740d2a747d67'
$Patched['mvc.06a'] = '132316e99d02d197cd8b4b9227dccd9215f5f741bcd21d5abf5c3f07fb64354b'
$Patched['mvc.05a'] = '1dfd50972c8796624051857e16ddc2296932c39fb05ecfcd1bae6180a369285d'
$Patched['mvc.12m'] = '971b949ca3ad2471bf5ddc5c87c10286eacba51c30b4cc2261328ec46732b920'
$Patched['mvc.18m'] = '4f3ceefa11958fd92f88047f876e37a5a9a226ca9c28e3b9915a03f39a268947'
$Patched['mvc.11m'] = '5587602fb97953d0cd951392b3bbaef2d317ae943ff2f6608c46cb627b7b16ca'
$Patched['mvc.15m'] = '1b011a70ea37e387900e734ede0eac1440466618a8d45dd72534284627e5391f'
$Patched['mvc.16m'] = 'c94231770a7377bf6c7817427f28b87db79f3be03287279e8a19bea34cca85be'
$Patched['mvc.07'] = '8b29a492800ec41c9818f1cf880697b298af69c75d0dd6864ed06e65223930eb'
$Patched['mvc.09'] = 'fd0e680e0e1b14a5d1e913ddaef37076dd80203326e305b1fe1f8070be0cd6b2'
$Patched['mvce.03a'] = '9f19ad54717541df9da47ff9de65acd3ea915d87224558c8233e6eaa2722fbbf'
$Patched['mvc.08'] = '402a848e3860f20cdcf3ec80c640f27eaef36f4e7cd78a330913baedcd5356d8'
$Patched['mvc.01'] = '565f2c1befc6dbcd727b65c8954d3263b1e8105cd16be3ff7399c0ff513d144e'
$Patched['mvc.20m'] = '6cb08b23e33cc97bd14c6bf0ed179c99952146328f0dc0f01e8135c2783466bd'
$Patched['mvc.10'] = '5d72d9064628991ec19ed1cb448a11f807b98f097f4a28ba92b8d6ee4711d92f'
$Patched['mvc.17m'] = '18e550f30464af89fb770088ccceeb8ba278456899bd22cc921623886a25d74a'
$Patched['mvc.14m'] = '1be6ba60b3cc0e6b647c2a54ca06b948dbc72eb32efdead84bcaa1d2efd69db5'
$Patched['mvc.13m'] = 'e1ae365c0cf1bd390bc4292676ac945866981b253cb93dafc380f39d10a872a4'
$Patched['mvsc.key'] = '9ba914d01fdb50c562fef4d4a5c7ff4779f92340322272696a46025d33f7155c'
$Patched['mvc.19m'] = '18d37d4f4cef08d110afe6d4386364870fbb44a5ea22c0b8501a4a8a854f3a07'
$Patched['mvc.02'] = 'b460ea0d95c967e5276d0f1f6e0a68d3bcc6008ce7952b970069a211e460ec63'
foreach ($Record in $Vanilla) {
    $Name = [string]$Record.Name
    if (-not $Entries.ContainsKey($Name)) { continue }
    $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) 'mvc1-cross-fever-4p-v2.1-preview.zip'
$Destination = [IO.Path]::GetFullPath($Destination)
if ($Destination -eq [IO.Path]::GetFullPath($Source)) { Fail 'refusing to overwrite the input archive.' }
$ReceiptPath = "$Destination.receipt.json"
$Temporary = "$Destination.part"
$ReceiptTemporary = "$ReceiptPath.part"
foreach ($Path in @($Destination,$ReceiptPath,$Temporary,$ReceiptTemporary)) {
    if (Test-Path -LiteralPath $Path) { Fail "refusing to overwrite existing output: $Path" }
}

$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 order and bytes are authoritative; metadata 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()
}

try {
    $Written = Read-ZipMembers $Temporary
    if ($Written.Order.Count -ne $Order.Count) { throw 'output member count changed during 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 $Entries.ContainsKey($Name)) { continue }
        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." }
        if ((Get-Sha256 $Bytes) -ne $Patched[$Name]) { throw "output SHA-256 check failed for $Name." }
    }
} catch {
    $VerifyError = $_.Exception.Message
    if (Test-Path -LiteralPath $Temporary) { Remove-Item -LiteralPath $Temporary -Force }
    Fail "output verification failed: $VerifyError" 8
}

$InstallerSha = Get-Sha256 ([IO.File]::ReadAllBytes($env:SELFPATH))
$ChangedMembers = @(
    [pscustomobject][ordered]@{ name='mvc.15m'; vanillaSha256='37365288e38a8c41fb5d2eb72808038478def8c8aa368d85816b70a89f6a6f55'; patchedSha256='1b011a70ea37e387900e734ede0eac1440466618a8d45dd72534284627e5391f' }
    [pscustomobject][ordered]@{ name='mvce.03a'; vanillaSha256='48128b6f98c5171a0e734e99f01a273f526f2da74f08635732068c60c891d757'; patchedSha256='9f19ad54717541df9da47ff9de65acd3ea915d87224558c8233e6eaa2722fbbf' }
    [pscustomobject][ordered]@{ name='mvc.10'; vanillaSha256='e24119140931bc861bf791758af8eb375892260d8ffb633369ebedcbe21f172a'; patchedSha256='5d72d9064628991ec19ed1cb448a11f807b98f097f4a28ba92b8d6ee4711d92f' }
    [pscustomobject][ordered]@{ name='mvc.17m'; vanillaSha256='dc29f2f0bf56d60408e9b9a2fcfae6c684f8be97712c94f146386c9bbb9ab46f'; patchedSha256='18e550f30464af89fb770088ccceeb8ba278456899bd22cc921623886a25d74a' }
    [pscustomobject][ordered]@{ name='mvc.13m'; vanillaSha256='689b9f2e2f9105f6b6a456e836a289c65caf642d9c7d246ea95c2db256f2ce0d'; patchedSha256='e1ae365c0cf1bd390bc4292676ac945866981b253cb93dafc380f39d10a872a4' }
    [pscustomobject][ordered]@{ name='mvc.19m'; vanillaSha256='d245405b8fa57b315300d915b9b5f96859bc31b9a97f653e370c7f0f28a1fc7f'; patchedSha256='18d37d4f4cef08d110afe6d4386364870fbb44a5ea22c0b8501a4a8a854f3a07' }
)
$ReceiptPatches = @(
    foreach ($Patch in $Patches) {
        $AfterBytes = ConvertFrom-Hex ([string]$Patch.After)
        [pscustomobject][ordered]@{
            member = [string]$Patch.File
            offset = ('0x{0:X6}' -f [int]$Patch.Offset)
            bytes = $AfterBytes.Length
            afterSha256 = Get-Sha256 $AfterBytes
        }
    }
)
$Receipt = [pscustomobject][ordered]@{
    format = 5
    status = 'installed'
    archivePolicy = 'verified-parent-subset-v1'
    verifiedParentMembers = @($Order)
    ignoredArchiveMembers = @($IgnoredArchiveMembers)
    release = 'MVC1 four-player Cross Fever with player markers'
    version = 'v2.1-preview'
    draft = $true
    installer = [pscustomobject][ordered]@{ name = [IO.Path]::GetFileName($env:SELFPATH); sha256 = $InstallerSha }
    cleanBaseOnly = $true
    romExpansion = $false
    downloadsRom = $false
    networkAccess = $false
    embedsStockBytes = $HasBeforeProof
    inputArchive = [IO.Path]::GetFileName($Source)
    inputArchiveSha256 = Get-Sha256 ([IO.File]::ReadAllBytes($Source))
    outputArchive = [IO.Path]::GetFileName($Destination)
    outputArchiveSha256 = Get-Sha256 ([IO.File]::ReadAllBytes($Temporary))
    changedMembers = $ChangedMembers
    patches = $ReceiptPatches
    patchFingerprint = $Fingerprint
}
try {
    $ReceiptJson = ($Receipt | ConvertTo-Json -Depth 10) + [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 ([string]$ReceiptCheck.status -ne 'installed') { throw 'receipt status mismatch.' }
    if ([string]$ReceiptCheck.patchFingerprint -ne 'd24b6526f637a67cb48edf8ada94626d6e103a9f33a9cac328c59d88ed09908a') { throw 'receipt patch fingerprint mismatch.' }
} catch {
    $ReceiptError = $_.Exception.Message
    foreach ($Path in @($Temporary,$ReceiptTemporary)) {
        if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -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
    foreach ($Path in @($Temporary,$ReceiptTemporary)) {
        if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -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 '  Patched members: mvc.15m, mvce.03a, mvc.10, mvc.17m, mvc.13m, mvc.19m; verified parent member names, sizes, relative order and original key presence are preserved; non-parent members are excluded.'
Write-Host '  The original ROM was not changed.'
Write-Host ''
Write-Host '  NETPLAY / CRC NOTE' -ForegroundColor Yellow
Write-Host '  Both peers need byte-identical patched member data and a room/emulator'
Write-Host '  that accepts modified sets. Official Fightcade support is not claimed.'
Write-Host ''
Write-Host ('  patch fingerprint  ' + $Fingerprint)
Write-Host ''
Pause-IfConsole
exit 0
