167 lines
6.4 KiB
Python
167 lines
6.4 KiB
Python
#!/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()
|