#!/usr/bin/env python3 """ Timmy's Morrowind Console Bridge. Sends Lua commands to OpenMW via the in-game console. Takes screenshots and OCRs them for perception. """ import subprocess, time, os, shutil import Quartz, CoreFoundation SESSION_DIR = os.path.expanduser(f"~/.timmy/morrowind/session_{time.strftime('%Y%m%d_%H%M')}") os.makedirs(SESSION_DIR, exist_ok=True) frame_count = 0 def send_keys_to_openmw(keycode): """Send a single key code to OpenMW via System Events.""" subprocess.run(['osascript', '-e', f''' tell application "System Events" tell process "openmw" key code {keycode} end tell end tell '''], capture_output=True, timeout=3) def send_char_to_openmw(char): """Send a character keystroke to OpenMW.""" subprocess.run(['osascript', '-e', f''' tell application "System Events" tell process "openmw" keystroke "{char}" end tell end tell '''], capture_output=True, timeout=3) def send_text_to_openmw(text): """Type a string into OpenMW.""" # Escape special chars for AppleScript escaped = text.replace('\\', '\\\\').replace('"', '\\"') subprocess.run(['osascript', '-e', f''' tell application "System Events" tell process "openmw" keystroke "{escaped}" end tell end tell '''], capture_output=True, timeout=5) # Key codes BACKTICK = 50 # ` to open/close console RETURN = 36 ESCAPE = 53 SPACE = 49 def open_console(): send_keys_to_openmw(BACKTICK) time.sleep(0.4) def close_console(): send_keys_to_openmw(BACKTICK) time.sleep(0.3) def console_command(cmd): """Open console, type a command, execute, close console.""" open_console() time.sleep(0.3) send_text_to_openmw(cmd) time.sleep(0.2) send_keys_to_openmw(RETURN) time.sleep(0.3) close_console() time.sleep(0.2) def lua_player(code): """Run Lua code in player context.""" console_command(f"luap {code}") def lua_global(code): """Run Lua code in global context.""" console_command(f"luag {code}") def timmy_event(event_name, data_str="{}"): """Send a Timmy event via global script.""" lua_global(f'core.sendGlobalEvent("{event_name}", {data_str})') def screenshot(label=""): """Take a screenshot and save it to the session directory.""" global frame_count frame_count += 1 image = Quartz.CGDisplayCreateImage(Quartz.CGMainDisplayID()) if not image: return None fname = f"frame_{frame_count:04d}" if label: fname += f"_{label}" fname += ".png" path = os.path.join(SESSION_DIR, fname) url = CoreFoundation.CFURLCreateWithFileSystemPath(None, path, 0, False) dest = Quartz.CGImageDestinationCreateWithURL(url, 'public.png', 1, None) Quartz.CGImageDestinationAddImage(dest, image, None) Quartz.CGImageDestinationFinalize(dest) return path def ocr_screenshot(path): """OCR a screenshot using macOS Vision.""" from Foundation import NSURL from Quartz import CIImage import Vision url = NSURL.fileURLWithPath_(path) ci = CIImage.imageWithContentsOfURL_(url) if not ci: return [] req = Vision.VNRecognizeTextRequest.alloc().init() req.setRecognitionLevel_(1) handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(ci, None) handler.performRequests_error_([req], None) return [r.text() for r in (req.results() or [])] def read_log(n=10): """Read last N lines of OpenMW log.""" log_path = os.path.expanduser("~/Library/Preferences/openmw/openmw.log") with open(log_path) as f: return f.readlines()[-n:] # ═══════════════════════════════════════ # HIGH-LEVEL ACTIONS # ═══════════════════════════════════════ def walk_forward(duration=3, run=False): timmy_event("TimmyWalk", f'{{duration={duration}, run={str(run).lower()}}}') def stop(): timmy_event("TimmyStop") def turn(radians=0.5): timmy_event("TimmyTurn", f'{{angle={radians}}}') def jump(): timmy_event("TimmyJump") def attack(attack_type="any"): timmy_event("TimmyAttack", f'{{type="{attack_type}"}}') def explore(): timmy_event("TimmyExplore") def walk_to(x, y, z): timmy_event("TimmyWalkTo", f'{{x={x}, y={y}, z={z}}}') def perceive(): timmy_event("TimmyPerceive") def press_space(): send_keys_to_openmw(SPACE) def press_escape(): send_keys_to_openmw(ESCAPE) if __name__ == "__main__": print(f"Session: {SESSION_DIR}") print("Console bridge ready.")