diff --git a/CLAUDE.md b/CLAUDE.md index ddfe97e..0edef27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,6 +84,8 @@ ERROR|WARNING|failed|Approach failed|starting from True|DAMAGED|SendInput.*misse | `rejecting low-confidence .* implies reversal` | Working as intended — pather refused a phantom node match (Bug 25) | | `Found open panels .* Chickening to be safe` | If the char is at full HP, it walked onto the waypoint — Bug 26 | | `Got stuck exit pather` / char against town wall | Fabricated node position from the low-confidence fallback — Bug 25 | +| `expected result .* not found in GEMS convert panel` repeating | Stash tab never switched — gems being handled in the wrong tab, Bug 27 | +| `could not activate the .* stash tab` | Tab bar geometry drifted; re-measure tab centres against the live client — Bug 27 | --- @@ -504,6 +506,48 @@ threat: send `esc` **without** incrementing `_count_panel_detects`. Bounded by to the normal chicken path, and `_count_wp_panel_detects` resets as soon as a poll sees no panel at all. +### Bug 27: gem conversion ran in the PERSONAL tab — tab-switch click missed the tab bar (2026-08-26) +**Files:** `src/transmute/transmute.py`, `config/params.ini` + +`convert_all_gems` ran its entire routine in whichever stash tab happened to be open — +PERSONAL — never the GEMS tab. It logged `[819/999] … expected result +INVENTORY_TOPAZ_PERFECT not found in GEMS convert panel; trying first slot fallback` **on +every one of 800+ iterations** and kept going, ctrl+shift+clicking blind into the personal +stash grid. + +**Root cause:** `GEMS_TAB_Y = 100`. Measured off the live client, the stash tab labels +occupy the row **y = 63–78** (selected-tab underline at y=80) and the **stash grid starts at +y≈87**. So every `_switch_to_gems_tab()` click at y=100 landed on a *stash slot*, not a tab. +The X values were already correct — only Y was wrong, by ~30px. + +Measured tab centres on this client (5 tabs: PERSONAL SHARED GEMS MATERIALS RUNES): + +| PERSONAL | SHARED | GEMS | MATERIALS | RUNES | +|---|---|---|---|---| +| (68, 70) | (144, 70) | (220, 70) | (295, 70) | (370, 70) | + +**Second, independent bug found alongside it:** `params.ini` had `stash_tabs=6` with only +**5** tabs on screen. `inventory/common.tab_properties()` divides the tab bar by that number, +so it computed centres of `63/127/192/256/320/384` against real centres of +`68/144/220/295/370` — tabs 2, 3 and 4 clicked the **gaps between tabs**. Set to `5`. + +**Fix:** +1. `TAB_Y = 70`, plus named X constants for all five tabs. +2. `stash_tabs=5`. +3. **Tab switches are now verified, not fire-and-forget.** `_switch_to_tab()` clicks, confirms + the tab actually became active, retries up to 3×, and returns False. `convert_all_gems` + aborts on False instead of converting in the wrong tab. + +**How to detect the active tab** — measure the **cell background**, not the glyphs: +`np.percentile(gray_label_cell, 30) > 52` (active ≈ 67–68, inactive ≈ 38–39). A +brightness-of-text test does **not** work: label brightness scales with label length, so an +active `GEMS` peaks at 167 while an active `PERSONAL` hits 215 — any glyph threshold that +catches PERSONAL misreads GEMS as inactive. This was a real wrong turn during the fix. + +**The recurring lesson (same as Bug 24):** an unverified click that "should" have worked will +fail silently forever. Any click that changes UI *mode* — a tab, a page, a panel — must be +confirmed before the code acts as if it took effect. + --- ## Verifying a boss run actually worked diff --git a/config/params.ini b/config/params.ini index ce29357..77be765 100644 --- a/config/params.ini +++ b/config/params.ini @@ -389,8 +389,11 @@ stash_destination=0,1,2,3 transmute=flawless ;how often we want to run transmute routine(e.g. every 100 games) transmute_every_x_game=60 -; number of stash tabs (ROTW has more than 6, adjust as needed) -stash_tabs=6 +; number of stash tabs — drives tab click geometry in inventory/common.tab_properties(). +; MUST match the tab bar on screen. This client shows 5: PERSONAL SHARED GEMS MATERIALS RUNES. +; It was 6, which made tab_properties compute centres of 63/127/192/256/320/384 while the +; real label centres are 68/144/220/295/370 — tabs 2, 3 and 4 clicked the gaps between tabs. +stash_tabs=5 ; potion transmute settings ; convert_rejuv: 1 = convert regular Rejuv Potions to Full Rejuv via cube (3 -> 1) convert_rejuv=1 diff --git a/src/transmute/transmute.py b/src/transmute/transmute.py index a1b26b9..41c4641 100644 --- a/src/transmute/transmute.py +++ b/src/transmute/transmute.py @@ -239,28 +239,62 @@ class Transmute: # ── New-UI (D2R named-tab stash) gem helpers ────────────────────────── - # Named-tab stash UI (1280x720). 5 tabs ~76px wide starting at x=33: - # PERSONAL≈71 SHARED≈147 GEMS≈223 MATERIALS≈299 RUNES≈375 - PERSONAL_TAB_X = 71 - GEMS_TAB_X = 223 - GEMS_TAB_Y = 100 + # Named-tab stash UI (1280x720), measured off the live client: + # labels occupy the row y=63..78, so TAB_Y must sit on that row. It was 100, which is + # ~20px BELOW the tab bar (the stash grid starts at y=87) — every "switch tab" click + # landed on a stash slot, the tab never changed, and the whole gem conversion ran in + # whichever tab happened to be open (PERSONAL). It failed silently for 800+ transmutes. + # PERSONAL 68 | SHARED 144 | GEMS 220 | MATERIALS 295 | RUNES 370 + TAB_Y = 70 + PERSONAL_TAB_X = 68 + SHARED_TAB_X = 144 + GEMS_TAB_X = 220 + MATERIALS_TAB_X = 295 + RUNES_TAB_X = 370 + # The active tab is drawn with a lighter cell background. Measure the BACKGROUND + # (30th percentile of the label cell), not the glyphs: text brightness varies with + # label length — "PERSONAL" peaks at 215 while an active "GEMS" only reaches 167, so + # a glyph-based threshold misreads short labels as inactive. + # Measured across both states: active bg ≈ 67-68, inactive bg ≈ 38-39. + TAB_ACTIVE_BG = 52 GEMS_CONVERT_BUTTON = (225, 500) GEMS_CONVERT_PANEL_ROI = (160, 296, 128, 160) GEMS_CONVERT_FIRST_SLOT = (181, 318) - def _switch_to_personal_tab(self) -> None: - x, y = convert_screen_to_monitor((self.PERSONAL_TAB_X, self.GEMS_TAB_Y)) - mouse.move(x, y) - self._wait() - mouse.click("left") - wait(0.4, 0.5) + def _tab_is_active(self, tab_x: int, img: np.ndarray = None) -> bool: + """True if the stash tab centred on tab_x is the selected one.""" + img = grab() if img is None else img + label = img[63:78, max(0, tab_x - 34):tab_x + 34] + if label.size == 0: + return False + gray = cv2.cvtColor(label, cv2.COLOR_BGR2GRAY) + return float(np.percentile(gray, 30)) > self.TAB_ACTIVE_BG - def _switch_to_gems_tab(self) -> None: - x, y = convert_screen_to_monitor((self.GEMS_TAB_X, self.GEMS_TAB_Y)) - mouse.move(x, y) - self._wait() - mouse.click("left") - wait(0.4, 0.5) + def _switch_to_tab(self, tab_x: int, name: str, attempts: int = 3) -> bool: + """Click a stash tab and confirm it actually became active. + + Verified rather than fire-and-forget: an unverified tab click is what let gem + conversion run in the wrong tab indefinitely without a single error line. + """ + for attempt in range(attempts): + if self._tab_is_active(tab_x): + return True + x, y = convert_screen_to_monitor((tab_x, self.TAB_Y)) + mouse.move(x, y) + self._wait() + mouse.click("left") + wait(0.4, 0.5) + if self._tab_is_active(tab_x): + return True + Logger.debug(f" {name} tab did not activate (attempt {attempt + 1}/{attempts})") + Logger.error(f"_switch_to_tab: could not activate the {name} stash tab") + return False + + def _switch_to_personal_tab(self) -> bool: + return self._switch_to_tab(self.PERSONAL_TAB_X, "PERSONAL") + + def _switch_to_gems_tab(self) -> bool: + return self._switch_to_tab(self.GEMS_TAB_X, "GEMS") def _click_gems_tab_convert_button(self) -> None: """Click the native convert button in the GEMS tab panel.""" @@ -866,8 +900,11 @@ class Transmute: Logger.error("convert_all_gems: stash not open") return - # Switch to GEMS tab before counting and planning. - self._switch_to_gems_tab() + # Switch to GEMS tab before counting and planning. Abort if it did not take — + # counting and converting in the wrong tab is worse than not running at all. + if not self._switch_to_gems_tab(): + Logger.error("convert_all_gems: GEMS tab not active — aborting instead of converting in the wrong tab") + return wait(0.4, 0.5) Logger.info("convert_all_gems: reading gem counts via OCR") @@ -910,7 +947,9 @@ class Transmute: if not self._reopen_stash(): Logger.error(" could not reopen stash -- aborting") return - self._switch_to_gems_tab() + if not self._switch_to_gems_tab(): + Logger.error(f" [{i+1}] GEMS tab not active -- aborting rather than clicking blind") + return wait(0.3, 0.4) if not self._ensure_gems_convert_panel_empty():