import sys
import time
import ctypes
import keyboard
import pymem
import pymem.process
import pymem.exception
class LastEpochMonitor:
def __init__(self, exe="Last Epoch.exe", mod="GameAssembly.dll",
key_delay=1.0, low_hp_pct=0, potion_pct=50, busy_val=500,
auto_quit=False):
self.exe = exe
self.mod = mod
self.key_delay = key_delay
self.low_hp_pct = low_hp_pct
self.potion_pct = potion_pct
self.busy_val = busy_val
self.auto_quit = auto_quit
self.ptr_defs = {
"cur_hp": {"static": 0x4040968, "chain": (0x88, 0x128, 0x90, 0x80, 0xB8, 0x10, 0xB0), "type": "float"},
"es": {"static": 0x4105E38, "chain": (0xB8, 0x0, 0x18, 0x2D4), "type": "float"},
"max_hp": {"static": 0x40F7038, "chain": (0xB8, 0x0, 0xE0, 0x6

, "type": "int"}
}
self.pm = None
self.base = None
self.attach_process()
def attach_process(self):
try:
self.pm = pymem.Pymem(self.exe)
print(f"Successfully attached to process: {self.exe}")
except pymem.exception.ProcessNotFound:
print(f"Error: Process '{self.exe}' not running. Please start the game.")
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred while attaching to process: {e}")
sys.exit(1)
try:
self.base = pymem.process.module_from_name(self.pm.process_handle, self.mod).lpBaseOfDll
if not self.base:
raise pymem.exception.ProcessError(f"Got null base address for module {self.mod}")
print(f"Found module '{self.mod}' at base address: {hex(self.base)}")
except (AttributeError, pymem.exception.ProcessError):
print(f"Error: Module '{self.mod}' not found in '{self.exe}'.")
print("Ensure the game is fully loaded (e.g., at the character selection screen or in-game).")
self.pm.close_process()
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred while finding module: {e}")
if self.pm:
self.pm.close_process()
sys.exit(1)
def _resolve_ptr(self, base_addr, chain):
try:
addr = self.pm.read_longlong(base_addr)
if not addr:
raise pymem.exception.MemoryReadError(f"Base address read returned null at {hex(base_addr)}")
except pymem.exception.MemoryReadError as e:
raise pymem.exception.MemoryReadError(f"Failed initial read at {hex(base_addr)}: {e}")
for i, off in enumerate(chain[:-1]):
try:
next_addr = self.pm.read_longlong(addr + off)
if not next_addr:
raise pymem.exception.MemoryReadError(f"Pointer became null at chain index {i}, offset {hex(off)} (current address: {hex(addr)})")
addr = next_addr
except pymem.exception.MemoryReadError as e:
raise pymem.exception.MemoryReadError(f"Failed reading offset {hex(off)} at chain index {i} (current address: {hex(addr)}): {e}")
final_address = addr + chain[-1]
return final_address
def read_value(self, key):
p = self.ptr_defs[key]
try:
address = self._resolve_ptr(self.base + p["static"], p["chain"])
if p["type"] == "float":
value = self.pm.read_float(address)
elif p["type"] == "int":
value = self.pm.read_int(address)
else:
raise ValueError(f"Unknown data type '{p['type']}' for key '{key}'")
return value
except pymem.exception.MemoryReadError as e:
raise pymem.exception.MemoryReadError(f"Failed to read value for '{key}': {e}")
except ValueError as e:
raise ValueError(e)
def safe_read(self, key):
try:
return self.read_value(key)
except (pymem.exception.MemoryReadError, pymem.exception.WinAPIError, TypeError, ValueError) as e:
print(f"Read error for '{key}': {e}. Attempting to re-attach...")
self.attach_process()
time.sleep(1.5)
try:
return self.read_value(key)
except (pymem.exception.MemoryReadError, pymem.exception.WinAPIError, TypeError, ValueError) as e2:
print(f"Read error persisted for '{key}' after re-attach: {e2}")
return None
except Exception as e_fatal:
print(f"Unexpected critical error reading '{key}' after re-attach: {e_fatal}")
self.quit_game(f"Fatal read error: {e_fatal}")
return None
except Exception as e_fatal:
print(f"Unexpected critical error reading '{key}': {e_fatal}")
self.quit_game(f"Fatal read error: {e_fatal}")
return None
def quit_game(self, reason):
print(f"Quit triggered: {reason}")
try:
print("Attempting Alt+F4...")
keyboard.press_and_release('alt+f4')
time.sleep(0.3)
except Exception as e:
print(f"Warning: Failed to send Alt+F4 ({e}). Proceeding with termination.")
try:
if self.pm and self.pm.process_handle:
print(f"Terminating process ID: {self.pm.process_id}")
handle = int(self.pm.process_handle)
if handle:
success = ctypes.windll.kernel32.TerminateProcess(handle, 0)
if success:
print("Process terminated successfully.")
else:
error_code = ctypes.windll.kernel32.GetLastError()
print(f"Failed to terminate process. Error code: {error_code}")
else:
print("Process handle is invalid, cannot terminate.")
self.pm.close_process()
else:
print("No valid process handle to terminate.")
except Exception as e:
print(f"Error during process termination: {e}")
print("Exiting script.")
sys.exit(0)
def main(self):
last_pot_time = 0.0
error_count = 0
max_errors = 15
print("Starting health monitor loop...")
while True:
try:
hp = self.safe_read("cur_hp")
es = self.safe_read("es")
max_hp = self.safe_read("max_hp")
loading_state = False
loading_reason = ""
if hp is None or es is None or max_hp is None:
print("Failed to read one or more values after retry.")
error_count += 1
loading_state = True
loading_reason = "Read Error"
elif hp <= 0:
loading_state = True
loading_reason = f"HP <= 0 ({hp})"
elif hp == self.busy_val:
loading_state = True
loading_reason = f"HP == busy_val ({hp})"
elif max_hp <= 0:
loading_state = True
loading_reason = f"Max HP <= 0 ({max_hp})"
elif max_hp == self.busy_val:
loading_state = True
loading_reason = f"Max HP == busy_val ({max_hp})"
if loading_state:
if loading_reason != "Read Error":
print(f"Loading state detected ({loading_reason}). Waiting...")
error_count = 0
time.sleep(0.5)
continue
else:
error_count = 0
current_effective_hp = hp + es
pct = (current_effective_hp / max_hp) * 100
print(f"HP: {hp:.1f} ES: {es:.1f} | Total: {current_effective_hp:.1f}/{max_hp} ({pct:.0f}%)")
if pct < self.low_hp_pct:
if self.auto_quit:
self.quit_game(f"Health low: {pct:.0f}% < {self.low_hp_pct}%")
else:
print(f"LOW HEALTH WARNING: {pct:.0f}% < {self.low_hp_pct}% (Auto-quit disabled)")
current_time = time.time()
if pct < self.potion_pct and (current_time - last_pot_time) >= self.key_delay:
print(f"Using potion at {pct:.0f}% health.")
try:
keyboard.press_and_release('1')
last_pot_time = current_time
except Exception as key_error:
print(f"Warning: Failed to press potion key '1': {key_error}")
if error_count > max_errors:
print(f"Exceeded maximum consecutive read errors ({max_errors}). Exiting.")
self.quit_game("Too many read errors")
time.sleep(0.05 if error_count == 0 and not loading_state else 0.5)
except KeyboardInterrupt:
print("\nKeyboard interrupt received. Exiting.")
if self.pm:
self.pm.close_process()
sys.exit(0)
except Exception as loop_error:
print(f"Unexpected error in main loop: {loop_error}")
error_count +=1
if error_count > max_errors:
self.quit_game(f"Fatal loop error: {loop_error}")
time.sleep(1)
if __name__ == "__main__":
monitor = LastEpochMonitor(auto_quit=True)
monitor.main()