Standalone script: scans loot columns, tooltips every occupied slot to
identify chipped gems, then right-clicks each to convert in-place.
Uses the same screen/input_layer stack as the main bot (no D2R restart needed).
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
fg_market_scraper.py: broaden user.php href regex to allow a path prefix
before user.php (some topic pages use relative paths like ../user.php).
fg_scrape_pipeline.sh: --days 21 -> --days 60 for a fuller price history.
improve_fg_estimates.py: hoist min_valid_price constant out of the per-file
loop (was a constant being re-assigned every iteration).
fg_daily_estimates.json: refreshed from 853 topics over 60 days (was 21).
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- New 'count' scenario: read gem counts from GEMS tab via OCR, print plan
- gems_all: accept tier and gem-type filters + --max cap (e.g. 'gems_all flawless diamond')
- gems_all: fast-path ESC + stash-chest click before launching full TownManager
- gems_all: leave stash open after run so re-invocations skip navigation
- stash: stash chest auto-walk if stash not open
- Remove duplicate 'return 0' at end of _gems_all that made dead code
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
ocr.py: use -c tessedit_char_whitelist=0123456789 (tesseract 4+ style)
instead of the legacy blacklist+whitelist pair that was silently ignored.
a5.py: include left_inventory_ready() in the stash_is_open_func poll so
the open_stash success check fires even when GoldBtn templates miss at
current D2R rendering settings.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
New production flow for convert_all_gems_to_perfect:
- Count gem stacks via OCR badge on each GEMS-tab slot (Hermes as fallback)
- Open cube from PERSONAL stash per-transmute, return to GEMS tab while cube stays open
- ctrl+shift+right-click x3 loads gems directly into cube from GEMS tab
- ctrl+shift+left-click moves result from cube back to GEMS tab
- Pre-flight: clear any leftover items in cube before planning
- Stash reopen guard at every iteration in case ESC closed the stash
- _ensure_cube_available / _locate_cube / _ensure_cube_in_stash safety chain
- _empty_cube_to_gems_tab: blindly ctrl+shift+left-click all 12 slots to clear
Also adds tier_filter, gem_filter, max_transmutes params to convert_all_gems_to_perfect
and wires the new flow into run_transmutes when stash_tabs > 4.
Includes 8 pytest unit tests covering OCR plan, gem/tier filters,
cube-location logic and _open_available_cube_for_gems_tab edge cases.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Avoids blocking on town-marker detection failure when character is
in A5 but detect_current_act() can't find templates (e.g. stash was
just closed, unusual camera angle). Assumes A5_TOWN_START so open_stash
navigates to Harrogath stash without triggering Cain.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- convert_all_gems_to_perfect: revert to _open_cube_from_inventory() — cube lives
in character inventory, not stash PERSONAL tab
- After opening cube, switch to GEMS tab; ctrl+shift+click sends gems directly from
GEMS tab into the open cube without an inventory roundtrip
- testbed _gems_all: add left_inventory_ready() fallback so stash-open detection
works with new D2R UI (GoldBtnStash template doesn't match new UI)
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- _open_cube_from_stash(): switch to PERSONAL tab, right-click cube from stash
- _ctrl_shift_click_monitor(): ctrl+shift+click sends item from active stash tab
directly into the open cube (no inventory roundtrip)
- convert_all_gems_to_perfect: open cube once per gem type from stash, switch to
GEMS tab, ctrl+shift+click gem stack 3x -> into cube, transmute, ctrl+click
result -> back to GEMS tab; close cube when type is done
- Keeps _open_cube_from_inventory as fallback for other callers
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- _gems_tab_pull_3: click same stack position 3x (stacked slots = 1 match, not 3)
- convert_all_gems_to_perfect execute loop: same fix (was checking len(matches)<3)
- Replace inspect_inventory_area with search_all on right_inventory ROI in both
old _run_gem_transmutes_new_ui and new convert_all_gems_to_perfect - slot-by-slot
scan at 0.91 threshold missed gems that search_all finds at 0.80
- _plan_transmutes: handle both Hermes short keys (ruby_flawless) and template
fallback long keys (inventory_ruby_flawless) via dual-key lookup
- _count_gems_by_template: return 999 instead of slot count - stacked slots show
1 match per type regardless of how many gems are in the stack; execution loop
stops naturally when the stack empties
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The new D2R stash has named tabs (PERSONAL/SHARED/GEMS/MATERIALS/RUNES)
where gems are stacked (one slot per type/tier) instead of individual
inventory grid positions. The old slot-by-slot inspect_area approach
found 0 matches because gem icons don't align to the 38x38 grid.
Fix: _gems_tab_pull_3() uses template_finder.search_all() to find gem
positions anywhere in the left panel, then ctrl+clicks the 3 best hits.
Cube must now be in the character's right inventory (not personal stash).
Verified: 101 flawless->perfect transmutes in one run. Batch cap 100->500.
Also adds _switch_to_gems_tab() and _open_cube_from_inventory() helpers.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
transmute.py: convert_all_gems_to_perfect() chains all four gem tiers
(chipped->flawed->standard->flawless->perfect) by calling the existing
_run_gem_transmutes_lod for each tier in order. Result gems from each
pass land back in stash and feed the next pass.
testbed.py: gems_all scenario triggers convert_all_gems_to_perfect with
stash open; also adds missing return 0 to _gems().
hermes.py: fix UnicodeEncodeError on cp1252 terminals when the LLM
response contains non-ASCII characters (arrows, bullets, etc.).
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
thinking mode caused the model to loop on uncertain items (e.g. weapon
slot identification). disable_thinking=False + repetition_penalty=1.15
gives clean, non-repetitive responses in ~5s.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
tools/hermes.py grabs the current D2R window (same grab() path the bot
uses), sends it to the local Qwen vision model at 192.168.1.98:8010,
and runs an interactive REPL. The system prompt gives Hermes full context
on coordinate systems, template matching, input layer, and bot patterns
so it can reason about what's on screen and suggest feature code.
<botty-env-python> tools/hermes.py [initial prompt]
<botty-env-python> tools/testbed.py hermes [initial prompt]
Also wired into testbed.py as the 'hermes' scenario.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- scripts/stash_inventory.py: fix BaseItem["dimensions"] being a list
not a dict — use dims[0] for height so the claimed-slot dedup works
and multi-slot items (Large/Grand Charms, 2h weapons) are no longer
double-counted
- scripts/make_stash_csv.py: new — convert stash_inventory.json to a
deduplicated stash_list.csv (name, page, stats) for trade reference
- stash_list.csv: current stash export (71 unique items)
- config/params.ini: add stash_scan_interval (default 0/off)
- src/config.py: parse stash_scan_interval from [general]
- src/bot.py: after stash+transmute, trigger scan every N runs when
stash_scan_interval > 0
- src/inventory/personal.py: reset transfer_failures per stash tab so
each tab gets its own 2-failure budget before moving to the next
- tools/testbed.py: PASS/FAIL fix for `stash all` — equipped-area cols
(≥4) correctly left in inventory is a PASS not a FAIL (Bug 19 guard)
- CLAUDE.md: document stash scanner scripts in File Quick Reference
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
stash_all_items() treated any keep item remaining after transfer_items()
as "this tab is full", paged through every stash tab, and on the last page
called stash_full() -> taskkill D2R + a false Discord "stash full" alert.
A transfer can fail for non-fullness reasons (equipped-area click guard,
transient UI), so this could kill the game on a stash that has free slots.
Fix: before declaring a tab full, check EmptyStashSlot. If a slot is free
but the transfer still failed, count it as a transfer failure (cap 2) and
bail, leaving items in inventory -- never advance tabs / call stash_full()
on a non-full page. Also fix a leftover >3 page bound (-> >5) in the same loop.
Add a `stash` scenario to tools/testbed.py (self-bootstraps: main menu ->
create game -> walk -> open stash -> run stashing; `all` widens the scan to
all 10 columns to exercise the keep-item branch) and extract the conda-env
SSL cert workaround into a shared _patch_ssl() helper. Document as Bug 19.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- pickit: run-scoped loot accumulator (item names of every successful
pickup), consumed once per run.
- bot._run_wrapper: at run end (success, battle-fail, and approach-fail
paths) logs "Loot from run_diablo: JAH RUNE, 2x FLAWLESS SAPPHIRE" (or
"nothing picked up") at INFO level.
- game_stats: run_finished events now carry a counted loot dict, so
events_*.jsonl can be mined for per-boss drop value over time.
Co-Authored-By: Claude Fable 5 <[email protected]>
Session evidence (2026-06-11/12): 2 merc deaths vs Hell Diablo with exactly
1 heal fired and 0 rejuvs. The pipeline works (read 30% -> fed a potion)
but the tuning loses: Hell bosses chunk a merc 30-50% per hit, so the
25% rejuv band was skipped straight past between polls, leaving only the
slow 10.24s-cooldown health pot branch. Rejuv (instant, 4s cooldown) now
covers the real danger band under 45%; pots top up from 70%.
Co-Authored-By: Claude Fable 5 <[email protected]>
The most valuable drop must never be lost to pickup order or a chicken:
- pickit: ground items now sort high-value-first (HIGH_VALUE_KEYWORDS:
Ist+ runes, Tyrael's, DWeb, Griffon's, SoJ, facets, HC, etc.), nearest
of them first, junk after - if looting gets interrupted, the Jah is
already in the bag.
- dive mode (health_manager.set_loot_priority): while a high-value drop
is being picked up, the chicken threshold is halved (hard floor 20%
HP), the two-juv panic and merc chicken are suppressed, and potions
keep flowing - the bot spends its belt to secure the item instead of
save+exiting away from it. Auto-expires after 15s so a stuck flag can
never disable safety permanently; engaged/cleared around each
high-value pickup attempt.
Unit-tested (Jah-first ordering incl. over nearer gold/potions, flag
expiry) and validated in a live profile-enabled Pindle run.
Co-Authored-By: Claude Fable 5 <[email protected]>
Per-user character profiles (gitignored, survive git pulls):
- config/profiles/<name>/profile.ini overrides any params.ini key
(runs, difficulty, char type, keybinds...). Priority:
custom.ini > profile.ini > params.ini. Injected into _select_val and
all 22 build-section merges.
- Active profile selected via config/active_profile.txt (gitignored);
Config.get/set_active_profile + list_profiles/list_pickit_profiles.
Shared pickit profiles (git-tracked team content):
- config/pickit_profiles/<set>/*.bnip - one folder per season phase,
built once, shared via git. Selected per user with
[general] pickit_profile=<set> in their profile.
- Pickit dir priority: config/profiles/<me>/pickit/ (personal)
> config/pickit_profiles/<set>/ > config/bnip/ > default.bnip.
Menu integration (main.py):
- startup banner shows active profile + pickit set + available lists
- "end" hotkey cycles character profiles (applies on restart)
Verified: fistman profile active end-to-end (general/char/build-section
overrides + shared pickit set resolution + 474 expressions loaded).
Co-Authored-By: Claude Fable 5 <[email protected]>
Mirrors the D2R settings check: on the first on_maintenance of a bot
session, validate_build_skill_icons presses each configured skill hotkey
and verifies the icon on the right slot, warning loudly (with a pointer to
tools/set_binds_from_params.py) when a bind does not match params.ini.
Non-blocking; ~5s once per session.
Co-Authored-By: Claude Fable 5 <[email protected]>
Live finding (fresh game, 2026-06-12): D2R skill hotkeys select onto the
RIGHT skill slot, not the left as the old comments assumed. The left slot
permanently holds Blessed Hammer. Consequence: _cast_hammers pressing the
hammer hotkey after activating an aura was REPLACING the aura on the right
slot every cast cycle - the true root cause of fights running without
Concentration. _cast_hammers no longer touches the hammer hotkey: select
aura (lands on right, stays active), hold stand-still, spam left-click.
Verified live: full Diablo kill at 09:04, ~55s from last seal to kill.
Auto skill binding proven end-to-end (tools/set_binds_from_params.py):
blessed_hammer/concentration/redemption/vigor/holy_shield/teleport all
bound via the in-game picker and visually verified (6/7 OK; conviction
correctly reported missing - not skilled on this char).
- capture tool + preflight verify now watch the RIGHT slot
- fresh skill slot templates + clean PICKER_* cell templates captured at
current settings (blessed_hammer, concentration, redemption, vigor,
holy_shield, teleport)
- removed bogus conviction.png (had captured vigor's icon)
Co-Authored-By: Claude Fable 5 <[email protected]>
transfer_items now filters protected items out of BOTH the sell and drop
paths (previously only shields, only on sell): anything whose name contains
"charm" is blocked by default (charms live in the inventory permanently -
a pickit misread must not vendor them), shields stay protected, and
never_sell_keywords in [char] params can extend the list. Blocked items
stay untouched in the inventory.
Motivated by a real grand-charm-sold-by-mistake incident.
Co-Authored-By: Claude Fable 5 <[email protected]>
Layer 1 (Controls page / .keyo): tools/set_controls_keyo.py parses the
character keyo (same binary format as utils/key_detector) and verifies every
skill hotkey in params.ini is bound to a skill slot; --fix writes missing keys
into free skill slots with a backup (D2R must be closed). Picks the configured
character file. Verified: fistman controls match params.
Layer 2 (skill assignment / picker): enabled the existing skill_hotkey_setter
machinery for the hammerdin build:
- skill_preflight: hammerdin build rules (blessed_hammer left/required,
concentration right/required, redemption/vigor/conviction/holy_shield/
teleport right/optional) + PALADIN_TEMPLATE_ALIASES, merged TEMPLATE_ALIASES.
- skill_hotkey_setter: paladin picker template aliases with slot-icon fallback.
- tools/set_binds_from_params.py: end-to-end runner - opens the in-game picker,
binds each skill to its params hotkey, verifies via slot icon, reports
match/mismatch per skill.
- tools/capture_skill_hotkeys.py now saves crops straight into
assets/templates/ui/skills/ so captures immediately become live templates
for both the preflight and the picker search.
Co-Authored-By: Claude Fable 5 <[email protected]>
- hammerdin.py _cast_hammers: skip aura/hammer re-selection when the slot
already holds the wanted skill (tracked via _active_skill, invalidated by
pre_move/_weapon_switch/cast_buffs and all raw redemption/vigor hotkey
sends). Visible effect: aura stays on right-click and hammers fly from
plain left-click spam instead of constant F1/F3/F8 churn.
- 35 mid-clear attack casts switched from Redemption to Concentration
(damage aura while fighting); Redemption still pulses between packs and
after kills for corpse cleanup.
- DEPENDENCIES.md: full verified working-state snapshot (env paths, package
versions, tesseract wiring, D2R settings, DPI specifics, safety nets).
- tools/capture_skill_hotkeys.py: presses each params.ini skill hotkey
in-game and saves labeled left-slot icon crops - verifies binds match the
character and builds skill-icon templates for future aura checks.
- CLAUDE.md: remove stale main.exe mention (no exe build exists; bat runs
current source directly).
Co-Authored-By: Claude Fable 5 <[email protected]>
Session fixes (2026-06-11), all live-tested over multiple farm games:
- input_layer/hotkey.py: no-arg keyboard.wait() returned on ANY keypress,
silently killing the process right after F11 (all bot threads are
daemons). Now blocks forever like the original keyboard lib. Poll loop
is edge-triggered (no ~20ms refire while a hotkey is held) and callback
exceptions print instead of being swallowed. (Bug 13)
- d2r_image/ocr.py: pytesseract never read PYTESSERACT_TESSERACT_CMD;
tesseract_cmd is now wired from env var / PATH / winget default, fixing
exit 0xC0000135 on every OCR call. (Bug 14)
- bnip/utils.py + config/default.bnip: undefined NipSyntaxError ->
BNipSyntaxError, and Shaefershammer -> Schaefershammer typo; 474 pickit
expressions load (was 473 + parser error). (Bug 15)
- town_manager/a5/bot: A5 WP death-loop containment - per-game WP failure
budget (2 strikes), quick=True direct-path-only retries, sweep trimmed
10->6 steps with 4s select timeouts, A5 select thresholds lowered
(WP 0.62, stash 0.60/0.45; safe - every select is success_func-gated).
Worst case dropped from 10+ min wandering to ~4.5 min contained fail
with fresh-game recovery. (Bug 16)
- bot.py: vendor trip gating - the failure-prone A5->A4 Jamella round
trip now only runs when consumables are needed or 3+ sell items pend.
- town_manager.py: Cain identify skips acts whose Cain timed out this
session (straight to working A5 fallback).
- char/i_char.py: Battle Command buff check waits 0.6-0.8s after the
hotkey (icon fade-in) to reduce double weapon-swaps.
- char/paladin/hammerdin.py: kill_diablo fights with Concentration
instead of Conviction (useless for magic-damage hammers) and drops
mid-fight Redemption downtime - faster kills, merc survives. (Bug 17)
- inventory: stash supports all 6 pages (personal + 5 shared, D2R 2.7+);
gold deposits navigate via OCR-verified select_stash_page instead of
raw 4-tab clicks; rotation %6, shared-first starts at page 5. (Bug 18)
Docs: CLAUDE.md Bugs 13-18; .hermes/plans/dia_run_test_state.md has the
full test log (two complete Diablo kills verified end-to-end today).
Co-Authored-By: Claude Fable 5 <[email protected]>
OpenSSL 3.x + corrupted Windows cert store causes aiohttp to crash
at import time. Monkey-patch ssl.SSLContext.load_default_certs to
fall back to certifi when Windows store fails.
traverse_nodes previously gave up after one random-guess move when a node
template wasn't on screen, failing every travel path where the char was
mis-positioned (the A5-after-Pindle stranding was the worst case). Now,
on timeout during travel pathing (timeout > 3.1s, so boss approaches keep
their fast fail), it walks an expanding 12-step sweep, re-scanning for the
node each step, and only fails once the sweep is exhausted. Makes ALL
node pathing self-healing, not just A5.
Co-Authored-By: Claude Fable 5 <[email protected]>
The A5 waypoint was unopenable after a Pindle return: the char lands in
the NE near Anya, where the town_start->WP node templates are off-screen,
so the pather random-guessed and timed out. Pathing to NPC spots
(Qual-Kehk/Malah) DOES work from there, and each has a defined path to
the WP. New open_wp: direct -> re-anchor via Qual-Kehk/town_start/Malah
-> directed sweep that re-attempts node pathing + full-screen WP scan
each step. This is the linchpin: it unblocks the A5->A4 vendor redirect,
maintenance, stash, and the Diablo run's travel out of A5.
Co-Authored-By: Claude Fable 5 <[email protected]>
CRITICAL loot-loss bug: buy_consumables failure called end_game and
returned BEFORE the stash step, so Pindle runes/items piled in inventory
and were never banked (zero stash events across a whole session). Buying
pots is optional (belt refills from drops); stashing loot is the point.
Now warn + re-anchor + fall through to stash. Also raise
max_maintenance_time_s 120->240 so the A5 vendor thrash completes and
reaches stash instead of timing out first (transitional — disappears once
a Diablo run shifts spawns to A4).
Co-Authored-By: Claude Fable 5 <[email protected]>
A5-spawn games burn on the Malah vendor step (stale patch templates);
A4-spawn games sail through Jamella. The Diablo run ends with a TP to
A4 town, so running it LAST makes every next game spawn in A4.
Co-Authored-By: Claude Fable 5 <[email protected]>
The blind ESC after a failed sweep click opens the ESC game menu when no
dialogue was actually open (seen covering the screen during the Tyrael
hunt). Detect SaveAndExit after the ESC and close the menu again.
Co-Authored-By: Claude Fable 5 <[email protected]>
Diablo died in 22s but picked_up_items was false: his death animation /
lingering effects register as visible targets, so the mobs-alive guard
skipped his drops. pick_up_items(force=True) at the Diablo loot site.
Co-Authored-By: Claude Fable 5 <[email protected]>
The sweep for Tyrael clicked Deckard Cain (stands in the same zone) and
left his TALK dialogue open, blinding the rest of the sweep. Also sweep
the full search area as a second pass since NPCs can stand outside the
stored ROI, with a total time budget.
Co-Authored-By: Claude Fable 5 <[email protected]>
- OCR reads FISTMAN as "fabiman" — SequenceMatcher >= 0.6 on the first
word of the row handles the mangling
- start_bot_detached.bat kills existing main.py instances first: F11/F12
are global hotkeys, so duplicate bots receive every press and fight
each other (one starts while the other pauses/exits)
Co-Authored-By: Claude Fable 5 <[email protected]>
Body templates went stale with the June 2026 patch (Malah, Cain,
Tyrael, Larzuk, Qual-Kehk all undetectable), but name tags on hover
score 0.9+ reliably. When the body-template hunt times out, sweep a
coarse hover grid over the NPC ROI and click wherever the name tag
appears.
Co-Authored-By: Claude Fable 5 <[email protected]>
User-tuned detection thresholds and a safety guard against teleporting
into live packs during loot phases.
Co-Authored-By: Claude Fable 5 <[email protected]>
A misclick on the stash chest (Cain stands beside it) opens the stash
UI which covers most of the screen and blinds every template search:
NPC hunts, A5_WP selection, act detection. The old guard pressed the
inventory key, which leaves a stash panel open — ESC closes both.
Panels are now closed at hunt start, per hover-loop iteration, and
before WP traversal.
Co-Authored-By: Claude Fable 5 <[email protected]>