merge: town_manager.py — combine debug logging + set_panel_check_paused

Resolved 7 conflicts by keeping both local (Logger.info diagnostics in
buy_consumables and repair) and remote (set_panel_check_paused health-check
suppression during vendor/repair panels). All paths now log + pause correctly.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
alex
2026-06-06 22:07:24 +02:00
co-authored by Claude Sonnet 4.6
17 changed files with 344 additions and 191 deletions
+8 -2
View File
@@ -199,7 +199,13 @@ class Bot:
return self._curr_loc
def start(self):
self.trigger_or_stop('init')
from utils.misc import register_stop_condition, unregister_stop_condition
import threading
register_stop_condition(threading.get_ident(), lambda: self._stopping)
try:
self.trigger_or_stop('init')
finally:
unregister_stop_condition(threading.get_ident())
def stop(self):
self._stopping = True
@@ -718,6 +724,7 @@ class Bot:
res = False
self._do_runs[run_name] = False
self._game_stats.log_run_started(run_name)
set_pause_state(False)
self._curr_loc = run_obj.approach(self._curr_loc, *approach_args)
if not self._curr_loc:
# Approach failed — couldn't reach the boss
@@ -726,7 +733,6 @@ class Bot:
self._game_stats.log_run_finished(run_name, True, picked)
self._ending_run_helper(res)
return
set_pause_state(False)
try:
res = run_obj.battle(*battle_args)
except Exception as e:
+5 -2
View File
@@ -323,10 +323,13 @@ class IChar:
# Try to switch weapons and select bo until we find the skill on the right skill slot
start = time.time()
switch_success = False
while time.time() - start < 4:
while time.time() - start < 5:
if skills.is_right_skill_selected(["BC", "BO"]):
switch_success = True
break
self._weapon_switch()
keyboard.send(Config().char["battle_command"])
wait(0.2, 0.3)
wait(0.3, 0.5)
if skills.is_right_skill_selected(["BC", "BO"]):
switch_success = True
break
+22 -17
View File
@@ -68,25 +68,30 @@ class DeathManager:
return False
def start_monitor(self):
with self._state_lock:
self._do_monitor = True
self._died = False
Logger.info("Start Death monitoring")
while True:
from utils.misc import register_stop_condition, unregister_stop_condition
register_stop_condition(threading.get_ident(), lambda: not self._do_monitor)
try:
with self._state_lock:
if not self._do_monitor:
break
if self._died:
monitor_active = True
else:
monitor_active = False
if monitor_active:
wait(0.5)
continue
self._do_monitor = True
self._died = False
Logger.info("Start Death monitoring")
while True:
with self._state_lock:
if not self._do_monitor:
break
if self._died:
monitor_active = True
else:
monitor_active = False
if monitor_active:
wait(0.5)
continue
wait(self._loop_delay, self._loop_delay * 1.5) # no need to do this too frequent, when we died we are not in a hurry...
self.handle_death_screen()
wait(self._loop_delay, self._loop_delay * 1.5) # no need to do this too frequent, when we died we are not in a hurry...
self.handle_death_screen()
finally:
unregister_stop_condition(threading.get_ident())
Logger.debug("Stop death monitoring")
# Testing:
-2
View File
@@ -64,8 +64,6 @@ class GameController:
cooperative_shutdown(
self.bot_thread,
bot=self.bot,
health_manager=self.health_manager,
death_manager=self.death_manager,
timeout=8.0,
allow_force_kill=False,
)
+3 -3
View File
@@ -292,10 +292,10 @@ class GameStats:
failure_reason = self._last_failure_reason or "Unknown"
Logger.warning(f"End failed game: Elapsed time: {elapsed_time:.2f}s Fails: {self._consecutive_runs_failed} Reason: {failure_reason}")
self._log_event("game_ended", {"failed": True, "elapsed_seconds": round(elapsed_time, 2), "reason": failure_reason})
# Send Discord notification for failed games
if self._messenger.enabled:
# Send Discord notification for failed games (threshold: only every 5 consecutive fails)
if self._messenger.enabled and self._consecutive_runs_failed >= 5:
self._messenger.send_message(
f"Failed game (#{self._game_counter}): "
f"CRITICAL: 5+ Consecutive fails reached (#{self._game_counter}): "
f"{elapsed_time:.0f}s, consecutive fails: {self._consecutive_runs_failed}. "
f"Reason: {failure_reason}"
)
+96 -87
View File
@@ -88,101 +88,110 @@ class HealthManager:
self.set_pause_state(True)
def start_monitor(self):
Logger.info("Start health monitoring")
self._do_monitor = True
self._did_chicken = False
from utils.misc import register_stop_condition, unregister_stop_condition
import threading
register_stop_condition(threading.get_ident(), lambda: not self._do_monitor)
try:
Logger.info("Start health monitoring")
self._do_monitor = True
self._did_chicken = False
lp_hp_potion_delay = 10.24
lp_mp_potion_delay = 1.0
merc_hp_potion_delay = 10.24
lp_hp_potion_delay = 10.24
lp_mp_potion_delay = 0.5 # reduced from 1.0 for better high-mana-usage reliability
merc_hp_potion_delay = 10.24
while self._do_monitor:
if self._did_chicken or self.get_pause_state():
wait(0.5, 0.7)
continue
try:
fn_start = time.perf_counter()
img = grab()
if is_visible(ScreenObjects.InGame, img):
health_percentage = meters.get_health(img)
mana_percentage = meters.get_mana(img)
while self._do_monitor:
if self._did_chicken or self.get_pause_state():
wait(0.1, 0.2)
continue
try:
fn_start = time.perf_counter()
img = grab()
if is_visible(ScreenObjects.InGame, img):
health_percentage = meters.get_health(img)
mana_percentage = meters.get_mana(img)
# check rejuv first
success_drink_rejuv = False
last_drink = time.time() - self._last_rejuv
if last_drink > 0.60:
if (health_percentage <= Config().char["take_rejuv_potion_health"]) or \
(mana_percentage <= Config().char["take_rejuv_potion_mana"]):
# Critical: Reduce delay to minimum
wait(0.02, 0.05)
success_drink_rejuv = belt.drink_potion("rejuv", stats=[health_percentage, mana_percentage])
if not success_drink_rejuv:
Logger.warning(f"Failed to drink rejuv. Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
self._last_rejuv = time.time()
# check rejuv first
success_drink_rejuv = False
last_drink = time.time() - self._last_rejuv
if last_drink > 0.60:
if (health_percentage <= Config().char["take_rejuv_potion_health"]) or \
(mana_percentage <= Config().char["take_rejuv_potion_mana"]):
# Critical: Reduce delay to minimum
wait(0.02, 0.05)
success_drink_rejuv = belt.drink_potion("rejuv", stats=[health_percentage, mana_percentage])
if not success_drink_rejuv:
if health_percentage <= Config().char["take_rejuv_potion_health"]:
Logger.warning(f"Failed to drink rejuv for HP. Chickening, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
else:
Logger.debug(f"Failed to drink rejuv for MANA. Will try mana potion check.")
else:
self._last_rejuv = time.time()
if last_drink < 8:
Logger.warning(f"Two juvs drank within {last_drink:.1f} seconds. Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
if last_drink < 8:
Logger.warning(f"Two juvs drank within {last_drink:.1f} seconds. Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
if health_percentage <= Config().char["chicken"]:
Logger.warning(f"Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
if not success_drink_rejuv:
# check health
last_drink = time.time() - self._last_health
if health_percentage <= Config().char["take_health_potion"] and last_drink > lp_hp_potion_delay:
wait(0.05, 0.1)
if belt.drink_potion("health", stats=[health_percentage, mana_percentage]):
self._last_health = time.time()
# check mana
last_drink = time.time() - self._last_mana
if mana_percentage <= Config().char["take_mana_potion"] and last_drink > lp_mp_potion_delay:
wait(0.05, 0.1)
if belt.drink_potion("mana", stats=[health_percentage, mana_percentage]):
self._last_mana = time.time()
# check merc health
if any([Config().char[x] for x in ["heal_rejuv_merc", "merc_chicken", "heal_merc"]]):
merc_health = meters.get_merc_health(img)
last_drink = time.time() - self._last_merc_heal
if Config().char["merc_chicken"] and (merc_health < Config().char["merc_chicken"]):
Logger.warning(f"Trying to chicken, merc HP {(merc_health*100):.1f}%!")
if health_percentage <= Config().char["chicken"]:
Logger.warning(f"Trying to chicken, player HP {(health_percentage*100):.1f}%!")
self._do_chicken(img)
continue
if Config().char["heal_rejuv_merc"] and (merc_health < Config().char["heal_rejuv_merc"]) and (last_drink > 4.0):
wait(0.05, 0.1)
if belt.drink_potion("rejuv", merc=True, stats=[merc_health]):
self._last_merc_heal = time.time()
elif Config().char["heal_merc"] and (merc_health < Config().char["heal_merc"]) and (last_drink > merc_hp_potion_delay):
wait(0.05, 0.1)
if belt.drink_potion("health", merc=True, stats=[merc_health]):
self._last_merc_heal = time.time()
# Close any open panels that might block detection
if not self.get_panel_check_paused() and (is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img)):
self._count_panel_detects += 1
if self._count_panel_detects >= 2:
self._count_panel_detects = 0
Logger.warning(f"Found open panels (inv/quest/stats) twice. Chickening to be safe.")
self._do_chicken(img)
continue
Logger.debug("Found an open panel. Closing it.")
common.close()
fn_end = time.perf_counter()
# Target ~15 FPS polling with anti-cheat jitter
base_wait = 1/15 - (fn_end - fn_start)
wait_time = max(0.01, base_wait * random.uniform(0.8, 1.2))
wait(wait_time)
if not success_drink_rejuv:
# check health
last_drink = time.time() - self._last_health
if health_percentage <= Config().char["take_health_potion"] and last_drink > lp_hp_potion_delay:
wait(0.05, 0.1)
if belt.drink_potion("health", stats=[health_percentage, mana_percentage]):
self._last_health = time.time()
# check mana
last_drink = time.time() - self._last_mana
if mana_percentage <= Config().char["take_mana_potion"] and last_drink > lp_mp_potion_delay:
wait(0.05, 0.1)
if belt.drink_potion("mana", stats=[health_percentage, mana_percentage]):
self._last_mana = time.time()
# check merc health
if any([Config().char[x] for x in ["heal_rejuv_merc", "merc_chicken", "heal_merc"]]):
merc_health = meters.get_merc_health(img)
last_drink = time.time() - self._last_merc_heal
if Config().char["merc_chicken"] and (merc_health < Config().char["merc_chicken"]):
Logger.warning(f"Trying to chicken, merc HP {(merc_health*100):.1f}%!")
self._do_chicken(img)
continue
if Config().char["heal_rejuv_merc"] and (merc_health < Config().char["heal_rejuv_merc"]) and (last_drink > 4.0):
wait(0.05, 0.1)
if belt.drink_potion("rejuv", merc=True, stats=[merc_health]):
self._last_merc_heal = time.time()
elif Config().char["heal_merc"] and (merc_health < Config().char["heal_merc"]) and (last_drink > merc_hp_potion_delay):
wait(0.05, 0.1)
if belt.drink_potion("health", merc=True, stats=[merc_health]):
self._last_merc_heal = time.time()
# Close any open panels that might block detection
if not self.get_panel_check_paused() and (is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img)):
self._count_panel_detects += 1
if self._count_panel_detects >= 2:
self._count_panel_detects = 0
Logger.warning(f"Found open panels (inv/quest/stats) twice. Chickening to be safe.")
self._do_chicken(img)
continue
Logger.debug("Found an open panel. Closing it.")
common.close()
except Exception as e:
Logger.error(f"HealthManager error: {e}")
wait(0.5)
fn_end = time.perf_counter()
# Target ~15 FPS polling with anti-cheat jitter
base_wait = 1/15 - (fn_end - fn_start)
wait_time = max(0.01, base_wait * random.uniform(0.8, 1.2))
wait(wait_time)
except Exception as e:
Logger.error(f"HealthManager error: {e}")
wait(0.5)
finally:
unregister_stop_condition(threading.get_ident())
Logger.debug("Stop health monitoring")
+3
View File
@@ -58,6 +58,7 @@ def inventory_is_open(img: np.ndarray = None) -> bool:
)
def close(img: np.ndarray = None) -> np.ndarray | None:
from health_manager import set_panel_check_paused
img = grab() if img is None else img
if inventory_is_open(img):
# close open inventory
@@ -72,7 +73,9 @@ def close(img: np.ndarray = None) -> np.ndarray | None:
if not timer:
success = view.return_to_play()
if not success:
set_panel_check_paused(False)
return None
set_panel_check_paused(False)
return img
+7 -1
View File
@@ -365,7 +365,13 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
# sell if not keeping item, vendor is open, and item type can be traded
if vendor_open and item_can_be_traded and not (box.keep or box.need_id):
box.sell = True
transfer_items([box], action = "sell")
remaining = transfer_items([box], action = "sell")
if remaining:
# Not sold! (e.g. protected shield or gold full)
Logger.debug(f"Item {item_name} was not sold, adding to stash list")
box.sell = False
box.keep = True
boxes.append(box)
continue
# if item is to be kept and is already ID'd or doesn't need ID, log and stash
+37 -15
View File
@@ -16,7 +16,7 @@ from item import consumables
from item.consumables import ITEM_CONSUMABLES_MAP
from logger import Logger
from bnip.actions import should_pickup
from bnip.NTIPAliasType import NTIPAliasType as NTIP_TYPES
from bnip.NTIPAliasType import NTIPAliasType
from bnip.NTIPAliasQuality import NTIPAliasQuality as NTIP_QUALITY
from screen import grab, convert_abs_to_monitor
from ui_manager import ScreenObjects, is_visible
@@ -34,7 +34,7 @@ class PickedUpResult(Enum):
class PickIt:
def __init__(self):
self._cached_pickit_items = {} # * Cache the result of whether or not we should pick up the item. this should save some time
self._prev_item_pickup_attempt = None
self._prev_item_pickup_attempt = GroundItem()
self._fail_pickup_count = 0
self._picked_up_items = []
self._picked_up_item = False
@@ -51,11 +51,32 @@ class PickIt:
except OSError as e:
Logger.warning(f"Pickit JSON log failed: {e}")
@staticmethod
def _is_garbage_item(item: GroundItem) -> bool:
if item is None:
return True
name = (item.Name or "").upper().strip()
if not name or name == "NONE":
return True
if item.BaseItem is None:
return True
# Filter environmental labels (stairs, portals, etc)
garbage_keywords = ["TO THE", "LEVEL 1", "LEVEL 2", "LEVEL 3", "LEVEL 4", "LEVEL 5"]
for kw in garbage_keywords:
if kw in name:
return True
# Very short non-gold items are usually OCR artifacts unless they are known small names
if len(name) < 3 and name not in ["KEY", "GOLD"]:
return True
return False
@staticmethod
def _locate_items() -> tuple[GroundItemList, ndarray]:
img = grab()
start = time.time()
items = d2r_image.get_ground_loot(img).items.copy()
# Filter out garbage/environmental labels
items = [item for item in items if not PickIt._is_garbage_item(item)]
Logger.debug(f"Read {len(items)} ground items in {round(time.time() - start, 3)} seconds")
items = sorted(items, key=lambda item: item.Distance)
return items, img
@@ -233,19 +254,20 @@ class PickIt:
Logger.debug(f"Pick up expression: {raw_expression}")
Logger.info(f"Attempt to pick up {item.Name} at distance {item.Distance}")
pick_up_res = self._pick_up_item(char, item)
match pick_up_res:
case PickedUpResult.InventoryFull:
Logger.warning(f"Inventory is full, could not pick {item.Name}. Stop pickit") #TODO Create logic to handle inventory full
break
case PickedUpResult.PickedUp:
self._decrement_need_if_consumable(item)
self._picked_up_items.append(item)
case PickedUpResult.PickedUpFailed:
# Blacklist this item ID so we don't retry it this session.
# Gold piles with different amounts have different IDs and would otherwise
# loop forever because _yoink_item always returns PickedUp even on failure.
Logger.debug(f"Blacklisting {item.Name} (ID={item.ID}) after failed pickup — won't retry this session")
self._cached_pickit_items[item.ID] = False
if pick_up_res == PickedUpResult.InventoryFull:
Logger.warning(f"Inventory is full, could not pick {item.Name}. Stop pickit")
break
elif pick_up_res == PickedUpResult.PickedUp:
self._decrement_need_if_consumable(item)
self._picked_up_items.append(item)
elif pick_up_res == PickedUpResult.PickedUpFailed:
# Blacklist this item ID so we don't retry it this session.
# Gold piles with different amounts have different IDs and would otherwise
# loop forever because _yoink_item always returns PickedUp even on failure.
Logger.debug(f"Blacklisting {item.Name} (ID={item.ID}) after failed pickup — won't retry this session")
self._cached_pickit_items[item.ID] = False
self._picked_up_item = pick_up_res == PickedUpResult.PickedUp
item_count+=1
+3 -1
View File
@@ -106,7 +106,7 @@ class Logger:
@staticmethod
def install_exception_hooks():
def log_uncaught_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
if issubclass(exc_type, (KeyboardInterrupt, SystemExit)):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
Logger.error(
@@ -115,6 +115,8 @@ class Logger:
)
def log_thread_exception(args):
if issubclass(args.exc_type, SystemExit):
return
Logger.error(
f"Uncaught exception in thread {args.thread.name}:\n"
+ "".join(traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback))
+3
View File
@@ -206,11 +206,14 @@ def search_and_wait(
:Other params are the same as for template_finder.search()
:returns a TemplateMatch object
"""
from utils.misc import should_stop
if not suppress_debug:
Logger.debug(f"Waiting for templates: {ref}")
start = time.time()
template_match = TemplateMatch()
while (time_remains := time.time() - start < timeout):
if should_stop():
raise SystemExit()
img = grab()
is_loading_black_roi = np.average(img[:, 0:Config().ui_roi["loading_left_black"][2]]) < 1.0
if not is_loading_black_roi or "LOADING" in ref:
+69 -10
View File
@@ -63,11 +63,17 @@ class TownManager:
return self._acts[curr_act].wait_for_tp()
def open_wp(self, curr_loc: Location):
from health_manager import set_panel_check_paused
curr_act = TownManager.get_act_from_location(curr_loc)
if curr_act is None: return False
return self._acts[curr_act].open_wp(curr_loc)
set_panel_check_paused(True)
res = self._acts[curr_act].open_wp(curr_loc)
if not res:
set_panel_check_paused(False)
return res
def go_to_act(self, act_idx: int, curr_loc: Location) -> Location | bool:
from health_manager import set_panel_check_paused
curr_act = TownManager.get_act_from_location(curr_loc)
if curr_act is None: return False
# check if we already are in the desired act
@@ -82,8 +88,12 @@ class TownManager:
if curr_act == act:
return curr_loc
# if not, move to the desired act via waypoint
if not self._acts[curr_act].open_wp(curr_loc): return False
set_panel_check_paused(True)
if not self._acts[curr_act].open_wp(curr_loc):
set_panel_check_paused(False)
return False
waypoint.use_wp(act = act_idx, idx = 0)
set_panel_check_paused(False)
return self._acts[act].get_wp_location()
def heal(self, curr_loc: Location) -> Location | bool:
@@ -96,17 +106,20 @@ class TownManager:
return curr_loc
def buy_consumables(self, curr_loc: Location, items: list = None):
from health_manager import set_panel_check_paused
Logger.info(f"TownManager buy_consumables: starting from {curr_loc}")
curr_act = TownManager.get_act_from_location(curr_loc)
if curr_act is None:
Logger.error("TownManager buy_consumables: could not determine act from location")
return False, items
if self._acts[curr_act].can_buy_pots():
set_panel_check_paused(True)
Logger.info(f"TownManager buy_consumables: calling open_trade_menu")
new_loc = self._acts[curr_act].open_trade_menu(curr_loc)
Logger.info(f"TownManager buy_consumables: open_trade_menu returned {new_loc}")
if not (new_loc and common.wait_for_left_inventory()):
Logger.error("TownManager buy_consumables: left inventory not detected after opening trade menu")
set_panel_check_paused(False)
return False, items
img=grab()
def buy_best_available_potion(potion_templates: list[str], quantity: int, shift_click: bool) -> bool:
@@ -170,43 +183,60 @@ class TownManager:
return curr_loc, items
def resurrect(self, curr_loc: Location) -> Location | bool:
from health_manager import set_panel_check_paused
curr_act = TownManager.get_act_from_location(curr_loc)
if curr_act is None: return False
# check if we can resurrect in current act
if self._acts[curr_act].can_resurrect():
return self._acts[curr_act].resurrect(curr_loc)
set_panel_check_paused(True)
res = self._acts[curr_act].resurrect(curr_loc)
if not res:
set_panel_check_paused(False)
return res
new_loc = self.go_to_act(4, curr_loc)
if not new_loc: return False
return self._acts[Location.A4_TOWN_START].resurrect(new_loc)
set_panel_check_paused(True)
res = self._acts[Location.A4_TOWN_START].resurrect(new_loc)
if not res:
set_panel_check_paused(False)
return res
def identify(self, curr_loc: Location) -> Location | bool:
from health_manager import set_panel_check_paused
curr_act = TownManager.get_act_from_location(curr_loc)
if curr_act is None: return False
# try current act first
if self._acts[curr_act].can_identify():
set_panel_check_paused(True)
success = self._acts[curr_act].identify(curr_loc)
if success:
wait(0.2)
# close cain dialog so inventory key is not blocked
keyboard.send("esc")
set_panel_check_paused(False)
return True
else:
set_panel_check_paused(False)
view.return_to_play()
Logger.warning(f"Could not identify in act {curr_act}, trying A5 (Cain)")
# fallback to A5 Cain
new_loc = self.go_to_act(5, curr_loc)
if not new_loc: return False
set_panel_check_paused(True)
success = self._acts[Location.A5_TOWN_START].identify(new_loc)
if success:
wait(0.2)
# close cain dialog so inventory key is not blocked
keyboard.send("esc")
set_panel_check_paused(False)
else:
set_panel_check_paused(False)
view.return_to_play()
Logger.warning("Cain not available in A5 either, skipping identification")
return success
def open_stash(self, curr_loc: Location) -> Location | bool:
from health_manager import set_panel_check_paused
curr_act = TownManager.get_act_from_location(curr_loc)
new_loc = curr_loc
@@ -215,27 +245,43 @@ class TownManager:
if not new_loc: return False
curr_act = Location.A5_TOWN_START
set_panel_check_paused(True)
new_loc = self._acts[curr_act].open_stash(new_loc)
if not new_loc: return False
if not new_loc:
set_panel_check_paused(False)
return False
return new_loc
def gamble(self, curr_loc: Location) -> Location | bool:
from health_manager import set_panel_check_paused
curr_act = TownManager.get_act_from_location(curr_loc)
if curr_act is None: return False
# check if we can Identify in current act
if self._acts[curr_act].can_gamble():
return self._acts[curr_act].gamble(curr_loc)
set_panel_check_paused(True)
res = self._acts[curr_act].gamble(curr_loc)
if not res:
set_panel_check_paused(False)
return res
new_loc = self.go_to_act(4, curr_loc)
if not new_loc: return False
return self._acts[Location.A4_TOWN_START].gamble(new_loc)
set_panel_check_paused(True)
res = self._acts[Location.A4_TOWN_START].gamble(new_loc)
if not res:
set_panel_check_paused(False)
return res
def stash(self, curr_loc: Location, items: list = None, game_stats: "GameStats" = None):
from health_manager import set_panel_check_paused
curr_act = TownManager.get_act_from_location(curr_loc)
if curr_act is None: return False, False
# check if we can stash in current act
if self._acts[curr_act].can_stash():
set_panel_check_paused(True)
new_loc = self._acts[curr_act].open_stash(curr_loc)
if not new_loc: return False, False
if not new_loc:
set_panel_check_paused(False)
return False, False
wait(1.0)
items = personal.stash_all_items(items, game_stats=game_stats)
# Convert regular Rejuv -> Full Rejuv while stash is open
@@ -243,8 +289,11 @@ class TownManager:
return new_loc, items
new_loc = self.go_to_act(5, curr_loc)
if not new_loc: return False, False
set_panel_check_paused(True)
new_loc = self._acts[Location.A5_TOWN_START].open_stash(new_loc)
if not new_loc: return False, False
if not new_loc:
set_panel_check_paused(False)
return False, False
wait(1.0)
items = personal.stash_all_items(items, game_stats=game_stats)
# Convert regular Rejuv -> Full Rejuv while stash is open
@@ -252,6 +301,7 @@ class TownManager:
return new_loc, items
def repair(self, curr_loc: Location, items: list = None):
from health_manager import set_panel_check_paused
Logger.info(f"TownManager repair: starting from {curr_loc}")
curr_act = TownManager.get_act_from_location(curr_loc)
if curr_act is None:
@@ -265,9 +315,11 @@ class TownManager:
if not new_loc:
return False, False
Logger.info("TownManager repair: opening Halbu trade/repair menu")
set_panel_check_paused(True)
new_loc = self._acts[Location.A4_TOWN_START].open_trade_and_repair_menu(new_loc)
Logger.info(f"TownManager repair: open_trade_and_repair_menu returned {new_loc}")
if not new_loc:
set_panel_check_paused(False)
return False, False
if items:
Logger.info(f"TownManager repair: selling {len(items)} items before repair")
@@ -280,6 +332,7 @@ class TownManager:
if self._acts[curr_act].can_trade_and_repair():
Logger.info(f"TownManager repair: opening trade/repair menu at {curr_act}")
set_panel_check_paused(True)
new_loc = self._acts[curr_act].open_trade_and_repair_menu(curr_loc)
Logger.info(f"TownManager repair: open_trade_and_repair_menu returned {new_loc}")
if new_loc:
@@ -291,15 +344,18 @@ class TownManager:
wait(0.1, 0.2)
common.close()
return new_loc, items
set_panel_check_paused(False)
if curr_act == Location.A5_TOWN_START:
Logger.warning("A5 repair failed, attempting Act 4 Halbu fallback")
new_loc = self.go_to_act(4, Location.A5_LARZUK)
if not new_loc:
return False, False
Logger.info("TownManager repair: fallback - opening Halbu trade/repair menu")
set_panel_check_paused(True)
new_loc = self._acts[Location.A4_TOWN_START].open_trade_and_repair_menu(new_loc)
Logger.info(f"TownManager repair: fallback open_trade_and_repair_menu returned {new_loc}")
if not new_loc:
set_panel_check_paused(False)
return False, False
if items:
Logger.info(f"TownManager repair: selling {len(items)} items before repair (fallback)")
@@ -314,9 +370,12 @@ class TownManager:
new_loc = self.go_to_act(5, curr_loc)
if not new_loc: return False, False
Logger.info("TownManager repair: opening Larzuk trade/repair menu")
set_panel_check_paused(True)
new_loc = self._acts[Location.A5_TOWN_START].open_trade_and_repair_menu(new_loc)
Logger.info(f"TownManager repair: open_trade_and_repair_menu returned {new_loc}")
if not new_loc: return False, False
if not new_loc:
set_panel_check_paused(False)
return False, False
if items:
Logger.info(f"TownManager repair: selling {len(items)} items before repair")
items = personal.transfer_items(items, "sell")
+27 -18
View File
@@ -11,8 +11,8 @@ from ui_manager import detect_screen_object, ScreenObjects
def _maybe_wrong_waypoint(act: int, idx: int):
"""
2-3% chance of selecting a wrong waypoint first, then correcting.
Simulates human misclick on the waypoint menu.
2-3% chance of "mis-aiming" at a wrong waypoint first, then correcting.
Simulates human hesitation or near-miss on the waypoint menu.
"""
try:
from utils.stealth import should_wrong_waypoint
@@ -21,25 +21,25 @@ def _maybe_wrong_waypoint(act: int, idx: int):
except Exception:
return False, act, idx
# Pick a wrong waypoint in the same act (valid act range)
# Pick a wrong waypoint in the same act to "aim" at
wrong_idx = random.randint(0, 8)
while wrong_idx == idx:
wrong_idx = random.randint(0, 8)
# Click the wrong waypoint first
# Move to the wrong waypoint first (mis-aim)
wrong_pos = (Config().ui_pos["wp_first_btn_x"], Config().ui_pos["wp_first_btn_y"] + Config().ui_pos["wp_btn_height"] * wrong_idx)
wx, wy = convert_screen_to_monitor(wrong_pos)
mouse.move(wx, wy, randomize=8)
mouse.click(button="left")
Logger.info(f"[Stealth] Misclicked waypoint index {wrong_idx}, correcting to {idx}")
mouse.move(wx, wy, randomize=15, delay_factor=[0.8, 1.2])
# Wait a bit (realizing the "mistake" or hesitating)
wait(0.2, 0.4)
Logger.debug(f"[Stealth] Simulating mis-aim at waypoint index {wrong_idx}, correcting to {idx}")
# Wait a bit (realizing the mistake), then correct
wait(0.5, 1.0)
# Click the correct waypoint
# Now move to and click the correct waypoint
pos_wp_btn = (Config().ui_pos["wp_first_btn_x"], Config().ui_pos["wp_first_btn_y"] + Config().ui_pos["wp_btn_height"] * idx)
x, y = convert_screen_to_monitor(pos_wp_btn)
mouse.move(x, y, randomize=[60, 9], delay_factor=[0.9, 1.4])
mouse.move(x, y, randomize=[30, 5], delay_factor=[0.6, 1.0])
wait(0.1, 0.2)
mouse.click(button="left")
return True, act, idx
@@ -96,6 +96,7 @@ def use_wp(label: str = None, act: int = None, idx: int = None) -> bool:
:param act: Index of the desired act starting at 1 [A1 = 1, A2 = 2, A3 = 3, ...]
:param idx: Index of the waypoint from top. Note that it start at 0!
"""
from health_manager import set_panel_check_paused
if label:
act = _WAYPOINTS[label][0]
idx = _WAYPOINTS[label][1]
@@ -103,6 +104,7 @@ def use_wp(label: str = None, act: int = None, idx: int = None) -> bool:
curr_active_act = get_active_act_from_match(match)
else:
Logger.error("Could not find waypoint tabs")
set_panel_check_paused(False)
return False
if curr_active_act != act:
pos_act_btn = (Config().ui_pos["wp_act_i_btn_x"] + Config().ui_pos["wp_act_btn_width"] * (act - 1), Config().ui_pos["wp_act_i_btn_y"])
@@ -119,12 +121,19 @@ def use_wp(label: str = None, act: int = None, idx: int = None) -> bool:
mouse.move(x, y, randomize=[60, 9], delay_factor=[0.9, 1.4])
wait(0.4, 0.5)
mouse.click(button="left")
# wait till loading screen is over
if loading.wait_for_loading_screen(5):
while 1:
if not loading.wait_for_loading_screen(0.2):
return True
return False
# wait till loading screen is over if we changed acts
if curr_active_act != act:
if loading.wait_for_loading_screen(8):
while 1:
if not loading.wait_for_loading_screen(0.2):
break
else:
# same act, just wait a bit for the menu to close animation to finish
wait(0.3, 0.5)
set_panel_check_paused(False)
return True
def get_active_act_from_match(match):
try:
+39 -1
View File
@@ -168,7 +168,32 @@ def restore_d2r_window_visibility():
else:
print('OS not supported, unable to set D2R always on top')
_stop_conditions = {}
_stop_conditions_lock = threading.Lock()
def register_stop_condition(thread_id, condition_fn):
"""Registers a callback function that returns True if the thread should stop."""
with _stop_conditions_lock:
_stop_conditions[thread_id] = condition_fn
def unregister_stop_condition(thread_id):
"""Unregisters the stop condition for a thread."""
with _stop_conditions_lock:
if thread_id in _stop_conditions:
del _stop_conditions[thread_id]
def should_stop():
"""Checks if the current thread has been signalled to stop."""
thread_id = threading.get_ident()
with _stop_conditions_lock:
if thread_id in _stop_conditions:
return _stop_conditions[thread_id]()
return False
def wait(min_seconds, max_seconds = None):
if should_stop():
raise SystemExit()
if max_seconds is None:
max_seconds = min_seconds
base = random.uniform(min_seconds, max_seconds)
@@ -184,7 +209,20 @@ def wait(min_seconds, max_seconds = None):
jitter = max(jitter_min * 0.8, min(jitter_max * 1.2, jitter))
except Exception:
jitter = 1.0
time.sleep(base * jitter)
# Break long sleeps into smaller chunks to check for shutdown more frequently
sleep_time = base * jitter
chunk_size = 0.2
while sleep_time > chunk_size:
time.sleep(chunk_size)
if should_stop():
raise SystemExit()
sleep_time -= chunk_size
if sleep_time > 0:
time.sleep(sleep_time)
if should_stop():
raise SystemExit()
return
def _force_kill_thread(thread):