#!/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())