Compare commits

..
411 Commits
Author SHA1 Message Date
alexpolo1andClaude Sonnet 4.6 aa78bab638 ci: harden coverage omit so config-3.py phantom never trips xml
Botty - CI / test (push) Canceled after 0s
Botty - CI / build (push) Canceled after 0s
The bare "config-3.py" omit never matched the phantom's absolute path
(D:\a\...\config-3.py), so coverage xml only survived via --ignore-errors
and still logged the alarming "No source for code" line. Use a glob
(*config-*.py) that matches the phantom at any path while keeping
src/config.py measured (verified via coverage GlobMatcher).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-20 11:30:16 +02:00
alexpolo1andClaude Sonnet 4.6 3d11947bf2 ocr: bundle Tesseract into release for click-and-run OCR
Make the standalone exe work with OCR out of the box — no separate
Tesseract install, no tesserocr DLL hell. Verified end-to-end: built the
exe, ran it frozen with the system Tesseract blinded, confirmed it
resolves the bundled binary and reads text ("CHAM RUNE").

- ocr.py: resolve an _APP_BASE (exe dir when frozen, else cwd) and prefer
  a bundled <exe_dir>/tesseract/tesseract.exe over PATH / Program Files.
  Resolve assets/tessdata to an absolute path so OCR no longer depends on
  the current working dir. Applies to both the tesserocr and pytesseract
  paths.
- build.py: copy a portable Tesseract (exe + DLLs) from TESSERACT_DIR
  (default C:\Program Files\Tesseract-OCR) into <release>/tesseract/. Our
  trained models in assets/tessdata are used via --tessdata-dir, so their
  tessdata is skipped. Warns (non-fatal) if Tesseract isn't present.
- ci.yml: choco install tesseract before the build so the bundle is
  reproducible on the runner; verify it landed in the release dir.
- test/conftest.py: apply the SSL cert-store workaround so pytest can be
  collected on Windows boxes with a corrupted cert store (no-op on CI).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-20 10:49:25 +02:00
alexpolo1andClaude Sonnet 4.6 f31c30f388 security: untrack cached d2jsp scrapes, parametrize cookie helper
- git rm --cached data/d2jsp_pages (102 files, already gitignored) — saved
  authenticated HTML embedded the live d2jsp msec session token
- apply_manual_cookies.py: read member_id/msec from env vars instead of
  hardcoding the real session token

History purge of these blobs follows in the same cleanup.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-20 10:33:50 +02:00
alexpolo1andClaude Sonnet 4.6 b9f6635f8f security: stop tracking cookies.txt, ignore secrets, drop personal paths
Pre-public cleanup:
- Remove cookies.txt from tracking (held live d2jsp session cookies
  member_id + msec) and delete the local copy
- .gitignore: cookies.txt, cookies_temp*, *.cookies, config/custom.ini
- fg_scrape_pipeline.sh: replace hardcoded /c/Users/alex/Downloads path
  and /c/Python313/python with a script-relative cd and $PYTHON from PATH

NOTE: cookies.txt still exists in git history (commit e3d6605) — a
history purge + cookie rotation is still required before going public.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-20 10:28:47 +02:00
alexpolo1andClaude Sonnet 4.6 73c4601e2b ci: tag-triggered build-and-release in one run
Switch from the upload-to-an-existing-release model (create release in
UI/CLI first, release:published triggers CI) to a tag-driven flow:

  git tag v0.8.5 && git push --tags
  -> CI builds + smoke-tests both exes
  -> softprops/action-gh-release creates the release and attaches the zip

Build fails => no release is ever created (no more empty/half-published
releases). Removes the release: trigger so the workflow can't double-fire
when the action publishes the release. Keeps permissions: contents: write.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-20 10:25:17 +02:00
alexpolo1andClaude Sonnet 4.6 5647f323a7 ci: grant contents:write so release upload step can attach the zip
The build job's "Upload to Release" step failed with HTTP 403
"Resource not accessible by integration" because the default
GITHUB_TOKEN is read-only. Add top-level permissions: contents: write.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-20 10:12:44 +02:00
alexpolo1andClaude Sonnet 4.6 3db44fe4e6 build: bundle conda native DLLs + fix shopper SSL crash
Verified by building locally and running both exes to their menus.

build.py: prepend the conda env's Library\bin, Library\lib and DLLs
dirs to PATH before invoking PyInstaller. PyInstaller resolves binary
deps via PATH (not --paths, which only affects Python imports), so
without this the frozen exe crashed at startup with
"DLL load failed while importing _ctypes" (missing ffi-8.dll, plus
liblzma/libbz2). Now ffi-8/lzma/bz2/leptonica/tesseract52 all bundle.

src/shopper.py: mirror main.py's startup header — add the
ssl.load_default_certs monkey-patch (corrupted Windows cert store made
aiohttp crash at import with ASN1 NOT_ENOUGH_DATA) and drop Library\bin
from os.add_dll_directory (it ships mismatched OpenSSL DLLs that break
_ssl). shopper.exe previously crashed on any machine with a bad cert
store; CI's clean runner masked it.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-20 09:47:41 +02:00
alexpolo1andClaude Sonnet 4.6 57073946ae ci: split test job into named steps, fix coverage xml phantom-file error
- Split monolithic 'Validate Botty' into 4 named steps so failures are
  visible per-step without log diving: Python version / Syntax check /
  Tests / Coverage report
- Add scripts/ to compileall so new scripts are syntax-checked too
- Add -v to pytest for per-test pass/fail in CI output
- Fix coverage xml exiting 1 on conda phantom config-3.py:
  - Move ignore_errors to [report] section (was wrongly in [xml])
  - Add --ignore-errors flag on coverage xml command (belt+suspenders)
  - Omit config-3.py and site-packages paths from [run] tracking

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-20 09:34:11 +02:00
Alex 5c687773b1 Click-and-go install: self-bootstrapping install.bat + tesserocr DLL fix (#2)
Merges fix/stash-full-guard-and-testbed into main. See PR #2 for full description.
2026-06-20 09:27:12 +02:00
alexpolo1 6a8f1ed5d9 add fiskenersej character profile + pickit profile cycler (F10) 2026-06-12 12:08:21 +02:00
alexpolo1andClaude Fable 5 ebe2e354dc Log per-run loot summary at every run end for value judgment
- 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]>
2026-06-12 10:35:43 +02:00
alexpolo1andClaude Fable 5 868d117530 Merc healing: rejuv at 45% (was 25%), health pot at 70% (was 60%)
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]>
2026-06-12 10:31:19 +02:00
alexpolo1andClaude Fable 5 9244570e22 Pickup priority + dive mode: secure Jah-class drops above all else
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]>
2026-06-12 10:27:39 +02:00
alexpolo1andClaude Fable 5 b06c77d2e4 Profile system: per-user char profiles + shared pickit profiles
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]>
2026-06-12 10:16:12 +02:00
alexpolo1andClaude Fable 5 0834831220 Run skill bind preflight once per session at first town maintenance
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]>
2026-06-12 09:14:52 +02:00
alexpolo1andClaude Fable 5 40e4dd8a4f Skill hotkeys select the RIGHT slot - fix cast flow, prove auto-binding live
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]>
2026-06-12 09:13:52 +02:00
alexpolo1andClaude Fable 5 1b4773b42e Never sell or drop charms (and other protected items)
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]>
2026-06-12 08:55:16 +02:00
alexpolo1andClaude Fable 5 77b35df26b Two-layer keybind verification/setting driven by params.ini
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]>
2026-06-12 08:30:43 +02:00
alexpolo1andClaude Fable 5 63248a9437 Hammerdin: hold Concentration through trash clears; add deps doc + keybind tool
- 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]>
2026-06-12 00:18:23 +02:00
alexpolo1andClaude Fable 5 2862591492 Fix launcher exit, OCR backend, A5 WP loops, Diablo aura, stash paging
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]>
2026-06-11 22:05:53 +02:00
alexpolo1 ddb7008419 fix: reinstall OpenSSL, rename broken conda tesseract, use winget tesseract 5.5.0\n- Reinstalled OpenSSL (was corrupted in conda cache)\n- Renamed conda tesseract.exe to tesseract_broken.exe (access violation)\n- install.bat: winget tesseract-ocr.tesseract + rename conda binary\n- run_botty.bat: PYTESSERACT_TESSERACT_CMD points to winget version 2026-06-11 15:33:04 +02:00
alexpolo1 43c77deede fix: use winget tesseract 5.5.0, rename broken conda tesseract\n- winget install tesseract-ocr.tesseract\n- PYTESSERACT_TESSERACT_CMD points to winget version\n- conda tesseract renamed to tesseract_broken.exe 2026-06-11 15:17:27 +02:00
alexpolo1 b77347542d fix: patch pytesseract to handle tesseract --version crashes 2026-06-11 15:04:13 +02:00
alexpolo1 1b7721779c fix: remove tesserocr/tesseract from env yml; install.bat handles it\n- conda-forge tesserocr requires Python 3.11+, we use 3.10\n- conda tesseract crashes with access violation on this system\n- install.bat: tesseract=4.* + DLL renames + bundled wheel\n- run_botty.bat: SSL_CERT_DIR= + full PATH 2026-06-11 14:53:33 +02:00
alexpolo1 6822a7e495 fix: install.bat copies liblept.dll from conda leptonica 2026-06-11 14:51:23 +02:00
alexpolo1 66a71b4ad3 fix: add Scripts + DLLs to PATH in run_botty.bat for pytesseract/tesserocr 2026-06-11 14:34:50 +02:00
alexpolo1 b8479c2300 fix: broaden pytesseract import exception catch 2026-06-11 14:34:29 +02:00
alexpolo1 2bfcf0ff02 fix: patch ssl.load_default_certs to handle corrupted Windows cert store
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.
2026-06-11 13:53:54 +02:00
alexpolo1 c80e002ab9 fix: SSL cert store corruption + install.bat fixes\n- run_botty.bat: set SSL_CERT_DIR= to bypass corrupted Windows cert store\n- install.bat: use tesseract 4.x, bundled wheel, DLL copy, PATH fix 2026-06-11 13:45:32 +02:00
alexpolo1 1fe95aae08 fix: install.bat - use tesseract 4.x + bundled wheel, add DLL copy + PATH fix 2026-06-11 13:43:11 +02:00
alexandClaude Fable 5 f1c5b6cdf4 feat: pather auto-recovery sweep when a node is lost
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]>
2026-06-10 20:51:20 +02:00
alexandClaude Fable 5 19011c1a12 fix: robust A5 open_wp — re-anchor via NPC landmarks + directed sweep
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]>
2026-06-10 20:49:59 +02:00
alexandClaude Fable 5 78bc195bd9 fix: vendor failure non-fatal so maintenance always reaches stash
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]>
2026-06-10 20:28:23 +02:00
alexandClaude Fable 5 2784b50675 config: fixed route order [pindle, diablo] so games end in A4 town
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]>
2026-06-10 20:09:48 +02:00
alexandClaude Fable 5 4e7c517764 fix: grid sweep ESC could open the game menu and blind the rest of the hunt
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]>
2026-06-10 19:55:18 +02:00
alexandClaude Fable 5 3093e04517 fix: force boss loot past the mobs-alive pickit guard
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]>
2026-06-10 19:48:44 +02:00
alexandClaude Fable 5 b1bec0cecc fix: grid sweep waits 5s for walk-to-NPC before declaring click failed
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 19:36:18 +02:00
alexandClaude Fable 5 54e63197c2 fix: spawn fallback uses act detection then A5, not blind A1 default
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 19:26:47 +02:00
alexandClaude Fable 5 3372b91ed7 fix: CS entrance loop budget 10s -> 20s (most common Diablo-run abort)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 19:21:07 +02:00
alexandClaude Fable 5 9d0ae9322a fix: grid sweep ESC after wrong-NPC click, wide second pass, 25s budget
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]>
2026-06-10 18:49:13 +02:00
alexandClaude Fable 5 06c73e7cbf fix: fuzzy char-name OCR match; single-instance guard in detached launcher
- 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]>
2026-06-10 18:28:12 +02:00
alexandClaude Fable 5 f410ea21f1 feat: grid hover sweep fallback when NPC body templates fail
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]>
2026-06-10 18:00:44 +02:00
alexandClaude Fable 5 d9a6e7ce40 feat: per-NPC body/pose thresholds (Malah 0.35/200); skip pickit while mobs alive
User-tuned detection thresholds and a safety guard against teleporting
into live packs during loot phases.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 17:58:31 +02:00
alexandClaude Fable 5 c92b7730ee fix: close open stash/inventory panels before NPC hunts and WP searches
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]>
2026-06-10 14:37:34 +02:00
alexandClaude Fable 5 a7b12bc8a3 fix: buy consumables at A4 Jamella when in A5 (Malah unreliable in current patch)
Malah wanders and her body templates predate the patch; the hunt fails
most games and the failed-hunt position strands the char. Jamella is
static and detects at 0.99+. Travel cost ~30s vs a lost game.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 14:24:49 +02:00
alexandClaude Fable 5 0b8d67b4e4 config: max_game_length_s 600 -> 900
A full CS clear takes ~10 min; the 600s watchdog force-quit a game that
had all three seals open and was waiting for Diablo to spawn.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 14:15:46 +02:00
alexandClaude Fable 5 3479a74503 fix: A5 open_wp walk-and-scan last resort for stranded positions
After a failed NPC hunt the char can be in a corner where no path nodes
anchor; both node traverses fail and the game dies. Walk a search
pattern across the small town scanning the whole screen for the WP
template directly each step.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 14:00:48 +02:00
alexandClaude Fable 5 5727a81cb8 fix: A2-Y boss seal 36B variant template; sanitize seal screenshot filename
- Seal 36 has two visual variants; dia_a2y4_36b_closed.png existed in
  assets but was never searched, so the B variant could never be clicked
  (8 sealdance tries then aborted the run)
- info_failed_seal_ screenshot name contained ": " (illegal on Windows)
  producing 0-byte files

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:53:17 +02:00
alexandClaude Fable 5 95bd5a76ec fix: post-loot seal calibration hops are non-fatal
After Vizier/De Seis are dead, a failed recalibration traverse aborted
the whole Diablo run (threw away two boss kills at 13:43). The seal
stage re-anchors at the pentagram via template loop immediately after,
so just skip the extra loot pass and continue.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:45:51 +02:00
alexandClaude Fable 5 a05a13be67 fix: on_init save+exit recovery when stranded mid-town with no visible marker
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:34:43 +02:00
alexandClaude Fable 5 a06e46df09 fix: track physical act across failed cross-act NPC trips; prefer A4 Jamella retry
- town_manager.identify: return the reached A5 location when Cain fails
  after successful travel (returning False made bot.py fall back to the
  pre-travel act and run wrong-act pathing all maintenance long)
- town_manager.resurrect: leave last_known_loc breadcrumb when the A4
  revive fails after travel; bot.py consumes it to re-anchor
- bot.py: alternate vendor retry prefers A4 Jamella (static, reliable)
  over A1; Malah wanders and is the main A5 vendor failure

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:31:32 +02:00
alexandClaude Fable 5 8a58b44866 fix: substring match in OCR char-name row scan (rows include level/class text)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:26:22 +02:00
alexandClaude Fable 5 d99e3461fe fix: seal layout-check retry via pentagram; never skip hammers on unbound aura
- diablo.py _layoutcheck: ambiguous/lost layout check now loops back to
  the pentagram and retries the approach once before aborting the run;
  template checks bumped from single-frame 0.1s to 0.5s
- hammerdin.py _cast_hammers: an unbound aura (e.g. conviction in
  kill_diablo on a non-Infinity build) silently cast NOTHING, so Diablo
  was never attacked with the damage aura. Fall back to concentration
  and always cast hammers.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:24:50 +02:00
alexandClaude Fable 5 a695252614 fix: select configured char_name before saving char template
First encounter at char select blindly saved whichever character was
highlighted, locking in a wrong character for the whole session. Now,
when char_name is configured, select it via OCR first and warn if the
saved template name does not match.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:09:26 +02:00
alexandClaude Fable 5 7ef62161ca fix: spawn-act tracking, alternate-act vendor retry, broader CS entrance templates
- bot.py: track _spawn_act at game start as act-of-record when mid-town
  marker detection fails; _verify_town_location(assumed) fallback chain
  assumed_act -> spawn_act -> A5; pass previous location at every
  retry/fallback site instead of blind defaults
- bot.py: buy_consumables alternate-act retry ladder (A5 Malah unless
  already in A5, then A1 Akara) gated on confirmed go_to_act travel
- diablo.py/vizier.py: widen CS entrance template set (DIA_CS_ENTRANCE_*
  node-603 variants) and lower threshold 0.8 -> 0.75
- start_bot_detached.bat: detached launcher with per-launch console log

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 13:06:15 +02:00
alexandClaude Fable 5 6301490cc4 fix: act-state verification, NPC dialogue confirm wait, stats integrity
Root causes from the 2026-06-09 session (51 games, 45 failed):

- Act desync: add TownManager.detect_current_act() and
  Bot._verify_town_location(); every maintenance/end-run retry and
  fallback now verifies the physical act instead of hardcoding town
  starts. open_wp/go_to_act self-heal act mismatches. Never run A1
  pathing when travel to A1 failed.
- NPC dialogue: poll action buttons up to 2.5s after click instead of
  a single-frame check (premature retry click was closing the dialog).
- Stats integrity: log_end_game skips duplicate calls (phantom 0s
  "successful" games were resetting the consecutive-fail breaker);
  clear stale failure reason at game start; set chicken flag before
  bot.stop() so chickens are no longer labeled "Bot stopped".
- Repair: prefer in-act Larzuk over cross-act Halbu trip (Halbu
  detection failed 100% last session and desynced the act state).

Documented as Bugs 9-12 in CLAUDE.md.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 08:23:10 +02:00
alexandClaude Sonnet 4.6 83951db876 fix: resolve NPC name tag template mismatch causing open_npc_menu timeouts
NPC body templates find Akara/Malah correctly but the name tag templates
score ~0.28, below the 0.35 threshold, causing 20s spinning when the NPC
is detected but the hover confirmation never passes.

Three targeted changes in open_npc_menu():
- Use a 240x140px ROI directly above the hover cursor for name tag search
  instead of the full screen, containing false-positive risk
- Lower name tag threshold from 0.35 to 0.26 so Akara at ~0.28 now passes
- Check ScreenObjects.NPCDialogue after click as primary confirmation;
  this is UI-state based and immune to stale template images
- Reduce per-NPC search timeout from 20s to 8s for faster failure recovery

Also lower max_maintenance_time_s from 120 to 60 to cap total stuck time.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-08 19:09:11 +02:00
alexandClaude Sonnet 4.6 793d7ed967 feat: resilient town loop, step tracking, per-run disable, maintenance timeout
Reliability
- All run approach() methods now set approach_fail_step before every return False;
  bot.py _run_wrapper reads it and includes [step: X] in Discord/log failure reasons
- on_maintenance() sets _maintenance_step before each town step (heal, identify,
  buy_consumables, stash_items, repair, resurrect_merc, gamble) for the same coverage
- on_init() logs startup line: char=X | difficulty=Y | routes=[...] each game

Per-run disable (bot runs as long as possible)
- GameStats tracks per-run failure counts across Bot instances (previously reset each game)
- After disable_run_after_failures consecutive failures a run is disabled for the session;
  game-level consecutive-fail counter resets so the bot continues on remaining routes
- game_controller no longer quits on max_consecutive_fails if active routes remain;
  only exits when all routes are disabled

Maintenance timeout (params.ini: max_maintenance_time_s=120)
- Hard 120 s wall on the entire town maintenance loop; checked between each major step
  and before every retry (buy_consumables, stash, repair, resurrect_merc, gamble)
- On timeout: error screenshot + Discord, then trigger end_game → save-and-exit → rejoin

Bug fixes
- Win11 mouse overshoot: mouse_move() uses SetCursorPos + zero-delta MOUSEEVENTF_MOVE
- _curr_loc = True propagation: TownManager.identify() now returns the act Location enum
- DAMAGED KeyError in pickit: added ItemQualityKeyword.Damaged.value to NTIP_ALIAS_QUALITY_MAP
- A4 WP interaction range: force-move character to WP stone before select_by_template
- NPC click blocked by equipped-area guard: open_npc_menu() closes inventory if open

Docs
- Added CLAUDE.md: AI working guide with step tables, bug history, debugging tips

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-08 18:44:45 +02:00
alex 5fbac05e4e Stabilize Diablo verification and potion handling 2026-06-07 20:17:04 +02:00
alex 594d633787 Fix D2R window sizing and input targeting 2026-06-07 19:00:15 +02:00
alex 9f45539210 feat: capture missing screen verification evidence 2026-06-07 18:42:43 +02:00
alex 83539ed9c3 fix: require stable screen verification 2026-06-07 18:37:58 +02:00
alex f1d6c6bd1c ci: align validation environment with Windows 11 2026-06-07 18:31:10 +02:00
alex 86e3cccbc9 feat: detect Windows profile for install and input 2026-06-07 18:26:16 +02:00
alex d0ecf3afdc ci: run full Botty validation 2026-06-07 18:19:24 +02:00
alex 8ccdca7946 fix: restore validation suite 2026-06-07 18:17:44 +02:00
alex 7bd9d01023 docs: note Diablo template logging helper 2026-06-07 17:53:37 +02:00
alex 45fa775a2e fix: verify Diablo waypoint recovery 2026-06-07 17:52:55 +02:00
alexandClaude Sonnet 4.6 b781dd56b1 fix: save-and-exit on looping NPC click failure instead of continuing
open_npc_menu timeout reduced 35s → 20s and now saves a screenshot when
it gives up. buy_consumables and stash failures after their retry now
trigger end_game (save & exit) instead of silently continuing with a bad
location, preventing the bot from running the next game in a broken state.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-07 16:17:54 +02:00
alexandClaude Sonnet 4.6 fda747a1bf feat: restart bot process instead of killing D2R when stuck
When restart_d2r_when_stuck is enabled, spawn a fresh Python process
(same main.py) and exit immediately rather than killing and relaunching
D2R. The new bot detects D2R is already running and skips launching it,
preserving the game session. D2R is only killed on deliberate exits
(safe_exit) and the initial auto_login launch.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-07 16:13:18 +02:00
alexandClaude Sonnet 4.6 5254b7ee75 fix: identify() returned boolean True instead of Location, breaking post-Cain town routing
After using Cain to identify items, self._curr_loc was set to True (boolean)
instead of the actual location. Every subsequent get_act_from_location(True)
returned None, causing buy_consumables and stash to bail immediately and
fall back to A1 even when the character was physically in A5.

Now returns curr_loc when identification succeeds in the current act, or
new_loc (the A5 WP location) when the A5 Cain fallback is used — so the
bot correctly routes to Malah and the A5 stash instead of navigating back
to Akara.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-07 16:13:11 +02:00
alexandClaude Opus 4.8 f705091991 Send error message + screenshot to Discord on run failures
Adds Messenger.send_error across the Discord (red embed with screenshot attached)
and generic (text-only) APIs. Bot._save_error_screenshot now also pushes the
failure to the configured messenger after saving the screenshot to disk, so each
approach/battle/exception failure is reviewable in Discord with the visual.

Gated by new config discord_log_errors ([general], default 1) and the
[discord_events] error toggle. Both optional/backward-compatible. Verified wiring
and the suppression path via a stubbed messenger.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-07 15:55:41 +02:00
alexandClaude Opus 4.8 8cf152cfb4 Stop bot cleanly when all routes are disabled
When every run has been auto-disabled there are no routes left to run, so save a
session report and shut down via safe_exit() instead of calling restart_or_exit
(which would needlessly restart D2R into empty games when restart_d2r_when_stuck
is enabled).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-07 15:52:22 +02:00
alexandClaude Opus 4.8 af43051b22 Auto-disable runs after repeated failures + save error screenshots
Recovery: each run now has its own consecutive-failure counter. After
disable_run_after_failures (default 5) consecutive failures, that single run is
disabled for the rest of the session and the bot keeps doing the other runs
instead of stopping. A success resets the counter. If every run is disabled the
bot stops for investigation. Disable is in-memory only (restart re-enables).

Diagnostics: on every run failure (approach, battle, or exception) the bot saves
a timestamped screenshot to log/screenshots/error/ named with the run, reason,
and game/run counters so logs and visuals can be cross-referenced. Gated by the
new error_screenshots config (falls back to info_screenshots). The error/ dir is
routed through log rotation.

Adds config keys error_screenshots and disable_run_after_failures (both optional,
backward compatible) and docs/recovery_and_error_logging.md. Verified bot startup,
config parsing, Bot construction, the disable/reset logic, and screenshot writing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-07 15:49:44 +02:00
alexandClaude Sonnet 4.6 a611765e42 Add comprehensive failure logging to all town acts and run approach methods
Every silent return False in a1-a5 town act files (resurrect, heal, identify,
open_wp, open_trade_menu, open_stash, open_trade_and_repair_menu, gamble) now
emits Logger.error with the specific step that failed (traverse, NPC menu open,
button press, panel visibility). Also adds Logger.error to all run approach()
methods (arcane, shenk_eld, trav, andariel, countess, mephisto, baal, pindle,
nihlathak) for open_wp, use_wp, go_to_act, and traverse failures. Fixes a4.py
open_trade_menu to use LeftPanel check instead of unreliable GoldBtnVendor.
town_manager fallback go_to_act calls (resurrect, identify, open_stash, gamble,
stash, heal) now log when they fail.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-07 15:44:24 +02:00
alexandClaude Sonnet 4.6 21e63fc423 fix: detect and log WP failures in approach/go_to_act
Problems:
1. use_wp() fired immediately after open_wp() — WP panel animation
   still playing, WaypointTabs template not yet visible → silent fail
2. vizier.py + diablo.py ignored use_wp() return value → approach()
   returned a truthy Location even when the WP was never used, causing
   battle() to run from the wrong map location
3. go_to_act() also ignored use_wp() return value

Fixes:
- waypoint.use_wp(): retry WaypointTabs detection up to 4x (1.6s total)
  before giving up; log each retry + the target WP and act being switched
- vizier/diablo approach(): check use_wp() return, return False on failure
- town_manager.go_to_act(): check use_wp() return, log + return False

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-07 15:35:11 +02:00
alexandClaude Sonnet 4.6 02d113ea0c fix: replace GoldBtnVendor check with LeftPanel in open_trade_menu
GoldBtnVendor (small gold coin template in a tight ROI) was failing even
when the vendor window was fully open. Replace with wait_until_visible(
LeftPanel, timeout=3.0) which checks for the panel close-X in the header
— the same reliable check used by wait_for_left_inventory/left_inventory_ready.

Fixes: A5 (Malah) and A1 (Akara) buy_consumables failing every run.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-07 10:42:27 +02:00
alexandClaude Sonnet 4.6 17c97a61b1 chore: gitignore runtime price data files
Add auto-generated price files to .gitignore and remove them from
tracking. These are updated at runtime by the price tracker and
should not be versioned.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-07 10:38:24 +02:00
alexandClaude Sonnet 4.6 307b021352 fix: 1282px window coords, NPC detection robustness, TP window re-detect
- screen.py: default monitor_roi width 1280 -> 1282 (actual window size)
- diablo.py: re-detect window position before TP in CS run (2 places)
- npc_manager.py: adjust Akara ROI/poses for 1282px; lower NPC name tag
  threshold 0.5->0.35 for more reliable hover detection; add NPCDialogue
  visibility wait + per-threshold debug logging in press_npc_btn
- a1.py: add wait(0.5, 0.8) after trade button before GoldBtnVendor check;
  guard open_trade_and_repair_menu on NPC menu result; add Logger.info
  throughout open_trade_menu, heal, open_trade_and_repair_menu
- a5.py: same wait + logging pattern for Malah open_trade_menu

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-07 10:37:35 +02:00
alexandClaude Sonnet 4.6 3410a82895 feat: log Windows version, mouse mode, and OCR backend at startup
Adds _log_platform_info() called from startup_checks(). On each bot
start the log now shows:
  - Windows 10/11 + build number (Win11 = build >= 22000)
  - Mouse input mode: relative (Win11) or absolute (Win10)
  - OCR backend: tesserocr (primary) or pytesseract fallback or ERROR

Win11 (build 26200) confirmed working: mouse in relative mode,
tesserocr 5.2.0 with hover/ground botty models loading correctly.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-06 22:14:38 +02:00
alexandClaude Sonnet 4.6 35fa4ba442 merge: town_manager.py — combine debug logging + set_panel_check_paused
Resolved 7 conflicts by keeping both local (Logger.info diagnostics in
buy_consumables and repair) and remote (set_panel_check_paused health-check
suppression during vendor/repair panels). All paths now log + pause correctly.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-06 22:07:24 +02:00
alexandClaude Sonnet 4.6 e729dc0c0e fix: 1282px coords, TP window re-detect, town_manager debug logging
- game.ini: adjust ROIs/positions for 1282x720 window (gold_btn_stash,
  left_inventory, panel_header, npc_dialogue, inventory_tabs, skill_bar)
- i_char.py: re-detect window position before casting TP to fix template drift
- town_manager.py: add Logger.info/error throughout buy_consumables and repair
  for easier diagnosis of vendor interaction failures

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-06 22:06:00 +02:00
FiskenPoul 7e39119f33 fix: robust cooperative shutdown and stability improvements for Windows 10
- Implementation of centralized cooperative shutdown mechanism (C10) to replace dangerous thread killing.
- Consolidation of all timing signatures through jittered wait() for anti-cheat stealth (C1).
- Enhanced HealthManager reactivity with smart mana potion fallback and premature panel-closing protection.
- Refactored waypoint stealth logic to simulate human-like mis-aim instead of area-breaking misclicks.
- Implementation of stateless 'garbage item' filtering in PickIt to eliminate ghost item loops and OCR artifacts.
- Optimization of inventory management to automatically stash protected items that cannot be sold.
- Suppression of individual game failure notifications on Discord; alerts now trigger on 5+ consecutive fails.
2026-06-06 19:34:06 +02:00
FiskenPoul d69f066da5 fix: robust cooperative shutdown and stability improvements for Windows 10
- Implementation of centralized cooperative shutdown mechanism (C10) to replace dangerous thread killing.
- Consolidation of all timing signatures through jittered wait() for anti-cheat stealth (C1).
- Enhanced HealthManager reactivity with smart mana potion fallback and premature panel-closing protection.
- Refactored waypoint stealth logic to simulate human-like mis-aim instead of area-breaking misclicks.
- Implementation of stateless 'garbage item' filtering in PickIt to eliminate ghost item loops and OCR artifacts.
- Optimization of inventory management to automatically stash protected items that cannot be sold.
- Suppression of individual game failure notifications on Discord; alerts now trigger on 5+ consecutive fails.
2026-06-06 19:32:18 +02:00
alexandClaude Sonnet 4.6 88bd369bd0 merge: resolve params.ini conflict — keep transmute_every_x_game=2000
Alex's value (2000) preserved over FiskenPoul's (200) for transmute cadence.
stash_destination=0,1,2,3 accepted from remote (matches fill_shared_stash_first=0).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-06 18:01:32 +02:00
alexandClaude Sonnet 4.6 dc48a05d61 restore: params.ini to pre-merge (7dcbb52) + common.py/pickit.py fixes
- params.ini: restore Alex's settings (auto_login, webhooks, hotkeys,
  run order, casting_frames, potion thresholds, override_capabilities, etc.)
- common.py: increase wait_for_left_inventory timeout 5s → 10s for stability
- pickit.py: re-detect window position before pickup to fix template drift

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-06 18:00:29 +02:00
FiskenPoul 6f4d6880e4 fix: remove duplicate auto_downgrade_threshold in params.ini 2026-06-06 11:29:17 +02:00
FiskenPoul 97f4e55643 fix: comprehensive stability, stealth, and logic improvements
- Stability: Replaced unsafe kill_thread() with cooperative_shutdown() in GraphicDebugger.
- Stealth: Integrated jittered wait() and human-like typing variation in win_input.py.
- Robustness: Switched to absolute paths for template loading and log rotation anchoring.
- Bugfix: Resolved AttributeError: 'ScreenObjects' has no attribute 'MercPanelText'.
- Logic: Implemented thread-safe state in DeathManager and re-indexed PickedUpResult enum.
- Pickup: Added failure blacklisting for unreachable items to prevent infinite loops.
- Stash: Fixed gold stashing to correctly respect fill_shared_stash_first=0 (personal first).
- Config: Updated settings validation to allow common D2R variations (Gamma, VSync, etc.).
2026-06-06 11:24:20 +02:00
alexandClaude Sonnet 4.6 a91e128a1e docs: restore params.ini documentation lost in merge conflict resolution
The FiskenPoul merge (95f520e) resolved conflicts by dropping three comment
blocks that documented Hammerdin-specific tuning guidance:

- Hammerdin difficulty guide near the 'difficulty' setting (Normal/NM/Hell
  gear targets, chicken-rate advice)
- Hammerdin attack-length reference table near atk_len_* values (per-boss
  HP ranges, Conviction interaction, CS seal boss breakdown)
- auto_downgrade_threshold parameter and its description (the feature is
  parsed by config.py but not yet active; restored with a note)

No logic changes — comments and one dormant config key only.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-06 09:09:03 +02:00
alex 01a0351530 fix: apply 4 priority reliability fixes from codex analysis
- nihlathak: verify waypoint load with template check, add grayscale fallback for layout detection, guard traverse_nodes_fixed return
- npc_manager: press_npc_btn now returns bool with threshold retry (0.85->0.78) + grayscale fallback
- town/a1,a4,a5: check press_npc_btn return and verify GoldBtnVendor before proceeding
- char/i_char: expand stash waypoint close to A1_TOWN_0, add lower-threshold retry for stash in a1/a5
- char/i_char: add _weapon_switch helper that resets _active_skill cache, fix CTA switch-back validation, fix switch_sucess typo
2026-06-06 01:28:41 +02:00
alex 5ac00d16d0 Add Win10/Win11 auto-detection for mouse input mode
- win_input.py: detect OS build via platform.win32_ver(), use
  MOUSEEVENTF_ABSOLUTE only on Win10 (build < 22000)
- install.bat: detect and display Windows version on install
2026-06-05 22:51:54 +02:00
FiskenPoul 4db549cd85 Track tesserocr_source dependency 2026-06-05 22:36:10 +02:00
FiskenPoul 95f520e968 Resolve merge conflicts, keeping local Win10 specific changes 2026-06-05 21:47:11 +02:00
alex 7dcbb5284b fix: improve potion management by filling belt from inventory and adjusting consumable handling 2026-06-05 21:39:15 +02:00
alexandClaude Sonnet 4.6 dc61367a6e fix: guard fill_up_belt_from_inventory against None img from failed open_inventory
personal.open_inventory() returns None when the inventory cannot be opened
(e.g. after a failed vendor interaction leaves the UI in a bad state). The
previous code passed that None straight into common.get_slot_pos_and_img()
which tried to index it, crashing the bot thread with:
  TypeError: 'NoneType' object is not subscriptable

Also wraps the shift-click loop in try/finally so keyboard.release("shift")
is guaranteed even if an exception fires mid-loop (prevents stuck Shift key).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-05 20:47:11 +02:00
alexandClaude Sonnet 4.6 6c8da72bf4 fix: weapon swap verification, health pot selling, town healing, pickup drought alert
- i_char.py: rewrite _pre_buff_cta() to verify each weapon switch via BC
  skill-bar template; detects wrong slot on game start (leftover from
  interrupted buff cycle), retries failed switches once, logs clearly
  when stuck on CTA slot to prevent dying with wrong weapon in combat

- personal.py: protect needed consumables from sell/drop in inspect_items;
  check get_needs() before marking a pot for discard — if the belt needs
  that pot type, skip it so fill_up_belt_from_inventory can restock later

- bot.py: add fill_up_belt_from_inventory + update_pot_needs after
  buy_consumables so inventory pots reach the belt even when out of gold;
  add town-heal loop at start of on_maintenance to drink health/rejuv pots
  until HP >= 95% before the next run (health manager is paused in town)

- game_stats.py: add rolling 10-game pickup health check; warns in log and
  sends Discord alert when zero item pickups occur across 10 games while
  chickens or merc deaths are present

- params.ini: fix show_belt=n -> show_belt=k (belt key was wrong, causing
  1.5s wasted recovery attempt every first game in a session)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-06-05 16:36:07 +02:00
alex 77f2092572 Refactor bot behavior on TP failure, enhance item processing logic, and improve game stats logging
- Updated bot behavior to end the game when no TP charges are left, instead of attempting to walk back to town.
- Refined item processing logic in `processing_helpers.py` to use identity checks for item comparisons and improved item removal methods.
- Enhanced game stats logging to include failure reasons and send Discord notifications for failed games.
- Improved health management logic to trigger chicken behavior based on health percentage after rapid rejuvination.
- Added error handling for item pickup failures in `pickit.py` to blacklist items that cannot be picked up.
- Introduced walking fallback nodes for Countess, Andariel, Mephisto, and Baal runs in `pather.py`.
- Implemented path data validation in run scripts for Countess, Andariel, Mephisto, and Baal to ensure paths are correctly defined.
- Enhanced item identification and stash processes in `diablo.py` with retry logic for failures.
- Updated town management to handle identification failures gracefully and log appropriate warnings.
- Improved character selection logic to provide clearer logging messages.
- Modified game restart logic to launch the game via PowerShell for better session handling.
- Added documentation for recording paths for the Countess run and created a PowerShell script for scheduled task management.
2026-06-05 14:58:50 +02:00
alex 6d42216fd1 fix: add missing mouse.right-click after _select_skill for CTA buffs 2026-06-04 20:43:20 +02:00
alex d1502a008a fix: guard on_maintenance against _curr_loc=False crash 2026-06-04 20:41:19 +02:00
alex b94147a9c9 fix: CTA use _select_skill with right-click (matches working forks aeon0/redjon) 2026-06-04 20:34:12 +02:00
alex 75a35821e4 fix: CTA pre-buff is just w, 7+RClick, 8+RClick, w (no extra buffs - Concentration is attack rotation) 2026-06-04 20:31:03 +02:00
alex d4ccca8496 fix: CTA buff sequence: w, 7+RClick, 8+RClick, buffs, w (no visual verification) 2026-06-04 20:29:52 +02:00
alex 26f5faf977 fix: CTA buffs move mouse to center before right-click to target self 2026-06-04 20:25:05 +02:00
alex 3f46ccbe23 fix: CTA buffs use left-click to cast on self; simplify swap-back to 2 attempts
- BC and BO cast with left-click (targets self) instead of right-click
- Swap-back loop simplified to 2 attempts with match score logging
- Added full test route order: trav, pindle, eldritch_shenk, nihlathak, arcane, diablo, vizier
2026-06-04 20:21:54 +02:00
alex 08a79b73c1 fix: CTA pre-buff no longer depends on broken BC/BO skill templates
Removed the visual skill check (is_right_skill_selected BC/BO) that always
failed because the BC.png/BO.png templates do not match D2R icons. New flow:
switch weapon, cast BC, cast BO, buff, switch back. Only verifies swap-back
by matching the original skill icon via template matching.
2026-06-04 19:47:24 +02:00
alex 22b7ac5b49 fix: set failure reasons before log_end_game to eliminate 'Unknown'
- game_controller: set reason on max_game_length, chicken, force_stop
- bot.py on_end_game: set default reason when failed=True has no reason yet
- bot.py save_and_exit failure: set reason before stop()
- bot.py restart_or_exit: capture error message as failure reason
2026-06-04 19:39:15 +02:00
alex 3d7163df16 fix: prevent _curr_loc=False crash in town manager retries; add travincal route
- All retry paths (stash, buy_consumables, heal, repair) now use known-good
  location constants instead of corrupted self._curr_loc (False)
- Guard gamble->stash chain against False location
- Guard wait_for_tp against False location
- Add run_trav to route order (travincal then pindle)
2026-06-04 19:30:23 +02:00
alex 4c88c7204f docs: add plan for broken runs - missing coords and templates 2026-06-03 23:28:38 +02:00
alex 4b96155a37 Hammerdin Diablo/Pindle fixes
- Fix merc icon detection: expanded ROI from 40x40 to 60x60 (matchTemplate requires search area > template)
- Add MercPanelText screen object using mercenary_screen.png for reliable merc alive check
- Bot merc check now uses MercPanelText instead of generic LeftPanel
- Diablo: wait for boss to spawn before attacking (up to 15s), use conviction aura
- Diablo/Pindle attack lengths increased (diablo 3->10s, pindle 3->8s)
- Aura key switch delay increased from 50-100ms to 200-300ms for reliable activation
- Add conviction hotkey (F5) to hammerdin config
- Move Pindle CTA pre-buff from town to after entering red portal
- More aggressive healing thresholds (health potion at 98%, rejuv at 85%)
- Max game length increased from 160s to 600s for full Diablo runs
2026-06-02 19:41:42 +02:00
alex 2baf51c717 Fix merc detection and portal clicking
- Crop A5_RED_PORTAL template to remove empty space (center now aligns with portal)
- Fix merc_icon ROI from 10,9,50,50 to 15,14,40,40 (matches 40x40 merc templates)
- Lower MercIcon threshold from 0.9 to 0.85, add MERC_A2_2 variant
- Add merc panel confirmation with 'O' key before resurrect (prevents false resurrect when icon detection fails)
- Override capabilities to can_teleport_natively
2026-06-01 22:20:49 +02:00
alex 2cd683ac7b Add not enough gold screenshot logging and new MISSING_GOLD template
- src/inventory/vendor.py: Log screenshots when NotEnoughGold detected
  during gambling, purchasing, and repairing
- src/ui_manager.py: Update NotEnoughGold to use new MISSING_GOLD template
  from assets/npc/missing_gold/ (full dialog match instead of color-based)
- assets/npc/missing_gold/missing_gold.png: New template for the popup
2026-06-01 11:21:53 +02:00
alex 30bf163ee3 Fix move_d2r_window to position client area, not outer window
- src/utils/misc.py: move_d2r_window now calculates outer window position
  to place client area at target coords (was incorrectly setting outer
  window to target, shifting client area unpredictably)
- src/bot.py: Call move_d2r_window(5,98) at game start to restore original
  working client area position and prevent 13px offset drift
2026-06-01 11:07:53 +02:00
alex cb3d49fa22 Fix window offset drift causing pathing and template matching failures
- src/screen.py: Add force param to find_and_set_window_position/set_window_position
  to bypass early-return guard and skip wait when called programmatically
- src/run/pindle.py: Force window re-detection before each traverse_nodes and
  select_by_template call to handle offset drift between runs
- src/pather.py: Add fallback full-image search with lower threshold (0.55) when
  cropped ROI search fails in find_abs_node_pos
- src/bot.py: Move D2R window to stable position (0,0) at game start via
  move_d2r_window; skip merc resurrect after first failure (no gold)
- src/game_stats.py: Add _merc_resurrect_failed flag (reset each game)
- src/utils/misc.py: Add move_d2r_window() function
- config/params.ini: Updated teleport=b, show_belt=k, override_capabilities,
  restore_settings_from_backup_key=insert, graphic_debugger_key=delete
2026-06-01 11:01:00 +02:00
alex 5244a50ea3 feat: potion transmute system - convert Rejuv to Full Rejuv via cube 2026-06-01 09:54:26 +02:00
alex f59ffd5d1f fix: raise stash page limit to 20 for ROTW support, fix merc_icon ROI, add red resurrect button detection 2026-06-01 09:27:42 +02:00
alex 8eafd91f0a fix: health_manager merc healing by actual HP + clean up stray TODO comment
- health_manager.py: read actual merc health for heal/chicken/rejuv decisions
  instead of timer-only logic; pass merc_health to belt.drink_potion stats
- processing.py: remove stale TODO comment (traceback import is used)
2026-05-31 13:43:26 +02:00
alex a83dc15b62 fix: heal merc via keyboard timer instead of fragile icon detection 2026-05-31 13:30:39 +02:00
alex 78bb963654 fix: correct merc_icon and merc_health ROI to top-left corner 2026-05-31 13:29:29 +02:00
alex 7d1f03beb7 chore: commit all changes 2026-05-31 12:51:21 +02:00
alex 7d6314b575 Save all config, data, and tools state 2026-05-30 20:21:15 +02:00
alex 21c33a3dc9 Add D2R Keys (Terror, Destruction, Hate, 3x3 Set) to daily price tracker 2026-05-30 19:55:10 +02:00
alex b9f3253e24 fix(ci): restore test assets from git history, remove dead download step
- Restore test/assets/ from commit 5127417^ (was moved to bottytools/botty-test-assets which is now 404)
- Remove 'Download test assets' step from CI workflow (assets are now in-repo)
- Make download_test_assets.py a no-op for backward compatibility
- Fix import: from pipes import Template -> from template_finder import Template
- Remove test/assets/ from .gitignore
2026-05-30 10:03:11 +02:00
alex fefcfa013f fix(ci): correct import from template_finder instead of non-existent pipes 2026-05-30 09:54:30 +02:00
alex 4b339228fe fix(ci): merge coverage into pytest step to fix Coverage failure 2026-05-30 09:41:50 +02:00
alex 43f973ebc2 bump version to 0.8.4 2026-05-30 09:34:41 +02:00
alex 16a4d121c0 feat: log rotation with Discord notifications + failure reason tracking
- Add utils/log_rotation.py: safe_imwrite() auto-rotates screenshot dirs
  when they exceed configured file count or size limits
- Add [log_rotation] config section in params.ini (pickit/info/items
  max_files, max_mb, discord_notify_rotation)
- Replace 40 cv2.imwrite() calls with safe_imwrite() across 13 files
- Add failure reason tracking: bot.py catches run exceptions, stores
  reason in game_stats, Discord message includes the error
- Add Discord notification when log rotation deletes old files
- Prevents disk-full crashes that stopped botty on May 30
2026-05-30 09:32:24 +02:00
alex be874f3275 Ignore debug_forum.html and fg_daily_estimates.json 2026-05-28 20:43:03 +02:00
alex 719b0b1dea Clean up .gitignore: add data/, screenshots/, tools/__pycache__, remove duplicates 2026-05-28 19:27:57 +02:00
alex c228fa9661 Add log/ and *.log to .gitignore
- Ignore log/, log/archive/, log/runs/, log/stats/, *.log
- Remove duplicate *.log and log/ entries that were lower in the file
2026-05-28 19:12:45 +02:00
alex 8853620786 Archive rotated logs to log/archive/ as zips
- Custom ArchiveRotatingFileHandler zips old log.txt.N files
  into log/archive/log_YYYYMMDD_HHMMSS.zip
- Keeps log/ directory clean — no loose .txt.1 files
2026-05-28 19:12:18 +02:00
alex e4c201bf2d Add rotating session reports and log rotation
- on_exit() now generates a session report (txt + json) in log/runs/
- Log file rotates daily, keeps 7 days (TimedRotatingFileHandler)
- Added log/runs directory creation in main.py
2026-05-28 19:07:42 +02:00
alex 9491169549 Add gold tracking to stats and fix game resolution 2026-05-28 16:52:41 +02:00
alex 3f50a6529c Update FG database with 1229 sellers 2026-05-27 20:00:00 +02:00
alex ed65691257 Add post links to sellers: extract post anchors, store direct links to seller posts 2026-05-27 19:57:51 +02:00
alex 14b4835702 Update FG data: 1500 topics, 1229 sellers with usernames 2026-05-27 16:44:34 +02:00
alex ecf91683b7 Add query_fg_prices.py: CLI tool to query FG price database 2026-05-27 16:04:11 +02:00
alex 29e6cc3c99 Add query_fg_prices.py: CLI tool to query FG price database 2026-05-27 15:56:22 +02:00
alex 9c00ca5de7 Fix DB update script: correct prev_scrape query 2026-05-27 15:16:24 +02:00
alex dbee47e152 Add database update script for FG price tracker 2026-05-27 15:15:46 +02:00
alex 512acb02b9 Fix username extraction: correct regex for user.php links and split indexing 2026-05-27 15:12:30 +02:00
alex 69dc33706a Add SQLite database for FG price tracking with sellers table 2026-05-27 13:46:32 +02:00
alex 93771198f1 Add username extraction: per-seller cheapest prices in output and DB 2026-05-27 13:42:36 +02:00
alex 5cfa91fe64 Major scraper overhaul: multi-page pagination, 80+ item patterns, multi-item detection 2026-05-27 11:23:32 +02:00
alex 8574eedc16 Update FG daily estimates: 1500 topics, 24 item types 2026-05-27 09:46:21 +02:00
alex bc649ed863 Fix FG report: normalize item names, estimate FG from item_kept events 2026-05-27 09:38:14 +02:00
alex ea4a753e1c Update FG daily estimates: 500 topics, 20 item types with prices 2026-05-27 08:57:42 +02:00
alex 635ccb0dea Ignore fg_reports output directory 2026-05-27 08:51:20 +02:00
alex fd49d9700d Add FG report outputs 2026-05-27 08:51:02 +02:00
alex ef29136e16 Add FG daily earnings report tool (8h interval) 2026-05-27 08:50:57 +02:00
alex 51319fd11f Fix d2jsp scraper: Cloudflare bypass, relative URL handling, URL dedup, Python 3.10 compat 2026-05-27 01:13:10 +02:00
alex e3d6605595 Update FG price tracker, game stats, and add d2jsp collection tooling 2026-05-27 01:08:45 +02:00
alex 19f4b8a553 Harden offline FG parser and add d2jsp collection tooling 2026-05-26 13:04:28 +02:00
alex 3888cc9666 Fix status XP/level math, env refs, and potion tier fallback 2026-05-26 11:36:58 +02:00
alex 055cd4fdf1 Expand cows route plan with screenshot and coordinate SOP 2026-05-26 09:53:13 +02:00
alex 377b2803e3 Improve run stability, repair flow, and stats tracking 2026-05-26 09:43:53 +02:00
alex 7620bd4ef7 Document each route in params.ini routes section 2026-05-26 09:34:24 +02:00
alex 9b5ba463c2 Improve routes documentation and add Hammerdin keyrun example 2026-05-26 09:33:25 +02:00
alex 64349a988d Add Hermes (Qwen) Botty takeover development/testing guide 2026-05-26 08:49:06 +02:00
alex cdc13d1c6c Add configurable Discord event toggles in params.ini 2026-05-26 08:42:31 +02:00
alex a3ac31463e Fix Discord embed send by omitting file arg when no attachment 2026-05-26 08:28:49 +02:00
alex e54bb64d03 Add phased Linux port plan documentation 2026-05-26 08:19:07 +02:00
alex 179eac8b80 Document .env workflow and recent stability/safety fixes 2026-05-25 20:14:55 +02:00
alex b7a7280964 Fix XP status math edge-cases causing 'Failed to log exp' 2026-05-25 19:49:15 +02:00
alex 909aa88977 Make XP OCR parsing robust to casing and common OCR typos 2026-05-25 19:46:51 +02:00
alex 254c167cbe Reduce XP logging warning spam and keep run stable 2026-05-25 19:45:27 +02:00
alex b3fd60c26d Add sell safety for shields and log sold item names 2026-05-25 19:43:46 +02:00
alex 55def980cd Make repair flow best-effort and harden Discord send fallback 2026-05-25 19:23:09 +02:00
alex 265326a471 Stop tracking personal .env file 2026-05-25 17:17:58 +02:00
alex af6ab38939 Add untracked .env overrides with documented template 2026-05-25 17:15:58 +02:00
alex 5ee0061936 Add Discord embed unit tests for safe send behavior 2026-05-25 17:13:15 +02:00
alex 0bd8e7e7bc Add A5->A4 repair fallback and harden Discord embed send 2026-05-25 17:03:16 +02:00
alex 25c5810778 Handle missing OCR deps without crashing XP logging 2026-05-25 15:18:45 +02:00
alex f20ac02720 Add tesserocr wheel fallback to Windows installer 2026-05-25 15:16:09 +02:00
alex c96fcdf545 Improve dependency checker with concrete remediation steps 2026-05-25 15:13:37 +02:00
alex b77d62144a Add requirements.txt package validation to dependency checker 2026-05-25 15:11:58 +02:00
alex e573a28e8a Add Windows dependency health-check script and launcher 2026-05-25 15:10:23 +02:00
alex d79f472c33 Add robust Larzuk repair interaction fallback with logging 2026-05-25 15:07:17 +02:00
alex 3bf1c3d539 Fix town marker check in start_game by passing current frame 2026-05-25 15:04:45 +02:00
alex a691d4bdfc Harden game creation, tome parsing, and A5 repair menu flow 2026-05-25 15:03:19 +02:00
alex b84b745dbd Accept 'page up' alias for hotkey mapping 2026-05-25 14:56:11 +02:00
alex a70302f44d Change default auto settings hotkey to page up 2026-05-25 14:52:29 +02:00
alex 0b1af05841 Avoid unsafe force-kill for bot/controller shutdown 2026-05-25 14:46:11 +02:00
alex d96cf544bf Fix rare-for-gold override when pickit cache is used 2026-05-25 14:41:36 +02:00
alex fc3ac30431 Persist stats snapshot continuously for crash resilience 2026-05-25 14:36:11 +02:00
alex a39acf510d Fix pytest setup and handle missing tesserocr in test env 2026-05-25 14:35:13 +02:00
alex c9ffcf1e4f Add Discord status updates every N runs with config option 2026-05-25 14:30:33 +02:00
alex 0ccf8ae5ea Add structured run/item logging and per-area item stats 2026-05-25 14:28:52 +02:00
alex d4e1758d97 Make pick_gold override BNIP gold threshold 2026-05-25 14:25:37 +02:00
alex 40d46bb592 Add simple pick_gold config toggle for ground gold 2026-05-25 14:22:35 +02:00
alex c5519ff290 Add config toggle to pick and sell rares for gold 2026-05-25 14:18:57 +02:00
alex dd45386579 Improve config docs and harden discord+difficulty parsing 2026-05-25 14:10:54 +02:00
alex 54f0e3ebcc Update docs and params defaults 2026-05-25 13:52:39 +02:00
alex 7eb961476e Fix Discord webhook init and add local config defaults/ignores 2026-05-25 13:51:50 +02:00
alex d44acf8bf2 Add CI launch smoke test for built executables 2026-05-25 13:47:09 +02:00
alex f597a0108c Fix cooldown pacing for Blizzard and FoH casts 2026-05-25 11:14:52 +02:00
alexandClaude Sonnet 4.6 af06d8aff4 Fix show_belt key, post-attack wait, and pickit variable scope
- config/params.ini: show_belt k->n matches the actual D2R keybind so
  the bot can now correctly read and manage belt potion inventory.
  pickit_screenshots=1 enables per-run loot scan evidence.
- blizz_sorc.py: kill_pindle() now waits 1.5-2.0s (was ~0.33s) after
  the attack loop before teleporting to the loot area.  Blizzard has an
  ~1.8s fall duration, so the old wait left enemies alive when the sorc
  arrived.  Also adds _cast_static() at the start to reduce pack HP
  immediately.
- pickit.py: rename loop var i->ground_item (fixes potential NameError
  when items list is empty) and improve tele-fail warning message.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 20:46:00 +02:00
alexandClaude Sonnet 4.6 9ee102a0f1 Fix charged teleport not firing in traverse_nodes_fixed
The TELE_ACTIVE template only matches native teleport's icon. Charged
teleport (from a staff/item) shows a different icon with a blue charge
indicator, so is_right_skill_selected(["TELE_ACTIVE"]) returns False
and char.move() falls back to walking.

Walking a path calibrated for teleport drops the sorc in the wrong
game-world position, so blizzard casts land south of Pindle ("below
targets") and the sorc is exposed to the full minion pack.

Fix: for can_teleport_with_charges chars, call select_tp() once then
pass force_tp=True to each char.move() hop. force_tp bypasses the
template check and re-arms the teleport hotkey before each right-click,
so charges are actually consumed per hop. If charges deplete mid-path
char.move() right-clicks with no effect and the remaining hops degrade
to standing still rather than walking into monsters.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 20:09:11 +02:00
alexandClaude Sonnet 4.6 5e320f0eef Fix charges-sorc walking into Pindle's minion pack (death)
pindle.py: route can_teleport_with_charges chars through
traverse_nodes_fixed for the Pindle approach instead of traverse_nodes.
traverse_nodes (node-based) falls back to walking for charges chars
because pre_move does nothing for them — so the sorc was slow-walking
through the entire minion pack before the fight, arriving at near-zero HP.

pather.py: traverse_nodes_fixed now calls char.select_tp() before
pre_move when using charge-based teleport, so char.move() sees
teleport on the right-skill slot and uses charges for each step.
When charges deplete mid-path it falls back to walking gracefully.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 19:54:15 +02:00
alexandClaude Sonnet 4.6 2634619b18 Fix window-detection race condition and None monitor range crash
- misc.py: catch psutil.NoSuchProcess in WindowSpec.match() — a process
  can exit between EnumWindows enumerating it and us calling .name(),
  which was crashing the window-detection thread and leaving
  monitor_x_range/monitor_y_range as None
- screen.py: guard convert_screen_to_monitor against None ranges so
  template matching doesn't throw TypeError before the D2R window is
  first located; logs a warning and returns unclamped coords instead
- main.py: switch BeautifulTable to STYLE_DEFAULT (ASCII only) to avoid
  UnicodeEncodeError on cp1252 terminals; add try/except fallback

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 19:42:40 +02:00
alexandClaude Sonnet 4.6 dc224bec5c Fix CTA spam, traverse_nodes_fixed crash, and add skill preflight/setter
- config: cta_available=0 (user has no CTA weapon; was causing 5 failed
  weapon-swap attempts per run)
- pather: traverse_nodes_fixed now allows can_teleport_with_charges chars
  through instead of raising ValueError; when charges deplete mid-path
  char.move() falls back to walking gracefully
- utils: add skill_preflight.py (visual hotkey verification, all blizz_sorc
  skills use side=right) and skill_hotkey_setter.py (automated picker binding
  with step-by-step logging)
- test: add test_skill_preflight.py (3 passing tests)
- assets: add sorc skill icon templates for preflight matching
- tools: add capture_sorc_skill_icons.py and set_sorc_skill_hotkeys.py
- key_detector: extend VK map with numpad/F-key/symbol codes; add
  validate_key_bindings() and parse_key_file(); fix char_name lookup

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 19:26:00 +02:00
alex a77e87e138 Improve Botty launch and runtime recovery 2026-05-24 18:14:23 +02:00
alex 080b7d1679 Set Botty to run Pindle with blizz sorc 2026-05-24 17:53:29 +02:00
alexandClaude Sonnet 4.6 d4fec70950 feat: add character selector (blizz sorc / fohdin / hammerdin) to F6 UI
The run-selector window now also shows a character section with radio
buttons for the three supported builds. Selecting a char updates
Config().char["type"] in-memory alongside the run selection; both take
effect on the next game.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 17:52:45 +02:00
alexandClaude Sonnet 4.6 d3dbf2bf9c fix: disable auto-login by default, require explicit auto_login=1
Auto-launch of D2R and bnet credential injection are now gated behind
an explicit `auto_login=0` flag in params.ini. Setting it to 1 restores
the previous behaviour. Credentials stored in bnet_name/bnet_pass are
never appended to launch options while auto_login=0.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 17:45:23 +02:00
alexandClaude Sonnet 4.6 887a97c93b feat: add F6 hotkey to select boss/farm runs at runtime
Adds a tkinter run-selector window accessible from the botty console via
the new select_runs_key hotkey (default F6). Users can check/uncheck any
of the 11 supported boss/farm runs; the selection updates Config in-memory
and takes effect on the next game without restarting botty.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 17:42:10 +02:00
alexandClaude Sonnet 4.6 d6c8d8d5f7 fix: replace custom tesserocr wheel with conda-forge package
The custom tesserocr-2.5.2 wheel was compiled on a specific machine
against DLL versions that do not match a fresh conda-forge installation.
This caused persistent ImportError: DLL load failed regardless of PATH
or LoadLibraryExW approach.

conda-forge's tesserocr package is compiled against the exact same
conda-forge tesseract/leptonica binaries, so all DLL dependencies
are automatically satisfied within the conda environment — no manual
DLL path manipulation needed.

Changes:
- environment.yml: add tesserocr + tesseract as conda-forge packages,
  remove leptonica pin (no longer needed), remove custom wheel from pip
- install.bat: replace wheel force-reinstall with pip uninstall cleanup
- src/*.py: simplify DLL fix to os.add_dll_directory only

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 14:49:00 +02:00
alexandClaude Sonnet 4.6 e1c18faf26 fix: use conda run instead of direct python for DLL loading
Every approach to manually replicating conda's DLL environment from
Python code or batch PATH manipulation has failed. conda run activates
the environment exactly like "conda activate botty" — setting PATH,
running activate.d scripts, and properly resolving all transitive DLL
dependencies for tesseract51.dll.

run_botty.bat now derives conda.exe from the botty python.exe path
(two levels up: envs/botty -> envs -> miniforge3 -> Scripts/conda.exe)
and uses "conda run -n botty --no-capture-output python src/main.py".

install.bat smoke test now uses "%CONDA_EXE% run -n botty python -c ..."
which already has CONDA_EXE set from the install step.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 14:37:14 +02:00
alexandClaude Sonnet 4.6 bdd6247342 fix: use LoadLibraryExW(0x1000) to preload tesseract51.dll
os.add_dll_directory alone is not enough — LOAD_LIBRARY_SEARCH_USER_DIRS
does not propagate to transitive deps of deps when loaded automatically
by the OS (e.g. tesseract51.dll's deps like mingw runtimes, leptonica).

LoadLibraryExW with LOAD_LIBRARY_SEARCH_DEFAULT_DIRS (0x1000) explicitly
propagates user DLL dir search to the entire transitive dep chain, so
leptonica, libgcc, libstdc++, zlib etc. are all found in Library\bin
and Library\mingw-w64\bin without conda activate.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 14:30:12 +02:00
alexandClaude Sonnet 4.6 77ad97022e fix: remove ctypes.WinDLL preload — use only os.add_dll_directory
ctypes.WinDLL uses LoadLibraryW which does NOT search user DLL dirs
registered via os.add_dll_directory/AddDllDirectory. It was throwing
FileNotFoundError and blocking the import before tesserocr was ever tried.

Python 3.8+ loads .pyd files with LOAD_LIBRARY_SEARCH_USER_DIRS which
DOES search user-registered dirs for the pyd and all its transitive DLL
dependencies. os.add_dll_directory(Library\bin) alone is sufficient.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 14:19:18 +02:00
alexandClaude Sonnet 4.6 d9383cc164 feat: add update.bat for git pull workflow
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 14:12:31 +02:00
alexandClaude Sonnet 4.6 a77dafaf08 fix: set conda DLL PATH at batch level before Python starts
os.environ['PATH'] set from inside Python does not affect the Windows
DLL loader used by ctypes.WinDLL — the loader reads the process PATH
at load time, not from Python's env dict. Set PATH in the .bat files
before python.exe is launched so tesseract51.dll's transitive deps
(leptonica, zlib, libpng, etc.) are findable by the loader.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 14:10:00 +02:00
alexandClaude Sonnet 4.6 18c08a4c65 fix: prepend conda Library\bin to PATH before ctypes.WinDLL call
When ctypes.WinDLL loads tesseract51.dll by absolute path, Windows
resolves that DLL's own transitive deps using the standard search order:
app-dir → System32 → Windows → cwd → PATH. Library\bin is in none of
those (conda activate was not run), so leptonica, zlib, libpng, etc.
are invisible and the load fails even though the DLLs are all present.

Fix: prepend all conda DLL dirs to os.environ['PATH'] before the
ctypes.WinDLL call so the standard DLL search finds them. os.add_dll_directory
is still called for Python's LOAD_LIBRARY_SEARCH_USER_DIRS path.
Together the three steps guarantee the import works without conda activate:
  1. os.add_dll_directory  - for .pyd loading
  2. os.environ PATH       - for ctypes transitive dep resolution
  3. ctypes.WinDLL(abs)    - pre-cache tesseract so .pyd reuses it

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 14:03:00 +02:00
alexandClaude Sonnet 4.6 f8380af3bd fix: pre-load tesseract51.dll by absolute path to fix transitive DLL deps
os.add_dll_directory alone is not enough on Windows. When Python loads
the tesserocr .pyd via LOAD_LIBRARY_SEARCH_USER_DIRS, Windows finds
tesseract51.dll in the added directory but then resolves tesseract's own
transitive deps (leptonica, zlib, libpng etc.) using only the standard
system search path -- not the user DLL dirs. Those libs live in
Library\bin, not System32, so they're invisible and the load fails even
though every DLL is present.

Fix: call ctypes.WinDLL(absolute_path_to_tesseract51.dll) before the
tesserocr import. LoadLibraryW with a full path anchors tesseract51.dll
to Library\bin, so Windows searches that directory for its transitive
deps. The already-loaded DLL is then returned from cache when the .pyd
requests it, making the import succeed.

Applied to ocr.py (test entry point), main.py, and shopper.py.
Also updated install.bat smoke test and diagnostic to use the same fix.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 13:59:21 +02:00
alexandClaude Sonnet 4.6 ee940b7e3d fix: pin leptonica=1.78.0 to match tesserocr wheel DLL dependency
The custom tesserocr wheel links against leptonica-1.78.0.dll at
compile time. Unpinned leptonica on conda-forge resolves to 1.82+
which installs leptonica-1.82.0.dll — a different filename — so
Windows DLL loader cannot find it regardless of os.add_dll_directory.

Also force-reinstall the wheel in install.bat to guarantee the
correct binary is used (not a stale cached version), and add a
diagnostic that prints which DLLs are actually present when the
smoke test fails so the root cause is visible instead of a vague
warning.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 13:51:54 +02:00
alexandClaude Sonnet 4.6 91403e49d3 feat: make zip-download install flow work for first-time users
- README: add step-by-step Installation section (Miniforge → download
  ZIP → install.bat → config → run_botty.bat) so a non-technical user
  can follow it without reading development.md
- config/params.ini: reset personal fields (name, char_name,
  saved_games_folder) to generic defaults so the downloaded zip
  works out of the box for anyone
- install.bat: apply os.add_dll_directory before the tesserocr smoke
  test so it stops emitting a false warning on every install

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 09:39:12 +02:00
alexandClaude Sonnet 4.6 8ee31d9691 fix: use sys.prefix for tesserocr DLL search in all entry points
dirname(dirname(sys.executable)) walks two levels up from the exe on
Windows, landing in the envs/ parent rather than the env root. Replace
with sys.prefix which is always the correct conda env root, and cover
Library/{bin,mingw-w64/bin,usr/bin} to handle all conda-forge layouts.
Also remove unused _dll_fix.py which had the same bug.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-24 09:34:59 +02:00
alex 40a207be9a feat: harden install.bat + add CI checks for launcher scripts
install.bat:
- Add conda self-test (conda --version) before env create
- Verify botty python.exe exists after env creation
- Smoke-test key imports (cv2, tesserocr, discord, etc.)

CI (.github/workflows/ci.yml):
- Add test_setup_bat_files.py to test matrix

test/test_setup_bat_files.py (7 tests):
- All expected .bat files exist
- No hardcoded usernames (alex, alexpolo, ultimate) in run scripts
- No absolute home paths in run scripts (must use %~dp0 / %USERNAME%)
- All run_*.bat source find_python.bat (no duplicated conda detection)
- find_python.bat checks >= 6 conda locations
- install.bat has conda self-test and python verification
2026-05-23 23:15:24 +02:00
alex dec3269a0c fix: make launcher bat files work from any working directory
- Add find_python.bat shared helper to auto-detect conda env (6 locations)
- All bat scripts now resolve paths relative to script location (%~dp0)
- Fixes crash when launching from Start Menu / pinned shortcut (cwd=System32)
- Fixes hardcoded username path in run_asset_extractor.bat
2026-05-23 23:10:16 +02:00
alex 27573bf362 add blizzpw.txt to .gitignore 2026-05-23 00:06:10 +02:00
alexandClaude Sonnet 4.6 5451357b86 fix tesserocr DLL load failure on CI Windows runners
Python 3.8+ no longer searches conda's Library\bin for DLLs automatically.
Add os.add_dll_directory(sys.prefix/Library/bin) before importing tesserocr
so leptonica and tesseract DLLs are found on any conda-based Windows env.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-23 00:04:40 +02:00
alexandClaude Sonnet 4.6 94f3e6bca8 fix install.bat: cd to script dir so environment.yml is found
Without cd /d "%~dp0", conda resolves environment.yml relative to
wherever the user launched the bat from (e.g. C:\Windows\system32).

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 23:54:33 +02:00
alexandClaude Sonnet 4.6 6c261c3cbb fix fuzzy matching bug and CI coverage collection
find_best_match was using extractOne (which maximises its scorer) with
Levenshtein distance (lower = better), so it returned the worst match
instead of the best. Switch to extract+min to correctly minimise distance.
Fixes 6 failing tests in test_text_correction.py.

CI: run pytest under `coverage run` so coverage.xml has data to report;
drop deprecated use-only-tar-bz2 flag from setup-miniconda steps.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 23:53:15 +02:00
alexandClaude Sonnet 4.6 6ace0a7970 include install.bat, src, env files in release zip
Adds install.bat, run_botty.bat, run.bat, environment.yml, src/, and
dependencies/ to the build output so users can download zip → run
install.bat → run_botty.bat without needing a pre-built conda env.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 23:43:36 +02:00
alexandClaude Sonnet 4.6 ed4aaf77f8 add install.bat: automates conda env setup from environment.yml
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-05-22 23:40:26 +02:00
alex 416b6714dd fix CI: add PYTHONPATH=./src to all steps 2026-05-22 23:31:27 +02:00
alex ab29fa0a46 fix release bundle: main.exe naming, full zip with config+assets, add release trigger 2026-05-22 23:28:21 +02:00
alex 77d84680c6 fix CI: remove broken pytest-pythonpath, add test→build pipeline, fix test asset guards 2026-05-22 23:24:41 +02:00
alex d0e6eb7d21 update params.ini, bot.py, win_input.py 2026-05-22 21:11:59 +02:00
alex 75f64351ee add stealth system, key detection, click recorder, FOHdin, new routes, and tooling updates 2026-05-21 23:34:20 +02:00
alex 8dc677d2e6 document stealth, key detection, click recorder, and FOHdin in README 2026-05-20 20:06:21 +02:00
alex 8d1a2f20c8 merge main into mine 2026-05-20 20:02:52 +02:00
Alex ce5816a130 Initial commit 2026-05-20 19:55:00 +02:00
alex e9d88f9763 feat: auto-launch D2R on botty startup
- main.py checks if D2R.exe is running, launches with auto-login if not
- Set char_name=Burr in config for OCR character selection
2026-05-20 09:20:37 +02:00
alex 776a09da5d feat: auto-login + OCR character selection
- Added bnet_name, bnet_pass, char_name to [general] config
- Config._build_launch_options() appends -bnetname/-bnetpass when set
- character_select.py: OCR fallback when template matching fails
2026-05-20 09:16:48 +02:00
alex b3052e50b4 feat: auto-login + OCR character selection
- Added bnet_name, bnet_pass, char_name config options
- Launch options auto-append -bnetname/-bnetpass when credentials set
- character_select.py: OCR fallback when template matching fails
- Scans character list row-by-row, matches by name, clicks and scrolls
2026-05-20 08:55:04 +02:00
alex 8575bc4446 fix: npc_auto_label global declaration order 2026-05-20 07:44:39 +02:00
alex 3f6363aec6 feat: enhanced stealth layer - click delay, endpoint wobble, behavior model
- custom_mouse.click(): added 50-800ms arrival-to-click delay (human hesitation)
- custom_mouse.stealth_move(): added endpoint wobble (2-5px micro-adjustment)
- stealth.py: new functions for keyboard timing, skill hesitation, wrong waypoint, skill mistakes
- params.ini: 8 new tunable stealth config options with documented defaults

All 46 files using mouse module automatically benefit — stealth is centralized.
2026-05-20 07:22:24 +02:00
alex bab6e24438 feat: parallel template search, async mouse moves, NPC auto-label
- template_finder.search(): parallel matching via ThreadPoolExecutor (4 workers)
- utils/custom_mouse.py: async_move() for non-blocking mouse movement
- utils/npc_auto_label.py: detect_visible_npcs() scans 16 NPCs in parallel
2026-05-19 22:47:30 +02:00
alex 9bbc753a16 feat: complete key auto-detection with binary .key parser
- Parse D2R binary .key format (10-byte entries, action=1=skill, action=0=non-skill)
- Detect skill slot bindings and validate against params.ini
- Scan Saved Games for any .key/.keyo file (handles battle tag names)
- Config loads cleanly, warns on skill/non-skill mismatches
2026-05-19 18:32:18 +02:00
alex df501d3ea9 docs: improvements reference + d2r-key-detection skill
- Added references/improvements.md tracking all implemented fixes
- Created d2r-key-detection skill for future sessions
2026-05-19 16:50:10 +02:00
alex 5ea1c17902 fix: target false positives, pickit timing, hardcore death loop
- target_detect: add aspect ratio filtering to reject health bars and immune text
- pickit: add 0.2-0.3s wait after pickup before moving on (fix #939)
- game_controller: detect hardcore mode and stop instead of infinite death loop
- config: add 'hardcore' flag (default 0)

Closes #959, #939, #942
2026-05-19 16:49:24 +02:00
alex 0af8a54116 feat: auto-detect D2R key bindings from .key/.keyo file
- Read character .key file from Saved Games / D2R install dir / APPDATA
- Auto-fill empty hotkeys in [char] section (inventory, belt, potions, etc.)
- Match skill slots to configured skills for validation
- Normalize key variants (left alt ~ alt, left shift ~ shift)
- params.ini values always take priority over detected bindings
- Warn on genuine mismatches between config and .key file
2026-05-19 16:46:24 +02:00
Alex 23e7e54f8d fix: resolve default.bnip merge conflicts from aeon0; fix raw string in config.py 2026-05-17 20:31:38 +02:00
Alex b52f7ebca9 fix: defer npc_manager template loading for headless Linux compatibility 2026-05-17 19:25:56 +02:00
Alex 329b7e77d9 fix: guard Windows-only imports for Linux compatibility (pywin32, mouse._winmouse, mss, rapidfuzz) 2026-05-17 17:56:29 +02:00
Alex 5ab9d1c68a feat: implement boss kill methods for fire_lock, hammerdin, fohdin + resolve params.ini merge conflicts
- Add kill_andariel(), kill_countess(), kill_mephisto(), kill_baal(), kill_baal_waves() to fire_lock (warlock), hammerdin, and fohdin (paladin)
- Use build-appropriate attack patterns (deathmark/chaos combos for fire_lock, hammers/FOH for paladins)
- Add configurable attack durations via atk_len_* in params.ini
- Resolve remaining merge conflicts from prebuff cherry-picks
- Add boss run config section: atk_len_andariel, atk_len_countess, atk_len_mephisto, atk_len_baal, atk_len_baal_waves
2026-05-17 15:53:08 +02:00
Alex 15a90bfd47 feat: add route scaffolding tool and contributor guide 2026-05-17 15:22:24 +02:00
Alex 17f32e65b2 feat: add 4 new boss runs (Andariel, Countess, Mephisto, Baal)
Full run implementations with approach/battle methods following the
existing botty pattern. Added 12 Location constants to pather.py,
12 placeholder path nodes to game.ini (coords recorded in-game later),
and 6 new kill_* methods to IChar interface. Updated params.ini
with new route documentation.

Path nodes are 0,0 placeholders — record real coords with
node_recorder.py in-game before enabling these routes.
2026-05-17 14:25:26 +02:00
Alex b397d07baf feat: implement 4 new boss runs (Andariel, Countess, Mephisto, Baal)
Full run implementations following existing botty patterns:
- andariel.py: Act 1 Catacombs L2→L3→L4, waypoint-based approach
- countess.py: Act 1 Forgotten Tower L1→L5 via Black Marsh WP
- mephisto.py: Act 3 Durance of Hate L2→L3 via A3 WP
- baal.py: Act 5 Throne of Destruction, wave clearing + boss

Added 12 Location constants to pather.py and 12 placeholder
path nodes to game.ini (record in-game with node_recorder.py).

Path node coordinates are 0,0 placeholders — must be recorded
in-game before runs can actually execute.
2026-05-17 14:24:46 +02:00
Alex eee99c069b feat(stealth): wire should_skip_run, maybe_afk_break, reshuffle_each_rotation into bot state machine
- on_end_run: calls maybe_afk_break() after returning to town
- run selection loop: checks should_skip_run() before each run
- on_init: reshuffles _do_runs_reset if reshuffle_each_rotation is enabled

Also scaffolded 4 new run stubs: baal, mephisto, andariel, countess
2026-05-17 14:07:01 +02:00
Alex 81ee6557f4 feat: add route scaffolder CLI, smoke tests, stealth docs 2026-05-17 13:26:31 +02:00
Alex f484a9bef1 feat(stealth): add anti-detection layer with wait jitter, click variance, AFK breaks, and route skip 2026-05-17 13:26:26 +02:00
RedJon18 6d19819de5 Prebuff updates and rare items for pickit logic
Updates the prebuffs for paladin to use holy shield before entering Pindleskin's portal. Also, adds additional pickit logic for picking up and storing all rare items when uncommenting the sections.
2026-05-17 13:21:42 +02:00
aeon0 71311cb584 fix not stashing gold if buying sth before 2026-05-17 13:21:14 +02:00
Riddle 5c4f62ac25 Add @cache to functions 2026-05-17 13:18:14 +02:00
Riddle 869de31315 fixed error code 2026-05-17 12:49:00 +02:00
Randi 6f6c663b43 Added eldritch routine to firelock. Also made minor adjustments to a3 error finding stash to reduce error rate. 2026-03-24 22:51:06 -04:00
Randi fdd0fc2230 Adjusted a3_stash_left pic as it could misclick cain on occasion. 2026-03-23 13:58:40 -04:00
Randi 5d5a9c2a63 Made improvements to resolve issues with finding stash in a3. Created left and right stash pictures to reduce issues of being covered. Also implemented move on first failure in an attempt to move merc. 2026-03-23 13:38:41 -04:00
Randi eb75fc9aee Made adjustment to determining if stash is full of gold. Also remade inv_no_gold picture as it would detect numbers starting with 91 as 0. 2026-03-23 12:11:27 -04:00
Randi 600904e6bc Reverted edge case for handling not finding stash. 2026-03-22 21:30:10 -04:00
Randi ce821695e8 Minor tweak to handle case when stash is covered by merc. 2026-03-22 21:17:24 -04:00
Randi 455fe8e64f Minor adjustments to warlock. 2026-03-22 20:44:23 -04:00
Randi f4bc3b7401 Added trav run to fire warlock and added ability to buff in town with consume. 2026-03-22 10:39:23 -04:00
Randi b528957c40 Minor tweak to cta functionity to disable buff_with_cta if cta_available is false. 2026-03-20 21:32:32 -04:00
Randi f8c9d81f52 Minor tweak to static. 2026-03-20 20:32:40 -04:00
Randi 1a5a261f7c Updated stash inventory roi based on warlock exp updates. 2026-03-19 19:53:07 -04:00
Randi 95e019e88d Updated the Run classes to properly reference the _do_run variables so changes to runs in bot.py will persist into Run classes 2026-03-19 19:09:39 -04:00
Randi 466197fd8b Fixed bug where bot would always check stash when gold was full even when stash_gold is disabled. 2026-03-18 00:20:21 -04:00
Randi 4a4bf0300a Added extra failsafe to ensure no keys get left held down when game controller kills bot thread. It was possible that a max length violation could occur while stashing and cauase ctrl and shift to get stuck down. 2026-03-17 20:35:59 -04:00
Randi 953b9aedb3 Corrected transmute to correctly keep track of transmute counter after x games. 2026-03-17 07:38:57 -04:00
Randi 2ce9636d3a Added additional delay after cta to swap weapons 2026-03-17 07:36:41 -04:00
Randi b03b2fd226 Refined cta and buffing so chars now use cast_buffs routine which doesn't need the cta_available logic. This routine also allows the casting delay to be kept separate from cta as the casting_delay is passed in. Failed buffs will now cause bot to exit game. 2026-03-16 20:42:15 -04:00
Randi d96eb2d34c Fixed bug when bot is killed due to max game length violation while transmuting. Transmuting should now complete before game exits. 2026-03-16 19:10:07 -04:00
Randi 5f2149ed1e Removed last node from trav run as it didn't seem necessary for TP and took extra time. 2026-03-16 18:42:24 -04:00
Randi a7509c90e7 Added functionality to transmute flawless gems from the stash gem tab. Works with the transmute every x games variable in params.ini 2026-03-15 23:24:56 -04:00
Randi 8b48b583b9 Fixed issue in which CTA was not properly detected if bot is started with incorrect weapon swap. 2026-03-15 20:13:03 -04:00
Randi 58f761c372 Added logic if doing trav runs we will skip additional runs if juvs are full. 2026-03-15 10:01:39 -04:00
Randi 36d364517b Reduced telek pickup range as it can fail if item is off screen but text is not. 2026-03-14 20:17:03 -04:00
Randi 869eeb5539 Added transmute routine for graphic debugger which requires manual uncommenting. May need to find a more automated way in future. 2026-03-14 08:42:53 -04:00
Randi b2afbaab93 Minor tweaks to pickup 2026-03-13 22:21:47 -04:00
Randi 9512856cfc Refined item pickup to properly tele, use telekinesis, and reduce delays when walking for pickup. 2026-03-13 20:22:25 -04:00
Randi 593e509bc6 Added gheed's wager, horizon boots, and sunders to be picked up. Also fixed error that was causing set and unique names to be improperly reported. 2026-03-11 19:14:51 -04:00
Randi 68ed191efc Made fix to ensure the new warlock expansion uniques would get stashed. 2026-02-27 20:43:31 -05:00
Randi 77925964a0 Fixed in town healing to trigger based on juv settings. Added error case for failed to join game. 2026-02-25 20:34:01 -05:00
Randi 665e6b6ce4 Minor bnip config update to add twink items. 2026-02-20 17:36:47 -05:00
Randi 51306f3067 Fixed the old graphics debugger. 2026-02-20 13:55:35 -05:00
Randi c47d9573ad Updated pick up code to accept red text to allow worldstone shards to be picked up. 2026-02-20 10:41:02 -05:00
Randi bbe5395f4a Added worldstone shards to be stashed. 2026-02-19 23:38:50 -05:00
Randi 6f7a9b681d Added all unique grimoires to default bnip config file. 2026-02-17 22:39:49 -05:00
Randi ed0a8270cf Added grimories to bnip files. 2026-02-17 22:22:14 -05:00
Randi 4b3a47b13c Updated bnip_data.py to add abyss skill to bnip. 2026-02-16 22:08:47 -05:00
Randi 6f4f4ca472 Updated bnip files to support warlock skillers. 2026-02-16 21:55:48 -05:00
Randi 55f7d86b3d Tweaked stash gold button roi as it moved in expansion. 2026-02-16 16:33:40 -05:00
Randi 01f03ea9af Handle the error case if save_and_exit fails. 2026-02-16 15:04:00 -05:00
Randi 82ac44c4c1 Minor change to abyss warlock targeting on pindle. 2026-02-15 22:24:13 -05:00
Randi 65ee5a6b5c Updated bnip for echo strike bases 2026-02-15 21:51:19 -05:00
Randi b7c83e6208 Added preliminary warlock characters for bot. 2026-02-15 20:49:12 -05:00
Randi f2c639fc64 Minor fix to a typo for void base entry in bnip 2026-02-15 18:58:08 -05:00
Randi 0978258513 Minor fix to bnip. 2026-02-15 15:19:06 -05:00
Randi b243aec74f Added void base to bnip 2026-02-15 14:24:31 -05:00
Randi ec5a7e4292 Fixed issue with a chicken not counting as a failed game. 2026-02-15 09:54:48 -05:00
Randi 4852ef9e3a Adjusted fast save and exit to work properly with new loot filter buttons 2026-02-15 09:36:51 -05:00
Randi d7a1d65348 Minor cleanup with tabs, and added a few entries to bnip. 2026-02-15 09:12:57 -05:00
Randi 758a0a3db8 Missed adding the transmute.py file in last commit for tabs. 2026-02-14 23:39:35 -05:00
Randi b27911adae Updated stash tabs to work with Warlock expansion. 2026-02-14 23:26:15 -05:00
Randi 43e5a8ecab Fixed the missing string issue and inventory tab names. 2026-02-14 20:17:05 -05:00
Randi b52ed15079 Updated barbarian prebuff delays, horking, and added howling. 2025-11-15 19:05:18 -05:00
Randi c82051f276 Adjusted barbarian to use leap attack skill. 2025-11-15 15:39:57 -05:00
Randi 6fc80f0a96 Fixed fast_save_and_exit routine if menu was alreay openned. 2025-11-15 14:17:54 -05:00
Randi 2da55e138c Added fast save/exit for game controller and synchronized exit with health manager. 2025-11-12 19:36:06 -05:00
Randi 5cbf7ea366 Removed synchronizatino of exit routine as it can cause issues due to threads getting terminated. 2025-11-11 21:55:53 -05:00
Randi a2d27fd4c3 Prevent chicken from being called twice in one loop. 2025-11-11 08:09:03 -05:00
Randi be446fc9d9 Minor comment update. 2025-11-10 23:34:30 -05:00
Randi 4b93a50927 Updated uses between juvs to be 16frames, the max recovery time, to avoid wasted juvs. 2025-11-10 22:49:37 -05:00
Randi 76cbe0abbc Synchronized the exit routines so two threads exiting the game don't stomp on each other. 2025-11-10 21:03:48 -05:00
Randi a54b674ce6 Updated chicken to save and exit faster. 2025-11-10 20:23:03 -05:00
Randi 11440a3d24 Hardcoded numpy in environment.yml as latest version causes issue. 2025-11-08 11:15:49 -05:00
Randi 5f478a6884 Updated development.md to reflect the correct git location for this fork. 2025-11-08 10:49:03 -05:00
Randi fb2edcf70a Minor fixes and tweaks to trav path. 2025-11-07 21:22:54 -05:00
Randi ed2a921d9e Fixed bug where bad inventory check would cause thread exception. 2025-11-07 21:22:13 -05:00
Randi ba8a412309 Updated item distance to make tele pickup further away as it is not reliable when items bunch up. 2025-11-07 21:21:09 -05:00
Randi 5d42a46891 Added trav attack routine for blizzorb sorc. 2024-09-14 21:18:19 -04:00
Randi c13367f221 Added large charms to bnip file for early ladder season. 2024-09-13 22:37:37 -04:00
Randi c94be67727 Minor tweak to nova sorc trav run. 2024-09-13 22:36:53 -04:00
Randi 1e8bb16a22 Added large charms to bnip for early ladder season. 2024-09-13 20:15:19 -04:00
Randi 3e87fe9d6f Simplified nova trav attack pattern and made it quicker. 2024-09-13 20:03:35 -04:00
Randi b517a80bea Fixed gull dagger in bnip. 2024-09-11 21:26:29 -04:00
Randi b2938e6297 Added crescent moon and doom bases to bnip file. 2024-09-11 20:46:44 -04:00
Randi 3a96bf69cc Enabled frostburns in pickit for early ladder season 2024-09-11 20:35:43 -04:00
Randi 429a1cd4ae Removed uneccesary statics from nova sorc pindle/trav runs. 2024-09-11 20:28:23 -04:00
Randi dab2fd9259 Fixed bug in bnip file that discarded war travs and death's fathom. Also enabled all early ladder season items. 2024-09-11 20:25:33 -04:00
Randi 8048d87222 Minor config file updates 2024-08-25 21:19:25 -04:00
Randi edcefbe62c Added config updates for blizzorb sorc. 2024-08-23 18:22:22 -04:00
Randi aea75aa417 Minor trapsin changes. 2024-08-23 18:19:50 -04:00
Randi 30d407e978 Reworked trapsin a bit to include casting/attack delays between skills. Also added mindblast and added to pindle run. 2024-06-26 19:59:50 -04:00
Randi 8df42bbcb1 Added attack frames into config.py 2024-06-24 22:04:58 -04:00
Randi da396208f0 Added attack frames to config and character class. 2024-06-24 21:02:34 -04:00
Randi 466c698b1d Updated Anya shopper to properly filter on +3 3/20 gloves. Also added MA 3/20 gloves in addition to the current jav gloves. 2024-06-24 20:54:15 -04:00
Randi 58bc20ce0e Made fixes to anya shop for shopping 3/20 jav gloves as previous implementation used old font which didn't work. 2024-06-20 19:50:53 -04:00
Randi 25fdd5485d Added action frame to paladin for accurate timeing with foh. Also added prelimnary foh clear screen sequence. 2024-06-18 19:51:25 -04:00
Randi bfccf60a07 Added testing attack routines for cs and summoner for blizzorb. 2024-06-18 19:42:32 -04:00
Randi 8be1223e32 Added javazon 2024-06-18 00:25:16 -04:00
Randi df1259f87c Fixed bug where health manager would drink rejuv back-to-back. 2024-06-16 20:32:49 -04:00
Randi 1ca3578e06 minor bug fixes to vizier run 2024-06-16 13:25:55 -04:00
Randi 3c70214ca9 Cleaned up the vizier runa and removed commented code. 2024-06-16 12:33:20 -04:00
Randi 8c44d7ad17 Added new vizier run to just kill vizier is cs, as diablo run has high failure rate. 2024-06-16 12:27:33 -04:00
Randi 4199705fe8 Expanded scope of thread lock in screen::grab() method. 2024-06-13 19:59:35 -04:00
Randi 7141e671d1 Fixed issue in which tele paladin would occasionally walk from eldritch to shenk. 2024-06-13 19:57:24 -04:00
Randi c20ec7343c Minor tweaks to blizzorb sorcs offsets for attacks. Also added in summoner attack routine. 2024-06-12 23:45:27 -04:00
Randi 9a714b969e Made fix to pickit in which it did not properly read physical damage reduction attributes. Caused automatic discard for ss, verdungo, soe etc. 2024-06-12 23:40:09 -04:00
Randi d518389493 Added shenk and nilathak runs to blizzorb sorc. 2024-06-11 21:53:00 -04:00
Randi c6f81c70a6 Fixed bug with health manager that would cause unintended chicken on rejuv use. 2024-06-11 21:51:25 -04:00
Randi 70659b7544 Added new blizzorb sorc that specializes in comboing blizzard with frozen orb. 2024-06-11 19:32:56 -04:00
Randi aa56bbc484 Made fix to paladin.py to support wait_tp parameter in overriden pre_move() method. 2024-06-09 15:39:34 -04:00
Randi 901592a54a Fixed an issue with hammerdin overriden pre_move() method which was missing wait_tp variable. 2024-06-09 14:29:34 -04:00
Randi a51db00a15 Updated pickit to remove edge-case it which last item checked will not attempt retries for pickups. Updated health_manager to never skip chicken check. Also added chicken conditions when out of juvs, or if juvs are used in rapid succession. 2024-06-07 15:24:03 -04:00
Randi 0197298e31 Ensure foh does not have holybolt selected when walking as it causes targeting issues with merc. 2024-06-06 00:46:17 -04:00
Randi a4dada9237 Make fohdin suck less against pindle. 2024-06-05 20:34:37 -04:00
Randi 6b2f719605 Tweaked env file. 2024-05-25 13:24:34 -04:00
Randi ed70c8e688 Tried to fixed issue of failed teleport at start. Fixed issue that prevented potions from being picked up. Fixed item inspection issue with tooltip. 2024-05-23 19:33:11 -04:00
Randi f68ebf0861 Cleaned up readme and development.md with new instructions based on fork. 2024-05-20 13:29:21 -04:00
Randi 39788df29c Set params.ini to match sorc. Updated environment.yml to set dependencies on package versions such as mss 7.0.1. Updated discord_embeds.py to work with latest discord version. Updated save_exit button image. Updated char_state image and roi. 2024-05-19 21:44:00 -04:00
aeon0 f1ea45ce2a Update README.md 2022-07-01 15:06:32 +02:00
aeon0 31a0579f65 Update README.md 2022-07-01 06:47:53 +02:00
aeon0 ad8ea13444 Update README.md 2022-07-01 06:47:23 +02:00
aeon0 5df97c4a7e Update README.md 2022-07-01 06:46:51 +02:00
Legit f5538916fe Merge pull request #958 from noblesigma/master
Update location
2022-06-30 09:35:30 -05:00
noblesigma d7be49edd7 Update location 2022-06-30 07:38:55 -04:00
Riddle 6e10c549e4 Merge pull request #954 from definitelynotsosa/patch-8
Update default.bnip
2022-06-28 06:07:42 -04:00
Sosa 656e52ffff Update default.bnip
Fixing Natures Peace Ring line
2022-06-28 01:51:14 -04:00
mgleed 8c9e98ebd8 add a couple mouse waits 2022-06-27 18:35:36 -04:00
Gleed da09cb820a Feature: More descriptive pather.py output when run from src, support list of template patterns 2022-06-27 15:47:22 -04:00
Legit d45d974280 Merge pull request #949 from definitelynotsosa/patch-7
Update default.bnip
2022-06-26 16:54:50 -05:00
Sosa 613d4f0ff3 Update default.bnip
Minor fix
Corrected Magic Necrohead line
2022-06-26 17:47:17 -04:00
Riddle 8f4942a3a2 Merge pull request #948 from bottytools/bnip-incorrect-reading
fixes #936
2022-06-26 10:54:11 -04:00
Riddle ef34efdd54 Merge branch 'master' of https://github.com/aeon0/botty 2022-06-26 10:52:13 -04:00
Riddle 9886f2ee96 fixes #936 2022-06-26 10:52:07 -04:00
Riddle 3e4c0d8fa5 Merge pull request #947 from bottytools/encode-bnip-files
Add encoding to .bnip files
2022-06-26 07:36:46 -04:00
Riddle f72856a8bd Add encoding to .bnip files 2022-06-26 06:26:23 -04:00
mgleed 980a8f5036 get rid of redundant id_items param 2022-06-24 20:28:41 -04:00
Riddle 9f1a4772cd Merge pull request #926 from bottytools/nip-to-bnip
Replace nip with bnip
2022-06-24 13:37:19 -04:00
Riddle c68d92c8e1 Merge branch 'master' of https://github.com/aeon0/botty into nip-to-bnip 2022-06-24 13:36:53 -04:00
Riddle 076737672e Merge branch 'master' into nip-to-bnip 2022-06-24 13:36:31 -04:00
Gleed 689a93a3f7 Bugfix: Make character name OCR not freeze program if fails, also improved read 2022-06-23 17:01:45 -04:00
mgleed 9ed7869ddd fix typo 👀 2022-06-23 15:13:10 -04:00
Gleed c62c237442 Bugfix: template_finder.search_all() messing with input arguments (#931)
* init

* remove print
2022-06-23 14:53:58 -04:00
Legit 4e10dbb2c7 Updated staves and nip errors (#932)
Corrected white staves and other syntax errors.
2022-06-23 14:49:45 -04:00
Gleed 197ff41e69 Fix Fix: template_finder.search() tests were terminating after first "assert" 2022-06-23 13:42:39 -04:00
Wang Xiang 5089abaca6 Update default.nip Shadowplates (#927)
Add expressions for Shadowplates in Armor Bases section.
2022-06-23 07:13:59 -04:00
jobithu 8469d51f0c Merge pull request #928 from xw220/improve/diablo_sealdance
Update diablo.py _sealdance() -reviewed and approved. also implemented for PR867
2022-06-23 13:11:25 +02:00
Wang Xiang 9b2d52a39b Update diablo.py _sealdance()
I watched 10 hours of diablo run by 4 fohdins. About 30% were fail games which aborted in middle run, especially on detecting that if the seal is openned.  In almost every fail game caused by this reason, the seal was openned successfully, but the decisions were closed for 5 times and the game was aborted. By increasing the numbers of try and reduce the seal_opentemplates threshold to 0.7, the incorrect judgements on seal's open status seldom occur, and the ratio of failed games typically reduced. Now it is about 12% mostly caused by temporary high latency or getting lost after teleport to pick item.
2022-06-23 17:51:40 +08:00
jagarop d95b581dae Merge pull request #920 from definitelynotsosa/patch-5
Update default.nip
2022-06-22 09:42:54 +00:00
Riddle 759c4fff82 Remove the test321.. 2022-06-22 05:39:06 -04:00
Riddle d5bdb769b3 Replace nip with bnip 2022-06-22 05:37:54 -04:00
Riddle 8a1a15d77d Merge pull request #925 from bottytools/nip-error-update
Added better expections for bnip
2022-06-22 04:41:16 -04:00
Riddle 24d2da8e67 Added better expections for bnip 2022-06-22 04:24:04 -04:00
aeon0 6d1779ea5f fix not stashing gold if buying sth before (#922) 2022-06-22 09:59:14 +02:00
Riddle 0cbc5f759e Merge pull request #924 from bottytools/nip-type-checking
fixed liner errors for nip files
2022-06-22 02:57:01 -04:00
Riddle ce1d99bfd8 fixed liner errors 2022-06-22 02:47:54 -04:00
Riddle 257d687166 Merge pull request #923 from bottytools/nip-fix-subtraction-2
fixed
2022-06-22 02:30:27 -04:00
Riddle cfc192e1dc fixed 2022-06-22 02:28:47 -04:00
Riddle 161b9226fb Merge pull request #921 from bottytools/nip-ntipaliasstatkeyword-fix
Nip ntipaliasstatkeyword fix
2022-06-22 02:27:11 -04:00
Riddle 46b8d9bf17 Remove prints / replace ntipaliasvalue to use keyword 2022-06-22 02:17:32 -04:00
Riddle 9faa3fd25a forgot to replace in transpile.py 2022-06-22 02:10:06 -04:00
Riddle bca0471d9f removed print statements 2022-06-22 01:01:40 -04:00
Riddle 5610490db6 removed print statements 2022-06-22 01:01:23 -04:00
Riddle 5efebd0bb7 fixed missing keyword... o-o 2022-06-22 01:00:13 -04:00
Sosa 81bc9028d1 Update default.nip
Fixing white claw bases
2022-06-22 00:43:49 -04:00
Gleed 3de9f6db93 Bugfix: Pickit IndexError 2022-06-21 22:33:27 -04:00
mgleed 0f08cd8e93 bump version 2022-06-21 20:07:25 -04:00
329 changed files with 35747 additions and 3540 deletions
+13
View File
@@ -1,8 +1,21 @@
# .coveragerc to control coverage.py
[run]
branch = True
source = src
# Exclude conda/xonsh internal shims that leak into coverage data. The phantom
# is an absolute path like D:\a\...\config-3.py, so a bare "config-3.py" omit
# never matches — use a glob that matches any path ending in config-<n>.py.
omit =
*config-*.py
*/site-packages/*
*/conda-meta/*
[xml]
output = coverage.xml
[report]
# Ignore missing source files (conda shims reference files not in tree)
ignore_errors = True
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
-1
View File
@@ -1 +0,0 @@
PYTHONPATH=./src
+25
View File
@@ -0,0 +1,25 @@
# Botty personal environment overrides.
# Copy this file to ".env" in the repo root and edit your values there.
# ".env" is ignored by git and should never be committed.
#
# Format: BOTTY_<CONFIG_KEY>=<value>
# These override values from config/params.ini and config/custom.ini at runtime.
#
# Common personal settings:
# Bot display/account name used in logs/Discord usernames.
# BOTTY_NAME=zapzap
# Main Discord webhook (status/death/chicken/general messages).
# BOTTY_CUSTOM_MESSAGE_HOOK=https://discord.com/api/webhooks/...
# Optional dedicated loot webhook for item drops.
# BOTTY_CUSTOM_LOOT_MESSAGE_HOOK=https://discord.com/api/webhooks/...
# Optional auto-login credentials (keep private).
# BOTTY_BNET_NAME=your-battlenet-email-or-name
# BOTTY_BNET_PASS=your-battlenet-password
# BOTTY_CHAR_NAME=your-character-name
# Optional path override if you reference it in params.ini:
# BOTTY_SAVED_GAMES_FOLDER=C:\Users\you\Saved Games\Diablo II Resurrected
+151 -36
View File
@@ -1,52 +1,167 @@
name: Botty - CI
on: [pull_request]
on:
workflow_dispatch:
pull_request:
push:
branches: [main, mine]
# Pushing a version tag (e.g. `git tag v0.8.5 && git push --tags`) builds,
# smoke-tests, then creates the GitHub release with the zip attached — all
# in one run. If the build fails, no release is ever created.
tags: ['v*']
# Default GITHUB_TOKEN is read-only; the build job's release step needs
# contents:write to create the release and attach the built zip
# (else HTTP 403 "Resource not accessible by integration").
permissions:
contents: write
jobs:
tests:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Setup Miniconda Python 3.10
uses: actions/setup-python@v2
with:
python-version: '3.10'
# TODO: The below setp only chaches conda packages, need to cache pip seperatly
- name: Cache conda
uses: actions/cache@v2
env:
# Increase this value to reset cache if environment.yml has not changed
CACHE_NUMBER: 1
with:
path: ~/conda_pkgs_dir
key: conda-${{ env.CACHE_NUMBER }}-${{hashFiles('environment.yml') }}
- name: Cache pip
uses: actions/cache@v2
env:
# Increase this value to reset cache if environment.yml has not changed
CACHE_NUMBER: 1
with:
path: ~/pip
key: pip-${{ env.CACHE_NUMBER }}-${{ hashFiles('environment.yml') }}
- name: Install Miniconda
uses: conda-incubator/setup-miniconda@v2
uses: conda-incubator/setup-miniconda@v3
with:
python-version: '3.10'
activate-environment: botty
channel-priority: strict
environment-file: environment.yml
use-only-tar-bz2: true # Needed for caching
- name: Activate conda
shell: powershell
run: |
C:\Miniconda\condabin\conda.bat init powershell
set PYTHONPATH=./src
- name: Pytest & Coverage
environment-file: environment-win11.yml
use-only-tar-bz2: false
- name: Python version
shell: powershell
run: |
C:\Miniconda\condabin\conda.bat activate botty
python -c "import sys; print(sys.version)"
coverage run --source=./src -m pytest -v -s
- name: Coverage Report Generation
- name: Syntax check
shell: powershell
env:
PYTHONPATH: ./src
run: |
$ErrorActionPreference = "Stop"
C:\Miniconda\condabin\conda.bat activate botty
python -m compileall -q src tools test scripts
- name: Tests
shell: powershell
env:
PYTHONPATH: ./src
RUN_ENV: test
run: |
$ErrorActionPreference = "Stop"
C:\Miniconda\condabin\conda.bat activate botty
coverage run -m pytest -v
- name: Coverage report
shell: powershell
env:
PYTHONPATH: ./src
run: |
$ErrorActionPreference = "Stop"
C:\Miniconda\condabin\conda.bat activate botty
coverage xml --ignore-errors
build:
needs: test
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup Miniconda Python 3.10
uses: conda-incubator/setup-miniconda@v3
with:
python-version: '3.10'
activate-environment: botty
channel-priority: strict
environment-file: environment-win11.yml
use-only-tar-bz2: false
- name: Install Tesseract (bundled into the release for click-and-run OCR)
shell: powershell
run: |
$ErrorActionPreference = "Stop"
choco install tesseract --no-progress -y
if (-not (Test-Path "C:\Program Files\Tesseract-OCR\tesseract.exe")) {
throw "Tesseract install did not produce tesseract.exe"
}
- name: Build exe
shell: powershell
env:
BOTTY_NO_RENAME: '1'
PYTHONPATH: ./src
run: |
C:\Miniconda\condabin\conda.bat activate botty
coverage xml
python build.py --conda_path C:\Miniconda
- name: Verify Tesseract was bundled
shell: powershell
run: |
$ErrorActionPreference = "Stop"
$BOTTY_DIR = Get-ChildItem -Directory -Name | Where-Object { $_ -match '^botty_v' } | Select-Object -First 1
$tess = Join-Path $BOTTY_DIR "tesseract\tesseract.exe"
if (-not (Test-Path $tess)) { throw "Tesseract was not bundled into $BOTTY_DIR" }
Write-Host "Bundled: $tess"
- name: Launch smoke test (built executables)
shell: powershell
run: |
$ErrorActionPreference = "Stop"
$BOTTY_DIR = Get-ChildItem -Directory -Name | Where-Object { $_ -match '^botty_v' } | Select-Object -First 1
if (-not $BOTTY_DIR) { throw "No botty_v* build directory found." }
$mainExe = Join-Path $BOTTY_DIR "main.exe"
$shopperExe = Join-Path $BOTTY_DIR "shopper.exe"
if (-not (Test-Path $mainExe)) { throw "Missing $mainExe" }
if (-not (Test-Path $shopperExe)) { throw "Missing $shopperExe" }
$procs = @()
try {
$mainProc = Start-Process -FilePath $mainExe -PassThru -WindowStyle Hidden
Start-Sleep -Seconds 6
if ($mainProc.HasExited) { throw "main.exe exited early with code $($mainProc.ExitCode)" }
$procs += $mainProc
$shopperProc = Start-Process -FilePath $shopperExe -PassThru -WindowStyle Hidden
Start-Sleep -Seconds 6
if ($shopperProc.HasExited) { throw "shopper.exe exited early with code $($shopperProc.ExitCode)" }
$procs += $shopperProc
}
finally {
foreach ($p in $procs) {
if ($p -and -not $p.HasExited) {
Stop-Process -Id $p.Id -Force
}
}
}
- name: Prepare release zip
shell: powershell
run: |
$BOTTY_DIR = Get-ChildItem -Directory -Name | Where-Object { $_ -match '^botty_v' } | Select-Object -First 1
Write-Host "Botty dir: $BOTTY_DIR"
Get-ChildItem -Path $BOTTY_DIR -Recurse | Select-Object -Property FullName, Length
$ZIP = "${BOTTY_DIR}.zip"
Compress-Archive -Path "${BOTTY_DIR}\*" -DestinationPath $ZIP -Force
Write-Host "Release zip: $ZIP"
Get-Item $ZIP | Select-Object -Property FullName, Length
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: botty-build
path: botty_v*/
retention-days: 7
# On a version-tag push, create the release (if absent) and attach the
# zip atomically. Runs only after the build + smoke test above succeed.
- name: Create release and upload zip
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: botty_v*.zip
fail_on_unmatched_files: true
generate_release_notes: true
+57 -6
View File
@@ -15,18 +15,58 @@ botty_v*/
custom.ini
config/custom.ini
config/custom.*.ini
config/*.local.ini
.vscode
.vs/
test/assets/
# Logs and run reports
log/
log/archive/
log/runs/
log/stats/
*.log
# Scraped data (cached web pages, URLs)
data/d2jsp_pages/
data/d2jsp_topic_urls.txt
# Debug files
debug_forum.html
# Generated/runtime price data (updated automatically by bot)
config/fg_daily_estimates.json
config/fg_prices.json
config/fg_prices.db
config/daily_prices.json
config/daily_prices_history.json
config/traderie_prices.json
# Screenshots (debug/test captures)
screenshots/
# Tools dev cache
tools/__pycache__/
# Build artifacts
botty_v*/
botty_v*.zip
.venv
.env
.env.*
!.env.example
*.bak
.coverage
htmlcov/
# Secrets / account-specific — never commit
cookies.txt
cookies_temp*
*.cookies
config/custom.ini
coverage.xml
utils/live-view/
config/nip/*
!config/nip/.gitkeep
config/bnip/*
!config/bnip/.gitkeep
stats/
info_screenshots/
item_error_screenshots/
@@ -34,5 +74,16 @@ loot_screenshots/
pickit_screenshots/
*info_log_parsed.txt
*info_*.png
*.log
log/
blizzpw.txt
# Local user/workspace files
.claude/
GEMINI.md
test_config_load.py
# Debug directory
botty_debug/
# per-user character profiles (survive git pulls)
config/profiles/
config/active_profile.txt
+140
View File
@@ -0,0 +1,140 @@
# Botty - Open Issues & TODO Plan
Generated: 2026-06-01
---
## HIGH PRIORITY (affects botting reliability)
### 1. BNIP transpiler broken code (`src/bnip/transpile.py:369`)
- **Status:** `# TODO FIX THIS SHIT``remove_quantity()` function is hacky
- **Problem:** Expression splitting by `#` is fragile; can corrupt BNIP expressions with multiple `#` delimiters
- **Impact:** Pickit rules may silently misparse quantity operators
- **Fix:** Rewrite `remove_quantity()` to properly handle `#`-delimited expressions with edge cases
- **File:** `src/bnip/transpile.py`
### 2. FOH cast delay missing (`src/char/paladin/fohdin.py:99`)
- **Status:** `# TODO: add delay between FOH casts--doesn't properly cast each FOH in sequence`
- **Problem:** FOH casts fire too fast; some casts don't land in sequence
- **Impact:** Reduced DPS, wasted FOH rotations
- **Fix:** Add `wait()` between FOH casts to ensure each cast completes before next
- **File:** `src/char/paladin/fohdin.py`
### 3. Chest opening telekinesis workaround (`src/chest.py:51`)
- **Status:** `# TODO: Act as picking up a potion to support telekinesis`
- **Problem:** Chest open simulates potion pickup to work around telekinesis skill
- **Impact:** Fragile interaction; may break with game updates
- **Fix:** Implement proper chest interaction that accounts for telekinesis
- **File:** `src/chest.py`
### 4. Inventory full handling (`src/item/pickit.py:236`)
- **Status:** `#TODO Create logic to handle inventory full`
- **Problem:** When inventory fills, pickit just stops — doesn't try to stash, sell, or prioritize
- **Impact:** Bot stops picking up items mid-run; lost gold/runes
- **Fix:** Add fallback logic: stop picking, trigger town run to stash/sell
- **File:** `src/item/pickit.py`
### 5. Overburdened handling (`src/ui/view.py:109`)
- **Status:** `#TODO: handle "Overburdened"`
- **Problem:** `pickup_corpse()` doesn't detect "Overburdened" state after clicking
- **Impact:** Bot may get stuck trying to pickup corpse when overweight
- **Fix:** Add template detection for "Overburdened" UI and handle gracefully
- **File:** `src/ui/view.py`
---
## MEDIUM PRIORITY (code quality / edge cases)
### 6. BNIP parenthesis cross-section check (`src/bnip/transpile.py:199`)
- **Status:** `# TODO Backtrace until the last opening to make sure it wasn't from the past section.`
- **Problem:** Parenthesis validation doesn't catch `(` in one BNIP section and `)` in another
- **Impact:** Silent BNIP syntax errors that pass validation
- **Fix:** Implement backtrace to reject cross-section parentheses
- **File:** `src/bnip/transpile.py`
### 7. BNIP lexer misplaced checks (`src/bnip/lexer.py:319`)
- **Status:** `# TODO: The second checks seem a little misplaced`
- **Problem:** `NTIPAliasClass` and `TokenType.CLASS:` checks should be in transpiler validation, not lexer
- **Impact:** Code organization; potential missed validation
- **Fix:** Move validation logic to `transpile.py` and emit warnings
- **File:** `src/bnip/lexer.py`, `src/bnip/transpile.py`
### 8. BNIP actions error handling (`src/bnip/actions.py:209`)
- **Status:** `# TODO look at these errors`
- **Problem:** BNIP load errors are printed but not properly logged or categorized
- **Impact:** Hard to diagnose BNIP parse failures
- **Fix:** Replace `print()` with proper `Logger.error()` and structured error reporting
- **File:** `src/bnip/actions.py`
### 9. Pickit return type (`src/item/pickit.py:165`)
- **Status:** `TODO :return: return a list of the items that were picked up`
- **Problem:** Docstring says it should return a list, but function returns `bool`
- **Impact:** Inconsistent API; callers can't know what was actually picked
- **Fix:** Return `list[Item]` of picked items instead of `bool`
- **File:** `src/item/pickit.py`
### 10. Consumable auto-belt (`src/inventory/personal.py:350`)
- **Status:** `# TODO: logic for trying to add potion to belt if there are needs`
- **Problem:** Consumables found during inventory management aren't auto-added to belt
- **Impact:** Bot doesn't restock belt potions from inventory during runs
- **Fix:** Add logic to detect belt needs and move potions from inventory
- **File:** `src/inventory/personal.py`
### 11. Merc blocking templates (`src/run/nihlathak.py:45`)
- **Status:** `# TODO: We might need a second template for each option as merc might run into the template`
- **Problem:** Merc can stand on template match location, causing detection failure
- **Impact:** Nihlathak run fails to detect layout variant
- **Fix:** Add backup templates with offset ROIs for each layout variant
- **File:** `src/run/nihlathak.py`
---
## LOW PRIORITY (cleanup / refactoring)
### 12. Character select cleanup (`src/ui/character_select.py:109`)
- **Status:** `# TODO: can cleanup logic here, can we utilize a generic ScreenObject or use custom locator?`
- **Problem:** Character selection uses ad-hoc template search instead of reusable ScreenObject
- **Fix:** Refactor to use `ScreenObjects` pattern
- **File:** `src/ui/character_select.py`
### 13. Screen utility functions (`src/screen.py:104`)
- **Status:** `# TODO: Move the below funcs to utils(?)`
- **Problem:** `convert_monitor_to_screen()` and related functions live in `screen.py` but could be in utils
- **Fix:** Move coordinate conversion functions to `src/utils/`
- **File:** `src/screen.py`
### 14. Graphic debugger re-init (`src/utils/graphic_debugger.py:60`)
- **Status:** `# TODO: these two layers variable needs to be reassigned because F10 will not re-init`
- **Problem:** Debugger layers don't reinitialize properly on F10 toggle
- **Fix:** Move layer state into controller class; reinit on stop/start
- **File:** `src/utils/graphic_debugger.py`
### 15. mttkinter logging (`src/utils/mttkinter.py:62-63`)
- **Status:** `# TODO: Replace custom logging functionality with standard logging.Logger`
- **Problem:** Custom logging in tkinter utils instead of standard library
- **Fix:** Replace with `logging.Logger`
- **File:** `src/utils/mttkinter.py`
### 16. Pickit test note (`test/nip/keep_item_test_cases.py:978`)
- **Status:** `# TODO: I had to change from [defense] >= 47 to [plusdefense] >= 47`
- **Problem:** `[defense]` is a calculated property; `[plusdefense]` is the raw value. Note for future reference.
- **Fix:** Document in BNIP docs that `[defense]` is calculated; `[plusdefense]` is the raw modifier
- **File:** `test/nip/keep_item_test_cases.py`
### 17. New route scaffolding (`src/utils/new_route.py`)
- **Status:** Multiple TODO placeholders (by design — it's a code generator template)
- **Problem:** Template placeholders are intentional; not bugs
- **Fix:** No action needed — these are scaffolding placeholders
- **File:** `src/utils/new_route.py`
---
## Summary
| Priority | Count | Files |
|----------|-------|-------|
| HIGH | 5 | transpile.py, fohdin.py, chest.py, pickit.py, view.py |
| MEDIUM | 6 | transpile.py, lexer.py, actions.py, pickit.py, personal.py, nihlathak.py |
| LOW | 6 | character_select.py, screen.py, graphic_debugger.py, mttkinter.py, keep_item_test_cases.py, new_route.py |
**Total: 17 items across 13 files**
+87
View File
@@ -0,0 +1,87 @@
# Botty Improvements Implementation Plan
## Status Legend
- [ ] Not started
- [~] In progress
- [x] Done
- [-] Cancelled / low priority
---
## Phase 1: Key Auto-Detection (Issue #940)
Read D2R .key file and auto-fill hotkeys.
- [x] Create src/utils/key_detector.py module
- [x] VK code mapping (partial — needs review for accuracy)
- [x] Parse .key file (text format: VK action_type param)
- [x] Auto-fill empty [char] hotkeys from detected bindings
- [x] Auto-fill build-specific skill hotkeys (fohdin, hammerdin, etc.)
- [x] Wire into config.py load_data()
- [ ] REVIEW: Verify VK_MAP accuracy (D2R uses its own VK offset scheme)
- [ ] REVIEW: Skill slot-to-config matching is heuristic — may misassign
- [ ] TEST: Verify against actual D2R .key file on user's machine
## Phase 2: Target Detection False Positives (Issues #959/#964)
Health bars and "immune to X" text mistaken for targets.
- [ ] Analyze current get_visible_targets() in target_detect.py
- [ ] Add shape/size filtering: health bars are thin horizontal strips, immune text is small
- [ ] Add aspect ratio check: real targets (poison/freeze auras) are roughly circular/elliptical
- [ ] Add minimum bounding box height constraint (filter out thin text)
- [ ] Optionally: add color temperature check (immune text is yellow/gold, not blue/green)
- [ ] Test with screenshots of edge cases
## Phase 3: Pickit Timing Fix (Issue #939)
Items skipped because bot teleports away before grabbing.
- [ ] Review pickit.py _yoink_item() for timing issues
- [ ] Add configurable pickup_delay parameter (current: fixed timing)
- [ ] Add retry logic: if item still visible after pickup attempt, re-try
- [ ] Add "slow mode" for large/heavy items (framed/magic items may animate longer)
- [ ] Ensure bot doesn't teleport until pickup animation completes
- [ ] Test: verify no "Attempt to pick xyz" warnings followed by teleport
## Phase 4: Parallel Template Search (Issue #848)
Speed up template_finder.search() with threading.
- [ ] Add ThreadPoolExecutor-based search_all_parallel()
- [ ] Keep existing search() for single-template (no overhead)
- [ ] Only parallelize when searching >3 templates simultaneously
- [ ] Benchmark: measure speedup on typical 1280x720 grab
## Phase 5: Async Mouse Moves (Issue #955)
Non-blocking mouse movement.
- [ ] Add async_move() to utils/custom_mouse.py
- [ ] Run movement in background thread
- [ ] Add is_moving() / wait_for_move() synchronization
- [ ] Integrate into game_controller.py for smoother action chains
## Phase 6: Hardcore Chicken Loop Fix (Issue #942)
Prevent infinite death loops on Hardcore characters.
- [ ] Review death_manager.py chicken logic
- [ ] Add max_chicken_count config parameter (default: 3)
- [ ] If max chicken count exceeded on HC, exit gracefully instead of re-entering
- [ ] Add defensive chicken config option (chicken to TP instead of full chicken)
- [ ] Test: verify HC character exits cleanly after N deaths
## Phase 7: Auto-Label NPCs (Issue #950)
Learn vendor identities automatically during gameplay.
- [ ] During town states, detect NPC name plates via OCR
- [ ] Cross-reference detected names with known NPC list
- [ ] Auto-capture NPC templates when confidence is high
- [ ] Store learned templates in assets/npc/
- [ ] This is a long-term feature — lower priority
---
## Priority Order (implement in this order)
1. **Phase 1** - Key auto-detection (already partially done, needs review + test)
2. **Phase 3** - Pickit timing (high impact on loot collection)
3. **Phase 2** - Target detection (high impact on kill reliability)
4. **Phase 6** - Hardcore chicken fix (safety critical)
5. **Phase 4** - Parallel template search (performance)
6. **Phase 5** - Async mouse moves (quality of life)
7. **Phase 7** - Auto-label NPCs (long-term feature)
+114
View File
@@ -0,0 +1,114 @@
# Dia-run test #2 — session state & plan (2026-06-11 ~20:45)
## OUTCOME (21:29) — ALL FIXES VERIFIED ✅
Full Diablo run completed with every fix live: game 1 (21:14 session) ran Pindle (45s, clean,
no BC double-swap... BC retried once — template marginal, see below) then run_diablo SUCCEEDED
(21:17:28→21:29:04): A5 WP found FIRST TRY at new 0.62 threshold (was 0-for-5 before),
vendor trip skipped by gating, CS layout matched 91.5%, all seals, Diablo killed, game ended
clean at 872s. Implemented beyond the original plan: quick-mode for open_wp (failure 1 → direct
path only on next call; failure 2 → instant fail), sweep 10→6 steps, select timeout 4s, and
A5 threshold drops (stash 0.60/0.45, WP 0.62 — safe because success_func gates every click).
Remaining known marginals (non-blocking): BC/BO skill icon template (1 extra swap ~2s, 2/3 games),
A5_TOWN_0/1 town markers (detect_current_act warns but soft-falls-back), npc body templates
(Larzuk/Cain flaky, fallbacks work), numpy truthiness bug in missing-template debug screenshot
helper, inventory-full pickit skips until next successful stash. Char left at char-select/lobby,
D2R running, no bot processes.
## Goal
User asked: "trigger a full dia run and monitor it for loops and mistakes". A full Diablo run
(WP → ROF → CS → 3 seals → kill) must complete while logging all loops/mistakes, then deliver
an analysis report.
## Current state
- Bot was F12-stopped at 20:37 (was stuck in A5_WP search loop, game 3, char wandered to town wall).
- D2R is OPEN, character "fistman" is IN the stuck game with the **Options→Video menu open**.
- Next immediate steps: Esc out of options, click SAVE AND EXIT at **physical (650, 424)**,
relaunch bot, F11, re-arm monitor, wait for a full dia run (stealth may randomly skip runs).
- After run completes: F12 stop, kill leftover `cmd`/`pwsh` with `run_botty` in commandline,
delete `log/_*.png` and `log/_run2_out.txt` scratch files, deliver findings report.
## How to drive (hard-won specifics)
- Display is 1920x1200 physical, 125% scaling (1536x960 logical). **Use the botty env python**
(`C:\ProgramData\miniforge3\envs\botty\python.exe`) with `ctypes.windll.user32.SetProcessDPIAware()`
+ `src/input_layer/win_input.py` `mouse_move/mouse_click/mouse_wheel` for clicks (physical coords;
cwd must be C:\Users\alex\my-botty with sys.path.insert(0,"src")).
PowerShell `SetCursorPos`/`mouse_event` are DPI-virtualized → clicks land 1.25x off — do NOT use.
- Click into D2R twice (first click only activates the window).
- F11/F12 hotkeys work via `keybd_event` from anywhere (GetAsyncKeyState polling).
- Screenshots: PIL `ImageGrab.grab(all_screens=True)` in the DPI-aware python = physical pixels.
- Launch: `cmd /c C:\Users\alex\my-botty\run_botty.bat *> log\_run2_out.txt` (PowerShell bg task).
- Watch `log/stats/events_*.jsonl` (newest) + `log/log.txt`.
## Findings so far for the final report (test #2, started 20:23)
1. **A5_WP selection loop (CRITICAL, 3/3 occurrences after Pindle returns)**: every A5 WP open
after a Pindle run fails first try ("Wanted to select A5_WP"); anchor retries (qual_kehk, malah)
sometimes recover (~25s cost), but in game 3 (~20:30) ALL anchors + directed sweep failed,
char wandered to the town wall off all pather nodes, looped 15+ times until manual intervention.
Hypothesis: after Pindle TP return, pather position estimate is wrong; traverses compound the error.
2. **NPC detection failures**: Cain (A4) timed out → fell back to A5 Cain (worked); Tyrael resurrect
timed out once, retry worked. Town maintenance took ~3 min in game 1 due to these.
3. **Battle Command prebuff retry fired 3/3 games** ("Failed to find Battle Command, swapping
weapons again") — CTA buff icon detection systematically needs a second swap.
4. **Mouse misses (relative mode)**: ~6 occurrences, 12-80px off, all self-corrected via SetCursorPos
retry (win_input fallback working as designed).
5. **Player chicken at Pindle** game 2 (HP 37.2%, 59s game) — survivability, not logic.
6. **Stealth random skip** skipped Diablo in game 1 — by design but reduces dia throughput.
7. **D2R settings now verified correct** (in-game screenshots 20:42): DLSS OFF, 1280x720 windowed,
texture HIGH, details LOW, AA/AO off → matches assets/d2r_settings.json (startup warning gone).
Previous session's CS template failures should be fixed; pentagram matched 95% last session.
8. Earlier fixes this session (all verified live): hotkey.wait() no-arg blocking bug, edge-triggered
hotkeys, OCR tesseract_cmd wiring, NipSyntaxError→BNipSyntaxError + Schaefershammer typo.
## PERMANENT FIX PLAN (user-approved direction: fix properly, prefer smarter designs)
### Fix 1 — A5_WP loop: fail-fast + fresh game (CRITICAL, the 10-min wander)
`a5.py:open_wp` already has 3 escalation layers (direct path → 3 anchors → directed sweep,
~3.5 min total). The death loop is the OUTER chain: `bot.on_maintenance` retry sites call
`buy_consumables`/`go_to_act` again → `town_manager.open_wp` again → full 3.5-min escalation
again, from an ever-worse position estimate. Each failed cycle compounds.
**Smart fix:** position estimates can't be trusted after a failure, but a NEW GAME gives a
guaranteed-known spawn in ~40s. Add a per-game WP-failure budget on the `Bot` instance:
- `self._wp_fail_count` reset in `on_init`; `town_manager.open_wp` failure increments it
(thread the signal via return or a callback).
- In `on_maintenance`/`on_end_run`: if `_wp_fail_count >= 2``_save_error_screenshot` +
`trigger_or_stop("end_game", failed=True)` immediately. No more wandering retries.
- Also cap `a5.open_wp` layer 3 (sweep) to run only on the FIRST failure per game; subsequent
calls in the same game go straight to fail (the sweep from an unknown spot is what walked the
char onto the town wall).
### Fix 2 — same family: A5_RED_PORTAL first-click miss (Pindle approach, seen 20:47)
Same position-estimate root cause, already has a "retry from town start" recovery that works.
Include its failure in the same per-game budget rather than new mechanisms.
### Fix 3 — reduce A5→A4 Jamella trips (exposure reduction, smarter)
`buy_consumables: in A5 — traveling to A4 Jamella (Malah unreliable)` runs every game even when
only selling 1-2 junk items. Gate the trip: only travel to A4 if (pots needed below threshold)
OR (tp/id tomes low) OR (inventory has >N sell items). Selling junk can wait; stash is in A5.
Fewer WP trips = fewer chances to hit Fix-1 territory.
### Fix 4 — Battle Command prebuff double-swap (3/3 games)
`Failed to find Battle Command, swapping weapons again` every game. The buff check runs too
soon after weapon swap (buff icons fade in). In the prebuff code (char/hammerdin.py or
i_char.pre_buff): add ~0.4-0.6s wait after CTA casts before checking the buff bar, and lower
the BC icon threshold slightly (capture shows icons render fine). Saves a full swap cycle/game.
### Fix 5 — Cain ID: sticky act preference
A4 Cain timed out (20s wasted) then A5 Cain worked. Cache `self._last_good_cain_act` on Bot;
try that act first next game. One-line behavioral memory, halves ID time after first game.
### Fix 6 — leave as-is (verified fine)
- Mouse misses: ~6/session, all self-corrected by SetCursorPos retry (stealth Bezier primary
path is intentional). No change.
- Stealth random run skip: by design.
- Pindle chicken @ 37% HP: gear/survivability, not code. Mention to user only.
### Verification after implementing
- Unit-light: run 3+ games (`run_pindle`+`run_diablo`), grep log for: no second consecutive
`Wanted to select A5_WP` burst per game; `Battle Command` retry absent; Jamella trip skipped
when nothing needed; failed-WP game ends < 90s instead of 900s timeout.
## Stats so far (test #2)
- Game 1: Pindle OK (46s) + Diablo stealth-skipped. Maintenance ~3min (Cain/Tyrael/A5_WP issues).
- Game 2: Pindle chicken @ HP 37% (59s, failed).
- Game 3: Pindle OK (43s), then A5_WP loop before Diablo → manually stopped 20:37.
- Diablo run not yet completed in test #2.
+561
View File
@@ -0,0 +1,561 @@
# Quest Framework + Den of Evil Plan
## Goal
Build a quest automation framework in botty that can interact with D2R NPCs, handle dialogue,
track quest progress, and run Den of Evil as the first quest -- all usable by a low-level FoHdin.
---
## Architecture
The quest framework is a new subsystem that plugs into the existing botty state machine.
It follows the same patterns as existing runs (approach -> battle -> return to town) but adds
NPC dialogue interaction and quest state persistence.
### New files
```
src/quest/
__init__.py # Exports
quest_manager.py # Quest state machine + persistence (JSON)
quest_dialogue.py # OCR-based NPC dialogue interaction
quest_items.py # Quest item detection/pickup
quest_combat.py # Lightweight combat wrapper (killing trash)
a1/
__init__.py
q_den_of_evil.py # Den of Evil run
```
### Modified files
```
src/npc_manager.py # Add TOWN_MAIDEN NPC constant + templates
src/pather.py # Add A1_ROARING_CANYON + DoE entrance locations
src/bot.py # Add quest state, transitions, handler
src/run/__init__.py # Export DenOfEvil
src/town/a1.py # (optional) Add can_do_den_of_evil method
config/params.ini # Add run_doe to [routes]
config/bnip/ # Add town_maiden.png template
```
---
## Phase 1: Foundation
### 1.1 `src/quest/quest_manager.py`
Purpose: Track which quests are done, persist between sessions, dispatch to quest modules.
```python
class QuestManager:
"""Manages quest state: tracks done/available quests per act, persists to JSON."""
# Quest definitions per act
QUESTS = {
"a1": ["den_of_evil"],
"a2": [], # future: radament, horadric_staff, etc.
...
}
def __init__(self):
self._state_file = "config/quest_state.json"
self._state = self._load()
def is_done(self, quest_name: str) -> bool:
return self._state.get(quest_name, False)
def mark_done(self, quest_name: str):
self._state[quest_name] = True
self._save()
def mark_all_done(self, act: str):
for q in self.QUESTS.get(act, []):
self._state[q] = True
self._save()
def next_pending(self, act: str) -> str | None:
for q in self.QUESTS.get(act, []):
if not self.is_done(q):
return q
return None
def all_done(self, act: str) -> bool:
return all(self._state.get(q, False) for q in self.QUESTS.get(act, []))
def _load(self) -> dict:
if os.path.exists(self._state_file):
with open(self._state_file) as f:
return json.load(f)
return {}
def _save(self):
with open(self._state_file, "w") as f:
json.dump(self._state, f, indent=2)
```
JSON format (config/quest_state.json):
```json
{
"den_of_evil": true,
"search_for_smith": true,
...
}
```
### 1.2 `src/quest/quest_dialogue.py`
Purpose: Talk to NPCs, read dialogue options via OCR, click the right branch.
This is the core of quest automation -- it makes the bot "converse" with NPCs.
```python
class QuestDialogue:
"""OCR-based NPC dialogue interaction for quest conversations."""
# ROI at 1280x720
DIALOGUE_TEXT_ROI = (200, 470, 680, 100) # NPC speech text
DIALOGUE_OPTIONS_ROI = (200, 560, 680, 140) # Player response buttons
DIALOGUE_CLOSE_Y = 670 # Close button area
@staticmethod
def open_dialogue(npc_name: str) -> bool:
"""Walk to NPC and open their dialogue menu."""
from npc_manager import Npc, open_npc_menu
return open_npc_menu(getattr(Npc, npc_name.upper()))
@staticmethod
def read_dialogue() -> dict:
"""OCR the current dialogue box. Returns:
{
'npc_text': str, # What the NPC said
'options': [str, ...], # Response options (may be empty if no choice)
'has_continue': bool # True if just need to click continue
}
"""
img = grab()
npc_text = ocr_roi(img, self.DIALOGUE_TEXT_ROI)
options_text = ocr_roi(img, self.DIALOGUE_OPTIONS_ROI)
# Parse options: split by line, filter out empty, return list
options = [line.strip() for line in options_text.split('\n') if line.strip()]
has_continue = len(options) == 0 or "continue" in options_text.lower()
return {
'npc_text': npc_text.strip(),
'options': options,
'has_continue': has_continue
}
@staticmethod
def click_option(option_text: str) -> bool:
"""Find and click a specific dialogue option by matching text via OCR.
Searches the options ROI for a template match of the option text."""
img = grab()
options_img = cut_roi(img, self.DIALOGUE_OPTIONS_ROI)
# Use template_finder or OCR to locate which button matches
# Then click at that position
...
@staticmethod
def continue_dialogue() -> bool:
"""Click the close/continue button to advance dialogue."""
# Click in the close button area
x, y, w, h = self.DIALOGUE_CLOSE_Y
mouse.click at center of close area
...
@staticmethod
def follow_conversation(expected_options: list[str]) -> bool:
"""Follow a multi-step conversation:
- Read NPC text
- If options present, click the expected one
- If no options, click continue
- Repeat until dialogue closes or unexpected text appears
"""
max_steps = 20 # Safety limit
for i in range(max_steps):
dialogue = self.read_dialogue()
if not dialogue['has_continue'] and dialogue['options']:
# We have a choice - click the expected option
for opt in expected_options:
if opt.lower() in ' '.join(dialogue['options']).lower():
if not self.click_option(opt):
return False
break
else:
Logger.warning(f"Unexpected dialogue options: {dialogue['options']}")
return False
else:
# Just continue
if not self.continue_dialogue():
return False
wait(1.0, 1.5)
# Check if dialogue box is still visible
if not is_visible(ScreenObjects.NPCDialogue):
return True # Done
return False # Hit max steps
```
Key design: `follow_conversation()` takes a list of expected response text. It will
match against whatever options the NPC presents and click the right one. This handles
multi-branch dialogues without hardcoding step-by-step clicks.
### 1.3 `src/quest/quest_combat.py`
Purpose: Lightweight combat for clearing trash during quests. Reuses existing char methods.
```python
class QuestCombat:
"""Combat helpers for quest areas -- reuses existing character combat logic."""
@staticmethod
def clear_area(pather: Pather, char: IChar, path_nodes: list[int],
timeout: float = 60) -> bool:
"""Walk a path while killing monsters until timeout or all nodes cleared.
This is the core of DoE: walk down, kill, walk back."""
return pather.traverse_nodes(path_nodes, char, timeout=timeout, do_combat=True)
@staticmethod
def wait_for_clear(char: IChar, timeout: float = 15) -> bool:
"""Wait until no monsters are visible (area is clear)."""
start = time.time()
while time.time() - start < timeout:
targets = get_visible_targets()
if not targets or len(targets) == 0:
return True
# Attack if enemies present
char.attack()
wait(0.5)
return False
```
### 1.4 `src/quest/quest_items.py`
Purpose: Detect and pick up quest items (gold glow detection).
```python
class QuestItems:
"""Quest item detection and management."""
@staticmethod
def detect_quest_items(img: np.ndarray) -> list[tuple[float, float]]:
"""Detect gold-glowing items on screen (quest items).
Returns list of (x, y) positions in monitor coords."""
quest_item_mask, _ = color_filter(img, Config().colors.get("gold_glow", [
(180, 140, 0), (255, 220, 80)
]))
# Find contours, return centers
...
@staticmethod
def pick_up_quest_items(char: IChar, img: np.ndarray = None) -> bool:
"""Find and pick up any quest items currently visible."""
if img is None:
img = grab()
items = self.detect_quest_items(img)
for pos in items:
char.pick_up_item(pos, item_name="Quest Item")
wait(0.5)
return len(items) > 0
```
---
## Phase 2: NPC & Location additions
### 2.1 Add Town_Maiden to `src/npc_manager.py`
```python
# In class Npc:
TOWN_MAIDEN = "town_maiden" # Act 1, Roaring Canyon
# In _build_npcs():
Npc.TOWN_MAIDEN: {
"head": "town_maiden.png", # Need to capture template
"actions": {} # No trade/identify - just dialogue
}
```
The Town Maiden sits in Roaring Canyon (eastern part of Act 1 town). She has a simple
dialogue: you talk to her to "unlock" the Den of Evil entrance, then you talk to her
again after clearing it to get the XP reward and reset it for another run.
### 2.2 Add locations to `src/pather.py`
```python
class Location:
# ... existing locations ...
# Act 1 Roaring Canyon / Den of Evil
A1_ROARING_CANYON = "a1_roaring_canyon" # Town area where Maiden is
A1_DEN_OF_EVIL_ENTRANCE = "a1_doe_entrance" # Stairs down to DoE
A1_DEN_LEVEL_1 = "a1_doe_level_1"
A1_DEN_LEVEL_2 = "a1_doe_level_2"
A1_DEN_LEVEL_3 = "a1_doe_level_3"
A1_DEN_LEVEL_4 = "a1_doe_level_4"
# (DoE has 3-5 levels depending on game version - need to confirm)
```
Path nodes will need to be added for the Roaring Canyon area and each DoE level.
These are captured via quest_debug.py by walking the path and recording waypoints.
---
## Phase 3: Den of Evil run module
### 3.1 `src/quest/a1/q_den_of_evil.py`
```python
class DenOfEvil:
"""Den of Evil run - Act 1 repeatable quest for XP.
Flow:
1. Ensure character is in Act 1
2. Walk to Roaring Canyon (Town Maiden)
3. Talk to Town Maiden (unlock entrance if needed)
4. Enter Den of Evil
5. Pre-buff (FoH + Conviction for FoHdin)
6. Walk through each level, killing trash
7. Exit back to Roaring Canyon
8. Talk to Town Maiden again for reward
9. Return to town center
"""
name = "run_doe"
# Path nodes per level (to be filled in via quest_debug.py)
LEVEL_PATHS = {
1: [], # Entrance to level 1 stairs
2: [], # Level 1 to level 2
3: [], # Level 2 to level 3
4: [], # Level 3 to level 4 (or final area)
}
def __init__(self, pather, town_manager, char, pickit, runs):
self._pather = pather
self._town_manager = town_manager
self._char = char
self._pickit = pickit
self._runs = runs
self._quest_manager = QuestManager()
self._dialogue = QuestDialogue()
def approach(self, curr_loc: Location, do_buff: bool) -> Location | bool:
"""Get to Roaring Canyon and talk to Town Maiden."""
Logger.info("Run Den of Evil")
# Ensure we're in Act 1
if TownManager.get_act_from_location(curr_loc) != Location.A1_TOWN_START:
curr_loc = self._town_manager.go_to_act(1, curr_loc)
if not curr_loc:
return False
# Walk to Roaring Canyon (Town Maiden area)
if not self._pather.traverse_nodes(
(curr_loc, Location.A1_ROARING_CANYON), self._char, force_move=True
):
return False
# Talk to Town Maiden to unlock/open the Den
if not self._dialogue.open_dialogue("town_maiden"):
return False
# Follow the conversation (expect "Oh no, not again" or similar)
if not self._dialogue.follow_conversation(["Tell me more", "I'll help you"]):
return False
# Enter the Den
if not self._pather.traverse_nodes(
(Location.A1_ROARING_CANYON, Location.A1_DEN_OF_EVIL_ENTRANCE),
self._char, force_move=True
):
return False
return Location.A1_DEN_OF_EVIL_ENTRANCE
def battle(self, do_pre_buff: bool) -> bool | tuple[Location, bool]:
"""Fight through the Den of Evil."""
# Pre-buff
if do_pre_buff:
if not self._char.pre_buff():
return False
# Clear each level
for level in sorted(self.LEVEL_PATHS.keys()):
Logger.info(f"Clearing Den of Evil level {level}")
if not self._pather.traverse_nodes(
self.LEVEL_PATHS[level], self._char, timeout=120, do_combat=True
):
Logger.error(f"Failed to clear DoE level {level}")
return False
# Pick up any quest items / loot
self._pickit.pick_up_items(self._char)
QuestItems.pick_up_quest_items(self._char)
# Walk back to Roaring Canyon
if not self._pather.traverse_nodes(
(Location.A1_DEN_OF_EVIL_ENTRANCE, Location.A1_ROARING_CANYON),
self._char, force_move=True
):
return False
# Talk to Town Maiden for reward
if not self._dialogue.open_dialogue("town_maiden"):
return False
if not self._dialogue.follow_conversation(["Yes", "Thank you"]):
Logger.warning("Failed to collect DoE reward from Town Maiden")
# Mark as done (for non-repeatable quests) or just return success
# Note: DoE is repeatable once per real-day, so we DON'T mark permanently done
# self._quest_manager.mark_done("den_of_evil") # Only if non-repeatable
return (Location.A1_ROARING_CANYON, True)
```
---
## Phase 4: Bot integration
### 4.1 `src/bot.py` changes
```python
# Add import
from quest.a1.q_den_of_evil import DenOfEvil
# In __init__:
self._do_runs["run_doe"] = Config().routes.get("run_doe")
self._doe = DenOfEvil(self._pather, self._town_manager, self._char, self._pickit, self._do_runs)
# In _states list:
# (No new state needed - DoE uses the existing pattern: town -> doe -> end_run -> town)
# In _transitions list (add):
{ 'trigger': 'run_doe', 'source': 'town', 'dest': 'doe', 'before': "on_run_doe" },
# Add 'doe' to end_run source list:
{ 'trigger': 'end_run', 'source': [..., 'doe'], 'dest': 'town', 'before': "on_end_run" },
# Add end_game source:
{ 'trigger': 'end_game', 'source': [..., 'doe'], 'dest': 'initialization', 'before': "on_end_game" },
# Add handler method:
def on_run_doe(self):
res = False
self._do_runs["run_doe"] = False
self._game_stats.update_location("DoE")
self._curr_loc = self._doe.approach(self._curr_loc, not self._pre_buffed)
if self._curr_loc:
set_pause_state(False)
res = self._doe.battle(not self._pre_buffed)
self._ending_run_helper(res)
```
### 4.2 `src/run/__init__.py` changes
```python
# No change needed if DoE lives in src/quest/ (not src/run/)
# But if we want consistency, add:
from quest.a1.q_den_of_evil import DenOfEvil
```
### 4.3 `config/params.ini` changes
```ini
[routes]
; ... existing runs ...
; run_doe (Act 1 Den of Evil - repeatable daily XP)
order=run_doe
```
### 4.4 `config/params.ini` FoHdin config
For a lvl 1 Paladin running DoE, the params.ini needs:
```ini
[char]
type=fohdin
...
[fohdin]
; FoHdin-specific config for low-level DoE runs
teleport=
; No teleport at lvl 1-9, so pathing is on foot
```
---
## Phase 5: Testing workflow
### What needs user input (I cannot see D2R):
1. **Capture Town_Maiden template:**
- Go to Roaring Canyon in Act 1
- Stand near the Town Maiden
- Run `quest_debug.py`, press F4 (NPC detection)
- Paste output so I can save the template
2. **Capture DoE path nodes:**
- Enter the Den of Evil
- Run `quest_debug.py`, press F1 at each waypoint
- Walk from entrance through each level
- Paste outputs so I can build the path arrays
3. **Capture dialogue:**
- Talk to Town Maiden (both before and after clearing)
- Run `quest_debug.py`, press F2 (dialogue OCR)
- Paste output so I can code the conversation flow
4. **Test run:**
- After I write the code, you run botty with `run_doe` in the route order
- Report what happens / paste terminal output
- I iterate based on results
### Lvl 1 Paladin specifics:
- **FoHdin requires FOH skill lvl 6 for Feign of Life passive** -- this needs 3 skill points
in FoH, meaning character level 9 minimum (or level 4 with a +1 skill weapon)
- Before reaching lvl 9, the bot can still run DoE but will be much more fragile
- Recommended: manually level Paladin to ~lvl 4-5 (short runs in Area 1 or 2) before
letting the bot solo DoE with FoH
- The bot pathing should handle the walk-through at low speed with heavy FoH spam
---
## Implementation order
1. Write `quest_manager.py` (simple JSON state tracker)
2. Write `quest_dialogue.py` (OCR-based NPC interaction)
3. Write `quest_items.py` + `quest_combat.py` (lightweight helpers)
4. Add Town_Maiden NPC to npc_manager.py
5. Write `q_den_of_evil.py` (skeleton with placeholder paths)
6. Integrate into bot.py (state, transitions, handler)
7. Update params.ini
8. **USER TESTS** -- captures templates, paths, dialogue
9. I fill in the actual path nodes and dialogue based on your captures
10. Full test run and iterate
---
## File tree after implementation
```
my-botty/
├── config/
│ ├── params.ini # Modified: +run_doe in routes
│ ├── quest_state.json # New: auto-created by QuestManager
│ └── bnip/
│ └── town_maiden.png # New: captured template
├── src/
│ ├── quest/ # New directory
│ │ ├── __init__.py
│ │ ├── quest_manager.py
│ │ ├── quest_dialogue.py
│ │ ├── quest_items.py
│ │ ├── quest_combat.py
│ │ └── a1/
│ │ ├── __init__.py
│ │ └── q_den_of_evil.py
│ ├── npc_manager.py # Modified: +TOWN_MAIDEN
│ ├── pather.py # Modified: +A1_ROARING_CANYON, +A1_DEN_* locations
│ ├── bot.py # Modified: +doe state, transitions, handler
│ └── run/__init__.py # Modified: +DenOfEvil export
```
+324
View File
@@ -0,0 +1,324 @@
# Botty Architecture
## Overview
Botty is an autonomous bot for Diablo II Resurrected (D2R) that uses computer vision and native Windows API input to run endgame content without kernel drivers. It's built in Python 3.10 and distributed as a PyInstaller onefile executable.
## Entry Points
Two standalone executables are built from the same source:
| Executable | Source | Purpose |
|---|---|---|
| `main.exe` | `src/main.py` | Main bot controller |
| `shop.exe` | `src/shopper.py` | Standalone vendor shopping |
## Threading Model
main.py spawns three concurrent threads:
```
main.py (main thread)
├── game_controller.py (bot loop thread)
│ └── bot.py (state machine thread)
├── health_manager.py (background thread)
└── death_manager.py (background thread)
```
- **Main thread**: Shows UI, registers hotkeys (F7-F12), calls `keyboard.wait()` to block
- **Game Controller**: Orchestrates the bot loop - starts/stops the bot thread, handles game recovery, tracks stats
- **Health Manager**: Polls health/mana/merc every ~1s, auto-potions, chickens when low
- **Death Manager**: Polls every ~1s for "You Have Died" screen, triggers recovery
## Core Modules
### Input Layer (`src/input_layer/`)
Native Windows API input replacement designed to evade kernel-level anti-cheat (Warden). It bypasses common Python libraries like `pyautogui` or `pynput` which can be easily detected.
| File | Purpose |
|---|---|
| `win_input.py` | Low-level `ctypes` wrappers for `SendInput`, `GetAsyncKeyState`, `GetCursorPos`. Uses standard Windows user-mode APIs. |
| `mouse_impl.py` | Humanized mouse controller. Features include: Bezier curve trajectories, Gaussian noise (hand tremor), endpoint wobble, and randomized arrival-to-click delays. |
| `hotkey.py` | Polling-based hotkey manager that avoids global hooks. All polling intervals include micro-jitter. |
| `__init__.py` | Drop-in API that shims standard input calls with stealth timing and variable duration automatically. |
All key presses include stealth micro-pauses and variable press duration automatically.
### Screen Capture (`src/screen.py`)
Handles D2R window detection and screenshot capture via MSS library. Converts between coordinate systems: Monitor (top-left first monitor), Screen (per-monitor), Absolute (character center), Relative (template-matched).
### Image Recognition (`src/template_finder.py`)
Template matching via OpenCV (`cv2.matchTemplate`). Searches against pre-captured asset templates in `assets/templates/`. Returns match position and validity.
### UI Detection (`src/ui/`)
| File | Detects |
|---|---|
| `main_menu.py` | Main menu buttons (play, save & exit) |
| `character_select.py` | Character portraits and selection |
| `skills.py` | Skill bar state, skill availability |
| `meters.py` | Health/mana globes, potion charges |
| `player_bar.py` | Player status bar, XP bar |
| `view.py` | Minimap, inventory screen detection |
| `waypoint.py` | Waypoint portal selection |
| `error_screens.py` | Error dialogs, death screen |
| `loading.py` | Loading screen detection |
### Pathing (`src/pather.py`)
Pathfinding via reference template matching with relative coordinates. Each location has numbered nodes with template references. The `Location` class defines named destinations (towns, dungeons). Path definitions map (start, end) pairs to node sequences.
### Character System (`src/char/`)
Inheritance hierarchy:
```
IChar (abstract base)
├── Basic / Basic_Ranged
├── Paladin
│ ├── Hammerdin
│ └── FoHdin
├── Sorceress (base for all sorc builds)
│ ├── BlizzSorc
│ ├── BlizzorbSorc
│ ├── NovaSorc
│ ├── LightSorc
│ └── HydraSorc
├── Amazon
│ └── Javazon
├── Trapsin
├── Barbarian
├── Necro
├── Poison_Necro
├── Bone_Necro
└── Warlock
├── FireLock
├── EchoLock
└── AbyssLock
```
Each character class implements: run methods, skill usage, attack patterns, position-specific behavior.
### Run System (`src/run/`)
| File | Run |
|---|---|
| `pindle.py` | Pindle of Purity |
| `shenk_eld.py` | Shenk + Eldritch |
| `trav.py` | Travincal |
| `nihlathak.py` | Nihlathak's Temple |
| `arcane.py` | Arcane Sanctuary |
| `diablo.py` | Chaos Sanctuary + Diablo |
| `vizier.py` | Vizier (Seal Boss) |
| `baal.py` | Baal |
| `mephisto.py` | Mephisto |
| `andariel.py` | Andariel |
| `countess.py` | Countess |
| `level.py` | General leveling runs |
### Town System (`src/town/`)
`TownManager` orchestrates act-specific town routines. Each act (A1-A5) has its own module with NPC interactions, waypoint usage, and stash handling.
### Inventory (`src/inventory/`)
| File | Purpose |
|---|---|
| `belt.py` | Belt slot management, potion detection |
| `personal.py` | Personal inventory grid, item positions |
| `vendor.py` | Vendor trade window |
| `stash.py` | Personal stash, shared stash |
| `cube.py` | Horadric Cube |
| `common.py` | Shared inventory utilities |
### Item Recognition (`src/item/`)
| File | Purpose |
|---|---|
| `pickit.py` | Pickup decisions, item filtering |
| `consumables.py` | Potion and consumable identification |
BNIP parser (`src/bnip/`) handles item filter rules from `.bnip` files.
### Transmute (`src/transmute/`)
Gem transmutation: collects gems from inventory/stash, performs cube recipes, manages stash destinations.
### Messages (`src/messages/`)
| File | Purpose |
|---|---|
| `messenger.py` | Message dispatcher |
| `discord_embeds.py` | Discord webhook formatting |
| `generic_api.py` | Generic HTTP webhook support |
### Configuration (`src/config.py`)
Singleton that merges config files in priority order:
`custom.ini` > `params.ini` > `game.ini` > `shop.ini` > `transmute.ini`
Supports variable substitution via `[variables]` sections.
### Stealth System (`src/utils/stealth.py`)
A multi-tiered approach to mimicking human behavior and evading detection:
- **Tier 1: Input Stealth**: Automatic micro-pauses (20-120ms), variable key press durations (20-200ms), and non-linear mouse paths via `input_layer`.
- **Tier 2: Behavioral Stealth**: Probabilistic "mistakes" such as clicking the wrong waypoint (2.5% chance) or skill hesitation (80-300ms) before casting.
- **Tier 3: Session Stealth**: Randomized run durations (+/-15%), AFK breaks (2-12 mins), and shuffling of farming routes between rotations.
All timing across the bot is routed through `utils.misc.wait()`, which applies Gaussian jitter to every sleep call.
### Utilities (`src/utils/`)
| File | Purpose |
|---|---|
| `misc.py` | Window management, DPI awareness, timing utilities |
| `restart.py` | Game launch/kill, D2R always-on-top |
| `auto_settings.py` | D2R game settings adjustment |
| `key_detector.py` | Auto-detect skill bindings from `.key`/`.keyo` files |
| `graphic_debugger.py` | Visual debugging overlay |
| `node_recorder.py` | Record new path nodes |
| `stealth.py` | Stealth behavior randomization |
### Auxiliary Tools
Standalone tools located in the root directory for project maintenance and development:
- `asset_manager.py`: Unified interface for auditing, searching, and optimizing template assets.
- `asset_extractor.py`: Screenshot capture and AI-assisted entity cropping workflow.
- `build.py`: PyInstaller wrapper for building production executables.
- `desktop_snap.py`: Lightweight tool for capturing full desktop screenshots.
- `quest_debug.py`: Debugging interface for the questing system.
- `screenshot_tool.py`: Simple utility for taking D2R client area screenshots.
## Data Flow
```
D2R Game Window
screen.grab() ──→ Image (numpy array)
template_finder.search() ──→ Position + Validity
ui_manager.detect_screen_object() ──→ Current UI state
bot.py (state machine) ──→ Decides next action
char.run() / pather.go_to() ──→ Movement + Interaction
input_layer.keyboard.send() / mouse.click() ──→ SendInput API
D2R Game Window (response captured on next grab)
```
## Build System
- **Build script**: `build.py` (PyInstaller with `--onefile --noconsole`)
- **Dependencies**: `pyproject.toml`
- **Distribution**: Random exe name, no console window, no kernel drivers
- **Assets**: `assets/` directory copied to build output
## Key Detection
At startup, `key_detector.py` reads the character's `.key`/`.keyo` file from `Saved Games/Diablo II Resurrected/` to auto-detect skill bindings and non-skill keys (inventory, show items, etc.). This eliminates manual key configuration.
Slot-to-key mapping (`CHAR_BINDING_SLOTS`):
| Slot | Config key |
|---|---|
| 41 | `show_belt` |
| 36 | `stand_still` |
| 44 | `weapon_switch` |
| 43 | `force_move` |
If params.ini and the `.keyo` file disagree, params.ini wins and a `"Keeping configured key binding"` line is logged. No disagreement = no log line.
## Coordinate Systems
| System | Origin | Used By |
|---|---|---|
| Monitor | Top-left of first monitor | `screen.grab()` |
| Screen | Per-monitor client area | UI detection |
| Absolute | Character at screen center | Pathing, target detection |
| Relative | Template match position | Inventory grid, NPC interaction |
Conversion functions in `screen.py`: `convert_monitor_to_screen()`, `convert_screen_to_abs()`, etc.
## State Machine
`bot.py` uses the `transitions` library. States and transitions are defined at `Bot.__init__` time.
**States**: `initialization`, `hero_selection`, `town`, `level`, `pindle`, `shenk`, `trav`, `nihlathak`, `arcane`, `diablo`, `vizier`, `baal`, `mephisto`, `andariel`, `countess`
**Key transitions**:
| Trigger | Source | Dest | Notes |
|---|---|---|---|
| `init` | initialization | initialization | Screen detection, routes to create_game or start_from_town |
| `select_character` | initialization | hero_selection | |
| `start_from_town` | initialization/hero_selection | town | |
| `maintenance` | town | town | Heal, buy pots, stash, repair, resurrect merc |
| `run_pindle` | town | pindle | |
| `run_arcane` | town | arcane | |
| `end_run` | any run state | town | TP back; calls `on_end_run` which TPs to town |
| `end_game` | town/any run state | initialization | Save & exit; use when no TP scrolls or unrecoverable |
`end_run` requires working TP scrolls. If charges = 0, trigger `end_game` instead — otherwise the bot loops trying to TP back indefinitely.
## HealthManager — Internal Timing
The background health monitor (`src/health_manager.py`) polls at:
```
interval = max(0, (3/25 - fn_elapsed) * jitter(0.81.2))
```
That's approximately every 3 game frames (96144ms at 25 FPS). The jitter prevents perfectly regular polling patterns from being detectable.
**Rejuv logic**:
1. Minimum 0.60s between rejuv drinks (hit recovery guard).
2. Drinks rejuv if `health ≤ take_rejuv_potion_health` OR `mana ≤ take_rejuv_potion_mana`.
3. "Double rejuv" chicken fires only if `last_drink < 8s` **AND** `health ≤ take_rejuv_potion_health`.
- The HP check is critical: mana-triggered rejuvs can legitimately fire back-to-back at full HP (Hammerdin spending mana fast). Without the HP check, false chickens occur at 99.9% HP.
## PickIt — Item Identity
`GroundItem` has two identity fields:
| Field | Formula | Purpose |
|---|---|---|
| `ID` | `slugify(Name + all as_dict() values including Amount)` | Pickit cache key; different gold amounts = different IDs |
| `UID` | `ID + screen center position` | Deduplication within a single items list; same pile at same coords = same UID |
The fail-detection in `_pick_up_item` uses `item.ID == prev.ID` for gold and `item.UID == prev.UID` for everything else. Two nearby gold piles with different amounts bypass the ID check since they produce different IDs.
`_yoink_item()` always returns `PickedUpResult.PickedUp` regardless of actual success. True pickup failures surface only through `_pick_up_item`'s same-ID/UID repeat detection. On confirmed failure, the item's `ID` is blacklisted in `_cached_pickit_items` so it isn't retried in the same session.
## Configuration Priority
Merge order (highest priority first):
```
custom.ini > params.ini > game.ini > shop.ini > transmute.ini
```
`Config` is a singleton (`__new__` + `data_loaded` class variable). First instantiation loads all files; subsequent calls return the same instance. Key detection runs during `__init__` after `self.char` is populated.
## Known Architectural Issues
See `IMPROVEMENTS.md` for the full list. Highest-priority unresolved items:
- **C10**: `kill_thread()` uses `PyThreadState_SetAsyncExc` (CPython private API). Can corrupt locks/GIL. Replace with `threading.Event` cooperative shutdown.
- **H12/H14**: Health/death managers use module-level globals for state. HealthManager now has a `_state_lock`; death manager does not yet.
- **M14**: `PickedUpResult` enum has a gap (values 0,1,3,4,5 — missing 2).
- **H10/H11**: `pather.py` (750 lines) and `config.py` are oversized and should be split.
+444
View File
@@ -0,0 +1,444 @@
# Claude Code Working Guide — my-botty
This file is auto-read by Claude Code at session start. It covers what you need to debug failures, extend the bot, and avoid known pitfalls. For static architecture see `ARCHITECTURE.md`; for Gemini conventions see `GEMINI.md`.
---
## Quick Orientation
`my-botty` is a Python bot that automates Diablo II: Resurrected boss runs using computer vision and native Windows API input. No kernel drivers — only `SendInput`, `GetCursorPos`, `SetCursorPos` via `ctypes`.
**Active setup (as of 2026-06-08):**
- Character: Hammerdin (`fistman`), Hell difficulty
- Runs: `run_diablo`, `run_pindle` (see `config/params.ini`)
- OS: Windows 11, D2R 1280×720 windowed
- Start bot: `run_botty.bat` (runs `python src/main.py` from the conda env — always current with source; no exe build exists/needed)
**Always start a session by reading logs first:**
```
log/log.txt ← current session (DEBUG level)
log/stats/events_*.jsonl ← per-run event stream (JSON lines)
log/stats/stats_*.log ← human-readable summary
```
Use `/check-logs` slash command to get an instant summary.
---
## How to Read a Failure
### Startup line (every session)
```
=== BOT START === char=hammerdin | difficulty=hell | routes=['run_diablo', 'run_pindle']
```
If this is missing, the bot crashed before `on_init()`.
### Approach failure (run couldn't get to the boss area)
```
ERROR Approach failed for run_diablo [step: use_wp_rof]
```
The `[step: X]` tells you exactly which sub-step failed. Also sent to Discord via `_save_error_screenshot`. Step names per run:
| Run | Step names (in order) |
|-----|----------------------|
| `diablo` | `open_wp``use_wp_rof``verify_rof_first``retry_open_wp``retry_use_wp_rof``verify_rof_retry` |
| `vizier` | `open_wp``use_wp_rof` |
| `arcane` | `open_wp``use_wp_arcane` |
| `shenk` | `open_wp``use_wp_frigid` |
| `trav` | `open_wp``use_wp_travincal` |
| `nihlathak` | `open_wp``use_wp_halls_of_pain``verify_halls_of_pain` |
| `pindle` | `go_to_act5``traverse_to_portal``retry_traverse_to_portal``click_red_portal` |
| `andariel` | `go_to_act1``traverse_to_wp``open_wp``use_wp_catacombs` |
| `countess` | `go_to_act1``traverse_to_wp``open_wp``use_wp_black_marsh` |
| `mephisto` | `go_to_act3``traverse_to_wp``open_wp``use_wp_durance` |
| `baal` | `go_to_act5``traverse_to_wp``open_wp``use_wp_worldstone` |
**Implementation:** Each run class has `self.approach_fail_step: str | None = None`. Set to step name just before `return False`. Read in `bot.py` `_run_wrapper()` via `getattr(run_obj, "approach_fail_step", None)`.
### Maintenance failure (town routine broke)
```
ERROR Maintenance failed [step: buy_consumables] — vendor NPC not found after retry
```
Also sent to Discord. Maintenance steps in execution order:
`town_heal``inspect_inventory``identify_items``buy_consumables` / `heal``stash_items``repair``resurrect_merc``gamble`
Tracked in `self._maintenance_step` on the `Bot` instance (`bot.py`). Non-fatal failures (repair, resurrect) just log a warning with the step name. Fatal failures (buy_consumables, stash_items) call `_save_error_screenshot("maintenance", reason)` then `trigger_or_stop("end_game", failed=True)`.
### Common error patterns to grep for
```
ERROR|WARNING|failed|Approach failed|starting from True|DAMAGED|SendInput.*missed
```
| Pattern | Cause |
|---------|-------|
| `starting from True` | `_curr_loc` became Python `True` instead of a `Location` enum — see `TownManager.identify()` bug below |
| `failed on item.*DAMAGED` | `NTIP_ALIAS_QUALITY_MAP` missing `ItemQualityKeyword.Damaged.value` — fixed in `bnip_data.py` |
| `SendInput.*missed target` | Win11 pointer acceleration amplifying relative mouse moves — fixed in `win_input.py` |
| `Repair/vendor interaction failed` | Flaky NPC detection, best-effort — bot continues |
| `Could not identify act from location` | `_curr_loc` is `None` or `True`, not a `Location` string |
---
## Known Bugs and Fixes (permanent reference)
### Bug 1: `_curr_loc = True` propagation
**File:** `src/town/town_manager.py``identify()` method (~line 236)
The act-level `identify()` returns a `Location` enum on success. The wrapper previously returned `curr_loc` (the passed-in value) unchanged. If `curr_loc` was somehow `True` (Python bool), it propagated. `True == 1` in Python, so pather's `traverse_nodes((True, A5_QUAL_KEHK))` could match `A5_TOWN_START` (enum value 1), masking the bug.
**Fix:** Both return sites in `identify()` now return `success if isinstance(success, Location) else curr_loc` (primary) and `success if isinstance(success, Location) else new_loc` (A5 fallback).
**Defensive guard in bot.py** (~line 438):
```python
self._curr_loc = self._town_manager.identify(self._curr_loc)
if self._curr_loc is True:
Logger.warning("identify() returned True — resetting to A5_TOWN_START")
self._curr_loc = Location.A5_TOWN_START
```
### Bug 2: `DAMAGED` quality KeyError
**File:** `src/d2r_image/bnip_data.py``NTIP_ALIAS_QUALITY_MAP`
`ItemQualityKeyword.Damaged = 'DAMAGED'` exists but the map was missing an entry for it. Caused `KeyError: 'DAMAGED'` on 20+ items per session whenever Charsi repair triggered ground-item reads.
**Fix:** Added `ItemQualityKeyword.Damaged.value: 1` to the map alongside `LowQuality`, `Crude`, `Cracked`.
### Bug 3: NPC name tag templates stale → `open_npc_menu` always times out
**File:** `src/npc_manager.py``open_npc_menu()`
NPC body templates find the NPC correctly (Akara consistently at screen ~(649, 407)), but the name tag "AKARA"/"MALAH" templates score only ~0.28 on hover, just below the original 0.35 threshold. This caused a 20-second full-screen search that never clicked anyone.
**Root causes:**
1. Name tag templates are slightly stale vs. current D2R rendering
2. Name tag search used the full screen ROI, so false-positive text anywhere on screen could also score 0.28+ — raising threshold didn't help
3. After click, the old code checked gold name tag template (also stale) instead of the NPC dialogue UI element
**Fix:** Three changes in `open_npc_menu()`:
1. **Small ROI for name tag check**: after hovering, search for the name tag only in a 240×140px box directly above the cursor (where D2R always renders name tags), preventing distant false positives
2. **Lower name tag threshold 0.35 → 0.26**: Akara at 0.28 now passes; with the small ROI false-positive risk is contained
3. **NPCDialogue confirmation instead of gold tag**: after clicking, check `is_visible(ScreenObjects.NPCDialogue)` first — more reliable than matching a stale gold name tag template
4. **Timeout 20 s → 8 s**: fails faster when the NPC is genuinely off-screen
**Symptom to grep for (before fix):** `NPC akara hover - White score: 0.28x` spinning for 20 s
---
### Bug 4: `open_npc_menu` body+pose scoping bug → false positive Akara/Malah clicks
**File:** `src/npc_manager.py``open_npc_menu()`, hover loop
User-added body+pose fallback click path had a scoping bug: `min_dist` was computed per template in the build loop but the variable was NOT stored in the result dict, so the hover loop's `pose_confirmed = ... and min_dist < 100` used the stale value from the last template processed — not the current result's distance to a known pose. This caused false positives (e.g. Akara body template scoring 0.531 at screen `(589, 560)`, far outside her ROI and 211 px from any pose) to pass `pose_confirmed` and be clicked, while the real Akara at `(979, 443)` (body 0.420.47, 102 px from pose) never got clicked.
**Fix** (`open_npc_menu` build + hover loops):
1. Renamed `min_dist``min_dist_val`, stored in result dict: `results.append({..., "min_dist": min_dist_val})`
2. Hover loop uses `result["min_dist"]` instead of stale `min_dist`
3. Body threshold 0.50 → 0.40 (real Akara at 0.420.47)
4. Pose tolerance 100 → 150 px (real Akara at 102 px from nearest pose; false positive at 211 px, correctly rejected)
5. Removed `elif body_confident and attempts == 0` blind-first-attempt path (was the direct enabler of false positive clicks)
**Symptom to grep for (before fix):** `Clicking on akara at (589, 559) (body+pose confirmed, name tag score 0.225)` followed by `dialogue not open`
---
### Bug 5: `_curr_loc` becomes `A1_TOWN_START` when Cain ID fails → A1 pathing in A5 environment
**Files:** `src/bot.py``on_maintenance()` lines 473475 and 502510
When Cain identification fails, `identify()` returns False → `_curr_loc` was reset to `Location.A1_TOWN_START` as a fallback. But the character is physically still in A5 Harrogath (game just spawned there). The subsequent `buy_consumables(A1_TOWN_START)` call runs A1 pather navigation in the A5 environment: pather can't find A1 reference templates → moves character to a random A5 spot → Akara body templates fire false positives → clicks fail → fatal.
The A1 retry at line 505 also hardcoded `Location.A1_TOWN_START` without calling `go_to_act` first, so the character was never actually navigated to A1 before running A1 pathing.
**Fix (both sites in `bot.py` `on_maintenance()`):**
1. Identify fallback: `A1_TOWN_START``A5_TOWN_START` (character IS in A5 when Cain fails)
2. A1 retry: add `go_to_act(1, retry_start)` before `buy_consumables` so the bot actually opens the waypoint and travels to A1 first:
```python
retry_start = self._curr_loc or Location.A5_TOWN_START
a1_loc = self._town_manager.go_to_act(1, retry_start)
self._curr_loc, result_items = self._town_manager.buy_consumables(a1_loc or Location.A1_TOWN_START, items=items)
```
**Symptom to grep for (before fix):** `TownManager buy_consumables: starting from a1_town_start` immediately after `Could not identify items (Cain not available)`
---
### Bug 6: `open_npc_menu` dialogue confirmation always False → retry click closes dialogue → loop
**File:** `src/npc_manager.py` — `open_npc_menu()` post-click confirmation block
After clicking Akara/Malah the bot checked `is_visible(ScreenObjects.NPCDialogue)`. The `npc_dialogue.png` template is a narrow (~15px) gold vertical border strip from an old D2R rendering; the ROI `456,0,30,150` points at the top-center of the screen where no dialogue border renders. Result: `NPCDialogue` is **always False** even when the dialogue IS open.
Fallback was `name_tag_gold` template — also stale. Both checks failing triggered a retry click on the NPC body, which **closes** the already-open dialogue, creating an open→can't detect→close→retry loop until timeout.
Also: `press_npc_btn` had `wait_until_visible(ScreenObjects.NPCDialogue, timeout=3.0)` which wasted 3 seconds on every NPC button press.
**Fix:** Replace NPCDialogue + gold-tag checks with action button detection (`_action_btns_visible`). The TRADE/RESURRECT/IDENTIFY buttons appear when the dialogue opens and are reliably matched by the same templates used in `press_npc_btn`. Added helper `_action_btns_visible(npc_key, img)` that mirrors `press_npc_btn`'s white→blue→grayscale search. Also added a **fast path** at the top of the hover loop: if action buttons are already visible at the start of an iteration, return True immediately without hovering.
**Symptom to grep for (before fix):** `NPC akara - dialogue not open, retrying click on body` repeating 23 times then `NPC akara - clicked but neither dialogue nor gold tag found`
---
### Bug 7: High-score false-positive name tags at positions outside NPC ROI bypass pose check
**File:** `src/npc_manager.py` — `open_npc_menu()` click decision (was line 327)
White text in the game world (ground items, skill effects, UI elements) could produce name-tag white-template scores of 0.98+ at positions far outside the NPC's known ROI. Because `name_tag_confirmed = res_w.valid or res_g.valid` was unconditional, these positions bypassed the `pose_confirmed` check entirely and got clicked (e.g. (200, 539) while Akara's ROI is x=6051004).
**Fix:** Gate `name_tag_confirmed` on an ROI boundary check when `attempts == 0` and the NPC has a defined ROI:
```python
if "roi" in npcs[npc_key] and attempts == 0:
npc_roi = npcs[npc_key]["roi"]
in_npc_roi = (npc_roi[0] <= hover_screen[0] <= npc_roi[0] + npc_roi[2] and
npc_roi[1] <= hover_screen[1] <= npc_roi[1] + npc_roi[3])
else:
in_npc_roi = True
name_tag_confirmed = (res_w.valid or res_g.valid) and in_npc_roi
```
After the first pass (`attempts > 0`), ROI restriction is lifted so the wide fallback search can still find the NPC if it wandered.
**Symptom to grep for (before fix):** `Clicking on akara at (200, 539) (name tag confirmed)` with x < 605
---
### Bug 8: Win11 mouse 36× overshoot
**File:** `src/input_layer/win_input.py` — `mouse_move()` (~line 270)
Win11 relative `SendInput` deltas are amplified by Windows Enhanced Pointer Precision (pointer acceleration). A 209px delta became 678px. Affected every NPC click, waypoint click, and template interaction.
**Fix:** On Win11 (`_USE_ABSOLUTE_MOUSE == False`), use `SetCursorPos(x, y)` for accurate positioning, then send a zero-delta `MOUSEEVENTF_MOVE` event so D2R's hover/cursor pipeline fires at the new position.
```python
# Win11 path in mouse_move():
user32.SetCursorPos(target_x, target_y)
_send_input(_make_mouse_input(MOUSEEVENTF_MOVE, 0, 0))
```
The OS mode is detected at import time via `utils.os_detect.detect_os()` → `_USE_ABSOLUTE_MOUSE`.
---
### Bug 9: Act-state desync → char respawns in wrong act → every subsequent game fails (2026-06-10)
**Files:** `src/bot.py`, `src/town/town_manager.py`
The bot's believed location (`_curr_loc`) and the character's PHYSICAL act diverge after any town
failure: retries hardcoded acts without traveling (`buy_consumables(A1_TOWN_START)` even when
go_to_act(1) FAILED, `repair(A4_TOWN_START)`, `heal(A1_TOWN_START)`) and failure fallbacks blindly
set `_curr_loc = A5_TOWN_START`/`A1_TOWN_START`. D2R respawns the char in the act it save+exited
from, so one desync poisons EVERY following game: A5 pathing runs in A1/A4 town → `A5_WP` never
found → 42 consecutive `open_wp` approach failures in the 2026-06-09 session.
**Fix:**
1. `TownManager.detect_current_act()` — searches `TOWN_MARKERS` to find the physical act town.
2. `TownManager.open_wp()` / `go_to_act()` — on failure/early-return, verify the physical act and
retry with the detected act's pather.
3. `Bot._verify_town_location(assumed)` — used at EVERY retry/fallback site in `on_maintenance()`
and `on_end_run()` instead of hardcoded town starts. Never run act-X pathing without confirmed
presence in act X.
**Symptom to grep for:** `Wanted to select A5_WP, but could not find it` repeated across games;
`A1 open_trade_menu: navigating from a1_town_start` + `Pather: taking a random guess` while
physically elsewhere.
### Bug 10: Duplicate `log_end_game` → phantom 0s "successful" games reset the fail circuit breaker (2026-06-10)
**File:** `src/game_stats.py`
`bot.on_end_game()` and `game_controller.run_bot()` can BOTH call `log_end_game` for the same game.
The second call emitted a phantom `game_ended failed:false elapsed 0` event and reset
`_consecutive_runs_failed` to 0 — so `max_consecutive_fails=5` never triggered during the 3-hour
death spiral. **Fix:** `log_end_game` returns early if `self._timer is None` (already logged).
Also: `_last_failure_reason` is now cleared in `log_start_game` so events can't inherit a stale
reason from a previous game (games 4350 were blamed on `open_wp` when they actually failed in
maintenance).
### Bug 11: Chickens mislabeled "Bot stopped (F12 or crash)" (2026-06-10)
**Files:** `src/health_manager.py`, `src/game_controller.py`
`_do_chicken()` called `bot.stop()` (callback) FIRST and only set `_did_chicken = True` several
seconds later (after save/exit + screenshot). The controller poll loop saw `_stopping` before the
chicken flag and labeled the failure "Bot stopped (F12 or crash)". **Fix:** set `_did_chicken =
True` at the top of `_do_chicken()` before the callback; controller re-checks the flag before
resetting it.
### Bug 12: A4 Halbu repair trip = act-desync trigger (2026-06-10)
**File:** `config/params.ini`
`repair_npc=a4_halbu` sent the bot A5→A4 via WP every 5 runs. Halbu detection failed 100% in the
2026-06-09 session (body score ~0.39, name tag ~0.19), wasting ~60s per attempt and leaving the
char in A4 (see Bug 9). **Fix:** `repair_npc=a5_larzuk` — stays in-act; the Larzuk flow has a
direct-template fallback and still falls back to Halbu (with proper travel) if Larzuk fails.
### Bug 13: hotkey `wait()` no-arg returned on ANY key → silent process exit (2026-06-11)
**File:** `src/input_layer/hotkey.py`
The reimplemented `keyboard.wait()` (no key) returned on any keypress; `main.py` relies on it
blocking forever to keep the process alive (all bot threads are daemons). Pressing F11 both
started the bot AND killed the process moments later, with no traceback. **Fix:** no-arg `wait()`
now sleeps forever; the poll loop is also edge-triggered (one fire per physical press — held
keys no longer refire every ~20ms) and callback exceptions print instead of being swallowed.
### Bug 14: OCR dead — pytesseract never configured (2026-06-11)
**File:** `src/d2r_image/ocr.py`
pytesseract only looks for plain `tesseract` on PATH; the `PYTESSERACT_TESSERACT_CMD` env var
set by run_botty.bat is NOT a pytesseract feature and was never read. **Fix:** ocr.py applies
that env var (fallback: PATH, then `C:\Program Files\Tesseract-OCR\tesseract.exe`) to
`pytesseract.pytesseract.tesseract_cmd` at import.
### Bug 15: NIP loader NameError + dropped rule (2026-06-11)
**Files:** `src/bnip/utils.py`, `config/default.bnip:1714`
`find_unique_or_set_base` raised undefined `NipSyntaxError` (→ `BNipSyntaxError`), and the
underlying trigger was a typo `Shaefershammer` → `Schaefershammer`. 474 expressions now load.
### Bug 16: A5 WP death loop after Pindle returns (2026-06-11)
**Files:** `src/town/town_manager.py`, `src/town/a5.py`, `src/bot.py`
After a Pindle return the believed location (`a5_town_start`) is wrong (char is at the stash);
the direct WP node path failed 5/5, and outer maintenance retries re-ran the full anchor/sweep
escalation from ever-worse positions (one 10-min wander onto the town wall). A5 templates also
score marginally low at current D2R settings (stash ~0.51, WP needs ≤0.62).
**Fix (layered):** per-game WP budget on TownManager (reset in `bot.on_init`): failure 1 = full
escalation allowed; failure 2 = `quick=True` direct path only; failure 3+ = instant False so the
caller's fatal path ends the game (~40s fresh spawn beats wandering). Sweep trimmed 10→6 steps,
WP select timeout 4s, thresholds dropped (WP 0.62; stash 0.60→0.45 retry) — safe because every
select is gated by a success_func (panel-open / WP-label check).
### Bug 17: kill_diablo fought with Conviction + mid-fight Redemption (2026-06-11)
**File:** `src/char/paladin/hammerdin.py` `kill_diablo()`
Conviction doesn't boost magic-damage hammers, and the interleaved 0.8s Redemption casts were
pure downtime vs a solo boss with no corpses — long fights got the merc killed. **Fix:** attack
with Concentration (also buffs the merc via party aura), Redemption only once post-kill.
### Bug 18: stash only used personal + 3 shared (2026-06-11)
**Files:** `src/inventory/personal.py`, `src/inventory/stash.py`
Gold used raw `select_tab()` clicks with a 4-tab rotation (`% 4`, `> 3`); items already paged.
D2R 2.7+ has 5 shared pages. **Fix:** gold now navigates via `select_stash_page()` (OCR-verified
page arrows, layout-proof), rotation `% 6` / stop at `> 5`, shared-first starts at page 5.
Also fixed a leftover `> 3` bound in the item-stash loop (`personal.py` ~line 185) → `> 5`.
### Bug 19: failed item transfer mistaken for "stash full" → taskkill D2R (2026-06-12)
**File:** `src/inventory/personal.py` — `stash_all_items()` stash loop
When `transfer_items("stash")` fails for any reason OTHER than fullness (e.g. the equipped-area
click guard, a transient UI hiccup), keep items stay in inventory. The loop interpreted ANY
remaining keep item as "this tab is full", paged through all stash tabs, and on the last page
called `stash.stash_full()` → `taskkill /f /im D2R.exe` + a false Discord "stash full" alert.
Surfaced by `tools/testbed.py stash all` (charms in cols 49 hit the equipped-area guard, so every
transfer was cancelled and the loop nuked the game).
**Fix:** before declaring the tab full, check `is_visible(ScreenObjects.EmptyStashSlot)`. If a slot
IS free but the transfer still failed, count it as a transfer failure (cap 2) and bail out,
leaving items in inventory — never advance tabs / call `stash_full()` on a non-full page.
**Symptom to grep for (before fix):** repeated `transfer_items: inventory unchanged after
attempting to stash` followed by `Wanted to stash item ... Assumes full stash` across rising page
numbers, ending in `All stash is full, quitting`.
**Test tip:** `tools/testbed.py stash` exercises the gold + open-stash path live; add `all` to scan
all 10 inventory columns and exercise the keep-item transfer branch on existing charms.
---
## How to Add a New Run
1. Create `src/run/my_run.py` — class `MyRun` with `name = "run_my_run"`
2. Add `self.approach_fail_step: str | None = None` in `__init__`
3. In `approach()`: set `self.approach_fail_step = None` at top, then set step name before every `return False`
4. Register in `bot.py`: instantiate in `__init__`, add to `self._do_runs`, add state + transition, add `on_run_my_run()` handler
5. Add route toggle to `config/params.ini` under `[routes]`
6. Add to `Config().routes_order` list
The `_run_wrapper()` in `bot.py` handles approach failure reporting automatically — it calls `getattr(run_obj, "approach_fail_step", None)` so no changes needed there.
---
## How Town Maintenance Works
Sequence in `bot.py` `on_maintenance()` (called between every run):
1. **town_heal** — drink belt potions if HP < 95%
2. **inspect_inventory** — open inventory, count items, update TP/ID/key needs
3. **identify_items** — go to Cain if any `item.need_id` is True
4. **buy_consumables** — visit vendor for HP/mana pots, TP scrolls, ID scrolls, keys; sell flagged items (fatal if fails twice)
5. **heal** — visit healer NPC if HP/mana below threshold (alternative to buy_consumables branch)
6. **stash_items** — put kept items / gold in stash; run transmutes after (fatal if fails twice)
7. **repair** — repair gear + sell via Halbu (A4) or Larzuk (A5); non-fatal, bot continues
8. **resurrect_merc** — revive dead merc; non-fatal
9. **gamble** — buy gamble items if Jamella has stock; non-fatal
`TownManager` methods (`buy_consumables`, `stash`, `repair`, etc.) return `(new_loc, items)` tuples or `(False, False)` on failure. On failure, bot.py retries once from a fallback location before giving up.
---
## Coordinate Systems (quick ref)
| Name | Origin | Notes |
|------|--------|-------|
| Monitor | Top-left of first monitor | `screen.grab()` output |
| Screen | D2R client area top-left | UI detection |
| Absolute | Character at screen center | Pathing targets |
| Relative | Template match position | NPC interaction |
Convert via `screen.py`: `convert_monitor_to_screen()`, `convert_screen_to_abs()`, `convert_abs_to_monitor()`, `convert_screen_to_monitor()`.
---
## Config System
Singleton `Config()` merges in priority order: `custom.ini` > `params.ini` > `game.ini` > `shop.ini` > `transmute.ini`. First instantiation loads; subsequent calls return the same object. User overrides go in `custom.ini` (not tracked in git).
Key sections in `params.ini`:
- `[char]` — character type, keybinds, difficulty, runs_per_repair, etc.
- `[general]` — discord webhook, auto_login, info_screenshots, difficulty
- `[routes]` — boolean flags for each run (run_diablo=1, run_pindle=1, etc.)
- `[routes_order]` — execution order
---
## Discord Notifications
`src/messages/messenger.py` wraps a Discord webhook. Error events are sent via `_save_error_screenshot(run_name, reason)` in `bot.py`, which:
1. Saves a timestamped screenshot to `log/screenshots/error/`
2. Calls `messenger.send_error(run_name, reason, screenshot_path)` if `discord_log_errors=1`
Enable in `params.ini`:
```ini
discord_log_errors=1
discord_hook_url=https://discord.com/api/webhooks/...
```
---
## Threading Safety Rules
- **Never call `input_layer` from the health_manager or death_manager threads.** Those threads only read screen state. All input goes through the bot thread.
- **`set_panel_check_paused(True)`** must be called before opening any UI panel (vendor, stash, WP). Forgetting it causes health_manager to misread HP through the panel overlay and false-chicken.
- **`_stash_mutex`** on `Bot` must be held during transmutes (stash is open). Acquired in `on_maintenance()` after stash opens.
---
## Debugging Tips
**Template match failures** — Add `save_debug=True` to `template_finder.search_and_wait(...)` to dump the failed match image to `log/screenshots/debug/`.
**Path node failures** — `pather.py` traverse failures usually mean the bot is in the wrong position. Check the log for the node sequence — the last successful node before the failure shows where the bot got lost.
**Verify D2R window** — `screen.find_and_set_window_position(force=True)` re-detects the window. Call this before template searches in flaky areas (Pindle portal approach does this).
**Adding step tracking to a new failure point** — just set `self.approach_fail_step = "descriptive_name"` before `return False`. No other wiring needed.
---
## File Quick Reference
```
src/bot.py main state machine, maintenance loop
src/run/*.py one file per boss run
src/town/town_manager.py orchestrates all town NPC interactions
src/town/a1.py .. a5.py per-act NPC/WP/stash implementations
src/input_layer/win_input.py mouse_move(), key_press(), SendInput wrappers
src/input_layer/mouse_impl.py humanized Bezier mouse paths
src/pather.py node-based pathfinding
src/template_finder.py OpenCV template matching
src/screen.py window detection, screenshot, coord conversion
src/d2r_image/bnip_data.py NTIP alias maps (quality, stat, flag)
src/item/pickit.py item pickup decision logic
src/health_manager.py background HP/mana potion auto-drinker
src/death_manager.py death detection + recovery
src/config.py Config singleton
config/params.ini user config (routes, difficulty, Discord)
config/game.ini D2R UI coordinates, template ROIs
scripts/stash_inventory.py scan all 6 stash pages → log/stash_inventory.json
scripts/make_stash_csv.py convert stash_inventory.json → stash_list.csv (trade list)
stash_list.csv deduplicated item list (name, page, stats); auto-updated by bot
log/log.txt current session log
log/stats/events_*.jsonl per-run event stream
```
+44
View File
@@ -0,0 +1,44 @@
# Botty working-state dependencies (verified 2026-06-11)
Everything required for the bot to run as well as it did during the verified
full Diablo runs on 2026-06-11. If a future setup misbehaves, diff against this.
## Runtime stack
| Layer | Requirement | Verified value |
|---|---|---|
| Python env | conda env `botty` | `C:\ProgramData\miniforge3\envs\botty` (python 3.10.14) — NOTE: `C:\Users\alex\miniforge3\envs\botty` is a broken leftover (no python.exe); `find_python.bat` skips it correctly |
| OCR binary | Tesseract 5.5.0 (winget) | `C:\Program Files\Tesseract-OCR\tesseract.exe` — wired in `src/d2r_image/ocr.py` (env var `PYTESSERACT_TESSERACT_CMD` → PATH → this default). Conda tesseract must stay UNINSTALLED (access violations) |
| OCR backend | pytesseract fallback | tesserocr wheel is DLL-broken (needs Tesseract 4.x libs) — pytesseract is the working path; startup logs `OCR backend: pytesseract (fallback)` |
| Launcher | `run_botty.bat` | sets conda-like PATH, UTF-8, TESSDATA. No exe build exists — bot always runs current source |
## Key python packages (installed, working)
```
opencv-python==4.5.5.64 numpy==1.26.4 pytesseract==0.3.13
mss==7.0.1 beautifultable==1.1.0 colorama==0.4.6
discord.py==2.7.1 aiohttp==3.14.1 certifi==2026.5.20
pillow==12.2.0 rapidfuzz==2.15.1 pywin32==312
psutil==7.2.2 cryptography==48.0.1
```
## D2R requirements (template matching breaks without these)
- Settings must match `assets/d2r_settings.json` — startup warns loudly if not.
Verified in-game 2026-06-11: 1280x720 windowed, resolution scale 100, DLSS OFF,
AA OFF, AO OFF, texture HIGH, character/environment/transparency/shadow LOW.
(DLSS ON was the root cause of the CS template failures earlier that day.)
- Window: bot enforces client area at (5, 98) size 1280x720 (`enforce_d2r_window`).
- Keybinds file: `C:\Users\alex\Saved Games\Diablo II Resurrected\Fistman*.keyo`
(auto-parsed at startup). Skill hotkeys in `config/params.ini` must match the
in-game skill assignments: blessed_hammer=f1, holy_shield=f2, redemption=f3,
vigor=f4, conviction=f5, concentration=f8, teleport=b, BO=7, BC=8.
Use `tools/capture_skill_hotkeys.py` to verify/capture them from the live game.
- Char: hammerdin "fistman", Hell, CTA swap on weapon slot 2.
## Host specifics
- Windows 11 build 26200, display 1920x1200 physical at 125% scaling (1536x960
logical). D2R renders 1:1 physical. Any DPI-unaware automation (PowerShell
SetCursorPos/mouse_event) lands 1.25x off — use the bot's input_layer from the
conda env with `SetProcessDPIAware()` instead.
- Mouse mode: relative (Win11) with SetCursorPos retry fallback (`win_input.py`).
- `max_consecutive_fails=5`, `max_game_length_s=900`, maintenance timeout ~280s,
per-game WP failure budget = 2 (town_manager) are the safety nets that keep a
bad game cheap.
+417
View File
@@ -0,0 +1,417 @@
# Botty D2R - Improvement Ideas
Generated: 2026-05-20 | Status: Partially implemented
Based on analysis of ~100+ source files across src/, test/, config/, and assets/.
Character: FOH Paladin | Priority: Anti-cheat stealth > everything else
---
## DONE
- [x] **C8** Bug: personal.py `open()` -> `open_inventory()` (fixed)
- [x] **C9** Bug: FoHdin missing PickIt (fixed)
- [x] **C10** Bug: game_controller race condition - `game_stats` now in `__init__()` (fixed)
- [x] **M15** Bug: TARGET_ASPECT_MIN duplicate removed (fixed)
- [x] **M17** Bug: chest.py relative path -> `Path(__file__)` absolute (fixed)
- [x] **Quick win**: All 47 bare `time.sleep()` replaced with `wait()` (15 files fixed)
- [x] **Quick win**: Stealth fallback `Logger.warning()` added to mouse_impl.py (3 blocks)
- [x] **Quick win**: `requirements.txt` created (120 lines, 20+ deps)
- [x] **Quick win**: `ruff.toml` created (Python 3.10, line-length 120)
- [x] **Tool**: `asset_manager.py` created (inventory, search, key, audit, quality, similarity, capture, crop, auto_crop, validate, cleanup, batch)
---
## CRITICAL - DO FIRST
- [x] **C1. Stealth: Consolidate all timing through centralized wait()**
- [x] All 47 bare `time.sleep()` replaced with `wait()` (which has Gaussian jitter)
- [x] Verified no remaining bare `time.sleep()` in `src/` (except inside `utils.misc.wait`)
### C2. Stealth: Add variable typing rhythm
`win_input.py` `send_text()` uses fixed `0.05` per character.
Real humans type with 0.02-0.12 per character with variation.
Fix: Add per-character randomization to `send_text()`.
### C3. Stealth: Human curve complexity should adapt to distance
`mouse_impl.py` `HumanCurve` uses static parameters.
Short movements (teleport to adjacent tile) should be simpler/faster.
Long movements should have more complex arcs.
Fix: Add distance-to-complexity mapping in `HumanCurve.__init__()`.
### C4. Stealth: Variable pathing speed within single movement arc
Current implementation varies timing BETWEEN movements but not WITHIN one.
Real humans speed up and slow down during a single mouse arc.
Fix: Add per-segment timing variation in `HumanCurve` execution loop.
### C5. Stealth: Extend endpoint wobble to ALL click sequences
~~`mouse_impl.py` `endpoint_wobble()` only fires for `stealth_move()`.~~
~~Most paths use regular `mouse.move()` + `mouse.click()` with no wobble.~~
~~Fix: Make `stealth_move()` the default, or add wobble to regular click flow.~~
(Still needed - stealth fallback logging added but wobble not yet extended to all clicks.)
### C6. ~~Stealth: Screen capture timing jitter~~
~~`screen.py` uses `dxcam` with perfectly regular capture intervals (~80ms).~~
~~Anti-cheat can detect this regular polling pattern.~~
~~Fix: Add 5-10% jitter to grab timing intervals.~~
(Done - all timing now routes through `wait()` with jitter.)
### C7. ~~Stealth: Input event spacing jitter~~
~~`hotkey.py` `GetAsyncKeyState` polling runs at exactly 50Hz.~~
~~Real human keyboard polling is variable.~~
~~Fix: Add microsecond-level jitter to polling intervals.~~
(Done - all `time.sleep()` in hotkey.py replaced with `wait()` calls.)
### C8. ~~Bug: Fix personal.py open() shadowing (line 60)~~ ~~(DONE)~~
### C9. ~~Bug: Fix FoHdin missing PickIt (bot.py line 72)~~ ~~(DONE)~~
- [x] **C10. Bug: Replace thread killing with cooperative shutdown**
- [x] `utils.misc.kill_thread()` now prefers `cooperative_shutdown()`
- [x] Added `register_stop_condition` to `utils.misc`
- [x] Centralized `wait()` and `search_and_wait()` now check for shutdown signals
- [x] `Bot`, `HealthManager`, and `DeathManager` register their stop conditions
---
## HIGH PRIORITY
### H1. Stealth: Add AFK countermeasures during ALL idle states
`stealth.py` `maybe_afk_break()` only runs during run transitions.
Should also run during health monitoring idle periods and town states.
Fix: Add micro mouse adjustments during health/death manager idle loops.
### H2. Stealth: Add "thinking" pauses before major actions
Before executing major actions (entering waypoint, starting run), add 0.5-3s "deliberation" pause.
Humans plan before acting.
Fix: Add configurable pause in `bot.py` state transitions.
### H3. Stealth: Randomize action order in town
Bot always does: belt update -> stash -> waypoint (same order every time).
Real players vary their town routine.
Fix: Randomize which town tasks are performed first in `town_manager.py`.
### H4. Stealth: Add variable "reaction time" to potion drinking
When health drops, bot should NOT react instantly.
Add 100-500ms delay to potion drinking to simulate human reaction time.
Fix: Add reaction delay in `health_manager.py` before invoking heal skill.
### H5. Stealth: Mouse trail entropy / Brownian overlay
Record actual mouse path during movements and add small random perturbations
that accumulate over time. Makes movement traces look more human.
Fix: Add Brownian motion overlay in `mouse_impl.py` `HumanCurve`.
### H6. Stealth: Click position micro-jitter
Before and after every click, add a tiny 1-3 pixel random offset.
Real humans don't click at exactly the same coordinates.
Fix: Add jitter in `mouse_impl.py` click methods.
### H7. Performance: Pre-load templates into memory at startup
`template_finder.py` loads PNGs with `cv2.imread()` on each search call.
With 1111+ templates, this wastes CPU on every detection cycle.
Current: Has `@cache` on `stored_templates()` but cache is never invalidated.
Fix: Good as-is if cache works. Verify cache isn't being bypassed in hot paths.
### H8. Performance: Cache screen grabs in tight pathing loops
`pather.py` `traverse_nodes()` calls `grab(force_new=True)` for every node.
If retrying quickly, the frame won't have changed.
Fix: Cache last grab timestamp; skip if less than 16ms since last grab.
### H9. Performance: Convert BGR->HSV once, apply all color masks
`utils.misc.color_filter()` converts BGR->HSV for every filter range.
With multiple NPC templates, this creates redundant conversions.
Fix: Convert once, apply all masks from single HSV image.
### H10. Architecture: Split pather.py (750 lines)
Handles node definition, path data, traversal, offset management, AND debug main.
Split into:
- path_data.py (constants and route definitions)
- path_traversal.py (traverse_nodes logic)
- path_utils.py (node offset helpers)
### H11. Architecture: Split config.py (29,500 chars)
Loads, validates, and caches all configuration in one massive file.
Split into:
- config_loader.py (read params.ini / config.yaml)
- config_schema.py (Pydantic/dataclass validation)
- config_defaults.py (default values per section)
### H12. Architecture: Encapsulate health_manager global state
Uses module-level globals `pause_state` and `panel_check_paused` with getter/setter functions.
Should be encapsulated in `HealthManager` class instance for thread safety.
### H13. Architecture: Fix npc_manager deferred init fragility
`npcs` dict is populated in try/except at module level, silently staying empty on failures.
If templates load late, `open_npc_menu()` crashes with unhelpful KeyError.
Fix: Add explicit initialization check with clear error message.
### H14. Architecture: Add thread safety to shared state
`health_manager.py` and `death_manager.py` share `set_pause_state` without mutex/lock.
Bot uses `self._stash_mutex` for stashing but health/death managers don't.
Fix: Add threading.Lock around shared state access.
### H15. Configuration: Add schema validation to params.ini
A typo in any key name causes runtime crash deep in the call stack.
Fix: Use Pydantic or dataclasses with defaults and type hints.
### H16. Configuration: Eliminate config duplication
Default values are scattered across params.ini AND config.py with hardcoded fallbacks.
Fix: Single source of truth - either params.ini with config.py as schema only,
or config.py with params.ini as user overrides.
### H17. Configuration: Add inline config documentation
No comments explaining what each param does, valid ranges, or D2R version compatibility.
Fix: Add docstrings to config sections with examples and ranges.
### H18. Asset management: Add startup health check
If a template file is missing or corrupted, the bot crashes mid-run.
Fix: Add startup validation that checks all required templates exist and load.
### H19. Asset management: Compress PNG templates with lossless optimization
1111+ PNG files. Many can be reduced with optipng/pngcrush without quality loss.
Fix: Run `optipng -o7` on all assets/ PNGs.
### H20. Error handling: Add timeout to _do_chicken
If D2R is frozen/unresponsive, `view.fast_save_and_exit()` hangs indefinitely.
Fix: Add timeout with fallback `taskkill` on timeout.
### H21. Error handling: Add graceful degradation for missing features
If `d2r_image` fails (OCR/library issues), the bot crashes entirely.
Fix: Add fallback pathfinding mode without OCR, with clear warning.
### H22. Error handling: Add total path timeout to traverse_nodes
Per-node timeout exists but total traversal can be extremely long if many nodes barely timeout.
Fix: Add cumulative path timeout in `pather.py` `traverse_nodes()`.
### H23. Error handling: Add verbose failure logging to NPC interaction
`open_npc_menu()` returns False after 35s but gives no diagnostic about WHY.
Fix: Log which template failed, current screen state, and suggested fixes.
---
## MEDIUM PRIORITY
### M1. Code quality: Duplicate code in i_char.py move()/walk()
`move()` and `walk()` methods (lines 214-261) are nearly identical.
Walk distance adjustment logic is copied verbatim.
Fix: Extract to `_adjust_walk_position()` helper.
### M2. Code quality: Silent failures in stealth fallback logging
`mouse_impl.py` has bare `except Exception:` blocks around stealth features (lines 178-183, 209-227).
These silently fail without logging, making stealth debugging impossible.
Fix: Add `Logger.warning()` in all stealth fallback except blocks.
### M3. Code quality: Inconsistent import style
Some files use `from config import Config`, others `import template_finder`, others `from screen import grab`.
Fix: Standardize on absolute imports throughout.
### M4. Code quality: Remove `__main__` blocks from production modules
Nearly every module has a standalone test block at the bottom that imports and configures
the full environment, making `import *` unreliable.
Fix: Move to dedicated test directory.
### M5. Code quality: Add full type hints coverage
Python 3.10+ type hints exist in some places but are incomplete.
`pather.py` has 750 lines with minimal typing.
Fix: Add type annotations to all public methods and data structures.
### M6. Code quality: Standardize asset naming convention
Some use `_BACK`, some use `_SIDE_2`, some use `_0`, `_45`, `_135` angles.
Fix: `NPCNAME_ANGLE_VARIANT.png` (e.g. `akara_front_1.png`, `akara_side_45_1.png`)
### M7. Developer experience: Add requirements.txt / pyproject.toml
Dependencies are scattered: `dxcam`, `opencv-python`, `pyparsing`, `rapidfuzz`, `numpy`, `colorama`, `transitions`.
Fix: Single `requirements.txt` or `pyproject.toml` with pinned versions.
### M8. Developer experience: Add linting/formatting config
No `ruff.toml`, `pyproject.toml`, `.flake8`, or `black` config. Code style is inconsistent.
Fix: Add `ruff.toml` with consistent formatting rules.
### M9. Developer experience: Extract debug mode from production code
`if Config().general["info_screenshots"]:` checks pollute every module.
Fix: Extract to a `@debug_if` decorator or context manager.
### M10. Developer experience: Add CI/CD pipeline
No `.github/workflows/`, no GitHub Actions, no automated test runner.
Fix: Add GitHub Actions for linting + tests on push/PR.
### M11. Testing: Add unit tests for core logic
`pather.py`, `bot.py`, `game_controller.py` have ZERO tests.
Fix: At minimum, add tests for state machine transitions in bot.py.
### M12. Testing: Improve test mocks
`test/mocks/screen_mock.py` doesn't mock `grab()`, `convert_*()` comprehensively.
Many tests likely skip silently.
Fix: Add comprehensive mocks for screen, mouse, and keyboard.
### M13. Testing: Add integration test for full run cycle
A lightweight test validating bot start->run->town cycle would catch regressions.
Fix: Add `test/integration/test_run_cycle.py` with mocked D2R.
### M14. Bug: PickedUpResult enum has gap (values 0,1,3,4,5 - missing 2)
Will cause issues if anyone iterates expecting contiguous integers.
Fix: Either fill gap or use named values only (don't rely on int values).
### M15. Bug: TARGET_ASPECT_MIN defined twice in target_detect.py
Lines 21-22 define as 0.5, lines 26-27 redefine as 0.4. Second wins, first is dead code.
Fix: Remove the dead definition.
### M16. Bug: game_controller.py self.game_stats race condition
`self.game_stats.get_consecutive_runs_failed()` called at line 72,
but `game_stats` is only set in `start()` at line 128.
Fix: Initialize `game_stats` in `__init__()` with default/None.
### M17. Bug: chest.py hardcoded relative path
`os.listdir("assets/chests/")` with relative path fails if bot runs from different cwd.
Fix: Use `Path(__file__).parent.parent / "assets" / "chests"`.
### M18. Bug: death_manager callback set to None after first fire
If death screen appears during recovery, callback won't fire again.
Fix: Re-register callback after each death handling.
### M19. Asset: hud_mask.png uses hardcoded absolute path
`ui_manager.py` references `assets/hud_mask.png` with absolute path.
Fix: Use same asset resolution system as other templates.
### M20. Architecture: Singleton anti-pattern in Config()
Creates new instance every call but caches via `@lru_cache`.
Multiple modules import redundantly.
Fix: Consider application context that passes config once, or document the caching behavior clearly.
---
## FOH PALADIN SPECIFIC
### F1. Mercenary healing optimization
FOH mercenary takes heavy damage. Current thresholds wait too long.
Fix: Proactive mercenary health monitoring (heal at 75% instead of waiting for thresholds).
### F2. Bottle of Holy Water targeting
FOH builds use BoWH on undead/demons. No logic to detect monster type and switch BoWH on/off.
Fix: Add monster class detection with BoWH toggle.
### F3. Corpse retrieval strategy
`ScreenObjects.Corpse` exists but no logic to navigate to corpse and recover items.
FOH is tanky but can still die on high-tier runs.
Fix: Add corpse recovery routine in death/recovery flow.
### F4. Automatic rebuff detection
FOH needs Vigor + Concentrate + Redemption. If any aura drops (merc dies), bot should re-cast.
Fix: Add aura monitoring in health_manager or combat loop.
### F5. Portal position intelligence
`tp_town()` uses hardcoded ROI and tries fixed positions.
Fix: Add template matching to verify portal opened in expected location before clicking through.
---
## QUICK WINS (Low effort, high impact)
- [x] Run `optipng -o7` on all assets (saves disk space + load time)
- [x] Add `requirements.txt` (120 lines, 20+ deps)
- [x] Add `ruff.toml` (line-length 120, Python 3.10)
- [x] Fix personal.py `open()` shadowing -> `open_inventory()`
- [x] Fix FoHdin missing PickIt
- [x] Fix TARGET_ASPECT_MIN duplicate
- [x] Add `asset_manager.py` (unified asset management tool)
- [x] Replace 47 bare `time.sleep()` with `wait()` (15 files)
- [x] Add logger.warning() to stealth except blocks (mouse_impl.py)
- [ ] Run `optipng -o7` on all assets (still pending - use `asset_manager.py batch` or run manually)
- [ ] Fix `pather.py` hardcoded relative path (similar to chest.py fix)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Alex
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+212 -79
View File
@@ -1,16 +1,52 @@
# <img src="assets/docs/header_green.png" width="370">
Pixelbot for Diablo 2 Resurrected. This project is for informational and educational purposes only and is not meant for online usage. Feel free to dig around, add stuff, make PRs, or ask questions should you get stuck!
Pixelbot for Diablo 2 Resurrected. This project is for informational and educational purposes only.
[**Download here**](https://github.com/aeon0/botty/releases) and got to have a [**Discord**](https://discord.gg/Jf3J8cuXWg) nowadays I guess :man_shrugging:
## Installation (first time)
**Step 1 — Install Miniforge** (only needed once, skip if you already have conda/Miniconda)
Download and run the installer from: https://github.com/conda-forge/miniforge/releases/latest
Pick the Windows x86_64 `.exe`. Keep defaults; tick "Add to PATH" if asked.
**Step 2 — Download Botty**
Click the green **Code** button on this GitHub page → **Download ZIP**. Extract the ZIP anywhere (e.g. `C:\botty`).
**Step 3 — Install dependencies**
Double-click **`install.bat`** inside the extracted folder. It will create the `botty` conda environment and install everything. This takes a few minutes the first time.
`install.bat` detects Windows 10 vs Windows 11 automatically:
- Windows 10 uses `environment-win10.yml`, `requirements-win10.txt`, and absolute mouse input.
- Windows 11 uses `environment-win11.yml`, `requirements-win11.txt`, and relative mouse input.
Botty also runs OS detection at startup and logs the selected requirements profile and mouse mode.
**Step 4 — Configure**
Open `config\params.ini` in Notepad and set at minimum:
- `[char] type=` — your build (`sorceress`, `hammerdin`, `paladin`, `trapsin`, …)
- `[routes] order=` — which bosses to farm (e.g. `run_pindle`)
- Hotkeys under your build's section to match your D2R keybinds
**Step 5 — Start**
Double-click **`run_botty.bat`**. Switch to D2R, go to the hero selection screen, then press **F11** to start. Press **F12** to stop.
> D2R must be in **English** and running at **720p** window mode.
Optional personal setup:
- Copy `.env.example` to `.env` in the repo root and set personal values there.
- `.env` is git-ignored so multiple testers can use different local values without git conflicts.
---
## Getting started & Prerequisites
- D2R needs to be in English Language,
- Botty currently works with 720p D2R window (will be adjusted automatically on auto settings)
### 1) Graphics and Gameplay Settings
All settings will automatically be set when you execute `main.exe` and press the hotkey for "Adjust D2R settings" (default f9). It is not a 100% thing, in rare cases you might still have to fiddle around with your brightness. I suggest using the "Graphic Debugger" to verify your settings.
All settings will automatically be set when you execute `main.exe` and press the hotkey for "Adjust D2R settings" (default **ctrl+f9**). It is not a 100% thing, in rare cases you might still have to fiddle around with your brightness. I suggest using the "Graphic Debugger" to verify your settings.
**Note**: Make sure that no other external programs adapt your graphics settings (HDR, Geforce Experience, etc.)
### 2) Supported builds
@@ -23,17 +59,24 @@ Open up D2R and wait till you are at the hero selection screen. Make sure the ch
### 4) Start Botty
You can either run from python. Follow [development.md](development.md) for that. Or you download the a prebuilt release [here](https://github.com/aeon0/botty/releases) (the .rar file!). Start `main.exe` in the botty folder. Focus your D2R window and press the start key (default f11). You can always force stop botty with f12. Note: Botty will use the /nopickup command in the first game to avoid pickup up trash while traversing. This command will only allow item pickup when "show items" is active.
- **Quick start**: Double-click `run_botty.bat` (auto-detects your conda env)
- **Manual**: `conda activate botty` then `python src\main.py`
After starting, focus your D2R window and press the start key (default f11). You can always force stop botty with f12. Note: Botty will use the /nopickup command in the first game to avoid pickup up trash while traversing. This command will only allow item pickup when "show items" is active.
### Stability and safety updates
- XP OCR parsing now tolerates common OCR mistakes (`I/l/| -> 1`, `O/o -> 0`, mixed-case "experience").
- XP status math no longer throws on early-session edge cases; unavailable projections show as `n/a`.
- Routine repair is best-effort for native-teleport builds to reduce fail spirals when NPC detection is flaky.
- Repair fallback from A5 now attempts A4 from Larzuk location for better path reliability.
- Discord message sending now guards invalid embed payloads and has a plain-text fallback.
- Selling now logs item names (not just positions) and shields are protected by default.
## Development
Check out the [development.md](development.md) docu for infos on how to build from source and details of the project structure and code.
## Support this project 💓
You can support this project by giving feedback, reporting bugs, or creating pull requests.
Contributions are welcome, and we encourage you to contribute to this project if you would like to help out. Botty is a open source project and almost excessively maintained by contributors (there has been 50+ contributors! <3). In our [**discord**](https://discord.gg/Jf3J8cuXWg) there is a contributor role, and you can ping one of the admins and ask for the role to talk to other contributors! Though you don't need to be in the [**discord**](https://discord.gg/Jf3J8cuXWg) to contribute, we do encourage you to do so.
## BNIP Pickit
Botty NIP (BNIP) is an extended version of Njaguar's Item Parser (NIP).
@@ -99,7 +142,10 @@ order=run_pindle, run_eldritch
| [routes] | Descriptions |
| ------------ | ------------------------------------------------------------------------ |
| order | List of runs botty should do. These will be run in the the order listed unless `randomize_runs` is set to 1. Possible runs: </br> run_trav, run_pindle, run_eldritch, run_eldritch_shenk, run_nihlathak (requires teleport), run_arcane (requires teleport), run_diablo (requires teleport, only hammardin)
| order | Comma-delimited run list. If `randomize_runs=0`, Botty executes left-to-right. If `randomize_runs=1`, enabled runs are shuffled each game. Possible runs: </br> run_trav, run_pindle, run_eldritch, run_eldritch_shenk, run_nihlathak (teleport strongly recommended), run_arcane (teleport strongly recommended), run_diablo (teleport recommended), run_vizier, run_andariel, run_countess, run_mephisto, run_baal |
Hammerdin keyrun example (stable-first):
`order=run_countess, run_arcane, run_nihlathak`
| [char] | Descriptions |
| ------------------ | -------------------------------------------------------------------------------------------------|
@@ -153,7 +199,6 @@ order=run_pindle, run_eldritch
| enable_no_pickup | When enabled, will type `/nopickup` into chat at game start, which can help reduce accidental pickups especially for walking characters. |
| fill_shared_stash_first | Fill stash tabs starting from right to left, filling personal stash last |
| gamble_items | List of items to gamble when stash fills with gold. Leave blank to disable. Supported items currently include circlet, ring, coronet, talon, amulet
| id_items | Will identify items with tome of ID or at Cain if enabled |
| open_chests | Open up chests in some places. E.g. on dead ends of arcane. |
| pre_buff_every_run | 0: Will only prebuff on first run, 1: Will prebuff after each run/boss |
| runs_per_repair | Force repair after `runs_per_repair` of runs. Set to 0 to repair only when needed. |
@@ -166,84 +211,172 @@ order=run_pindle, run_eldritch
| transmute | Add any or all of `chipped, flawed, standard, flawless` to trasmute gems of these types |
| transmute_every_x_game | How often to run transmute routine (currently transmutes flawless gems into perfect gems). Transmute routine depends on stashing routine it will only start after items stashing is done. E.g. so it could take more than X games to perform transmutes if there were no items to stash at the time. Default: 20 |
### Stealth Mode
Botty includes a built-in stealth/anti-detection system configured in `[stealth]` section of params.ini. All features aim to make bot behaviour look more human.
|| [stealth] | Descriptions |
|| ------------------------------ | ----------------------------------------------------------------------------- |
|| wait_jitter_min / wait_jitter_max | Multiplier range applied to all wait() calls for timing variation |
|| click_variance | Extra pixel variance added to every mouse click (0 = off) |
|| reshuffle_each_rotation | Re-shuffle run order after each full rotation (vs. only at session start) |
|| skip_run_chance | Probability (0-100) of randomly skipping a run for stealth |
|| afk_break_chance | Probability (0-100) of taking an unscheduled AFK break |
|| afk_break_min_m / afk_break_max_m | Duration range (in minutes) for AFK breaks |
|| run_duration_variance | Gaussian variation factor for run/battle duration (0.15 = +/-15%) |
|| micro_pause_min_ms / micro_pause_max_ms | Random micro-pauses between actions (simulates human hesitation) |
|| click_delay_min_ms / click_delay_max_ms | Delay between arriving at a target and clicking (50-800ms default) |
Additional stealth features include endpoint wobble (pixel-level hand tremor simulation), variable key press durations, and naturalistic mouse movement paths via a motion planner.
### Key Auto-Detection
Botty can auto-detect D2R key bindings from your character's `.key` / `.keyo` file in `~\Saved Games\Diablo II Resurrected`. Run `python tools/test_key_detector.py` to test parsing. The key detector reads the binary key file format and populates `config.char` values (inventory, potions, teleport, skills, etc.) so you don't need to set them manually in params.ini.
### Click Recorder
A click recording and playback tool is included for debugging and path testing:
```bash
python tools/click_recorder.py record # F11=start/stop, F12=exit
python tools/click_recorder.py playback # replay with human-like timing
```
Records mouse clicks with timestamps and replays them with configurable speed and repeat count.
### Asset Manager
A comprehensive tool for managing bot assets (templates). It helps with inventorying, auditing, searching, and optimizing template images.
```bash
python asset_manager.py inventory # List all templates
python asset_manager.py audit # Check for missing/low-quality templates
python asset_manager.py search <name> # Search for a specific template
python asset_manager.py quality # Analyze template quality (SNR, contrast)
python asset_manager.py batch resize 64x64 # Batch process templates
```
Features include similarity checking (to find duplicates), automatic cropping, and validation of template paths.
### Asset Extractor
A workflow tool for capturing and cropping new templates from D2R screenshots. Designed to work alongside an AI agent for rapid asset generation.
```bash
python asset_extractor.py
```
- **F1**: Capture D2R screen to `screenshots/debug/latest.png`.
- **F2**: Crop entities using an AI-generated `latest_annotations.json` file.
- **F3**: List all currently extracted assets.
### Builds
| [sorceress] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
| energy_shield | Optional Hotkey for energy shield |
| thunder_storm | Optional Hotkey for thunder storm |
| static_field | Optional Hotkey for static field |
| telekinesis | Optional Hotkey for telekinesis |
|| [sorceress] | Descriptions |
|| ------------- | ----------------------------------------------------------------------------- |
|| frozen_armor | Optional Hotkey for frozen armor (or any of the other armors) |
|| energy_shield | Optional Hotkey for energy shield |
|| thunder_storm | Optional Hotkey for thunder storm |
|| static_field | Optional Hotkey for static field |
|| telekinesis | Optional Hotkey for telekinesis |
| [light_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| chain_lightning | Optional Hotkey for chain_lightning (must be bound to left skill) |
| lightning | Required Hotkey for lightning (must be bound to right skill) |
| frozen_orb | Optional Hotkey for frozen orb (must be bound to right skill) |
|| [light_sorc] | Descriptions |
|| ------------- | ----------------------------------------------------------------------------- |
|| chain_lightning | Optional Hotkey for chain_lightning (must be bound to left skill) |
|| lightning | Required Hotkey for lightning (must be bound to right skill) |
|| frozen_orb | Optional Hotkey for frozen orb (must be bound to right skill) |
| [blizz_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| blizzard | Required Hotkey for Blizzard (must be bound to right skill) |
| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
|| [blizz_sorc] | Descriptions |
|| ------------- | ----------------------------------------------------------------------------- |
|| blizzard | Required Hotkey for Blizzard (must be bound to right skill) |
|| ice_blast | Optional Hotkey for ice_blast (must be bound to left skill) |
| [nova_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| nova | Required Hotkey for Nova (must be bound to right skill) |
|| [nova_sorc] | Descriptions |
|| ------------- | ----------------------------------------------------------------------------- |
|| nova | Required Hotkey for Nova (must be bound to right skill) |
| [hydra_sorc] | Descriptions |
| ------------- | ----------------------------------------------------------------------------- |
| alt_attack | Required Hotkey for any alternate attacking skill. Fireball,Lightning,Frozen Orb, etc. (must be bound to right skill) |
| hydra | Required Hotkey for Hydra (must be bound to right skill) |
|| [hydra_sorc] | Descriptions |
|| ------------- | ----------------------------------------------------------------------------- |
|| alt_attack | Required Hotkey for any alternate attacking skill. Fireball,Lightning,Frozen Orb, etc. (must be bound to right skill) |
|| hydra | Required Hotkey for Hydra (must be bound to right skill) |
| [paladin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| cleansing | Optional Hotkey for Cleansing |
| holy_shield | Required Hotkey for Holy Shield |
| redemption | Optional Hotkey for Redemption |
| vigor | Optional Hotkey for Vigor |
|| [paladin] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| cleansing | Optional Hotkey for Cleansing |
|| holy_shield | Required Hotkey for Holy Shield |
|| redemption | Optional Hotkey for Redemption |
|| vigor | Optional Hotkey for Vigor |
| [fohdin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| blessed_hammer | Hotkey for Blessed Hammer. (Optional. Bind to left skill) |
| concentration | Hotkey for Concentration |
| conviction | Hotkey for Conviction |
| foh | Hotkey for Fist of Heavens (Required) |
| holy_bolt | Hotkey for Holy Bolt (Required) |
|| [fohdin] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| blessed_hammer | Hotkey for Blessed Hammer. (Optional. Bind to left skill) |
|| concentration | Hotkey for Concentration |
|| conviction | Hotkey for Conviction |
|| foh | Hotkey for Fist of Heavens (Required) |
|| holy_bolt | Hotkey for Holy Bolt (Required) |
FoHdin (Fist of the Heavens) is a FOH-based FoH Paladin build that uses Feign of Health to group monsters, then attacks with Fist of the Heavens and Holy Bolt. Works well for Hell difficulty farming runs.
| [hammerdin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| concentration | Required Hotkey for Concentration |
| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
|| [hammerdin] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| concentration | Required Hotkey for Concentration |
|| blessed_hammer | Required Hotkey for Blessed Hammer. (must be bound to left skill!) |
| [trapsin] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| burst_of_speed | Optional Hotkey for Burst of Speed |
| death_sentry | Required Hotkey for Death Sentry |
| fade | Optional Hotkey for Fade |
| lightning_sentry | Required Hotkey for Lightning Sentry |
| shadow_warrior | Optional Hotkey for Shadow Warrior |
| skill_left | Optional Hotkey for Left Skill |
|| [trapsin] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| burst_of_speed | Optional Hotkey for Burst of Speed |
|| death_sentry | Required Hotkey for Death Sentry |
|| fade | Optional Hotkey for Fade |
|| lightning_sentry | Required Hotkey for Lightning Sentry |
|| shadow_warrior | Optional Hotkey for Shadow Warrior |
|| skill_left | Optional Hotkey for Left Skill |
| [barbarian] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
| find_item | Optional Hotkey for Find Item |
| leap | Required Hotkey for Leap |
| shout | Required Hotkey for Shout |
| war_cry | Required Hotkey for War Cry |
|| [barbarian] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| cry_frequency | Time in seconds between each cast of war_cry. Set to 0.0 if max fcr should be used |
|| find_item | Optional Hotkey for Find Item |
|| leap | Required Hotkey for Leap |
|| shout | Required Hotkey for Shout |
|| war_cry | Required Hotkey for War Cry |
| [Necro] | Descriptions |
| -------------- | ----------------------------------------------------------------------------------- |
| skill_left | Required Hotkey for attack (bonespear/teeth) |
| bone_armor | Required Hotkey for Bone Armor |
| clay_golem | Required Hotkey for Clay Golem |
| raise_skeleton | Required Hotkey for Raise Skeleton |
| amp_dmg | Required Hotkey for Amplify Damage |
| corpse_explosion | Required Hotkey Corpse Explosion |
| raise_revive | Required Hotkey revive |
| damage_scaling | Adjusts time spent casting attack skills. Ex: 2 will cast twice as long |
| clear_pindle_packs | Clears mobs before pindle |
|| [Necro] | Descriptions |
|| -------------- | ----------------------------------------------------------------------------------- |
|| skill_left | Required Hotkey for attack (bonespear/teeth) |
|| bone_armor | Required Hotkey for Bone Armor |
|| clay_golem | Required Hotkey for Clay Golem |
|| raise_skeleton | Required Hotkey for Raise Skeleton |
|| amp_dmg | Required Hotkey for Amplify Damage |
|| corpse_explosion | Required Hotkey Corpse Explosion |
|| raise_revive | Required Hotkey revive |
|| damage_scaling | Adjusts time spent casting attack skills. Ex: 2 will cast twice as long |
|| clear_pindle_packs | Clears mobs before pindle |
| [advanced_options] | Descriptions |
| -------------------- | --------------------------------------------------------------------- |
|| [advanced_options] | Descriptions |
|| -------------------- | --------------------------------------------------------------------- |
## Tooling
### New route scaffolding
A CLI tool is provided to scaffold new farming routes in one step:
```bash
python3 tools/new_route.py --name <name> --display "<Display Name>" --act <1-5>
```
This creates `src/run/<name>.py` from a template and mutates `src/bot.py` and
`src/run/__init__.py` to wire the new route into the bot's state machine.
Options:
| Option | Description |
|--------|-------------|
| `--name` | Snake-case run name (e.g. `baal`, `smoketest`) |
| `--display` | Human-readable name for in-game location tracking (default: PascalCase of name) |
| `--act` | Act number 1-5 (default: 1) |
| `--location-id` | Optional location identifier |
| `--class-name` | Python class name (default: PascalCase of name) |
| `--dry-run` | Show what would change without writing files |
| `--undo NAME` | Reverse all mutations for a previously scaffolded route and remove the run file |
After scaffolding, edit the generated `src/run/<name>.py` to implement `approach()` and `battle()`
methods, then add `run_<name>` to the `[routes] order=` list in `config/params.ini`.
+111
View File
@@ -0,0 +1,111 @@
# Botty Stealth - Deep Dive
## What Warden (Battle.net Anti-Cheat) Can Detect
Warden is a kernel-level anti-cheat. It can:
- **Enumerate processes** - see `main.exe` running, check the process name and parent
- **Scan loaded DLLs** - look for known botting libraries (keyboard_io.dll, mousetool.dll, pyclick)
- **Monitor API calls** - detect patterns in `SendInput` frequency and timing
- **Analyze input timing** - bots have perfectly consistent timing; humans don't
- **Check mouse trajectory** - bots move in straight lines; humans move in curves
- **Monitor memory** - detect hooks, injected code, or unusual data patterns
## Our Stealth Strategy (3 Tiers)
### Tier 1: Input-Level Stealth (Already Working)
**What we fixed:**
- Removed `keyboard` library (installed `keyboard_io.dll` kernel driver - instant detection)
- Removed `mouse`/`pyclick` library (installed `mousetool.dll` kernel driver - instant detection)
- Replaced with `ctypes` + `user32.dll` `SendInput` (standard Windows API, no drivers)
- Hidden the console window (`--noconsole`) so no visible CMD window
- Randomized the exe name from `main.exe` to something like `whvitjz2.exe`
**Mouse movement stealth (in `input_layer/mouse_impl.py`):**
- Every `mouse.move()` generates a Bezier curve with random control points
- Gaussian distortion applied to the curve (simulates hand tremor)
- Endpoint wobble - final position is 2-5 pixels off, then corrected
- Click variance - target position is randomized by +/-8px (configurable)
- Arrival-to-click delay - 50-800ms pause between arriving and clicking (beta distribution)
- Distance-based timing - closer clicks are faster, farther clicks take longer
- Human curve complexity multiplier from config (default 1.0)
**Keyboard stealth (in `input_layer/__init__.py`):**
- Every `keyboard.send()` includes micro-pauses before and after (20-120ms)
- Key press duration varies (20-200ms, exponential distribution - most short, some linger)
- Combo keys (e.g. `shift + a`) press modifiers individually with timing between
**What's NOT yet wired:**
The stealth functions `human_key_press()` and `human_keyboard_send()` exist in `utils/stealth.py` but are called via the input layer shim automatically. Every `keyboard.send()` now gets stealth timing.
### Tier 2: Behavior-Level Stealth (Partially Integrated)
**Functions defined but NOT called from anywhere in char code:**
| Function | What it does | Where it SHOULD be used |
|---|---|---|
| `should_wrong_waypoint()` | 2.5% chance of clicking wrong waypoint | `ui/waypoint.py` when selecting TP portal |
| `skill_rotation_hesitation()` | 80-300ms pause before casting | Before every skill cast in char files |
| `should_correct_skill_mistake()` | 1.5% chance of miscasting then correcting | During skill rotation in combat |
| `randomize_click_position()` | Gaussian click offset | Before every `mouse.click()` |
**Current gap:** These functions exist but are NOT called from the character combat code. The char files call `keyboard.send("1")` directly without going through a stealth wrapper. The input layer adds micro-pauses, but the behavior-level stealth (wrong skill, hesitation) is not wired in.
**What IS wired:**
- `mouse.stealth_move()` calls `randomize_click_position()` and `endpoint_wobble()` automatically
- `mouse.click()` calls `apply_click_delay()` automatically
- `keyboard.send()` calls `_stealth_before()` / `_stealth_after()` (micro-pauses) and `_stealth_duration()` (variable press duration)
### Tier 3: Session-Level Stealth (Fully Working)
**Fully integrated in `bot.py`:**
| Function | Config | Behavior |
|---|---|---|
| `should_skip_run()` | `skip_run_chance = 10` | 10% chance to skip a run entirely (randomizes runtime) |
| `maybe_afk_break()` | `afk_break_chance = 5` | 5% chance after each run to take a 2-12 minute break |
| Run reshuffle | `reshuffle_each_rotation = 1` | Re-shuffles run order after each full rotation |
| `randomize_run_duration()` | `run_duration_variance = 0.15` | +/-15% variation on expected run time |
| Wait jitter | `wait_jitter_min = 0.85` / `max = 1.20` | Every `wait()` call is multiplied by 0.85x-1.20x |
## Current Stealth Summary
| Vector | Status | Notes |
|---|---|---|
| Kernel drivers | FIXED | No keyboard_io.dll or mousetool.dll |
| Process name | PARTIAL | Randomized but still visible as a running process |
| Console window | FIXED | `--noconsole` hides the window |
| Mouse trajectory | WORKING | Bezier curves + Gaussian distortion + wobble |
| Click timing | WORKING | 50-800ms arrival-to-click delay + click variance |
| Key press timing | WORKING | 20-200ms variable duration + micro-pauses |
| Run timing | WORKING | 10% skip chance, 5% AFK break, +/-15% duration variance |
| Wait jitter | WORKING | 0.85x-1.20x on all waits |
| Wrong waypoint | WORKING (2.5%) | Wired in waypoint.py |
| Skill mistakes | WORKING (1.5%) | Wired in input_layer/__init__.py |
| Skill hesitation | WORKING (80-300ms) | Wired in input_layer/__init__.py |
## Remaining Risks
1. **Process visibility**: Warden can still see a hidden process running alongside D2R. The exe name is random but the timing of when it starts (right when D2R launches) is suspicious.
2. **Click pattern analysis**: Even with Bezier curves, Warden might detect that every "path click" follows a curve while a human sometimes double-clicks or moves in straight lines.
3. **No idle mouse movement**: When the bot is fighting or waiting, the mouse is perfectly still. Humans constantly fidget with the mouse.
4. **Skill mistake only on 1-0 keys**: The skill mistake feature only fires for skill hotkeys (number keys 1-0). It doesn't apply to inventory keys, stand_still, or other non-skill actions.
## What Would Improve Stealth Further
### High Impact (Warden likely checks these)
1. **Idle mouse movement**: Add background thread that moves mouse 1-3 pixels randomly every 1-5 seconds (simulates hand resting on mouse)
2. **Occasional straight-line clicks**: 10% of the time skip the Bezier curve and click directly (humans don't always move in curves)
### Medium Impact
3. **Process parent spoofing**: Launch the bot from a different process (e.g. from Explorer instead of directly) to hide the relationship with D2R
4. **Variable scroll speed**: Add variance to `mouse.wheel()` calls (inventory scrolling)
5. **Right-click variation**: Sometimes right-click to cancel actions before re-doing them
### Low Impact (but good for completeness)
6. **Randomize AFK break more**: Current range is 2-12 minutes, could be wider
7. **Add personality per character**: Different timing distributions per character seed
8. **Typing delays**: When using text chat (if implemented), add keystroke-by-keystroke delays
+198
View File
@@ -0,0 +1,198 @@
"""
D2R Asset Extractor
Runs on your local Windows machine. Captures D2R, saves screenshot.
You then send the screenshot to the AI agent for analysis.
AI returns bounding boxes -> run crop.py to extract PNGs.
Usage:
Run: python asset_extractor.py
F1: Capture D2R screen -> screenshots/debug/latest.png
F2: Crop entities from screenshots/debug/latest_annotations.json
F3: List existing assets
F12: Exit
Workflow:
1. Run this script in the botty conda env
2. F1 to capture
3. Tell your AI agent to analyze screenshots/debug/latest.png
4. AI writes screenshots/debug/latest_annotations.json with bounding boxes
5. F2 to crop entities into assets/enemies/ or assets/npc/
"""
import os, sys, cv2, numpy as np, keyboard, json, ctypes, win32gui
from datetime import datetime
from mss import mss
# DPI awareness - must be first
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except:
try:
ctypes.windll.shcore.SetProcessDpiAwareness(1)
except:
pass
# Fix tesserocr DLLs
if sys.platform == "win32":
_dll = os.path.join(os.path.dirname(os.path.dirname(sys.executable)), "Library", "bin")
if os.path.isdir(_dll):
os.add_dll_directory(_dll)
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "src"))
BASE = os.path.dirname(os.path.abspath(__file__))
SAVE_DIR = os.path.join(BASE, "screenshots", "debug")
ENEMIES_DIR = os.path.join(BASE, "assets", "enemies")
NPC_DIR = os.path.join(BASE, "assets", "npc")
for d in [SAVE_DIR, ENEMIES_DIR, NPC_DIR]:
os.makedirs(d, exist_ok=True)
LATEST_PATH = os.path.join(SAVE_DIR, "latest.png")
ANNOTATIONS_PATH = os.path.join(SAVE_DIR, "latest_annotations.json")
# Known NPC names for routing
NPC_NAMES = {
'akara', 'charsi', 'kashya', 'cain', 'drognan', 'lysander',
'fara', 'ormus', 'tyrael', 'jamella', 'halbu', 'qual_kehk',
'qual-kehk', 'qualkehk', 'malah', 'larzuk', 'anya'
}
def find_d2r():
hwnds = []
def cb(h, r):
title = win32gui.GetWindowText(h)
if 'diablo' in title.lower() and win32gui.IsWindowVisible(h):
r.append(h)
win32gui.EnumWindows(cb, hwnds)
return hwnds[0] if hwnds else None
def grab():
"""Grab D2R client area. Resizes to 1280x720 if needed."""
hwnd = find_d2r()
if not hwnd:
print(" [ERROR] D2R not found. Is it running and visible?")
return None
client = win32gui.GetClientRect(hwnd)
w, h = client[2] - client[0], client[3] - client[1]
screen_pos = win32gui.ClientToScreen(hwnd, (0, 0))
with mss() as sct:
region = {
'top': screen_pos[1],
'left': screen_pos[0],
'width': w,
'height': h
}
sct_img = sct.grab(region)
img = np.array(sct_img)[:, :, :3] # BGRA -> BGR
if w != 1280 or h != 720:
img = cv2.resize(img, (1280, 720), interpolation=cv2.INTER_LINEAR)
print(f" [RESIZED] {w}x{h} -> 1280x720")
else:
print(f" [CAPTURED] {w}x{h}")
return img
def on_f1():
"""Capture D2R and save."""
print("\n[=== CAPTURING ===]")
img = grab()
if not img:
return
cv2.imwrite(LATEST_PATH, img)
print(f" [SAVED] {LATEST_PATH}")
print(f" Now ask your AI agent to analyze: {LATEST_PATH}")
print(f" AI should write: {ANNOTATIONS_PATH}")
print(' Format: [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
def on_f2():
"""Crop entities from latest capture using annotations JSON."""
print("\n[=== CROPPING ENTITIES ===]")
if not os.path.exists(LATEST_PATH):
print(" [ERROR] No capture found. Press F1 first.")
return
if not os.path.exists(ANNOTATIONS_PATH):
print(" [ERROR] No annotations found.")
print(f" Create: {ANNOTATIONS_PATH}")
print(' [{"name":"skeleton","x":100,"y":200,"w":60,"h":80}, ...]')
return
img = cv2.imread(LATEST_PATH)
with open(ANNOTATIONS_PATH) as f:
entities = json.load(f)
print(f" Image: {img.shape[1]}x{img.shape[0]}, Entities: {len(entities)}")
saved = 0
for ent in entities:
name = ent['name'].lower().replace(' ', '_')
x, y = int(ent['x']), int(ent['y'])
w, h = int(ent['w']), int(ent['h'])
i_w, i_h = img.shape[1], img.shape[0]
# Crop with 5px padding
pad = 5
x1, y1 = max(0, x - pad), max(0, y - pad)
x2, y2 = min(i_w, x + w + pad), min(i_h, y + h + pad)
crop = img[y1:y2, x1:x2]
# Route to npc or enemies folder
if name in NPC_NAMES:
save_dir = NPC_DIR
else:
save_dir = ENEMIES_DIR
# Auto-number duplicates
fname = f"{name}.png"
save_path = os.path.join(save_dir, fname)
variant = 1
while os.path.exists(save_path):
variant += 1
fname = f"{name}_{variant}.png"
save_path = os.path.join(save_dir, fname)
cv2.imwrite(save_path, crop)
print(f" [SAVED] {save_path} ({crop.shape[1]}x{crop.shape[0]})")
saved += 1
print(f"\n Total: {saved} assets cropped.")
def on_f3():
"""List existing assets."""
print("\n[=== ASSETS INVENTORY ===]")
for label, d in [("enemies", ENEMIES_DIR), ("npc", NPC_DIR)]:
if os.path.isdir(d):
files = sorted(os.listdir(d))
print(f"\n assets/{label}/ ({len(files)} files):")
for f in files:
sz = os.path.getsize(os.path.join(d, f))
print(f" {f} ({sz}b)")
else:
print(f"\n assets/{label}/ - EMPTY")
def run():
print("=== D2R Asset Extractor ===")
print(" F1 - Capture D2R screen")
print(" F2 - Crop entities from annotations")
print(" F3 - List assets")
print(" F12 - Exit")
print("Ready.")
keyboard.add_hotkey('f1', on_f1)
keyboard.add_hotkey('f2', on_f2)
keyboard.add_hotkey('f3', on_f3)
keyboard.add_hotkey('f12', lambda: (print("\nBye."), sys.exit(0)))
keyboard.wait()
if __name__ == "__main__":
run()
+1106
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -100,6 +100,9 @@ NTIPAliasType["ruby"] = 99;
NTIPAliasType["sapphire"] = 100;
NTIPAliasType["topaz"] = 101;
NTIPAliasType["skull"] = 102;
NTIPAliasType["swordsandknives"] = 103;
NTIPAliasType["spearsandpolearms"] = 104;
NTIPAliasType["grimoires"] = 105;
var NTIPAliasClassID = {};
NTIPAliasClassID["hax"] = 0; NTIPAliasClassID["handaxe"] = 0;
@@ -761,6 +764,26 @@ NTIPAliasClassID["ceh"] = 655; NTIPAliasClassID["chargedessenceofhatred"] = 655
NTIPAliasClassID["bet"] = 656; NTIPAliasClassID["burningessenceofterror"] = 656;
NTIPAliasClassID["fed"] = 657; NTIPAliasClassID["festeringessenceofdestruction"] = 657;
NTIPAliasClassID["std"] = 658; NTIPAliasClassID["standardofheroes"] = 658;
NTIPAliasClassID["wa1"] = 659; NTIPAliasClassID["oldbook"] = 659;
NTIPAliasClassID["wa2"] = 660; NTIPAliasClassID["tome"] = 660;
NTIPAliasClassID["wa3"] = 661; NTIPAliasClassID["codex"] = 661;
NTIPAliasClassID["wa4"] = 662; NTIPAliasClassID["compendium"] = 662;
NTIPAliasClassID["wa5"] = 663; NTIPAliasClassID["grimoire"] = 663;
NTIPAliasClassID["wa6"] = 664; NTIPAliasClassID["burnttext"] = 664;
NTIPAliasClassID["wa7"] = 665; NTIPAliasClassID["darktome"] = 665;
NTIPAliasClassID["wa8"] = 666; NTIPAliasClassID["darkcodex"] = 666;
NTIPAliasClassID["wa9"] = 667; NTIPAliasClassID["possessedcompendium"] = 667;
NTIPAliasClassID["waa"] = 668; NTIPAliasClassID["possessedgrimoire"] = 668;
NTIPAliasClassID["wab"] = 669; NTIPAliasClassID["forgottenvolume"] = 669;
NTIPAliasClassID["wac"] = 670; NTIPAliasClassID["occulttome"] = 670;
NTIPAliasClassID["wad"] = 671; NTIPAliasClassID["occultcodex"] = 671;
NTIPAliasClassID["wae"] = 672; NTIPAliasClassID["blasphemouscompendium"] = 672;
NTIPAliasClassID["waf"] = 673; NTIPAliasClassID["blasphemousgrimoire"] = 673;
NTIPAliasClassID["northernworldstoneshard"] = 674;
NTIPAliasClassID["easternworldstoneshard"] = 675;
NTIPAliasClassID["southernworldstoneshard"] = 676;
NTIPAliasClassID["westernworldstoneshard"] = 677;
NTIPAliasClassID["deepworldstoneshard"] = 678;
var NTIPAliasClass = {};
NTIPAliasClass["normal"] = 0;
@@ -900,6 +923,7 @@ NTIPAliasStat["itemaddpaladinskills"] = [83,3]; NTIPAliasStat["paladinskills"] =
NTIPAliasStat["itemaddbarbarianskills"] = [83,4]; NTIPAliasStat["barbarianskills"] = [83,4];
NTIPAliasStat["itemadddruidskills"] = [83,5]; NTIPAliasStat["druidskills"] = [83,5];
NTIPAliasStat["itemaddassassinskills"] = [83,6]; NTIPAliasStat["assassinskills"] = [83,6];
NTIPAliasStat["itemaddwarlockskills"] = [83,7]; NTIPAliasStat["warlockskills"] = [83,7];
NTIPAliasStat["unsentparam1"] = 84;
NTIPAliasStat["itemaddexperience"] = 85;
@@ -931,6 +955,9 @@ NTIPAliasStat["plusskillwerewolf"] = [97,223];
NTIPAliasStat["plusskillshapeshifting"] = [97,224]; NTIPAliasStat["plusskilllycanthropy"] = [97,224];
NTIPAliasStat["plusskillsummonspiritwolf"] = [97,227];
NTIPAliasStat["plusskillferalrage"] = [97,232];
// Warlock
NTIPAliasStat["plusskillabyss"] = [97,281]
NTIPAliasStat["plusskillenhancedentropy"] = [97,282]
NTIPAliasStat["state"] = 98;
NTIPAliasStat["itemfastergethitrate"] = 99; NTIPAliasStat["fhr"] = 99;
@@ -1160,6 +1187,9 @@ NTIPAliasStat["skillbladeshield"] = [107,277];
NTIPAliasStat["skillvenom"] = [107,278];
NTIPAliasStat["skillshadowmaster"] = [107,279];
NTIPAliasStat["skillphoenixstrike"] = [107,280];
// Warlock skills
NTIPAliasStat["skillabyss"] = [107,281];
NTIPAliasStat["skillenhancedentropy"] = [107,282];
NTIPAliasStat["itemrestinpeace"] = 108;
NTIPAliasStat["curseresistance"] = 109;
@@ -1279,6 +1309,9 @@ NTIPAliasStat["itemaddelementalskilltab"] = [188,42]; NTIPAliasStat["elementals
NTIPAliasStat["itemaddtrapsskilltab"] = [188,48]; NTIPAliasStat["trapsskilltab"] = [188,48];
NTIPAliasStat["itemaddshadowdisciplinesskilltab"] = [188,49]; NTIPAliasStat["shadowdisciplinesskilltab"] = [188,49];
NTIPAliasStat["itemaddmartialartsskilltab"] = [188,50]; NTIPAliasStat["martialartsskilltab"] = [188,50];
NTIPAliasStat["itemaddchaosskilltab"] = [188,51]; NTIPAliasStat["chaosskilltab"] = [188,51];
NTIPAliasStat["itemaddeldritchskilltab"] = [188,52]; NTIPAliasStat["eldritchskilltab"] = [188,52];
NTIPAliasStat["itemadddemonskilltab"] = [188,53]; NTIPAliasStat["demonskilltab"] = [188,53];
NTIPAliasStat["unused189"] = 189;
NTIPAliasStat["unused190"] = 190;
+2 -2
View File
@@ -8,14 +8,14 @@
"Screen Resolution (Windowed)": "1280x720",
"Resolution Scale": 100,
"Sharpening": 6,
"Game Resolution": 1,
"Game Resolution": 2,
"Light Quality": 2,
"Blended Shadows": 0,
"Perspective": 0,
"VSync": 1,
"Framerate Cap": 60,
"Framerate Target": 0,
"Window Mode": 0,
"Window Mode": 1,
"Graphic Presets": 4,
"Texture Quality": 4,
"Texture Anisotropy": 0,
@@ -17066,5 +17066,464 @@
"ptBR": "Berserker",
"ruRU": "[ms]лютый[fs]лютая[ns]лютое[pl]лютые",
"zhCN": "狂战士"
},
{
"id": 27875,
"Key": "Virulent",
"enUS": "Virulent",
"zhTW": "怨毒",
"deDE": "[ms]Virulenter[fs]Virulente[ns]Virulentes[pl]Virulente",
"esES": "[fs]virulenta[ms]virulento[fp]virulentas[mp]virulentos",
"frFR": "[ms]virulent[fs]virulente[mp]virulents[fp]virulentes",
"itIT": "[ms]Virulento[fs]Virulenta[mp]Virulenti[fp]Virulente",
"koKR": "악성",
"plPL": "[ms]Zakażający[fs]Zakażająca[ns]Zakażające[p]Zakażające",
"esMX": "[ms]virulento[fs]virulenta[mp]virulentos[fp]virulentas",
"jaJP": "猛毒に満ちた",
"ptBR": "[fs]Virulenta[fp]Virulentas[ms]Virulento[mp]Virulentos",
"ruRU": "[ms]едкий[fs]едкая[ns]едкое[pl]едкие",
"zhCN": "剧毒"
},
{
"id": 27876,
"Key": "of Acidity",
"enUS": "of Acidity",
"zhTW": "酸蝕之",
"deDE": "der Säure",
"esES": "de acidez",
"frFR": "dacidité",
"itIT": "dell'Acidità",
"koKR": "산성의",
"plPL": "Kwasowości",
"esMX": "de acidez",
"jaJP": "酸性の",
"ptBR": "da Acidez",
"ruRU": "едкости",
"zhCN": "酸蚀之"
},
{
"id": 27877,
"Key": "Incendiary",
"enUS": "Incendiary",
"zhTW": "煽火",
"deDE": "[ms]Zündelnder[fs]Zündelnde[ns]Zündelndes[pl]Zündelnde",
"esES": "[fs]incendiaria[ms]incendiario[fp]incendiarias[mp]incendiarios",
"frFR": "[ms]incendiaire[fs]incendiaire[mp]incendiaires[fp]incendiaires",
"itIT": "[ms]Incendiario[fs]Incendiaria[mp]Incendiari[fp]Incendiarie",
"koKR": "방화",
"plPL": "[ms]Zapalający[fs]Zapalająca[ns]Zapalające[p]Zapalające",
"esMX": "[ms]incendiario[fs]incendiaria[mp]incendiarios[fp]incendiarias",
"jaJP": "焼夷の",
"ptBR": "[fs]Incendiária[fp]Incendiárias[ms]Incendiário[mp]Incendiários",
"ruRU": "[ms]воспламеняющий[fs]воспламеняющая[ns]воспламеняющее[pl]воспламеняющие",
"zhCN": "焚烧"
},
{
"id": 27878,
"Key": "of Kindling",
"enUS": "of Kindling",
"zhTW": "引火之",
"deDE": "des Entfachens",
"esES": "de fajina",
"frFR": "de petit bois",
"itIT": "dell'Accensione",
"koKR": "불쏘시개의",
"plPL": "Rozpalania",
"esMX": "de incendaja",
"jaJP": "種火の",
"ptBR": "da Combustão",
"ruRU": "разжигания",
"zhCN": "点燃之"
},
{
"id": 27879,
"Key": "Gelid",
"enUS": "Gelid",
"zhTW": "嚴寒",
"deDE": "[ms]Eiskalter[fs]Eiskalte[ns]Eiskaltes[pl]Eiskalte",
"esES": "[fs]gélida[ms]gélido[fp]gélidas[mp]gélidos",
"frFR": "[ms]transi[fs]transie[mp]transis[fp]transies",
"itIT": "[ms]Gelido[fs]Gelida[mp]Gelidi[fp]Gelide",
"koKR": "혹한",
"plPL": "[ms]Zlodowaciały[fs]Zlodowaciała[ns]Zlodowaciałe[p]Zlodowaciałe",
"esMX": "[ms]gélido[fs]gélida[mp]gélidos[fp]gélidas",
"jaJP": "氷結",
"ptBR": "[fs]Gélida[fp]Gélidas[ms]Gélido[mp]Gélidos",
"ruRU": "[ms]застывший[fs]застывшая[ns]застывшее[pl]застывшие",
"zhCN": "极寒"
},
{
"id": 27880,
"Key": "of Numbing",
"enUS": "of Numbing",
"zhTW": "麻木之",
"deDE": "des Erstarrens",
"esES": "de entumecimiento",
"frFR": "dengourdissement",
"itIT": "dell'Intorpidimento",
"koKR": "마비의",
"plPL": "Odrętwienia",
"esMX": "de entumecimiento",
"jaJP": "麻痺の",
"ptBR": "do Entorpecimento",
"ruRU": "обморожения",
"zhCN": "麻痹之"
},
{
"id": 27881,
"Key": "Magnetic",
"enUS": "Magnetic",
"zhTW": "磁性",
"deDE": "[ms]Magnetischer[fs]Magnetische[ns]Magnetisches[pl]Magnetische",
"esES": "[fs]magnética[ms]magnético[fp]magnéticas[mp]magnéticos",
"frFR": "[ms]magnétique[fs]magnétique[mp]magnétiques[fp]magnétiques",
"itIT": "[ms]Magnetico[fs]Magnetica[mp]Magnetici[fp]Magnetiche",
"koKR": "자성",
"plPL": "[ms]Magnetyczny[fs]Magnetyczna[ns]Magnetyczne[p]Magnetyczne",
"esMX": "[ms]magnético[fs]magnética[mp]magnéticos[fp]magnéticas",
"jaJP": "超電磁",
"ptBR": "[fs]Magnética[fp]Magnéticas[ms]Magnético[mp]Magnéticos",
"ruRU": "[ms]магнитный[fs]магнитная[ns]магнитное[pl]магнитные",
"zhCN": "磁性"
},
{
"id": 27882,
"Key": "of Conductivity",
"enUS": "of Conductivity",
"zhTW": "導電之",
"deDE": "der Leitfähigkeit",
"esES": "de conductividad",
"frFR": "de conductivité",
"itIT": "della Conduttività",
"koKR": "전도성의",
"plPL": "Przewodzenia",
"esMX": "de conductividad",
"jaJP": "通電の",
"ptBR": "da Condutividade",
"ruRU": "проводимости",
"zhCN": "传导之"
},
{
"id": 27883,
"Key": "Mystical",
"enUS": "Mystical",
"zhTW": "神秘",
"deDE": "[ms]Mystischer[fs]Mystische[ns]Mystisches[pl]Mystische",
"esES": "[fs]mística[ms]místico[fp]místicas[mp]místicos",
"frFR": "[ms]mystique[fs]mystique[mp]mystiques[fp]mystiques",
"itIT": "[ms]Mistico[fs]Mistica[mp]Mistici[fp]Mistiche",
"koKR": "신비한",
"plPL": "[ms]Mistyczny[fs]Mistyczna[ns]Mistyczne[p]Mistyczne",
"esMX": "[ms]místico[fs]mística[mp]místicos[fp]místicas",
"jaJP": "神秘を司る",
"ptBR": "[fs]Mística[fp]Místicas[ms]Místico[mp]Místicos",
"ruRU": "[ms]мистический[fs]мистическая[ns]мистическое[pl]мистические",
"zhCN": "神秘"
},
{
"id": 27884,
"Key": "of Thaumaturgy",
"enUS": "of Thaumaturgy",
"zhTW": "奇術之",
"deDE": "der Thaumaturgie",
"esES": "de taumaturgia",
"frFR": "de thaumaturgie",
"itIT": "della Taumaturgia",
"koKR": "마도의",
"plPL": "Taumaturgii",
"esMX": "de taumaturgia",
"jaJP": "魔術の",
"ptBR": "da Taumaturgia",
"ruRU": "чудотворства",
"zhCN": "奇术之"
},
{
"id": 27885,
"Key": "Breaching",
"enUS": "Breaching",
"zhTW": "突破",
"deDE": "[ms]Brechender[fs]Brechende[ns]Brechendes[pl]Brechende",
"esES": "[fs]quebrantadora[ms]quebrantador[fp]quebrantadoras[mp]quebrantadores",
"frFR": "[ms]offensif[fs]offensive[mp]offensifs[fp]offensives",
"itIT": "[ms]Penetrante[fs]Penetrante[mp]Penetranti[fp]Penetranti",
"koKR": "침범하는",
"plPL": "[ms]Przełamujący[fs]Przełamująca[ns]Przełamujące[p]Przełamujące",
"esMX": "[ms]vulnerante[fs]vulnerante[mp]vulnerantes[fp]vulnerantes",
"jaJP": "強行突破",
"ptBR": "[fs]Abrangente[fp]Abrangentes[ms]Abrangente[mp]Abrangentes",
"ruRU": "[ms]ломающий[fs]ломающая[ns]ломающее[pl]ломающие",
"zhCN": "破甲"
},
{
"id": 27886,
"Key": "of Force",
"enUS": "of Force",
"zhTW": "力迫之",
"deDE": "der Macht",
"esES": "de pujanza",
"frFR": "de force",
"itIT": "della Potenza",
"koKR": "위력의",
"plPL": "Mocy",
"esMX": "de potencia",
"jaJP": "力の",
"ptBR": "de Força",
"ruRU": "принуждения",
"zhCN": "力量之"
},
{
"id": 27893,
"Key": "Chaotic",
"enUS": "Chaotic",
"zhTW": "混沌",
"deDE": "[ms]Chaotischer[fs]Chaotische[ns]Chaotisches[pl]Chaotische",
"esES": "[fs]caótica[ms]caótico[fp]caóticas[mp]caóticos",
"frFR": "[ms]chaotique[fs]chaotique[mp]chaotiques[fp]chaotiques",
"itIT": "[ms]Caotico[fs]Caotica[mp]Caotici[fp]Caotiche",
"koKR": "혼돈",
"plPL": "[ms]Chaotyczny[fs]Chaotyczna[ns]Chaotyczne[p]Chaotyczne",
"esMX": "[ms]caótico[fs]caótica[mp]caóticos[fp]caóticas",
"jaJP": "混乱せし",
"ptBR": "[fs]Conturbada[fp]Conturbadas[ms]Conturbado[mp]Conturbados",
"ruRU": "[ms]хаотический[fs]хаотическая[ns]хаотическое[pl]хаотические",
"zhCN": "混乱"
},
{
"id": 27894,
"Key": "Sullied",
"enUS": "Sullied",
"zhTW": "玷汙",
"deDE": "[ms]Beschmutzter[fs]Beschmutzte[ns]Beschmutztes[pl]Beschmutzte",
"esES": "[fs]maculada[ms]maculado[fp]maculadas[mp]maculados",
"frFR": "[ms]profané[fs]profanée[mp]profanés[fp]profanées",
"itIT": "[ms]Infangato[fs]Infangata[mp]Infangati[fp]Infangate",
"koKR": "더럽혀진",
"plPL": "[ms]Splugawiony[fs]Splugawiona[ns]Splugawione[p]Splugawione",
"esMX": "[ms]mancillado[fs]mancillada[mp]mancillados[fp]mancilladas",
"jaJP": "汚物の",
"ptBR": "[fs]Manchada[fp]Manchadas[ms]Manchado[mp]Manchados",
"ruRU": "[ms]пятнающий[fs]пятнающая[ns]пятнающее[pl]пятнающие",
"zhCN": "污染"
},
{
"id": 27895,
"Key": "Fiendish",
"enUS": "Fiendish",
"zhTW": "妖魔",
"deDE": "[ms]Teuflischer[fs]Teuflische[ns]Teuflisches[pl]Teuflische",
"esES": "[fs]maligna[ms]maligno[fp]malignas[mp]malignos",
"frFR": "[ms]diabolique[fs]diabolique[mp]diaboliques[fp]diaboliques",
"itIT": "[ms]Mostruoso[fs]Mostruosa[mp]Mostruosi[fp]Mostruose",
"koKR": "극악한",
"plPL": "[ms]Czarci[fs]Czarcia[ns]Czarcie[p]Czarcie",
"esMX": "[ms]diablesco[fs]diablesca[mp]diablescos[fp]diablescas",
"jaJP": "悪魔のような",
"ptBR": "[fs]Demoníaca[fp]Demoníacas[ms]Demoníaco[mp]Demoníacos",
"ruRU": "[ms]бесовский[fs]бесовская[ns]бесовское[pl]бесовские",
"zhCN": "邪鬼"
},
{
"id": 27896,
"Key": "Erratic",
"enUS": "Erratic",
"zhTW": "失序",
"deDE": "[ms]Unberechenbarer[fs]Unberechenbare[ns]Unberechenbares[pl]Unberechenbare",
"esES": "[fs]errática[ms]errático[fp]erráticas[mp]erráticos",
"frFR": "[ms]erratique[fs]erratique[mp]erratiques[fp]erratiques",
"itIT": "[ms]Imprevedibile[fs]Imprevedibile[mp]Imprevedibili[fp]Imprevedibili",
"koKR": "변덕스러운",
"plPL": "[ms]Rozpaczliwy[fs]Rozpaczliwa[ns]Rozpaczliwe[p]Rozpaczliwe",
"esMX": "[ms]errático[fs]errática[mp]erráticos[fp]erráticas",
"jaJP": "奇矯な",
"ptBR": "[fs]Errática[fp]Erráticas[ms]Errático[mp]Erráticos",
"ruRU": "[ms]беспорядочный[fs]беспорядочная[ns]беспорядочное[pl]беспорядочные",
"zhCN": "善变"
},
{
"id": 27897,
"Key": "Torrid",
"enUS": "Torrid",
"zhTW": "熾熱",
"deDE": "[ms]Sengender[fs]Sengende[ns]Sengendes[pl]Sengende",
"esES": "[fs]tórrida[ms]tórrido[fp]tórridas[mp]tórridos",
"frFR": "[ms]torride[fs]torride[mp]torrides[fp]torrides",
"itIT": "[ms]Torrido[fs]Torrida[mp]Torridi[fp]Torride",
"koKR": "격한",
"plPL": "[ms]Palący[fs]Paląca[ns]Palące[p]Palące",
"esMX": "[ms]tórrido[fs]tórrida[mp]tórridos[fp]tórridas",
"jaJP": "熱烈な",
"ptBR": "[fs]Tórrida[fp]Tórridas[ms]Tórrido[mp]Tórridos",
"ruRU": "[ms]жгучий[fs]жгучая[ns]жгучее[pl]жгучие",
"zhCN": "灼热"
},
{
"id": 27898,
"Key": "TaintedAffix",
"enUS": "Tainted",
"zhTW": "魔汙",
"deDE": "[ms]Besudelter[fs]Besudelte[ns]Besudeltes[pl]Besudelte",
"esES": "[fs]mancillada[ms]mancillado[fp]mancilladas[mp]mancillados",
"frFR": "[ms]corrompu[fs]corrompue[mp]corrompus[fp]corrompues",
"itIT": "[ms]Contaminato[fs]Contaminata[mp]Contaminati[fp]Contaminate",
"koKR": "오염된",
"plPL": "[ms]Splamiony[fs]Splamiona[ns]Splamione[p]Splamione",
"esMX": "[ms]contaminado[fs]contaminada[mp]contaminados[fp]contaminadas",
"jaJP": "穢された",
"ptBR": "[fs]Maculada[fp]Maculadas[ms]Maculado[mp]Maculados",
"ruRU": "[ms]помутненный[fs]помутненная[ns]помутненное[pl]помутненные",
"zhCN": "污染"
},
{
"id": 27899,
"Key": "Forbidden",
"enUS": "Forbidden",
"zhTW": "封禁",
"deDE": "[ms]Verbotener[fs]Verbotene[ns]Verbotenes[pl]Verbotene",
"esES": "[fs]prohibida[ms]prohibido[fp]prohibidas[mp]prohibidos",
"frFR": "[ms]interdit[fs]interdite[mp]interdits[fp]interdites",
"itIT": "[ms]Proibito[fs]Proibita[mp]Proibiti[fp]Proibite",
"koKR": "금지된",
"plPL": "[ms]Zakazany[fs]Zakazana[ns]Zakazane[p]Zakazane",
"esMX": "[ms]prohibido[fs]prohibida[mp]prohibidos[fp]prohibidas",
"jaJP": "禁断なる",
"ptBR": "[fs]Proibida[fp]Proibidas[ms]Proibido[mp]Proibidos",
"ruRU": "[ms]запретный[fs]запретная[ns]запретное[pl]запретные",
"zhCN": "禁忌"
},
{
"id": 27900,
"Key": "Dreadful",
"enUS": "Dreadful",
"zhTW": "怖懼",
"deDE": "[ms]Furchterregender[fs]Furchterregende[ns]Furchterregendes[pl]Furchterregende",
"esES": "[fs]pavorosa[ms]pavoroso[fp]pavorosas[mp]pavorosos",
"frFR": "[ms]effroyable[fs]effroyable[mp]effroyables[fp]effroyables",
"itIT": "[ms]Terrificante[fs]Terrificante[mp]Terrificanti[fp]Terrificanti",
"koKR": "무시무시한",
"plPL": "[ms]Przerażający[fs]Przerażająca[ns]Przerażające[p]Przerażające",
"esMX": "[ms]espantoso[fs]espantosa[mp]espantosos[fp]espantosas",
"jaJP": "恐ろしき",
"ptBR": "[fs]Pavorosa[fp]Pavorosas[ms]Pavoroso[mp]Pavorosos",
"ruRU": "[ms]жуткий[fs]жуткая[ns]жуткое[pl]жуткие",
"zhCN": "可怖"
},
{
"id": 27901,
"Key": "Malevolent",
"enUS": "Malevolent",
"zhTW": "惡毒",
"deDE": "[ms]Arglistiger[fs]Arglistige[ns]Arglistiges[pl]Arglistige",
"esES": "[fs]malévola[ms]malévolo[fp]malévolas[mp]malévolos",
"frFR": "[ms]malfaisant[fs]malfaisante[mp]malfaisants[fp]malfaisantes",
"itIT": "[ms]Malevolo[fs]Malevola[mp]Malevoli[fp]Malevole",
"koKR": "고약한",
"plPL": "[ms]Złowrogi[fs]Złowroga[ns]Złowrogie[p]Złowrogie",
"esMX": "[fs]malevolente[ms]malevolente[fp]malevolentes[mp]malevolentes",
"jaJP": "悪しき",
"ptBR": "[fs]Malévola[fp]Malévolas[ms]Malévolo[mp]Malévolos",
"ruRU": "[ms]злонравный[fs]злонравная[ns]злонравное[pl]злонравные",
"zhCN": "恶意"
},
{
"id": 27902,
"Key": "of Miasma Bolt",
"enUS": "of Miasma Bolt",
"zhTW": "瘴氣彈之",
"deDE": "des Miasmablitzes",
"esES": "de descarga de miasma",
"frFR": "de traits de miasmes",
"itIT": "del Dardo Miasmatico",
"koKR": "독기 볼트의",
"plPL": "Miazmatycznego Pocisku",
"esMX": "de saeta de miasma",
"jaJP": "ミアズマボルトの",
"ptBR": "da Seta de Miasma",
"ruRU": "стрелы миазм",
"zhCN": "瘴气弹之"
},
{
"id": 27903,
"Key": "of Lethargy",
"enUS": "of Lethargy",
"zhTW": "昏沉之",
"deDE": "der Lethargie",
"esES": "de letargo",
"frFR": "de léthargie",
"itIT": "della Letargia",
"koKR": "무기력의",
"plPL": "Otępienia",
"esMX": "de letargo",
"jaJP": "沈滞の",
"ptBR": "da Letargia",
"ruRU": "летаргии",
"zhCN": "迟缓之"
},
{
"id": 27904,
"Key": "of Rancor",
"enUS": "of Rancor",
"zhTW": "冤仇之",
"deDE": "des Grolls",
"esES": "de rencor",
"frFR": "de rancœur",
"itIT": "del Rancore",
"koKR": "울분의",
"plPL": "Animozji",
"esMX": "de rencor",
"jaJP": "怨恨の",
"ptBR": "do Rancor",
"ruRU": "озлобленности",
"zhCN": "怨恨之"
},
{
"id": 27905,
"Key": "of Apocalypse",
"enUS": "of Apocalypse",
"zhTW": "天啟末日之",
"deDE": "der Apokalypse",
"esES": "de la hecatombe",
"frFR": "dapocalypse",
"itIT": "dell'Apocalisse",
"koKR": "종말의",
"plPL": "Apokalipsy",
"esMX": "de apocalipsis",
"jaJP": "終焉の",
"ptBR": "do Apocalipse",
"ruRU": "апокалипсиса",
"zhCN": "末日之"
},
{
"id": 27912,
"Key": "Devil's",
"enUS": "Devil's",
"zhTW": "妖鬼的",
"deDE": "[ms]Diabolischer[fs]Diabolische[ns]Diabolisches[pl]Diabolische",
"esES": "del demonio",
"frFR": "de démon",
"itIT": "del Diavolo",
"koKR": "악마",
"plPL": "[ms]Diabelski[fs]Diabelska[ns]Diabelskie[p]Diabelskie",
"esMX": "[ms]del demontre[fs]del demontre[mp]del demontre[fp]del demontre",
"jaJP": "悪魔が携えし",
"ptBR": "do Demônio",
"ruRU": "[ms]дьявольский[fs]дьявольская[ns]дьявольское[pl]дьявольские",
"zhCN": "恶魔的"
},
{
"id": 27913,
"Key": "Arch-Devil's",
"enUS": "Arch-Devil's",
"zhTW": "大妖鬼",
"deDE": "[ms]Erzdiabolischer[fs]Erzdiabolische[ns]Erzdiabolisches[pl]Erzdiabolische",
"esES": "del archidemonio",
"frFR": "darchidémon",
"itIT": "dell'Arcidiavolo",
"koKR": "고위악마",
"plPL": "[ms]Arcydiabelski[fs]Arcydiabelska[ns]Arcydiabelskie[p]Arcydiabelskie",
"esMX": "[ms]del archidemontre[fs]del archidemontre[mp]del archidemontre[fp]del archidemontre",
"jaJP": "大悪魔の",
"ptBR": "do Arquidemônio",
"ruRU": "[ms]архидьявольский[fs]архидьявольская[ns]архидьявольское[pl]архидьявольские",
"zhCN": "大恶魔的"
}
]
File diff suppressed because it is too large Load Diff
@@ -4010,5 +4010,209 @@
"ptBR": "Zefir",
"ruRU": "Бриз",
"zhCN": "和风"
}
},
{
"id": 27360,
"Key": "Runeword171",
"enUS": "Hysteria",
"zhTW": "歇斯底里",
"deDE": "Hysterie",
"esES": "Histeria",
"frFR": "Hystérie",
"itIT": "Isteria",
"koKR": "발작",
"plPL": "Histeria",
"esMX": "Histeria",
"jaJP": "発奮",
"ptBR": "Histeria",
"ruRU": "Истерия",
"zhCN": "狂乱"
},
{
"id": 27361,
"Key": "Runeword172",
"enUS": "Mania",
"zhTW": "狂躁",
"deDE": "Manie",
"esES": "Obsesión",
"frFR": "Mania",
"itIT": "Mania",
"koKR": "광기",
"plPL": "Mania",
"esMX": "Manía",
"jaJP": "マニア",
"ptBR": "Obsessão",
"ruRU": "Мания",
"zhCN": "癫狂"
},
{
"id": 27362,
"Key": "Runeword173",
"enUS": "Mosaic",
"zhTW": "嵌飾",
"deDE": "Mosaik",
"esES": "Mosaico",
"frFR": "Mosaïque",
"itIT": "Mosaico",
"koKR": "모자이크",
"plPL": "Mozaika",
"esMX": "Mosaico",
"jaJP": "坩堝",
"ptBR": "Mosaico",
"ruRU": "Мозаика",
"zhCN": "模糊"
},
{
"id": 27363,
"Key": "Runeword174",
"enUS": "Metamorphosis",
"zhTW": "變化",
"deDE": "Metamorphose",
"esES": "Metamorfosis",
"frFR": "Métamorphose",
"itIT": "Metamorfosi",
"koKR": "탈태",
"plPL": "Metamorfoza",
"esMX": "Metamorfosis",
"jaJP": "変容",
"ptBR": "Metamorfose",
"ruRU": "Метаморфоза",
"zhCN": "变形"
},
{
"id": 27364,
"Key": "Runeword175",
"enUS": "Ground",
"zhTW": "接地",
"deDE": "Boden",
"esES": "Tierra",
"frFR": "Terre",
"itIT": "Suolo",
"koKR": "접지",
"plPL": "Grunt",
"esMX": "Suelo",
"jaJP": "大地",
"ptBR": "Solo",
"ruRU": "Земля",
"zhCN": "接地"
},
{
"id": 27365,
"Key": "Runeword176",
"enUS": "Temper",
"zhTW": "和緩",
"deDE": "Temperament",
"esES": "Temperamento",
"frFR": "Tempérament",
"itIT": "Tempra",
"koKR": "담금질",
"plPL": "Charakter",
"esMX": "Ira",
"jaJP": "沈着",
"ptBR": "Índole",
"ruRU": "Закалка",
"zhCN": "淬火"
},
{
"id": 27366,
"Key": "Runeword177",
"enUS": "Hearth",
"zhTW": "火爐",
"deDE": "Heim",
"esES": "Hogar",
"frFR": "Âtre",
"itIT": "Focolare",
"koKR": "화로",
"plPL": "Palenisko",
"esMX": "Fogón",
"jaJP": "炉辺",
"ptBR": "Lar",
"ruRU": "Очаг",
"zhCN": "壁炉"
},
{
"id": 27367,
"Key": "Runeword178",
"enUS": "Cure",
"zhTW": "治癒",
"deDE": "Heilung",
"esES": "Cura",
"frFR": "Remède",
"itIT": "Cura",
"koKR": "치료",
"plPL": "Remedium",
"esMX": "Cura",
"jaJP": "治癒",
"ptBR": "Cura",
"ruRU": "Лекарство",
"zhCN": "解药"
},
{
"id": 27650,
"Key": "Runeword180",
"enUS": "Coven",
"zhTW": "巫師會",
"deDE": "Zirkel",
"esES": "Aquelarre",
"frFR": "Cabale",
"itIT": "Congrega",
"koKR": "마녀단",
"plPL": "Sabat",
"esMX": "Aquelarre",
"jaJP": "集会",
"ptBR": "Pacto",
"ruRU": "Ковен",
"zhCN": "女巫团"
},
{
"id": 27651,
"Key": "Runeword181",
"enUS": "Vigilance",
"zhTW": "戒慎",
"deDE": "Wachsamkeit",
"esES": "Vigilancia",
"frFR": "Vigilance",
"itIT": "Vigilanza",
"koKR": "경계",
"plPL": "Czujność",
"esMX": "Vigilancia",
"jaJP": "警戒",
"ptBR": "Vigilância",
"ruRU": "Бдительность",
"zhCN": "警戒"
},
{
"id": 27652,
"Key": "Runeword182",
"enUS": "Ritual",
"zhTW": "儀式",
"deDE": "Ritual",
"esES": "Ritual",
"frFR": "Rituel",
"itIT": "Rituale",
"koKR": "의식",
"plPL": "Rytuał",
"esMX": "Ritual",
"jaJP": "儀式",
"ptBR": "Ritual",
"ruRU": "Ритуал",
"zhCN": "仪式"
},
{
"id": 27974,
"Key": "Runeword179",
"enUS": "Bulwark",
"zhTW": "壁壘",
"deDE": "Bollwerk",
"esES": "Baluarte",
"frFR": "Rempart",
"itIT": "Baluardo",
"koKR": "방벽",
"plPL": "Szaniec",
"esMX": "Baluarte",
"jaJP": "防塁",
"ptBR": "Baluarte",
"ruRU": "Оплот",
"zhCN": "壁垒"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

+46 -22
View File
@@ -11,6 +11,7 @@
(OPTIONAL)
(PALADIN
(SORCERESS
(WARLOCK
A
ABADDON
ABHAYA
@@ -75,6 +76,7 @@ AJHEED
AKARA
AKARAN
AKARAT'S
AL'DIABOLOS
ALACRITY
ALADDIN'S
ALARIC
@@ -175,6 +177,7 @@ ARREAT
ARREAT'S
ARROW
ARROWS
ARS
ARSENAL
ART
ARTFUL
@@ -339,6 +342,7 @@ BLADEBUCKLE
BLADES
BLAISE
BLANCHED
BLASPHEMOUS
BLAST
BLASTBARK
BLAZE
@@ -551,8 +555,8 @@ CHAMPION
CHANCE
CHANDELIER
CHANG
CHANNEL.
CHANNEL:
CHANNEL.
CHANT
CHAOS
CHAOTIC
@@ -561,8 +565,8 @@ CHARGE
CHARGE-UP
CHARGED
CHARGES
CHARGES)
CHARGES:
CHARGES)
CHARM
CHARSI
CHARSI'S
@@ -603,6 +607,7 @@ CLAYMORE
CLEANSING
CLEARED
CLEAVER
CLEFT
CLEGLAW'S
CLICK
CLICKING
@@ -625,6 +630,7 @@ COBALT
COBRA
COCOON
CODE:
CODEX
COFFIN
COIF
COIL
@@ -653,10 +659,11 @@ COMMUNAL
COMPACT
COMPARE
COMPELLING
COMPENDIUM
COMPLETE
COMPLETED
COMPLETED.
COMPLETED:
COMPLETED.
COMPOSITE
CONCENTRATE
CONCENTRATION
@@ -826,6 +833,7 @@ DECKARD
DECLARE
DECOY
DECREPIFY
DEEP
DEFEAT
DEFENDER
DEFENSE
@@ -856,8 +864,8 @@ DESTINATION
DESTROY
DESTROYER
DESTRUCTION
DESTRUCTION'S
DESTRUCTION.
DESTRUCTION'S
DETAIL
DETAILS.
DETECT
@@ -879,8 +887,8 @@ DIE
DIED
DIES,
DIFFERENCE
DIFFERENCE.
DIFFERENCE:
DIFFERENCE.
DIFFERENT
DIFFICULTY
DIFFICULTY:
@@ -959,6 +967,7 @@ DRULAN'S
DRUS
DRY
DUE
DUL'MEPHISTOS
DUN
DUNE
DUNES
@@ -997,6 +1006,7 @@ EARS
EARTH
EARTHSHAKER
EASE
EASTERN
EATER
EATS
EBURINE
@@ -1412,8 +1422,8 @@ GOBLIN
GODLY
GOES
GOLD
GOLD.
GOLD:
GOLD.
GOLDEN
GOLDSKIN
GOLDSTRIKE
@@ -1460,6 +1470,7 @@ GRIFFON
GRIFFON'S
GRIM
GRIM'S
GRIMOIRE
GRIN
GRINDER
GRINDING
@@ -1782,8 +1793,8 @@ ITCHIES
ITEM
ITEM.
ITEMS
ITEMS.
ITEMS:
ITEMS.
ITH
ITHERA
ITONYA
@@ -1957,9 +1968,9 @@ LEORIC
LESSON
LESTRON'S
LEVEL
LEVEL)
LEVEL-UP
LEVEL:
LEVEL)
LEVELS
LEVER
LEVIATHAN
@@ -1971,8 +1982,8 @@ LIDLESS
LIEF
LIENE
LIFE
LIFE/MANA
LIFE:
LIFE/MANA
LIFECHOKE
LIGHT
LIGHTBRAND
@@ -2136,12 +2147,13 @@ MAULER
MAUSOLEUM
MAW
MAX
MAX.
MAX:
MAX.
MAXIMUM
MAY
MCAULEY'S
MEASURE
MEASURED
MEAT
MEATSCRAPE
MECHANIC'S
@@ -2261,8 +2273,8 @@ NARPHET
NATALYA
NATALYA'S
NATURAL
NATURE'S
NATURE,
NATURE'S
NEAR
NEARBY
NEBUCHADNEZZAR'S
@@ -2305,6 +2317,7 @@ NONE
NOOSE
NORD'S
NORMAL
NORTHERN
NOSFERATU'S
NOT
NOVA
@@ -2323,6 +2336,7 @@ OBLIVION
OBSCENE
OBSESSION
OBSIDIAN
OCCULT
OCEAN
OCHER
OCULUS
@@ -2349,6 +2363,7 @@ ONES
ONLY
ONLY)
OOZE
OPALVEIN
OPEN
OPENS
OPTIMAL
@@ -2471,8 +2486,8 @@ PLAYED
PLAYER
PLAYER.
PLAYERS
PLAYERS.
PLAYERS:
PLAYERS.
PLEDGE
PLUCKEYE
PLUS
@@ -2617,8 +2632,8 @@ RATMEN
RATTLE
RATTLECAGE
RAVEN
RAVEN'S
RAVEN:
RAVEN'S
RAVENLORE
RAVENS
RAVENS:
@@ -2692,9 +2707,9 @@ REND
RENDER
REPAIR
REPAIRED
REPAIRED.
REPAIRED)
REPAIRED),
REPAIRED.
REPAIRING
REPAIRS
REPEATING
@@ -2804,6 +2819,7 @@ RUN/WALK
RUNE
RUNES
RUNIC
RUPTURE
RUSSET
RUST
RUSTHANDLE
@@ -2945,8 +2961,8 @@ SHELL
SHELTER
SHENK
SHIELD
SHIELD'S
SHIELD:
SHIELD'S
SHIELDS
SHIELDS:
SHIFT
@@ -3001,6 +3017,7 @@ SISTER'S
SIX
SIZE
SIZE:
SLING
SKELETAL
SKELETON
SKELETONS
@@ -3093,6 +3110,7 @@ SOULSTONE
SOUND
SOUNDING
SOURCE
SOUTHERN
SPACE
SPACING
SPANISH
@@ -3173,8 +3191,8 @@ STARLIGHT
STARS
STARS:
STASH
STASH.
STASH:
STASH.
STAT
STAT/SKILL
STATE
@@ -3303,8 +3321,8 @@ TAINTBREEDER
TAINTED
TAKE
TAKEN
TAKEN.
TAKEN:
TAKEN.
TAKES
TAL
TALBERD'S
@@ -3322,8 +3340,8 @@ TANNR
TAP
TARGE
TARGET
TARGET'S
TARGET:
TARGET'S
TARGETS
TARNHELM
TAUNT
@@ -3353,6 +3371,7 @@ TERRA'S
TERRENE
TERROR
TERROR'S
TEXT
THADAR
THAN
THANK
@@ -3431,6 +3450,7 @@ TOOTH
TOOTHROW
TOP
TOPAZ
TOR'BAALOS
TORC
TORCH
TORKEL
@@ -3645,6 +3665,7 @@ VOID
VOIDBRINGER
VOLCANIC
VOLCANO
VOLUME
VOMIR
VORTEX
VOULGE
@@ -3653,6 +3674,7 @@ VS.
VULPINE
VULTURE
WACKER
WAGER
WAHEED
WAIL
WAILING
@@ -3682,6 +3704,7 @@ WARDEN'S
WARDER
WARDING
WARHOUND
WARLOCK
WARLORD'S
WARMING
WARMTH
@@ -3691,8 +3714,8 @@ WARPED
WARPS
WARPSPEAR
WARRIOR
WARRIOR'S
WARRIOR.
WARRIOR'S
WARRIV
WARRIV'S
WARSHRIKE
@@ -3715,8 +3738,8 @@ WEAPON
WEAPON:
WEAPONS
WEAPONS,
WEAPONS.
WEAPONS:
WEAPONS.
WEAVER
WEB
WEDDING
@@ -3727,6 +3750,7 @@ WENDY
WERE
WEREBEAR
WEREWOLF
WESTERN
WHALE
WHAT
WHEEL
@@ -3831,10 +3855,10 @@ YETI
YEW
YOLK
YOU
YOU'VE
YOU,
YOU.
YOU:
YOU.
YOU'VE
YOUNG
YOUR
YOUTH
File diff suppressed because it is too large Load Diff
+57 -5
View File
@@ -76,20 +76,65 @@ if __name__ == "__main__":
if args.use_key:
key = Fernet.generate_key().decode("utf-8")
key_cmd = " --key " + key
installer_cmd = f"pyinstaller --onefile --distpath {botty_dir}{key_cmd} --exclude-module graphviz --paths .\\src --paths {args.conda_path}\\envs\\botty\\Lib\\site-packages src\\{exe}"
os.system(installer_cmd)
botty_env = os.path.join(args.conda_path, "envs", "botty")
pyinstaller_exe = os.path.join(botty_env, "Scripts", "pyinstaller.exe")
# Conda ships native DLLs (ffi-8/liblzma/libbz2 for _ctypes/_lzma/_bz2,
# plus leptonica/tesseract52 for tesserocr) in Library\bin and DLLs.
# PyInstaller resolves binary dependencies via the PATH (NOT --paths,
# which only affects Python module imports). If these dirs aren't on
# PATH the built exe crashes at import with
# "DLL load failed while importing _ctypes". Prepend them for both
# local and CI builds.
dll_dirs = [
os.path.join(botty_env, "Library", "bin"),
os.path.join(botty_env, "Library", "lib"),
os.path.join(botty_env, "DLLs"),
]
os.environ["PATH"] = os.pathsep.join(dll_dirs) + os.pathsep + os.environ.get("PATH", "")
installer_cmd = f'{pyinstaller_exe} --onefile --noconsole --distpath {botty_dir}{key_cmd} --exclude-module graphviz --exclude-module keyboard --exclude-module mouse --exclude-module pyclick --exclude-module mouseinfo --paths .\\src --paths "{botty_env}\\Lib\\site-packages" src\\{exe}'
ret = os.system(installer_cmd)
if ret != 0:
raise RuntimeError(f"PyInstaller failed for {exe} (exit {ret})")
os.system(f"cd {botty_dir} && mkdir config && cd ..")
os.makedirs(f"{botty_dir}/config", exist_ok=True)
with open(f"{botty_dir}/config/custom.ini", "w") as f:
f.write("; Add parameters you want to overwrite from param.ini here")
shutil.copy("config/game.ini", f"{botty_dir}/config/")
shutil.copy("config/params.ini", f"{botty_dir}/config/")
shutil.copy("config/shop.ini", f"{botty_dir}/config/")
shutil.copy("config/default.nip", f"{botty_dir}/config/")
os.makedirs(f"{botty_dir}/config/nip", exist_ok=True)
shutil.copy("config/default.bnip", f"{botty_dir}/config/")
os.makedirs(f"{botty_dir}/config/bnip", exist_ok=True)
shutil.copy("README.md", f"{botty_dir}/")
shutil.copytree("assets", f"{botty_dir}/assets")
shutil.copytree("src", f"{botty_dir}/src")
shutil.copy("environment.yml", f"{botty_dir}/")
shutil.copy("install.bat", f"{botty_dir}/")
shutil.copy("find_python.bat", f"{botty_dir}/")
shutil.copy("run_botty.bat", f"{botty_dir}/")
shutil.copy("run.bat", f"{botty_dir}/")
if os.path.exists("dependencies"):
shutil.copytree("dependencies", f"{botty_dir}/dependencies")
# Bundle a portable Tesseract so the standalone exe is click-and-run with
# working OCR and no separate install. ocr.py prefers <exe_dir>/tesseract/
# tesseract.exe. Source: TESSERACT_DIR env or the default UB Mannheim path.
# Skipped (with a warning) if not present — the bot still works once the
# user runs install.bat, which sets OCR up the conda way.
tesseract_src = os.environ.get("TESSERACT_DIR", r"C:\Program Files\Tesseract-OCR")
tess_exe = os.path.join(tesseract_src, "tesseract.exe")
if os.path.isfile(tess_exe):
print(f"Bundling Tesseract from {tesseract_src}")
# Copy the exe + DLLs; skip their tessdata (we ship our own trained
# models in assets/tessdata and pass --tessdata-dir to point at them).
os.makedirs(f"{botty_dir}/tesseract", exist_ok=True)
for entry in os.listdir(tesseract_src):
src = os.path.join(tesseract_src, entry)
if os.path.isfile(src) and entry.lower().endswith((".exe", ".dll")):
shutil.copy(src, f"{botty_dir}/tesseract/")
else:
print(f"WARNING: Tesseract not found at {tesseract_src} — release will "
f"rely on install.bat for OCR setup. Set TESSERACT_DIR to bundle it.")
clean_up()
if args.random_name:
@@ -97,6 +142,13 @@ if __name__ == "__main__":
new_name = ''.join(random.choices(string.ascii_letters, k=random.randint(6, 14)))
os.rename(f'{botty_dir}/main.exe', f'{botty_dir}/{new_name}.exe')
# Rename main.exe to avoid Warden flagging the obvious name
# In CI/production builds (env BOTTY_NO_RENAME=1) keep main.exe as-is
if not args.random_name and not os.environ.get("BOTTY_NO_RENAME"):
new_name = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
os.rename(f'{botty_dir}/main.exe', f'{botty_dir}/{new_name}.exe')
print(f"Renamed main.exe -> {new_name}.exe")
if new_version_code is not None:
os.system(f'git add .')
os.system(f'git commit -m "Bump version to v{args.version}"')
+15
View File
@@ -0,0 +1,15 @@
@echo off
setlocal
set "BOTTY_DIR=%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -File "%BOTTY_DIR%tools\check_dependencies.ps1"
set "RC=%ERRORLEVEL%"
if not "%RC%"=="0" (
echo.
echo Dependency check reported issues.
)
pause
exit /b %RC%
+23
View File
@@ -0,0 +1,23 @@
; Personal overrides for your local machine/user.
; This file is tracked as an example.
; Copy to config/custom.ini and edit values there.
;
; Botty auto-loads config/custom.ini when present.
[general]
; name=botty
; custom_message_hook=
; custom_loot_message_hook=
; discord_status_runs=10
[char]
; type=blizz_sorc
; atk_len_pindle=3.0
; show_items=alt
[sorceress]
; teleport=t
[blizz_sorc]
; blizzard=f1
; ice_blast=f2
File diff suppressed because it is too large Load Diff
+800
View File
@@ -0,0 +1,800 @@
{
"generated_at": "2026-06-17T07:19:33.572415+00:00",
"mode": "offline",
"offline_dir": "data\\d2jsp_pages",
"ladder_start_date": "2026-05-12",
"days": 60,
"topics_scanned": 853,
"estimates": {
"day_1": {},
"day_2": {},
"day_3": {},
"day_4": {},
"day_5": {},
"day_6": {},
"day_7": {},
"day_8": {},
"day_9": {},
"day_10": {},
"day_11": {},
"day_12": {},
"day_13": {},
"day_14": {},
"day_15": {},
"day_16": {},
"day_17": {},
"day_18": {},
"day_19": {},
"day_20": {},
"day_21": {},
"day_22": {},
"day_23": {},
"day_24": {},
"day_25": {},
"day_26": {},
"day_27": {},
"day_28": {},
"day_29": {},
"day_30": {},
"day_31": {
"CTA": {
"median_fg": 100.0,
"avg_fg": 358.0,
"min_fg": 1.0,
"max_fg": 1199.0,
"samples": 5
},
"Cham Rune": {
"median_fg": 30.0,
"avg_fg": 121.0,
"min_fg": 5.0,
"max_fg": 450.0,
"samples": 5
},
"Lo Rune": {
"median_fg": 25.0,
"avg_fg": 38.8,
"min_fg": 5.0,
"max_fg": 100.0,
"samples": 4
},
"Um Rune": {
"median_fg": 75.0,
"avg_fg": 99.6,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 10
},
"Lem Rune": {
"median_fg": 60.0,
"avg_fg": 72.1,
"min_fg": 5.0,
"max_fg": 190.0,
"samples": 7
},
"Sorc Torch": {
"median_fg": 100.0,
"avg_fg": 100.0,
"min_fg": 100.0,
"max_fg": 100.0,
"samples": 1
},
"Aldur's Advance": {
"median_fg": 27.5,
"avg_fg": 43.8,
"min_fg": 20.0,
"max_fg": 100.0,
"samples": 4
},
"Stealth RW": {
"median_fg": 55.0,
"avg_fg": 55.0,
"min_fg": 10.0,
"max_fg": 100.0,
"samples": 2
},
"Ist Rune": {
"median_fg": 95.0,
"avg_fg": 112.2,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 12
},
"Mal Rune": {
"median_fg": 100.0,
"avg_fg": 112.0,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 13
},
"Pul Rune": {
"median_fg": 60.0,
"avg_fg": 72.1,
"min_fg": 5.0,
"max_fg": 190.0,
"samples": 7
},
"Unid Anni": {
"median_fg": 550.0,
"avg_fg": 416.7,
"min_fg": 100.0,
"max_fg": 600.0,
"samples": 3
},
"Dual Leech Ring": {
"median_fg": 85.0,
"avg_fg": 85.0,
"min_fg": 70.0,
"max_fg": 100.0,
"samples": 2
},
"Gul Rune": {
"median_fg": 25.0,
"avg_fg": 63.1,
"min_fg": 5.0,
"max_fg": 200.0,
"samples": 8
},
"Key": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Insight": {
"median_fg": 25.0,
"avg_fg": 91.0,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 6
},
"Ohm Rune": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Vampire Gaze": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"White": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Shako": {
"median_fg": 25.0,
"avg_fg": 91.0,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 6
},
"Black": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Monarch": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Vex Rune": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Java SK": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"BK Ring": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Skin": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Oculus": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Spirit": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Bone": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Jah Rune": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Moser": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Ber Rune": {
"median_fg": 20.0,
"avg_fg": 18.3,
"min_fg": 5.0,
"max_fg": 30.0,
"samples": 3
},
"Arach": {
"median_fg": 90.0,
"avg_fg": 163.7,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 3
},
"5/5 Facet": {
"median_fg": 90.0,
"avg_fg": 163.7,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 3
},
"Mara": {
"median_fg": 90.0,
"avg_fg": 163.7,
"min_fg": 1.0,
"max_fg": 400.0,
"samples": 3
}
},
"day_32": {},
"day_33": {},
"day_34": {},
"day_35": {},
"day_36": {},
"day_37": {
"Thresher": {
"median_fg": 32.5,
"avg_fg": 133.3,
"min_fg": 0.0,
"max_fg": 3500.0,
"samples": 210
},
"Monarch": {
"median_fg": 20.0,
"avg_fg": 71.5,
"min_fg": 0.0,
"max_fg": 1300.0,
"samples": 318
},
"Spirit": {
"median_fg": 25.0,
"avg_fg": 78.3,
"min_fg": 0.0,
"max_fg": 1300.0,
"samples": 237
},
"Wind": {
"median_fg": 30.0,
"avg_fg": 175.0,
"min_fg": 0.0,
"max_fg": 1200.0,
"samples": 60
},
"Arach": {
"median_fg": 40.0,
"avg_fg": 184.2,
"min_fg": 5.0,
"max_fg": 3500.0,
"samples": 210
},
"Sorc Torch": {
"median_fg": 50.0,
"avg_fg": 166.2,
"min_fg": 10.0,
"max_fg": 2500.0,
"samples": 64
},
"Griffon": {
"median_fg": 60.0,
"avg_fg": 254.7,
"min_fg": 5.0,
"max_fg": 3500.0,
"samples": 111
},
"Guardian's Light": {
"median_fg": 50.0,
"avg_fg": 342.6,
"min_fg": 0.0,
"max_fg": 3500.0,
"samples": 236
},
"Skin": {
"median_fg": 30.0,
"avg_fg": 206.8,
"min_fg": 0.0,
"max_fg": 3000.0,
"samples": 242
},
"Viper Gorge": {
"median_fg": 30.0,
"avg_fg": 75.9,
"min_fg": 0.0,
"max_fg": 1200.0,
"samples": 231
},
"Lo Rune": {
"median_fg": 30.0,
"avg_fg": 113.6,
"min_fg": 5.0,
"max_fg": 1500.0,
"samples": 105
},
"Enigma": {
"median_fg": 20.0,
"avg_fg": 182.4,
"min_fg": 0.0,
"max_fg": 1500.0,
"samples": 49
},
"Bone": {
"median_fg": 40.0,
"avg_fg": 244.3,
"min_fg": 0.0,
"max_fg": 3500.0,
"samples": 264
},
"Um Rune": {
"median_fg": 45.0,
"avg_fg": 187.3,
"min_fg": 0.0,
"max_fg": 3500.0,
"samples": 191
},
"Lem Rune": {
"median_fg": 20.0,
"avg_fg": 48.7,
"min_fg": 0.0,
"max_fg": 750.0,
"samples": 126
},
"Crescent": {
"median_fg": 10.0,
"avg_fg": 63.0,
"min_fg": 5.0,
"max_fg": 600.0,
"samples": 70
},
"Ist Rune": {
"median_fg": 45.0,
"avg_fg": 74.9,
"min_fg": 3.0,
"max_fg": 750.0,
"samples": 173
},
"Highlord": {
"median_fg": 25.0,
"avg_fg": 113.0,
"min_fg": 5.0,
"max_fg": 1500.0,
"samples": 70
},
"BK Ring": {
"median_fg": 20.0,
"avg_fg": 70.0,
"min_fg": 0.0,
"max_fg": 1200.0,
"samples": 161
},
"Mal Rune": {
"median_fg": 30.0,
"avg_fg": 67.0,
"min_fg": 3.0,
"max_fg": 750.0,
"samples": 166
},
"Insight": {
"median_fg": 40.0,
"avg_fg": 189.9,
"min_fg": 5.0,
"max_fg": 3500.0,
"samples": 155
},
"Pul Rune": {
"median_fg": 30.0,
"avg_fg": 69.6,
"min_fg": 5.0,
"max_fg": 750.0,
"samples": 113
},
"CTA": {
"median_fg": 50.0,
"avg_fg": 251.5,
"min_fg": 5.0,
"max_fg": 3500.0,
"samples": 171
},
"Blessed": {
"median_fg": 30.0,
"avg_fg": 180.3,
"min_fg": 7.0,
"max_fg": 1200.0,
"samples": 36
},
"Ohm Rune": {
"median_fg": 25.0,
"avg_fg": 39.8,
"min_fg": 3.0,
"max_fg": 400.0,
"samples": 119
},
"Java SK": {
"median_fg": 20.0,
"avg_fg": 47.0,
"min_fg": 1.0,
"max_fg": 450.0,
"samples": 58
},
"Vampire Gaze": {
"median_fg": 10.0,
"avg_fg": 110.6,
"min_fg": 0.0,
"max_fg": 1500.0,
"samples": 80
},
"Giant": {
"median_fg": 20.0,
"avg_fg": 96.1,
"min_fg": 0.0,
"max_fg": 600.0,
"samples": 70
},
"Ber Rune": {
"median_fg": 60.0,
"avg_fg": 253.1,
"min_fg": 5.0,
"max_fg": 1200.0,
"samples": 58
},
"Key": {
"median_fg": 20.0,
"avg_fg": 83.1,
"min_fg": 1.0,
"max_fg": 1200.0,
"samples": 114
},
"Shako": {
"median_fg": 30.0,
"avg_fg": 113.6,
"min_fg": 0.0,
"max_fg": 1500.0,
"samples": 208
},
"Pcomb SK": {
"median_fg": 160.0,
"avg_fg": 685.0,
"min_fg": 5.0,
"max_fg": 3000.0,
"samples": 48
},
"Sacred": {
"median_fg": 35.0,
"avg_fg": 166.1,
"min_fg": 0.0,
"max_fg": 3500.0,
"samples": 244
},
"Light Jewel": {
"median_fg": 80.0,
"avg_fg": 234.0,
"min_fg": 20.0,
"max_fg": 1500.0,
"samples": 39
},
"Fist": {
"median_fg": 20.0,
"avg_fg": 51.7,
"min_fg": 15.0,
"max_fg": 120.0,
"samples": 9
},
"Vex Rune": {
"median_fg": 30.0,
"avg_fg": 97.2,
"min_fg": 5.0,
"max_fg": 1500.0,
"samples": 132
},
"Unid Anni": {
"median_fg": 35.0,
"avg_fg": 45.0,
"min_fg": 3.0,
"max_fg": 150.0,
"samples": 96
},
"Light SK": {
"median_fg": 40.0,
"avg_fg": 383.2,
"min_fg": 5.0,
"max_fg": 3500.0,
"samples": 41
},
"Mara": {
"median_fg": 50.0,
"avg_fg": 146.1,
"min_fg": 5.0,
"max_fg": 1500.0,
"samples": 132
},
"Cold SK": {
"median_fg": 50.0,
"avg_fg": 162.9,
"min_fg": 7.0,
"max_fg": 1200.0,
"samples": 45
},
"Black": {
"median_fg": 30.0,
"avg_fg": 106.7,
"min_fg": 0.0,
"max_fg": 2400.0,
"samples": 199
},
"Jah Rune": {
"median_fg": 60.0,
"avg_fg": 267.5,
"min_fg": 0.0,
"max_fg": 1200.0,
"samples": 87
},
"Troll": {
"median_fg": 30.0,
"avg_fg": 32.1,
"min_fg": 1.0,
"max_fg": 75.0,
"samples": 66
},
"Ward": {
"median_fg": 10.0,
"avg_fg": 21.8,
"min_fg": 1.0,
"max_fg": 50.0,
"samples": 45
},
"White": {
"median_fg": 20.0,
"avg_fg": 37.6,
"min_fg": 3.0,
"max_fg": 300.0,
"samples": 126
},
"Stealth RW": {
"median_fg": 32.5,
"avg_fg": 31.7,
"min_fg": 5.0,
"max_fg": 50.0,
"samples": 24
},
"Doom": {
"median_fg": 50.0,
"avg_fg": 285.3,
"min_fg": 45.0,
"max_fg": 1500.0,
"samples": 30
},
"Gul Rune": {
"median_fg": 30.0,
"avg_fg": 50.4,
"min_fg": 0.0,
"max_fg": 280.0,
"samples": 136
},
"Death's Fathom": {
"median_fg": 110.0,
"avg_fg": 96.7,
"min_fg": 60.0,
"max_fg": 120.0,
"samples": 12
},
"Unid Torch": {
"median_fg": 60.0,
"avg_fg": 49.5,
"min_fg": 5.0,
"max_fg": 120.0,
"samples": 39
},
"Sanctuary": {
"median_fg": 85.0,
"avg_fg": 82.5,
"min_fg": 5.0,
"max_fg": 170.0,
"samples": 24
},
"Grief": {
"median_fg": 50.0,
"avg_fg": 170.2,
"min_fg": 5.0,
"max_fg": 1500.0,
"samples": 56
},
"Night": {
"median_fg": 250.0,
"avg_fg": 209.0,
"min_fg": 20.0,
"max_fg": 700.0,
"samples": 15
},
"5/5 Facet": {
"median_fg": 50.0,
"avg_fg": 313.3,
"min_fg": 10.0,
"max_fg": 1300.0,
"samples": 15
},
"Treachery": {
"median_fg": 20.0,
"avg_fg": 157.0,
"min_fg": 5.0,
"max_fg": 1300.0,
"samples": 30
},
"Pala Torch": {
"median_fg": 50.0,
"avg_fg": 215.6,
"min_fg": 20.0,
"max_fg": 1500.0,
"samples": 43
},
"Bulwark": {
"median_fg": 50.0,
"avg_fg": 440.0,
"min_fg": 50.0,
"max_fg": 1300.0,
"samples": 10
},
"Fortitude": {
"median_fg": 70.0,
"avg_fg": 915.0,
"min_fg": 20.0,
"max_fg": 3500.0,
"samples": 16
},
"Cham Rune": {
"median_fg": 20.0,
"avg_fg": 22.5,
"min_fg": 5.0,
"max_fg": 60.0,
"samples": 53
},
"War Traveler": {
"median_fg": 20.0,
"avg_fg": 33.0,
"min_fg": 5.0,
"max_fg": 80.0,
"samples": 33
},
"Oculus": {
"median_fg": 20.0,
"avg_fg": 29.3,
"min_fg": 5.0,
"max_fg": 90.0,
"samples": 63
},
"Moser": {
"median_fg": 12.5,
"avg_fg": 12.5,
"min_fg": 5.0,
"max_fg": 20.0,
"samples": 16
},
"Hellfire": {
"median_fg": 60.0,
"avg_fg": 53.3,
"min_fg": 20.0,
"max_fg": 80.0,
"samples": 12
},
"Conviction": {
"median_fg": 20.0,
"avg_fg": 13.7,
"min_fg": 1.0,
"max_fg": 20.0,
"samples": 6
},
"Terror Key": {
"median_fg": 5.0,
"avg_fg": 6.7,
"min_fg": 5.0,
"max_fg": 10.0,
"samples": 12
},
"Hate Key": {
"median_fg": 5.0,
"avg_fg": 6.7,
"min_fg": 5.0,
"max_fg": 10.0,
"samples": 12
},
"Dual Leech Ring": {
"median_fg": 50.0,
"avg_fg": 198.8,
"min_fg": 20.0,
"max_fg": 1500.0,
"samples": 16
},
"Cyclone": {
"median_fg": 300.0,
"avg_fg": 306.7,
"min_fg": 20.0,
"max_fg": 600.0,
"samples": 12
},
"Fury": {
"median_fg": 30.0,
"avg_fg": 76.9,
"min_fg": 5.0,
"max_fg": 250.0,
"samples": 32
}
},
"day_38": {},
"day_39": {},
"day_40": {},
"day_41": {},
"day_42": {},
"day_43": {},
"day_44": {},
"day_45": {},
"day_46": {},
"day_47": {},
"day_48": {},
"day_49": {},
"day_50": {},
"day_51": {},
"day_52": {},
"day_53": {},
"day_54": {},
"day_55": {},
"day_56": {},
"day_57": {},
"day_58": {},
"day_59": {},
"day_60": {}
}
}
+50
View File
@@ -0,0 +1,50 @@
{
"notes": "Estimated Day 1-14 ladder prices derived from Day 3 snapshot using decay multipliers. Use as planning guidance, not exact market truth.",
"source_day": 3,
"day_multipliers": {
"day_1": 1.45,
"day_2": 1.2,
"day_3": 1.0,
"day_4": 0.93,
"day_5": 0.88,
"day_6": 0.84,
"day_7": 0.8,
"day_8": 0.77,
"day_9": 0.74,
"day_10": 0.72,
"day_11": 0.7,
"day_12": 0.68,
"day_13": 0.66,
"day_14": 0.64
},
"base_day_3_fg": {
"Cham Rune": 800,
"Lo Rune": 900,
"Ohm Rune": 600,
"Vex Rune": 400,
"Gul Rune": 200,
"Ist Rune": 200,
"Mal Rune": 120,
"Um Rune": 90,
"Pul Rune": 50,
"Lem Rune": 70,
"Unid Anni": 400,
"Unid Torch": 1500,
"Unid Griffon": 2000,
"Unid Eth Andy": 2000,
"Shako": 400,
"Mara 30": 1200,
"Mara Mid": 750,
"BK 5": 600,
"War Traveler": 500,
"Death's Fathom": 800,
"5/5 Facet": 500,
"5@res SC": 300,
"20life SC": 100,
"7mf SC": 100,
"Pcomb SK": 200,
"Cold SK": 250,
"Java SK": 200,
"Light SK": 200
}
}
+40 -20
View File
@@ -18,7 +18,7 @@ rejuv_potion=140,50,40,160,255,255
skill_charges=70,30,25,150,163,255
health_globe_red=178,110,20,183,255,255
health_globe_green=47,90,20,54,255,255
mana_globe=117,120,20,121,255,255
mana_globe=110,50,15,125,255,255
blue_slot=102,194,18,138,230,54
green_slot=33,181,18,87,258,69
red_slot=161,204,28,197,240,64
@@ -51,8 +51,8 @@ potion1_y=695
potion_width=30
potion_height=30
potion_next=41
merc_health_top=14
merc_health_left=15
merc_health_top=675
merc_health_left=305
merc_health_width=40
; skills
skill_y=693
@@ -70,8 +70,8 @@ wp_act_btn_width=67
wp_first_btn_x=227
wp_first_btn_y=129
wp_btn_height=47
; pickit
item_dist=267
; pickit **Note: 120 is apx pickup range. Set to this value or below to prevent walking on pickup
item_dist=121
; pather
reached_node_dist=100
min_walk_dist=25
@@ -88,24 +88,26 @@ play_btn=426,616,320,71
difficulty_select=536,236,210,320
gold_btn=997,521,20,18
inventory_gold=980,510,150,40
gold_btn_stash=150,480,40,65
gold_btn_stash=158,518,30,30
vendor_gold_digits=186,509,97,16
stash_gold_digits=185,507,99,15
stash_gold_digits=184,527,100,16
inventory_gold_digits=1017,523,100,16
stash_page_digits=222,478,10,15
health_globe=160,580,240,140
mana_globe=887,580,240,140
health_slice=309,610,7,101
mana_slice=961,610,7,101
cut_skill_bar=0,0,1280,653
cut_skill_bar=0,0,1284,653
reduce_to_center=120,60,1040,540
search_npcs=120,0,1040,620
merc_icon=0,0,100,100
search_npcs=120,0,1042,620
merc_icon=10,9,56,56
loading_left_black=0,0,350,720
death=444,198,397,71
tp_search=353,120,547,400
repair_btn=318,473,90,80
left_inventory=35,92,378,378
right_inventory=866,348,379,152
left_inventory=33,84,382,382
right_inventory=868,348,379,152
transmute_third_slot=242,342,38,38
skill_right=664,673,41,41
skill_right_expanded=655,375,385,255
skill_left=574,673,41,41
@@ -118,21 +120,23 @@ stash_btn_roi=20,52,412,53
enemy_info=440,15,405,150
shrine_check=500,50,350,100
character_select=1033, 44, 226, 554
character_online_status=1033, 19, 226, 25
character_online_status=1030, 18, 240, 40
; character_sub_roi is with respect to matched active character template
character_name_sub_roi=6, 22, 186, 18
character_name_sub_roi=2, 20, 194, 22
cube_area_roi=167,209,113,152
cube_btn_roi=160,368,125,57
xp_bar_text=369,630,554,34
corpse=459,195,414,213
chat_icon=7,555,43,41
left_panel_header=0,0,450,54
right_panel_header=830,0,450,54
npc_dialogue=458,4,21,140
left_panel_header=0,0,455,56
right_panel_header=830,0,455,56
npc_dialogue=456,0,30,150
bind_skill=516,619,251,29
quest_skill_btn=284,455,710,121
left_inventory_tabs=31,62,385,28
tab_indicator=31,82,385,14
left_inventory_tabs=29,60,389,30
tab_indicator=31,71,385,14
stash_page_select_left=155,475,20,20
stash_page_select_right=270,475,20,20
deposit_btn=454,365,186,43
equipped_inventory_area=861,59,387,287
inventory_bg_pattern=21,622,1236,75
@@ -141,7 +145,7 @@ inventory_bg_pattern=21,622,1236,75
; static pathes in format: x0,y0, x1,y1, x2,y2, ...
pindle_safe_dist=921,35, 1123,70, 960,88
eldritch_safe_dist=652,63, 563,73
trav_safe_dist=1156,438, 1120,282, 1059,525, 1146,312, 1132,346, 985,538, 1125,333
trav_safe_dist=1156,438, 1120,282, 1059,525, 1146,312, 1132,346, 1100,538
pindle_end=1037,174
eldritch_end=675,210
shenk_end=1067,544
@@ -244,3 +248,19 @@ dia_c2g_home_loop=175,175
dia_a_layout_bold=189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 189,193, 1100,337
dia_b_layout_bold=1129,148, 1129,148, 1129,148, 1129,148, 1129,148, 1129,148
dia_c_layout_bold=1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503, 1112,503
; Andariel (A1 Catacombs) - placeholder, fill in real coords later
a1_andy_level3_enter=0,0
a1_andy_level4_enter=0,0
a1_andy_safe_dist=0,0
; Countess (A1 Forgotten Tower) - placeholder, fill in real coords later
a1_tower_level2_enter=0,0
a1_tower_level3_enter=0,0
a1_tower_level4_enter=0,0
a1_tower_level5_enter=0,0
a1_countess_safe_dist=0,0
; Mephisto (A3 Durance of Hate) - placeholder, fill in real coords later
a3_meph_level3_enter=0,0
a3_meph_safe_dist=0,0
; Baal (A5 Throne of Destruction) - placeholder, fill in real coords later
a5_baal_throne_entry=0,0
a5_baal_safe_dist=0,0
+437 -80
View File
@@ -1,61 +1,248 @@
; There is detailed documentation for each parameter in the README.md
[general]
; Personal values can reference env keys from repo-root .env:
; Example syntax:
; custom_message_hook=${BOTTY_CUSTOM_MESSAGE_HOOK}
; custom_loot_message_hook=${ENV:BOTTY_CUSTOM_LOOT_MESSAGE_HOOK}
; bnet_name=${BOTTY_BNET_NAME}
; bnet_pass=${BOTTY_BNET_PASS}
; char_name=${BOTTY_CHAR_NAME}
; difficulty: game difficulty to create ("normal", "nightmare", "hell")
;
; Hammerdin difficulty guide (CTA build with Conviction aura):
; NORMAL - Very easy. 1-2h Hammer kills most packs. No CTA needed.
; NIGHTMARE - Recommended starting point. Bosses hit ~100-200 dmg.
; CTA + Conviction drops resists. Need decent armor (ED/HR).
; Gear targets: ~200 AR, ~15 ED, ~30% HR, ~50% FCR on hammer.
; HELL - Bosses hit 400-800+ dmg per swing. Conviction is mandatory.
; Gear targets: ~350+ AR, ~20+ ED, ~50%+ HR, ~60%+ FCR,
; ~150% IAS on weapon. Full Rejuv belt recommended.
; If you chicken/die repeatedly, drop to Nightmare first.
difficulty=hell
name=Botty
; name: bot profile name used in logs/messages and mod launch option replacement
name=fistman
; randomize_runs: 0 = run in listed order, 1 = shuffle run order
randomize_runs=0
; target_tz: target Terror Zone id (leave as default unless you know the mapping)
target_tz=1
; saved_games_folder: optional override path to D2R Saved Games folder (blank = auto)
saved_games_folder=
; level_max_steps: max pathing steps for leveling-style routines
level_max_steps=20
; Set to 1 to enable auto-login and auto-launch of D2R on startup.
; Credentials below are ONLY used when auto_login=1.
auto_login=0
bnet_name=${BOTTY_BNET_NAME}
bnet_pass=${BOTTY_BNET_PASS}
; Character name to auto-select from the character selection screen
; If empty, bot relies on the saved character template from previous sessions
char_name=${BOTTY_CHAR_NAME}
; messaging
custom_loot_message_hook=
custom_message_hook=
; custom_loot_message_hook: optional separate webhook for loot notifications
custom_loot_message_hook=${BOTTY_CUSTOM_LOOT_MESSAGE_HOOK}
; custom_message_hook: main webhook for status/death/chicken messages
custom_message_hook=${BOTTY_CUSTOM_MESSAGE_HOOK}
; discord_log_chicken: 1 = send chicken/death style notifications
discord_log_chicken=1
; discord_log_errors: 1 = send a Discord message + error screenshot every time a
; run fails (approach/battle/exception). Set to 0 to keep error screenshots on
; disk only. Can also be toggled via [discord_events] error=0.
discord_log_errors=1
; discord_status_runs: send periodic status every X completed runs (blank/0 disables)
discord_status_runs=10
; discord_status_count: legacy fallback, send periodic status every X games (blank/0 disables)
discord_status_count=20
; message_api_type: "" disables messaging, "discord" or "generic_api"
message_api_type=discord
; breaks
; break_length_m: scheduled break duration in minutes (0 = disabled)
break_length_m=0
; max_runtime_before_break_m: runtime before taking scheduled break (0 = disabled)
max_runtime_before_break_m=0
; timers / fail handling
; d2r_path: Diablo II: Resurrected install path
d2r_path=C:\Program Files (x86)\Diablo II Resurrected
; max_consecutive_fails: stop bot after this many failed runs in a row
max_consecutive_fails=5
max_game_length_s=380
; if you set this field to 1, botty will attempt to restart d2 after a crash or failure
restart_d2r_when_stuck=0
; max_game_length_s: emergency timeout per run/game
; 900: a full Chaos Sanctuary clear (3 seals + bosses + loot) takes ~10 min;
; 600 force-quit a game while literally waiting for Diablo to spawn (2026-06-10).
; Genuinely stuck games are caught much earlier by max_maintenance_time_s and
; approach step timeouts.
max_game_length_s=900
; max_maintenance_time_s: if the town maintenance loop (heal/buy/stash/repair) takes
; longer than this many seconds, save-and-exit and rejoin a fresh game.
; Prevents the bot staying stuck in A5 town forever when NPCs or pathing fail.
max_maintenance_time_s=240
; auto_downgrade_threshold: if combined chickens+deaths exceed this number within 1 hour,
; bot automatically lowers difficulty by one tier (hell->nightmare->normal) and restarts.
; Set to 0 to disable. (NOTE: feature is parsed but not yet active in bot logic)
auto_downgrade_threshold=0
restart_d2r_when_stuck=1
; hardcore: 1 enables hardcore-safe assumptions in some routines
hardcore=0
; screenshots
; info_screenshots: save screenshots for info/chicken/death events
info_screenshots=1
; error_screenshots: save a screenshot to log/screenshots/error/ every time a run
; fails (approach failure, battle failure, or an exception) so logs and visuals can
; be reviewed side by side. Falls back to info_screenshots if unset.
error_screenshots=1
; loot_screenshots: save screenshots for picked loot
loot_screenshots=0
pickit_screenshots=0
; pickit_screenshots: save screenshots for pickit debugging
pickit_screenshots=1
; recovery
; disable_run_after_failures: after this many CONSECUTIVE failures of the same run
; (e.g. run_vizier), the bot disables just that run for the rest of the session and
; keeps doing the other runs instead of stopping. A single success resets the count.
; If every run gets disabled the bot stops for investigation. Set high to effectively
; disable this behaviour.
disable_run_after_failures=5
; stash_scan_interval: scan all stash tabs and export stash_list.csv every X runs (0 disables)
stash_scan_interval=0
[discord_events]
; Fine-grained Discord event toggles (1=send, 0=disable)
; status: periodic status + generic bot messages (breaks, shop notifications, etc.)
status=1
; item_keep: send kept item notifications (loot webhooks/embeds)
item_keep=1
; death: send death notifications
death=1
; chicken: send chicken (emergency leave) notifications
chicken=1
; stash_full: send stash full notifications
stash_full=1
; gold_full: send gold full notifications
gold_full=1
; error: send run-failure notifications (message + error screenshot)
error=1
[stealth]
; Multiplies all wait() calls by a random value in this range each call
; 1.0 = no change. Set range wider for more human-like timing variation.
wait_jitter_min = 0.85
wait_jitter_max = 1.20
; Extra pixel variance added to every mouse click (on top of existing randomize=5)
; 0 = off, 10 = +/-10px extra random offset per click
click_variance = 8
; Re-shuffle run order after completing a full rotation (vs only at session start)
; 0: keep [pindle, diablo] fixed so every game ENDS in A4 town (Diablo run TPs
; there) — the next game then spawns at A4 where Jamella/Cain/Tyrael work,
; avoiding the A5 Malah vendor entirely (stale templates in current patch).
reshuffle_each_rotation = 0
; Probability (0-100) of randomly skipping a run each game
; 0 = never skip, 20 = skip ~1 in 5 runs
skip_run_chance = 10
; Probability (0-100) of taking an unscheduled AFK break after any given run
afk_break_chance = 5
; Break duration range in minutes
afk_break_min_m = 2
afk_break_max_m = 12
; Vary run/battle duration by a Gaussian factor (default +/-15%)
; 0.0 = no variation, 0.3 = up to +/-30% variation
run_duration_variance = 0.15
; Micro-pause between actions (simulates human hesitation in milliseconds)
micro_pause_min_ms = 20
micro_pause_max_ms = 120
; Vary kill time to avoid perfectly consistent boss fight durations
vary_kill_time = 1
; Human mouse curve complexity (1.0 = default, 0.5 = more direct, 1.5 = more winding)
human_curve_complexity = 1.0
; Arrival-to-click delay: human-like pause between mouse arriving and clicking (milliseconds)
; 50-800ms range simulates "is this the right thing?" hesitation
click_delay_min_ms = 50
click_delay_max_ms = 800
; Key press duration variance: how long a key is held (milliseconds)
; Most presses are short (20-100ms), some linger (up to 200ms)
key_press_min_ms = 20
key_press_max_ms = 200
; Skill rotation hesitation: pause before casting a skill (milliseconds)
; Prevents machine-speed skill spam
skill_hesitation_min_ms = 80
skill_hesitation_max_ms = 300
; Wrong waypoint chance: 2-3% of selecting wrong TP portal then correcting
; Humans occasionally misclick waypoint targets
wrong_waypoint_chance = 0.025
; Skill mistake chance: 1-2% chance of miscasting and correcting
; Simulates human error during combat
skill_mistake_chance = 0.015
[routes]
; Add these possible routes to "order" to run them:
; run_trav
; run_pindle
; run_eldritch
; run_eldritch_shenk
; run_nihlathak
; run_arcane
; run_diablo
order=run_pindle, run_eldritch_shenk
; Controls which farm runs Botty performs each game.
; "order" is a comma-delimited list and runs left-to-right when randomize_runs=0.
; If randomize_runs=1 (in [general]), Botty shuffles enabled runs each game.
;
; Hammerdin keyrun recommendation (stable-first):
; order=run_countess, run_arcane, run_nihlathak
;
; Route quick notes:
; run_trav (Act 3 Travincal council farm; short/high-density run)
; run_pindle (Act 5 Nihlathak temple red portal boss farm)
; run_eldritch (Act 5 Frigid Highlands Eldritch-only run)
; run_eldritch_shenk (Act 5 Eldritch then Shenk in same game)
; run_nihlathak (Act 5 Halls of Vaught, teleport strongly recommended)
; run_arcane (Act 2 Arcane Sanctuary / Summoner, teleport strongly recommended)
; run_diablo (Act 4 Chaos Sanctuary, teleport recommended)
; run_vizier (Act 4 Chaos Vizier-only route; lighter/faster than full Diablo run)
; run_andariel (Act 1 Catacombs)
; run_countess (Act 1 Forgotten Tower)
; run_mephisto (Act 3 Durance of Hate)
; run_baal (Act 5 Throne of Destruction)
order=run_pindle, run_diablo
[char]
; ==========================
; ==== Mandatory Fields ====
; ==========================
; These configs have to be alligned with your d2r settings and char build
type=light_sorc
; type: character build profile to use (must match one section below)
; examples: blizz_sorc, hammerdin, fohdin
type=hammerdin
; belt_rows: in-game belt size (2/3/4)
belt_rows=4
casting_frames=10
cta_available=0
; casting_frames: your char cast breakpoint (affects action timing)
casting_frames=8
; cta_casting_frames: cast breakpoint on CTA swap (if used)
cta_casting_frames=8
; attack_frames: base attack animation timing for non-cast attacks
attack_frames=15
; cta_available: 1 if Call to Arms swap exists, else 0
cta_available=1
;Do we want to cast non-cta buffs (ex energy shield) with cta
buff_with_cta=1
; safer_routines: enable for optional defensive maneuvers/etc during combat/runs at the cost of increased runtime (ex. hardcore players)
safer_routines=0
safer_routines=1
; num_loot_columns: Number of empty columns from left to right of inventory to be used for looting.
; Store charms, etc. to the right of the inventory.
num_loot_columns=5
num_loot_columns=4
; game hotkeys:
; NOTE: each key must match your in-game binding exactly
force_move=e
inventory_screen=i
potion1=1
@@ -68,12 +255,13 @@ show_items=alt
; stand_still cannot be the default "shift" as it would interfere with merc healing
stand_still=capslock
; teleport: leave empty if you can't use
teleport=
town_portal=f9
teleport=b
town_portal=6
; call to arms settings:
weapon_switch=x
battle_orders=f7
battle_command=f8
; weapon_switch/battle_orders/battle_command only used when cta_available=1
weapon_switch=w
battle_orders=7
battle_command=8
; ==========================
; ==== Optional configs ====
@@ -82,23 +270,61 @@ stash_gold=1
use_merc=1
; Attack length for barbarians should be as high as 8-10 and even 10-12 for trav/shenk
;
; Hammerdin attack lengths (seconds of hammer spam per boss):
; atk_len_trav = 4.0 (Council of 3 - 3 council members, easy)
; atk_len_pindle = 8.0 (Pindle — Hell: 13094-16070 HP, 75% fire, 100% poison)
; Pindle stats by difficulty:
; Normal: 1064-1588 HP, 75% fire, 70% poison
; Nightmare: 4773-5859 HP, 100% poison
; Hell: 13094-16070 HP, 75% fire, 50% cold, 33% light, 100% poison
; With Conviction (-33% res), Hell Pindle = 50% fire, 17% cold, 0% light, 67% poison
; CTA adds +100% dmg to hammer. Need ~60%+ FCR and good IAS to kill in 8s.
; atk_len_nihlathak = 4.0 (Nihlathak - single boss)
; atk_len_eldritch = 3.0 (Eldritch only - single boss)
; atk_len_shenk = 4.0 (Shenk only - single boss)
; atk_len_arc = 2.5 (Summoner in Arcane - single boss)
; atk_len_diablo = 10.0 (Diablo in CS - longest single boss)
; atk_len_countess = 3.0 (Countess - single boss, easy)
; atk_len_andariel = 4.0 (Andariel - single boss)
; atk_len_mephisto = 12.0 (Mephisto - single boss, high HP)
; atk_len_baal = 10.0 (Baal - single boss)
; atk_len_baal_waves = 30.0 (Baal's spawn waves before boss)
;
; Chaos Sanctuary (Diablo run) individual fights:
; atk_len_cs_trashmobs = 2.0 (Trash packs in CS)
; atk_len_diablo_vizier = 2.0 (Vizier of Chaos - seal boss A)
; atk_len_diablo_infector = 4.0 (Infector of Souls - seal boss C)
; atk_len_diablo_deseis = 5.0 (Lord De Seis - seal boss B, hardest seal)
;
; Increase these if your hammer kills slower (low IAS/FCR).
; Decrease if your hammer kills faster (high IAS/FCR, good gear).
; If you die during a fight, the attack length is too long for your defense.
atk_len_arc=2.5
atk_len_eldritch=3.0
atk_len_nihlathak=4.0
atk_len_pindle=3.0
atk_len_pindle=8.0
atk_len_shenk=4.0
atk_len_trav=3.0
atk_len_diablo=3.0
atk_len_trav=4.0
; Boss run attack lengths (per-character defaults in kill_* methods)
; Adjust these if your build is faster/slower against these bosses
atk_len_andariel=4.0
atk_len_countess=3.0
atk_len_mephisto=12.0
atk_len_baal=10.0
atk_len_baal_waves=30.0
; Chaos Sanctuary settings
atk_len_cs_trashmobs=1.5
atk_len_cs_trashmobs=2.0
atk_len_diablo_deseis=5.0
atk_len_diablo_infector=4.0
atk_len_diablo_vizier=2.0
atk_len_diablo=3.0
cs_mob_detect=1
cs_mob_detect=0
; cs_town_visits is currently broken, ignore for now
cs_town_visits=0
kill_cs_trash=0
kill_cs_trash=1
; Belt settings
belt_hp_columns=1
@@ -106,45 +332,70 @@ belt_mp_columns=1
belt_rejuv_columns=2
; Potion/chicken settings
take_health_potion=0.8
take_mana_potion=0.5
take_rejuv_potion_health=0.4
take_rejuv_potion_mana=0.1
heal_merc=0.7
heal_rejuv_merc=0.2
chicken=0.35
merc_chicken=0
take_health_potion=0.60
take_mana_potion=0.40
take_rejuv_potion_health=0.45
take_rejuv_potion_mana=0.10
heal_merc=0.70
heal_rejuv_merc=0.45
chicken=0.40
merc_chicken=0.20
; Misc.
; helps reduce accidental pickups when enabled especially on walking characters
enable_no_pickup=1
enable_no_pickup=0
; fill_shared_stash_first: 1 = prefer shared stash tabs before personal stash
fill_shared_stash_first=0
; to gamble, add any/all of the following: circlet, ring, coronet, talon, amulet
gamble_items=
id_items=1
; open_chests: 1 = open clickable chests along path when possible
open_chests=1
; pre_buff_every_run: 1 = always recast buffs at run start
pre_buff_every_run=1
runs_per_repair=0
; runs_per_repair: visit repair vendor every X runs (blank/0 disables)
runs_per_repair=5
; repair_npc: preferred repair vendor strategy.
; - a5_larzuk (recommended: stays in A5, no cross-act trip; falls back to Halbu)
; - a4_halbu (requires WP trip to A4 every repair — act desync risk if it fails)
; 2026-06-10: switched to a5_larzuk — session logs showed Halbu detection failing
; 100% (body score ~0.39) and each failed A4 trip desynced the bot's act state.
repair_npc=a5_larzuk
; runs_per_stash: stash/sell every X runs (blank/0 disables)
runs_per_stash=4
sell_junk=0
; sell_junk: 1 = vendor non-keep items automatically
sell_junk=1
; protect_shields_from_sell: 1 = never vendor items with "shield" in detected name (recommended safety)
protect_shields_from_sell=1
; pick_rares_for_gold: 1 = pick all yellow (rare) ground items; non-keep rares will be sold
; Requires sell_junk=1 to convert extra pickups into gold.
pick_rares_for_gold=0
; pick_gold: 1 = pick up ground gold piles, 0 = ignore all ground gold
pick_gold=1
[transmute]
;stash tabs by priority where to put transmuted gems
stash_destination=3,2,1,0
stash_destination=0,1,2,3
; Add these possible gems to "transmute" to transmute them:
; chipped, flawed, standard, flawless
transmute=flawless
;how often we want to run transmute routine(e.g. every 100 games)
transmute_every_x_game=20
transmute_every_x_game=60
; number of stash tabs (ROTW has more than 6, adjust as needed)
stash_tabs=6
; potion transmute settings
; convert_rejuv: 1 = convert regular Rejuv Potions to Full Rejuv via cube (3 -> 1)
convert_rejuv=1
; min_rejuv_to_convert: minimum regular rejuv potions in inventory before starting conversion
min_rejuv_to_convert=6
; ===========================
; ==== Builds: Sorceress ====
; ===========================
[sorceress]
energy_shield=
frozen_armor=
static_field=
telekinesis=
energy_shield=f4
frozen_armor=f7
static_field=f5
telekinesis=f6
thunder_storm=
[light_sorc]
@@ -157,55 +408,97 @@ frozen_orb=
[blizz_sorc]
; blizzard must be right skill, hotkey required
blizzard=
blizzard=f1
; ice_blast must be left skill (hotkey optional as it shouldnt change)
ice_blast=
ice_blast=f2
[blizzorb_sorc]
; frozen orb must be left skill and preselected (no hotkey required)
;All others must be right skill and hotkey required!
blizzard=f1
glacial_spike=f7
[nova_sorc]
; nova must be right skill, hotkey required
nova=
nova=f1
[hydra_sorc]
; only supports Pindle and Eldritch currently
; alt_attack is any alternate attacking right skill. Fireball,Lightning,Frozen Orb, hotkey required
alt_attack=
alt_attack=f7
; hydra must be right skill, hotkey required
hydra=
hydra=f1
; =========================
; ==== Builds: Paladin ====
; =========================
[paladin]
cleansing=
holy_shield=
redemption=
vigor=
cleansing=f9
holy_shield=f2
redemption=f3
vigor=f4
[fohdin]
; foh must be left skill, hotkey required
blessed_hammer=
concentration=
conviction=
foh=
holy_bolt=
blessed_hammer=f1
concentration=f8
conviction=f5
foh=f6
holy_bolt=f7
[hammerdin]
blessed_hammer=
concentration=
blessed_hammer=f1
concentration=f8
conviction=f5
; =========================
; ==== Builds: Warlock ====
; =========================
[warlock]
; All skills should be on right-click unless otherwise specified
deathmark=f3
lethargy=f4
;Set summon_demon to one of the three demons of your choice.
summon_demon=f5
;If you are summoning a 2nd demon set hotkey here. Can be same as above
summon_demon2=
psychic_ward=
[fire_lock]
; Supported runs: Pindle
;flame_wave must be assigned to left-click and doesn't require hotkey
ring_of_fire=f1
apocalypse=f2
[abyss_lock]
; Supported runs: Pindle
;miasma_bolt must be assigned to left-click and doesn't require hotkey
miasma_chain=f1
abyss=f2
[echo_lock]
; Supported runs: Trav (tele only)
echo_strike=f1
eldritch_blast=f2
hex_bane=f3
;Set ring of fire if you want to stun before echo blast
ring_of_fire=
; ==========================
; ==== Builds: Assassin ====
; ==========================
; Currently no Trav implementaiton!
[trapsin]
burst_of_speed=
death_sentry=
fade=
lightning_sentry=
shadow_warrior=
; We assume fire blast or shock web is left-click skill which is optional hotkey if pre-assigned.
skill_left=
; Assume all other skills are right-click and hotkey required!
burst_of_speed=f1
death_sentry=f2
fade=
lightning_sentry=f3
shadow_warrior=f4
mind_blast=f5
; ===========================
; ==== Builds: Barbarian ====
@@ -220,6 +513,21 @@ leap=
shout=
war_cry=
; ===========================
; ==== Builds: Amazon ====
; ===========================
; Supported runs: Trav, Pindle, Eld, Shenk, Nihlathak (tele only)
; Make sure leap hotkey is set if you do not have Enigma
; Ensure Battle Order/Command hotkeys are set above in the [char] section
[amazon]
;all must be right-click skill
valkyrie=f1
[javazon]
; charged strike must be left-click skill and preselected, no hotkey required
;all others must be right-click
lightning_fury=f2
; ==========================
; ==== Builds: Necro ====
@@ -277,7 +585,7 @@ damage_scaling=1
; if the buffs are not set they are not used (they are part of the prebuff setup)
[basic]
left_attack=
left_attack=1
right_attack=
buff_1=
buff_2=
@@ -297,25 +605,74 @@ buff_2=
[advanced_options]
; startup hotkeys
restore_settings_from_backup_key=f7
settings_backup_key=f8
auto_settings_key=f9
graphic_debugger_key=f10
; select_runs_key: open run selector UI
select_runs_key=pagedown
; restore_settings_from_backup_key: restore backed-up D2R settings
restore_settings_from_backup_key=insert
; settings_backup_key: create backup of current D2R settings
settings_backup_key=pause
; auto_settings_key: auto-apply required D2R settings
auto_settings_key=pageup
; graphic_debugger_key: toggle on-screen debug layers
graphic_debugger_key=delete
; resume_key: start/pause bot loop
resume_key=f11
; exit_key: hard stop bot
exit_key=f12
; cycle_pickit_profile_key: cycle through pickit profiles in config/pickit_profiles/
cycle_pickit_profile_key=f10
; etc.
; graphic_debugger_layer_creator: 1 = enable interactive layer creator tooling
graphic_debugger_layer_creator=0
; hwnd_window_process: process regex used to locate D2R window handle
hwnd_window_process=D2R\.exe
; hwnd_window_title: optional title regex override for window lookup
hwnd_window_title=
; launch_options: will replace <name> with setting for [general] "name" above
launch_options=-mod <name> -txt
; logg_lvl: logging verbosity (debug/info/warning/error)
logg_lvl=debug
; message_body_template: payload template for generic_api mode
message_body_template={{"content": "{msg}"}}
; message_headers: optional JSON headers for generic_api mode
message_headers=
; ocr_during_pickit: 1 = run OCR while looting (slower, more diagnostics)
ocr_during_pickit=0
;use "can_teleport_natively" or "can_teleport_with_charges" if you want to force certain behavior in case autodetection isn't working properly
override_capabilities=
pathing_delay_factor=4
;If you want to control Hyper-V window from host use 0,51 here
override_capabilities=can_teleport_natively
; pathing_delay_factor: movement/click delay multiplier (1 fast .. 10 slow)
pathing_delay_factor=2
; If you want to control Hyper-V window from host use 0,51 here
; window_client_area_offset: x,y pixel offset for captured game client area
window_client_area_offset=0,0
[log_rotation]
; Log rotation prevents screenshot directories from filling the disk.
; Each managed directory has a max file count and max total size.
; When limits are exceeded, oldest files are deleted automatically.
; Checked every 60 seconds (to avoid I/O overhead during runs).
; pickit directory (log/screenshots/pickit/):
; Can grow very fast — every item scan writes a PNG + JSON.
; pickit_max_files=500 ; max files before rotation kicks in
; pickit_max_mb=500 ; max total size in MB
pickit_max_files=300
pickit_max_mb=200
; info directory (log/screenshots/info/):
; Debug screenshots for deaths, chickens, errors, etc.
; info_max_files=200 ; max files before rotation kicks in
; info_max_mb=500 ; max total size in MB
info_max_files=100
info_max_mb=200
; items directory (log/screenshots/items/):
; Loot screenshots sent to Discord.
; items_max_files=100
; items_max_mb=100
items_max_files=50
items_max_mb=50
; discord_notify_rotation: 1 = send Discord message when log rotation deletes files
discord_notify_rotation=0
+26
View File
@@ -0,0 +1,26 @@
# Shared pickit profiles (git-tracked)
One folder per pickit set, e.g. for season phases:
```
config/pickit_profiles/
season_start/ *.bnip (leveling: keep bases, gems, chipped...)
mid_season/ *.bnip
endgame/ *.bnip (GG-only filter)
```
Drop `.bnip` (or `.nip`) files in a folder; a `.nipignore` works like in
`config/bnip`. These folders ARE committed — build them once, everyone
gets them via git pull.
## Selecting a set
Per user, in your gitignored `config/profiles/<you>/profile.ini`:
```ini
[general]
pickit_profile=season_start
```
Priority: `config/profiles/<you>/pickit/` (personal set, gitignored)
> `config/pickit_profiles/<pickit_profile>/` (shared, from this folder)
> `config/bnip/` > `config/default.bnip`.
+12 -3
View File
@@ -1,4 +1,5 @@
[claws]
; shop_trap_claws: 1 = enable claw-shopping routine for trap claws from Anya/Drognan flow
; Current scoring for trap claws
; 3 traps: +12
; 2/1 traps: +8
@@ -6,18 +7,23 @@
; x light senetry: +6
; x weapon block: +1
; x death sentrey: +4
shop_trap_claws=1
shop_trap_claws=0
; trap_min_score: minimum combined score required to keep a trap claw
trap_min_score=13
; shop_melee_claws: 1 = enable shopping melee claws (venom/block focused)
; Current scoring for melee claws
; 2 assa: +10
; x venom: +6
; x weapon block: +2
shop_melee_claws=1
shop_melee_claws=0
; melee_min_score: minimum combined score required to keep a melee claw
melee_min_score=13
[gloves]
; shop_3_skills_ias_gloves: 1 = shop +3 skill tree + IAS gloves
shop_3_skills_ias_gloves=1
; shop_2_skills_ias_gloves: 1 = shop +2 skill tree + IAS gloves
shop_2_skills_ias_gloves=0
;
@@ -29,6 +35,9 @@ shop_2_skills_ias_gloves=0
; apply_pather_adjustment - alternative option that applies an adjustment to movements.
; Should not need this. Try it if you have trouble when it is off.
[scepters]
; shop_hammerdin_scepters: 1 = enable Hammerdin scepter shopping route
shop_hammerdin_scepters=1
; speed_factor: movement compensation for FRW and path timing during shopping loop
speed_factor=0.25
apply_pather_adjustment=0
; apply_pather_adjustment: optional alternate node adjustment if default route misses NPC/shop spots
apply_pather_adjustment=0
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
View File
Binary file not shown.
Binary file not shown.
+1
Submodule dependencies/tesserocr_source added at fe9d88d4a6
+80
View File
@@ -0,0 +1,80 @@
"""
Desktop screenshot tool - captures the full Windows desktop or a specific window.
Usage:
python desktop_snap.py # capture full desktop
python desktop_snap.py D2R # capture D2R window only
Saves to screenshots/desktop_snap.png
"""
import os
import sys
import cv2
from mss import mss
SAVE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "screenshots", "desktop_snap.png")
os.makedirs(os.path.dirname(SAVE_PATH), exist_ok=True)
def snap_full_desktop():
"""Capture the full desktop."""
with mss() as sct:
img = sct.grab(sct.monitors[1]) # monitors[1] = primary display
# Convert from BGRA to BGR
img_bgr = img.rgb
cv2.imwrite(SAVE_PATH, img_bgr)
print(f"Saved full desktop to: {SAVE_PATH}")
print(f"Shape: {cv2.imread(SAVE_PATH).shape}")
def snap_d2r_window():
"""Capture the D2R window."""
import numpy as np
import win32gui
import win32ui
import win32con
# Find D2R window
def enum_cb(hwnd, results):
if win32gui.IsWindowVisible(hwnd):
title = win32gui.GetWindowText(hwnd)
if "diablo" in title.lower() or "d2r" in title.lower():
results.append(hwnd)
hwnds = []
win32gui.EnumWindows(enum_cb, hwnds)
if not hwnds:
print("ERROR: D2R window not found. Is it running?")
return
hwnd = hwnds[0]
print(f"Found D2R window: {win32gui.GetWindowText(hwnd)}")
# Get window client area
rect = win32gui.GetClientRect(hwnd)
w, h = rect[2] - rect[0], rect[3] - rect[1]
# Capture client area
hdc = win32gui.GetDC(hwnd)
hdc_mem = win32gui.CreateCompatibleDC(hdc)
bmp = win32gui.CreateCompatibleBitmap(hdc, w, h)
win32gui.SelectObject(hdc_mem, bmp)
win32gui.BitBlt(hdc_mem, 0, 0, w, h, hdc, 0, 0, win32con.SRCCOPY)
# Convert to image
bmp_info = win32ui.CreateBitmapFromHandle(bmp)
bmp_info.SaveBitmapFile(hdc_mem, SAVE_PATH)
win32gui.DeleteObject(bmp)
win32gui.DeleteDC(hdc_mem)
win32gui.ReleaseDC(hwnd, hdc)
img = cv2.imread(SAVE_PATH)
print(f"Saved D2R window to: {SAVE_PATH}")
print(f"Shape: {img.shape}")
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "D2R":
snap_d2r_window()
else:
snap_full_desktop()
+46 -9
View File
@@ -1,21 +1,58 @@
# Dev Docu
## Dependencies
- Install latest miniconda (https://docs.conda.io/en/latest/miniconda.html). Note: You will have to check "Add conda to my PATH environment variable" in order to access the conda command in the cmd.
- Install git (https://gitforwindows.org/)
- Install [Miniforge](https://github.com/conda-forge/miniforge) (recommended over Miniconda for conda-forge packages). Check "Add to PATH" during installation.
- Alternatively, [Miniconda](https://docs.conda.io/en/latest/miniconda.html) works too.
- Install [git](https://gitforwindows.org/)
## Getting started
```bash
git clone https://github.com/aeon0/botty.git
git clone https://github.com/Hoblirm/botty.git
cd botty
conda env create environment.yml
# Create the conda environment (installs all Python deps + tesserocr with tesseract)
# Windows 10:
conda env create -f environment-win10.yml
# Windows 11:
conda env create -f environment-win11.yml
# Activate
conda activate botty
# Optional pip profile after the conda env exists:
# Windows 10:
python -m pip install -r requirements-win10.txt
# Windows 11:
python -m pip install -r requirements-win11.txt
# Run
python src/main.py
```
`install.bat` detects Windows 10 vs Windows 11 and installs the matching
environment and requirements profiles automatically. Botty runtime detection
lives in `src/utils/os_detect.py`; startup logs show the selected profile and
mouse mode.
### PowerShell users
```powershell
conda init powershell # One time setup
conda activate botty
python src/main.py
```
Important info for Powershell users:
```bash
# for powershell you have to init conda before using any conda commands:
conda init powershell
### No conda? Quick setup
If you don't want to use conda, you can install dependencies with pip, but `tesserocr`
requires the tesseract C library which is easiest to get via conda. See `environment.yml`
for the full dependency list.
## Running with the launcher
A `run_botty.bat` file is provided in the project root. It auto-detects the conda
environment and launches the bot. You can also run manually:
```cmd
conda activate botty
cd C:\path\to\botty
python src\main.py
```
## Tests
@@ -28,7 +65,7 @@ pytest -s -v
# To run a specific test:
pytest test/smoke_test.py
```
To test single files / routines, most files also can be executed seperatly. E.g. running `python src/pickit.py` -> going to d2r window -> throw stuff on the groudn -> press f11, will test the pickit.
To test single files / routines, most files also can be executed separately. E.g. running `python src/pickit.py` -> going to d2r window -> throw stuff on the groudn -> press f11, will test the pickit.
## Adding Items
To add items you can check the **assets/items** folder. Screenshot whatever you want to pick up in the same way (all settings must be as if you ran the bot). Then add the filename to the param.ini [items] section (e.g. if boots_rare.png add boots_rare=1)
+102
View File
@@ -0,0 +1,102 @@
# Adding a New Farming Route
## Quick Start (Scaffolding Tool)
```bash
python3 tools/new_route.py --name my_target --act 1 --location "Black Marsh"
```
This generates `src/run/my_target.py` and prints every line you need to add to `bot.py`, `params.ini`, and `pather.py`. The tool also runs `ruff check` on the generated file.
For a simpler inline version (no ruff check):
```bash
python3 src/utils/new_route.py --name my_target --act 1 --location "Black Marsh"
```
---
## Step-by-Step Manual Guide
### 1. Create the run file: `src/run/my_target.py`
Every run has two methods:
**`approach(start_loc, do_pre_buff)`** — gets the character to the run start location:
- Navigate to the right act's WP via `traverse_nodes_automap()`
- Use `pather.go_to_area("Area Name", "WP")` to take the waypoint
- Return a `Location` constant on success, `False` on failure
**`battle()`** — kills the target and picks items:
- Navigate to the target via recorded path nodes
- Call `char.kill_X()` (add to `IChar` / your char class if missing)
- Call `pickit.pick_up_items(char)`
- Return `(Location, picked_up_bool)` on success, `False` on failure
### 2. Add path node constants to `src/pather.py`
In `class Location`, add string constants for each nav point:
```python
MY_TARGET_SAFE_DIST = "my_target_safe_dist"
```
### 3. Record path nodes in-game
Requires: **D2R game running**, **bot window active**, **game window NOT in fullscreen**
```bash
python3 src/utils/node_recorder.py
```
Enter the run name (e.g. `andariel`) when prompted. A debug visualization window will appear.
#### Recording workflow:
1. **F8** — Record a visual template: click top-left corner of a distinctive UI element or landmark, press F8, then click bottom-right corner, press F8 again. This creates a reusable reference point.
2. **F9** — Record a navigation node at the current mouse cursor position (relative to the templates you recorded).
3. **F10** — Update all recorded nodes with currently visible templates.
4. **F12** — Exit recorder.
Recorded templates go to `log/screenshots/generated/templates/<run_name>/`.
The generated pather code is written to `log/screenshots/generated/pather_generated.py`.
#### Level requirements for boss areas (need campaign progress):
| Boss | Act | Minimum Level | Area |
|------|-----|---------------|------|
| Countess | 1 | 1 | Forgotten Tower |
| Andariel | 1 | 6 | Catacombs Level 4 |
| Duriel | 2 | 15 | Sewers Level 3 |
| Mephisto | 3 | 30 | Durance of Hate Level 3 |
| Baal | 5 | 60 | Throne of Destruction |
You need a character that has progressed through the campaign to access these areas. If you're starting from level 1, you'll need to play through each act to unlock the areas, then record nodes as you go.
### 4. Register the route in `src/bot.py`
Four additions (the scaffold tool prints the exact lines):
1. Import: `from run import ..., MyTarget`
2. `_do_runs` dict entry: `"run_my_target": Config().routes.get("run_my_target"),`
3. Instance: `self._my_target = MyTarget(...)`
4. State machine state + transition + handler method
### 5. Document in `config/params.ini`
Add `; run_my_target` to the routes comment block so users know it exists.
### 6. Enable the route
In your `config/params.ini` (or `config/custom.ini`):
```ini
[routes]
order=run_pindle, run_my_target
```
---
## Stealth Tips for New Routes
- Use `wait(min, max)` for all pauses — the stealth jitter config multiplies these automatically
- Use `stealth_move(x, y)` instead of `mouse.move(x, y)` for clicks in the new run
- Keep path node counts low (3-5 nodes) — more nodes = more predictable pathing patterns
- Vary which areas you teleport through using random sub-paths if the route allows it
+141
View File
@@ -0,0 +1,141 @@
# Auto Skill + Attribute Allocation Plan
## Goal
Add an optional system that automatically assigns:
- skill points
- attribute points
based on:
- active character profile (`blizz_sorc`, `fohdin`, `hammerdin`, etc.)
- current character level
without breaking existing manual setups.
## Scope
- Planning and architecture for Botty repo.
- No forced behavior changes: feature must be opt-in.
## Requirements
1. Determine current level reliably at runtime.
2. Select a build template by character profile.
3. Apply points safely only when unspent points exist.
4. Record every allocation in logs/events for audit/replay.
5. Abort safely on uncertainty (wrong UI state, OCR mismatch, missing templates).
## Current Level Detection Strategy
### Primary path
Use `player_bar.get_experience()` (already used in `game_stats.log_exp`) to derive level from XP table.
### Secondary fallback
Open character panel (`C`) and OCR level/name line directly from upper-left panel region.
### Tertiary fallback
If OCR fails repeatedly:
- keep previous known good level for session,
- do **not** allocate points until confidence is restored.
### Confidence rules
- Require two consistent reads before first allocation in a session.
- Reject impossible jumps (e.g., +5 levels at once).
- Persist `last_known_level` in session stats snapshot.
## Build Template Model
Add config-backed build templates, e.g.:
- `config/auto_builds/blizz_sorc.ini`
- `config/auto_builds/hammerdin.ini`
- `config/auto_builds/fohdin.ini`
Each template defines per-level targets:
- desired skill totals by level milestone
- desired attribute distribution (str/dex/vit/ene)
Example concept:
- Level 1-17: early progression targets
- Level 18-29: mid-game unlock path
- Level 30+: core skill maxing order
## Runtime Flow
1. Enter town and open character/skill UI.
2. Detect level and unspent points.
3. Load template for `Config().char["type"]`.
4. Compute delta between current allocation and target-at-level.
5. Apply points stepwise:
- attributes first (optional toggle),
- skills second.
6. Verify post-apply state.
7. Log allocation summary and persist snapshot.
## Safety Guards
- Only run in town.
- Require stash/vendor windows closed.
- Hard cap per cycle (e.g., max 10 clicks per stat/skill group).
- On mismatch/timeout:
- stop allocation immediately,
- screenshot + structured error event,
- continue bot without crashing.
## Config Additions (Planned)
In `[char]` or new `[auto_build]` section:
- `auto_assign_skills=0/1`
- `auto_assign_attributes=0/1`
- `auto_build_profile=` (defaults to `char.type`)
- `auto_build_check_every_x_games=`
- `auto_build_safe_mode=1` (extra verification)
## Logging / Telemetry
Add structured events:
- `auto_build_check_started`
- `auto_build_level_detected`
- `auto_build_points_detected`
- `auto_build_applied`
- `auto_build_skipped`
- `auto_build_error`
Include:
- profile
- level
- points spent
- before/after snapshots
## UI / Input Dependencies
Need stable template references for:
- character panel level region
- unspent attribute points indicator
- unspent skill points indicator
- individual plus-buttons for stats/skills
## Test Plan
1. Unit tests:
- level-to-target mapping
- delta computation
- guard conditions
2. Integration dry-run mode:
- compute and log planned actions without clicking.
3. Live smoke tests per profile:
- `blizz_sorc`, `hammerdin`, `fohdin`
4. Regression:
- ensure normal runs unaffected with feature disabled.
## Inputs Needed From You
1. Screenshots for each supported class at:
- character panel open,
- skill tree open,
- visible unspent points.
2. Preferred leveling templates:
- exact skill priority order by level range.
- attribute rules (e.g., str to gear breakpoint, then vit).
3. Whether respec-aware logic is needed in v1.
## Rollout Phases
1. Phase 1: Level detection + dry-run planner only.
2. Phase 2: Attribute auto-assign (safer, fewer UI branches).
3. Phase 3: Skill auto-assign with full verification.
4. Phase 4: Expanded profile templates + docs.
## Definition of Done
- Feature is opt-in and stable for `blizz_sorc`, `hammerdin`, `fohdin`.
- Level detection is reliable with fallback behavior.
- No crash on detection/allocation failure.
- Full logs available for every auto-allocation decision.
+74
View File
@@ -0,0 +1,74 @@
# BNIP Guide
This guide explains how to edit `config/default.bnip` safely and predictably.
## What BNIP Does
BNIP rules decide which items Botty keeps.
Each line is a filter expression evaluated against detected item data.
## Rule Shape
Typical rule format:
```text
[Name] == Ring && [Quality] == Rare # [Fcr] >= 10 && [Allres] >= 15
```
- Left side (`[Name]`, `[Type]`, `[Quality]`, etc.) narrows item identity.
- Right side after `#` checks stats/rolls.
## Enable or Disable Rules
- Enabled: line starts with `[...`
- Disabled: line starts with `//`
Example:
```text
//[Name] == Lemrune
[Name] == Pulrune
```
## Safe Editing Workflow
1. Copy an existing nearby rule.
2. Keep your new rule commented out initially (`//`).
3. Enable one new rule at a time.
4. Run a few games and verify behavior before adding more.
## Rule Ordering
BNIP files are easier to maintain when ordered from specific to broad.
- Put strict high-value rules first.
- Put broad catch-all rules later.
- Avoid duplicated broad rules in multiple sections.
## Common Fields You Will Use
- `[Name]`
- `[Type]`
- `[Quality]`
- `[Flag]` (for ethereal/sockets behavior)
- Stat aliases like `[Fcr]`, `[Allres]`, `[Enhanceddefense]`, `[Enhanceddamage]`
## Troubleshooting
If a desired item is not kept:
1. Confirm the rule is enabled (no `//`).
2. Relax one condition at a time.
3. Check for typos in stat aliases.
4. Ensure no local BNIP file in `config/bnip/` is overriding expectations.
If too much junk is kept:
1. Tighten broad rules.
2. Disable catch-all rules first.
3. Add stricter stat thresholds.
## Recommended Local Customization
Keep `config/default.bnip` as the team baseline.
Put personal experiments in separate local `.bnip` files under `config/bnip/` and test there first.
+142
View File
@@ -0,0 +1,142 @@
# Broken Runs - Setup Guide
## Current Status
All 4 boss runs (Countess, Andariel, Mephisto, Baal) have **code structure in place** but require **path recording** before they can run.
| Run | Code | Guards | Path Coords | Templates | Walking Fallback |
|---|---|---|---|---|---|
| Countess | OK | (0,0) check | 0,0 placeholder | Empty dir | Nodes 1000-1004 |
| Andariel | OK | (0,0) check | 0,0 placeholder | Empty dir | Nodes 1010-1012 |
| Mephisto | OK | (0,0) check | 0,0 placeholder | Empty dir | Nodes 1020-1021 |
| Baal | OK | (0,0) check | 0,0 placeholder | Empty dir | Nodes 1030-1031 |
## What's Done
- **Run classes**: `countess.py`, `andariel.py`, `mephisto.py`, `baal.py` - all wired in `bot.py`
- **Kill methods**: `kill_countess()`, `kill_andariel()`, `kill_mephisto()`, `kill_baal()`, `kill_baal_waves()` - implemented in Hammerdin/FoHdin/Warlock
- **Path guards**: All 4 runs check for `(0,0)` paths and refuse to run with a clear error message
- **Walking fallback**: `pather.py` has node definitions (1000-1031) and path routes for walking fallback
- **Template dirs**: `assets/templates/countess/`, `andariel/`, `mephisto/`, `baal/` created (empty)
## What's Needed to Enable Each Run
### Option A: Teleport (faster, recommended)
Record path coordinates in `config/game.ini` using `node_recorder.py`:
```bash
cd C:\Users\alex\Downloads\my-botty
python src/utils/node_recorder.py
```
Then follow the on-screen instructions:
- **F8**: Capture template ROI (top-left, then bottom-right) -> saves PNG
- **F9**: Record node position at cursor -> generates path coordinates
- **F10**: Update all nodes with visible templates
### Option B: Walking (slower, needs templates + nodes)
Same as above, but also requires creating template PNGs for each landmark.
---
## 1. COUNTESS (Act 1 Forgotten Tower)
**game.ini keys to record (5):**
| Key | What to Record |
|---|---|
| `a1_tower_level2_enter` | First click inside tower after entering from Black Marsh |
| `a1_tower_level3_enter` | Top of stairs from L2 to L3 |
| `a1_tower_level4_enter` | Top of stairs from L3 to L4 |
| `a1_tower_level5_enter` | Top of stairs from L4 to L5 |
| `a1_countess_safe_dist` | Position near Countess at safe hammer range |
**Templates for walking fallback (in `assets/templates/countess/`):**
| Template File | Template Key | Purpose |
|---|---|---|
| `countess_tower_l2.png` | COUNTESS_TOWER_L2 | Tower L2 entrance |
| `countess_tower_l3.png` | COUNTESS_TOWER_L3 | Stairs L2->L3 |
| `countess_tower_l4.png` | COUNTESS_TOWER_L4 | Stairs L3->L4 |
| `countess_tower_l5.png` | COUNTESS_TOWER_L5 | Stairs L4->L5 |
| `countess_boss.png` | COUNTESS_BOSS | Countess boss area |
**Nodes in pather.py:** 1000-1004 (placeholder coords `(0,0)` - update after recording)
---
## 2. ANDARIEL (Act 1 Catacombs)
**game.ini keys to record (3):**
| Key | What to Record |
|---|---|
| `a1_andy_level3_enter` | Catacombs L3 entrance (from L2) |
| `a1_andy_level4_enter` | Catacombs L4 entrance (from L3) |
| `a1_andy_safe_dist` | Position near Andariel's cage at safe hammer range |
**Templates for walking fallback (in `assets/templates/andariel/`):**
| Template File | Template Key | Purpose |
|---|---|---|
| `andy_l3_stairs.png` | ANDY_L3_STAIRS | Stairs from L2 to L3 |
| `andy_l4_stairs.png` | ANDY_L4_STAIRS | Stairs from L3 to L4 |
| `andy_cage.png` | ANDY_CAGE | Andariel cage area |
**Nodes in pather.py:** 1010-1012 (placeholder coords `(0,0)` - update after recording)
---
## 3. MEPHISTO (Act 3 Durance of Hate)
**game.ini keys to record (2):**
| Key | What to Record |
|---|---|
| `a3_meph_level3_enter` | Durance of Hate L3 entrance (from L2) |
| `a3_meph_safe_dist` | Position near Mephisto's cage at safe hammer range |
**Templates for walking fallback (in `assets/templates/mephisto/`):**
| Template File | Template Key | Purpose |
|---|---|---|
| `meph_l3_stairs.png` | MEPH_L3_STAIRS | Stairs from L2 to L3 |
| `meph_cage.png` | MEPH_CAGE | Mephisto cage area |
**Nodes in pather.py:** 1020-1021 (placeholder coords `(0,0)` - update after recording)
---
## 4. BAAL (Act 5 Throne of Destruction)
**game.ini keys to record (2):**
| Key | What to Record |
|---|---|
| `a5_baal_throne_entry` | Throne of Destruction entrance (from Worldstone Keep L2) |
| `a5_baal_safe_dist` | Position near Baal at safe hammer range (after wave clear) |
**Templates for walking fallback (in `assets/templates/baal/`):**
| Template File | Template Key | Purpose |
|---|---|---|
| `baal_throne_entry.png` | BAAL_THRONE_ENTRY | Entrance to Throne area |
| `baal_arena.png` | BAAL_ARENA | Baal arena / boss position |
**Nodes in pather.py:** 1030-1031 (placeholder coords `(0,0)` - update after recording)
---
## Quick Start: Record Paths for One Run
1. Launch D2R and navigate to the boss area
2. Run `python src/utils/node_recorder.py` from the botty directory
3. Enter the run name when prompted (e.g., `countess`)
4. Position your cursor at the target location in-game
5. Press F8 to capture a template, F9 to record a node
6. Press F10 to update all nodes with visible templates
7. Copy the generated path coordinates to `config/game.ini`
8. Copy the generated template PNGs to `assets/templates/<run>/`
9. Update `pather.py` node definitions with real coordinates from the recorder output
+539
View File
@@ -0,0 +1,539 @@
# Codex Fix Analysis
Source files read:
- `docs/fix_plan.md`
- `src/run/nihlathak.py`
- `src/town/town_manager.py`
- `src/inventory/vendor.py`
- `src/char/i_char.py` (`src/char.py` does not exist in this repo; CTA is implemented here)
- `config/game.ini`
- Supporting files needed to trace the failures: `src/town/a1.py`, `src/town/a5.py`, `src/town/a4.py`, `src/npc_manager.py`, `src/pather.py`, and the CTA key section in `config/params.ini`
## Priority 1: Nihlathak approach fails
### Finding
The Nihlathak route has three brittle points:
1. `approach()` returns success immediately after clicking the waypoint and never verifies that the Halls of Pain actually loaded.
2. Level 1 layout detection has no fallback if `NI1_A`, `NI1_B`, or `NI1_C` is stale.
3. `traverse_nodes_fixed()` always returns `True`, so a bad static path in `config/game.ini` cannot be detected until the stairs click times out.
The `config/game.ini` Nihlathak path keys are present:
```ini
ni1_a=871,472, 1205,600, 1162,600, 1169,584, 1169,584, 1232,213, 1221,237, 1164,228, 1145,572, 1146,547, 1223,185
ni1_b=23,187, 23,187, 23,187, 23,187, 12,192, 10,192, 10,190, 123,70, 378,120
ni1_c=118,500, 158,602, 187,577, 217,563, 184,551, 70,413, 127,240, 154,493, 197,504, 218,545, 83,246, 45,526, 300,380
```
So the immediate code fix is not "add missing keys"; it is to verify waypoint/area entry and reduce the failure blast radius when stale templates or coordinates are encountered.
### Exact code that needs to change
`src/run/nihlathak.py`:
```python
wait(0.4)
if waypoint.use_wp("Halls of Pain"): # use Halls of Pain Waypoint (5th in A5)
return Location.A5_NIHLATHAK_START
return False
```
```python
template_match = template_finder.search_and_wait(["NI1_A", "NI1_B", "NI1_C"], threshold=0.65, timeout=20)
if not template_match.valid:
return False
```
```python
self._pather.traverse_nodes_fixed(template_match.name.lower(), self._char)
```
### Proposed fix
Replace the waypoint block with a verified load:
```python
wait(0.4)
if not waypoint.use_wp("Halls of Pain"):
return False
if not template_finder.search_and_wait(["NI1_A", "NI1_B", "NI1_C"], threshold=0.55, timeout=8).valid:
Logger.error("Nihlathak approach: waypoint click did not land in Halls of Pain")
return False
return Location.A5_NIHLATHAK_START
```
Replace layout detection and static path traversal with a lower-threshold retry and an explicit path result check:
```python
template_match = template_finder.search_and_wait(["NI1_A", "NI1_B", "NI1_C"], threshold=0.65, timeout=20)
if not template_match.valid:
Logger.warning("Nihlathak: strict NI1 layout detection failed, retrying with grayscale/lower threshold")
template_match = template_finder.search_and_wait(
["NI1_A", "NI1_B", "NI1_C"],
threshold=0.55,
best_match=True,
timeout=6,
use_grayscale=True,
)
if not template_match.valid:
return False
```
```python
if not self._pather.traverse_nodes_fixed(template_match.name.lower(), self._char):
Logger.error(f"Nihlathak: failed static route {template_match.name.lower()}")
return False
```
If `search_and_wait()` does not support `use_grayscale` in this repo version, use this compatible form instead:
```python
if not template_match.valid:
start = time.time()
while time.time() - start < 6:
template_match = template_finder.search(
["NI1_A", "NI1_B", "NI1_C"],
grab(),
threshold=0.55,
best_match=True,
use_grayscale=True,
)
if template_match.valid:
break
wait(0.2)
```
That compatible form also needs imports:
```python
import time
from screen import grab, convert_abs_to_monitor
```
### Why it will work
This turns the approach from "clicked the waypoint, assume success" into "clicked the waypoint, confirm an NI1 layout is visible." If the waypoint interaction fails or lands somewhere unexpected, the run fails immediately instead of burning 600+ seconds.
The lower-threshold/grayscale retry handles the likely stale-template case without permanently weakening the first pass. The strict threshold still wins when templates are good; the fallback only runs when the current behavior would fail.
The static route check makes future changes safer. `traverse_nodes_fixed()` currently returns `True`, but guarding the call is still correct because it protects this runner if path traversal later gains real validation.
Fresh templates and re-recorded `ni1_*` coordinates are still required if the fallback logs low-confidence matches or reaches the wrong stairs side. The code fix limits total game loss and gives a useful failure point.
## Priority 2: Vendor trade button not found
### Finding
The failure is not in `src/inventory/vendor.py`; that file buys items after the vendor panel is already open. The failing log comes from `src/npc_manager.py`:
```python
Logger.error(f"Could not find {action_btn_key} btn. Should not happen! Continue...")
```
The current town code also returns a vendor location even if pressing the trade button failed. For A5 Malah:
```python
def open_trade_menu(self, curr_loc: Location) -> Location | bool:
if not self._pather.traverse_nodes((curr_loc, Location.A5_MALAH), self._char, force_move=True): return False
if open_npc_menu(Npc.MALAH):
press_npc_btn(Npc.MALAH, "trade")
return Location.A5_MALAH
return False
```
And `press_npc_btn()` does not return `True` on success:
```python
if res.valid:
mouse.move(*res.center_monitor, randomize=3, delay_factor=[1.0, 1.5])
wait(0.2, 0.4)
mouse.click(button="left")
wait(0.04, 0.08)
center_mouse()
else:
Logger.error(f"Could not find {action_btn_key} btn. Should not happen! Continue...")
keyboard.send("esc")
```
The template threshold is also hard-coded very high for white and blue text:
```python
filtered_inp_w, 0.85, roi=Config().ui_roi["cut_skill_bar"]
```
### Exact code that needs to change
`src/npc_manager.py`, `press_npc_btn()` needs to return a boolean and use a retry/fallback. A5/A1/A4 trade functions need to check that boolean or verify the vendor panel.
### Proposed fix
Replace `press_npc_btn()` with:
```python
def press_npc_btn(npc_key: Npc, action_btn_key: str) -> bool:
global npcs
for threshold in (0.85, 0.78):
img = grab()
img = escape_dialogue(img)
_, filtered_inp_w = color_filter(img, Config().colors["white"])
res = template_finder.search(
npcs[npc_key]["action_btns"][action_btn_key]["white"],
filtered_inp_w,
threshold,
roi=Config().ui_roi["cut_skill_bar"],
)
if not res.valid and "blue" in npcs[npc_key]["action_btns"][action_btn_key]:
_, filtered_inp_b = color_filter(img, Config().colors["blue"])
res = template_finder.search(
npcs[npc_key]["action_btns"][action_btn_key]["blue"],
filtered_inp_b,
threshold,
roi=Config().ui_roi["cut_skill_bar"],
)
if not res.valid:
res = template_finder.search(
npcs[npc_key]["action_btns"][action_btn_key]["white"],
img,
threshold,
roi=Config().ui_roi["cut_skill_bar"],
use_grayscale=True,
)
if res.valid:
mouse.move(*res.center_monitor, randomize=3, delay_factor=[1.0, 1.5])
wait(0.2, 0.4)
mouse.click(button="left")
wait(0.2, 0.3)
center_mouse()
return True
if "red" in npcs[npc_key]["action_btns"][action_btn_key]:
img = grab()
_, filtered_inp_r = color_filter(img, Config().colors["red"])
res = template_finder.search(
npcs[npc_key]["action_btns"][action_btn_key]["red"],
filtered_inp_r,
0.78,
roi=Config().ui_roi["cut_skill_bar"],
)
if res.valid:
Logger.warning(f"Cannot afford {action_btn_key} (red button detected). Skipping...")
keyboard.send("esc")
wait(0.3)
return False
Logger.error(f"Could not find {action_btn_key} btn. Should not happen! Continue...")
keyboard.send("esc")
return False
```
Then change A5 Malah trade from:
```python
if open_npc_menu(Npc.MALAH):
press_npc_btn(Npc.MALAH, "trade")
return Location.A5_MALAH
return False
```
to:
```python
if open_npc_menu(Npc.MALAH):
if press_npc_btn(Npc.MALAH, "trade") and is_visible(ScreenObjects.GoldBtnVendor):
return Location.A5_MALAH
return False
```
Make the same pattern in `src/town/a1.py`:
```python
if open_npc_menu(Npc.AKARA):
if press_npc_btn(Npc.AKARA, "trade") and is_visible(ScreenObjects.GoldBtnVendor):
return Location.A1_AKARA
return False
```
and in `src/town/a4.py`:
```python
if open_npc_menu(Npc.JAMELLA):
if press_npc_btn(Npc.JAMELLA, "trade") and is_visible(ScreenObjects.GoldBtnVendor):
return Location.A4_JAMELLA
return False
```
### Why it will work
The bot currently proceeds as if trade opened even when the button was not clicked. Returning `False` stops `TownManager.buy_consumables()` at the correct point:
```python
new_loc = self._acts[curr_act].open_trade_menu(curr_loc)
if not (new_loc and common.wait_for_left_inventory()): return False, items
```
The fallback search keeps the current exact template behavior first, then retries with a slightly lower threshold and grayscale. That covers text color/anti-aliasing differences without making every match permissive.
Verifying `ScreenObjects.GoldBtnVendor` makes the action result state-based. Even if the template click returns true, the caller only continues when the vendor panel is actually open.
Fresh `TRADE` / `TRADE_BLUE` templates are still recommended, but this code fix prevents false success and reduces sensitivity to minor UI rendering differences.
## Priority 3: Stash detection fails
### Finding
The stash failures are in act-specific methods, not in `TownManager.stash()` itself. A1 and A5 use default `IChar.select_by_template()` threshold `0.68`:
```python
if not self._char.select_by_template(["A1_TOWN_0"], stash_is_open_func):
return False
```
```python
if not self._char.select_by_template(["A5_STASH", "A5_STASH_2"], stash_is_open_func, telekinesis=True):
return False
```
The common selector only closes the waypoint menu for A5 stash templates:
```python
if type(template_type) == list and "A5_STASH" in template_type:
# sometimes waypoint is opened and stash not found because of that, check for that
if is_visible(ScreenObjects.WaypointLabel):
keyboard.send("esc")
```
So A1 stash can fail when a waypoint/dialog is left open, and both A1/A5 have no lower-threshold retry.
### Exact code that needs to change
`src/town/a1.py`:
```python
if not self._char.select_by_template(["A1_TOWN_0"], stash_is_open_func):
return False
```
`src/town/a5.py`:
```python
if not self._char.select_by_template(["A5_STASH", "A5_STASH_2"], stash_is_open_func, telekinesis=True):
return False
```
`src/char/i_char.py`:
```python
if type(template_type) == list and "A5_STASH" in template_type:
# sometimes waypoint is opened and stash not found because of that, check for that
if is_visible(ScreenObjects.WaypointLabel):
keyboard.send("esc")
```
### Proposed fix
Change the selector guard in `src/char/i_char.py` to handle all stash templates:
```python
templates = template_type if isinstance(template_type, list) else [template_type]
if any(template in ["A1_TOWN_0", "A5_STASH", "A5_STASH_2"] for template in templates):
# sometimes waypoint is opened and stash not found because of that, check for that
if is_visible(ScreenObjects.WaypointLabel):
keyboard.send("esc")
wait(0.2, 0.3)
```
Change A1 stash to retry lower after the default threshold fails:
```python
if not self._char.select_by_template(["A1_TOWN_0"], stash_is_open_func, threshold=0.68, timeout=4.0):
Logger.warning("A1 stash: default threshold failed, retrying with lower threshold")
if not self._char.select_by_template(["A1_TOWN_0"], stash_is_open_func, threshold=0.58, timeout=4.0):
return False
```
Change A5 stash similarly:
```python
if not self._char.select_by_template(["A5_STASH", "A5_STASH_2"], stash_is_open_func, threshold=0.68, timeout=4.0, telekinesis=True):
Logger.warning("A5 stash: default threshold failed, retrying with lower threshold")
if not self._char.select_by_template(["A5_STASH", "A5_STASH_2"], stash_is_open_func, threshold=0.58, timeout=4.0, telekinesis=True):
return False
```
### Why it will work
The default threshold remains unchanged for normal cases. The lower threshold only runs after a specific stash attempt fails, which narrows the risk of false-positive clicks.
Closing the waypoint menu for A1 and A5 prevents stale UI overlays from blocking the stash templates. This directly addresses the logged sequence where town/stash templates are not found after other town interactions.
The success function already checks for stash/inventory gold buttons:
```python
found = is_visible(ScreenObjects.GoldBtnInventory, img)
found |= is_visible(ScreenObjects.GoldBtnStash, img)
```
That means a lower-threshold click must still produce the actual stash UI to count as success.
Fresh `A1_TOWN_0`, `A5_STASH`, and `A5_STASH_2` templates should still be captured if logs continue to show low match confidence. The code change makes the current templates less brittle and prevents open UI overlays from causing avoidable failures.
## Priority 4: CTA weapon switch fails
### Finding
The CTA code is in `src/char/i_char.py`. It depends on `Config().char["weapon_switch"]`, which comes from `config/params.ini`, not `config/game.ini`:
```ini
weapon_switch=w
battle_orders=f6
battle_command=f5
```
The current CTA routine has two reliability problems:
1. It invalidates active-skill cache implicitly by switching weapons but does not reset `_active_skill`.
2. It verifies the switch back by comparing a screenshot of the previous right-skill icon. That can fail when the same skill exists on both swaps, when the icon is visually similar, or when the UI updates slightly late.
Current code:
```python
while time.time() - start < 4:
keyboard.send(Config().char["weapon_switch"])
wait(0.4, 0.45)
keyboard.send(Config().char["battle_command"])
wait(0.2, 0.3)
if skills.is_right_skill_selected(["BC", "BO"]):
switch_sucess = True
break
else:
Logger.warning("Failed to find Battle Command, swapping weapons again.")
```
```python
while time.time() - start < 4:
keyboard.send(Config().char["weapon_switch"])
wait(0.4, 0.45)
skill_after = cut_roi(grab(), Config().ui_roi["skill_right"])
_, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED))
if max_val > 0.8:
switch_sucess = True
break
else:
Logger.warning("Failed to switch weapon, try again")
wait(0.5)
return switch_sucess
```
### Exact code that needs to change
Add a helper inside `IChar` and use it whenever the weapon switch key is sent in `_pre_buff_cta()`.
### Proposed fix
Add this method to `IChar`:
```python
def _weapon_switch(self):
keyboard.send(Config().char["weapon_switch"])
self._set_active_skill("left", "")
self._set_active_skill("right", "")
wait(0.55, 0.65)
```
Change the first CTA-side check from:
```python
if skills.is_right_skill_selected(["BC", "BO"]):
keyboard.send(Config().char["weapon_switch"])
wait(0.4, 0.45)
```
to:
```python
if skills.is_right_skill_selected(["BC", "BO"]):
self._weapon_switch()
```
Change the switch-to-CTA loop from:
```python
keyboard.send(Config().char["weapon_switch"])
wait(0.4, 0.45)
keyboard.send(Config().char["battle_command"])
```
to:
```python
self._weapon_switch()
keyboard.send(Config().char["battle_command"])
```
Change the switch-back loop from:
```python
keyboard.send(Config().char["weapon_switch"])
wait(0.4, 0.45)
skill_after = cut_roi(grab(), Config().ui_roi["skill_right"])
_, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED))
if max_val > 0.8:
switch_sucess = True
break
else:
Logger.warning("Failed to switch weapon, try again")
wait(0.5)
```
to:
```python
self._weapon_switch()
if not skills.is_right_skill_selected(["BC", "BO"]):
switch_sucess = True
break
skill_after = cut_roi(grab(), Config().ui_roi["skill_right"])
_, max_val, _, _ = cv2.minMaxLoc(cv2.matchTemplate(skill_after, skill_before, cv2.TM_CCOEFF_NORMED))
if max_val > 0.8:
switch_sucess = True
break
Logger.warning("Failed to switch weapon, try again")
wait(0.5)
```
Also fix the spelling while touching this code:
```python
switch_success = False
```
instead of:
```python
switch_sucess = False
```
### Why it will work
Resetting `_active_skill` after weapon swap prevents `_select_skill()` from skipping a hotkey press because it thinks the old right skill is still selected. Weapon swap changes the available skill bar, so the cache is no longer trustworthy.
The longer wait gives D2R more time to update the skill icon and weapon state before validation. The current 0.4 second delay is close to the UI transition timing and is likely why the failure is intermittent.
The switch-back validation now accepts the most direct state: the right skill is no longer Battle Command/Battle Orders. The old image comparison remains as a fallback, so this still handles cases where BC/BO detection briefly lags.
The configured key should be checked in `config/params.ini`, not `config/game.ini`. If the user's actual D2R weapon switch key is Caps Lock, then this line must change:
```ini
weapon_switch=w
```
to:
```ini
weapon_switch=capslock
```
Only do that if the in-game key binding is actually Caps Lock; otherwise leave it as `w`.
+57
View File
@@ -0,0 +1,57 @@
# d2jsp Semi-Automated Scraping Guide
Due to Cloudflare's aggressive bot protection, fully automated scraping is currently restricted. This project uses a **Semi-Automated (Offline) Workflow** that leverages your authenticated browser session to safely collect market data.
## Prerequisites
1. **Python Dependencies:** Ensure `keyboard` and `requests` are installed (included in `requirements.txt`).
2. **Browser:** Google Chrome is recommended.
3. **Authentication:** Be logged into [forums.d2jsp.org](https://forums.d2jsp.org/) in your browser.
---
## The Workflow
### 1. Initial Directory Setup
Before running the automation, you must set the default "Save As" path in your browser:
1. Open any topic on d2jsp.
2. Press `Ctrl + S`.
3. Navigate to your bot folder: `data/d2jsp_pages/`.
4. Save the file. Your browser will now remember this location as the default.
### 2. Collect Topic URLs
If you don't have a fresh list of URLs, run the collector on a saved forum listing page:
```powershell
python tools/d2jsp_topic_collector.py --offline-dir data/d2jsp_pages --out data/d2jsp_topic_urls.txt
```
### 3. Run Browser Automation
This script will open the first 20 topics from your list, wait for Cloudflare to pass, and simulate the save command.
```powershell
python tools/browser_auto_save.py
```
**Important:**
* Keep your browser as the active window.
* Do not move the mouse or type while the script is running.
* It will automatically `Ctrl + S` -> `Enter` -> `Ctrl + W` for each tab.
### 4. Generate Price Estimates
Once the HTML files are saved in `data/d2jsp_pages/`, run the offline scraper to update your bot's configuration:
```powershell
python tools/fg_market_scraper.py --ladder-start-date 2026-05-20 --offline-dir data/d2jsp_pages --out config/fg_daily_estimates.json
```
---
## Troubleshooting
### Cloudflare "Just a Moment" Loop
If the automation is too fast and hits a "Just a Moment" screen that doesn't resolve:
1. Increase the `time.sleep(8)` value in `tools/browser_auto_save.py`.
2. Manually solve one challenge in the browser to "warm up" the IP clearance.
### Files Not Saving to Correct Folder
If files are saving to your "Downloads" folder instead of `data/d2jsp_pages`, the browser's default path was reset. Repeat **Step 1** to fix it.
### Date Parsing Errors
If the scraper reports 0 topics scanned or dates are missing, ensure your browser language isn't translating the page, as the scraper expects English month names (Jan, Feb, Mar, etc.).
+18
View File
@@ -0,0 +1,18 @@
# D2R Window and Input Troubleshooting
Botty expects Diablo II: Resurrected to expose a stable 1280x720 client area. If
the live D2R window reports a slightly different client size, template matching
can still succeed while mouse clicks land at the wrong monitor coordinate.
At startup and before creating a game, Botty now resizes and positions the D2R
client area to the configured `config/game.ini` dimensions. The expected size is
`1280x720`, matching `assets/d2r_settings.json` and the template assets.
If a UI click is detected but the cursor does not visibly move, the native input
layer first tries `SendInput` and then verifies the cursor position. When
`SendInput` misses the target coordinate, Botty falls back to `SetCursorPos` and
logs the fallback.
If movement or clicks still do not reach D2R, check that D2R and the bot process
are running at the same privilege level. Windows can block input from a
non-admin process into an elevated game window.
+41
View File
@@ -0,0 +1,41 @@
# Diablo Waypoint Recovery Notes
Date: 2026-06-07
## What changed
- Diablo is now first in the configured run order, followed by Pindle only.
- After using the Act 4 River of Flame waypoint, the Diablo approach now closes the waypoint panel, resets stale health-manager panel detections, and verifies that River of Flame templates are visible.
- If the first River of Flame waypoint attempt leaves the bot in town, the approach retries from the Act 4 town start before failing the run.
- River of Flame and Pentagram traversal now include template-based verification before continuing into Chaos Sanctuary logic.
- Diablo now has a reusable `_search_and_log()` wrapper for template searches that can report match confidence during route debugging.
- Health manager reset now clears `_count_panel_detects` so detections from a previous game do not immediately chicken the next game.
- `config/fg_daily_estimates.json` was regenerated with the improved offline estimator output, including skipped-topic counts and trimmed price statistics.
## Why
Same-act waypoint use can leave the waypoint panel open without a loading screen. That stale panel could combine with the CTA weapon-swap panel during pre-buffing and trigger the health manager panel-detection chicken path. The bot could also silently remain in town after a missed waypoint click and continue as if it had reached River of Flame.
The new checks make Diablo startup state-based: the bot confirms River of Flame and Pentagram markers before continuing. Failed waypoint transitions are retried once, then reported as approach failures instead of drifting into later pathing.
## Validation
Run a focused route order:
```ini
order=run_diablo, run_pindle
```
Expected log behavior:
- `_verify_in_rof: confirmed in River of Flame ...` after the Act 4 waypoint.
- `CS: Calibrated at PENTAGRAM` after Pentagram traversal.
- No immediate chicken caused by the waypoint panel plus CTA weapon-swap panel sequence.
If River of Flame or Pentagram verification fails repeatedly, refresh the affected templates:
- `DIABLO_ROF_WP_0`
- `DIABLO_ROF_WP_1`
- `DIABLO_ENTRANCE_50` through `DIABLO_ENTRANCE_55`
- `DIA_NEW_PENT_TP`
- `DIA_NEW_PENT_0` through `DIA_NEW_PENT_2`
+44
View File
@@ -0,0 +1,44 @@
# FG Market Scraper (Day 1-14)
This tool estimates FG prices per ladder day by scraping public trade topics from:
- https://forums.d2jsp.org/forum.php?f=271
## Script
- `tools/fg_market_scraper.py`
## What it does
1. Scans forum listing pages for topic links.
2. Fetches topic pages (rate-limited).
3. Detects known item/rune keywords.
4. Extracts `fg` prices from post text.
5. Buckets prices by ladder day index (Day 1..Day N).
6. Writes median estimates + sample counts.
## Usage
From repo root:
```powershell
python tools/fg_market_scraper.py --ladder-start-date 2026-05-23 --days 14
```
Output:
- `config/fg_daily_estimates.json`
## Tuning
- `--max-forum-pages`: listing pages to scan (default 40)
- `--max-topics`: hard cap on fetched topics (default 800)
- `--delay-s`: delay between requests (default 0.35s)
Example heavier run:
```powershell
python tools/fg_market_scraper.py --ladder-start-date 2026-05-23 --days 14 --max-forum-pages 120 --max-topics 2400 --delay-s 0.5
```
## Notes
- This is a heuristic estimator, not a full market engine.
- Accuracy depends on post format quality and keyword matches.
- Keep request rate polite to avoid stressing the forum.
- Some environments/IPs will receive HTTP `403` from d2jsp. In that case use:
- `config/fg_day_estimates.json` (Day 1-14 estimated multiplier model from Day 3 snapshot),
- and update `config/fg_prices.json` manually from your current market sample.
+91
View File
@@ -0,0 +1,91 @@
# Botty Fix Plan — 2026-06-05
## Symptoms (from today's logs)
- 4 sessions today: 31 + 1 + 9 + 6 games = 47 games total
- 51 deaths across all sessions
- 0 valuable items found (only charms, jewels, gold)
- Nihlathak run fails every time, ending entire games
- Vendor trade button never found (can't buy potions)
- Stash never opens (can't stash items)
- CTA weapon switch fails occasionally
---
## Priority 1: Nihlathak approach fails (game-ending)
**Error:** `Approach failed for run_nihlathak` — ends game after 600-650s
**Impact:** 3+ games ended per session
**Root cause:** Teleport activation times out, bot can't reach Nihlathak
**Fix:**
- Check `config/game.ini` for Nihlathak coordinates (`a5_nihlathak_*`)
- Likely missing or stale path nodes
- Re-record path with `node_recorder.py`
- May need fresh template images for Nihlathak area
**Files to check:**
- `config/game.ini` (Nihlathak section)
- `src/run/nihlathak.py`
- `assets/templates/nihlathak/`
---
## Priority 2: Vendor trade button not found
**Error:** `Could not find trade btn. Should not happen!`
**Impact:** Can't buy potions every town return
**Root cause:** Trade button template outdated or offset wrong for Hell difficulty UI
**Fix:**
- Take fresh screenshot of vendor trade window
- Update trade button template in `assets/templates/`
- Check if trade button position shifted between Normal/Nightmare/Hell
**Files to check:**
- `assets/templates/` (trade button template)
- `src/` (vendor interaction code — search for "trade btn")
---
## Priority 3: Stash detection fails
**Error:** `select_by_template: could not find ['A1_TOWN_0']` then `['A5_STASH', 'A5_STASH_2']`
**Impact:** Can't stash items, inventory fills with junk
**Root cause:** Stash template confidence threshold too high or template outdated
**Fix:**
- Take fresh screenshot of stash UI in Hell difficulty
- Update A1_TOWN_0, A5_STASH, A5_STASH_2 templates
- Consider lowering confidence threshold (currently 0.68)
**Files to check:**
- `assets/templates/a1_town/`
- `assets/templates/a5_stash/`
- Template matching threshold in code
---
## Priority 4: CTA weapon switch fails
**Error:** `_pre_buff_cta: switch to CTA slot failed — retrying`
**Impact:** Occasional, bot retries
**Root cause:** Weapon switch key or template unreliable
**Fix:**
- Verify weapon switch key binding (capslock per memory)
- Check CTA slot detection template
- May need more robust retry logic
**Files to check:**
- `src/` (search for `_pre_buff_cta`)
- `config/` (weapon_switch key)
---
## Approach
All four issues are template/coordinate problems — stale screenshots or wrong positions.
Fix pattern: screenshot current game UI at those locations, update templates.
Do in order: Nihlathak > vendor > stash > CTA.
+360
View File
@@ -0,0 +1,360 @@
# Hermes Takeover Guide
Practical handoff for continuing Botty development. Read every section before touching code — the trap patterns section alone will save you hours.
---
## 1) Working model
- Treat Botty as a **state machine app** with side effects across D2R UI automation, OCR parsing, inventory/sell/stash routines, and messaging (Discord/webhooks).
- Prefer **small, production-safe patches** over broad refactors.
- Prioritize runtime stability: avoid failing full runs because one subsystem is flaky.
- **Always reproduce from logs first** — most bugs leave a clear trail.
---
## 2) Daily workflow
1. Get fresh logs:
- `log/log.txt`
- `log/stats/events_*.jsonl`
- `log/stats/stats_*.log`
2. Confirm failure path in code with `rg`.
3. Patch narrowly with targeted edits.
4. Validate:
- `python -m compileall <changed files>`
- targeted pytest where available
5. Commit with a clear, single-purpose message.
---
## 3) Core debug commands
From repo root (PowerShell):
```powershell
# Tail errors/warnings
rg -n "ERROR|WARNING|Failed|chicken|exception" log/log.txt | tail -50
# Last 200 log lines
Get-Content log/log.txt | Select-Object -Last 200
# Search source code
rg -n "<keyword>" src/
# Compile check after edits
python -m compileall src/health_manager.py src/item/pickit.py
# Config smoke test (confirm a key's resolved value)
python -c "import sys; sys.path.insert(0,'src'); from config import Config; print(Config().char['show_belt'])"
# Run test suite
python -m pytest -q test/
```
Use `rg` first. It's the fastest way to localize faults.
---
## 4) High-risk areas
### 4.1 Repair/vendor flow
- A5 Larzuk detection is noisy and can fail.
- Current behavior: A5 normal flow → Larzuk direct template click → Act 4 Halbu fallback.
- `repair_npc=a4_halbu` config key skips Larzuk entirely.
Files: `src/town/a5.py`, `src/town/town_manager.py`, `src/bot.py`
### 4.2 Discord messaging
Known issue: `'NoneType' object has no attribute 'to_dict'`
Current fix: `src/messages/discord_embeds.py` `_send_embed` only passes `file=` when attachment exists. Plain text fallback on embed failure.
Config: `config/params.ini``[discord_events]`
### 4.3 Selling safety
Current protections:
- Sell logs include item names (not coordinates only).
- `protect_shields_from_sell=1` blocks selling items whose detected name includes "shield".
Files: `src/inventory/personal.py`, `config/params.ini`, `src/config.py`
### 4.4 XP logging
Two separate concerns:
1. OCR extraction: `src/ui/player_bar.py` — parser handles `I/l/|→1`, `O/o→0`.
2. XP status projection math: `src/game_stats.py` `_create_msg()` — zero denominators guarded.
### 4.5 HealthManager (rejuv / chicken logic) ⚠️
The most subtle source of false positives. See Section 10.2 for full details.
Key thresholds (params.ini `[char]`):
- `take_rejuv_potion_health = 0.80` — drink rejuv if HP ≤ 80%
- `take_rejuv_potion_mana = 0.55` — drink rejuv if mana ≤ 55%
- `chicken = 0.50` — flee if HP ≤ 50%
The "two juvs in 8s → chicken" check **must also verify HP was the trigger**, not just mana. Hammerdins burn mana fast; mana can legitimately trigger back-to-back rejuvs at full HP.
### 4.6 PickIt gold loop ⚠️
`_yoink_item()` **always returns `PickedUpResult.PickedUp`** regardless of actual success (line 129). This means pickup failures are only detectable through `_pick_up_item`'s same-ID or same-UID checks — and different gold pile amounts produce different IDs, bypassing those checks entirely.
Fix in place: `pick_up_items()` now blacklists `item.ID` in `_cached_pickit_items` on `PickedUpFailed`. Do not remove this case.
---
## 5) State machine (bot.py)
States: `initialization → hero_selection → town → [run state] → town → ...`
Full state list: `initialization`, `hero_selection`, `town`, `level`, `pindle`, `shenk`, `trav`, `nihlathak`, `arcane`, `diablo`, `vizier`, `baal`, `mephisto`, `andariel`, `countess`
Key transitions:
| Trigger | Source | Dest | Handler |
|---|---|---|---|
| `init` | initialization | initialization | `on_init` |
| `select_character` | initialization | hero_selection | `on_select_character` |
| `start_from_town` | initialization/hero_selection | town | `on_start_from_town` |
| `maintenance` | town | town | `on_maintenance` |
| `run_pindle` | town | pindle | `on_run_pindle` |
| `run_arcane` | town | arcane | `on_run_arcane` |
| `end_run` | any run state | town | `on_end_run` |
| `end_game` | town/any run state | initialization | `on_end_game` |
**`on_maintenance` guard**: If `_curr_loc` is None (e.g. after a chicken recovery), defaults to `A1_TOWN_START` with a warning log. Do not remove this guard.
**`end_game` vs `end_run`**: When TP charges run out, trigger `end_game` so the bot restarts and restocks in the next game — not `end_run` which tries to TP back and loops.
---
## 6) Config system
Priority order (highest first):
```
custom.ini > params.ini > game.ini > shop.ini > transmute.ini
```
`custom.ini` is gitignored — user-only overrides. Never edit `game.ini` for user settings.
### Key detection flow
On every `Config()` instantiation:
1. Reads `Saved Games/Diablo II Resurrected/<charname>.keyo` binary file.
2. Parses slot-to-key mapping (`CHAR_BINDING_SLOTS` in `key_detector.py`).
3. Slot 41 = `show_belt`, slot 36 = `stand_still`, slot 44 = `weapon_switch`, etc.
4. Compares each detected key against params.ini value.
5. If they match → use detected key silently.
6. If they differ → logs `"Keeping configured key binding for X: 'params_val' (detected 'keyo_val')"` and keeps the params.ini value.
**What "Keeping..." means**: params.ini disagrees with the in-game binding. Usually indicates a config typo or stale params.ini. If you see `Keeping show_belt: 'k' (detected 'n')`, the fix is `show_belt=n` in params.ini `[char]`.
### Config singleton pattern
`Config` uses `__new__` with a `data_loaded` class variable. Calling `Config()` multiple times in the same process returns the same loaded instance. After editing params.ini at runtime, `Config().reload()` is needed (or restart the bot).
---
## 7) Current bot profile (Fistman — as of 2026-06-05)
- **Character**: Hammerdin (`src/char/paladin/hammerdin.py`)
- **Runs**: Pindle + Arcane Sanctuary
- **Key bindings** (from .keyo + params.ini):
- `show_belt = n` (belt hotkey)
- `stand_still = capslock` (overrides detected 'shift')
- `weapon_switch = w`
- `show_items = alt`
- `potion1..4 = 1,2,3,4`
- **Capabilities**: `can_teleport_natively = True` (set via `override_capabilities` in `[advanced_options]`)
- **Thresholds**: `take_rejuv_potion_health=0.80`, `take_rejuv_potion_mana=0.55`, `chicken=0.50`
---
## 8) Testing strategy by change type
### Messaging changes
```powershell
python -m pytest -q test/test_discord_embeds.py
```
### Config parsing changes
```powershell
python -m compileall src/config.py
python -c "import sys; sys.path.insert(0,'src'); from config import Config; c=Config(); print(c.char['show_belt'], c.char['stand_still'])"
```
### Inventory/sell logic
- Validate no exceptions in `inspect_items` / `transfer_items`.
- Log outputs should include item names (not just coordinates).
- Prefer dry functional checks from logs before gameplay runs.
### HealthManager changes
- Check that chicken thresholds still fire at the right HP%.
- Verify the two-rejuv check only triggers when `health_percentage <= take_rejuv_potion_health`.
### PickIt changes
- Confirm `_cached_pickit_items` is populated on both `PickedUp` (cached True) and `PickedUpFailed` (cached False).
- Confirm `_yoink_item` return value is not relied on for success detection.
---
## 9) Git hygiene
- Keep untracked: `.env`, `config/custom.ini`, `log/`, `log/screenshots/`
- Do not revert unrelated user changes.
- Commit frequently with single-purpose messages.
- `python -m compileall src/` must pass cleanly before committing.
---
## 10) Module internals and trap patterns
### 10.1 Config / key detection traps
**Trap**: `apply_key_bindings` runs *after* `self.char` is built in `config.py`. If you add a new key to `self.char` dict and it doesn't exist in `CHAR_BINDING_SLOTS`, the keyo detector won't touch it — but the user still needs to have it in params.ini.
**Trap**: "Keeping configured key binding" is NOT an error. It means the user explicitly configured something different from the game default. It becomes a problem only if the params.ini value is wrong (e.g., 'k' instead of 'n' for show_belt).
**Trap**: `Config()` is a singleton via `__new__`. The first call loads everything. Subsequent calls within the same process return the cached instance. Do NOT expect param changes at runtime to be visible without `Config().reload()`.
### 10.2 HealthManager rejuv traps
The rejuv logic in `start_monitor()`:
```python
if last_drink > 0.60: # minimum between rejuvs
if health <= take_rejuv_potion_health or mana <= take_rejuv_potion_mana:
drink_rejuv()
self._last_rejuv = time.time()
# Two juvs in 8 seconds → chicken ONLY if HP was the trigger
if last_drink < 8 and health_percentage <= Config().char["take_rejuv_potion_health"]:
self._do_chicken(img)
```
**Critical**: The `last_drink < 8` chicken check MUST also check `health_percentage`. Without it, any mana-triggered second rejuv (common for Hammerdins) will false-chicken at 99.9% HP. The fix is already in place — do not revert it.
**Timing**: The monitor polls every `3/25s * jitter(±20%)` ≈ 96144ms. At 25 FPS that's every 3 frames.
**Thread safety**: `_pause_state` and `_panel_check_paused` are protected by `_state_lock`. Module-level `get_pause_state()` / `set_pause_state()` functions delegate to the singleton. Always use these functions from external code.
### 10.3 PickIt ID/UID system
`GroundItem` has two identifiers:
```python
ID = slugify(f"{Name}_{'_'.join([str(v) for _,v in as_dict().items()])}")
# Includes Amount in the string. Two gold piles with different amounts = different IDs.
UID = f"{ID}_{'_'.join([str(v) for v in center])}"
# ID + screen position. Same pile at same coordinates = same UID.
```
**Trap**: `_pick_up_item`'s gold-fail detection uses `item.ID == prev.ID`. If two nearby gold piles have different amounts (e.g., 338g and 157g), they alternate as the "next" item and each one's ID never matches the previous, so the same-ID fail check never fires. The loop runs until timeout (20s).
**Trap**: `_yoink_item` ALWAYS returns `PickedUpResult.PickedUp`. It never returns `PickedUpFailed`. Pickup failures for teleport builds are silently swallowed.
**Fix in place**: `pick_up_items()` match block now has:
```python
case PickedUpResult.PickedUpFailed:
self._cached_pickit_items[item.ID] = False # blacklist this session
```
This stops the alternating-gold loop by blacklisting the item after the first confirmed failure.
### 10.4 Belt system open() key chain
`belt.open()` tries keys in this order:
```python
[config_val, "n", "k", "`", "~"] # deduplicated
```
If `show_belt = n` in params.ini, the first key tried is 'n'. If it works, no fallback keys appear in logs. If you see `"Trying to open belt with key: k"` it means 'n' failed — check if `show_belt` is actually set to 'n' and if the D2R window is focused.
### 10.5 TownManager location routing
`get_act_from_location(loc)` returns `None` for non-string inputs (e.g., `True`, `False`). The isinstance guard at line 36 (`if not isinstance(loc, str): return None`) prevents `AttributeError: 'bool' object has no attribute 'upper'`. Do not remove it.
All town methods that receive a `Location` return `False` (not `None`) on failure. Callers should check `if not new_loc` not `if new_loc is None`.
### 10.6 State machine: `end_game` vs `end_run`
`end_run` sends a TP, waits in town, does maintenance, then starts another run. If something prevents getting back to town (no TP scrolls, merc dead with no body, disconnected), `end_run` loops.
`end_game` saves and exits, restarts the game fresh. Use it when:
- TP charges = 0 (bot will restock on next game start)
- Unrecoverable in-game state
- Max consecutive failed runs reached
Triggering `end_run` when TP is gone causes an infinite "No TP charges left, trying to walk back" loop (pre-fix behavior).
### 10.7 distance calculation (processing_helpers.py)
The y-center of the screen for distance math is `screen_height / 2`, NOT `screen_width / 2`. Using the wrong dimension skews distance sorting for items on the top/bottom half of the screen. Fix is already applied.
---
## 11) Bugs fixed in 2026-06-04/05 session
All fixes were applied and verified by Python compile/config tests:
| Bug | File | Symptom in logs | Fix |
|---|---|---|---|
| `show_belt` wrong key (`n` instead of `k`) | `config/params.ini` | "Recovered belt hotkey using 'k'" on first game, then silent in-memory mutation | `show_belt=n``show_belt=k` |
| `AttributeError: 'bool' object has no attribute 'upper'` | `src/town/town_manager.py:36` | Crash in `get_act_from_location` when `True`/`False` passed as loc | Added `isinstance(loc, str)` guard |
| No-TP → infinite loop | `src/bot.py` | "No TP charges left, trying to walk back" repeated forever | Trigger `end_game` instead of `end_run` on zero TP |
| Distance y-axis wrong | `src/d2r_image/processing_helpers.py` | Items sorted by wrong distance; far items picked first | `screen_width/2``screen_height/2` for y |
| `on_maintenance` crash with no location | `src/bot.py` | Crash after chicken recovery when `_curr_loc=None` | Guard: default to `A1_TOWN_START` if None |
| False-positive chicken on mana rejuv | `src/health_manager.py:133` | "Two juvs drank within 0.63s. Chicken, HP 99.9%!" | Added `and health_percentage <= take_rejuv_potion_health` |
| Gold pickup infinite loop | `src/item/pickit.py:241` | 338g/157g alternating in logs for 20s | Added `PickedUpFailed` case to blacklist `item.ID` |
| Health pots sold when needed | `src/inventory/personal.py:351` | "Discarding SUPER HEALING POTION." + "Confirmed sell SUPER HEALING POTION" despite health needs | Check `get_needs()` before dropping consumable; `continue` to skip sell/drop when pot is needed |
| No fill_from_inventory after failed buy | `src/bot.py` (after line 438) | Belt empty all game despite pots sitting in inventory; "Out of gold" then nothing fills belt | After buy_consumables block, call `fill_up_belt_from_inventory` + `update_pot_needs` when needs > 0 |
| Wrong weapon in combat after chicken mid-buff | `src/char/i_char.py` `_pre_buff_cta` | Character dies immediately; dies with CTA flail/shield instead of main weapon | Added BC skill-bar template verification after each `weapon_switch`; corrects slot if wrong at game start; retries once on failure |
---
## 12) Known pending issues (as of 2026-06-05)
- **C10** (IMPROVEMENTS.md): `kill_thread()` uses `PyThreadState_SetAsyncExc` — can leave locks inconsistent. Replace with `threading.Event` cooperative shutdown. High risk.
- **optipng pass on assets/**: Pending. Run `asset_manager.py batch` or `optipng -o7` on all PNGs.
- **Thread safety** (H14 in IMPROVEMENTS.md): `health_manager` and `death_manager` shared state — Lock is now present in HealthManager but verify all paths use it.
- **PickedUpResult enum gap** (M14): Values are 0,1,3,4,5. Value 2 is missing. Non-critical but confusing.
- **Gold vicious cycle**: Low gold → can't buy pots → health empty → more chickens/deaths → less gold. Monitor runs after the personal.py + bot.py fix — if the cycle still triggers, also check that `inspect_items` isn't being called with vendor_open=True before `fill_up_belt_from_inventory`.
---
## 13) Fast triage mapping
| Symptom in logs | Where to look | Likely cause |
|---|---|---|
| "Recovered belt hotkey using 'k'" on game 1, then silent | `config/params.ini` | `show_belt=n` should be `show_belt=k` |
| "Two juvs drank... Chicken" at HP > 80% | `src/health_manager.py:133` | Missing HP check on two-rejuv condition |
| Gold pile (XYZg) repeating 5+ times | `src/item/pickit.py:241` | `PickedUpFailed` case missing; item not blacklisted |
| "Failed to pick up X" then same X again immediately | `_yoink_item` / `_cached_pickit_items` | Blacklist not being set on failure |
| `AttributeError: 'bool' object has no attribute 'upper'` | `src/town/town_manager.py:36` | isinstance guard removed or bypassed |
| "No TP charges left, trying to walk back" (repeating) | `src/bot.py` around `end_run` | `end_run` triggered when should be `end_game` |
| "No current location set" | `src/bot.py on_maintenance` | `_curr_loc` was None after chicken/recovery |
| Discord embed errors | `src/messages/discord_embeds.py` | `file=` kwarg passed when attachment is None |
| Repair fail loops | `src/town/a5.py` + `town_manager.py` | Larzuk template noise; check A4 fallback path |
| "Failed to log exp" | `src/ui/player_bar.py` | OCR misread; check for `I/l``1` ambiguity |
| Sell includes wrong items | `src/inventory/personal.py` | `protect_shields_from_sell` or item filter issue |
| "Discarding SUPER HEALING POTION" + "Confirmed sell..." | `src/inventory/personal.py:351` | Consumable sold despite belt need — fixed by get_needs() guard |
| Belt needs stay health=3/mana=3 game after game; pots never drunk | `src/bot.py` after buy_consumables + `personal.py:351` | Health pots sold during inspect; no fill_from_inventory fallback |
| "started on CTA slot" in logs; dies in first seconds of run | `src/char/i_char.py _pre_buff_cta` | Game saved with CTA slot active (interrupted buff). Use `BC` template check at startup to detect and correct |
| Character enters run at partial HP (e.g. 40% after chicken) | `src/bot.py on_maintenance` | Health manager paused in town; no town-heal loop. Check `meters.get_health` and drink belt pots in maintenance before `update_pot_needs` |
---
## 14) "Done" checklist for a fix
- [ ] Reproduced from logs
- [ ] Root cause identified in source
- [ ] Patch applied in smallest reasonable scope
- [ ] `python -m compileall <changed_files>` passes
- [ ] Target tests pass (or explicitly explain why unavailable)
- [ ] Python smoke test confirms the fix (e.g., `Config().char['show_belt']`)
- [ ] Behavior documented here or in README/params if user-visible
---
If you need to continue immediately: start from latest `main`, run a short bot session, then inspect only the newest 200300 log lines before changing anything.
+258
View File
@@ -0,0 +1,258 @@
# Anti-Detection Framework for Botty-Go
## Overview
This document outlines the multi-layered anti-detection system built into botty-go.
Each layer addresses a specific detection vector that Blizzard and modern anti-cheat
systems use to identify bots.
---
## 1. Server-Side Behavior Analysis Countermeasures
### Detection: Session length, timing consistency, pathing patterns, repetition
### Countermeasures:
#### 1a. Variable Session Scheduling
- **Implementation:** `internal/schedule/scheduler.go`
- Randomized session start times using a circadian model
- Simulated human sleep patterns: 6-10 hour breaks between sessions
- Weekend/weekday behavior variance (humans play differently on weekends)
- Random session lengths: 20min to 6hours with exponential distribution
- Occasional "just 5 more minutes" overtime and "I'm tired" early stops
#### 1b. Stochastic Pathing
- **Implementation:** `internal/pather/stochastic.go`
- Add deliberate pathing imperfection: 5-15% deviation from optimal route
- Occasional wrong-way teleports followed by course correction
- Non-optimal waypoint selections (humants don't always take shortest path)
- Variable route ordering with cooldown-dependent choices
- 2-3% chance of "getting lost" and using wrong waypoint first
#### 1c. Skill Rotation Variance
- **Implementation:** `internal/char/behavior.go`
- Variable pre-buff timing (humans rush sometimes, sometimes take time)
- Occasional wrong skill selection followed by correction
- Potion usage with human-like hesitation (check multiple times before drinking)
- Merc healing variance: sometimes forget, sometimes over-heal
#### 1d. Route Randomization with Context
- **Implementation:** `internal/bot/route_planner.go`
- Dynamic route selection based on:
- Time since last run of each type
- Current TP scroll count (humans adapt)
- Gem/transmute urgency
- Occasional "feels like it" switches
- Never perfect round-robin; use weighted probability with drift
#### 1e. Farming Repetition Masking
- Never run the same route more than 8 times consecutively
- Insert "town breaks": stash visit, shrine check, repair, gamble
- 1-2% chance of "I'm bored, switching to different run" mid-session
- Vary kill strategies: sometimes rush, sometimes methodical
---
## 2. Warden / Client Integrity Countermeasures
### Detection: Loaded modules, injected DLLs, memory signatures, debuggers
### Countermeasures:
#### 2a. Pixel-Only Architecture (No Memory Access)
- **Implementation:** entire bot reads game state ONLY via screenshots
- NO memory reading, NO DLL injection, NO process hooking
- Same attack surface as a human with a camera pointed at the screen
- This is the #1 defense: if you only use screen capture + input simulation,
there's nothing to scan in process memory
#### 2b. Clean Process Environment
- **Implementation:** `internal/runtime/clean_env.go`
- Standard Go binary with no suspicious imports
- No debuggers, no memory readers, no process manipulation
- Run as a normal application, not injected
#### 2c. Overlay Avoidance
- Never draw on top of game window
- No window hooking or injection
- Screenshot from a separate thread, not an overlay
---
## 3. Input Pattern Analysis Countermeasures
### Detection: Synthetic inputs, smooth cursor paths, periodic inputs, no micro-corrections
### Countermeasures:
#### 3a. Human Motor Model
- **Implementation:** `internal/mouse/human_model.go`
- Full biomechanical mouse model based on Fitts' Law and human motion studies
- Real human mouse data characteristics:
- Multi-segment movement with micro-pauses (1-3 segments per motion)
- Acceleration curve: start slow, peak in middle, decelerate into target
- Endpoint micro-adjustments: 2-5 pixel wobble before click
- Inter-trial variability: each movement is unique even to same target
- Asymmetric error distribution: overshoot more right/down (human bias)
#### 3b. Click Timing Model
- **Implementation:** `internal/mouse/click_model.go`
- Variable time between "arriving" at target and clicking: 50ms-800ms
- Pressure curve: humans don't click at exact same speed
- Double-click rate varies naturally
- Occasional misses: 0.5-1% of clicks land slightly off (1-3px)
#### 3c. Keyboard Behavior Model
- **Implementation:** `internal/keyboard/human_model.go`
- Key press duration variance: not all keypresses are identical
- Typing rhythm for skill hotkeys: natural cadence with micro-pauses
- Occasional key repeat (holding too long = rapid fire)
- Realistic key-up/key-down timing ratios
#### 3d. Statistical Indistinguishability
- **Implementation:** `internal/input/stats.go`
- All input streams modeled from real human motion capture data
- Entropy analysis of output matches human baselines
- Auto-calibration: measure user's own input if they do manual play
- Periodically inject "manual-looking" variance spikes
---
## 4. Economy and Item-Flow Countermeasures
### Detection: Gold accumulation, rune farming, item transfer networks, mule behavior
### Countermeasures:
#### 4a. Natural Accumulation Rate
- **Implementation:** `internal/inventory/economy.go`
- Vary farming intensity: some sessions heavy, some light
- Match accumulation to stated playtime (more sessions = more loot)
- Occasionally "waste" items on gambling/repairs like a real player
#### 4b. Realistic Trading Patterns
- No mass item funneling
- If trading, do it in human-sized batches with natural pauses
- Vary trade partners and timing
#### 4c. Rune Farming Variance
- Don't farm the same runes every session
- Match rune acquisition to character progression
- Occasionally skip rune picks when "full"
---
## 5. Ban Wave Defense
### Detection: Delayed batch bans
### Countermeasures:
#### 5a. Graceful Degradation
- **Implementation:** `internal/runtime/safe_mode.go`
- If one account gets banned, immediately reduce intensity across all
- Auto-pause farming for 48-72 hours (simulating "taking a break")
- Gradual return with reduced session lengths
- Change behavior patterns after any ban event
#### 5b. Account Diversity
- Each account has distinct "personality":
- Different session timing preferences
- Different route preferences
- Different response timing distributions
- Different play styles (rusher vs methodical)
---
## 6. Server Authority Countermeasures
### Detection: Server-side validation of movement, drops, combat, inventory
### Countermeasures:
#### 6a. Server-Authoritative Behavior
- **Implementation:** `internal/bot/server_aware.go`
- Only interact with what the server actually shows
- Wait for server confirmation before acting (e.g., confirm item picked up)
- Respect server-enforced movement limits (no speed hacks)
- Process drops in game-authorized order
#### 6b. No Client Manipulation
- Never try to spoof packets, modify client, or exploit desync
- Purely reactive: see screen -> decide -> act -> wait for response
---
## 7. Social/Reporting System Countermeasures
### Detection: Player reports + telemetry correlation
### Countermeasures:
#### 7a. Social Stealth
- **Implementation:** `internal/social/stealth.go`
- Play during off-peak hours less suspiciously
- Avoid solo-public routes that attract attention
- Occasionally join other players' games (with reduced automation)
- Inherit human-like chat behavior if configured
---
## 8. Hardware/Identity Correlation Countermeasures
### Detection: IP patterns, hardware fingerprints, VMs, account clusters
### Countermeasures:
#### 8a. Clean Deployment
- **Implementation:** `internal/deploy/clean.go`
- Run on real hardware, not VMs
- Use residential IP, not datacenter
- One account per hardware profile
- No VPN/proxy during play sessions
---
## Implementation Architecture
```
internal/
├── input/ # Human-like input generation
│ ├── mouse_model.go # Fitts' Law mouse movement
│ ├── click_model.go # Human click timing
│ ├── keyboard_model.go # Keyboard behavior
│ └── stats.go # Statistical verification
├── behavior/ # High-level human behavior simulation
│ ├── scheduler.go # Session scheduling
│ ├── route_planner.go # Dynamic route selection
│ ├── fatigue.go # Simulated fatigue/boredom
│ └── personality.go # Per-account personality
├── economy/ # Economic behavior masking
│ ├── accumulation.go # Natural loot accumulation
│ └── trading.go # Human-like trading patterns
├── safe_mode/ # Graceful degradation
│ ├── detection.go # Ban wave detection
│ └── cooldown.go # Auto-pause and return
└── deploy/ # Clean deployment helpers
└── check.go # Pre-flight integrity checks
```
## Key Design Principles
1. **Statistical indistinguishability:** Output must be statistically
indistinguishable from real human input. We use actual human motion
capture data distributions, not made-up random numbers.
2. **Controlled imperfection:** A human is inefficient, forgetful, and
inconsistent. The bot should be too — but in a way that matches
real human distributions.
3. **No single fingerprint:** Every instance should have unique enough
characteristics that correlating two accounts is hard.
4. **Adaptability:** If behavior changes are detected, the system should
be able to recalibrate based on new data.
5. **Defense in depth:** No single countermeasure is sufficient. The
combination across all layers is what provides real protection.
+33
View File
@@ -0,0 +1,33 @@
# Botty-Go
D2R Pixel Bot rewritten in Go for cross-platform support (Linux + Windows).
Based on the Python Botty project (johannes-do/botty), this is a ground-up rewrite
in Go that maintains compatibility with the same config files, templates, and run
logic while adding native Linux support.
## Features
- Cross-platform: Linux (X11/Wayland) and Windows
- Same config format as original Botty (params.ini, game.ini, shop.ini)
- Template matching with OpenCV Go bindings
- Tesseract OCR for item identification
- Human-like mouse movement (Bezier curves)
- BNIP pickit language
- All original character builds (Sorc, Paladin, Necro, Barbarian, etc.)
- All original runs (Pindle, Eldritch, Shenk, Trav, Nihlathak, Arcane, Diablo)
## Building
```bash
# Linux
go build -o botty ./cmd/botty
# Windows (from Linux with cross-compile)
GOOS=windows GOARCH=amd64 go build -o botty.exe ./cmd/botty
```
## Configuration
Copy `config/` from the original Botty project. Params, routes, and character
config work identically.
+19
View File
@@ -0,0 +1,19 @@
# Legacy: Go Rewrite Design Notes
These docs are archived from an abandoned `~/git/botty-go` directory (May 2026).
That project was a planned ground-up Go rewrite of `johannes-do/botty` for
cross-platform (Linux + Windows) support. Only design docs existed — no `.go`
source was ever written.
The Python `my-botty` project (this repo) is the active path. These docs are
kept here as **reference material**, primarily for Milestone 2 (anti-detection /
stealth) of `~/.claude/plans/continue-the-make-up-sunny-honey.md`.
## Files
- **`ANTI_DETECTION.md`** — Multi-layer anti-detection framework. Covers
server-side behavior analysis countermeasures (session scheduling, stochastic
pathing, skill rotation variance) and more. Directly applicable as the design
basis for the Python stealth layer.
- **`GO_REWRITE_README.md`** — Original README of the abandoned Go project.
Context only — explains feature scope and what the rewrite was aiming for.

Some files were not shown because too many files have changed in this diff Show More