diff --git a/eve_local_watcher.py b/eve_local_watcher.py new file mode 100644 index 0000000..5af5256 --- /dev/null +++ b/eve_local_watcher.py @@ -0,0 +1,161 @@ +#!/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 + + poll = cp.getint("local", "poll_secs", fallback=8) if cp.has_section("local") else 8 + cooldown = cp.getint("local", "cooldown_secs", fallback=30) if cp.has_section("local") else 30 + + 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 > prev: + # confirm over 2 reads to shrug off OCR jitter + pend_n = pend_n + 1 if pending == count else 1 + pending = count + if pend_n >= 2 and now - last_alert > cooldown: + w.notify(cp, "Someone entered Local", + f"+{count - prev} in Local — {count} pilots now (was {prev}). " + "Check d-scan / watch for a visitor on your field.", + priority="high", tags="eyes,warning") + 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()