fix(login): enhance connectivity checks to prevent false positives in home detection
- Implemented `_connectivity_confirmed` to verify session validity by opening the Mission panel, addressing two failure modes: 1. Bright full-screen transition frames that could falsely satisfy home checks. 2. Dead sessions due to server resets or concurrent logins, which appear as normal home screens until navigation is attempted. - Improved detection logic by requiring a second verification of `_true_home` after a short wait, reducing the likelihood of coincidental false positives. - Confirmed live against real scenarios, ensuring robust handling of session states during login.
This commit is contained in:
parent
4857bd5f8f
commit
d21aadbde8
File diff suppressed because one or more lines are too long
@ -113,6 +113,88 @@ real cold start (game process not running at all) correctly launched the
|
||||
game, clicked through the title screen, and reached the genuinely-confirmed
|
||||
home screen.
|
||||
|
||||
A second real correctness bug was found via real unattended cron runs the
|
||||
following day (2026-07-17), reported live by the user together with the
|
||||
actual pulled cron logs (`~/ba_logs/daily.log`/`q4h.log`): the 4:30 AM
|
||||
`daily` fire logged `[login] reached home` cleanly, but then every single
|
||||
task that ran afterward failed to open its own screen -- garbled OCR reads,
|
||||
"screen/panel not detected after click" repeated across cafe, event_sweep,
|
||||
circle, lesson, arena, both shops, gem_shop, mailbox, and stamina -- and
|
||||
this persisted across multiple consecutive `q4h` fires spanning hours (the
|
||||
account only recovered around the 17:00 fire the following day). Per the
|
||||
user's own direct knowledge of this game's real behavior: the server's own
|
||||
daily reset (~4 AM JST) or a concurrent login from another device (e.g. the
|
||||
phone) can silently kill the session while the client keeps showing the
|
||||
last-known-good home screen with no visible error -- the error only
|
||||
surfaces once an actual navigation/API call is attempted, as the game's own
|
||||
"connection lost" popup, which no individual task's own screen-detection
|
||||
logic was built to recognize. `_true_home`'s checks are all purely visual
|
||||
(nav bar shape, subscreen/modal darkness, news-dialog color), so a dead
|
||||
session that still *looks* like home sailed straight through undetected.
|
||||
|
||||
Separately, reproducing this live (the user logged into the account on
|
||||
their phone specifically to trigger it) surfaced a THIRD, distinct false
|
||||
positive in the same family: a real run printed "reached home" while the
|
||||
game was still showing a bright, fully non-interactive loading/transition
|
||||
frame (tower splash art, zero HUD) -- a transient frame apparently bright
|
||||
and flat enough to coincidentally satisfy `_home_nav_bar_visible` for one
|
||||
screenshot, moments before the actual home screen rendered.
|
||||
|
||||
Both gaps share one fix, per the user's own suggested design ("change the
|
||||
login to enter the stamina claim/reward area then go back home"):
|
||||
`_connectivity_confirmed` now opens the Mission panel (config.MISSION_ICON,
|
||||
the same icon stamina.py uses) and closes it again, right after `_true_home`
|
||||
passes, before `_wait_for_home` will return success. A transition frame's
|
||||
false positive gets caught because the click lands on non-interactive
|
||||
background art and the panel never opens; a truly dead session gets caught
|
||||
because the game's own connection-error dialog appears instead of the
|
||||
Mission panel -- either way, `_wait_for_home` just treats it as "not home
|
||||
yet" and retries the whole cycle, which will naturally fall through to the
|
||||
existing generic Enter-fallback if an actual dialog needs dismissing, and
|
||||
escalate to `_recover`'s kill+relaunch if the whole thing never resolves
|
||||
within the timeout. **Confirmed live**: reproduced the real stuck-login
|
||||
state from the phone-login trigger, ran the actual `login` task against it,
|
||||
and caught it printing a false "reached home" while genuinely still on a
|
||||
loading transition frame (verified via a follow-up screenshot showing no
|
||||
HUD at all) -- this is the exact live evidence that led to
|
||||
`_connectivity_confirmed`.
|
||||
|
||||
That first version of `_connectivity_confirmed` used `navigation.
|
||||
is_on_subscreen` (a single-pixel check) to verify the Mission panel opened
|
||||
-- re-run live against the SAME still-stuck session and it ALSO
|
||||
false-positived: a different coincidental frame in the extended animated
|
||||
loading sequence satisfied the single subscreen-header pixel right after
|
||||
the click, on the very same run whose `_true_home` check had already been
|
||||
fooled by yet another frame. Switched to `navigation.is_header_bar_visible`
|
||||
(8 spread-out points, all must read bright) -- the same "harder to fool by
|
||||
one coincidental bright pixel" upgrade this project already made for
|
||||
cafe.py's rank-up detection, reused here for the identical reason. Re-run
|
||||
live a third time against the same still-stuck session: STILL a false
|
||||
positive, confirmed by an immediate independent follow-up check (screenshot
|
||||
plus a direct re-run of `_true_home`/`is_header_bar_visible`/
|
||||
`_connectivity_confirmed`, all reading false) moments later, on a THIRD
|
||||
different splash frame (two characters on a tank, a `巡回中` sign). This
|
||||
extended loading sequence turned out to genuinely never resolve on its own
|
||||
across three separate `login` invocations and several real minutes of
|
||||
elapsed time (matching the user's own "if this doesn't solve in 3-5
|
||||
minutes, restart" guidance) -- rich enough in distinct splash frames that
|
||||
two DIFFERENT independent checks (nav-bar shape, then header-bar
|
||||
brightness) could each individually be fooled by a different frame within
|
||||
the same run, even though neither check alone is easily fooled.
|
||||
|
||||
Rather than chasing a fourth, even-stronger single-snapshot probe,
|
||||
`_wait_for_home` now re-verifies `_true_home` a second time, after a short
|
||||
wait, before finally trusting a passed `_connectivity_confirmed` check --
|
||||
two DIFFERENT coincidental frames independently satisfying the full check
|
||||
sequence twice in a row, moments apart, is far less likely than either
|
||||
false positive alone. The stuck session itself was cleared with a manual
|
||||
kill+relaunch (the user's own direct guidance) rather than continuing to
|
||||
poke at an already-anomalous, hours-old stuck state. Deployed; not yet
|
||||
re-confirmed live against a fresh instance of either original failure mode
|
||||
end-to-end (a truly dead session, or the ordinary transition-frame case),
|
||||
though the double-confirmation logic itself follows directly from live
|
||||
evidence, not guesswork.
|
||||
|
||||
Not yet live-confirmed: the ~infrequent 業務復帰ログインボーナス
|
||||
(welcome-back login bonus, a "long trip" returning-player reward,
|
||||
`screenshots/daily_login/3_claim2(...).png`) card the user's own reference
|
||||
@ -172,11 +254,81 @@ def _true_home(driver, config):
|
||||
)
|
||||
|
||||
|
||||
def _connectivity_confirmed(driver, config):
|
||||
"""Opens the Mission panel (a cheap, always-available screen, same icon
|
||||
stamina.py uses) and closes it again, to prove the session is actually
|
||||
live -- not just that this exact screenshot happens to satisfy
|
||||
_true_home's checks. Two distinct real failure modes this catches:
|
||||
|
||||
1. A bright full-screen transition/wipe frame between loading and home
|
||||
can transiently satisfy every _true_home probe at once (confirmed
|
||||
live 2026-07-17: a real run printed "reached home" while still
|
||||
mid-transition -- no HUD at all, a tower splash-art background --
|
||||
moments before the real home screen actually rendered).
|
||||
2. A genuinely dead session: the server's own daily reset around 4 AM
|
||||
JST, or a concurrent login from another device, leaves the client
|
||||
showing a cached-looking, visually normal home screen with no error
|
||||
until an actual navigation/API call is attempted -- at which point
|
||||
the game's own "connection lost" popup appears instead of the
|
||||
requested screen. Reported live by the user with real cron logs:
|
||||
after one such silent-death login, EVERY task that ran afterward
|
||||
(cafe, event_sweep, circle, lesson, arena, both shops, gem_shop,
|
||||
mailbox, stamina) failed to open its own screen, each in its own
|
||||
inconsistent way, across multiple consecutive cron fires spanning
|
||||
hours -- because nothing had verified the session was actually
|
||||
alive before handing off to them.
|
||||
|
||||
Catching either here, once, right after login, is far more reliable
|
||||
than leaving it to be discovered piecemeal by whichever task happens to
|
||||
run next. If the click doesn't open the panel, we don't know whether
|
||||
that's cafe-empty-air (transition frame, will resolve) or a real
|
||||
connection-error dialog -- either way we just report failure and let
|
||||
_wait_for_home's own loop retry the whole cycle (including this probe)
|
||||
on its next iteration, exactly like any other not-yet-home state.
|
||||
"""
|
||||
# is_on_subscreen (a single-pixel check) was tried first and also
|
||||
# false-positived live (2026-07-17): during an extended, multi-frame
|
||||
# animated loading sequence, a *different* coincidental frame satisfied
|
||||
# the single subscreen-header pixel right after the click, on the same
|
||||
# run whose _true_home check had already been fooled by yet another
|
||||
# frame. navigation.is_header_bar_visible (8 spread-out points, all
|
||||
# must read bright) is the same "harder to fool by a single coincidental
|
||||
# bright pixel" upgrade this project already made for cafe.py's rank-up
|
||||
# detection -- reused here for the identical reason.
|
||||
driver.click(*config.MISSION_ICON)
|
||||
driver.wait(2)
|
||||
opened = navigation.is_header_bar_visible(driver)
|
||||
if opened:
|
||||
driver.keypress("Escape")
|
||||
driver.wait(1.5)
|
||||
return opened
|
||||
|
||||
|
||||
def _wait_for_home(driver, config):
|
||||
start = time.time()
|
||||
while time.time() - start < config.LOGIN_TIMEOUT_SECONDS:
|
||||
if _true_home(driver, config):
|
||||
if not _connectivity_confirmed(driver, config):
|
||||
print("[login] looked like home but the mission panel wouldn't open -- possible transition frame or dead session, retrying")
|
||||
driver.wait(config.LOGIN_POLL_INTERVAL)
|
||||
continue
|
||||
# Even the 8-point header check was fooled live (2026-07-17,
|
||||
# same session): an extended, highly-varied animated loading
|
||||
# sequence apparently cycles through enough different splash
|
||||
# frames that occasionally ONE frame coincidentally satisfies
|
||||
# _true_home and a LATER, DIFFERENT frame coincidentally
|
||||
# satisfies the connectivity probe too -- two independent
|
||||
# false positives stacking, confirmed by an immediate follow-up
|
||||
# check moments later reading false on all three signals at
|
||||
# once. A single coincidental frame is unlikely; two DIFFERENT
|
||||
# coincidental frames in a row, moments apart, is much less
|
||||
# likely still -- so re-verify _true_home again after a short
|
||||
# wait before finally trusting it.
|
||||
driver.wait(config.LOGIN_POLL_INTERVAL)
|
||||
if _true_home(driver, config):
|
||||
return True
|
||||
print("[login] home check didn't hold on re-verification -- treating as a coincidental frame, retrying")
|
||||
continue
|
||||
if _news_dialog_open(driver, config):
|
||||
driver.click(*config.LOGIN_NEWS_CLOSE_BUTTON)
|
||||
elif _logo_state(driver, config) == "bright":
|
||||
|
||||
18
plan.md
18
plan.md
@ -859,6 +859,24 @@ Times are nik-gpu's local system time, confirmed `Asia/Tokyo` (JST) via `timedat
|
||||
|
||||
**Follow-up, same day**: per explicit user request ("I also want to have logs for the cron. So I can detect if anythings go wrong. daily and q4h should have separate .log file") -- the per-preset log file separation was already in place (`~/ba_logs/<preset>.log`, one file per `$PRESET`), but a real gap was found in the failure-detection half of the ask: a genuinely crashed task and a lock-skip (an overlapping fire time, expected/harmless) both exit 1, and the original wrapper logged only a bare `(exit N)`, making them indistinguishable at a glance. Fixed by switching from the `flock -n LOCKFILE COMMAND` form to an fd-based `flock` (`exec 9>"$LOCK_FILE"; flock -n 9`), checking lock acquisition as its own explicit step separate from the wrapped command's own exit code, so every log line is now tagged with one of three explicit outcomes: `starting`/`finished ... OK (exit 0)`/`finished ... FAILED (exit N)`/`SKIPPED ... -- previous run still in progress` -- a real problem can now be found with `grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log` alone, without having to reason about ambiguous exit codes. **Verified on nik-gpu without touching the game**: a bogus-preset run correctly logged `FAILED (exit 1)`; a deliberately-forced lock-contention run (same technique as the original test) correctly logged `SKIPPED ... -- previous run still in progress` instead of a second ambiguous `(exit 1)`; the grep command above correctly surfaced both lines from `~/ba_logs/smoke_test_bad.log`. Test log removed afterward -- `~/ba_logs/` currently contains no `daily.log`/`q4h.log` yet, since no real scheduled fire has happened.
|
||||
|
||||
### Phase 18 follow-up #3: connectivity probe -- session death and false-positive home detection (2026-07-17)
|
||||
|
||||
Cron's real scheduled fires had now happened (`daily` at 3:30/4:30, `q4h` every 4h -- see follow-up #2). The user pulled the actual logs with `pull_logs.sh` and reported: the 4:30 AM `daily` fire logged a clean `[login] reached home`, but then EVERY task that ran afterward failed to open its own screen -- garbled OCR reads, "screen/panel not detected after click" repeated across cafe, event_sweep, circle, lesson, arena, both shops, gem_shop, mailbox, and stamina -- and this persisted across multiple consecutive `q4h` fires spanning hours (the account only recovered around the 17:00 fire the following day).
|
||||
|
||||
The user's own direct knowledge of the game's real behavior explained it: the server's own daily reset (~4 AM JST) or a concurrent login from another device (their phone) can silently kill the session while the client keeps showing the last-known-good home screen with no visible error -- the error only surfaces once an actual navigation/API call is attempted, as the game's own "connection lost" popup, which no individual task's screen-detection logic recognizes. `login.py`'s `_true_home` checks are all purely visual (nav bar shape, subscreen/modal darkness, news-dialog color), so a dead session that still *looks* like home sailed straight through undetected, and every downstream task discovered the same root cause independently, inconsistently, over hours. The user's own suggested fix: "change the login to enter the stamina claim/reward area then go back home."
|
||||
|
||||
The user then offered to log into the account on their phone right now to reproduce the failure live, which immediately kicked the PC client on nik-gpu into a real stuck-login-screen state (confirmed via screenshot -- a full-bleed loading/splash frame, no HUD), matching the user's own prior guidance: "if this doesn't solve after 3~5 mins restarting the game is best way."
|
||||
|
||||
Implemented `_connectivity_confirmed`: opens the Mission panel (`config.MISSION_ICON`, the same icon `stamina.py` uses) and closes it again, required to pass before `_wait_for_home` will return success. This directly catches a dead session (the game's own connection-error dialog appears instead of the panel) -- and, it turned out, ALSO an entirely separate false positive: a bright, non-interactive loading/transition frame can transiently satisfy `_true_home`'s checks. Three live iterations were needed against the real stuck session before this actually held:
|
||||
|
||||
1. First version verified the panel opened via `navigation.is_on_subscreen` (single-pixel). Live-tested against the still-stuck session: `login` printed `[login] reached home`, but an independent follow-up screenshot taken moments later showed the game still on a *different* loading/transition frame (tower splash art, zero HUD) -- a coincidental false positive, not a real recovery.
|
||||
2. Switched to `navigation.is_header_bar_visible` (8 spread-out points, all must read bright) -- the same upgrade this project already made for cafe.py's rank-up detection, for the identical "harder to fool by one coincidental bright pixel" reason. Re-tested live against the SAME still-stuck session: still a false positive, confirmed by an immediate independent re-check (screenshot plus a direct re-run of `_true_home`/`is_header_bar_visible`/`_connectivity_confirmed`, all reading false) on a THIRD different splash frame.
|
||||
3. This extended loading sequence turned out to genuinely never resolve on its own across three separate `login` invocations and several real minutes of elapsed time -- rich enough in distinct splash frames that two independent checks (nav-bar shape, then header-bar brightness) could each individually be fooled by a *different* frame within the same run. Rather than chasing a fourth, even-stronger single-snapshot probe, `_wait_for_home` now re-verifies `_true_home` a second time, after a short wait, before finally trusting a passed connectivity check -- two different coincidental frames independently satisfying the full check sequence twice in a row, moments apart, is far less likely than either false positive alone.
|
||||
|
||||
The stuck session itself was cleared with a manual kill+relaunch (the user's own direct guidance) rather than continuing to poke at an already-anomalous, hours-old stuck state. **Confirmed live**: a genuinely fresh cold-start `login` run (game process not running at all, via `pkill` then `login.py`'s own launch-if-missing path) correctly launched the game, passed through the title screen, and reached independently-verified true home -- full HUD (Lv/AP/credits/gems/bottom nav all visible), confirmed via a direct screenshot AND a separate `_true_home`/`is_header_bar_visible` check run independently, not just the module's own printed message.
|
||||
|
||||
Not yet re-confirmed: a fresh instance of the *original* reported failure mode specifically -- a session that silently dies while still looking exactly like home, discovered only when a later task tries to navigate (the cron-log scenario). The live reproduction available this session was the full-kick-to-login-screen variant (triggered by the phone login), not that exact "looks fine, breaks on navigation" variant -- though `_connectivity_confirmed` is designed to catch both by the same mechanism (a real API round-trip, not a visual check), and the false positives it exposed and fixed along the way are a strict improvement regardless.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### OCR
|
||||
|
||||
BIN
screenshots/cafe/bug_student_close_together.png
Normal file
BIN
screenshots/cafe/bug_student_close_together.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Loading…
x
Reference in New Issue
Block a user