Files
timmy-home/morrowind/console.py
Alexander Whitestone 0d64d8e559 initial: sovereign home — morrowind agent, skills, training-data, research, specs, notes, operational docs
Tracked: morrowind agent (py/cfg), skills/, training-data/, research/,
notes/, specs/, test-results/, metrics/, heartbeat/, briefings/,
memories/, skins/, hooks/, decisions.md, OPERATIONS.md, SOUL.md

Excluded: screenshots, PNGs, binaries, sessions, databases, secrets,
audio cache, timmy-config/ and timmy-telemetry/ (separate repos)
2026-03-27 13:05:57 -04:00

167 lines
4.7 KiB
Python

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