fix(transmute): gem conversion ran in the PERSONAL tab — tab click missed the tab bar

convert_all_gems never reached the GEMS tab. It logged "expected result ... 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
y=63..78 and the stash GRID starts at y~87 — so every tab-switch click landed on a stash
slot, not a tab. The X constants were already correct; only Y was wrong, by ~30px.
Measured centres: PERSONAL 68 | SHARED 144 | GEMS 220 | MATERIALS 295 | RUNES 370, y=70.

Second, independent bug found alongside it: params.ini had stash_tabs=6 with only 5 tabs
on screen. tab_properties() divides the bar by that count, giving centres of
63/127/192/256/320/384 against real centres of 68/144/220/295/370 — tabs 2, 3 and 4 were
clicking the gaps between tabs. Set to 5.

Tab switches are no longer fire-and-forget: _switch_to_tab() confirms the tab actually
became active, retries up to 3x, and returns False; convert_all_gems now aborts rather
than converting in the wrong tab. Active-tab detection measures the cell BACKGROUND
(p30 > 52; active ~67, inactive ~38) rather than glyph brightness — text brightness
scales with label length, so an active "GEMS" peaks at 167 while "PERSONAL" hits 215 and
any glyph threshold misreads the short label as inactive.

Verified live: four consecutive PERSONAL<->GEMS switches, each confirmed, using the real
class constants and detector.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
alexpolo1
2026-08-26 18:26:55 +02:00
co-authored by Claude Opus 5
parent 98e12c0e00
commit 5df9ff4732
3 changed files with 108 additions and 22 deletions
+44
View File
@@ -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
+5 -2
View File
@@ -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
+59 -20
View File
@@ -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():