@echo off
rem ===========================================================================
rem  999-HIT COUNTER v1 -- Marvel vs. Capcom (CPS-2, mvsc)
rem
rem  DRAG mvsc.zip ONTO THIS FILE
rem       or run:  MVSC-999-HIT-COUNTER-v1.cmd "C:\path\mvsc.zip"
rem
rem  Input must be the dat-official Euro parent: the 21 reference members
rem  that MAME and FBNeo ship in their DATs, matched by name, size, and
rem  SHA-256 in any archive order. Writes mvsc_999_hit_counter_v1.zip beside
rem  the input. The original is never modified. This installer contains
rem  patch bytes and hashes only; it does not contain or download ROM data.
rem ===========================================================================

setlocal
set "ROMPATH=%~1"
set "SELFPATH=%~f0"
where powershell.exe >nul 2>&1
if errorlevel 1 (
  echo ERROR: Windows PowerShell was not found.
  pause
  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:MVSC999_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=[Security.Cryptography.SHA256]::Create()
    try { return ([BitConverter]::ToString($Hash.ComputeHash($Bytes))).Replace('-','').ToLowerInvariant() }
    finally { $Hash.Dispose() }
}
function Get-FileMd5([string]$Path) {
    $Hash=[Security.Cryptography.MD5]::Create(); $Stream=[IO.File]::OpenRead($Path)
    try { return ([BitConverter]::ToString($Hash.ComputeHash($Stream))).Replace('-','').ToLowerInvariant() }
    finally { $Stream.Dispose(); $Hash.Dispose() }
}
function Get-FileSha256([string]$Path) {
    $Hash=[Security.Cryptography.SHA256]::Create(); $Stream=[IO.File]::OpenRead($Path)
    try { return ([BitConverter]::ToString($Hash.ComputeHash($Stream))).Replace('-','').ToLowerInvariant() }
    finally { $Stream.Dispose(); $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-Zip([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=[IO.Compression.ZipFile]::OpenRead($Path) }
    catch { throw "not a readable ZIP archive: $Path" }
    try {
        foreach($Entry in $Zip.Entries) {
            if($Entries.ContainsKey($Entry.FullName)){throw "duplicate ZIP member '$($Entry.FullName)'"}
            $Memory=New-Object 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 '  MARVEL VS. CAPCOM -- 999-HIT COUNTER v1'
Write-Host '  ================================================================'
Write-Host '  Displays 0-999 HIT while preserving Capcom''s native 99-hit gameplay cap.'
Write-Host '  No ROM expansion: same 21 dat-official members, same member sizes.'

$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 your clean mvsc.zip onto this file, or pass it on the command line.'}
try{$Source=(Resolve-Path -LiteralPath $Source -ErrorAction Stop).ProviderPath}catch{Fail "file not found: $Source"}

Add-Type -AssemblyName System.IO.Compression -ErrorAction SilentlyContinue
Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue

$Vanilla=@(
@{Name='mvce.04a';Size=524288;Sha='71ded7040f279d0ddea1326fc062465159ccd832a327230098e5740d2a747d67'},
@{Name='mvc.06a';Size=524288;Sha='132316e99d02d197cd8b4b9227dccd9215f5f741bcd21d5abf5c3f07fb64354b'},
@{Name='mvc.05a';Size=524288;Sha='1dfd50972c8796624051857e16ddc2296932c39fb05ecfcd1bae6180a369285d'},
@{Name='mvc.12m';Size=4194304;Sha='971b949ca3ad2471bf5ddc5c87c10286eacba51c30b4cc2261328ec46732b920'},
@{Name='mvc.18m';Size=4194304;Sha='4f3ceefa11958fd92f88047f876e37a5a9a226ca9c28e3b9915a03f39a268947'},
@{Name='mvc.11m';Size=4194304;Sha='5587602fb97953d0cd951392b3bbaef2d317ae943ff2f6608c46cb627b7b16ca'},
@{Name='mvc.15m';Size=4194304;Sha='37365288e38a8c41fb5d2eb72808038478def8c8aa368d85816b70a89f6a6f55'},
@{Name='mvc.16m';Size=4194304;Sha='c94231770a7377bf6c7817427f28b87db79f3be03287279e8a19bea34cca85be'},
@{Name='mvc.07';Size=524288;Sha='8b29a492800ec41c9818f1cf880697b298af69c75d0dd6864ed06e65223930eb'},
@{Name='mvc.09';Size=524288;Sha='fd0e680e0e1b14a5d1e913ddaef37076dd80203326e305b1fe1f8070be0cd6b2'},
@{Name='mvce.03a';Size=524288;Sha='48128b6f98c5171a0e734e99f01a273f526f2da74f08635732068c60c891d757'},
@{Name='mvc.08';Size=524288;Sha='402a848e3860f20cdcf3ec80c640f27eaef36f4e7cd78a330913baedcd5356d8'},
@{Name='mvc.01';Size=131072;Sha='565f2c1befc6dbcd727b65c8954d3263b1e8105cd16be3ff7399c0ff513d144e'},
@{Name='mvc.20m';Size=4194304;Sha='6cb08b23e33cc97bd14c6bf0ed179c99952146328f0dc0f01e8135c2783466bd'},
@{Name='mvc.10';Size=524288;Sha='e24119140931bc861bf791758af8eb375892260d8ffb633369ebedcbe21f172a'},
@{Name='mvc.17m';Size=4194304;Sha='dc29f2f0bf56d60408e9b9a2fcfae6c684f8be97712c94f146386c9bbb9ab46f'},
@{Name='mvc.14m';Size=4194304;Sha='1be6ba60b3cc0e6b647c2a54ca06b948dbc72eb32efdead84bcaa1d2efd69db5'},
@{Name='mvc.13m';Size=4194304;Sha='689b9f2e2f9105f6b6a456e836a289c65caf642d9c7d246ea95c2db256f2ce0d'},
@{Name='mvsc.key';Size=20;Sha='9ba914d01fdb50c562fef4d4a5c7ff4779f92340322272696a46025d33f7155c'},
@{Name='mvc.19m';Size=4194304;Sha='d245405b8fa57b315300d915b9b5f96859bc31b9a97f653e370c7f0f28a1fc7f'},
@{Name='mvc.02';Size=131072;Sha='b460ea0d95c967e5276d0f1f6e0a68d3bcc6008ce7952b970069a211e460ec63'}
)

try{$Read=Read-Zip $Source}catch{Fail $_.Exception.Message}
$Entries=$Read.Entries;$Metadata=$Read.Metadata;$Order=$Read.Order

$Required=@{}
foreach($R in $Vanilla){$Required[$R.Name]=$R}
$Extras=New-Object 'System.Collections.Generic.List[string]'
foreach($Name in $Order){
    if(-not $Required.ContainsKey($Name)){[void]$Extras.Add($Name);continue}
    $R=$Required[$Name]
    $B=[byte[]]$Entries[$Name]
    if($B.Length -ne $R.Size){Fail "$Name size mismatch. Start from the dat-official Euro parent mvsc.zip (MAME/FBNeo reference set)."}
    $Got=Get-Sha256 $B
    if($Got -ne $R.Sha){Fail "$Name SHA-256 mismatch. Start from the dat-official Euro parent mvsc.zip (MAME/FBNeo reference set)."}
}
if($Extras.Count -gt 0){Fail ("unexpected extra members: {0}. Clean dat-official parent only; merged, stacked, or phoenix sets are rejected." -f ($Extras -join ', '))}
$Missing=New-Object 'System.Collections.Generic.List[string]'
foreach($R in $Vanilla){ if(-not $Entries.ContainsKey($R.Name)){[void]$Missing.Add($R.Name)} }
if($Missing.Count -gt 0){Fail ("missing members: {0}." -f ($Missing -join ', '))}

$Patches=@(
@{File='mvce.04a';Offset=0x03C5BA;Before='e3733fb75f8e8137af3856160fd08ed5c1a82eddfd9b';After='2e73ab522f7402dd4d507b6cefb3ead66eef4b04f285';Purpose='encrypted 16-bit packed-BCD render hook'},
@{File='mvce.04a';Offset=0x03C684;Before='a8260994d1a9132b0540ea9a';After='3f859a224afe6c17ee2ff9c6';Purpose='encrypted hundreds-sprite insertion hook'},
@{File='mvce.03a';Offset=0x01D18E;Before='8f0afa24ca32343503f5bcebaa305f6f';After='8d4159c3502493db2e97e86a82f55ca7';Purpose='encrypted increment/display-shadow hook'},
@{File='mvc.10';Offset=0x071000;Before='ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';After='012ff943ff00207f284a02000667f943ff00227f007228122001014a026651420070113041b0026401304052400ce70304633c30e7038032400c6300026363701f22f94e01009ed1092ff943ff00207f2c4a02000667f943ff00227f007011305f22400ce70304633c30e703b84e6219f94e0b00d0c50a7098429842c851faffe748f0c0f943ff00207f2c4a02000667f943ff00227f1130400c64006c656e203400e8432800e845300009722125c951fcff28320800813068310a0002002c4a02001466e8430800047251062000e9430800c951f6ff046050042000fc80640040020f0040d0f9470b0090c67331000004007c311f1106004048400c0a000a6453310c007c311f110e00df4c030f754e';Purpose='272-byte 68000 payload in stock mvc.10 FF tail'}
)
foreach($P in $Patches){
    $Buffer=[byte[]]$Entries[$P.File];$Before=ConvertFrom-Hex $P.Before;$After=ConvertFrom-Hex $P.After
    if($Before.Length -ne $After.Length){Fail 'internal patch length mismatch.' 8}
    if(-not(Test-Bytes $Buffer $P.Offset $Before)){Fail ("{0} stock-byte proof failed at +0x{1:X6}; nothing written." -f $P.File,$P.Offset)}
    [Array]::Copy($After,0,$Buffer,$P.Offset,$After.Length)
    Write-Host ("  patch : {0} +0x{1:X6} ({2} bytes) -- {3}" -f $P.File,$P.Offset,$After.Length,$P.Purpose)
}

$Patched=New-Object 'System.Collections.Generic.Dictionary[string,string]'
foreach($R in $Vanilla){$Patched[$R.Name]=$R.Sha}
$Patched['mvc.10']='7cc249a037732587efc33639364473ef805bafe3d0c199c09fb75be8a6c83184'
$Patched['mvce.03a']='88093767524214a39e3357ea9c6e7c004f98b5fbcc921b6d138a2e6254806541'
$Patched['mvce.04a']='745f7d060a9ab6296f14899f783b76b518f6d5382e196022d2dedecca425c2ba'
foreach($Name in $Order){$Got=Get-Sha256([byte[]]$Entries[$Name]);if($Got -ne $Patched[$Name]){Fail "internal patched hash mismatch: $Name" 8}}

$Directory=Split-Path -Parent $Source
$Destination=Join-Path $Directory 'mvsc_999_hit_counter_v1.zip'
$Receipt=Join-Path $Directory 'mvsc_999_hit_counter_v1.receipt.json'
$Temporary=$Destination+'.part'
if(Test-Path -LiteralPath $Destination){Fail "refusing to overwrite existing output: $Destination"}
if(Test-Path -LiteralPath $Receipt){Fail "refusing to overwrite existing receipt: $Receipt"}
if(Test-Path -LiteralPath $Temporary){Fail "stale temporary output exists: $Temporary"}

try{
    $File=[IO.File]::Open($Temporary,[IO.FileMode]::CreateNew,[IO.FileAccess]::ReadWrite,[IO.FileShare]::None)
    try{
        $Zip=New-Object IO.Compression.ZipArchive($File,[IO.Compression.ZipArchiveMode]::Create,$true)
        try{
            foreach($Name in $Order){
                $Entry=$Zip.CreateEntry($Name,[IO.Compression.CompressionLevel]::Optimal)
                $Entry.LastWriteTime=$Metadata[$Name].LastWriteTime;$Entry.ExternalAttributes=$Metadata[$Name].ExternalAttributes
                $Stream=$Entry.Open();try{$Bytes=[byte[]]$Entries[$Name];$Stream.Write($Bytes,0,$Bytes.Length)}finally{$Stream.Dispose()}
            }
        }finally{$Zip.Dispose()}
    }finally{$File.Dispose()}
    [IO.File]::Move($Temporary,$Destination)
}catch{if(Test-Path -LiteralPath $Temporary){Remove-Item -LiteralPath $Temporary -Force};Fail $_.Exception.Message}

try{$Check=Read-Zip $Destination}catch{Fail $_.Exception.Message}
if($Check.Order.Count -ne $Order.Count){Fail 'output member count changed.' 8}
for($i=0;$i -lt $Order.Count;$i++){
    $Name=$Order[$i]
    if($Check.Order[$i] -cne $Name){Fail 'output member order changed.' 8}
    $Bytes=[byte[]]$Check.Entries[$Name]
    if($Bytes.Length -ne $Entries[$Name].Length){Fail "output member size changed: $Name" 8}
    if((Get-Sha256 $Bytes) -ne $Patched[$Name]){Fail "output verification failed: $Name" 8}
}

$ReceiptObject=[ordered]@{
    schema=1;kind='mvsc_999_hit_counter_windows_install_receipt';status='PASS_INSTALL'
    feature='mvsc-no-expansion-999-hit-counter-v1';clean_parent_only=$true;rom_expansion=$false
    vanilla_definition='21 dat-official members (MAME/FBNeo reference), matched by name+size+SHA-256 in any order'
    input=[ordered]@{path=$Source;member_count=$Order.Count}
    output=[ordered]@{path=$Destination;md5=(Get-FileMd5 $Destination);sha256=(Get-FileSha256 $Destination);member_count=$Order.Count;all_member_sizes_preserved=$true}
    changed_members=@('mvc.10','mvce.03a','mvce.04a')
    native_gameplay_counter='fighter+0x120 remains byte-capped at 99'
    display_shadow='0xFF7F20/0xFF7F22 words, display-only, saturated at 999'
    hundreds_layout='tens zero forced for 100-109/200-209/...; <=99 rendering remains byte-for-byte stock'
    patch_count=$Patches.Count
}
[IO.File]::WriteAllText($Receipt,($ReceiptObject|ConvertTo-Json -Depth 6)+[Environment]::NewLine,(New-Object Text.UTF8Encoding($false)))

Write-Host ''
Write-Host '  PASS -- new no-expansion ROM written:' -ForegroundColor Green
Write-Host "  $Destination"
Write-Host "  MD5    $($ReceiptObject.output.md5)"
Write-Host "  SHA256 $($ReceiptObject.output.sha256)"
Write-Host '  Fightcade peers must use byte-identical patched member data;'
Write-Host '  the three patched member SHA-256 values in the receipt are authoritative.'
Pause-IfConsole
exit 0
