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.
This commit is contained in:
parent
88cb1a933a
commit
a9f0bbe42f
@ -1159,3 +1159,92 @@ SOCIAL_ICON = (812, 1080)
|
|||||||
# サークル card, leftmost of three (サークル/フレンド/助っ人) on the
|
# サークル card, leftmost of three (サークル/フレンド/助っ人) on the
|
||||||
# ソーシャル hub page.
|
# ソーシャル hub page.
|
||||||
CIRCLE_CARD = (463, 613)
|
CIRCLE_CARD = (463, 613)
|
||||||
|
|
||||||
|
# Login flow: the "TOUCH TO START" title screen through whatever one-off
|
||||||
|
# daily popups appear (a real network-hiccup notice, the daily attendance
|
||||||
|
# card, an infrequent welcome-back login bonus, S.C.H.A.L.E NEWS) to the
|
||||||
|
# true home screen. Ports the reference's core/Baas_thread.py::to_main_page
|
||||||
|
# (its own generic post-launch arrival routine -- co_detect reacting to
|
||||||
|
# ~20 named one-off img_reactions/rgb_possibles until the 'main_page' rgb
|
||||||
|
# state is reached) plus module/restart.py's kill-and-relaunch-if-stuck
|
||||||
|
# pattern. See ba_auto/tasks/login.py's module docstring for the full
|
||||||
|
# live-calibration writeup, including a real stuck-loading incident hit
|
||||||
|
# during calibration itself.
|
||||||
|
#
|
||||||
|
# All coordinates/colors below are pixel-scanned from real scrot captures
|
||||||
|
# on nik-gpu at the native 1920x1200, not the non-native-resolution
|
||||||
|
# screenshots/daily_login/*.png reference photos the user originally
|
||||||
|
# supplied (same "not 1:1 with real game coordinates" finding already
|
||||||
|
# documented for screenshots/gem_shop/ and screenshots/cafe/student/).
|
||||||
|
GAME_PROCESS_NAME = "BlueArchive.exe"
|
||||||
|
GAME_LAUNCH_SCRIPT = "/usr/local/bin/launch-blue-archive.sh"
|
||||||
|
|
||||||
|
LOGIN_TOUCH_TO_START = (960, 1060)
|
||||||
|
# S.C.H.A.L.E NEWS popup's own X close button.
|
||||||
|
LOGIN_NEWS_CLOSE_BUTTON = (1710, 215)
|
||||||
|
|
||||||
|
# The ブルーアーカイブ logo (top-left) is fixed UI chrome, independent of
|
||||||
|
# the title screen's own rotating seasonal background art -- confirmed
|
||||||
|
# live across two completely different background pieces (a beach BBQ
|
||||||
|
# scene and a train-interior scene) reading the exact same RGB at every
|
||||||
|
# probe point. It reads one of three ways:
|
||||||
|
# bright cyan (0, 215, 250) -- clean title screen, tap to proceed
|
||||||
|
# dimmed cyan (0, 97, 114) -- a notice/dialog open on top of the title
|
||||||
|
# screen (live-confirmed: a real "network
|
||||||
|
# connection failed" error), Enter dismisses
|
||||||
|
# neither -- title screen has been left entirely
|
||||||
|
# (loading, the attendance card, home, etc.)
|
||||||
|
# Multi-point (not single-pixel), matching this project's established
|
||||||
|
# multi-point-beats-single-point technique (navigation.is_header_bar_visible,
|
||||||
|
# gem_shop's GEM_SHOP_DIALOG_PROBES) -- confirmed against every other
|
||||||
|
# captured state (loading screen, attendance card, true home) to avoid
|
||||||
|
# false-matching a background art color that coincidentally lands in range
|
||||||
|
# at any single one of these points.
|
||||||
|
LOGIN_LOGO_PROBES = ((100, 120), (300, 120), (320, 100))
|
||||||
|
LOGIN_LOGO_BRIGHT_RGB = (0, 215, 250)
|
||||||
|
LOGIN_LOGO_DIMMED_RGB = (0, 97, 114)
|
||||||
|
LOGIN_LOGO_COLOR_TOLERANCE = 20
|
||||||
|
|
||||||
|
# S.C.H.A.L.E NEWS popup's own header bar -- a solid, distinctive blue
|
||||||
|
# spanning its full width. navigation.is_modal_open/is_on_subscreen both
|
||||||
|
# proved unreliable here (same class of default-probe mismatch as
|
||||||
|
# gem_shop.py's own dialog, see GEM_SHOP_DIALOG_PROBES): the dialog
|
||||||
|
# overlays home directly, and is_modal_open's single probe point happens to
|
||||||
|
# land on the dialog's own bright header/body rather than a dimmed
|
||||||
|
# backdrop, so it reads not-dark (i.e. "no modal") whether the dialog is
|
||||||
|
# open or not -- confirmed by direct pixel comparison against the closed
|
||||||
|
# state at the same points.
|
||||||
|
LOGIN_NEWS_HEADER_PROBES = ((500, 215), (700, 215), (900, 215), (1100, 215), (1300, 215))
|
||||||
|
LOGIN_NEWS_HEADER_RGB = (30, 155, 248)
|
||||||
|
LOGIN_NEWS_HEADER_TOLERANCE = 35
|
||||||
|
|
||||||
|
# Positive confirmation that the bottom nav bar (カフェ/スケジュール/...)
|
||||||
|
# is showing: a flat, near-white, tightly-clustered background between the
|
||||||
|
# icons. Needed because navigation.is_on_subscreen/is_modal_open are BOTH
|
||||||
|
# calibrated only to distinguish in-game states from each other -- neither
|
||||||
|
# was ever designed to rule out the pre-login title screen or the daily
|
||||||
|
# attendance card, and a real live test (2026-07-16) found both of those
|
||||||
|
# states also read "not a subscreen, no modal open", the same false/false
|
||||||
|
# pattern as true home, causing login.py's very first home-check to return
|
||||||
|
# a false positive while still sitting on the title screen (a second run
|
||||||
|
# hit the same false positive while still on the unclaimed attendance
|
||||||
|
# card, which would have silently skipped that day's reward). This
|
||||||
|
# multi-point check (min channel + max spread, not a single RGB target)
|
||||||
|
# was confirmed live to uniquely hold on true home and fail on every other
|
||||||
|
# captured state (both title-screen background arts, the attendance card,
|
||||||
|
# and home with the news dialog still open and dimming this same area).
|
||||||
|
LOGIN_HOME_NAV_BAR_PROBES = ((430, 1150), (720, 1150), (940, 1150), (1230, 1150), (1430, 1150))
|
||||||
|
LOGIN_HOME_NAV_BAR_MIN_CHANNEL = 220
|
||||||
|
LOGIN_HOME_NAV_BAR_MAX_SPREAD = 25
|
||||||
|
|
||||||
|
LOGIN_POLL_INTERVAL = 2
|
||||||
|
# Generous per-attempt budget: a normal run (title tap -> loading ->
|
||||||
|
# attendance card -> home) completed in well under 15s during live
|
||||||
|
# calibration, but a real stuck-loading incident during that same session
|
||||||
|
# ran past 6 minutes with zero progress before a manual kill+relaunch was
|
||||||
|
# needed -- wide margin above the normal case, still well under the
|
||||||
|
# reference's own 600s co_detect default timeout.
|
||||||
|
LOGIN_TIMEOUT_SECONDS = 240
|
||||||
|
LOGIN_MAX_RELAUNCHES = 2
|
||||||
|
LOGIN_KILL_WAIT_ATTEMPTS = 10 # ~20s for the old process to fully exit
|
||||||
|
LOGIN_RELAUNCH_WAIT_ATTEMPTS = 45 # ~90s+ for the window to reappear
|
||||||
|
|||||||
@ -127,3 +127,42 @@ def colors_at(points):
|
|||||||
|
|
||||||
def wait(seconds):
|
def wait(seconds):
|
||||||
time.sleep(seconds)
|
time.sleep(seconds)
|
||||||
|
|
||||||
|
|
||||||
|
def kill_game():
|
||||||
|
# check=False -- pkill exits 1 with no output when nothing matches,
|
||||||
|
# which is a normal outcome here (e.g. the process already died on its
|
||||||
|
# own), not an error worth raising on.
|
||||||
|
run_command(["pkill", "-f", config.GAME_PROCESS_NAME], check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def launch_game():
|
||||||
|
# The launch script (see config.GAME_LAUNCH_SCRIPT) blocks until the
|
||||||
|
# game process exits, by design (it's meant to be run as a foreground
|
||||||
|
# session launcher) -- Popen + start_new_session=True detaches it so
|
||||||
|
# this process can keep polling for the window instead of hanging for
|
||||||
|
# the whole play session.
|
||||||
|
subprocess.Popen(
|
||||||
|
[config.GAME_LAUNCH_SCRIPT],
|
||||||
|
env=config.ENV,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
stdin=subprocess.DEVNULL,
|
||||||
|
start_new_session=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_game_running():
|
||||||
|
result = run_command(
|
||||||
|
["pgrep", "-f", config.GAME_PROCESS_NAME],
|
||||||
|
check=False, capture_output=True, text=True,
|
||||||
|
)
|
||||||
|
return bool(result.stdout.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def window_exists():
|
||||||
|
result = run_command(
|
||||||
|
["xdotool", "search", "--name", config.WINDOW_NAME],
|
||||||
|
check=False, capture_output=True, text=True,
|
||||||
|
)
|
||||||
|
return bool(result.stdout.strip())
|
||||||
|
|||||||
@ -118,7 +118,21 @@ def return_to_home(driver):
|
|||||||
Returns True once home is confirmed reached, False if still not home
|
Returns True once home is confirmed reached, False if still not home
|
||||||
after RETURN_HOME_MAX_ROUNDS -- callers should treat False as "abort,
|
after RETURN_HOME_MAX_ROUNDS -- callers should treat False as "abort,
|
||||||
don't guess further" rather than assume home was reached.
|
don't guess further" rather than assume home was reached.
|
||||||
|
|
||||||
|
Bails out immediately (False) if the game window doesn't exist at all,
|
||||||
|
rather than pressing/escalating into a guaranteed crash: the escalation
|
||||||
|
step below calls driver.focus_game(), which raises RuntimeError if it
|
||||||
|
can't find the window -- a real crash hit live (2026-07-16) when
|
||||||
|
ba_daily.py's centralized pre-task _ensure_home() called this function
|
||||||
|
before login.py (the one task able to launch the game itself, see its
|
||||||
|
own module docstring) ever got a chance to run. Every OTHER task still
|
||||||
|
crashes as before via its own driver.focus_game() call once dispatched
|
||||||
|
-- correct for them, since they have no ability to start the game --
|
||||||
|
this only changes behavior for the specific "no window yet" case this
|
||||||
|
function itself can't do anything about anyway.
|
||||||
"""
|
"""
|
||||||
|
if not driver.window_exists():
|
||||||
|
return False
|
||||||
escalated = False
|
escalated = False
|
||||||
for round_num in range(1, RETURN_HOME_MAX_ROUNDS + 1):
|
for round_num in range(1, RETURN_HOME_MAX_ROUNDS + 1):
|
||||||
if not _not_home(driver):
|
if not _not_home(driver):
|
||||||
@ -161,7 +175,7 @@ def click_back(driver, verify_fn, max_attempts=3):
|
|||||||
unconfirmed after max_attempts.
|
unconfirmed after max_attempts.
|
||||||
"""
|
"""
|
||||||
for attempt in range(1, max_attempts + 1):
|
for attempt in range(1, max_attempts + 1):
|
||||||
if attempt == max_attempts:
|
if attempt == max_attempts and driver.window_exists():
|
||||||
driver.focus_game()
|
driver.focus_game()
|
||||||
driver.click(*BACK_BUTTON)
|
driver.click(*BACK_BUTTON)
|
||||||
driver.wait(1.5)
|
driver.wait(1.5)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
234
ba_auto/tasks/login.py
Normal file
234
ba_auto/tasks/login.py
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
"""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")
|
||||||
14
ba_daily.py
14
ba_daily.py
@ -3,9 +3,10 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from ba_auto import config, driver, navigation
|
from ba_auto import config, driver, navigation
|
||||||
from ba_auto.tasks import arena, bounty, cafe, circle, event_sweep, gem_shop, lesson, mailbox, shop_common, shop_tactical, stamina, story_sweep
|
from ba_auto.tasks import arena, bounty, cafe, circle, event_sweep, gem_shop, lesson, login, mailbox, shop_common, shop_tactical, stamina, story_sweep
|
||||||
|
|
||||||
TASKS = {
|
TASKS = {
|
||||||
|
"login": login.run,
|
||||||
"mailbox": mailbox.run,
|
"mailbox": mailbox.run,
|
||||||
"cafe": cafe.run,
|
"cafe": cafe.run,
|
||||||
"stamina": stamina.run,
|
"stamina": stamina.run,
|
||||||
@ -28,8 +29,15 @@ TASKS = {
|
|||||||
# module docstring. gem_shop and circle are the opposite case -- like
|
# module docstring. gem_shop and circle are the opposite case -- like
|
||||||
# mailbox/cafe/stamina, they only ever reclaim a genuinely free (0 yen),
|
# mailbox/cafe/stamina, they only ever reclaim a genuinely free (0 yen),
|
||||||
# once-per-day resource with no choice to make (claim it or don't, nothing
|
# once-per-day resource with no choice to make (claim it or don't, nothing
|
||||||
# to select), so they belong in the default flow rather than opt-in.
|
# to select), so they belong in the default flow rather than opt-in. login
|
||||||
DEFAULT_ORDER = ["mailbox", "cafe", "stamina", "gem_shop", "circle"]
|
# is first, ahead of everything else: every other task's own navigation
|
||||||
|
# assumes the home screen is already reachable via the ordinary
|
||||||
|
# Escape/BACK_BUTTON press loop in navigation.return_to_home, which has no
|
||||||
|
# way to get there from the title/loading/attendance-card states login.py
|
||||||
|
# handles -- see that module's docstring. login.run() checks true-home
|
||||||
|
# first on every internal poll, so this is a fast no-op on a session that's
|
||||||
|
# already logged in, not a risky blind click every single day.
|
||||||
|
DEFAULT_ORDER = ["login", "mailbox", "cafe", "stamina", "gem_shop", "circle"]
|
||||||
|
|
||||||
# How many times _ensure_home retries navigation.return_to_home as a whole
|
# How many times _ensure_home retries navigation.return_to_home as a whole
|
||||||
# (not to be confused with that function's own internal
|
# (not to be confused with that function's own internal
|
||||||
|
|||||||
24
plan.md
24
plan.md
@ -768,6 +768,30 @@ This is a recurrence of a gotcha first found during `event_sweep.py`'s original
|
|||||||
|
|
||||||
**Follow-up, same session — "could the same error happen in other scripts?"** The two-layer fix above only covers `navigation.return_to_home`/`driver.focus_game()`. Auditing every direct use of the shared `(85,55)` coordinate found 5 places that clicked it *without* going through either: `shop_common.py`/`shop_tactical.py`'s end-of-run cleanup clicks, and — the real risk — `lesson.py`'s `_ensure_location_select_list` (had its own retry loop, but no overlay escalation) and `_close_region_grid` (called up to 12x per run, zero retry or verification at all; a silently-missed click here leaves the next region's `_open_region_grid` starting from the wrong screen and silently skipping that region). Fixed with a new shared `navigation.click_back(driver, verify_fn, max_attempts=3)` — the same retry + windowraise-escalation pattern as `return_to_home`, but parameterized by a caller-supplied verification check instead of hardcoding "reached home," for callers (like `lesson.py`'s per-region-map → list transition) that only want to go back ONE level. Applied to both `lesson.py` call sites; the three lower-risk end-of-run cleanup clicks (`lesson.py`, `shop_common.py`, `shop_tactical.py`) were switched to call `navigation.return_to_home` directly instead, since "go all the way home" is exactly their intent anyway. `config.SHOP_BACK_BUTTON`/`LESSON_BACK_BUTTON` were then removed as dead code (redundant with `navigation.BACK_BUTTON`, matching `EVENT_BACK_BUTTON`'s earlier removal for the identical reason). **Confirmed live**: `lesson` (0 tickets, so the no-op path — confirmed clean return home, no warnings), `shop_common` (a real purchase, 8 items/1,211,500 credits — confirmed clean return home), and `shop_tactical` (a real purchase, 2 items/45 tactical coin — confirmed clean return home) all ran successfully through the real CLI with the new navigation. `lesson.py`'s two hardened mid-run call sites (`_close_region_grid`/`_ensure_location_select_list`) weren't actually exercised by this test, though, since 0 tickets meant the scan/execute phase never ran — they'll get real coverage the next time `lesson` runs with tickets available.
|
**Follow-up, same session — "could the same error happen in other scripts?"** The two-layer fix above only covers `navigation.return_to_home`/`driver.focus_game()`. Auditing every direct use of the shared `(85,55)` coordinate found 5 places that clicked it *without* going through either: `shop_common.py`/`shop_tactical.py`'s end-of-run cleanup clicks, and — the real risk — `lesson.py`'s `_ensure_location_select_list` (had its own retry loop, but no overlay escalation) and `_close_region_grid` (called up to 12x per run, zero retry or verification at all; a silently-missed click here leaves the next region's `_open_region_grid` starting from the wrong screen and silently skipping that region). Fixed with a new shared `navigation.click_back(driver, verify_fn, max_attempts=3)` — the same retry + windowraise-escalation pattern as `return_to_home`, but parameterized by a caller-supplied verification check instead of hardcoding "reached home," for callers (like `lesson.py`'s per-region-map → list transition) that only want to go back ONE level. Applied to both `lesson.py` call sites; the three lower-risk end-of-run cleanup clicks (`lesson.py`, `shop_common.py`, `shop_tactical.py`) were switched to call `navigation.return_to_home` directly instead, since "go all the way home" is exactly their intent anyway. `config.SHOP_BACK_BUTTON`/`LESSON_BACK_BUTTON` were then removed as dead code (redundant with `navigation.BACK_BUTTON`, matching `EVENT_BACK_BUTTON`'s earlier removal for the identical reason). **Confirmed live**: `lesson` (0 tickets, so the no-op path — confirmed clean return home, no warnings), `shop_common` (a real purchase, 8 items/1,211,500 credits — confirmed clean return home), and `shop_tactical` (a real purchase, 2 items/45 tactical coin — confirmed clean return home) all ran successfully through the real CLI with the new navigation. `lesson.py`'s two hardened mid-run call sites (`_close_region_grid`/`_ensure_location_select_list`) weren't actually exercised by this test, though, since 0 tickets meant the scan/execute phase never ran — they'll get real coverage the next time `lesson` runs with tickets available.
|
||||||
|
|
||||||
|
### Phase 18: Login (title screen -> home) (2026-07-16)
|
||||||
|
|
||||||
|
Reference: `core/Baas_thread.py`'s `to_main_page` (the generic post-launch arrival routine every other reference feature's own navigation reuses -- there's no separate "login" module in the reference) plus `module/restart.py`'s `implement`/`start` (check the app is running, launch it if not). Local: `ba_auto/tasks/login.py`. Requested directly by the user, with the game genuinely sitting on the real login screen at request time: "go through login page to go to home."
|
||||||
|
|
||||||
|
Reference flow: `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. This project has no image-template assets for that whole catalog, so rather than hand-calibrating every possible reference dialog, the port scopes down to what was actually confirmed live plus a bounded generic Enter-press fallback for anything else recognized, mirroring co_detect's own "blind action once nothing matches" fallback shape using this project's own established Enter-dismiss idiom.
|
||||||
|
|
||||||
|
**Live-calibrated 2026-07-16** on nik-gpu against the account's real overnight login-screen state, native 1920x1200 captures throughout (not the non-native `screenshots/daily_login/*.png` the user originally supplied -- same not-1:1 gap already documented for `screenshots/gem_shop/`/`screenshots/cafe/student/`). Confirmed states, in the order actually hit:
|
||||||
|
|
||||||
|
1. Title screen ("TOUCH TO START") -- the ブルーアーカイブ logo (top-left) is fixed brand chrome independent of the rotating seasonal background art, confirmed identical across two completely different background pieces (a beach BBQ scene, a train interior). Click `LOGIN_TOUCH_TO_START`.
|
||||||
|
2. A real "ネットワークへの接続に失敗しました" (network connection failed) notice, hit unprompted during calibration -- same logo, uniformly dimmed by the notice's own overlay. Enter dismisses it, dropping back to state 1.
|
||||||
|
3. A loading transition, two visually distinct variants: a brief chrome-visible one, and a full-bleed one (rotating splash art, center spinner, no chrome at all) that **genuinely got stuck for over 6 minutes with zero progress** during the first live attempt -- confirmed not a network/process-health issue (ping and the game process were both healthy throughout). The user's own live guidance mid-session ("Usually I would kill the game and rerun it to fix it", identifying `/usr/local/bin/launch-blue-archive.sh` and the game's working directory) matched the reference's own `restart.py` kill+relaunch pattern exactly. A manual kill+relaunch recovered immediately; a second attempt completed the entire remaining flow in well under 15 seconds, confirming the stall was a genuine stuck state, not normal variance. Ported as `login.py`'s own `_recover`, using new `driver.kill_game`/`launch_game`/`is_game_running`/`window_exists` primitives.
|
||||||
|
4. アロナの毎日出席簿 (the daily attendance card) -- only appears once per day (confirmed absent on an immediate same-day re-run after the first run's Enter already claimed 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 this dialog specifically (same class of mismatch as `gem_shop.py`'s own dialog: 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). Fixed with a dedicated `_news_dialog_open` multi-point header-color check; closed via its own X button, not Enter (not confirmed to be Enter-bound, unlike everything else in this flow).
|
||||||
|
|
||||||
|
**A known pre-existing client bug was observed live but deliberately not worked around**: per the user, a rendering bug (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: the stuck rectangle sits well clear of every probe point this module and `navigation.py` use, and the true-home checks still read correctly through it. 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 via the actual module's own first live run** (not manual clicking): the original `_true_home` (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 (neither looks like a subscreen or an open modal to checks that 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 clicking anything. Fixed by adding `_home_nav_bar_visible`, a positive multi-point 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 -- confirmed via `pkill`) correctly launched the game via `driver.launch_game()`, clicked through the title screen, and reached the genuinely-confirmed home screen.
|
||||||
|
|
||||||
|
**A second real bug was found via the same cold-start test, one layer up**: `ba_daily.py`'s centralized pre-task `_ensure_home()` calls `navigation.return_to_home`, which calls `driver.focus_game()` partway through its escalation budget -- and `focus_game()` hard-raises `RuntimeError` if the game window doesn't exist at all, crashing the whole process *before* `login.run()` (the one task able to launch the game itself) ever got a chance to run, making its own launch-if-missing logic unreachable dead code. Fixed by having `navigation.return_to_home` bail out immediately (`False`) if `driver.window_exists()` is false, rather than pressing/escalating into a guaranteed crash, plus the same guard on `navigation.click_back`'s own escalation. Every other task is unaffected -- they still crash exactly as before once dispatched (correct for them, since none of them can launch the game themselves).
|
||||||
|
|
||||||
|
Added as the very first step in `ba_daily.py`'s `DEFAULT_ORDER`, 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 poll, so a session that's already logged in and sitting at home is a fast no-op, not a risky blind click every day.
|
||||||
|
|
||||||
|
**Not yet live-confirmed**: the infrequent 業務復帰ログインボーナス welcome-back login bonus card from the user's own reference screenshots (account wasn't in that state during calibration) -- expected to fall through to the same generic-Enter path already confirmed for the attendance card and network notice, but not yet exercised for real; and the automatic kill+relaunch recovery path (`_recover`) has only been exercised once, manually, not yet triggered by `login.py`'s own `LOGIN_TIMEOUT_SECONDS` budget hitting a real stuck state on its own.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### OCR
|
### OCR
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user