diff --git a/config/fg_daily_estimates.json b/config/fg_daily_estimates.json index 59c374a..05eefe5 100644 --- a/config/fg_daily_estimates.json +++ b/config/fg_daily_estimates.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-08-26T06:13:10.206599+00:00", + "generated_at": "2026-09-02T06:11:52.162227+00:00", "mode": "offline-improved", "offline_dir": "data\\d2jsp_pages", "ladder_start_date": "2026-05-20", @@ -8,7 +8,7 @@ "skipped": { "blocked": 2, "no_topic": 0, - "no_date": 868, + "no_date": 872, "no_item": 0 }, "filters": { diff --git a/config/params.ini b/config/params.ini index da40619..cdaa2c5 100644 --- a/config/params.ini +++ b/config/params.ini @@ -665,11 +665,14 @@ join_timeout_s=180 ; Difficulty of the public games to join: normal | nightmare | hell. ; Hiding in a game above the character's level is a death sentence. difficulty=normal -; Join games in this player band. Baal runs fill within seconds, so 1-2 players -; usually means nobody is running; 8 is full and the join bounces back to the -; lobby. This is a PREFERENCE, not a hard gate: when no game in the band is -; listed, the best remaining game is joined anyway and the in-game checks -; (portal wait, party size, XP idle) decide whether it is worth staying. +; Join games in this player band. This is a PREFERENCE, not a hard gate: when +; no game in the band is listed, the best remaining game is joined anyway and +; the in-game checks (portal wait, party size, XP idle) decide whether it is +; worth staying. Keep min_players at 1: a public Baal game spawns at 1-2 players +; and fills within seconds, so a 3-player floor skips every real game and the +; join fails every cycle (observed 2026-09-02: all listed games read 1-2p and +; were skipped, the fallback candidate's join never completed, and the cycle +; failed until the controller's circuit breaker restarted D2R). min_players=1 max_players=7 ; Take the host's town portal to the Throne rather than hiding in town. diff --git a/src/config.py b/src/config.py index c94eab6..e0c20f4 100644 --- a/src/config.py +++ b/src/config.py @@ -404,9 +404,11 @@ class Config: # character can actually survive while hiding. "difficulty": self._normalize_difficulty( self._select_optional("baal_xp", "difficulty", "normal")), - # A public Baal run fills within seconds; a game still showing one - # or two players is someone idling, not a run. - "min_players": int(self._select_optional("baal_xp", "min_players", "3")), + # A public Baal game spawns at 1-2 players and fills within seconds, + # so a floor above 1 skips every real game and the join fails every + # cycle. Keep the default at 1; the in-game checks (portal wait, + # party size, XP idle) are what decide whether a game is worth staying. + "min_players": int(self._select_optional("baal_xp", "min_players", "1")), # Take the host's town portal to the Throne. XP is shared by area, # so hiding in town earns nothing while the run is in the Worldstone. "take_portal": self._select_optional("baal_xp", "take_portal", "1") not in (None, "", "0", "false", "False"), diff --git a/src/ui/game_browser.py b/src/ui/game_browser.py index ebf91ca..29d06cf 100644 --- a/src/ui/game_browser.py +++ b/src/ui/game_browser.py @@ -168,19 +168,37 @@ def open_lobby(timeout: float = 20.0) -> bool: """From the character-select screen, click LOBBY and select the JOIN GAME tab. No-op (returns True) if that is already the state.""" if not _in_lobby(): - if not (lb := detect_screen_object(ScreenObjects.LobbyBtn)).valid: - Logger.error("game_browser: not at character select - cannot open the lobby") - return False - # LOBBY and PLAY are inert until a character is selected, so a click on a - # perfectly-detected button silently does nothing. Say so instead of - # burning the full timeout on a misleading "lobby did not open". - if not is_visible(ScreenObjects.SelectedCharacter): - Logger.error("game_browser: no character selected - LOBBY will not respond. " - "The character list is empty or none is highlighted " - "(a dropped Battle.net session does this); log in again.") - return False - select_screen_object_match(lb) - + # The LOBBY button is the "am I at character select" signal, but its + # template is the one that goes stale (after the mod font was removed + # it scored 0.70 against the 0.8 threshold and every save-and-exit + # failed with "not at character select" while the screen was plainly + # showing). Retry the detect a few times before giving up, and fall back + # to a coordinate click on the known LOBBY position when the template + # never resolves - a covered or stale template must not stall the whole + # join, since the button is at a fixed spot on this screen. + lb = None + for _ in range(3): + lb = detect_screen_object(ScreenObjects.LobbyBtn) + if lb.valid: + break + wait(0.4, 0.7) + if lb.valid: + # LOBBY and PLAY are inert until a character is selected, so a click on + # a perfectly-detected button silently does nothing. Say so instead of + # burning the full timeout on a misleading "lobby did not open". + if not is_visible(ScreenObjects.SelectedCharacter): + Logger.error("game_browser: no character selected - LOBBY will not respond. " + "The character list is empty or none is highlighted " + "(a dropped Battle.net session does this); log in again.") + return False + select_screen_object_match(lb) + else: + if not at_character_select(): + Logger.error("game_browser: not at character select - cannot open the lobby") + return False + Logger.warning("game_browser: LOBBY button template not matching - " + "falling back to a coordinate click") + _click(LOBBY_BTN, "LOBBY (coordinate fallback)") start = time.time() while not _in_lobby(): if time.time() - start > timeout: @@ -449,8 +467,14 @@ def join_game(name_filter: str = "", max_wait_s: float = 60.0, Logger.warning("game_browser: already in a game") return True + # open_lobby can fail on a transient template miss or a covered window. + # Retry once before giving up - the join is the whole point of the cycle. if not open_lobby(): - return False + Logger.warning("game_browser: open_lobby failed - retrying once") + wait(1.0, 1.5) + if not open_lobby(): + Logger.error("game_browser: could not open the lobby") + return False # Difficulty first: it changes which games the list shows. if not set_difficulty(difficulty):