Enabling gauges crashed (null-this, illegal 0x478). Root cause was NOT video:
the MUNGA main heap (fixed 6MB default) exhausted loading gauge images. The
heap size comes from getenv("HEAPSIZE") (BTL4OPT.EXE @0x401076; default
0x600000); the pod's PARAMETR.BAT sets it (:POD=15MB, :REVIEW/:LOOP=32MB) but
our minimal setenv launch never did. memsize is irrelevant (it's the game's
own heap, not DOS memory -- verified memsize=127 still gave 6MB). Pods have
32MB RAM so 15MB fits.
Fix (no binary patch): set HEAPSIZE=15000000 + L4GAUGE=640x480x16 in
gauge.conf. Game now runs sustained with gauges on (500+ frames); the VESA
640x480x16 mode switches fine on our emulated S3 and the cockpit instrument
panels draw (DISPLAY/PROGRAM/ENG DATA/TRIGGER CONFIG...).
patch_btl4opt.py also gained Verify_Failed (DEBUG-build assertion) neutral-
ization -- release-equivalent robustness, separate from the heap fix.
Known-open (GAUGE-NOTES.md): the framebuffer renders only a top strip,
garbled -- the game bank-switches via a hardcoded far pointer in L4GAUGE.INI
(C000:2616, the STB Horizon+ card's VESA WinFuncPtr) that isn't valid on our
emulated S3, so only the first bank(s) page in. Fixing bank-switching (or an
LFB mode) is next; then the full buffer can be split into the six displays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
74 lines
2.9 KiB
Python
74 lines
2.9 KiB
Python
#!/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()
|