82 lines
3.3 KiB
Python
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())
|