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:
+8
-2
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user