"""Local PC/Steam/Proton control backend (xdotool/scrot wrappers).""" import os import subprocess import time import cv2 from ba_auto import config PROBE_SHOT_PATH = os.path.join(config.SCRATCHPAD_DIR, "probe.png") def run_command(args, **kwargs): kwargs.setdefault("env", config.ENV) kwargs.setdefault("check", True) return subprocess.run(args, **kwargs) def focus_game(): result = run_command( ["xdotool", "search", "--name", config.WINDOW_NAME], check=False, capture_output=True, text=True, ) window_ids = result.stdout.split() if not window_ids: raise RuntimeError("Blue Archive window not found. Is the game running?") run_command(["xdotool", "windowactivate", window_ids[0]]) # windowactivate alone isn't enough -- confirmed live (twice now, see # Handoff.md's original finding and plan.md's Bounty phase for a second # occurrence) that a stray anti-cheat XIGNCODE window can render # visibly on top of the game and intercept clicks at fixed positions # (notably the shared BACK_BUTTON coordinate) even while xdotool still # reports BlueArchive as the "active" window -- windowactivate changes # input focus, not stacking order, so it doesn't fix this by itself. # windowraise does. Cheap and harmless when no overlay is present. run_command(["xdotool", "windowraise", window_ids[0]]) wait(0.5) def click(x, y): # A combined "mousemove X Y click 1" invocation is unreliable here -- # empirically (see plan.md's click-flakiness writeups) the game's input # handler sometimes misses clicks fired before it's processed the move. # Splitting into separate commands with a short pause between fixes it. run_command(["xdotool", "mousemove", str(x), str(y)]) wait(0.2) run_command(["xdotool", "click", "1"]) wait(0.5) def move_mouse(x, y): run_command(["xdotool", "mousemove", str(x), str(y)]) def drag(start_x, start_y, end_x, end_y, duration=0.6, steps=12): """Click-and-drag gesture: mousedown at (start_x, start_y), move to (end_x, end_y) over `duration` seconds in `steps` increments, mouseup. Distinct from scroll()'s wheel-based gesture -- scroll()'s own comment already documents that a plain drag does NOT register as a list-scroll gesture in this Proton client, but that finding was specific to scrollable list widgets (mailbox/lesson/bounty/etc.); a room-view camera (e.g. cafe.py's horizontal pan) is a different UI surface, not a list, and needs an actual drag rather than a wheel click. """ run_command(["xdotool", "mousemove", str(start_x), str(start_y)]) wait(0.2) run_command(["xdotool", "mousedown", "1"]) step_delay = duration / steps for i in range(1, steps + 1): x = start_x + (end_x - start_x) * i // steps y = start_y + (end_y - start_y) * i // steps run_command(["xdotool", "mousemove", str(x), str(y)]) wait(step_delay) run_command(["xdotool", "mouseup", "1"]) wait(0.3) def scroll(x, y, direction, clicks=1): # xdotool button 4/5 = scroll wheel up/down. A drag (mousedown/move/mouseup) # does not register as a list-scroll gesture in this Proton client; the # wheel does. button = "4" if direction == "up" else "5" run_command(["xdotool", "mousemove", str(x), str(y)]) wait(0.2) for _ in range(clicks): run_command(["xdotool", "click", button]) wait(0.15) wait(0.3) def keypress(key): run_command(["xdotool", "key", key]) wait(0.5) def screenshot(path): run_command(["scrot", "-a", "0,0,1920,1200", "-o", path]) def color_at(x, y): screenshot(PROBE_SHOT_PATH) image = cv2.imread(PROBE_SHOT_PATH) b, g, r = image[y, x] return int(r), int(g), int(b) def wait(seconds): time.sleep(seconds)