Refactor gem shop task for UI redesign and improve dialog handling

- Recalibrated gem shop task after client UI update, replacing the old overlay dialog with a full subscreen.
- Removed custom dialog open/close checks in favor of shared navigation functions.
- Updated method names and logic to reflect new tab structure and improve clarity.
- Enhanced error handling for dialog closure and navigation return to home.
- Verified live against real account states to ensure functionality and correctness.
This commit is contained in:
Nik Afiq 2026-07-29 21:23:49 +09:00
parent bf87a097c3
commit 8d3da6e6e1
3 changed files with 104 additions and 136 deletions

View File

@ -1298,95 +1298,82 @@ BOUNTY_SWEEP_CONFIRM_TEXT_RECT = (605, 505, 1320, 615)
# state below reduces to a clean, high-contrast flat color rather than a # state below reduces to a clean, high-contrast flat color rather than a
# complex shape needing find_cafe_sparkle-style matching. # complex shape needing find_cafe_sparkle-style matching.
# #
# All coordinates/colors below are pixel-scanned from real scrot captures on # RECALIBRATED 2026-07-29 after a real Blue Archive client UI update changed
# nik-gpu at the native 1920x1200 (not estimated from the non-native-resolution # this screen's whole layout. Re-verified live on nik-gpu the same day (see
# screenshots/gem_shop/*.png reference photos -- confirmed those are NOT 1:1 # scratchpad/gem_shop_after_click.png / gem_shop_weekly_tab.png /
# with real game coordinates, same finding as screenshots/cafe/student/'s own # gem_shop_after_close.png). What changed from the original 2026-07-15
# calibration history). Live-calibrated 2026-07-15 by manually driving the # calibration:
# real dialog end-to-end (see scratchpad/gem_shop_*.png for the captures) -- # - The 期間限定/青輝石/パッケージ 3-top-tab layout is GONE. The dialog is
# this walk-through also genuinely claimed the account's real free package # now a real full subscreen (own header + top-left back arrow at the
# for the day (0 yen, +10 AP / +10,000 credits, confirmed via before/after # same (85, 55) navigation.BACK_BUTTON every other subscreen uses, not
# counter values), so the two states below are both real, not guessed. # an overlay on top of home), with a left sidebar
# (おすすめの商品/限定商品/一般商品/定額商品/青輝石/特別支援) and, for
# 一般商品 specifically, a further デイリー/ウィークリー/マンスリー
# switch top-right. The daily free package lives under
# 一般商品 -> デイリー.
# - Because it's now a genuine subscreen, navigation.is_on_subscreen
# correctly reads True here (confirmed live: SUBSCREEN_HEADER_PROBE read
# (247,250,252) on this screen vs (50,57,137) on true home) and
# navigation.return_to_home correctly closes it (confirmed live) -- the
# old GEM_SHOP_DIALOG_PROBES workaround below is no longer needed at all
# and has been removed; gem_shop.py now shares the same
# is_on_subscreen/return_to_home machinery as mailbox/cafe/shop/lesson.
# - The status-bar/button coordinates all moved (card is taller, sits
# higher). GEM_SHOP_STATUS_CLAIMED_RGB was re-read directly off today's
# real "already claimed" state. GEM_SHOP_STATUS_AVAILABLE_RGB could NOT
# be re-read off the free card itself (already claimed for the day by
# the time of this recalibration) -- instead it was read off three
# other real, currently-purchasable cards under the ウィークリー tab
# (レポートパッケージ/レポートパッケージ(Lite)/強化珠パッケージ, all
# three independently reading the identical (226,236,246)), since Blue
# Archive's shop UI reuses one shared "N times purchasable" status-bar
# style across every card in this screen, not a per-card color. This is
# an inferred-but-live-observed value, not a guess -- treat as
# implemented-but-not-yet-confirmed against the daily free card itself
# specifically, same disclosed-gap shape the original calibration had
# for the "available -> claim" path in general. Re-confirm the exact
# first time gem_shop actually claims a package post-redesign.
GEM_SHOP_ICON = (204, 345) GEM_SHOP_ICON = (204, 345)
# パッケージ (package) tab, third of three (期間限定/青輝石/パッケージ). # 一般商品 (general products), 3rd item in the left sidebar
# # (おすすめの商品/限定商品/一般商品/定額商品/青輝石/特別支援). Always
# KNOWN FUTURE-BREAKAGE RISK (flagged in review, not yet fixable): this is a # clicked explicitly rather than relying on it already being selected --
# single fixed coordinate calibrated only against the CURRENT 3-tab layout, # even though it was the landing tab in this recalibration, per this
# where 期間限定 (time-limited) is present as the first tab. The reference's # project's own established discipline of never trusting a remembered/
# own to_purchase_type branches on exactly this -- it picks a different # default UI selection before a real click (see memory: BA count/quantity
# パッケージ click x depending on whether 期間限定 is currently showing, # steppers remember last-used values; the same caution applies to any
# because removing it reflows the remaining tabs. screenshots/gem_shop/2.png # selected-tab state here).
# (captured live the same day as this calibration) shows 期間限定's own GEM_SHOP_GENERAL_PRODUCTS_TAB = (196, 376)
# on-screen countdown ("終了まであと6日" / "26.06.24〜26.07.29"), meaning # デイリー (daily), 1st of 3 sub-tabs (デイリー/ウィークリー/マンスリー)
# this account's 3-tab layout is itself temporary and due to change around # shown top-right once 一般商品 is open. Also always clicked explicitly for
# 2026-07-21 -- not a theoretical edge case. No real screenshot of the # the same reason as GEM_SHOP_GENERAL_PRODUCTS_TAB above.
# resulting 2-tab (青輝石/パッケージ) layout exists yet to calibrate the GEM_SHOP_DAILY_SUBTAB = (1330, 148)
# correct alternate x, so a second coordinate has NOT been guessed in here # 毎日無料パッケージ card's own 購入 (purchase) button -- the only card
# rather than grounded in a real capture, per this project's own # shown under 一般商品 -> デイリー (fixed position, no scrolling/search
# no-guessing-at-pixel-values discipline. # needed).
# GEM_SHOP_FREE_CARD_BUY_BUTTON = (576, 630)
# This is safe to leave as a known gap rather than a live hazard: if this
# coordinate goes stale, _open_package_tab's own status-probe check reads
# "unknown" (neither the available-navy nor claimed-red color, since it'd
# be looking at the wrong tab's content) and run() aborts cleanly via
# _close_gem_shop's verified close WITHOUT ever clicking a purchase button
# on whatever tab it actually landed on -- no currency-risk path exists
# through this failure mode. Re-calibrate against a real capture once the
# tab count actually changes (or the countdown resets on a new campaign
# and this account still shows 3 tabs, meaning it wasn't actually an issue).
GEM_SHOP_PACKAGE_TAB = (1345, 330)
# 毎日無料パッケージ card's own 購入 (purchase) button -- always the
# leftmost card, top row, of the package tab (fixed position, matching the
# reference's own fixed (385, 479)-style offset rather than a search).
GEM_SHOP_FREE_CARD_BUY_BUTTON = (573, 795)
# The free card's own status bar (just below its artwork, above the price # The free card's own status bar (just below its artwork, above the price
# button) reads a flat, highly distinct color depending on claim state -- # button) reads a flat, highly distinct color depending on claim state --
# confirmed live by claiming the real package and diffing before/after # see the RECALIBRATED note above for how each value was obtained:
# captures pixel-for-pixel at this exact point: # available ("一日にN回まで購入可能"): flat light blue ~(226, 236, 246)
# available ("一日に1回まで購入可能"): flat dark navy ~(41, 65, 90) # claimed ("一日に0回まで購入可能"): flat dark red ~(190, 56, 66)
# claimed ("一日に0回まで購入可能"): flat dark red ~(144, 38, 47) # The two are far enough apart (light vs dark, blue-family vs red-family)
# r vs b relationship alone cleanly separates them (navy: b>r by ~50; red: # that no OCR of the "0"/"N" count text is needed -- the reference itself
# r>b by ~100), so no OCR of the "0"/"1" count text is needed -- the # doesn't OCR this either, it template-matches two whole separate
# reference itself doesn't OCR this either, it template-matches two whole # "purchasable"/"non-purchasable" card images.
# separate "purchasable"/"non-purchasable" card images. GEM_SHOP_FREE_CARD_STATUS_PROBE = (410, 485)
GEM_SHOP_FREE_CARD_STATUS_PROBE = (420, 710) GEM_SHOP_STATUS_AVAILABLE_RGB = (226, 236, 246)
GEM_SHOP_STATUS_AVAILABLE_RGB = (41, 65, 90) GEM_SHOP_STATUS_CLAIMED_RGB = (190, 56, 66)
GEM_SHOP_STATUS_CLAIMED_RGB = (144, 38, 47)
GEM_SHOP_STATUS_TOLERANCE = 20 GEM_SHOP_STATUS_TOLERANCE = 20
# Generic "is the 青輝石購入 dialog (or a notice/confirm card nested on top
# of it) currently showing" check -- navigation.is_on_subscreen/is_modal_open
# both proved unreliable here, confirmed live: this dialog overlays directly
# on the home screen (keeping its own header bar visible, unlike mailbox/
# cafe/shop's own full subscreen headers) so is_on_subscreen's header probe
# stays dark in both states; is_modal_open's (960, 200) probe lands on the
# dialog's own opaque white card interior rather than a dimmed backdrop, so
# it reads bright (not-dark) whether the dialog is open or not -- the exact
# same class of default-probe mismatch story_sweep.py/bounty.py already
# document for their own wide modals.
#
# These 4 points sit in the dialog card's plain gutter background (confirmed
# identical whichever of the 3 tabs is active, and also identical whether
# the base dialog or a nested confirm/notice card is showing on top) and
# read a tight, near-white, near-equal-RGB cluster there in every dialog
# state captured; the true home screen's own character art was confirmed
# NOT to coincidentally match this closely at ALL 4 spread-out points
# simultaneously (same multi-point-beats-single-point reasoning as
# navigation.is_header_bar_visible, added the same day for an unrelated
# cafe bug -- reused here independently since this dialog's false-positive
# risk is a different shape, not the header row).
GEM_SHOP_DIALOG_PROBES = ((960, 200), (960, 250), (1500, 600), (420, 600))
GEM_SHOP_DIALOG_PROBE_MIN_CHANNEL = 220
GEM_SHOP_DIALOG_PROBE_MAX_SPREAD = 15
GEM_SHOP_ICON_RETRIES = 3 GEM_SHOP_ICON_RETRIES = 3
# Bounds the "press Enter, re-check the free card's status" loop that # Bounds the "press Enter, re-check the free card's status" loop that
# advances through the confirm-purchase notice and the "報酬獲得!" reward # advances through the confirm-purchase notice and the "報酬獲得!" reward
# banner after clicking 購入 -- confirmed live the reward banner's own entry # banner after clicking 購入 -- confirmed live (pre-redesign) the reward
# animation did not accept input on the first 1-2 presses while its sparkle # banner's own entry animation did not accept input on the first 1-2
# animation was still playing, so this needs real patience, not just 1-2 # presses while its sparkle animation was still playing, so this needs real
# tries. # patience, not just 1-2 tries. Not yet re-confirmed against the redesigned
# confirm/reward notice specifically -- see gem_shop.py's module docstring.
GEM_SHOP_CLAIM_MAX_ATTEMPTS = 6 GEM_SHOP_CLAIM_MAX_ATTEMPTS = 6
# Circle (サークル / guild), ported from module/group.py. Home -> bottom-nav # Circle (サークル / guild), ported from module/group.py. Home -> bottom-nav

File diff suppressed because one or more lines are too long

View File

@ -21,55 +21,54 @@ on nik-gpu, which genuinely claimed the account's real free package for the
day (0 yen, confirmed +10 AP / +10,000 credits via before/after counter day (0 yen, confirmed +10 AP / +10,000 credits via before/after counter
values) -- see scratchpad/gem_shop_*.png for the captures. That exercised values) -- see scratchpad/gem_shop_*.png for the captures. That exercised
the "available -> claim it" path for real, via raw xdotool/scrot rather the "available -> claim it" path for real, via raw xdotool/scrot rather
than this actual module. This module was then live-tested for real than this actual module.
against the resulting "already claimed today" state (see plan.md's Gem
shop phase) -- the "available -> claim" code path itself is implemented
against the same coordinates/logic already confirmed live, but has not
yet been exercised by this actual module end-to-end; treat as
implemented-but-unverified for that one path until a run happens naturally
on a day the package hasn't been claimed yet.
A `reference-parity-reviewer` pass the same day caught a real bug in the RECALIBRATED 2026-07-29 after a real Blue Archive client UI update replaced
first version of this module: the final dialog-close step verified success the old 3-top-tab overlay dialog (期間限定/青輝石/パッケージ) with a real
via `navigation.is_on_subscreen`, but config.py's own writeup on full subscreen (left sidebar + デイリー/ウィークリー/マンスリー sub-tabs
`GEM_SHOP_DIALOG_PROBES` already documents that probe as blind to this under 一般商品). See config.py's "Gem shop daily free package" section for
specific dialog (reads the same whether it's open or closed) -- so that the full writeup of what was re-verified live and what's still an inferred
check could never fail, making a stuck-open dialog unrecoverable by this value. The old `_dialog_open`/`_close_gem_shop` custom probes existed only
module (and, per the same blindness, by `navigation.return_to_home`'s because the pre-redesign overlay dialog was invisible to
shared fallback too). Fixed by adding `_close_gem_shop`, which verifies navigation.is_on_subscreen/is_modal_open; the redesigned screen is a real
against the module's own correctly-calibrated `_dialog_open` instead, with subscreen, confirmed live, so this module now shares the same
a bounded retry -- reused for both closing paths (the "unknown status" is_on_subscreen/return_to_home machinery every other subscreen-based task
abort and the normal end-of-run close), replacing the original's second, (mailbox/cafe/shop/lesson) already uses, and those two custom probes are
separately-broken blind Escape in the abort path too. gone.
A `reference-parity-reviewer` pass on the original version caught a real
bug: the final dialog-close step verified success via
`navigation.is_on_subscreen`, which the pre-redesign overlay dialog read
identically whether open or closed -- so that check could never fail,
making a stuck-open dialog unrecoverable. That specific blindness no longer
applies post-redesign (is_on_subscreen now genuinely distinguishes this
screen from home, confirmed live), which is what made switching to the
shared navigation.return_to_home safe here.
""" """
def _dialog_open(driver, config): from ba_auto import navigation
for x, y in config.GEM_SHOP_DIALOG_PROBES:
r, g, b = driver.color_at(x, y)
spread = max(r, g, b) - min(r, g, b)
if not (min(r, g, b) > config.GEM_SHOP_DIALOG_PROBE_MIN_CHANNEL and spread < config.GEM_SHOP_DIALOG_PROBE_MAX_SPREAD):
return False
return True
def _open_gem_shop(driver, config): def _open_gem_shop(driver, config):
for attempt in range(1, config.GEM_SHOP_ICON_RETRIES + 1): for attempt in range(1, config.GEM_SHOP_ICON_RETRIES + 1):
driver.click(*config.GEM_SHOP_ICON) driver.click(*config.GEM_SHOP_ICON)
driver.wait(2) driver.wait(2)
if _dialog_open(driver, config): if navigation.is_on_subscreen(driver):
return True return True
print(f"[gem_shop] dialog not detected after click (attempt {attempt}/{config.GEM_SHOP_ICON_RETRIES})") print(f"[gem_shop] subscreen not detected after click (attempt {attempt}/{config.GEM_SHOP_ICON_RETRIES})")
return False return False
def _open_package_tab(driver, config): def _open_daily_general_tab(driver, config):
for attempt in range(1, config.GEM_SHOP_ICON_RETRIES + 1): for attempt in range(1, config.GEM_SHOP_ICON_RETRIES + 1):
driver.click(*config.GEM_SHOP_PACKAGE_TAB) driver.click(*config.GEM_SHOP_GENERAL_PRODUCTS_TAB)
driver.wait(1)
driver.click(*config.GEM_SHOP_DAILY_SUBTAB)
driver.wait(1.5) driver.wait(1.5)
status = _read_free_card_status(driver, config) status = _read_free_card_status(driver, config)
if status != "unknown": if status != "unknown":
return status return status
print(f"[gem_shop] package tab's free card not recognized after click (attempt {attempt}/{config.GEM_SHOP_ICON_RETRIES})") print(f"[gem_shop] free card status not recognized after click (attempt {attempt}/{config.GEM_SHOP_ICON_RETRIES})")
return "unknown" return "unknown"
@ -86,24 +85,6 @@ def _read_free_card_status(driver, config):
return "unknown" return "unknown"
def _close_gem_shop(driver, config):
# Verify with the module's own _dialog_open probe rather than
# navigation.is_on_subscreen/is_modal_open -- both are documented above
# (see the "Generic... dialog... showing" comment in config.py) to read
# the same regardless of whether this specific dialog is open or closed,
# so a check against either can never fail and would silently mask a
# stuck-open dialog (caught in review before this was live-tested: the
# original version of this function used is_on_subscreen for exactly
# that dead check).
for attempt in range(1, config.GEM_SHOP_ICON_RETRIES + 1):
driver.keypress("Escape")
driver.wait(1.5)
if not _dialog_open(driver, config):
return True
print(f"[gem_shop] dialog still open after Escape (attempt {attempt}/{config.GEM_SHOP_ICON_RETRIES})")
return False
def _claim_free_package(driver, config): def _claim_free_package(driver, config):
driver.click(*config.GEM_SHOP_FREE_CARD_BUY_BUTTON) driver.click(*config.GEM_SHOP_FREE_CARD_BUY_BUTTON)
driver.wait(2) driver.wait(2)
@ -119,14 +100,14 @@ def run(driver, config):
driver.focus_game() driver.focus_game()
if not _open_gem_shop(driver, config): if not _open_gem_shop(driver, config):
print("[gem_shop] could not confirm the gem shop dialog opened, aborting without pressing further keys") print("[gem_shop] could not confirm the gem shop screen opened, aborting without pressing further keys")
return return
status = _open_package_tab(driver, config) status = _open_daily_general_tab(driver, config)
if status == "unknown": if status == "unknown":
print("[gem_shop] could not read the free package card's status, aborting without pressing further keys") print("[gem_shop] could not read the free package card's status, aborting without pressing further keys")
if not _close_gem_shop(driver, config): if not navigation.return_to_home(driver):
print("[gem_shop] warning: could not confirm the dialog closed") print("[gem_shop] warning: could not confirm return to home")
return return
if status == "claimed": if status == "claimed":
@ -138,6 +119,6 @@ def run(driver, config):
else: else:
print("[gem_shop] warning: could not confirm the free package was claimed after retries") print("[gem_shop] warning: could not confirm the free package was claimed after retries")
if not _close_gem_shop(driver, config): if not navigation.return_to_home(driver):
print("[gem_shop] warning: could not confirm the dialog closed") print("[gem_shop] warning: could not confirm return to home")
print("[gem_shop] Done.") print("[gem_shop] Done.")