- 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.
235 lines
12 KiB
Python
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")
|