From e6ba9ad830663ee32c85cd5fce8677bf8622076f Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Fri, 28 Aug 2026 16:44:33 +0200 Subject: [PATCH 1/2] fix(char): guard walk() against HUD clicks too, and set a >=5h session budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit walk() carried the identical unguarded click that move() had: x, y = convert_abs_to_monitor(pos_abs) mouse.move(x, y, randomize=5, ...) Same fix, same ordering: jitter first via _hud_safe_target, then click with randomize=0. walk() is reached from bot.py's walk-back-to-town path and from poison_necro, so it was a live second route to the same loot-filter toggle. Found while verifying PR #35 had landed — grepping main for randomize= showed a third mouse.move in i_char.py that the move()-scoped tests did not cover. session_budget_h 10 -> 8 for a >=5h run. The value is rolled at 0.65-1.35x, so setting 5 would AVERAGE five hours but could stop after 3.25. 8 gives a 5.2-10.8h window, which guarantees the five while keeping the variation. Co-Authored-By: Claude Opus 5 --- config/params.ini | 5 ++++- src/char/i_char.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/params.ini b/config/params.ini index a3f1910..53870fe 100644 --- a/config/params.ini +++ b/config/params.ini @@ -227,7 +227,10 @@ skill_hesitation_max_ms = 300 ; Raised to 10 for an overnight run: the budget is rolled at 0.65-1.35x, so 6 ; could have stopped the bot after 3.9h and left it idle. 10 guarantees at ; least ~6.5h while keeping the day-to-day variation. -session_budget_h = 10 +; Set to 8 for a >= 5h run (2026-08-28). The value is rolled at 0.65-1.35x, so +; session_budget_h=5 would average 5h but could stop after 3.25h. 8 gives a +; 5.2-10.8h window, guaranteeing the 5 hours while keeping day-to-day variation. +session_budget_h = 8 ; Small cursor movement during long idles. Between actions the cursor otherwise ; sits exactly where the last click left it. idle_drift_enabled = 1 diff --git a/src/char/i_char.py b/src/char/i_char.py index cb8dfc9..6721efb 100644 --- a/src/char/i_char.py +++ b/src/char/i_char.py @@ -309,7 +309,9 @@ class IChar: adjust_factor = max(max_wd, min(min_wd, dist - 50)) / max(min_wd, dist) pos_abs = [int(pos_abs[0] * adjust_factor), int(pos_abs[1] * adjust_factor)] x, y = convert_abs_to_monitor(pos_abs) - mouse.move(x, y, randomize=5, delay_factor=[factor*0.1, factor*0.14]) + # Same HUD guard as move(). walk() had the identical unguarded click. + x, y = self._hud_safe_target((x, y), 5) + mouse.move(x, y, randomize=0, delay_factor=[factor*0.1, factor*0.14]) wait(0.012, 0.02) if force_move: keyboard.send(Config().char["force_move"]) From ee39aa5dbfc2218195c17762839b0629dacb18ef Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Fri, 28 Aug 2026 16:57:01 +0200 Subject: [PATCH 2/2] test: scope the HUD-click invariant to every movement method, not just move() walk() carried the identical unguarded click that move() had and was missed because the tests were written against move() alone. This asserts the invariant across the movement methods, so a future one is caught without anyone remembering to extend the tests. Deliberately NOT covered, because relocating these breaks what they do: pick_up_item must click the item itself _remap_skill_hotkey deliberately clicks the UI cast_in_arc aims a cast direction, not a destination Only clicks that choose a DESTINATION may be moved off the HUD. A first draft of this test flagged all of the above and was wrong to; the distinction is between "go here" and "hit that". Verified by falsification: reverting walk() to randomize=5 fails with "walk: mouse.move(x, y, randomize=5, ...)". Co-Authored-By: Claude Opus 5 --- test/test_hud_clicks.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/test_hud_clicks.py b/test/test_hud_clicks.py index 93c85c2..db6159c 100644 --- a/test/test_hud_clicks.py +++ b/test/test_hud_clicks.py @@ -108,3 +108,41 @@ def test_move_clicks_with_randomization_disabled(): f"move() still randomizes after the guard, which can re-enter " f"the HUD: {line.strip()}" ) + + +def test_every_movement_click_in_ichar_is_guarded(): + """Scope the invariant to every MOVEMENT method, not one of them. + + The first version of this guard covered move(), and its tests asserted only + on move(). walk() carried the identical unguarded click and was missed — it + is reached from bot.py's walk-back-to-town path and from poison_necro, so it + was a live second route to the same loot-filter toggle. + + Only movement clicks may be relocated. Clicks that must land on a specific + thing are deliberately excluded, because moving them breaks what they do: + pick_up_item must hit the item itself + _remap_skill_hotkey deliberately clicks the UI + cast_in_arc aims a cast direction, not a destination + """ + import inspect + from char.i_char import IChar + + MOVEMENT = ("move", "walk") + + offenders = [] + for name in MOVEMENT: + fn = getattr(IChar, name, None) + if fn is None: + continue + src = inspect.getsource(fn) + body = chr(10).join(l for l in src.splitlines() if not l.strip().startswith("#")) + parts = body.split('"""') + body = parts[0] + "".join(parts[2:]) if len(parts) > 2 else parts[0] + for line in body.splitlines(): + if "mouse.move(" in line and "randomize=0" not in line: + offenders.append(f"{name}: {line.strip()}") + + assert not offenders, ( + "movement clicks that can land on the HUD (must go through " + "_hud_safe_target then move with randomize=0): " + "; ".join(offenders) + )