fix(cafe): resolve invite ticket cooldown handling and improve rank-up cutscene detection
This commit is contained in:
parent
a0e1965353
commit
bb0215cb04
@ -32,12 +32,15 @@ CAFE_INCOME = (1780, 1105)
|
|||||||
CAFE_MAX_CLICKS_PER_ROOM = 15
|
CAFE_MAX_CLICKS_PER_ROOM = 15
|
||||||
CAFE_SPARKLE_TEMPLATE = os.path.join(ASSET_DIR, "cafe_sparkle.png")
|
CAFE_SPARKLE_TEMPLATE = os.path.join(ASSET_DIR, "cafe_sparkle.png")
|
||||||
# A pat that crosses an affection-rank threshold shows a full-screen "絆ラン
|
# A pat that crosses an affection-rank threshold shows a full-screen "絆ラン
|
||||||
# クアップ!" (Bond Rank Up!) cutscene with no cafe header visible at all --
|
# クアップ!" (Bond Rank Up!) cutscene with no cafe header visible at all.
|
||||||
# confirmed against screenshots/cafe/student/01-02: navigation.is_on_subscreen's
|
# Originally believed navigation.is_on_subscreen's single-pixel header probe
|
||||||
# header probe reads (183,220,240) there (r<200, fails) vs (248,249,250) on
|
# reliably told this apart from the real cafe screen (confirmed against
|
||||||
# the normal cafe screen (r>200, passes), so the existing header-brightness
|
# screenshots/cafe/student/01-02 at the time), but a real 2026-07-15 run hit
|
||||||
# check already tells the two apart. Bounds how many Enter presses
|
# a character whose cutscene art happened to read bright at that exact
|
||||||
# _dismiss_rank_up_if_shown will try before giving up.
|
# pixel, misreading the cutscene as already cleared -- see cafe.py's
|
||||||
|
# _dismiss_rank_up_if_shown and navigation.is_header_bar_visible for the
|
||||||
|
# fix (many header-row points must all read bright, not just one). Bounds
|
||||||
|
# how many Enter presses _dismiss_rank_up_if_shown will try before giving up.
|
||||||
CAFE_RANK_UP_DISMISS_RETRIES = 5
|
CAFE_RANK_UP_DISMISS_RETRIES = 5
|
||||||
|
|
||||||
# Horizontal camera panning before farming, per explicit user direction
|
# Horizontal camera panning before farming, per explicit user direction
|
||||||
|
|||||||
@ -105,5 +105,25 @@ def color_at(x, y):
|
|||||||
return int(r), int(g), int(b)
|
return int(r), int(g), int(b)
|
||||||
|
|
||||||
|
|
||||||
|
def colors_at(points):
|
||||||
|
"""Sample multiple (x, y) points from a single screenshot, instead of one
|
||||||
|
scrot capture per point -- added for navigation.is_header_bar_visible's
|
||||||
|
multi-point header check, which otherwise called color_at() 8 times (8
|
||||||
|
full-screen captures) for one logical check. Also more correct than
|
||||||
|
looping color_at(): all points come from the exact same frame rather
|
||||||
|
than points sampled sequentially across several hundred ms of separate
|
||||||
|
captures, which could straddle a screen transition.
|
||||||
|
|
||||||
|
Returns a list of (r, g, b) tuples in the same order as `points`.
|
||||||
|
"""
|
||||||
|
screenshot(PROBE_SHOT_PATH)
|
||||||
|
image = cv2.imread(PROBE_SHOT_PATH)
|
||||||
|
colors = []
|
||||||
|
for x, y in points:
|
||||||
|
b, g, r = image[y, x]
|
||||||
|
colors.append((int(r), int(g), int(b)))
|
||||||
|
return colors
|
||||||
|
|
||||||
|
|
||||||
def wait(seconds):
|
def wait(seconds):
|
||||||
time.sleep(seconds)
|
time.sleep(seconds)
|
||||||
|
|||||||
@ -28,6 +28,36 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
# Every confirmed subscreen (mailbox/cafe/shop/lesson/event) renders a
|
||||||
|
# uniform light header bar spanning nearly the full screen width at this y --
|
||||||
|
# is_on_subscreen only samples one x on that row (SUBSCREEN_HEADER_PROBE),
|
||||||
|
# which is cheap and has been fine for ordinary subscreen-vs-home checks, but
|
||||||
|
# was found live (2026-07-15, cafe.py's rank-up dismiss loop) to misread a
|
||||||
|
# full-screen "絆ランクアップ!" cutscene as "already back on subscreen" for
|
||||||
|
# some characters -- the single x=500 sample happened to land on a bright
|
||||||
|
# patch of that character's own art/background, not the real header. A real
|
||||||
|
# header bar is flat and uniform across its whole width; a photo-real
|
||||||
|
# cutscene composition (hair, uniform, the dark rank-up banner itself) is
|
||||||
|
# very unlikely to coincidentally read bright at MANY spread-out x offsets on
|
||||||
|
# the same row simultaneously. Used where that specific ambiguity matters
|
||||||
|
# (so far only cafe.py's rank-up dismiss) rather than swapped in for
|
||||||
|
# is_on_subscreen everywhere, to avoid changing already-working behavior at
|
||||||
|
# every other call site.
|
||||||
|
HEADER_ROW_Y = 10
|
||||||
|
HEADER_ROW_X_OFFSETS = (300, 500, 700, 900, 1100, 1300, 1500, 1700)
|
||||||
|
|
||||||
|
|
||||||
|
def is_header_bar_visible(driver):
|
||||||
|
# One screenshot for all 8 points (driver.colors_at) rather than 8
|
||||||
|
# separate color_at() calls -- cheaper, and atomic (every point comes
|
||||||
|
# from the same frame instead of drifting across ~8 sequential captures).
|
||||||
|
points = [(x, HEADER_ROW_Y) for x in HEADER_ROW_X_OFFSETS]
|
||||||
|
for r, g, b in driver.colors_at(points):
|
||||||
|
if not (r > SUBSCREEN_HEADER_MIN_CHANNEL and g > SUBSCREEN_HEADER_MIN_CHANNEL and b > SUBSCREEN_HEADER_MIN_CHANNEL):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _not_home(driver):
|
def _not_home(driver):
|
||||||
# "Home" means neither on a subscreen NOR under an open modal. Checking
|
# "Home" means neither on a subscreen NOR under an open modal. Checking
|
||||||
# only is_on_subscreen was found live (2026-07-11, ba_daily.py's
|
# only is_on_subscreen was found live (2026-07-11, ba_daily.py's
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@ -36,8 +36,34 @@ into a dialog that has none of them, repeatedly. Fixed by checking
|
|||||||
navigation.is_modal_open (a real dialog's darker dim) before the
|
navigation.is_modal_open (a real dialog's darker dim) before the
|
||||||
list-opened check: a dialog appearing before any row was clicked can only
|
list-opened check: a dialog appearing before any row was clicked can only
|
||||||
mean the ticket click itself raised one directly, so this now dismisses
|
mean the ticket click itself raised one directly, so this now dismisses
|
||||||
it via SWEEP_CONFIRM_BUTTON and returns False (skip invite this room)
|
it and returns False (skip invite this room) instead of proceeding.
|
||||||
instead of proceeding.
|
|
||||||
|
Two further real-usage bugs, both reported 2026-07-15 (next_fix.md) and
|
||||||
|
both fixed:
|
||||||
|
|
||||||
|
1. The cooldown-notice dismiss above originally clicked
|
||||||
|
SWEEP_CONFIRM_BUTTON's fixed coordinate, which could miss the notice's
|
||||||
|
own button, leaving it open -- the camera-pan drags that followed then
|
||||||
|
landed on the still-open dialog instead of the room view, breaking that
|
||||||
|
room's farming. Switched to an Escape keypress, verified with
|
||||||
|
navigation.is_modal_open and retried up to ROOM_OPEN_RETRIES times rather
|
||||||
|
than assumed to work on the first press -- an unconfirmed keypress would
|
||||||
|
have reproduced the exact same "stuck dialog, blind pan drags" failure
|
||||||
|
through a different unverified assumption (caught in review before this
|
||||||
|
was live-tested).
|
||||||
|
2. _dismiss_rank_up_if_shown originally checked navigation.is_on_subscreen,
|
||||||
|
which samples a single header pixel -- for some characters' rank-up art
|
||||||
|
that pixel reads bright by coincidence, so the loop believed the cutscene
|
||||||
|
had already cleared without ever pressing Enter, then the pat loop kept
|
||||||
|
polling find_cafe_sparkle() against the still-showing cutscene for the
|
||||||
|
rest of its budget and missed further students. Switched to
|
||||||
|
navigation.is_header_bar_visible, which requires many spread-out
|
||||||
|
header-row x positions to all read bright -- much less likely to
|
||||||
|
coincidentally match a full-screen character composition than a single
|
||||||
|
point. Best-effort fix: could not force a live rank-up on demand to
|
||||||
|
confirm end-to-end, so treat as implemented-but-unverified until one
|
||||||
|
happens naturally during a real cafe run (same caveat this file already
|
||||||
|
carried for the original rank-up dismiss before it was live-confirmed).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ba_auto import detector, navigation
|
from ba_auto import detector, navigation
|
||||||
@ -81,12 +107,25 @@ def _dismiss_rank_up_if_shown(driver, config):
|
|||||||
# returning None against it (a full-screen character portrait, nothing
|
# returning None against it (a full-screen character portrait, nothing
|
||||||
# like the sparkle template) for the rest of the room's click budget.
|
# like the sparkle template) for the rest of the room's click budget.
|
||||||
# That's the "freeze" -- not a timing fluke, a genuinely unhandled state.
|
# That's the "freeze" -- not a timing fluke, a genuinely unhandled state.
|
||||||
|
#
|
||||||
|
# Real-usage bug (2026-07-15, next_fix.md bug 1): this originally checked
|
||||||
|
# navigation.is_on_subscreen, which samples a single header pixel -- for
|
||||||
|
# some characters' rank-up art that pixel reads bright by coincidence, so
|
||||||
|
# the loop believed the cutscene had already cleared on the very first
|
||||||
|
# check and returned True without ever pressing Enter. The pat loop then
|
||||||
|
# kept polling find_cafe_sparkle() against the still-showing cutscene for
|
||||||
|
# the rest of its budget, finding nothing, which is the "skipped instead
|
||||||
|
# of self-healing and missed further students" the user reported. Fixed
|
||||||
|
# by switching to navigation.is_header_bar_visible, which requires many
|
||||||
|
# spread-out header-row x positions to all read bright -- much less
|
||||||
|
# likely to coincidentally match a full-screen character composition than
|
||||||
|
# a single point. See navigation.py's own comment for the full reasoning.
|
||||||
for _ in range(config.CAFE_RANK_UP_DISMISS_RETRIES):
|
for _ in range(config.CAFE_RANK_UP_DISMISS_RETRIES):
|
||||||
if navigation.is_on_subscreen(driver):
|
if navigation.is_header_bar_visible(driver):
|
||||||
return True
|
return True
|
||||||
driver.keypress("Return")
|
driver.keypress("Return")
|
||||||
driver.wait(1.5)
|
driver.wait(1.5)
|
||||||
return navigation.is_on_subscreen(driver)
|
return navigation.is_header_bar_visible(driver)
|
||||||
|
|
||||||
|
|
||||||
def _pat_current_view(driver, config):
|
def _pat_current_view(driver, config):
|
||||||
@ -201,8 +240,30 @@ def _open_invite_list(driver, config):
|
|||||||
if navigation.is_modal_open(driver):
|
if navigation.is_modal_open(driver):
|
||||||
title = detector.read_text(config.CAFE_INVITE_DIALOG_TITLE_RECT, psm=6, lang="jpn")
|
title = detector.read_text(config.CAFE_INVITE_DIALOG_TITLE_RECT, psm=6, lang="jpn")
|
||||||
print(f"[cafe] invite ticket click opened a dialog instead of the list (title: '{title}') -- likely on cooldown, treating invitation as unavailable")
|
print(f"[cafe] invite ticket click opened a dialog instead of the list (title: '{title}') -- likely on cooldown, treating invitation as unavailable")
|
||||||
driver.click(*config.SWEEP_CONFIRM_BUTTON)
|
# Real-usage bug (2026-07-15, next_fix.md bug 2): this originally
|
||||||
driver.wait(1)
|
# clicked SWEEP_CONFIRM_BUTTON's fixed coordinate to dismiss the
|
||||||
|
# notice, but that position can miss (this is a plain single-OK
|
||||||
|
# notice, not necessarily laid out identically to the two-button
|
||||||
|
# confirm/cancel dialogs SWEEP_CONFIRM_BUTTON was calibrated
|
||||||
|
# against) -- a missed click left the notice open, and the
|
||||||
|
# camera-pan drags that followed landed on the still-open dialog
|
||||||
|
# instead of the room view, breaking that room's farming.
|
||||||
|
# navigation.py's own return_to_home docstring documents Escape
|
||||||
|
# as confirmed live to close dialogs in this project without
|
||||||
|
# confirming anything, but that has never specifically been
|
||||||
|
# confirmed against THIS single-OK notice variant -- an unverified
|
||||||
|
# keypress is no more trustworthy than the unverified click it
|
||||||
|
# replaced. So verify with is_modal_open and retry the press
|
||||||
|
# (bounded, matching this file's own click-then-verify
|
||||||
|
# convention) instead of assuming one Escape worked.
|
||||||
|
for dismiss_attempt in range(1, ROOM_OPEN_RETRIES + 1):
|
||||||
|
driver.keypress("Escape")
|
||||||
|
driver.wait(1)
|
||||||
|
if not navigation.is_modal_open(driver):
|
||||||
|
break
|
||||||
|
print(f"[cafe] cooldown notice still open after Escape (attempt {dismiss_attempt}/{ROOM_OPEN_RETRIES})")
|
||||||
|
else:
|
||||||
|
print("[cafe] could not confirm the cooldown notice closed -- room state may still be blocked")
|
||||||
return False
|
return False
|
||||||
if not navigation.is_on_subscreen(driver):
|
if not navigation.is_on_subscreen(driver):
|
||||||
return True
|
return True
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user