#!/usr/bin/env python3
"""Neutralize the shipped-in debug crash macros in BTL4OPT.EXE.

The Rel 4.10 BattleTech binary is a DEBUG build (DEBUG_LEVEL 1; the binary
even contains "// this file has not been approved for release"). Two families
of deliberate "crash to the debugger" macros are compiled in, both faulting
by writing to 0xFFFFFFFF (Exception 0E). On the real pod a clean cockpit
harness rarely tripped them; through emulation + serial passthrough they fire
on benign hiccups and halt the whole game.

1. DISABLE_AND_DIE -- the RIO serial driver's `DIE_ON_ERROR equ 1` macro
   (CODE/RP/MUNGA_L4/PCSPAK.ASM):
       push eax; push edx
       mov edx, 0FFFFFFFFh          ; crash loudly
       mov eax, <error-code>
       mov [edx], eax
   e.g. error 3 = a RIO packet byte > 0x7F (PCSPAK.ASM:1630), tripped by a
   serial glitch during a board reset. Release build (DIE_ON_ERROR equ 0)
   compiles it out and recovers (`and al,07Fh`).

2. Verify_Failed / Fail_To_Debugger -- the general C++ assertion routines
   (CODE/RP/MUNGA/VERIFY.HPP; Verify(c) -> Verify_Failed on !c). After
   printing message/file/line they crash:
       or edx, 0FFFFFFFFh
       xor ecx, ecx
       mov [edx], ecx
   The routine's own re-entry guard already jumps *past* this to the clean
   pop/ret, so NOPing the crash makes every failed assertion "print and
   continue" -- the DEBUGOFF behavior. (Failing gauge-init Verifys were
   halting the game before the secondary displays could come up.)

This tool finds every site of both idioms by exact signature and NOPs them,
writing a <name>.orig backup first. Idempotent.

Usage: patch_btl4opt.py <path-to-BTL4OPT.EXE>
"""
import os
import re
import sys

DIE = re.compile(
    rb'\x50\x52\xBA\xFF\xFF\xFF\xFF\xB8(.)\x00\x00\x00\x89\x02', re.DOTALL)
VERIFY = re.compile(rb'\x83\xCA\xFF\x33\xC9\x89\x0A')   # or edx,-1;xor ecx;mov[edx],ecx


def main():
    if len(sys.argv) != 2:
        raise SystemExit(__doc__)
    path = sys.argv[1]
    data = bytearray(open(path, 'rb').read())
    die = list(DIE.finditer(bytes(data)))
    ver = list(VERIFY.finditer(bytes(data)))
    if not die and not ver:
        print("no crash sites found (already patched?)")
        return
    bak = path + '.orig'
    if not os.path.exists(bak):
        open(bak, 'wb').write(data)
        print(f"backup written: {bak}")
    for m in die:
        va = 0x401000 + m.start() - 0xe00   # CODE VA 0x401000 @ file 0xe00
        print(f"  NOP  VA 0x{va:06x}  DISABLE_AND_DIE (code {m.group(1)[0]})")
        data[m.start():m.end()] = b'\x90' * (m.end() - m.start())
    for m in ver:
        va = 0x401000 + m.start() - 0xe00
        print(f"  NOP  VA 0x{va:06x}  Verify_Failed crash tail")
        data[m.start():m.end()] = b'\x90' * (m.end() - m.start())
    open(path, 'wb').write(data)
    print(f"patched {len(die)} DISABLE_AND_DIE + {len(ver)} Verify sites")


if __name__ == "__main__":
    main()
