"""Click every RIO button the layout claims, and check each one dispatched. Reads a BT_RIOBANK_LOG=1 dump for the rects, posts a real WM_LBUTTONDOWN/UP at each button's centre, then compares the addresses the game LOGGED against the addresses the layout placed. This is the end-to-end check the geometry dump alone cannot give: it proves the hit test, the coordinate mapping and the RIO dispatch agree with the rects. python clickbank.py [--mode surround|exploded] surround: one window, buttons are in backbuffer canvas space (== client space for a windowed run), so the centres go straight to the main window. exploded: one window per bank, each with its own client space; the bank tag names the window. """ import ctypes import re import sys import time from ctypes import wintypes user32 = ctypes.WinDLL("user32", use_last_error=True) WM_LBUTTONDOWN = 0x0201 WM_LBUTTONUP = 0x0202 MK_LBUTTON = 0x0001 BANK_RE = re.compile(r"\[riobank\] (.+?) bounds=") BTN_RE = re.compile(r"\[riobank\] addr=0x([0-9a-f]+) rect=\((-?\d+),(-?\d+),(-?\d+),(-?\d+)\)") def parse(path): banks, current, done = {}, None, set() with open(path, "r", errors="replace") as handle: for line in handle: match = BANK_RE.search(line) if match: current = match.group(1) if current in done: current = None else: done.add(current) banks[current] = [] continue match = BTN_RE.search(line) if match and current is not None: addr = int(match.group(1), 16) x, y, w, h = (int(match.group(i)) for i in range(2, 6)) banks[current].append((addr, x + w // 2, y + h // 2)) return banks def find_windows(substring): found = [] @ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) def callback(hwnd, _): length = user32.GetWindowTextLengthW(hwnd) if length: buffer = ctypes.create_unicode_buffer(length + 1) user32.GetWindowTextW(hwnd, buffer, length + 1) if substring.lower() in buffer.value.lower() and user32.IsWindowVisible(hwnd): # Skip zero/degenerate clients: the game also owns a # "BattleTech - Plasma" window with a 0x0 client, and taking # windows[0] blindly posted every click into it -- 144 clicks, # zero dispatches, and a scary-looking false regression # (2026-07-26). A window you cannot click in is not a target. r = RECT() user32.GetClientRect(hwnd, ctypes.byref(r)) if r.right > 50 and r.bottom > 50: found.append((hwnd, buffer.value)) return True user32.EnumWindows(callback, 0) return found class RECT(ctypes.Structure): _fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long), ("right", ctypes.c_long), ("bottom", ctypes.c_long)] def canvas_size(path): """The cockpit canvas from the boot line, e.g. '[cockpit] view 900x500 canvas 1452x1059'.""" with open(path, "r", errors="replace") as handle: for line in handle: match = re.search(r"\[cockpit\] view \d+x\d+ canvas (\d+)x(\d+)", line) if match: return int(match.group(1)), int(match.group(2)) return None def fit_rect(client_w, client_h, canvas_w, canvas_h): """Same uniform-scale centred fit the game computes (BTCockpitFitRect).""" w = client_w h = client_w * canvas_h // canvas_w if h > client_h: h = client_h w = client_h * canvas_w // canvas_h return ((client_w - w) // 2, (client_h - h) // 2, max(w, 1), max(h, 1)) def click(hwnd, x, y): lparam = (y << 16) | (x & 0xFFFF) user32.PostMessageW(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, lparam) time.sleep(0.02) user32.PostMessageW(hwnd, WM_LBUTTONUP, 0, lparam) time.sleep(0.02) def main(): log = sys.argv[1] title = sys.argv[2] mode = "surround" if "--mode" in sys.argv: mode = sys.argv[sys.argv.index("--mode") + 1] banks = parse(log) if not banks: print("no [riobank] lines in", log) return 1 # exploded: each bank is its OWN window with its own caption, so match # across every visible window rather than filtering by one title windows = find_windows("" if mode == "exploded" else title) if not windows: print("no window matching", title) return 1 # surround: the dump is in CANVAS space; if the window has been resized the # canvas is letterboxed inside the client, so map through the same fit the # game uses or every click lands off its button transform = None if mode == "surround": canvas = canvas_size(log) client = RECT() user32.GetClientRect(windows[0][0], ctypes.byref(client)) if canvas and client.right > 0 and client.bottom > 0: ox, oy, fw, fh = fit_rect(client.right, client.bottom, canvas[0], canvas[1]) if (fw, fh) != canvas: transform = (ox, oy, fw, fh, canvas[0], canvas[1]) print("client %dx%d, canvas %dx%d -> fit %dx%d at (%d,%d)" % (client.right, client.bottom, canvas[0], canvas[1], fw, fh, ox, oy)) posted = set() for tag, buttons in banks.items(): if mode == "surround": target = windows[0][0] else: # the bank tag is the window caption match = [h for h, caption in windows if caption.strip() == tag.strip()] if not match: print(" no window for bank %r -- skipped" % tag) continue target = match[0] for addr, cx, cy in buttons: if transform is not None: ox, oy, fw, fh, cw, ch = transform cx = ox + cx * fw // cw cy = oy + cy * fh // ch click(target, cx, cy) posted.add(addr) print("posted clicks for %d addresses" % len(posted)) print(" ".join("0x%02x" % a for a in sorted(posted))) return 0 if __name__ == "__main__": sys.exit(main())