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:
parent
bf87a097c3
commit
8d3da6e6e1
@ -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
@ -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.")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user