modemwars/swiftlink/checkAbi.py
2026-08-23 02:09:40 -05:00

82 lines
3.3 KiB
Python

#!/usr/bin/env python3
# checkAbi.py - compare a rebuilt opponent module against the stock one and prove that nothing the
# rest of the game depends on has moved.
#
# python3 checkAbi.py build/swiftlinkDriverE000.bin ../disassembly/build/game_modemDriverE000.orig.bin
#
# The game reaches into the module at fixed addresses, and the map generator overlay calls a whole
# block of shared utilities in its top quarter. Those regions must stay byte-identical; the UART
# layer in the middle is what we are allowed to rewrite.
import sys
BASE = 0xE000
# Regions the rest of the game depends on, and why.
FROZEN = [
(0xE000, 0xE017, "jump table: $E000/$E003/$E006/$E009/$E00C/$E012/$E015"),
(0xE01D, 0xE047, "shared variables: packet buffers, host counts, build id, NMI chain, link state"),
(0xEC00, 0xEFFF, "shared utilities the map generator overlay calls ($EC00, $EC0F, $EC1C, $EC2B, "
"$EC54, $EC58, $EC91, $ECF3, $ED43, $EE26, $EE4A, $EEB9, $EEDD)"),
]
# Entry points other code calls, which must still hold a JMP or the routine itself.
ENTRY_POINTS = {
0xE000: "commRequest", 0xE003: "commLinkControl", 0xE006: "getLinkByte", 0xE009: "putLinkByte",
0xE00C: "pollLinkStatus", 0xE012: "keyboardScanHook", 0xE015: "commKeyEntry",
0xE079: "runTrainerRoundHook", 0xE0E8: "trainerUnitStepHook",
0xEC00: "clearBattlefieldMap", 0xEC0F: "initTrainerAi", 0xEC1C: "setViewOriginToHome",
0xEC2B: "loadMapSeedIntoScenarioRng", 0xEC54: "makeMapPointSymmetric",
0xEC58: "rotateMapAndMirrorCodes", 0xEC91: "mirrorAllUnitCoordinates",
0xECF3: "loadUnitStartTemplate", 0xED43: "exchangePlayerNamesAndSettings",
0xEE26: "measureRangeToTarget", 0xEE4A: "loadFilmStartSnapshot",
0xEEB9: "handleDisplayToggleKey", 0xEEDD: "toggleBattleDisplayMode",
}
def changedRanges(new, old):
out = []
start = None
for i in range(len(old)):
if new[i] != old[i]:
if start is None:
start = i
elif start is not None:
out.append((start, i - 1))
start = None
if start is not None:
out.append((start, len(old) - 1))
return out
def main():
new = open(sys.argv[1], "rb").read()
old = open(sys.argv[2], "rb").read()
if len(new) != 4096 or len(old) != 4096:
raise SystemExit("both modules must be 4096 bytes")
failures = []
for lo, hi, why in FROZEN:
a, b = lo - BASE, hi - BASE + 1
if new[a:b] != old[a:b]:
diffs = [f"${BASE+a+i:04X}" for i in range(b - a) if new[a+i] != old[a+i]]
failures.append(f"frozen region ${lo:04X}-${hi:04X} changed ({why}); first: {' '.join(diffs[:8])}")
for addr, name in sorted(ENTRY_POINTS.items()):
if new[addr - BASE] == 0x00:
failures.append(f"entry point ${addr:04X} ({name}) starts with $00 - almost certainly missing")
changed = changedRanges(new, old)
total = sum(hi - lo + 1 for lo, hi in changed)
print(f"bytes changed: {total} of 4096, in {len(changed)} region(s)")
for lo, hi in changed:
print(f" ${BASE+lo:04X}-${BASE+hi:04X} ({hi-lo+1} bytes)")
if failures:
print("\nABI CHECK FAILED:")
for f in failures:
print(" -", f)
return 1
print("\nABI CHECK PASSED: every address the rest of the game depends on is unchanged")
return 0
sys.exit(main())