Nik Afiq a9f0bbe42f Implement login flow from title screen to home screen
- Added `login.py` to handle the login process, including detection of various states such as the title screen, network error notices, loading transitions, daily attendance cards, and the home screen.
- Integrated a recovery mechanism to relaunch the game if it gets stuck during loading.
- Updated `ba_daily.py` to include the login task in the default execution order, ensuring it runs first to navigate to the home screen before other tasks.
- Fixed a bug in the home detection logic to accurately identify when the user is on the true home screen.
- Live-tested and calibrated the login flow against real game states, confirming functionality and addressing any issues encountered during testing.
2026-07-16 11:07:43 +09:00

235 lines
12 KiB
Python

"""Login: title screen through daily popups to the true home screen.
Reference flow: `module/restart.py`'s `implement`/`start` (check the app is
running, launch it if not) hands off into `core/Baas_thread.py`'s
`to_main_page` -- the reference's own generic post-launch arrival routine.
`to_main_page` is a `picture.co_detect` call wired with ~20 named
img_reactions/rgb_possibles (download notices, the daily attendance card,
a login-feature/login-store banner, news, rank-up cutscenes, etc.):
whatever recognized state appears gets clicked through, repeatedly, until
the 'main_page' rgb state is reached. There's no separate "login" module in
the reference -- it's baked into that shared arrival routine, reused by
every other feature's own navigation too.
This project doesn't have image-template assets for that whole catalog, so
rather than one-off hand-calibrating every possible reference dialog, this
port scopes down to what was actually confirmed live on nik-gpu (see below)
plus a bounded generic "press Enter" fallback for anything else recognized
neither as the title screen nor the news popup -- Enter is this project's
own established safe dismiss/advance action for exactly this class of
one-off notice/card (see gem_shop.py's `_claim_free_package`, circle.py's
`_dismiss_reward`), and this flow never reaches a screen where Enter is
dangerous (that hazard, per CLAUDE.md, is specific to in-battle/spend
confirmations arena.py/story_sweep.py guard against, not the pre-login
popup chain). This mirrors the shape of the reference's own co_detect
fallback (a blind action once nothing recognized matches for a while)
while staying closer to this project's own established idiom than
reference's blind coordinate tentative_click.
Live-calibrated 2026-07-16 on nik-gpu by driving the real flow end-to-end
from the account's actual overnight login-screen state (native 1920x1200
captures throughout, saved to scratchpad/login_probe_*.png during the
session -- NOT the non-native screenshots/daily_login/*.png the user
originally supplied, same "not 1:1 with real coordinates" gap already
documented for screenshots/gem_shop/ and screenshots/cafe/student/).
Confirmed states, in the order they were actually hit:
1. Title screen ("TOUCH TO START") -- `_logo_state` reads "bright" via the
ブルーアーカイブ logo's fixed brand-chrome color (see config.py's
`LOGIN_LOGO_*`), confirmed identical across two completely different
seasonal background arts. Click `LOGIN_TOUCH_TO_START`.
2. A real "ネットワークへの接続に失敗しました" (network connection failed)
notice, live-hit unprompted during calibration -- `_logo_state` reads
"dimmed" (same logo, uniformly darkened by the notice's own overlay).
Enter dismisses it, dropping back to the plain title screen (state 1
again).
3. A loading transition. Two visually distinct variants were both hit
live: a brief one that keeps the logo/UID chrome visible (resolves in a
few seconds), and a full-bleed one that replaces the ENTIRE screen with
rotating splash art and a center spinner, no chrome at all. The first
real run's loading genuinely got stuck in the second variant for over 6
minutes with zero progress (confirmed not a network issue -- ping and
the game process were both healthy) -- the user's own live guidance was
"kill the game and rerun it", identifying `/usr/local/bin/
launch-blue-archive.sh` as the relaunch entry point (see `_recover` and
config.py's `GAME_PROCESS_NAME`/`GAME_LAUNCH_SCRIPT`). This directly
parallels the reference's own `restart.py` kill+relaunch pattern, not
an invented workaround. A second attempt after relaunching completed the
entire remaining flow in well under 15 seconds, confirming the first
attempt's multi-minute stall was a genuine stuck state, not normal
loading variance.
4. アロナの毎日出席簿 (Arona's daily attendance card, a 10-day stamp
calendar) -- only appears if not yet claimed today (confirmed: absent
entirely on a same-day second run after the first run's Enter already
claimed it). No distinct visual detection was built for this specific
card -- it falls through to the generic Enter fallback like every other
unrecognized pre-home state, confirmed live to correctly claim/dismiss
it in one press.
5. True home, usually with the S.C.H.A.L.E NEWS popup open on top --
`navigation.is_on_subscreen`/`is_modal_open` both proved unreliable for
detecting this dialog specifically (confirmed live via direct pixel
comparison: it overlays home directly, and is_modal_open's probe point
lands on the dialog's own bright header/body rather than a dimmed
backdrop, reading "no modal" whether it's open or not -- same class of
mismatch already documented for gem_shop.py's own dialog). Fixed with a
dedicated `_news_dialog_open` multi-point header-color check; closed via
its own X button (`LOGIN_NEWS_CLOSE_BUTTON`), not Enter -- not
confirmed to be Enter-bound, unlike every other popup in this flow.
`_true_home` combines this with the existing shared
is_on_subscreen/is_modal_open so the terminal check is correct whether
or not the news popup happens to show that day.
One additional real client bug surfaced live but is explicitly NOT ported
around: a known intermittent rendering bug (per the user, pre-existing and
unrelated to this project) where the news popup's own promotional image can
get stuck on-screen as a blank white rectangle after the dialog is
otherwise closed. Confirmed live twice in the same session -- present once,
absent on an immediately-following clean run with identical navigation, so
not deterministic. Confirmed harmless to this task specifically: pixel-
checked live that the stuck rectangle sits well clear of every probe point
this module and navigation.py use (`LOGIN_LOGO_PROBES`,
`LOGIN_NEWS_HEADER_PROBES`, `navigation.SUBSCREEN_HEADER_PROBE`,
`navigation.MODAL_DIM_PROBE`), and `navigation.is_on_subscreen`/
`is_modal_open` both still correctly read true-home with the artifact
on-screen. The user's own fix (reload the app) is already this module's
existing stuck-recovery path, so no separate handling was added.
A real correctness bug was found and fixed the same session, via the actual
module's own first live run (not manual clicking): `_true_home`'s original
definition (not-subscreen AND not-modal AND no-news-dialog) is a set of
negative conditions never actually calibrated against the pre-login states
this module itself introduces -- both the plain title screen AND the
unclaimed daily attendance card independently satisfy all three of them
(neither looks like a subscreen or an open modal to those checks, which
were built assuming the game world is always already reached). The first
real `login` run false-positived "reached home" while still sitting on the
title screen, before ever clicking anything. Fixed by adding
`_home_nav_bar_visible`, a positive check for the bottom nav bar's own
flat, near-white background band -- confirmed live to hold uniquely on
true home and fail on every other captured state, including home with the
news dialog still open (which dims that same band). Re-run afterward: a
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.
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
screenshots showed -- did not appear during calibration (this account was
not in that state). Expected to fall through to the same generic Enter
fallback as the daily attendance card, matching the same "any one-off
pre-home card responds to Enter" pattern already confirmed for state 4
above and the network notice in state 2, but not yet exercised for real.
Added to `ba_daily.py`'s `DEFAULT_ORDER` as the very first step, ahead of
mailbox -- every other task's own navigation assumes the home screen is
already reachable via `navigation.return_to_home`'s ordinary
Escape/BACK_BUTTON press loop, which has no way to get there from the
title/loading/attendance-card states this module handles. `_wait_for_home`
checks true-home FIRST on every iteration (same contract as
`navigation.wait_for_state`), so a session that's already logged in and
sitting at home is a fast no-op, not a risky blind click.
"""
import time
from ba_auto import navigation
def _color_matches(rgb, target, tolerance):
return all(abs(c - t) <= tolerance for c, t in zip(rgb, target))
def _logo_state(driver, config):
colors = driver.colors_at(config.LOGIN_LOGO_PROBES)
if all(_color_matches(c, config.LOGIN_LOGO_BRIGHT_RGB, config.LOGIN_LOGO_COLOR_TOLERANCE) for c in colors):
return "bright"
if all(_color_matches(c, config.LOGIN_LOGO_DIMMED_RGB, config.LOGIN_LOGO_COLOR_TOLERANCE) for c in colors):
return "dimmed"
return "absent"
def _news_dialog_open(driver, config):
colors = driver.colors_at(config.LOGIN_NEWS_HEADER_PROBES)
return all(_color_matches(c, config.LOGIN_NEWS_HEADER_RGB, config.LOGIN_NEWS_HEADER_TOLERANCE) for c in colors)
def _home_nav_bar_visible(driver, config):
for r, g, b in driver.colors_at(config.LOGIN_HOME_NAV_BAR_PROBES):
if min(r, g, b) < config.LOGIN_HOME_NAV_BAR_MIN_CHANNEL:
return False
if max(r, g, b) - min(r, g, b) > config.LOGIN_HOME_NAV_BAR_MAX_SPREAD:
return False
return True
def _true_home(driver, config):
return (
_home_nav_bar_visible(driver, config)
and not navigation.is_on_subscreen(driver)
and not navigation.is_modal_open(driver)
and not _news_dialog_open(driver, config)
)
def _wait_for_home(driver, config):
start = time.time()
while time.time() - start < config.LOGIN_TIMEOUT_SECONDS:
if _true_home(driver, config):
return True
if _news_dialog_open(driver, config):
driver.click(*config.LOGIN_NEWS_CLOSE_BUTTON)
elif _logo_state(driver, config) == "bright":
driver.click(*config.LOGIN_TOUCH_TO_START)
else:
# Dimmed title-screen notice, a loading transition, the daily
# attendance card, an infrequent login-bonus card, or any other
# one-off dialog -- see module docstring for why a blind Enter
# is this flow's established safe generic action here.
driver.keypress("Return")
driver.wait(config.LOGIN_POLL_INTERVAL)
return False
def _wait_for_window(driver, config):
for _ in range(config.LOGIN_RELAUNCH_WAIT_ATTEMPTS):
if driver.window_exists():
driver.wait(3)
return True
driver.wait(2)
return False
def _recover(driver, config):
print("[login] not home after the timeout -- killing and relaunching the game")
driver.kill_game()
for _ in range(config.LOGIN_KILL_WAIT_ATTEMPTS):
if not driver.is_game_running():
break
driver.wait(2)
driver.launch_game()
if not _wait_for_window(driver, config):
print("[login] warning: game window did not reappear after relaunch")
return False
driver.focus_game()
return True
def run(driver, config):
if not driver.window_exists():
print("[login] game window not found -- launching")
driver.launch_game()
if not _wait_for_window(driver, config):
print("[login] warning: game window never appeared after launch -- giving up")
return
driver.focus_game()
for attempt in range(1, config.LOGIN_MAX_RELAUNCHES + 1):
if _wait_for_home(driver, config):
print("[login] reached home")
return
if attempt < config.LOGIN_MAX_RELAUNCHES:
_recover(driver, config)
print(f"[login] warning: could not reach home after {config.LOGIN_MAX_RELAUNCHES} attempts -- manual check needed")