236 lines
11 KiB
Python
236 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""Gamelog watcher — everything EVE logs but has no API for.
|
|
|
|
Tails EVE's local Gamelogs (Documents/EVE/logs/Gamelogs) and alerts via the same
|
|
channels as the ore-hold watcher (ntfy + Windows toast + Discord). Reading the log
|
|
file is legit — it never touches the game. Runs on the PC you play on.
|
|
|
|
Catches:
|
|
• TACKLED / EWAR'd (notify warp-scramble/disrupt/jam/web/neut) — urgent, no cooldown
|
|
• Incoming damage (combat 'from') — "rats?" — cooldown'd
|
|
• Hold full (notify cargo/ore hold full)
|
|
• Capacitor empty (notify)
|
|
• Bounty income (bounty lines) — accumulates a session total, milestone pings
|
|
|
|
Config: reuses config.ini, optional [combat] section:
|
|
gamelogs_dir = ; auto-detects ~/Documents/EVE/logs/Gamelogs
|
|
cooldown_secs = 60
|
|
min_damage = 1
|
|
bounty_milestone = 25000000 ; ping every this much ratting ISK
|
|
|
|
Run: python eve_combat_watcher.py (--test fires one alert)
|
|
"""
|
|
import glob
|
|
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 # reuse load_config() + notify()
|
|
|
|
TAG = re.compile(r"<[^>]+>")
|
|
INCOMING = re.compile(r"\(combat\)\s*([\d,]+)\s+from\s+(.+?)\s*[-\n]", re.IGNORECASE)
|
|
DANGER = re.compile(r"\(notify\).*(warp (?:scrambl|disrupt)|unable to warp|"
|
|
r"jam|target(?:ing)? (?:disrupt|jam)|energy neutraliz|web|stasis)",
|
|
re.IGNORECASE)
|
|
HOLDFULL = re.compile(r"\(notify\).*(cargo (?:hold )?is full|hold is full|"
|
|
r"not enough (?:cargo )?space|deactivat.*full|full.*deactivat)",
|
|
re.IGNORECASE)
|
|
CAPEMPTY = re.compile(r"\(notify\).*(capacitor is empty|not enough (?:capacitor|energy))",
|
|
re.IGNORECASE)
|
|
BOUNTY = re.compile(r"\(bounty\)\s*([\d,]+(?:\.\d+)?)\s*ISK", re.IGNORECASE)
|
|
# "(mining) You mined 76 units of Omber II-Grade" (after color/font tags stripped)
|
|
MINING = re.compile(r"\(mining\).*?mined\s+([\d,]+)\s+units?\s+of\s+(.+?)\s*$", re.IGNORECASE)
|
|
# "(notify) Successfully compressed Omber into 3029 Compressed Omber." -> hold was just freed
|
|
COMPRESSED = re.compile(r"successfully compressed", re.IGNORECASE)
|
|
# raw ore m³/unit (base name, longest first so 'dark ochre' beats 'ochre')
|
|
ORE_VOL = {"dark ochre": 8.0, "veldspar": 0.1, "scordite": 0.15, "pyroxeres": 0.3,
|
|
"plagioclase": 0.35, "omber": 0.6, "kernite": 1.2, "jaspet": 2.0,
|
|
"hemorphite": 3.0, "hedbergite": 3.0, "gneiss": 5.0, "crokite": 16.0,
|
|
"spodumain": 16.0, "bistot": 16.0, "arkonor": 16.0, "mercoxit": 40.0}
|
|
|
|
|
|
def ore_vol(name):
|
|
n = name.strip().lower()
|
|
for base in sorted(ORE_VOL, key=len, reverse=True):
|
|
if base in n:
|
|
return ORE_VOL[base]
|
|
return 0.6 # default ~Omber
|
|
|
|
|
|
def seed_raw_since_compress(path):
|
|
"""Sum raw ore m³ mined since the last 'Successfully compressed' in this log, so the
|
|
compress-tracker is accurate immediately after a restart (not reset to zero)."""
|
|
m3 = 0.0
|
|
try:
|
|
lines = open(path, "r", encoding="utf-8", errors="ignore").read().splitlines()
|
|
except Exception:
|
|
return 0.0
|
|
for ln in lines:
|
|
clean = TAG.sub("", ln)
|
|
if COMPRESSED.search(clean):
|
|
m3 = 0.0 # everything before a compress is gone
|
|
continue
|
|
mm = MINING.search(clean)
|
|
if mm:
|
|
m3 += int(mm.group(1).replace(",", "")) * ore_vol(mm.group(2))
|
|
return m3
|
|
|
|
|
|
def find_gamelogs(cp):
|
|
d = cp.get("combat", "gamelogs_dir", fallback="").strip() if cp.has_section("combat") else ""
|
|
if d and os.path.isdir(d):
|
|
return d
|
|
for base in (os.path.expanduser("~/Documents/EVE/logs/Gamelogs"),
|
|
os.path.expanduser("~/OneDrive/Documents/EVE/logs/Gamelogs")):
|
|
if os.path.isdir(base):
|
|
return base
|
|
return None
|
|
|
|
|
|
def newest(d):
|
|
files = glob.glob(os.path.join(d, "*.txt"))
|
|
return max(files, key=os.path.getmtime) if files else None
|
|
|
|
|
|
def main():
|
|
cp = w.load_config()
|
|
g = lambda k, d: (cp.getint("combat", k, fallback=d) if cp.has_section("combat") else d)
|
|
cooldown, min_dmg, milestone = g("cooldown_secs", 60), g("min_damage", 1), g("bounty_milestone", 25000000)
|
|
# mining-stall = lasers stopped pulling ore (rock depleted / out of range / you stopped).
|
|
# Reliable "switch rocks" signal straight from the log — no window or OCR needed.
|
|
# Must exceed a normal cycle gap (~35s observed) so it doesn't false-fire between cycles.
|
|
mining_stall = g("mining_stall_secs", 90)
|
|
# compress reminder: ping when raw ore piled up since your last compression crosses this
|
|
# many m³ (default 18,000 ≈ what Brock sits on). Tune via [combat] compress_at_m3.
|
|
compress_at_m3 = g("compress_at_m3", 18000)
|
|
# after this long with no ore flowing, treat the session as over and auto-pause ALL
|
|
# watchers (drop a marker bot_mining() honors) so quitting never spams Discord.
|
|
pause_secs = g("mining_pause_secs", 300)
|
|
PAUSE_MARKER = os.path.join(w.HERE, ".mining_paused")
|
|
|
|
if "--test" in sys.argv:
|
|
w.notify(cp, "Gamelog watcher test", "Combat/tackle/hold/bounty alerts will reach you.",
|
|
priority="high", tags="crossed_swords")
|
|
return
|
|
|
|
w.heartbeat(cp, "combat") # announce we're alive first
|
|
gdir = find_gamelogs(cp)
|
|
while not gdir: # don't die if EVE isn't running yet
|
|
print("[gamelog] no Gamelogs dir yet (EVE not started?) — retrying in 30s")
|
|
time.sleep(30)
|
|
gdir = find_gamelogs(cp)
|
|
print(f"[gamelog] watching {gdir}")
|
|
cur = newest(gdir)
|
|
fh = open(cur, "r", encoding="utf-8", errors="ignore") if cur else None
|
|
if fh:
|
|
fh.seek(0, os.SEEK_END)
|
|
last_dmg = 0.0
|
|
bounty_total = 0.0
|
|
bounty_reported = 0.0
|
|
last_mine = 0.0 # last time a "You mined N units" line appeared
|
|
last_ore = "" # ore on the most recent mining cycle
|
|
mine_alerted = False # fired the stall alert; re-arm on the next mining line
|
|
raw_m3 = seed_raw_since_compress(cur) if cur else 0.0 # uncompressed ore in hold
|
|
compress_alerted = False
|
|
print(f"[gamelog] raw ore since last compress ~{raw_m3:,.0f} m3 "
|
|
f"(alert at {compress_at_m3:,})")
|
|
|
|
while True:
|
|
nl = newest(gdir)
|
|
if nl and nl != cur:
|
|
cur = nl
|
|
if fh:
|
|
fh.close()
|
|
fh = open(cur, "r", encoding="utf-8", errors="ignore")
|
|
bounty_total = bounty_reported = 0.0 # new session
|
|
print(f"[gamelog] -> {os.path.basename(cur)}")
|
|
if not fh:
|
|
time.sleep(3); continue
|
|
line = fh.readline()
|
|
if not line:
|
|
# no new log line — check if mining has stalled (lasers stopped → switch rocks)
|
|
if last_mine and not mine_alerted and w.bot_mining(cp) and not w.bot_muted(cp) \
|
|
and time.time() - last_mine > mining_stall:
|
|
mine_alerted = True
|
|
ore = (last_ore.strip().title() + " ") if last_ore else ""
|
|
w.notify(cp, "Switch rocks", f"Lasers stopped pulling {ore}ore "
|
|
f"({int(time.time()-last_mine)}s) — rock depleted or out of range. "
|
|
"Lock a new rock.", priority="high", tags="pick")
|
|
print("[gamelog] mining stall — switch-rocks alert")
|
|
# stopped for good (you quit / warped off)? auto-pause every watcher so the rest
|
|
# of the session is silent until ore flows again.
|
|
if last_mine and time.time() - last_mine > pause_secs \
|
|
and not os.path.exists(PAUSE_MARKER):
|
|
open(PAUSE_MARKER, "w").close()
|
|
print("[gamelog] mining idle — auto-paused watchers")
|
|
time.sleep(1); continue
|
|
clean = TAG.sub("", line)
|
|
|
|
mm = MINING.search(clean)
|
|
if mm:
|
|
last_mine = time.time()
|
|
last_ore = mm.group(2)
|
|
mine_alerted = False # re-arm: lasers are pulling again
|
|
if os.path.exists(PAUSE_MARKER): # ore flowing -> resume all watchers
|
|
try:
|
|
os.remove(PAUSE_MARKER)
|
|
print("[gamelog] mining resumed — watchers un-paused")
|
|
except OSError:
|
|
pass
|
|
raw_m3 += int(mm.group(1).replace(",", "")) * ore_vol(mm.group(2))
|
|
if raw_m3 >= compress_at_m3 and not compress_alerted \
|
|
and w.bot_mining(cp) and not w.bot_muted(cp):
|
|
compress_alerted = True
|
|
w.notify(cp, "Compress now",
|
|
f"~{raw_m3:,.0f} m³ of raw ore piled up since your last "
|
|
f"compression — run the compressor to free the hold.",
|
|
priority="high", tags="package")
|
|
print(f"[gamelog] compress alert at {raw_m3:,.0f} m3")
|
|
continue
|
|
|
|
if COMPRESSED.search(clean): # you compressed -> hold freed, re-arm
|
|
raw_m3 = 0.0
|
|
compress_alerted = False
|
|
continue
|
|
|
|
if "(notify)" in line:
|
|
if DANGER.search(clean):
|
|
w.notify(cp, "⚠ TACKLED / EWAR",
|
|
"Scrambled/jammed/webbed — you may not be able to warp. ACT NOW.",
|
|
priority="urgent", tags="rotating_light"); continue
|
|
if HOLDFULL.search(clean):
|
|
w.notify(cp, "Hold full", "Your hold is full — compress / unload / swap.",
|
|
priority="high", tags="package"); continue
|
|
if CAPEMPTY.search(clean):
|
|
w.notify(cp, "Capacitor empty", "Cap's out — modules dropping.",
|
|
priority="default", tags="battery"); continue
|
|
continue
|
|
if "(bounty)" in line:
|
|
mb = BOUNTY.search(clean)
|
|
if mb:
|
|
bounty_total += float(mb.group(1).replace(",", ""))
|
|
if bounty_total - bounty_reported >= milestone:
|
|
bounty_reported = bounty_total
|
|
w.notify(cp, "Ratting income",
|
|
f"~{bounty_total/1e6:.0f}M ISK in bounties this session.",
|
|
priority="low", tags="moneybag")
|
|
continue
|
|
if "(combat)" in line:
|
|
m = INCOMING.search(clean)
|
|
if not m:
|
|
continue
|
|
dmg = int(m.group(1).replace(",", ""))
|
|
if dmg < min_dmg or time.time() - last_dmg < cooldown:
|
|
continue
|
|
last_dmg = time.time()
|
|
w.notify(cp, "Taking damage — rats?",
|
|
f"Incoming fire from {m.group(2).strip()} ({dmg}). Engage drones, "
|
|
f"watch shield.", priority="urgent", tags="crossed_swords")
|
|
print(f"[gamelog] damage {dmg} from {m.group(2).strip()}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|