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:
co-authored by
Claude Opus 5
parent
98e12c0e00
commit
5df9ff4732
@@ -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
@@ -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
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user