From 98f9b7302e9917f05055df7dbe13387aa9141b61 Mon Sep 17 00:00:00 2001 From: alexpolo1 Date: Mon, 14 Sep 2026 18:35:28 +0200 Subject: [PATCH] fix(baal_xp): restore the 09-08 join hardening lost in the 09-09 branch switch The stash taken before switching to main (stash@{1}) carried a re-verified _try_join - row click retried up to 3x with a re-OCR of the row when the list scrolled, JOIN GAME press retried up to 3x when the lobby swallowed it, and a join_game() default of 900s - but the switch was stashed, never re-applied, and the branch tip kept the single-click version from 09-02. A single missed click (or a list that scrolled between scan and click) then failed the whole join, which is exactly the 'no matching game found' the cycle kept hitting. Also add the finally: start_detecting_window() the 09-08 version had dropped: join_game() stops the window-detection thread at the top, and every return path (success, failure, exception) has to re-arm it or screen tracking stays dead for the rest of the cycle. --- src/ui/game_browser.py | 67 ++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/src/ui/game_browser.py b/src/ui/game_browser.py index 29d06cf..b65aa76 100644 --- a/src/ui/game_browser.py +++ b/src/ui/game_browser.py @@ -408,35 +408,67 @@ def _try_join(cand: dict) -> bool | None: True = joined False = that game was full or gone, try the next candidate None = we are no longer in a lobby state where retrying makes sense - """ + + Every click is re-verified against the screen: a single click missed by a + frame (window focus, a covered button, a list that scrolled) used to end + the whole join attempt, and the caller then reported "no matching game" + after a few seconds of a perfectly good lobby. Re-click the row when the + JOIN GAME button does not appear, re-OCR the row when the name no longer + matches (the list scrolled), and retry the JOIN press when the first one + is swallowed. Only give up on a candidate after the retries are spent.""" Logger.info(f"game_browser: joining '{cand['name']}' " f"({cand['players'] if cand['players'] is not None else '?'} players, row {cand['row']})") - _click((LIST_X + LIST_W // 2, cand["y_center"]), "game row") - wait(0.3, 0.5) - if not is_visible(ScreenObjects.LobbyJoinBtn): - # Usually a leftover error dialog sitting on top of the lobby. Clear it - # and let the caller try the next candidate rather than aborting. + for click_attempt in range(3): + _click((LIST_X + LIST_W // 2, cand["y_center"]), "game row") + wait(0.3, 0.5) + if is_visible(ScreenObjects.LobbyJoinBtn): + break if is_visible(ScreenObjects.ServerError): + # Usually a leftover error dialog sitting on top of the lobby. Clear + # it and let the caller try the next candidate rather than aborting. Logger.warning("game_browser: error dialog over the lobby - dismissing") error_screens.handle_error() return False + if not _in_lobby(): + Logger.error("game_browser: left the lobby while selecting a row") + return None + # The list may have scrolled between the scan and now: re-read the row + # we are actually pointing at before burning another click on it. + img = grab() + actual = _ocr_roi(img, [LIST_X, cand["y_center"] - 9, LIST_W, ROW_H]) + if actual and actual != cand["name"]: + Logger.debug(f"game_browser: row now reads '{actual}' (was '{cand['name']}') - list moved, re-scanning") + return False + Logger.debug(f"game_browser: JOIN GAME button not up after row click {click_attempt + 1}/3 - re-clicking") + wait(0.5, 0.8) + + if not is_visible(ScreenObjects.LobbyJoinBtn): if _in_lobby(): Logger.warning("game_browser: JOIN GAME button not visible but still in the lobby - trying the next") return False Logger.error("game_browser: JOIN GAME button not visible after selecting a row") return None - _click(JOIN_BTN, "JOIN GAME") - if _wait_for_join(): - return True - if is_visible(ScreenObjects.LobbyJoinBtn): - Logger.info(f"game_browser: '{cand['name']}' was full or gone - trying the next candidate") - # The lobby swallows input for a moment after a bounced join. - wait(1.0, 1.5) - return False - return None + + # The first JOIN press is the one that gets swallowed (lobby input lag right + # after a row click). Retry it before calling the candidate a failure. + for press in range(3): + _click(JOIN_BTN, "JOIN GAME") + if _wait_for_join(): + return True + if is_visible(ScreenObjects.LobbyJoinBtn): + Logger.info(f"game_browser: '{cand['name']}' was full or gone - trying the next candidate") + # The lobby swallows input for a moment after a bounced join. + wait(1.0, 1.5) + return False + if not _in_lobby(): + Logger.error("game_browser: no longer in the lobby after a JOIN GAME press") + return None + Logger.debug("game_browser: JOIN GAME press produced no state change - retrying") + wait(0.5, 0.8) + return False -def join_game(name_filter: str = "", max_wait_s: float = 60.0, +def join_game(name_filter: str = "", max_wait_s: float = 900.0, difficulty: str = "normal", min_players: int = 3, max_players: int = 7) -> bool: """ @@ -524,6 +556,9 @@ def join_game(name_filter: str = "", max_wait_s: float = 60.0, Logger.error(f"game_browser: no matching game found within {max_wait_s}s") return False finally: + # Re-arm the window-detection thread on every exit path (success, + # failure, or an exception mid-join) - leaving it stopped strands the + # bot's screen tracking for the rest of the cycle. start_detecting_window()