Every fatal message told the user THAT something failed but not what to do
about it. The worst was "conda env create failed. See output above." --
useless when run_install_capture.bat redirects that output to a 56 KB log.
Each error now names the likely cause and the concrete next step:
- download failed -> the URL tried, firewall/proxy hint, manual-install
fallback that install.bat will detect on re-run
- truncated download -> got N bytes vs expected ~78 MB, bad file deleted
- installer failed -> antivirus/UAC hint, how to run it by hand
- conda found but dead -> the exact command to reproduce the real error
- env create failed -> disk/network/antivirus causes, plus the
"env remove -n botty -y" recovery for a half
finished install
- pip install failed -> notes the env itself is fine and a re-run resumes
- python.exe missing -> explains partial env, gives the recovery commands
- find_python.bat -> distinguishes "never installed" from "install.bat
did not finish", pointing at the capture log
Added a shared ":fail" exit so every fatal path ends with how to produce a
full log for a bug report, and states that nothing else was changed.
Also added a disk-space pre-flight before env creation: under 3 GB now
fails immediately with a clear message instead of letting conda die halfway
through with an opaque error; 3-6 GB warns. The environment needs ~4 GB
plus ~1 GB of downloads.
Verified: install.bat still completes with exit 0; the pre-flight was
exercised at real, simulated-2 GB and simulated-5 GB levels and all three
branches render and exit correctly; find_python.bat still resolves; 140
tests pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
install.bat had 9 pause statements on failure paths and none on the
success path. The README tells users to double-click install.bat, so on a
successful install the console vanished the moment it finished -- a new
user never saw "Installation complete", the OCR verdict, or the dependency
verification, and had no way to tell whether it had worked.
Added pause to the success path.
run_install_capture.bat redirects stdout to install_log.txt, so that new
pause would have blocked behind the redirect: an empty window silently
waiting on a keypress the user cannot see. It now feeds stdin from nul,
reports success/failure with the log path, and pauses itself.
Verified non-interactive: run_install_capture.bat completes in ~28s with
exit 0 and no hang.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
test_setup_bat_files.py asserted that run_asset_extractor.bat and
run_quest_debug.bat exist in the repo root. Those are developer tools that
the end-user `stable` branch deliberately strips, so a fresh clone of
stable shipped 4 failing tests even though the bot was fine.
Split the list into CORE_BATS (install/find_python/run_botty -- required on
every branch) and OPTIONAL_BATS (dev tooling -- validated only when
present). The username, absolute-path and find_python checks now iterate
over the files that actually exist rather than a hardcoded list.
Found by cloning stable from GitHub onto a clean machine and running the
suite as a new user would.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Includes the pefile import-chain technique that found it, since WinError
126 names the importing DLL and never the missing dependency.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
tesserocr never loaded -- install.bat always reported "tesserocr: not
available (DLL issue)" and the bot ran on the pytesseract fallback, which
shells out to tesseract.exe per OCR call instead of using the in-process
C++ API.
Root cause, found by walking the import table with pefile:
tesserocr.pyd -> tesseract52.dll -> leptonica-1.78.0.dll -> tiff.dll
-> libdeflate.dll <- MISSING
Current conda-forge libdeflate (>=1.20) installs the library as
"deflate.dll", but the older tiff.dll from the tesseract=4.x stack still
imports the previous name "libdeflate.dll". Nothing provided that name, so
tiff.dll failed to load and every DLL above it failed with WinError 126
("The specified module could not be found") -- which is why the error
looked like a missing module even though every file was present.
Fix: install libdeflate explicitly alongside tesseract=4.*, then copy
deflate.dll to the legacy name libdeflate.dll when that name is absent.
Same library, same exports.
Verified: removing the alias reproduces the failure exactly; running
install.bat recreates it and the installer now reports "tesserocr: OK
(fast path)". tesserocr initialises and performs real OCR with both bundled
models, and the bot logs "OCR backend: tesserocr (primary)" at startup.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
run_install_capture.bat writes install_log.txt into the repo root. It was
untracked but not ignored, so it showed up as noise in git status and was
easy to commit by accident.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Both were found by running install.bat under simulated clean-machine
conditions (conda removed, winget stripped from PATH, Tesseract hidden).
Bug 20: an unescaped ")" in an echo inside a parenthesised block aborted
the script at parse time, killing the conda direct-download path -- the
only path available without winget.
Bug 21: winget defaulted to machine scope, so the installer needed admin
and failed silently on a normal double-click.
Also documents the two recurring batch pitfalls with an awk audit command,
and the measured limitation that Tesseract has no per-user install path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Testing the direct-download fallback with Tesseract absent and winget
unavailable showed the official installer self-elevates and its elevated
relaunch discards /D=, so it always installs machine-wide to
"C:\Program Files\Tesseract-OCR" regardless of TS_DEST.
The previous comment claimed this path gave a per-user install needing no
admin rights, which is not true: there is no per-user install path with
the official Tesseract installer, and it requires admin/UAC. Corrected the
comment rather than the code -- /D= is harmless as best-effort, and both
find_tesseract and src\d2r_image\ocr.py already search the machine-wide
and per-user locations, so either outcome works at runtime.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Miniforge direct-download fallback -- the only path available on a
clean machine without winget -- could never complete. install.bat aborted
with ". was unexpected at this time." immediately after running the
Miniforge installer, so conda was installed but the botty env was never
created and the bot was unusable.
Cause: line 133 echoed "(exit code %errorlevel%)" inside a parenthesised
if-block. An unescaped ")" inside a block terminates the block, leaving
"." as a stray token. cmd parses the entire if-block when it reaches it,
so this fired even when the installer SUCCEEDED and the block body was
never meant to run -- verified with a minimal repro: the unescaped form
exits 255 on a false condition, the escaped form exits 0.
Fix: escape as ^(exit code %errorlevel%^), matching the convention the
rest of the file already uses ("^(fast path^)"). Audited every echo
inside a block; this was the only remaining unescaped instance.
Found by running install.bat with winget removed from PATH to simulate a
clean Windows 10 machine.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Live clean-install test caught a syntax bug in the new Tesseract setup
block: install.bat died with "so was unexpected at this time." right
after "Setting up OCR...", so OCR setup and the whole dependency
verification stage never ran.
Cause: "::" comment lines placed INSIDE parenthesised if-blocks. Two
problems compound there -- a "::" line inside a ( ) block is itself a
parse error, and any parenthesis in the comment text closes the block
early. The text "(non-zero when already installed), so after each" left
"so" as a stray token.
Fix: move every comment out of the parenthesised blocks, in both the
Tesseract block and the conda winget block added earlier. The conda one
had survived only because its text happened to contain no parentheses.
Verified: install.bat now runs to completion with exit 0 --
Tesseract: C:\Program Files\Tesseract-OCR\tesseract.exe
pytesseract: OK (tesserocr: DLL issue, expected)
cv2/mss/numpy/transitions/rapidfuzz/pydantic/pytesseract/yaml/discord: OK
All dependencies verified.
140 tests pass against the freshly created env.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Follow-up to the conda scope fix: the same admin/winget assumptions broke
the OCR backend, which is what actually carries item text reading since
tesserocr's MSVC DLL chain commonly fails to load.
- install.bat installed Tesseract via `winget install` with no --scope,
i.e. machine-wide into "C:\Program Files", which requires admin. On a
clean non-admin box this failed and left NO working OCR backend at all
(tesserocr already fails), so OCR_READY=0 and item reading was dead.
Now: winget machine scope -> winget --scope user -> direct download of
the official NSIS installer with a per-user /D= target. Also stops
trusting winget's exit code (non-zero when already installed) and
re-resolves tesseract.exe after each attempt.
- The downloaded installer is size-checked (~50 MB; <20 MB = failed
download) before being executed, matching the Miniforge handling.
- Added a :find_tesseract subroutine that resolves tesseract.exe from
Program Files, Program Files (x86), %LOCALAPPDATA%\Programs,
%ProgramData% and PATH. Verification now uses the resolved path instead
of the hardcoded "C:\Program Files" one.
- ocr.py: added Program Files (x86) and the per-user
%LOCALAPPDATA%\Programs\Tesseract-OCR location to the runtime search
order, since per-user installs are not on PATH.
- run_botty.bat: only export PYTESSERACT_TESSERACT_CMD when the file
exists, falling back to the per-user path, so a stale machine-wide
value cannot shadow a valid per-user install.
Verified on this machine: all install.bat dependency imports OK
(cv2/mss/numpy/transitions/rapidfuzz/pydantic/pytesseract/yaml/discord),
pytesseract resolves tesseract 5.5.0, osdetect reports the win11 profile,
config loads, 140 tests pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
install.bat could fail to auto-install conda on a fresh, non-admin
machine:
- winget install used the default (machine) scope, landing conda in
%ProgramData% and requiring elevation. A normal double-click without
admin failed silently and conda never installed. Add --scope user so
it installs to %USERPROFILE%\miniforge3 with no admin needed.
- winget returns non-zero when the package is already present, so its
exit code was unreliable. Rescan for conda.exe after winget and only
fall through to the direct download when it is genuinely missing.
- The GitHub (git) download fallback never validated the file before
running it: a truncated download or an HTML error page served with a
200 would be launched as the "installer" and silently do nothing. Add
a size check (<40 MB => failed download, clear error + bail) plus a
pre-download cleanup of any stale temp file.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
rapidfuzz 3.x moved levenshtein out of rapidfuzz.string_metric (removed)
into rapidfuzz.distance.Levenshtein, but Levenshtein is now a module, not
a function. The fallback alias pointed at the module — fix it to bind the
.distance method directly.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
environment-win11.yml installs only from requirements.txt, which was missing
pyinstaller. build.py constructs the full path to pyinstaller.exe so the
install is sufficient; no PATH change needed.
Verified locally: 99 passed, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
environment-win11.yml pulls only requirements.txt (not environment.yml), so
coverage, pytest, pytest-env, pytest-mock, and pytest-pythonpath were missing
from the CI conda env. Pinned to versions matching the local botty env.
Verified locally: 99 passed, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
conda activate does not add Scripts/ to PowerShell PATH in the GitHub Actions
runner. Switching to 'python -m coverage' works regardless of PATH state.
Verified locally: 99 passed, 0 failed.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
CI was failing at conda env setup: pip could not find async-timeout==5.1.0.
Latest available version is 5.0.1.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
time.time() on Windows has ~15 ms resolution; a 0.0-second budget produced a
deadline equal to the current tick, so the anchor-loop check never fired and
traverse_calls reached 5 instead of 1. Using -1.0 puts the deadline one second
in the past — guaranteed expired on any hardware.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
A "999" gem count (OCR couldn't read the digit, code assumes "convert
until depleted") relied on the loop noticing an empty stack and
breaking — but that check only ran when _gems_stack_monitor_for
returned None. For any registered gem type it always returns a static
screen coordinate, so the depletion check was dead code: the loop
just kept blindly clicking the same fixed position forever.
Observed in the wild: stuck on Topaz Flawless for 30+ minutes and 177
iterations (of a fake "999" target, ~2.7h worst case) before being
manually force-exited, repeatedly clicking fixed convert-panel/GEMS
coordinates with nothing real there — the likely cause of it also
grabbing and re-placing unrelated stash items during that time.
Now always does a live template search before clicking, breaking
immediately once the stack is genuinely gone, on every gem type.
Verified end-to-end with a mocked run: a fake depleted "999" stack now
stops instantly instead of looping, and a real gem right after it
still converts correctly.
Every single game logged "Failed to find Battle Command, swapping
weapons again" — 1182 times in the last log alone, always on the
first attempt, always resolved by the very next loop iteration's
identical check with no extra wait in between. The skill icon just
takes a bit longer than the fixed 0.6-0.8s wait to render on this
system; the check was racing it every time.
Poll for up to 1.2s instead of a single check after a fixed wait.
Catches the skill as soon as it's actually visible rather than always
failing once first, and removes the latent risk of the fallback path
incorrectly swapping back to the main weapon if timing ever degraded
further.
It already read Config().char.get("protect_charms_from_sell", True) in
personal.py's drop/sell guard, but the key was never added to the char
config dict builder in config.py, so setting it in an ini file did
nothing — charms were unconditionally undroppable regardless of the
pickit verdict. Wired it up the same way protect_shields_from_sell
already works. Defaults to 1 (protected, unchanged behavior) so this
is opt-in only.
When a tab showed a free slot but the specific placement click kept
failing, the code deliberately gave up rather than advance tabs (to
avoid falsely triggering stash_full()'s taskkill on a transient
glitch). In practice this meant the bot got stuck retrying the same
tab forever every game, leaving loot in inventory even when every
other stash tab was completely empty.
Now it tries the next tab (up to all 6) on repeated transfer failure,
same as it does for a genuinely full tab — but never calls
stash_full() from this path, only from the original "confirmed no
empty slot anywhere" detection. Verified with a mocked simulation:
cycles through failing tabs to a working one, and degrades gracefully
(leaves items in inventory, no crash, no false stash_full) if every
tab fails.
Was hardcoded to 10 games; a strict pickit on a fast boss-only rush
route can legitimately go 10 games without a keep-worthy drop, making
the log warning noisy. Defaults to 10 (unchanged), override per-user
via profile.ini.
The run_diablo route was failing every game. Diagnosed and fixed live —
a full run now clears all three seals (Vizier, De Seis, Infector) and loots.
Pentagram navigation (was the #1 abort: "battle_failed", char stranded in
CS trash, pentagram never detected):
- _loop_pentagram now falls back to active node-602 navigation when the blind
fixed-path teleport loop fails to surface the pentagram. Node 602 searches the
PENT templates directly and teleports toward them with the pather's auto-
recovery sweep — the same robust approach _cs_pentagram already uses. Applied
in both diablo.py and vizier.py.
- Combined with the lowered _PENT_THRESHOLD (0.50), the pentagram now resolves:
live reads were 57-96% where the old 0.83 threshold rejected them.
Seal layout check (next abort after the pentagram fix, at the Vizier seal):
- Added per-seal score logging (LC primary/confirm). This revealed the real
cause is character-positioning variance, NOT template drift: the true layout
reads 84-88% and the other 42-57% (clean separation) when well-positioned, but
from a bad camera angle BOTH read ~55-66% and the check is ambiguous.
- So the fix is more re-approach attempts (max_attempts 2 -> 3), not lower
thresholds — lowering a disambiguation threshold risks picking the WRONG seal
from a bad-position read.
- Normalized seal-A threshold_confirmation 0.85 -> 0.80 (every other seal is
0.80; safe given the 84-88% vs 42-57% separation).
Crash fix (game_recovery.py): go_to_hero_selection had been dedented to module
level while its body kept method indentation, so it fell out of the GameRecovery
class. Every post-chicken/death/failed-game recovery threw
AttributeError: 'GameRecovery' object has no attribute 'go_to_hero_selection'
and killed the run_bot thread. Re-indented into the class. Verified live: the
bot now recovers from failed games and auto-starts the next one.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- replace print() with Logger.debug() in misc.py to fix colorama OSError crash on restart
- try/except RuntimeError in template_finder ThreadPoolExecutor so interpreter shutdown falls back to sequential matching
- add last-resort direct WP scan in A5 open_wp after anchors fail
- extend go_to_hero_selection timeout 30s->45s, add ESC fallback after 15s if blocked by UI panel
- remove startup warning spam in screen.py
The A5 waypoint stone matches reliably (65-91% when on screen). The real
failure mode is a stale curr_loc that lands the char off the stone, so
select_by_template("A5_WP") never matches. The old escalation (NPC anchors +
a 6-step directed sweep) then looped for 5+ minutes — the "stuck in town"
behavior seen in log/log.txt 2026-06-24 (08:28:48 -> 08:30 force-exit).
- Add a 45s hard wall-clock budget to open_wp; bail between anchors once past.
- Drop the directed sweep entirely: it never recovered in practice and was the
main multi-minute time sink. A failure now returns fast so the caller falls
back (buy at Malah / skip to stash) instead of stranding the bot.
- Also folds in the in-progress A5 repair-menu timing fix (wait_until_visible
instead of a too-short 0.2-0.3s peek).
- Add test/town/a5_open_wp_test.py covering fast-fail, quick-mode, and budget.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A single bot session produced a 22 GB log. The file logger used daily-only
rotation (TimedRotatingFileHandler when='midnight') with NO size cap, so a
long/spammy session grew log.txt unbounded within a day. Its archiver also
looked for .1/.2 backups that the timed handler never produced.
- logger.py: switch to size-based RotatingFileHandler — log.txt rotates at
50 MB (override via BOTTY_LOG_MAX_MB), keeps 5 zipped backups, and prunes
log/archive/ to 30 zips. Hard cap on both the live file and total disk.
The .1/.2 naming now matches what the handler emits, so archiving works.
- install.bat: pip --progress-bar off. The progress bar redraws via \r;
redirected to a file (run_install_capture.bat) those redraws became
millions of lines — the other way an install log balloons to GBs.
- params.ini: document the log.txt cap + BOTTY_LOG_MAX_MB.
Verified: with a tiny cap, log.txt stayed under the limit while rotated
files zipped to archive; full suite 80 passed / 2 skipped.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
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]>
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]>
- 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]>
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]>
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]>
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]>
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]>
- 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]>
Fixes CI build job which passes --conda_path C:\Miniconda but
botty_env was hardcoded to C:\Users\alex\.conda\envs\botty.
Also replace os.system mkdir with os.makedirs and add error
detection so PyInstaller failures are not silently swallowed.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>