publish eve_local_watcher.py
This commit is contained in:
parent
3f7d717685
commit
ef950fdccd
1 changed files with 161 additions and 0 deletions
161
eve_local_watcher.py
Normal file
161
eve_local_watcher.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue