diff --git a/ba_auto/config.py b/ba_auto/config.py index 0f01a20..92164f5 100644 --- a/ba_auto/config.py +++ b/ba_auto/config.py @@ -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 # complex shape needing find_cafe_sparkle-style matching. # -# All coordinates/colors below are pixel-scanned from real scrot captures on -# nik-gpu at the native 1920x1200 (not estimated from the non-native-resolution -# screenshots/gem_shop/*.png reference photos -- confirmed those are NOT 1:1 -# with real game coordinates, same finding as screenshots/cafe/student/'s own -# calibration history). Live-calibrated 2026-07-15 by manually driving the -# real dialog end-to-end (see scratchpad/gem_shop_*.png for the captures) -- -# this walk-through also genuinely claimed the account's real free package -# for the day (0 yen, +10 AP / +10,000 credits, confirmed via before/after -# counter values), so the two states below are both real, not guessed. +# RECALIBRATED 2026-07-29 after a real Blue Archive client UI update changed +# this screen's whole layout. Re-verified live on nik-gpu the same day (see +# scratchpad/gem_shop_after_click.png / gem_shop_weekly_tab.png / +# gem_shop_after_close.png). What changed from the original 2026-07-15 +# calibration: +# - The 期間限定/青輝石/パッケージ 3-top-tab layout is GONE. The dialog is +# now a real full subscreen (own header + top-left back arrow at the +# same (85, 55) navigation.BACK_BUTTON every other subscreen uses, not +# 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) -# パッケージ (package) tab, third of three (期間限定/青輝石/パッケージ). -# -# KNOWN FUTURE-BREAKAGE RISK (flagged in review, not yet fixable): this is a -# single fixed coordinate calibrated only against the CURRENT 3-tab layout, -# where 期間限定 (time-limited) is present as the first tab. The reference's -# own to_purchase_type branches on exactly this -- it picks a different -# パッケージ click x depending on whether 期間限定 is currently showing, -# because removing it reflows the remaining tabs. screenshots/gem_shop/2.png -# (captured live the same day as this calibration) shows 期間限定's own -# on-screen countdown ("終了まであと6日" / "26.06.24〜26.07.29"), meaning -# this account's 3-tab layout is itself temporary and due to change around -# 2026-07-21 -- not a theoretical edge case. No real screenshot of the -# resulting 2-tab (青輝石/パッケージ) layout exists yet to calibrate the -# correct alternate x, so a second coordinate has NOT been guessed in here -# rather than grounded in a real capture, per this project's own -# no-guessing-at-pixel-values discipline. -# -# 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) +# 一般商品 (general products), 3rd item in the left sidebar +# (おすすめの商品/限定商品/一般商品/定額商品/青輝石/特別支援). Always +# clicked explicitly rather than relying on it already being selected -- +# even though it was the landing tab in this recalibration, per this +# project's own established discipline of never trusting a remembered/ +# default UI selection before a real click (see memory: BA count/quantity +# steppers remember last-used values; the same caution applies to any +# selected-tab state here). +GEM_SHOP_GENERAL_PRODUCTS_TAB = (196, 376) +# デイリー (daily), 1st of 3 sub-tabs (デイリー/ウィークリー/マンスリー) +# shown top-right once 一般商品 is open. Also always clicked explicitly for +# the same reason as GEM_SHOP_GENERAL_PRODUCTS_TAB above. +GEM_SHOP_DAILY_SUBTAB = (1330, 148) +# 毎日無料パッケージ card's own 購入 (purchase) button -- the only card +# shown under 一般商品 -> デイリー (fixed position, no scrolling/search +# needed). +GEM_SHOP_FREE_CARD_BUY_BUTTON = (576, 630) # 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 -- -# confirmed live by claiming the real package and diffing before/after -# captures pixel-for-pixel at this exact point: -# available ("一日に1回まで購入可能"): flat dark navy ~(41, 65, 90) -# claimed ("一日に0回まで購入可能"): flat dark red ~(144, 38, 47) -# r vs b relationship alone cleanly separates them (navy: b>r by ~50; red: -# r>b by ~100), so no OCR of the "0"/"1" count text is needed -- the -# reference itself doesn't OCR this either, it template-matches two whole -# separate "purchasable"/"non-purchasable" card images. -GEM_SHOP_FREE_CARD_STATUS_PROBE = (420, 710) -GEM_SHOP_STATUS_AVAILABLE_RGB = (41, 65, 90) -GEM_SHOP_STATUS_CLAIMED_RGB = (144, 38, 47) +# see the RECALIBRATED note above for how each value was obtained: +# available ("一日にN回まで購入可能"): flat light blue ~(226, 236, 246) +# claimed ("一日に0回まで購入可能"): flat dark red ~(190, 56, 66) +# The two are far enough apart (light vs dark, blue-family vs red-family) +# that no OCR of the "0"/"N" count text is needed -- the reference itself +# doesn't OCR this either, it template-matches two whole separate +# "purchasable"/"non-purchasable" card images. +GEM_SHOP_FREE_CARD_STATUS_PROBE = (410, 485) +GEM_SHOP_STATUS_AVAILABLE_RGB = (226, 236, 246) +GEM_SHOP_STATUS_CLAIMED_RGB = (190, 56, 66) 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 # Bounds the "press Enter, re-check the free card's status" loop that # advances through the confirm-purchase notice and the "報酬獲得!" reward -# banner after clicking 購入 -- confirmed live the reward banner's own entry -# animation did not accept input on the first 1-2 presses while its sparkle -# animation was still playing, so this needs real patience, not just 1-2 -# tries. +# banner after clicking 購入 -- confirmed live (pre-redesign) the reward +# banner's own entry animation did not accept input on the first 1-2 +# presses while its sparkle animation was still playing, so this needs real +# 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 # Circle (サークル / guild), ported from module/group.py. Home -> bottom-nav diff --git a/ba_auto/reference_notes/mapping.md b/ba_auto/reference_notes/mapping.md index 4b0dfba..a7d2745 100644 --- a/ba_auto/reference_notes/mapping.md +++ b/ba_auto/reference_notes/mapping.md @@ -7,7 +7,7 @@ Maps each local feature to the corresponding `~/repo/baas-reference/module/...` | Mailbox | `module/mail.py` | `to_mail`, `implement` | `ba_auto/tasks/mailbox.py` | tap/click via xdotool, screenshot via scrot, `color.rgb_in_range` → `driver.color_at` pixel-probe check | Migrated: real Python, state-verified via color probe (no legacy bridge) | | Cafe | `module/cafe_reward.py` | `to_cafe` (its `relationship_rank_up` popup-handling now also ported, see below), `interaction_for_cafe_solve_method3`, `collect`, `invite_girl`/`invite_by_affection`/`checkConfirmInvite` (student invitation, added 2026-07-14) | `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. **Student invitation (2026-07-14)**: per explicit user direction, invite a student into each room before farming it, preferring highest affection, always skipping (never confirming) a candidate that would swap an already-seated student's costume or move one in from the other room — directly ports `invite_by_affection`/`checkConfirmInvite`'s own logic with the reference's default `cafe_reward_allow_exchange_student`/`cafe_reward_allow_duplicate_invite` both `False` (no local config exists to make either configurable). Live-calibrated against nik-gpu with **zero real tickets spent** — all 3 real dialog variants (normal confirm, same-room costume-swap warning, neighboring-room move warning) found and confirmed live, every one cancelled rather than confirmed during calibration. The heart-shaped affection badge OCR reuses `detector.read_int_on_heart_badge` directly (pixel-confirmed the same widget lesson.py's own badges use — digit pixels sampled RG, matching that function's exact masking assumption) rather than building a second OCR path. Dialog-type detection OCRs the title bar and checks for either warning's own distinctive substring (`衣装`/`隣`) rather than an exact match, mirroring `event_sweep.py`'s own `"終了"` substring-match reasoning. All 5 heart-badge reads and all 3 dialog classifications verified offline against the real saved calibration screenshots before deploying (exact match, zero mismatches). **Confirmed live with real tickets spent, both rooms**: room 1 correctly skipped one 衣装替え (costume-swap) candidate then invited row 1 cleanly; room 2 correctly skipped three consecutive 隣のカフェの生徒を招待 (neighboring-room-move) candidates (expected — room 1's own invite had just taken the account's highest-affection students) then invited row 3 cleanly. Both newly-invited students were immediately patted successfully in the same run (the "newly invited student can be farmed" requirement), income was claimed, and the task returned cleanly to the true home screen with no warnings anywhere in the log. **Horizontal camera panning (2026-07-14)**: per explicit user direction ("due to my screen size... move screen most right and most left then farm"), `_pat_room` now pans the room camera to its rightmost extreme via a new `driver.drag()` primitive (this project's first — distinct from `driver.scroll()`'s wheel-based gesture, which is for list widgets, not a room camera), farms there, pans to the leftmost extreme, farms there too — no vertical panning, per the user's own instruction. Ports the reference's `zoom_out`'s underlying intent (see the whole room regardless of width) via the user's own specified mechanism (panning) rather than the reference's (zoom). Live-confirmed drag-direction-to-reveal-side mapping, that HUD elements (status bar, ticket buttons) stay fixed regardless of pan (no camera reset needed afterward), and that one drag already reaches the true extreme (extra repeats are a confirmed no-op, kept as a safety margin). **Confirmed live with a real game-state change**: a full run patted a real sparkle in room 2 (score 0.997) specifically after panning to an extreme, while room 1 found nothing that run (expected — per-student cooldown). Clean end-to-end completion, no warnings. **Invite ticket cooldown bugfix (2026-07-14)**: real-usage report (with screenshot) that the invite step kept failing once the account's ticket went on cooldown — `_open_invite_list`'s original check (`not is_on_subscreen`) couldn't distinguish the real MomoTalk list opening from a "通知" cooldown notice ("待機時間が経過した後に、再度招待することができます。") opening directly instead, since both dim the header the same way; misread as "list opened", it sent `_ensure_invite_sort`/`_try_invite_row`'s fixed coordinates into a dialog that has none of them. Fixed by checking `navigation.is_modal_open` (the darker real-dialog reading) first — a dialog appearing before any row is clicked can only mean the ticket click raised one directly — and dismissing it via the shared `SWEEP_CONFIRM_BUTTON`, returning `False` so the existing "skip this room's invite" fallback handles it. **Confirmed live**: both rooms correctly detected and dismissed the cooldown notice with no cascading errors, task completed cleanly (exit 0, true home screen confirmed via screenshot). See `plan.md`'s Phase 6 follow-up #4. **Two further real-usage fixes (2026-07-15, `next_fix.md`)**: (1) the cooldown-notice dismiss above now presses Escape and verifies via `navigation.is_modal_open` with bounded retry, instead of a fixed-coordinate click that could miss and leave the notice open under the following pan drags; (2) `_dismiss_rank_up_if_shown` switched from `navigation.is_on_subscreen`'s single-pixel header probe (found live to misread some characters' rank-up cutscene art as bright) to a new `navigation.is_header_bar_visible`, requiring 8 spread-out header-row points to all read bright via one atomic multi-point capture (`driver.colors_at`, added alongside it). Both best-effort — not yet re-confirmed live post-fix. **A real rank-up happened naturally via cron (2026-07-16)**, reported live with a screenshot of the game left stuck on the cutscene — exposed a third bug, a timing gap rather than a threshold problem: `_dismiss_rank_up_if_shown` was only ever called once, immediately after a pat, with no wait beforehand, but the cutscene renders with its own client-side animation delay, so that single check could catch the tail end of the still-normal room view and conclude "clear" a beat before the actual cutscene appeared. Confirmed via a live pixel-check that the resulting stuck cutscene reads under `is_on_subscreen`/`is_modal_open`'s thresholds too (`SUBSCREEN_HEADER_PROBE` read `(180,227,244)`, r=180 < the 200 both checks need), so `navigation.return_to_home`'s generic cleanup also couldn't recover it — every "are we home" check downstream falsely agreed nothing was wrong, and the whole `q4h` cron run finished "successfully" with the game actually left stuck. Fixed by checking on every `_pat_current_view` poll iteration instead of only right after a pat, so a delayed cutscene appearance is caught (and dismissed) roughly a second later instead of never. **Confirmed live**: manually reproduced the exact stuck state left by the incident and confirmed a single Enter (the same action `_dismiss_rank_up_if_shown` already takes) cleared it back to the real cafe room view; the per-iteration check itself has not yet been re-exercised against a fresh live rank-up trigger end-to-end (same "could not force one on demand" caveat as the original fix). **`SPARKLE_CLICK_OFFSET` recalibrated (2026-07-17)**: reported live with a screenshot (`screenshots/cafe/bug_student_close_together.png`) — when two students stand close together, the original `(75, 47)` offset (ported byte-for-byte from `scripts/detect_and_click.py`, never independently verified) overshoots past the intended target's head and lands on a different, closer student instead; the click doesn't register, so `find_cafe_sparkle()` matches the same still-showing sparkle again next iteration, repeatedly clicking nearly the same point for the rest of the room's budget (confirmed live: 13 of 16 "pats" in one run clustered within a ~10px box). First recalibration attempt, `(0, 0)` (raw template-match center), was also wrong — reported live ("clicking the actual sparkle instead of the student head"): a coincidental ambient thought-bubble animation at the test position had been misread as a hover confirmation. Properly recalibrated via numbered candidate-point overlays (screenshots with several labeled offset options drawn on a real live sparkle; the user picked the correct one directly on-screen) against two independent real students in different poses (sitting at an arcade cabinet, lying on a couch) — both converged on the same small offset, `(51, 15)`. **Confirmed live** via the real `./ba_dailies.sh cafe` CLI path: 6 sparkles patted across both rooms at genuinely distinct coordinates with zero repeated-click clustering, clean `[cafe] Done.` and true-home return. | | Stamina/AP | `module/collect_daily_task_power.py` | `to_tasks`/`implement` | `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 | Migrated: Mission-panel claim done (see `plan.md` Phase 8). The gem shop's own Daily Free Power flow (`module/collect_daily_free_power.py`) is a separate task now — see the "Gem shop daily free package" row below. **Daily gem reward bugfix (2026-07-16)**: reported live by the user against a real account state — the reference's own `implement` checks TWO separate button regions (the main 一括受取 area, then a second, separate "claim daily pyroxenes" button checked independently after), but only the first was ported originally. Fixed by porting the second region too: a new `MISSION_DAILY_GEM_CLAIM_BUTTON` (config.py), reusing the same bright-yellow-vs-grey color-probe style as the existing `MISSION_CLAIM_PROBE`, clicked via `driver.click` (not Enter — unlike 一括受取, this button has no keybind shown on screen) with a click-then-verify retry loop matching this project's established discipline. **Confirmed live**: gem balance went from 12,384 → 12,404 (exactly the reward's stated +20), clean return to the true home screen, no warnings. | -| Gem shop daily free package | `module/collect_daily_free_power.py` | `implement`, `to_purchase_pyroxenes_menu`, `to_purchase_type`, `detect_free_power_availability`, `collect_daily_free_power`, `return_to_main_page` | `ba_auto/tasks/gem_shop.py` | Reference detects every step via `core.picture.co_detect` + fixed-region OpenCV template matching (no OCR anywhere in this reference flow) — ported here as plain color probes instead, matching this project's own established equivalent for a simple flat-color state-A/state-B difference (see `cafe.py`'s `CLAIM_DISABLED_RGB`, `stamina.py`'s `MISSION_CLAIM_PROBE`) rather than building new template assets. The free card's own status bar reads flat dark navy (~(41,65,90)) when available vs flat dark red (~(144,38,47)) once claimed today — a clean, high-contrast signal pixel-scanned from real captures, no OCR of the "0"/"1" count text needed. A dedicated 4-point "is this dialog (or a nested confirm/notice card) showing" check (`GEM_SHOP_DIALOG_PROBES`) was needed because `navigation.is_on_subscreen`/`is_modal_open`'s default probes both proved unreliable on this dialog (it overlays the home screen directly rather than being a full subscreen, and its own opaque white card sits right on top of `is_modal_open`'s probe point) — same class of default-probe mismatch `story_sweep.py`/`bounty.py` already documented for their own wide modals, fixed the same multi-point-beats-single-point way `navigation.is_header_bar_visible` was fixed for cafe's rank-up cutscene the same day. | Done. Live-calibrated against nik-gpu 2026-07-15 by manually driving the real dialog end-to-end via raw xdotool/scrot (see `scratchpad/gem_shop_*.png`) — this **genuinely claimed the account's real free package for the day** (0 yen, confirmed +10 AP / +10,000 credits via before/after counter values: AP 64→74, credits 161,391,144→161,401,144), which calibrated both the "available" and "already claimed" visual states from real data. The actual `gem_shop.py` module was then live-tested for real against the resulting "already claimed today" state. The "available → claim" code path itself uses the same coordinates/logic already confirmed live via the manual walkthrough, but has not yet been exercised by the module itself end-to-end — worth a follow-up live check the next time the package resets and hasn't been claimed yet. Added to `DEFAULT_ORDER` (unlike most recent additions): it only ever reclaims a genuinely free, once-per-day resource with no choice to make, matching mailbox/cafe/stamina's own "reclaim something free" category rather than the "spends a resource, needs opt-in" category. | +| Gem shop daily free package | `module/collect_daily_free_power.py` | `implement`, `to_purchase_pyroxenes_menu`, `to_purchase_type`, `detect_free_power_availability`, `collect_daily_free_power`, `return_to_main_page` | `ba_auto/tasks/gem_shop.py` | Reference detects every step via `core.picture.co_detect` + fixed-region OpenCV template matching (no OCR anywhere in this reference flow) — ported here as plain color probes instead, matching this project's own established equivalent for a simple flat-color state-A/state-B difference (see `cafe.py`'s `CLAIM_DISABLED_RGB`, `stamina.py`'s `MISSION_CLAIM_PROBE`) rather than building new template assets. **Recalibrated 2026-07-29** after a real client UI update replaced the old 3-top-tab overlay dialog with a genuine full subscreen (left sidebar + デイリー/ウィークリー/マンスリー sub-tabs under 一般商品). The free card's own status bar now reads flat light blue (~(226,236,246)) when available vs flat dark red (~(190,56,66)) once claimed today — still no OCR needed, same high-contrast-flat-color reasoning as before, just re-read off the new layout. The old dedicated `GEM_SHOP_DIALOG_PROBES` 4-point "is this dialog showing" check is gone entirely: the redesigned screen is a real subscreen (own header, back arrow at the shared `navigation.BACK_BUTTON` position), confirmed live that `navigation.is_on_subscreen`/`return_to_home` now work correctly here, so `gem_shop.py` was simplified to share that machinery with mailbox/cafe/shop/lesson instead of the old bespoke Escape-only close. | Done, recalibrated. Original: live-calibrated against nik-gpu 2026-07-15 by manually driving the real dialog end-to-end via raw xdotool/scrot — genuinely claimed the account's real free package for the day (0 yen, confirmed +10 AP / +10,000 credits). Recalibration (2026-07-29): the new "already claimed" status color and every new coordinate (icon, sidebar tab, daily sub-tab, buy button, status probe, screen-open/close detection) were re-verified live on nik-gpu against the real redesigned screen (see `scratchpad/gem_shop_after_click.png`/`gem_shop_weekly_tab.png`/`gem_shop_after_close.png`) — confirmed the icon click still opens it, confirmed `is_on_subscreen`/`return_to_home` now correctly detect/close it, and confirmed the "already claimed" red status color directly off the real card (still claimed from the 2026-07-15 session's calibration run, today's own reset hadn't been claimed yet at investigation time but the account's daily reset had already occurred, so it was showing the post-claim state). The new "available" light-blue color was inferred from three other real, currently-purchasable cards under the ウィークリー tab (same shared status-bar UI component, all three reading identically) rather than from the daily free card itself, since that card wasn't in the available state at recalibration time — **not yet re-confirmed against the daily free card's own available state, or against the full claim-confirmation/reward-banner flow post-redesign**; worth a deliberate check the next time the package resets and hasn't been claimed yet. Added to `DEFAULT_ORDER` (unlike most recent additions): it only ever reclaims a genuinely free, once-per-day resource with no choice to make, matching mailbox/cafe/stamina's own "reclaim something free" category rather than the "spends a resource, needs opt-in" category. | | 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; 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; rewritten Phase 22 to match story_sweep_hard.py's Phase 21 fixes and add its own campaign guard). Config-driven exact `(region, stage, count)` targets (`config.STORY_SWEEP_TARGETS`), not latest-region/random-stage. Opt-in only, not in default flow. **2026-07-21 rewrite (Phase 22)**: per explicit user direction ("match the hard implementation on best practices"), ported story_sweep_hard.py's fixes back into this module — an explicit Normal-tab guard (`config.NORMAL_TAB`/`_open_normal_tab`, needed now that story_sweep_hard can run earlier in the same `q4h` preset and leave Hard selected), a loop-level self-heal (`_ensure_task_screen`) replacing the old per-path "Escape if on subscreen" cleanup that would have backed out of the region browser after every target (never triggered in production since this module only ever swept one rotation target per run, but a real latent bug), MAX/+ clicks no longer requiring a visible stepper raise as proof of success (same low-AP false-negative class Hard hit), `_click_plus` now resets to a known baseline via a new `SWEEP_MIN_BUTTON` before raising (closing a latent version of bounty.py's real overspend bug that this module's original `_click_plus_and_verify` never guarded against), a newly-added `_confirm_dialog_is_sweep` OCR gate against the two-stacked-button hazard (confirmed live 2026-07-21 that Normal's own tabbed modal has the identical gold 任務開始 button beneath 掃討開始 that config.py's own `SWEEP_MAX_BUTTON` comment already documented but this module never actually guarded against), the same `clicked_any`-gated `_watch_sweep_result` fix, and a `REGION_READ_RETRIES` OCR-read retry. Also added a genuinely new campaign-active guard (`config.TASK_CAMPAIGN_BADGE_RECT`, renamed from `HARD_CAMPAIGN_BADGE_RECT` once shared) — confirmed live to be pixel-identical rect/color to Hard's own banner, just reading "任務Normalで獲得できる報酬量が2倍(+100%)になっています" instead of "Hard". Confirmed live that Normal mode has no per-stage daily clear cap (no "残り回数" indicator anywhere), so Hard's gem-refill hazard does not apply here. `story_sweep_force` added as the override command. **Live-tested for real the same day**: a real MAX sweep of the day's rotation target (30-2) succeeded cleanly end-to-end on the first attempt — campaign guard passed, OCR stage-row match succeeded despite the known leading-digit misread ("20-2" read but the "2" suffix still matched), sweep confirmed and correctly detected as `"swept"` (not the old cosmetic misreport), clean return home. AP dropped from ~139 to 1 and credits rose by 13,966, confirming a real substantial MAX sweep. | | 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). **Second bug found on the next real run** (after the above fix correctly recovered and correctly OCR'd the target row): the row's own 入場 (enter) button click had no retry, unlike every other click-then-confirm step in this module -- missed once, aborted the whole run. Fixed via `_open_stage_modal`, the same click-then-verify-then-retry pattern already used everywhere else in the file. **Third bug found on a third real run**: `_find_stage_row`'s wrong-page detection false-positived twice (a fresh navigation's list hadn't finished rendering on the first OCR pass) before self-correcting on the 3rd attempt, wasting expensive return-home retries on what was really just a timing race -- fixed with a cheap in-place rescan before concluding "wrong page." Also, once past that, the 掃討開始 (start sweep) click turned out to be the last bare, unretried click in the file -- fixed via `_click_sweep_start_and_verify`. **Fourth round, self-driven live iteration per explicit user direction (fix/deploy/run/screenshot/diagnose in a loop, no stopping to report)**: found two more root causes and reached the first confirmed real live sweep. (1) The badge carousel's auto-rotate timer is far slower than the retry window -- a run hit "wrong page" on all 3 attempts genuinely, confirmed by screenshot; fixed by discovering and clicking the carousel's own pagination dots directly (`config.EVENT_BADGE_DOT_X`) instead of hoping the ambiguous badge shows the right item. (2) A genuine cold-start settle delay (up to ~20s for the Quest tab's stage list to populate after a fresh navigation, most likely a one-time server round-trip), not a flaky race -- confirmed via standalone probe scripts polling the real OCR pipeline once/second for a minute; fixed by widening `STAGE_ROW_SCAN_ATTEMPTS`/`_RETRY_WAIT` to match. With both fixes, a real run completed end-to-end: 200 AP spent (10x MAX sweep), credits gained, clean return home, confirmed by screenshot -- see `plan.md`'s Phase 14 follow-up #4 for the full writeup. **Fifth round, reported by the user a day later**: the exact same wrong-page-looping symptom recurred. Per the user's explicit request, added a DIRECT check for the finished event's own "イベント期間が終了しました" text (Japanese OCR, newly installed on nik-gpu since this project previously only needed digit/English reads) instead of the indirect "zero valid rows" heuristic -- `config.EVENT_FINISHED_TEXT_RECT`/`_is_finished_event_page`, live-calibrated and confirmed both ways (exact phrase match on a real finished page, no false positive on the correct page). This immediately proved the badge carousel's dot-clicking (follow-up #4's fix) is NOT reliably controllable after all -- sometimes worked, sometimes didn't, on the identical badge state -- while simply waiting longer let the carousel's own auto-rotate timer land on the correct item independently. Fixed by widening `WRONG_PAGE_RETRIES`/`_WAIT` (3x3s -> 6x12s) to give the timer real room to cycle, keeping dot-clicking as a harmless supplementary nudge. Two more real sweeps confirmed this fully working (11x MAX sweep, 219 AP spent, credits gained, clean home return, confirmed by screenshot -- three real confirmed sweeps total across this whole investigation). Also found and fixed the real root cause of the lingering `"unrecognized_state"` cosmetic bug (not a budget issue after all): `_watch_sweep_result`'s "swept" condition required the stage modal to still be open, but this event's flow can auto-return all the way to the Quest list instead, a terminal state it never anticipated -- fixed with a second `ends` condition gated on having clicked at least one result button first. Not yet re-verified live (AP exhausted by the successful sweep). See `plan.md`'s Phase 14 follow-up #5 for the full writeup. **Sixth round (2026-07-11)**: the daily rotation picked stage 9 for the first time, hitting a previously-flagged-but-never-exercised gap -- rows 366/538 ("08"/"09") consistently misread by OCR ("2" and empty/None) on every psm mode, even though the crop looked completely clean by eye; confirmed NOT a navigation/timing bug since rows 710/883/1055 ("10"/"11"/"12") read fine in the same run. Root cause (`scratchpad/probe_ocr_fix_08_09.py`): tesseract's segmentation fails on this tight edge-to-edge crop (no whitespace margin) for a leading-zero digit pair specifically -- adding a plain white border around the upscaled crop before OCR fixed both digits exactly, at every psm mode, without affecting "10". Fixed via new `detector.read_int_bordered`, now used for all `EVENT_STAGE_ROW_Y` reads. **Confirmed live**: stage 9 found, sweep executed for real (AP 233->14, credits +5,892, screenshot-confirmed safe return home) -- the originally reported bug is fixed. The same run again logged `unrecognized_state`, proving follow-up #5's `clicked_any`-gate fix addressed a real but secondary issue, not the full story. Diagnosed at zero AP cost (`scratchpad/probe_result_button_fp.py`, checks `_find_result_button` against the plain Quest list with no sweep running): the shared `SWEEP_RESULT_BUTTON_REGION` (borrowed from `story_sweep.py`) reaches into this event's own character-art panel and false-positive-matches `SWEEP_CONFIRM_CYAN` there with no dialog showing at all, confirmed on both a wrong event page and the correct one's own plain list -- so `_watch_sweep_result` kept "finding" a result button after the real one was already dismissed, and its "modal closed, no result button" end condition could never match. Fixed with a new event_sweep-only `config.EVENT_SWEEP_RESULT_BUTTON_REGION` (x narrowed to exclude the character-art panel, still comfortably covering the real buttons), confirmed live against the actual false-positive condition (now returns `None` where it previously didn't) -- not yet re-confirmed via a fresh full sweep since AP was too low that day. See `plan.md`'s Phase 14 follow-up #6. | | Circle (Group/Club) daily check-in | `module/group.py` | `implement`, `to_group` | `ba_auto/tasks/circle.py` | Reference's `picture.co_detect` polling against fixed screen positions + `rgb_possible`/`img_possible` template states → this client's own 2-click nav (home → bottom-nav ソーシャル icon → サークル card) verified with the existing shared `navigation.is_modal_open`/`is_on_subscreen` (no new probe needed — pixel-confirmed both real "reached the circle screen" states, with or without the reward modal, satisfy that combined check) | Done. Live-calibrated 2026-07-15 on nik-gpu by driving the real flow end-to-end via raw xdotool/scrot — this **genuinely claimed the account's real circle check-in reward for the day** (+10 AP, confirmed via the real reward dialog "今日のサークルへの参加報酬... AP x10... 報酬はメールボックスから受け取ることができます"), which calibrated both real states from real data: the first-entry reward modal, and (by re-entering immediately after) the already-checked-in straight-to-chat state with no modal. Also live-confirmed a real `XIGNCODE` anti-cheat overlay stole a `BACK_BUTTON`-coordinate click mid-calibration (same recurring gotcha documented elsewhere in this project) — recovered via the standard `windowactivate`+`windowraise` escalation. Per explicit user direction, the task presses Escape directly to return home (confirmed live: a single Escape from the サークル screen returns straight to true home, skipping back through the intermediate ソーシャル hub page) rather than clicking `navigation.BACK_BUTTON`. The reference's `group_join-club` ("not in a circle") outcome is deliberately not ported — this account is already a member, and the task's scope (per explicit user direction) is entry only, no mailbox claim. The actual `circle.py` module was then live-tested for real via `./ba_dailies.sh circle`, correctly reading the "already checked in today" state left over from calibration and returning cleanly home. **Not yet live-tested**: the "first entry → claim reward" code path itself, since the account was already checked in for today by the time the module existed — same disclosed gap shape as gem_shop's own "available → claim" path. Added to `DEFAULT_ORDER` alongside mailbox/cafe/stamina/gem_shop, since it's a pure free reclaim with no decision to make. **A `reference-parity-reviewer` pass the same day caught and fixed three real gaps**: a too-generic terminal-state check that could false-positive "already checked in" from a stuck non-home starting state (fixed via `navigation.return_to_home(driver)` at the top of `run()`, matching arena.py/bounty.py precedent); an unverified reward-dismiss Enter press and closing Escape (fixed via `_dismiss_reward`/`_leave_circle`, bounded retry-until-verified loops directly porting `gem_shop.py`'s `_claim_free_package`/`_close_gem_shop`); and a missing `driver.focus_game()` escalation in the entry retry loop for the `XIGNCODE` overlay (which had already struck live during this task's own calibration) — fixed to match `navigation.click_back`'s own escalation convention. Re-confirmed live via `./ba_dailies.sh circle` after all three fixes. See `plan.md`'s Phase 17 for the full writeup. | diff --git a/ba_auto/tasks/gem_shop.py b/ba_auto/tasks/gem_shop.py index da01ed7..6f695d4 100644 --- a/ba_auto/tasks/gem_shop.py +++ b/ba_auto/tasks/gem_shop.py @@ -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 values) -- see scratchpad/gem_shop_*.png for the captures. That exercised the "available -> claim it" path for real, via raw xdotool/scrot rather -than this actual module. This module was then live-tested for real -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. +than this actual module. -A `reference-parity-reviewer` pass the same day caught a real bug in the -first version of this module: the final dialog-close step verified success -via `navigation.is_on_subscreen`, but config.py's own writeup on -`GEM_SHOP_DIALOG_PROBES` already documents that probe as blind to this -specific dialog (reads the same whether it's open or closed) -- so that -check could never fail, making a stuck-open dialog unrecoverable by this -module (and, per the same blindness, by `navigation.return_to_home`'s -shared fallback too). Fixed by adding `_close_gem_shop`, which verifies -against the module's own correctly-calibrated `_dialog_open` instead, with -a bounded retry -- reused for both closing paths (the "unknown status" -abort and the normal end-of-run close), replacing the original's second, -separately-broken blind Escape in the abort path too. +RECALIBRATED 2026-07-29 after a real Blue Archive client UI update replaced +the old 3-top-tab overlay dialog (期間限定/青輝石/パッケージ) with a real +full subscreen (left sidebar + デイリー/ウィークリー/マンスリー sub-tabs +under 一般商品). See config.py's "Gem shop daily free package" section for +the full writeup of what was re-verified live and what's still an inferred +value. The old `_dialog_open`/`_close_gem_shop` custom probes existed only +because the pre-redesign overlay dialog was invisible to +navigation.is_on_subscreen/is_modal_open; the redesigned screen is a real +subscreen, confirmed live, so this module now shares the same +is_on_subscreen/return_to_home machinery every other subscreen-based task +(mailbox/cafe/shop/lesson) already uses, and those two custom probes are +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): - 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 +from ba_auto import navigation def _open_gem_shop(driver, config): for attempt in range(1, config.GEM_SHOP_ICON_RETRIES + 1): driver.click(*config.GEM_SHOP_ICON) driver.wait(2) - if _dialog_open(driver, config): + if navigation.is_on_subscreen(driver): 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 -def _open_package_tab(driver, config): +def _open_daily_general_tab(driver, config): 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) status = _read_free_card_status(driver, config) if status != "unknown": 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" @@ -86,24 +85,6 @@ def _read_free_card_status(driver, config): 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): driver.click(*config.GEM_SHOP_FREE_CARD_BUY_BUTTON) driver.wait(2) @@ -119,14 +100,14 @@ def run(driver, config): driver.focus_game() 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 - status = _open_package_tab(driver, config) + status = _open_daily_general_tab(driver, config) if status == "unknown": print("[gem_shop] could not read the free package card's status, aborting without pressing further keys") - if not _close_gem_shop(driver, config): - print("[gem_shop] warning: could not confirm the dialog closed") + if not navigation.return_to_home(driver): + print("[gem_shop] warning: could not confirm return to home") return if status == "claimed": @@ -138,6 +119,6 @@ def run(driver, config): else: print("[gem_shop] warning: could not confirm the free package was claimed after retries") - if not _close_gem_shop(driver, config): - print("[gem_shop] warning: could not confirm the dialog closed") + if not navigation.return_to_home(driver): + print("[gem_shop] warning: could not confirm return to home") print("[gem_shop] Done.")