Fix window offset drift causing pathing and template matching failures

- src/screen.py: Add force param to find_and_set_window_position/set_window_position
  to bypass early-return guard and skip wait when called programmatically
- src/run/pindle.py: Force window re-detection before each traverse_nodes and
  select_by_template call to handle offset drift between runs
- src/pather.py: Add fallback full-image search with lower threshold (0.55) when
  cropped ROI search fails in find_abs_node_pos
- src/bot.py: Move D2R window to stable position (0,0) at game start via
  move_d2r_window; skip merc resurrect after first failure (no gold)
- src/game_stats.py: Add _merc_resurrect_failed flag (reset each game)
- src/utils/misc.py: Add move_d2r_window() function
- config/params.ini: Updated teleport=b, show_belt=k, override_capabilities,
  restore_settings_from_backup_key=insert, graphic_debugger_key=delete
This commit is contained in:
alex
2026-06-01 11:01:00 +02:00
parent 5244a50ea3
commit cb3d49fa22
7 changed files with 74 additions and 22 deletions
+21 -9
View File
@@ -261,6 +261,10 @@ class Bot:
def on_init(self):
self._game_stats.log_start_game()
keyboard.release(Config().char["stand_still"])
# Ensure D2R window is at a stable position to prevent template matching drift
from utils.misc import move_d2r_window
move_d2r_window(0, 0)
wait(0.3)
# If we're only doing run_level and character is already in-game, skip
# the town detection and go straight to the run
if list(self._do_runs.keys()) == ["run_level"]:
@@ -459,15 +463,23 @@ class Bot:
self._curr_loc = Location.A5_TOWN_START
# Check if merc needs to be revived
if not is_visible(ScreenObjects.MercIcon) and Config().char["use_merc"]:
Logger.info("Resurrect merc")
new_loc = self._town_manager.resurrect(self._curr_loc)
if new_loc is False:
# Failed to resurrect (can't afford or other error) - don't log death, just continue
Logger.warning("Failed to resurrect merc, continuing without merc")
else:
self._game_stats.log_merc_death()
self._curr_loc = new_loc
if Config().char["use_merc"]:
try:
merc_visible = is_visible(ScreenObjects.MercIcon)
except Exception:
# ROI/template shape error - skip resurrect check
Logger.debug("Merc icon detection error (ROI/template issue), skipping resurrect check")
merc_visible = True
if not merc_visible and not self._game_stats._merc_resurrect_failed:
Logger.info("Resurrect merc")
new_loc = self._town_manager.resurrect(self._curr_loc)
if new_loc is False:
# Failed to resurrect (can't afford or other error) - don't log death, just continue
Logger.warning("Failed to resurrect merc, continuing without merc")
self._game_stats._merc_resurrect_failed = True
else:
self._game_stats.log_merc_death()
self._curr_loc = new_loc
# Gamble if needed
while vendor.get_gamble_status() and Config().char["gamble_items"]:
+2
View File
@@ -88,6 +88,7 @@ class GameStats:
self._last_status_report_game = 0
self._last_status_report_run = 0
self._last_failure_reason = None
self._merc_resurrect_failed = False
os.makedirs("log/stats", exist_ok=True)
def set_failure_reason(self, reason: str):
@@ -257,6 +258,7 @@ class GameStats:
self._send_status_update()
self._game_counter += 1
self._timer = time.time()
self._merc_resurrect_failed = False
Logger.info(f"Starting game #{self._game_counter}")
self._log_event("game_started")
self._persist_snapshot()
+10
View File
@@ -547,6 +547,7 @@ class Pather:
def find_abs_node_pos(self, node_idx: int, img: np.ndarray, threshold: float = 0.68) -> tuple[float, float]:
node = self._nodes[node_idx]
# Try with cropped ROI first (cuts skill bar for better matching)
template_match = template_finder.search(
[*node],
img,
@@ -555,6 +556,15 @@ class Pather:
roi=Config().ui_roi["cut_skill_bar"],
use_grayscale=True
)
# If cropped search fails, try full image with lower threshold to handle window offset drift
if not template_match.valid:
template_match = template_finder.search(
[*node],
img,
best_match=False,
threshold=0.55,
use_grayscale=True
)
if template_match.valid:
# Get reference position of template in abs coordinates
ref_pos_abs = convert_screen_to_abs(template_match.center)
+7
View File
@@ -32,6 +32,9 @@ class Pindle:
loc = self._town_manager.go_to_act(5, start_loc)
if not loc:
return False
# Force window re-detection before pathing to handle offset drift
from screen import find_and_set_window_position
find_and_set_window_position(force=True)
if not self._pather.traverse_nodes((loc, Location.A5_NIHLATHAK_PORTAL), self._char):
return False
wait(0.5, 0.6)
@@ -39,11 +42,15 @@ class Pindle:
if do_pre_buff:
self._char.pre_buff()
found_loading_screen_func = lambda: loading.wait_for_loading_screen(2.0)
# Re-detect window before template search
find_and_set_window_position(force=True)
if not self._char.select_by_template("A5_RED_PORTAL", found_loading_screen_func, telekinesis=False):
Logger.warning("Pindle approach: first red portal click failed, retrying from town start")
find_and_set_window_position(force=True)
if not self._pather.traverse_nodes((Location.A5_TOWN_START, Location.A5_NIHLATHAK_PORTAL), self._char):
return False
wait(0.5, 0.7)
find_and_set_window_position(force=True)
if not self._char.select_by_template("A5_RED_PORTAL", found_loading_screen_func, telekinesis=False):
return False
return Location.A5_PINDLE_START
+6 -5
View File
@@ -42,16 +42,17 @@ def detect_window_position():
find_and_set_window_position()
Logger.debug('Detect window thread stopped')
def find_and_set_window_position():
def find_and_set_window_position(force: bool = False):
position = find_d2r_window(FIND_WINDOW, offset=Config(
).advanced_options["window_client_area_offset"])
if position is not None:
set_window_position(*position)
wait(1)
set_window_position(*position, force=force)
if not force:
wait(1)
def set_window_position(offset_x: int, offset_y: int):
def set_window_position(offset_x: int, offset_y: int, force: bool = False):
global monitor_roi, monitor_x_range, monitor_y_range, found_offsets
if found_offsets and monitor_roi["top"] == offset_y and monitor_roi["left"] == offset_x:
if not force and found_offsets and monitor_roi["top"] == offset_y and monitor_roi["left"] == offset_x:
return
Logger.debug(f"Set offsets: left {offset_x}px, top {offset_y}px")
monitor_roi["top"] = offset_y
+20
View File
@@ -111,6 +111,26 @@ def find_d2r_window(spec: WindowSpec, offset = (0, 0)) -> tuple[int, int]:
return (left + offset_x, top + offset_y)
return None
def move_d2r_window(x, y):
"""Move D2R window to a specific screen position to prevent offset drift."""
if os.name == 'nt':
import ctypes
from ctypes import wintypes
from win32con import SWP_SHOWWINDOW
user32 = ctypes.windll.user32
window_list = []
EnumWindows(lambda w, l: l.append((w, GetWindowText(w))), window_list)
for w in window_list:
if "Diablo II" in w[1]:
cr = wintypes.RECT()
user32.GetClientRect(w[0], ctypes.byref(cr))
w_width = cr.right
w_height = cr.bottom
SetWindowPos(w[0], HWND_TOPMOST, x, y, w_width, w_height, SWP_SHOWWINDOW)
Logger.debug(f"Moved D2R window to ({x}, {y})")
return True
return False
def set_d2r_always_on_top():
if os.name == 'nt':
for attempt in range(30):