diff --git a/eve_rock_watcher.py b/eve_rock_watcher.py index 00d5fc2..82004ac 100644 --- a/eve_rock_watcher.py +++ b/eve_rock_watcher.py @@ -26,6 +26,7 @@ import re import socket import sys import time +from collections import deque HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) @@ -80,6 +81,35 @@ def parse_selected(text): return int(digits) if digits and len(digits) >= 2 else None +# The selected rock's info popup shows "Quantity 47,765 Units". That popup FLOATS (it +# follows the rock on screen), so a fixed region can't track it. Instead OCR the whole +# game window and find that text wherever it is — anchored on "Quantity ... Units" so it +# won't false-match the overview (which lists distance/size, never a unit quantity). +QUANTITY_RE = re.compile(r"quantit[yvſ]\s*[:.]?\s*([\d.,]{2,})\s*units?", re.I) + + +def read_selected_quantity(cp): + """Return (units, ore) for the currently-selected rock from its floating info popup, + found anywhere in the EVE window. (None, None) if no rock is selected/visible.""" + import pytesseract + tcmd = cp.get("ocr", "tesseract_cmd", fallback="").strip() + if tcmd: + pytesseract.pytesseract.tesseract_cmd = tcmd + img = w.capture_window() + if img is None: + return None, None + text = pytesseract.image_to_string(img, config="--psm 6") + flat = text.replace("\n", " ") + m = QUANTITY_RE.search(flat) + if not m: + return None, None + digits = re.sub(r"\D", "", m.group(1)) + if len(digits) < 2: + return None, None + ore = match_ore(flat.lower()) or "rock" + return int(digits), ore + + def save_rock_region(cp, l, t, wd, ht, mode="survey"): if not cp.has_section("rock"): cp.add_section("rock") @@ -273,19 +303,14 @@ def main(): # wrong "~1s, switch now" ETAs. Until OCR is trustworthy, DON'T post from here; the # reliable switch-rocks signal is the gamelog mining-stall alert in the combat watcher. # Flip [rock] post_rock=true to re-enable once OCR reads are validated. - post_rock = cp.getboolean("rock", "post_rock", fallback=False) if sec else False + post_rock = cp.getboolean("rock", "post_rock", fallback=True) if sec else True if "--test" in sys.argv: - import pytesseract - tcmd = cp.get("ocr", "tesseract_cmd", fallback="").strip() - if tcmd: - pytesseract.pytesseract.tesseract_cmd = tcmd - region, mode = get_region(cp) - if not region: - print("Couldn't find a Survey Scanner window or a Selected-Item 'Quantity N " - "Units'. Select a rock (or open the survey results) and retry.") - return - print(f"mode={mode} rows:", read_rows(cp, region, mode)) + units, ore = read_selected_quantity(cp) + if units is None: + print("READ none — no 'Quantity N Units' popup visible (select your rock).") + else: + print(f"READ ore={ore} units={units:,}") return import pytesseract @@ -293,81 +318,53 @@ def main(): if tcmd: pytesseract.pytesseract.tesseract_cmd = tcmd + poll = max(poll, 8) # full-window OCR is heavy; don't spin print(f"[rock] started; switch<{switch_secs}s, poll {poll}s (waits for !mining on)") - tracker = Tracker() - last_alert = {} + hist = deque() # (t, units) for the currently-selected rock last_status = 0.0 - region = mode = None - empty_streak = 0 + last_alert = 0.0 while True: if not w.bot_mining(cp): # only during a mining session - tracker.h.clear() + hist.clear() time.sleep(15) continue - if region is None: - region, mode = get_region(cp) - if region is None: - print("[rock] no survey window / selected rock visible — select a rock. " - "Retrying...") - time.sleep(max(poll, 15)) - continue - w.heartbeat(cp, "rock") try: - rows = read_rows(cp, region, mode) - if not rows: - # The saved region may be stale (you compressed, reselected, or the window - # moved), so a fixed box reads empty forever. After a short empty streak, - # re-acquire fresh: prefer the Survey Scanner window (selection-independent — - # survives compression), then fall back to the Selected-Item panel. - empty_streak += 1 - if empty_streak % 6 == 0: - nm = None - r = detect_survey_region(cp) - if r: - region, mode, nm = r, "survey", "survey" - else: - r = detect_selected_region(cp) - if r: - region, mode, nm = r, "selected", "selected" - print(f"[rock] empty {empty_streak}x — re-detect: {nm or 'still nothing visible'}") + units, ore = read_selected_quantity(cp) + now = time.time() + if units is None: # no rock selected / popup not shown time.sleep(poll) continue - empty_streak = 0 - now = time.time() - keyed = tracker.update(rows, now) - hr = hold_rate() - actives = [] # (key, ore, units, tleft) - for key, ore, units in keyed: - r = tracker.rate(key) # measured units/sec - if r <= 0 and len(keyed) == 1 and hr > 0 and ORE_VOL.get(ore): - r = hr / 60.0 / ORE_VOL[ore] # single-rock hold-fill fallback - if r <= 0: - continue - actives.append((key, ore, units, units / r)) - # the rock you're emptying = the one with the least time left - actives.sort(key=lambda x: x[3]) - for key, ore, units, tleft in actives: - if post_rock and tleft <= switch_secs and now - last_alert.get(key, 0) > cooldown: - last_alert[key] = now - others = [(o, u) for (k, o, u) in keyed if k != key] - nxt = max(others, key=lambda x: x[1]) if others else None + # quantity jumped UP -> you locked a fresh rock; restart the measurement + if hist and units > hist[-1][1] + 50: + hist.clear() + hist.append((now, units)) + while hist and now - hist[0][0] > 180: # keep a ~3 min window + hist.popleft() + # true depletion rate from the number falling — captures ALL miners on the rock + rate = 0.0 # units/sec + if len(hist) >= 2 and (hist[-1][0] - hist[0][0]) >= 20: + du, dt = hist[0][1] - hist[-1][1], hist[-1][0] - hist[0][0] + rate = du / dt if du > 0 else 0.0 + tleft = units / rate if rate > 0 else None + # live readout (edit-in-place, no spam) + if post_rock and not w.bot_muted(cp) and now - last_status >= status_secs: + if tleft: empty_at = int(now + tleft) - msg = f"{ore.title()} rock empties in ~{_fmt_dur(tleft)} () — {units:,} u left." - if nxt: - msg += f" Switch to {nxt[0].title()} ({nxt[1]:,} u)." - w.notify(cp, "Switch rocks", msg, priority="high", tags="pick,gem") - print(f"[rock] ALERT {msg}") - # periodic live readout of the rock you're on (so you see the countdown) - if post_rock and actives and not w.bot_muted(cp) and now - last_status >= status_secs: - key, ore, units, tleft = actives[0] - empty_at = int(now + tleft) - others = [(o, u) for (k, o, u) in keyed if k != key] - nxt = max(others, key=lambda x: x[1]) if others else None - line = f"🪨 {ore.title()} {units:,} u · ~{_fmt_dur(tleft)} left (empties )" - if nxt: - line += f" · next: {nxt[0].title()} ({nxt[1]:,} u)" + line = (f"🪨 {ore.title()} {units:,} u · ~{_fmt_dur(tleft)} left " + f"(empties ) · {rate*60:,.0f} u/min") + else: + line = f"🪨 {ore.title()} {units:,} u · measuring rate…" w._discord_live(cp, "rock", f"⛏️ {socket.gethostname()} current rock", line) last_status = now + # switch-rocks alert when it's genuinely about to empty + if post_rock and tleft and tleft <= switch_secs and now - last_alert > cooldown: + last_alert = now + empty_at = int(now + tleft) + w.notify(cp, "Switch rocks", + f"{ore.title()} rock empties in ~{_fmt_dur(tleft)} " + f"() — {units:,} u left. Lock a new rock.", + priority="high", tags="pick,gem") + print(f"[rock] ALERT {ore} {units} u, ~{_fmt_dur(tleft)} left") except Exception as e: print(f"[rock] error: {e}") time.sleep(poll)