Merge pull request #38 from alexpolo1/fix/game-menu-guard
fix: the in-game ESC menu — one bug behind three separate incidents
This commit is contained in:
@@ -38,6 +38,7 @@ class HealthManager:
|
||||
self._count_panel_detects = 0
|
||||
self._count_wp_panel_detects = 0
|
||||
self._count_center_panel_detects = 0
|
||||
self._mana_below_threshold = False
|
||||
|
||||
def stop_monitor(self):
|
||||
self._do_monitor = False
|
||||
@@ -178,6 +179,22 @@ class HealthManager:
|
||||
self._last_health = time.time()
|
||||
# check mana
|
||||
last_drink = time.time() - self._last_mana
|
||||
# Issue #23 instrumentation. "1 mana potion per game,
|
||||
# never 2" was measured over 70 games, but the cause
|
||||
# was undecidable: mana is only logged when a potion
|
||||
# is DRUNK, so a second dip that failed to trigger
|
||||
# looks identical to mana never dipping twice. Log
|
||||
# every crossing of the threshold, whether or not it
|
||||
# results in a drink.
|
||||
_mana_low = mana_percentage <= Config().char["take_mana_potion"]
|
||||
if _mana_low and not self._mana_below_threshold:
|
||||
Logger.debug(
|
||||
f"MANA> crossed below {Config().char['take_mana_potion']:.2f} "
|
||||
f"at {mana_percentage*100:.1f}% "
|
||||
f"(last drink {last_drink:.1f}s ago, gate {lp_mp_potion_delay}s) "
|
||||
f"-> {'will drink' if last_drink > lp_mp_potion_delay else 'BLOCKED by gate'}"
|
||||
)
|
||||
self._mana_below_threshold = _mana_low
|
||||
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]):
|
||||
@@ -209,6 +226,28 @@ class HealthManager:
|
||||
# A5_RED_PORTAL unfindable, 66s approach failure. It is not a
|
||||
# threat, so escape it without counting toward a chicken,
|
||||
# bounded the same way as the waypoint panel.
|
||||
# The in-game ESC menu. A stray esc opens it, and its LOOT
|
||||
# FILTER / CHRONICLE / OPTIONS buttons sit at screen centre
|
||||
# where movement clicks land — which is how the loot filter
|
||||
# got toggled, how Chronicle blocked every template match,
|
||||
# and how the bot ended up in the video options with a
|
||||
# "settings have changed" modal. It has no close button, so
|
||||
# CenterPanel cannot detect it. esc toggles it shut.
|
||||
if not self.get_panel_check_paused() and is_visible(ScreenObjects.GameMenu, img):
|
||||
self._count_center_panel_detects = getattr(self, "_count_center_panel_detects", 0) + 1
|
||||
if self._count_center_panel_detects <= self._MAX_WP_PANEL_ESCAPES:
|
||||
Logger.debug(
|
||||
f"In-game menu open (its buttons sit under movement clicks) — closing it "
|
||||
f"({self._count_center_panel_detects}/{self._MAX_WP_PANEL_ESCAPES})"
|
||||
)
|
||||
from input_layer import keyboard as kb
|
||||
kb.send("esc")
|
||||
wait(0.1, 0.2)
|
||||
fn_end = time.perf_counter()
|
||||
wait(max(0.01, (1/15 - (fn_end - fn_start)) * random.uniform(0.8, 1.2)))
|
||||
continue
|
||||
Logger.warning("In-game menu would not close — treating as a blocking panel")
|
||||
|
||||
if not self.get_panel_check_paused() and is_visible(ScreenObjects.CenterPanel, img):
|
||||
self._count_center_panel_detects = getattr(self, "_count_center_panel_detects", 0) + 1
|
||||
if self._count_center_panel_detects <= self._MAX_WP_PANEL_ESCAPES:
|
||||
|
||||
@@ -71,6 +71,21 @@ def save_and_exit() -> bool:
|
||||
Performes save and exit action from within game
|
||||
:return: Bool if action was successful
|
||||
"""
|
||||
# Pause the panel check for the WHOLE sequence. This function deliberately
|
||||
# opens the in-game ESC menu, and the health manager now closes that menu on
|
||||
# sight (its LOOT FILTER / CHRONICLE / OPTIONS buttons sit under movement
|
||||
# clicks). Callers only pause AFTER save_and_exit returns, so without this
|
||||
# the guard raced the shutdown — observed as 75 escapes across 15 games,
|
||||
# interleaved with the save/exit clicks.
|
||||
from health_manager import set_panel_check_paused
|
||||
set_panel_check_paused(True)
|
||||
try:
|
||||
return _save_and_exit_inner()
|
||||
finally:
|
||||
set_panel_check_paused(False)
|
||||
|
||||
|
||||
def _save_and_exit_inner() -> bool:
|
||||
# if exit button isn't detected already, press escape
|
||||
attempts = 1
|
||||
success = False
|
||||
|
||||
@@ -225,6 +225,16 @@ class ScreenObjects:
|
||||
threshold=0.8,
|
||||
use_grayscale=True
|
||||
)
|
||||
GameMenu=ScreenObject(
|
||||
# The in-game ESC menu. It has NO close button, so CenterPanel cannot
|
||||
# see it, and it carries LOOT FILTER / CHRONICLE / OPTIONS buttons at
|
||||
# screen centre — where the HUD mask deliberately allows clicks. A
|
||||
# stray esc opens it and the next movement click lands on a button.
|
||||
ref=["SAVE_AND_EXIT_NO_HIGHLIGHT", "SAVE_AND_EXIT_HIGHLIGHT"],
|
||||
roi="reduce_to_center",
|
||||
threshold=0.8,
|
||||
use_grayscale=True
|
||||
)
|
||||
CenterPanel=ScreenObject(
|
||||
ref=["CLOSE_PANEL_2", "CLOSE_PANEL"],
|
||||
roi="center_panel_header",
|
||||
|
||||
66
test/test_game_menu_guard.py
Normal file
66
test/test_game_menu_guard.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""The in-game ESC menu must be detected and closed.
|
||||
|
||||
2026-08-28: the bot was found sitting in OPTIONS -> VIDEO with a "settings have
|
||||
changed, apply or discard?" modal. Discarding revealed the cause — the in-game
|
||||
ESC menu carries these buttons at SCREEN CENTRE:
|
||||
|
||||
OPTIONS / SAVE AND EXIT / RETURN TO GAME / LOOT FILTER / CHRONICLE
|
||||
|
||||
A stray esc opens the menu, and the bot's next movement click lands on one of
|
||||
them. The HUD mask deliberately leaves screen centre clickable, so nothing
|
||||
stops it.
|
||||
|
||||
That single mechanism explains three separate incidents: the loot filter being
|
||||
toggled, CHRONICLE blanking every template match for a whole run, and the video
|
||||
options being opened and changed.
|
||||
|
||||
It has NO close button, so CenterPanel (which matches CLOSE_PANEL_2) cannot see
|
||||
it. Bug 31 warned about exactly this: "just send esc is WORSE: with nothing
|
||||
open, esc opens the GAME MENU".
|
||||
"""
|
||||
import inspect
|
||||
|
||||
|
||||
def test_game_menu_screenobject_exists():
|
||||
from ui_manager import ScreenObjects
|
||||
assert hasattr(ScreenObjects, "GameMenu")
|
||||
|
||||
|
||||
def test_guard_closes_the_menu_before_chickening():
|
||||
from health_manager import HealthManager
|
||||
src = inspect.getsource(HealthManager)
|
||||
assert "ScreenObjects.GameMenu" in src, "the guard never checks for the in-game menu"
|
||||
menu_at = src.index("ScreenObjects.GameMenu")
|
||||
chicken_at = src.index("Chickening to be safe")
|
||||
assert menu_at < chicken_at, "menu escape must come before the chicken path"
|
||||
|
||||
|
||||
def test_mana_threshold_crossings_are_logged():
|
||||
"""Issue #23 cannot be settled without this.
|
||||
|
||||
Mana was only logged when a potion was DRUNK, so a second dip that failed to
|
||||
trigger was indistinguishable from mana never dipping twice.
|
||||
"""
|
||||
from health_manager import HealthManager
|
||||
src = inspect.getsource(HealthManager)
|
||||
assert "MANA>" in src, "no threshold-crossing log; #23 stays undecidable"
|
||||
assert "_mana_below_threshold" in src, "crossings are not edge-triggered"
|
||||
|
||||
|
||||
def test_save_and_exit_pauses_the_panel_check():
|
||||
"""The guard must not fight the shutdown it was built to protect.
|
||||
|
||||
save_and_exit deliberately opens the in-game ESC menu. The health manager
|
||||
now closes that menu on sight, and callers only pause AFTER save_and_exit
|
||||
returns — so the guard raced the shutdown: 75 escapes across 15 games,
|
||||
interleaved with the save/exit clicks.
|
||||
"""
|
||||
import inspect
|
||||
from ui import view
|
||||
|
||||
src = inspect.getsource(view.save_and_exit)
|
||||
assert "set_panel_check_paused(True)" in src, (
|
||||
"save_and_exit opens the ESC menu without pausing the panel check, so "
|
||||
"the GameMenu guard will close it mid-shutdown"
|
||||
)
|
||||
assert "finally" in src, "the pause must be released even if save/exit raises"
|
||||
Reference in New Issue
Block a user