# Grant 64 patcher for Windows (PowerShell 5.1 or 7; also PowerShell 7 on macOS/Linux). # Run it in the folder with your ROM: # irm grant64.borthwick.dev/apply.ps1 | iex # For a ROM elsewhere, or from scripts and agents (never prompts): # & ([scriptblock]::Create((irm grant64.borthwick.dev/apply.ps1))) -Rom 'D:\roms\sm64.z64' -Out 'D:\roms\grant64.z64' # It finds your own Super Mario 64 (USA) ROM (any byte order: here, then in # Downloads, Desktop and Documents, else it asks), downloads the # Grant 64 patch from this site, checks every hash and writes grant64.0.75.z64. # It never uploads anything and never changes your ROM. param([string]$Rom = $env:GRANT64_ROM, [string]$Out = $env:GRANT64_OUT) function Invoke-Grant64Patch([string]$Rom, [string]$Out) { $Version = '0.75' $PatchUrl = 'https://grant64.borthwick.dev/grant64.0.75.bps' $PatchSha1 = '4e11a8cc468b7e8e968df48dd6c215f1b94d0431' $BaseSha1 = '9bef1128717f958171a4afac3ed78ee2bb4e86ce' $OutSha1 = '22ed670d5bf49572e74180cb192cc61ba9741578' if (-not ('Grant64Patcher1' -as [type])) { Add-Type -Language CSharp -TypeDefinition @' public static class Grant64Patcher1 { // .z64 is big-endian; .v64 swaps byte pairs, .n64 reverses each 32-bit word. public static byte[] ToZ64(byte[] d) { byte[] r = (byte[])d.Clone(); if (d.Length >= 4 && d[0] == 0x37 && d[1] == 0x80) { for (int i = 0; i + 1 < r.Length; i += 2) { byte t = r[i]; r[i] = r[i + 1]; r[i + 1] = t; } } else if (d.Length >= 4 && d[0] == 0x40 && d[1] == 0x12) { for (int i = 0; i + 3 < r.Length; i += 4) { System.Array.Reverse(r, i, 4); } } return r; } // The first 64 bytes, in any byte order: "z64", "v64" or "n64" when the header says // Super Mario 64 (USA) (title SUPER MARIO 64, game code NSME), else "no: ". // Reads only the header, so other files stay unread. public static string Header(string path) { byte[] h = new byte[64]; try { using (var f = System.IO.File.OpenRead(path)) { if (f.Read(h, 0, 64) != 64) return "no: too small"; } } catch { return "no: cannot read it"; } string order; if (h[0] == 0x80 && h[1] == 0x37 && h[2] == 0x12 && h[3] == 0x40) order = "z64"; else if (h[0] == 0x37 && h[1] == 0x80 && h[2] == 0x40 && h[3] == 0x12) order = "v64"; else if (h[0] == 0x40 && h[1] == 0x12 && h[2] == 0x37 && h[3] == 0x80) order = "n64"; else return "no: not an N64 ROM"; h = ToZ64(h); var title = new System.Text.StringBuilder(); for (int i = 0x20; i < 0x34; i++) if (h[i] >= 0x20 && h[i] < 0x7f) title.Append((char)h[i]); var code = new System.Text.StringBuilder(); for (int i = 0x3B; i < 0x3F; i++) code.Append(h[i] >= 0x20 && h[i] < 0x7f ? (char)h[i] : '?'); string t = title.ToString().TrimEnd(), c = code.ToString(); if (t == "SUPER MARIO 64" && c == "NSME") return order; return "no: header says \"" + t + "\" (" + c + ")"; } static long Num(byte[] p, ref int pos) { long data = 0, shift = 1; while (true) { byte x = p[pos++]; data += (x & 0x7f) * shift; if ((x & 0x80) != 0) return data; shift <<= 7; data += shift; } } public static byte[] Apply(byte[] src, byte[] p) { if (p.Length < 16 || p[0] != 'B' || p[1] != 'P' || p[2] != 'S' || p[3] != '1') throw new System.Exception("not a BPS patch"); int pos = 4; long srcSize = Num(p, ref pos), tgtSize = Num(p, ref pos); int meta = (int)Num(p, ref pos); // not `pos += Num(...)`: C# reads pos before the call pos += meta; if (srcSize != src.Length) throw new System.Exception("wrong base ROM size"); byte[] t = new byte[tgtSize]; long o = 0, sr = 0, tr = 0; int end = p.Length - 12; while (pos < end) { long a = Num(p, ref pos); int n = (int)((a >> 2) + 1); switch ((int)(a & 3)) { case 0: System.Array.Copy(src, o, t, o, n); break; case 1: System.Array.Copy(p, pos, t, o, n); pos += n; break; default: { long d = Num(p, ref pos); d = (d & 1) != 0 ? -(d >> 1) : (d >> 1); if ((a & 3) == 2) { sr += d; System.Array.Copy(src, sr, t, o, n); sr += n; } else { tr += d; for (int k = 0; k < n; k++) t[o + k] = t[tr + k]; tr += n; } break; } } o += n; } if (o != tgtSize) throw new System.Exception("patch ended early"); return t; } } '@ } $sha = [System.Security.Cryptography.SHA1]::Create() function Sha1Hex([byte[]]$b) { ([System.BitConverter]::ToString($sha.ComputeHash($b)) -replace '-', '').ToLowerInvariant() } function Log([string]$m) { Write-Host "grant64: $m" } Log "Grant 64 $Version patcher (PowerShell). It reads your ROM, downloads the patch from" Log "grant64.borthwick.dev and writes one new file. Your ROM is never changed or uploaded." # Check one file, logging why it is or isn't the base ROM; returns its .z64 bytes or $null. function Test-Rom([string]$file) { if ((Split-Path -Leaf $file) -like 'grant64*') { Log " skip $file (a Grant 64 output)"; return $null } $h = [Grant64Patcher1]::Header($file) if ($h.StartsWith('no:')) { Log " skip $file ($($h.Substring(4)))"; return $null } Log " check ${file}: header says Super Mario 64 (USA), $h byte order; hashing all of it" try { $b = [Grant64Patcher1]::ToZ64([System.IO.File]::ReadAllBytes($file)) } catch { Log " skip $file (cannot read it)"; return $null } $got = Sha1Hex $b if ($got -eq $BaseSha1) { Log " match: SHA-1 $got is the stock US ROM"; return ,$b } Log " skip $file (SHA-1 $got is not the stock US ROM: a different revision, a hack or a bad dump)" return $null } $notStock = "is not the stock Super Mario 64 (USA) ROM (expected SHA-1 $BaseSha1 after byte-order conversion). Use a dump of the US cartridge." $base = $null if ($Rom) { Log 'checking the ROM you gave' $path = (Resolve-Path -LiteralPath $Rom -ErrorAction SilentlyContinue) if (-not $path -or -not (Test-Path -LiteralPath $path.ProviderPath -PathType Leaf)) { Write-Error "grant64: ERROR: no such file: $Rom"; return } $Rom = $path.ProviderPath $base = Test-Rom $Rom if (-not $base) { Write-Error "grant64: ERROR: $Rom $notStock"; return } } else { # Not given: look here, then in the usual places, then ask (only when someone is at the keyboard). $here = (Get-Location).Path $places = @($here) + @('Downloads', 'Desktop', 'Documents' | ForEach-Object { Join-Path $HOME $_ }) + @($HOME) $seen = @{} foreach ($place in $places) { if (-not (Test-Path -LiteralPath $place -PathType Container)) { Log "no folder $place, skipping"; continue } $depth = if ($place -eq $here) { 0 } else { 3 } Log "looking for your ROM in $place (8 MB .z64/.v64/.n64 files, $($depth + 1) folder level(s))" $files = @(Get-ChildItem -LiteralPath $place -File -Recurse -Depth $depth -ErrorAction SilentlyContinue | Where-Object { $_.Extension -match '^\.(z64|n64|v64)$' -and $_.Length -eq 8388608 }) if (-not $files) { Log ' nothing there'; continue } foreach ($f in $files) { if ($seen.ContainsKey($f.FullName)) { continue } # home overlaps Downloads etc. $seen[$f.FullName] = $true $base = Test-Rom $f.FullName if ($base) { $Rom = $f.FullName; break } } if ($base) { break } } # -NonInteractive (agents, CI) and redirected input never prompt. $interactive = [Environment]::UserInteractive -and -not [Console]::IsInputRedirected -and $Host.Name -eq 'ConsoleHost' -and -not ([Environment]::GetCommandLineArgs() -match '^-noni') if (-not $base -and $interactive) { Log "couldn't find it, so please pick it" $pick = $null if ($IsWindows -ne $false -and [System.Threading.Thread]::CurrentThread.GetApartmentState() -eq 'STA') { try { Add-Type -AssemblyName System.Windows.Forms $dlg = New-Object System.Windows.Forms.OpenFileDialog $dlg.Title = 'Pick your Super Mario 64 (USA) ROM' $dlg.Filter = 'N64 ROM (*.z64;*.n64;*.v64)|*.z64;*.n64;*.v64|All files (*.*)|*.*' Log 'opening a file picker (it may open behind this window)' if ($dlg.ShowDialog() -eq 'OK') { $pick = $dlg.FileName } } catch { $pick = $null } } if (-not $pick) { $pick = (Read-Host 'grant64: drag the ROM file here (or type its path) and press Enter').Trim().Trim('"', "'") } if (-not $pick) { Write-Error 'grant64: ERROR: no ROM picked'; return } if (-not (Test-Path -LiteralPath $pick -PathType Leaf)) { Write-Error "grant64: ERROR: no such file: $pick"; return } $Rom = (Resolve-Path -LiteralPath $pick).ProviderPath $base = Test-Rom $Rom if (-not $base) { Write-Error "grant64: ERROR: $Rom $notStock"; return } } if (-not $base) { Write-Error "grant64: ERROR: no Super Mario 64 (USA) ROM in $here, Downloads, Desktop, Documents or your home folder. Pass -Rom FILE. Expected SHA-1 $BaseSha1 (.z64, .v64 and .n64 all work)."; return } } Log "using $Rom" if (-not $Out) { $Out = Join-Path (Split-Path -Parent $Rom) "grant64.$Version.z64" } $Out = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Out) if ($Out -eq $Rom) { Write-Error "grant64: ERROR: -Out must not be your ROM itself: pick another file name"; return } if (Test-Path -LiteralPath $Out) { Log "$Out already exists; it will be replaced" } Log "downloading the patch: $PatchUrl" try { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 $patch = (Invoke-WebRequest -UseBasicParsing -Uri $PatchUrl).Content if ($patch -is [string]) { $patch = [System.Text.Encoding]::GetEncoding(28591).GetBytes($patch) } } catch { Write-Error "grant64: ERROR: download failed: $PatchUrl ($($_.Exception.Message))"; return } $got = Sha1Hex $patch if ($got -ne $PatchSha1) { Write-Error "grant64: ERROR: the downloaded patch is damaged (SHA-1 $got, expected $PatchSha1). Try again."; return } Log " got $($patch.Length) bytes, SHA-1 $got as published" Log 'applying the patch (in memory)' try { $result = [Grant64Patcher1]::Apply($base, $patch) } catch { Write-Error "grant64: ERROR: patching failed: $($_.Exception.Message)"; return } $got = Sha1Hex $result if ($got -ne $OutSha1) { Write-Error "grant64: ERROR: the patched ROM came out wrong (SHA-1 $got, expected $OutSha1). Nothing was written."; return } Log " result SHA-1 $got matches Grant 64 $Version" try { [System.IO.File]::WriteAllBytes($Out, $result) } catch { Write-Error "grant64: ERROR: cannot write ${Out}: $($_.Exception.Message)"; return } Log "OK wrote $Out" Log "SHA-1 $OutSha1 (Grant 64 $Version). Play it in an N64 emulator or on a flash cart." $script:Grant64Ok = $true } # A function, not top-level code, so `irm | iex` never runs `exit` in the caller's window. $script:Grant64Ok = $false Invoke-Grant64Patch -Rom $Rom -Out $Out # Run as a file (-File, CI): a failure exits 1. Under iex there is no script path, so no exit. if (-not $script:Grant64Ok -and $MyInvocation.MyCommand.Path) { exit 1 }