publish eve_rock_watcher.py
This commit is contained in:
parent
19880dbf36
commit
c356fc4fd3
1 changed files with 71 additions and 74 deletions
|
|
@ -26,6 +26,7 @@ import re
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, HERE)
|
sys.path.insert(0, HERE)
|
||||||
|
|
@ -80,6 +81,35 @@ def parse_selected(text):
|
||||||
return int(digits) if digits and len(digits) >= 2 else None
|
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"):
|
def save_rock_region(cp, l, t, wd, ht, mode="survey"):
|
||||||
if not cp.has_section("rock"):
|
if not cp.has_section("rock"):
|
||||||
cp.add_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
|
# 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.
|
# 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.
|
# 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:
|
if "--test" in sys.argv:
|
||||||
import pytesseract
|
units, ore = read_selected_quantity(cp)
|
||||||
tcmd = cp.get("ocr", "tesseract_cmd", fallback="").strip()
|
if units is None:
|
||||||
if tcmd:
|
print("READ none — no 'Quantity N Units' popup visible (select your rock).")
|
||||||
pytesseract.pytesseract.tesseract_cmd = tcmd
|
else:
|
||||||
region, mode = get_region(cp)
|
print(f"READ ore={ore} units={units:,}")
|
||||||
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))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
import pytesseract
|
import pytesseract
|
||||||
|
|
@ -293,81 +318,53 @@ def main():
|
||||||
if tcmd:
|
if tcmd:
|
||||||
pytesseract.pytesseract.tesseract_cmd = 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)")
|
print(f"[rock] started; switch<{switch_secs}s, poll {poll}s (waits for !mining on)")
|
||||||
tracker = Tracker()
|
hist = deque() # (t, units) for the currently-selected rock
|
||||||
last_alert = {}
|
|
||||||
last_status = 0.0
|
last_status = 0.0
|
||||||
region = mode = None
|
last_alert = 0.0
|
||||||
empty_streak = 0
|
|
||||||
while True:
|
while True:
|
||||||
if not w.bot_mining(cp): # only during a mining session
|
if not w.bot_mining(cp): # only during a mining session
|
||||||
tracker.h.clear()
|
hist.clear()
|
||||||
time.sleep(15)
|
time.sleep(15)
|
||||||
continue
|
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:
|
try:
|
||||||
rows = read_rows(cp, region, mode)
|
units, ore = read_selected_quantity(cp)
|
||||||
if not rows:
|
now = time.time()
|
||||||
# The saved region may be stale (you compressed, reselected, or the window
|
if units is None: # no rock selected / popup not shown
|
||||||
# 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'}")
|
|
||||||
time.sleep(poll)
|
time.sleep(poll)
|
||||||
continue
|
continue
|
||||||
empty_streak = 0
|
# quantity jumped UP -> you locked a fresh rock; restart the measurement
|
||||||
now = time.time()
|
if hist and units > hist[-1][1] + 50:
|
||||||
keyed = tracker.update(rows, now)
|
hist.clear()
|
||||||
hr = hold_rate()
|
hist.append((now, units))
|
||||||
actives = [] # (key, ore, units, tleft)
|
while hist and now - hist[0][0] > 180: # keep a ~3 min window
|
||||||
for key, ore, units in keyed:
|
hist.popleft()
|
||||||
r = tracker.rate(key) # measured units/sec
|
# true depletion rate from the number falling — captures ALL miners on the rock
|
||||||
if r <= 0 and len(keyed) == 1 and hr > 0 and ORE_VOL.get(ore):
|
rate = 0.0 # units/sec
|
||||||
r = hr / 60.0 / ORE_VOL[ore] # single-rock hold-fill fallback
|
if len(hist) >= 2 and (hist[-1][0] - hist[0][0]) >= 20:
|
||||||
if r <= 0:
|
du, dt = hist[0][1] - hist[-1][1], hist[-1][0] - hist[0][0]
|
||||||
continue
|
rate = du / dt if du > 0 else 0.0
|
||||||
actives.append((key, ore, units, units / r))
|
tleft = units / rate if rate > 0 else None
|
||||||
# the rock you're emptying = the one with the least time left
|
# live readout (edit-in-place, no spam)
|
||||||
actives.sort(key=lambda x: x[3])
|
if post_rock and not w.bot_muted(cp) and now - last_status >= status_secs:
|
||||||
for key, ore, units, tleft in actives:
|
if tleft:
|
||||||
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
|
|
||||||
empty_at = int(now + tleft)
|
empty_at = int(now + tleft)
|
||||||
msg = f"{ore.title()} rock empties in ~{_fmt_dur(tleft)} (<t:{empty_at}:R>) — {units:,} u left."
|
line = (f"🪨 {ore.title()} {units:,} u · ~{_fmt_dur(tleft)} left "
|
||||||
if nxt:
|
f"(empties <t:{empty_at}:R>) · {rate*60:,.0f} u/min")
|
||||||
msg += f" Switch to {nxt[0].title()} ({nxt[1]:,} u)."
|
else:
|
||||||
w.notify(cp, "Switch rocks", msg, priority="high", tags="pick,gem")
|
line = f"🪨 {ore.title()} {units:,} u · measuring rate…"
|
||||||
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 <t:{empty_at}:R>)"
|
|
||||||
if nxt:
|
|
||||||
line += f" · next: {nxt[0].title()} ({nxt[1]:,} u)"
|
|
||||||
w._discord_live(cp, "rock", f"⛏️ {socket.gethostname()} current rock", line)
|
w._discord_live(cp, "rock", f"⛏️ {socket.gethostname()} current rock", line)
|
||||||
last_status = now
|
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"(<t:{empty_at}:R>) — {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:
|
except Exception as e:
|
||||||
print(f"[rock] error: {e}")
|
print(f"[rock] error: {e}")
|
||||||
time.sleep(poll)
|
time.sleep(poll)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue