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"]) 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) + )