fix(event_sweep): implement wrong/stale event page detection and shared navigation return to home functionality
This commit is contained in:
parent
1b313d27ff
commit
af0a784bcd
@ -216,12 +216,20 @@ STORY_SWEEP_ROTATION_COUNT = "max"
|
|||||||
# (every confirm dialog reached was cancelled via Escape, verified by the AP
|
# (every confirm dialog reached was cancelled via Escape, verified by the AP
|
||||||
# counter being unchanged before/after).
|
# counter being unchanged before/after).
|
||||||
|
|
||||||
# Home screen's top-right event countdown badge/thumbnail (e.g. "終了まで
|
# Home screen's top-right event badge/thumbnail slot. Chosen over the
|
||||||
# あと11日"). Confirmed live as the only reliable entry point: the bottom-left
|
# bottom-left banner slot, which cycles between several unrelated banners
|
||||||
# banner slot cycles between several unrelated banners (gacha pickups, other
|
# (gacha pickups, other campaigns) and was confirmed live to not reliably
|
||||||
# campaigns) and does not consistently reach the current story event.
|
# reach the current story event either. This slot is ALSO a rotating
|
||||||
|
# carousel, though -- confirmed live post-launch: it cycles between the
|
||||||
|
# current event's own countdown (e.g. "終了まであと11日") and OTHER notices,
|
||||||
|
# including an already-finished event's remaining reward-claim-period
|
||||||
|
# reminder, so a single click can land on a stale event's page instead of
|
||||||
|
# the current one. event_sweep.py's own wrong-page detection + retry (via
|
||||||
|
# navigation.return_to_home) exists specifically to recover from this, since
|
||||||
|
# no fixed click here is guaranteed to hit the right content on the first
|
||||||
|
# try. Back-button navigation now goes through navigation.return_to_home's
|
||||||
|
# shared BACK_BUTTON constant instead of a dedicated config entry here.
|
||||||
EVENT_BADGE_ICON = (1787, 300)
|
EVENT_BADGE_ICON = (1787, 300)
|
||||||
EVENT_BACK_BUTTON = (85, 55)
|
|
||||||
|
|
||||||
# Top-right tab bar inside the event screen (reference's activity_menu
|
# Top-right tab bar inside the event screen (reference's activity_menu
|
||||||
# story/mission/challenge tabs -- rendered as English labels "Story / Quest /
|
# story/mission/challenge tabs -- rendered as English labels "Story / Quest /
|
||||||
|
|||||||
@ -9,6 +9,14 @@ SUBSCREEN_HEADER_MIN_CHANNEL = 200
|
|||||||
MODAL_DIM_PROBE = (960, 200)
|
MODAL_DIM_PROBE = (960, 200)
|
||||||
MODAL_DIM_MAX_CHANNEL = 150
|
MODAL_DIM_MAX_CHANNEL = 150
|
||||||
|
|
||||||
|
# Shared top-left back-arrow position -- every subscreen calibrated so far
|
||||||
|
# (mailbox, cafe, shop, lesson, event) puts its own back button here
|
||||||
|
# (confirmed identical across config.py's LESSON_BACK_BUTTON/
|
||||||
|
# SHOP_BACK_BUTTON/EVENT_BACK_BUTTON). Used by return_to_home below as a
|
||||||
|
# generic recovery affordance, independent of which task got stuck.
|
||||||
|
BACK_BUTTON = (85, 55)
|
||||||
|
RETURN_HOME_MAX_ROUNDS = 6
|
||||||
|
|
||||||
|
|
||||||
def is_on_subscreen(driver):
|
def is_on_subscreen(driver):
|
||||||
r, g, b = driver.color_at(*SUBSCREEN_HEADER_PROBE)
|
r, g, b = driver.color_at(*SUBSCREEN_HEADER_PROBE)
|
||||||
@ -20,6 +28,41 @@ def is_modal_open(driver):
|
|||||||
return r < MODAL_DIM_MAX_CHANNEL and g < MODAL_DIM_MAX_CHANNEL and b < MODAL_DIM_MAX_CHANNEL
|
return r < MODAL_DIM_MAX_CHANNEL and g < MODAL_DIM_MAX_CHANNEL and b < MODAL_DIM_MAX_CHANNEL
|
||||||
|
|
||||||
|
|
||||||
|
def return_to_home(driver):
|
||||||
|
"""Bounded "press back until the home screen is reached" loop -- the
|
||||||
|
shared recovery path for any task that ends up on an unexpected or wrong
|
||||||
|
subscreen (e.g. event_sweep landing on a stale/finished event's page
|
||||||
|
instead of the current one, see plan.md's Event sweep phase). Generic
|
||||||
|
across tasks: it only depends on is_on_subscreen and the shared
|
||||||
|
BACK_BUTTON position above, not on any task-specific state.
|
||||||
|
|
||||||
|
Checks is_on_subscreen before every single press and stops the instant
|
||||||
|
it reads False -- never presses Escape/clicks back while already on the
|
||||||
|
home screen. This matters: CLAUDE.md documents a real hazard where a
|
||||||
|
blind Escape press on the home screen itself raises Blue Archive's own
|
||||||
|
"exit the game?" confirmation, which is exactly the failure mode a
|
||||||
|
naive fixed-count blind-press loop could cause here.
|
||||||
|
|
||||||
|
Tries Escape first each round (works for most subscreens, confirmed for
|
||||||
|
mailbox/cafe/event's own stage modal) and falls back to clicking
|
||||||
|
BACK_BUTTON if Escape didn't clear it, re-checking after each.
|
||||||
|
|
||||||
|
Returns True once home is confirmed reached, False if still not home
|
||||||
|
after RETURN_HOME_MAX_ROUNDS -- callers should treat False as "abort,
|
||||||
|
don't guess further" rather than assume home was reached.
|
||||||
|
"""
|
||||||
|
for _ in range(RETURN_HOME_MAX_ROUNDS):
|
||||||
|
if not is_on_subscreen(driver):
|
||||||
|
return True
|
||||||
|
driver.keypress("Escape")
|
||||||
|
driver.wait(1)
|
||||||
|
if not is_on_subscreen(driver):
|
||||||
|
return True
|
||||||
|
driver.click(*BACK_BUTTON)
|
||||||
|
driver.wait(1)
|
||||||
|
return not is_on_subscreen(driver)
|
||||||
|
|
||||||
|
|
||||||
def wait_for_state(driver, config, reactions, ends, max_iterations=30, poll_interval=1.0):
|
def wait_for_state(driver, config, reactions, ends, max_iterations=30, poll_interval=1.0):
|
||||||
"""Generic "watch the screen, react to anything recognized, stop once a
|
"""Generic "watch the screen, react to anything recognized, stop once a
|
||||||
recognized destination is reached" loop -- the local equivalent of the
|
recognized destination is reached" loop -- the local equivalent of the
|
||||||
|
|||||||
@ -8,7 +8,7 @@ Maps each local feature to the corresponding `~/repo/baas-reference/module/...`
|
|||||||
| Cafe | `module/cafe_reward.py` | `to_cafe` (its `relationship_rank_up` popup-handling now also ported, see below), `interaction_for_cafe_solve_method3`, `collect` | `ba_auto/tasks/cafe.py` | `picture.co_detect`/`color.rgb_in_range` → `driver.color_at` pixel-probe checks; sparkle template match ported in-process into `ba_auto/detector.py` (`find_cafe_sparkle`, now multi-scale) | Migrated: real Python, state-verified via color probes (no legacy bridge). Pat loop now polls for the full attempt budget instead of stopping on the first miss (see `plan.md` Phase 6 follow-up) — not yet confirmed against a live sparkle since none was available during testing. `_dismiss_rank_up_if_shown` reuses `navigation.is_on_subscreen` to detect and clear the full-screen bond-rank-up cutscene after a pat (see `plan.md` Phase 6 follow-up: rank-up popups) — not yet live-confirmed against a real trigger |
|
| Cafe | `module/cafe_reward.py` | `to_cafe` (its `relationship_rank_up` popup-handling now also ported, see below), `interaction_for_cafe_solve_method3`, `collect` | `ba_auto/tasks/cafe.py` | `picture.co_detect`/`color.rgb_in_range` → `driver.color_at` pixel-probe checks; sparkle template match ported in-process into `ba_auto/detector.py` (`find_cafe_sparkle`, now multi-scale) | Migrated: real Python, state-verified via color probes (no legacy bridge). Pat loop now polls for the full attempt budget instead of stopping on the first miss (see `plan.md` Phase 6 follow-up) — not yet confirmed against a live sparkle since none was available during testing. `_dismiss_rank_up_if_shown` reuses `navigation.is_on_subscreen` to detect and clear the full-screen bond-rank-up cutscene after a pat (see `plan.md` Phase 6 follow-up: rank-up popups) — not yet live-confirmed against a real trigger |
|
||||||
| Stamina/AP | `module/collect_daily_free_power.py`, `module/collect_daily_task_power.py` | `to_tasks`/`implement` (task-power, ported); `to_purchase_pyroxenes_menu` (free-power, not ported) | `ba_auto/tasks/stamina.py` | `color.rgb_in_range` → `driver.color_at`; reference's per-tab claim loop → live UI's single "一括受取" bulk-claim button + Enter | Partially migrated: Mission-panel claim done (see `plan.md` Phase 8). Daily Free Power (real-money purchase menu) deliberately not automated |
|
| Stamina/AP | `module/collect_daily_free_power.py`, `module/collect_daily_task_power.py` | `to_tasks`/`implement` (task-power, ported); `to_purchase_pyroxenes_menu` (free-power, not ported) | `ba_auto/tasks/stamina.py` | `color.rgb_in_range` → `driver.color_at`; reference's per-tab claim loop → live UI's single "一括受取" bulk-claim button + Enter | Partially migrated: Mission-panel claim done (see `plan.md` Phase 8). Daily Free Power (real-money purchase menu) deliberately not automated |
|
||||||
| Normal/Hard story AP sweep | `module/explore_tasks/sweep_task.py`, `module/explore_tasks/task_utils.py` | `to_region` (ported: OCR region-number readout + delta-click), a scoped-down `swipe_search_target_str` (ported: OCR stage-row label matching), `start_sweep`'s named-outcome contract (ported via `navigation.wait_for_state`, this project's scoped `co_detect` port) | `ba_auto/tasks/story_sweep.py` | OCR region/stage-name matching, ported for real (Phase 10) — replaces Phase 9's "next-region arrow stops advancing, then random stage" heuristic; MAX click verified via `SWEEP_MINUS_BUTTON_PROBE` color check (reused, still correct), modal closed via its own X button (Escape doesn't close it; X-button position re-calibrated per stage-layout variant, see Phase 10) | Done (see `plan.md` Phase 10, supersedes Phase 9). Config-driven exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS`), not latest-region/random-stage. Opt-in only, not in default flow |
|
| Normal/Hard story AP sweep | `module/explore_tasks/sweep_task.py`, `module/explore_tasks/task_utils.py` | `to_region` (ported: OCR region-number readout + delta-click), a scoped-down `swipe_search_target_str` (ported: OCR stage-row label matching), `start_sweep`'s named-outcome contract (ported via `navigation.wait_for_state`, this project's scoped `co_detect` port) | `ba_auto/tasks/story_sweep.py` | OCR region/stage-name matching, ported for real (Phase 10) — replaces Phase 9's "next-region arrow stops advancing, then random stage" heuristic; MAX click verified via `SWEEP_MINUS_BUTTON_PROBE` color check (reused, still correct), modal closed via its own X button (Escape doesn't close it; X-button position re-calibrated per stage-layout variant, see Phase 10) | Done (see `plan.md` Phase 10, supersedes Phase 9). Config-driven exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS`), not latest-region/random-stage. Opt-in only, not in default flow |
|
||||||
| Event sweep | `module/sweep_activity.py`, `module/activities/activity_utils.py` | `activity_sweep` (main flow), `to_activity` (nav to the event's Story/Mission/Challenge tabs), `check_sweep_availability`/`color.check_sweep_availability` (SSS gate), `start_sweep`'s named-outcome contract (shared with story_sweep's, ported the same way via `navigation.wait_for_state`) | `ba_auto/tasks/event_sweep.py` | `to_activity`'s bottom-nav-icon entry -> this client's home-screen event countdown badge (`config.EVENT_BADGE_ICON`); the reference's config-string sweep-list parsing (arbitrary stage lists, per-stage float/fraction counts via `preprocess_activity_region`/`preprocess_activity_sweep_times`) -> a single date-ordinal-modulo rotation target over a fixed 9-12 sub-range, mirroring `story_sweep.py`'s own rotation, per explicit user direction; stage-number OCR (`detector.read_int`) replaces the reference's `swipe_search_target_str` template-button search, since this client's stage list only ever needs its bottom scroll extreme for the 9-12 target range | Done. Live-calibrated against nik-gpu 2026-07-10 against the currently-running "鉄道爆走事件" event (12 stages) -- zero real AP spent during calibration (every confirm dialog reached was cancelled via Escape, verified by the AP counter). The stage-info modal is structurally identical to story_sweep's (MIN/-/+/MAX stepper, same AP-confirm dialog reusing `SWEEP_CONFIRM_*`/`SWEEP_RESULT_BUTTON_REGION`), but simpler: no region navigation, one fixed modal layout confirmed across two different stages (09 and 12), and the modal closes on Escape (story_sweep's doesn't). NOT yet live-tested with a real sweep/AP spend, and the reference's SSS-availability gate for a never-cleared stage was never exercised (every stage 9-12 on this account was already 3-starred) -- `event_sweep.py` handles that gate the same defensive way story_sweep handles an unavailable target: if the MAX-button count-raise can't be verified, it aborts without spending AP rather than guessing. See `plan.md`'s Event sweep phase |
|
| Event sweep | `module/sweep_activity.py`, `module/activities/activity_utils.py` | `activity_sweep` (main flow), `to_activity` (nav to the event's Story/Mission/Challenge tabs), `check_sweep_availability`/`color.check_sweep_availability` (SSS gate), `start_sweep`'s named-outcome contract (shared with story_sweep's, ported the same way via `navigation.wait_for_state`) | `ba_auto/tasks/event_sweep.py`, `ba_auto/navigation.py` (`return_to_home`) | `to_activity`'s bottom-nav-icon entry -> this client's home-screen event badge (`config.EVENT_BADGE_ICON`, confirmed live to be a rotating carousel -- see Status); the reference's config-string sweep-list parsing (arbitrary stage lists, per-stage float/fraction counts via `preprocess_activity_region`/`preprocess_activity_sweep_times`) -> a single date-ordinal-modulo rotation target over a fixed 9-12 sub-range, mirroring `story_sweep.py`'s own rotation, per explicit user direction; stage-number OCR (`detector.read_int`) replaces the reference's `swipe_search_target_str` template-button search, since this client's stage list only ever needs its bottom scroll extreme for the 9-12 target range | Done, with a live-reported bug fixed (see `plan.md`'s Phase 14 follow-up). Live-calibrated against nik-gpu 2026-07-10 against the currently-running "鉄道爆走事件" event (12 stages) -- zero real AP spent during calibration (every confirm dialog reached was cancelled via Escape, verified by the AP counter). The stage-info modal is structurally identical to story_sweep's (MIN/-/+/MAX stepper, same AP-confirm dialog reusing `SWEEP_CONFIRM_*`/`SWEEP_RESULT_BUTTON_REGION`), but simpler: no region navigation, one fixed modal layout confirmed across two different stages (09 and 12), and the modal closes on Escape (story_sweep's doesn't). The reference's SSS-availability gate for a never-cleared stage was never exercised (every stage 9-12 on this account was already 3-starred) -- `event_sweep.py` handles that gate the same defensive way story_sweep handles an unavailable target: if the MAX-button count-raise can't be verified, it aborts without spending AP rather than guessing. **Bug found on a real run**: the home-screen event badge turned out to be a rotating carousel (cycles between the current event's countdown and other notices, e.g. a finished event's leftover reward-claim reminder), so a click could land on a stale event's page instead. Fixed via a new shared `navigation.return_to_home` primitive (bounded press-back-until-home loop, built generically so other tasks can reuse it, per explicit user request) plus wrong-page detection in `_find_stage_row` (zero stage-row numbers OCR'd at all -> retry via `return_to_home`, up to 3 attempts). Not yet live-tested against a real recurrence of the bug. |
|
||||||
| Group/Club AP | `module/group.py` | Need to inspect | `ba_auto/tasks/group.py` | fixed click + state check via local driver | Not started |
|
| Group/Club AP | `module/group.py` | Need to inspect | `ba_auto/tasks/group.py` | fixed click + state check via local driver | Not started |
|
||||||
| Bounty | `module/rewarded_task.py` | Need to inspect | `ba_auto/tasks/bounty.py` | sweep/color/OCR adaptation | Not started |
|
| Bounty | `module/rewarded_task.py` | Need to inspect | `ba_auto/tasks/bounty.py` | sweep/color/OCR adaptation | Not started |
|
||||||
| Commissions | `module/clear_special_task_power.py` | Need to inspect | `ba_auto/tasks/commission.py` | sweep/color adaptation | Not started |
|
| Commissions | `module/clear_special_task_power.py` | Need to inspect | `ba_auto/tasks/commission.py` | sweep/color adaptation | Not started |
|
||||||
|
|||||||
@ -26,6 +26,19 @@ Key differences from story_sweep.py, all confirmed live during calibration:
|
|||||||
- The stage modal DOES close on Escape, unlike story_sweep's (X-button-only).
|
- The stage modal DOES close on Escape, unlike story_sweep's (X-button-only).
|
||||||
- Both stages tested were already 3-starred; the reference's SSS-availability
|
- Both stages tested were already 3-starred; the reference's SSS-availability
|
||||||
gate for never-cleared stages was never actually exercised (see config.py).
|
gate for never-cleared stages was never actually exercised (see config.py).
|
||||||
|
|
||||||
|
Live-testing after initial calibration found the home screen's event badge
|
||||||
|
(config.EVENT_BADGE_ICON) is itself a rotating carousel, not a stable
|
||||||
|
single-event slot as calibration happened to suggest: it cycles between the
|
||||||
|
current event's countdown AND other notices (e.g. an already-finished
|
||||||
|
event's remaining reward-claim-period reminder), so a single click can land
|
||||||
|
on a stale/wrong event page instead of the current one. _find_stage_row now
|
||||||
|
distinguishes "wrong page entirely" (no stage-row numbers OCR at all) from
|
||||||
|
"right page, this specific stage just isn't there" (some numbers found, just
|
||||||
|
not the target) -- the former retries via navigation.return_to_home + a
|
||||||
|
re-click of the badge, hoping the carousel has moved on by the next attempt;
|
||||||
|
the latter is reported and left alone, since retrying won't fix a genuinely
|
||||||
|
different stage list.
|
||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
@ -36,6 +49,8 @@ OPEN_RETRIES = 3
|
|||||||
POST_SWEEP_DISMISS_ROUNDS = 6
|
POST_SWEEP_DISMISS_ROUNDS = 6
|
||||||
MAX_BUTTON_RETRIES = 3
|
MAX_BUTTON_RETRIES = 3
|
||||||
MODAL_CLOSE_RETRIES = 3
|
MODAL_CLOSE_RETRIES = 3
|
||||||
|
WRONG_PAGE_RETRIES = 3
|
||||||
|
WRONG_PAGE_RETRY_WAIT = 3
|
||||||
|
|
||||||
|
|
||||||
def _is_stage_modal_open(driver, config):
|
def _is_stage_modal_open(driver, config):
|
||||||
@ -96,16 +111,25 @@ def _find_stage_row(driver, config, stage):
|
|||||||
# This event's target range (9-12) always sits within the last 5 rows,
|
# This event's target range (9-12) always sits within the last 5 rows,
|
||||||
# confirmed live regardless of starting scroll position -- so unlike
|
# confirmed live regardless of starting scroll position -- so unlike
|
||||||
# story_sweep._find_stage_row, only the bottom extreme is ever checked.
|
# story_sweep._find_stage_row, only the bottom extreme is ever checked.
|
||||||
|
#
|
||||||
|
# Also tracks whether ANY row OCR'd a real number at all -- a finished/
|
||||||
|
# stale event's Quest tab shows plain "period ended" text instead of
|
||||||
|
# stage-row cards, so all 5 reads coming back empty is a strong signal
|
||||||
|
# we're on the wrong page entirely (see module docstring), distinct from
|
||||||
|
# "right page, this stage just isn't among the visible rows."
|
||||||
x, y = config.EVENT_STAGE_LIST_SCROLL_POINT
|
x, y = config.EVENT_STAGE_LIST_SCROLL_POINT
|
||||||
driver.scroll(x, y, "down", config.EVENT_STAGE_LIST_SCROLL_CLICKS)
|
driver.scroll(x, y, "down", config.EVENT_STAGE_LIST_SCROLL_CLICKS)
|
||||||
driver.wait(0.5)
|
driver.wait(0.5)
|
||||||
|
|
||||||
|
saw_any_valid_row = False
|
||||||
for row_y in config.EVENT_STAGE_ROW_Y:
|
for row_y in config.EVENT_STAGE_ROW_Y:
|
||||||
label = detector.read_int(_row_number_rect(config, row_y))
|
label = detector.read_int(_row_number_rect(config, row_y))
|
||||||
print(f"[event_sweep] row @ {row_y}: read '{label}'")
|
print(f"[event_sweep] row @ {row_y}: read '{label}'")
|
||||||
|
if label is not None:
|
||||||
|
saw_any_valid_row = True
|
||||||
if label == stage:
|
if label == stage:
|
||||||
return row_y
|
return row_y, saw_any_valid_row
|
||||||
return None
|
return None, saw_any_valid_row
|
||||||
|
|
||||||
|
|
||||||
def _click_max_and_verify(driver, config):
|
def _click_max_and_verify(driver, config):
|
||||||
@ -174,8 +198,11 @@ def _watch_sweep_result(driver, config):
|
|||||||
def _sweep_target(driver, config, stage, count):
|
def _sweep_target(driver, config, stage, count):
|
||||||
print(f"[event_sweep] --- target stage {stage} x {count} ---")
|
print(f"[event_sweep] --- target stage {stage} x {count} ---")
|
||||||
|
|
||||||
row_y = _find_stage_row(driver, config, stage)
|
row_y, saw_any_valid_row = _find_stage_row(driver, config, stage)
|
||||||
if row_y is None:
|
if row_y is None:
|
||||||
|
if not saw_any_valid_row:
|
||||||
|
print("[event_sweep] no stage-row numbers recognized at all -- likely on the wrong/stale event page")
|
||||||
|
return "wrong_page"
|
||||||
print(f"[event_sweep] stage {stage} not found in the visible stage list")
|
print(f"[event_sweep] stage {stage} not found in the visible stage list")
|
||||||
return "stage_not_found"
|
return "stage_not_found"
|
||||||
|
|
||||||
@ -237,16 +264,28 @@ def run(driver, config):
|
|||||||
stage, count = rotation
|
stage, count = rotation
|
||||||
print(f"[event_sweep] today's rotation target: stage {stage}")
|
print(f"[event_sweep] today's rotation target: stage {stage}")
|
||||||
|
|
||||||
if not _open_event_screen(driver, config):
|
outcome = None
|
||||||
print("[event_sweep] could not confirm event screen is open, aborting without pressing further keys")
|
for attempt in range(1, WRONG_PAGE_RETRIES + 1):
|
||||||
return
|
if not _open_event_screen(driver, config):
|
||||||
|
print(f"[event_sweep] could not confirm event screen is open (attempt {attempt}/{WRONG_PAGE_RETRIES})")
|
||||||
|
navigation.return_to_home(driver)
|
||||||
|
driver.wait(WRONG_PAGE_RETRY_WAIT)
|
||||||
|
continue
|
||||||
|
|
||||||
outcome = _sweep_target(driver, config, stage, count)
|
outcome = _sweep_target(driver, config, stage, count)
|
||||||
if outcome != "swept":
|
if outcome == "wrong_page":
|
||||||
|
print(f"[event_sweep] landed on the wrong event page (attempt {attempt}/{WRONG_PAGE_RETRIES}) -- returning home to retry")
|
||||||
|
navigation.return_to_home(driver)
|
||||||
|
driver.wait(WRONG_PAGE_RETRY_WAIT)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print(f"[event_sweep] could not reach the current event's stage list after {WRONG_PAGE_RETRIES} attempts, giving up")
|
||||||
|
|
||||||
|
if outcome is not None and outcome not in ("swept", "wrong_page"):
|
||||||
print(f"[event_sweep] target stage {stage} ended in '{outcome}'")
|
print(f"[event_sweep] target stage {stage} ended in '{outcome}'")
|
||||||
|
|
||||||
if navigation.is_on_subscreen(driver):
|
if not navigation.return_to_home(driver):
|
||||||
driver.click(*config.EVENT_BACK_BUTTON)
|
print("[event_sweep] warning: could not confirm return to home screen")
|
||||||
driver.wait(1)
|
|
||||||
|
|
||||||
print("[event_sweep] Done.")
|
print("[event_sweep] Done.")
|
||||||
|
|||||||
11
plan.md
11
plan.md
@ -482,6 +482,17 @@ This is the same class of finding CLAUDE.md already documents from mailbox/cafe'
|
|||||||
|
|
||||||
**Not yet exercised**: an actual real sweep/AP spend (calibration deliberately avoided this, per `CLAUDE.md`'s "validate offline before spending the resource" guidance — the confirm dialog, cyan/gold color distinction, and result-screen mechanics were all visually confirmed but never clicked through to completion); the reference's SSS-availability gate for a stage that has never been cleared (every stage 9-12 on this account was already 3-starred, so `check_sweep_availability`'s "not yet sweepable, needs a manual fight first" branch was never seen — `event_sweep.py` handles this the same defensive way story_sweep handles an unavailable target: if the MAX-button count-raise can't be verified, it aborts that target without spending AP rather than guessing what an unsweepable stage's modal looks like); behavior once this event ends 2026-07-22 and a future event's badge/stage-count/layout replaces it (the 9-12 rotation range and row-position calibration are specific to this 12-stage event, not proven durable across arbitrary future events).
|
**Not yet exercised**: an actual real sweep/AP spend (calibration deliberately avoided this, per `CLAUDE.md`'s "validate offline before spending the resource" guidance — the confirm dialog, cyan/gold color distinction, and result-screen mechanics were all visually confirmed but never clicked through to completion); the reference's SSS-availability gate for a stage that has never been cleared (every stage 9-12 on this account was already 3-starred, so `check_sweep_availability`'s "not yet sweepable, needs a manual fight first" branch was never seen — `event_sweep.py` handles this the same defensive way story_sweep handles an unavailable target: if the MAX-button count-raise can't be verified, it aborts that target without spending AP rather than guessing what an unsweepable stage's modal looks like); behavior once this event ends 2026-07-22 and a future event's badge/stage-count/layout replaces it (the 9-12 rotation range and row-position calibration are specific to this 12-stage event, not proven durable across arbitrary future events).
|
||||||
|
|
||||||
|
#### Phase 14 follow-up: wrong/stale event page fix + shared `navigation.return_to_home`
|
||||||
|
|
||||||
|
**Reported by the user after a real run**: `event_sweep` opened an old, already-finished event's page instead of the current "鉄道爆走事件". Root cause: `config.EVENT_BADGE_ICON` (the home screen's top-right event badge) is itself a rotating carousel, not the stable single-event slot calibration happened to suggest — it cycles between the current event's own countdown and OTHER notices, including a finished event's leftover reward-claim-period reminder. Calibration's several screenshots all happened to catch it showing the right event, which masked this.
|
||||||
|
|
||||||
|
**Fix, in two parts:**
|
||||||
|
|
||||||
|
1. **New shared primitive**: `navigation.return_to_home(driver)` — a bounded "press Escape, re-check, fall back to clicking the shared back-arrow position, re-check" loop that returns once the home screen is confirmed reached (via `is_on_subscreen` reading `False`). Checks before every single press and never presses blind, matching the project's established "verify before acting" discipline — this specifically avoids the exit-game-confirmation hazard `CLAUDE.md` already documents (a stray Escape on the home screen itself raises Blue Archive's own "exit the game?" dialog). Generic across tasks: it only depends on `is_on_subscreen` and the shared back-arrow position (`navigation.BACK_BUTTON`, confirmed identical across `config.py`'s `LESSON_BACK_BUTTON`/`SHOP_BACK_BUTTON`/the now-removed `EVENT_BACK_BUTTON`), not on any event_sweep-specific state — built specifically so other tasks can reuse it later, per explicit user request ("make the back to home page a module so it can be used by other feature too").
|
||||||
|
2. **Wrong-page detection + retry in `event_sweep.py`**: `_find_stage_row` now also reports whether it OCR'd *any* valid stage number at all across the 5 checked rows, not just whether the target matched. Zero valid reads across all 5 is a strong "not actually on a stage list, likely the wrong/finished event" signal (a finished event's Quest tab shows plain "period ended" text instead of stage-row cards) — reported as a distinct `"wrong_page"` outcome, separate from `"stage_not_found"` (right page, this specific stage just isn't visible, which retrying the same badge click won't fix). `run()` now retries the whole entry sequence up to `WRONG_PAGE_RETRIES` (3) times on a `wrong_page` outcome, calling `navigation.return_to_home` and waiting a few seconds before each re-click of the badge, hoping the carousel has moved on by the next attempt. `run()` also now unconditionally calls `navigation.return_to_home` at the very end as a final safety net, replacing the old single unconditional `EVENT_BACK_BUTTON` click.
|
||||||
|
|
||||||
|
**Not yet live-tested**: this fix has only been syntax/compile-checked and deployed, not run against a real recurrence of the wrong-page bug — the retry timing (3 attempts, 3s apart) is a reasonable-but-unconfirmed guess at how fast the badge carousel actually cycles.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### OCR
|
### OCR
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user