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:
+8
-8
@@ -176,7 +176,7 @@ order=run_pindle
|
||||
; These configs have to be alligned with your d2r settings and char build
|
||||
; type: character build profile to use (must match one section below)
|
||||
; examples: blizz_sorc, hammerdin, fohdin
|
||||
type=fohdin
|
||||
type=hammerdin
|
||||
; belt_rows: in-game belt size (2/3/4)
|
||||
belt_rows=4
|
||||
; casting_frames: your char cast breakpoint (affects action timing)
|
||||
@@ -206,12 +206,12 @@ potion2=2
|
||||
potion3=3
|
||||
potion4=4
|
||||
; show_belt is different from the default hotkey as "~" is for many keyboards not reachable without also pressing altgr
|
||||
show_belt=n
|
||||
show_belt=k
|
||||
show_items=alt
|
||||
; stand_still cannot be the default "shift" as it would interfere with merc healing
|
||||
stand_still=capslock
|
||||
; teleport: leave empty if you can't use
|
||||
teleport=
|
||||
teleport=b
|
||||
town_portal=6
|
||||
; call to arms settings:
|
||||
; weapon_switch/battle_orders/battle_command only used when cta_available=1
|
||||
@@ -372,8 +372,8 @@ foh=f6
|
||||
holy_bolt=f7
|
||||
|
||||
[hammerdin]
|
||||
blessed_hammer=f4
|
||||
concentration=f5
|
||||
blessed_hammer=f1
|
||||
concentration=f8
|
||||
|
||||
; =========================
|
||||
; ==== Builds: Warlock ====
|
||||
@@ -531,13 +531,13 @@ buff_2=
|
||||
; select_runs_key: open run selector UI
|
||||
select_runs_key=pagedown
|
||||
; restore_settings_from_backup_key: restore backed-up D2R settings
|
||||
restore_settings_from_backup_key=f10
|
||||
restore_settings_from_backup_key=insert
|
||||
; settings_backup_key: create backup of current D2R settings
|
||||
settings_backup_key=pause
|
||||
; auto_settings_key: auto-apply required D2R settings
|
||||
auto_settings_key=pageup
|
||||
; graphic_debugger_key: toggle on-screen debug layers
|
||||
graphic_debugger_key=insert
|
||||
graphic_debugger_key=delete
|
||||
; resume_key: start/pause bot loop
|
||||
resume_key=f11
|
||||
; exit_key: hard stop bot
|
||||
@@ -561,7 +561,7 @@ message_headers=
|
||||
; ocr_during_pickit: 1 = run OCR while looting (slower, more diagnostics)
|
||||
ocr_during_pickit=0
|
||||
;use "can_teleport_natively" or "can_teleport_with_charges" if you want to force certain behavior in case autodetection isn't working properly
|
||||
override_capabilities=
|
||||
override_capabilities=can_teleport_natively
|
||||
; pathing_delay_factor: movement/click delay multiplier (1 fast .. 10 slow)
|
||||
pathing_delay_factor=2
|
||||
; If you want to control Hyper-V window from host use 0,51 here
|
||||
|
||||
+21
-9
@@ -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"]:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user