#!/usr/bin/env python3 """Local watcher — 'someone just entered Local' alerts for isolated systems. EVE has no API for who's in Local, and the chat logs only record *messages*, not who jumps in/out. So this reads your **Local member list** off the screen and pings you when the pilot count rises — exactly what matters when you're mining somewhere quiet like Solitude and a stranger lands in system. Best in low-population systems (the whole member list is visible); in a busy hub it just tracks what's on screen. Only runs during a mining session (`!mining on` in Discord). Point it at your Local window once: python eve_local_watcher.py --snip (or send Brock a screenshot and he sets [local] region in config.ini). Auto-found if the window shows the word 'Local'. python eve_local_watcher.py # normal (waits for !mining on) python eve_local_watcher.py --snip # box your Local member-list window python eve_local_watcher.py --test # read once and print the pilot count """ import os import re import sys import time HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import eve_orehold_watcher as w # load_config, notify, grab_region, heartbeat, bot_mining, snip_region def count_pilots(text): """Count name-like lines in the OCR'd member list (a row per pilot).""" n = 0 for line in text.splitlines(): s = line.strip() if len(s) < 3: continue if s.lower().startswith("local"): # channel header, not a pilot continue if sum(c.isalpha() for c in s) >= 2: # looks like a name n += 1 return n def save_local_region(cp, l, t, wd, ht): if not cp.has_section("local"): cp.add_section("local") cp["local"]["region"] = f"{l},{t},{wd},{ht}" with open(w.CONFIG_PATH, "w") as f: cp.write(f) def detect_local_region(cp): """Best-effort: find the Local window by locating the 'Local' label, take the column beneath it as the member list. Falls back to None (use --snip).""" import mss import pytesseract from PIL import Image tcmd = cp.get("ocr", "tesseract_cmd", fallback="").strip() if tcmd: pytesseract.pytesseract.tesseract_cmd = tcmd try: with mss.mss() as sct: mon = sct.monitors[0] raw = sct.grab(mon) img = Image.frombytes("RGB", raw.size, raw.bgra, "raw", "BGRX") data = pytesseract.image_to_data(img, output_type=pytesseract.Output.DICT) except Exception as e: print(f"[local] detect error: {e}") return None for i in range(len(data["text"])): if data["text"][i].strip().lower() == "local": x = mon["left"] + data["left"][i] - 10 y = mon["top"] + data["top"][i] - 6 wd = max(220, data["width"][i] + 220) ht = 600 # capture the list beneath the label save_local_region(cp, x, y, wd, ht) print(f"[local] auto-located Local window near {x},{y}") return [x, y, wd, ht] return None def get_region(cp): rs = cp.get("local", "region", fallback="").strip() if cp.has_section("local") else "" if rs: return [int(v) for v in rs.split(",")] print("[local] no saved region — trying to auto-locate the Local window...") return detect_local_region(cp) def main(): cp = w.load_config() if "--snip" in sys.argv: r = w.snip_region("Drag a box around your whole Local member-list window. Esc to cancel.") if r: save_local_region(cp, *r) print(f"Saved Local region {r} to config.ini") else: print("Cancelled.") return import pytesseract tcmd = cp.get("ocr", "tesseract_cmd", fallback="").strip() if tcmd: pytesseract.pytesseract.tesseract_cmd = tcmd sec = cp.has_section("local") poll = cp.getint("local", "poll_secs", fallback=8) if sec else 8 cooldown = cp.getint("local", "cooldown_secs", fallback=180) if sec else 180 # only meaningful in QUIET systems (Solitude etc.); above this it's just noise max_pop = cp.getint("local", "max_pop", fallback=12) if sec else 12 confirm = cp.getint("local", "confirm_reads", fallback=3) if sec else 3 if "--test" in sys.argv: region = get_region(cp) if not region: print("No Local region. Run --snip (or send a screenshot).") return print("pilots:", count_pilots(pytesseract.image_to_string(w.grab_region(region)))) return print(f"[local] started; poll {poll}s (waits for !mining on)") region = None prev = None pending = None pend_n = 0 last_alert = 0.0 while True: if not w.bot_mining(cp): prev = None time.sleep(15) continue if region is None: region = get_region(cp) if region is None: print("[local] Local window not found — open it / run --snip. Retrying...") time.sleep(max(poll, 20)) continue w.heartbeat(cp, "local") try: count = count_pilots(pytesseract.image_to_string(w.grab_region(region))) now = time.time() if prev is None: prev = count print(f"[local] baseline {count} pilots") elif count > max_pop: prev = count # busy system — track, never alert pending, pend_n = None, 0 elif count > prev: # require the higher count to persist across `confirm` reads (OCR jitter) pend_n = pend_n + 1 if pending == count else 1 pending = count if pend_n >= confirm and now - last_alert > cooldown: w.notify(cp, "Someone entered Local", f"+{count - prev} in Local — {count} pilots now (was {prev}). " "Check d-scan.", priority="high", tags="eyes") last_alert = now prev = count pending, pend_n = None, 0 else: if count < prev: prev = count # someone left; rebaseline down pending, pend_n = None, 0 except Exception as e: print(f"[local] error: {e}") time.sleep(poll) if __name__ == "__main__": main()