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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
'coverage xml' was failing with 'No source for code: config-3.py' because
coverage was tracing conda internals (config-3.py from Miniconda's base env).
Add source=src to restrict coverage collection to our source tree, and
ignore_errors=True in [xml] as a safety net for any remaining phantom paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- bnip/utils.py: standardise error code prefix NIP_0x23 -> BNIP_0x23 to match
all other error codes in the bnip module (all use BNIP_ prefix)
- test_transpile: update syntax_test9 expected code 0x11 -> 0x23; parsing of
'[idname] > ring' now hits the unique/set lookup (BNIP_0x23) before the
logical-operator token check (BNIP_0x11) fires
- test_skill_preflight: test_unknown_build_has_no_checks used 'hammerdin' as
the unknown build, but hammerdin now has skill checks defined; switch to
'nonexistent_build' which has no checks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrapper for install.bat that redirects all output to install_log.txt so the
install can be run non-interactively and the log inspected after the fact.
Useful for debugging installs in CI or remote sessions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
discord.py imports aiohttp which calls ssl.create_default_context() at import
time. A malformed cert in the Windows cert store raises ASN1 NOT_ENOUGH_DATA.
The bot patches this at runtime via _patch_ssl(); the install check now applies
the same workaround (null-patch load_default_certs) so discord: OK on all machines.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Echo with (no ABSOLUTE flag) had unescaped ) closing the if block early,
causing BOTH Win11 and Win10 branches to run and wrong requirements used.
Fix: escape the parens with ^( and ^).
- %errorlevel% inside ( ) blocks is expanded at parse time, not after the
command that sets it. Affects winget install result check, tesseract winget
check, pytesseract check, and dependency verify loop.
Fix: use !errorlevel! (delayed expansion) in those four spots.
- Remove final pause so the CMD window closes automatically on success.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
winget (available on Win10 1709+ and all Win11) handles registration,
PATH, and package integrity checks automatically — cleaner than the
NSIS /S fallback. Fall back to curl+NSIS if winget is absent or fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Miniforge3's JustMe default install location changed from
%USERPROFILE%\miniforge3 to %LOCALAPPDATA%\miniforge3 in recent versions
(per robotology-superbuild docs and winget package metadata). Both the
initial scan and the post-bootstrap rescan now check %LOCALAPPDATA% first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs found by studying oobabooga/text-generation-webui (reference impl):
1. PowerShell Invoke-WebRequest without $ProgressPreference='SilentlyContinue'
renders an ASCII progress bar that drops download speed from ~50 MB/s to
~1 MB/s. Switch primary download to curl (built into Win10/11, fast, clean
progress bar). PowerShell is kept as a fallback with the flag set.
2. Running the NSIS installer directly ("installer.exe /S") can return before
the install finishes. The correct pattern is "start /wait "" installer.exe"
which blocks until the installer process truly exits. Also adds /NoShortcuts
and /NoRegistry flags (standard for embedded/self-contained installs).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NSIS /D= does not support paths with spaces. %USERPROFILE% often contains
spaces (e.g. C:\Users\John Smith). Omitting /D= lets Miniforge install to
its own default location (%USERPROFILE%\miniforge3) which it handles safely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users no longer need to pre-install conda. If the first scan finds no
conda.exe, install.bat downloads Miniforge3-Windows-x86_64.exe via
PowerShell (available on all Win10/Win11), installs it silently to
%USERPROFILE%\miniforge3 with no PATH changes, then re-scans and
continues. On failure it prints a manual fallback URL. Users with an
existing conda install are unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old install.bat copied tesseract41.dll as tesseract.dll (wrong name — wheel
needs tesseract51.dll) and the bundled .pyd used MSVC C++ ABI that the MinGW conda
DLLs can't satisfy. Both tesserocr imports now work:
tesseract52.dll — MSVC tesseract 5.2.0 from conda-forge (compiled vs2019)
tesserocr.cp310-...pyd — conda-forge 2.5.2 build linked against tesseract52.dll
The MSVC leptonica-1.78.0.dll comes from the existing conda tesseract=4.x install.
ocr.py already registers Library/bin via os.add_dll_directory so both backends
(tesserocr fast path, pytesseract reliable fallback) load correctly at bot startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
count scenario was registered in the scenarios dict but missing from
the module docstring — running testbed.py with no args hid it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Architecture: two backends, first one that works wins at runtime (ocr.py).
1. tesserocr — C extension via conda tesseract 4.x DLLs + bundled wheel (fast)
2. pytesseract — subprocess via winget tesseract.exe (always works if exe exists)
Install changes:
- tesserocr setup: best-effort, never fatal. Wheel install failure or DLL mismatch
just falls through to pytesseract. Removes the hard-exit on missing wheel.
- pytesseract setup: winget install with a manual download link fallback for Win10
users where winget is unavailable. pytesseract is now in requirements.txt so it
is always importable.
- OCR verification: tests BOTH backends after setup, reports which is active.
Warns clearly if neither works rather than silently leaving OCR broken.
Sets ALL_OK=0 so the installer ends with a visible failure state.
- Dependency loop: removes tesserocr from the import check — OCR is now verified
end-to-end by the backend test above, not by a bare import.
- Removes the broken pytesseract source-patch block (syntax error, never ran).
- TESS_PATH hoisted to :env_found so it is available for the OCR check.
requirements.txt: add pytesseract (pure Python, no native deps).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverts 63d0a33 and e712076. Both commits got the OCR architecture backwards:
- Made winget "optional" but DEPENDENCIES.md shows pytesseract is the working
backend on the actual machine (tesserocr is DLL-broken there)
- Added a tesserocr smoke test that would always fail on this machine
- Added pytesseract to verification loop but it's not in requirements.txt
Starting fresh with a correct understanding of the OCR stack.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tesserocr (conda DLLs + bundled wheel) is the primary OCR backend and works
identically on both platforms. pytesseract is an optional fallback that needs
a working tesseract.exe, which winget can install on Win11 but not reliably
on Win10.
Changes:
- Winget step: reframed as optional/best-effort; failure is informational only,
not a warning that "OCR will not work" (tesserocr handles all OCR)
- Remove pytesseract from the verification loop — it is not in requirements.txt
and would always fail on Win10 without winget; adding it in the previous commit
was wrong
- Remove pytesseract-specific smoke test block
- Add tesserocr end-to-end smoke test after the verification loop: opens a
PyTessBaseAPI against the real tessdata model so DLL resolution + tessdata
access are both exercised; sets ALL_OK=0 on failure so the installer reports
the right thing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three fixes:
1. After winget tesseract install, check C:\Program Files\Tesseract-OCR\tesseract.exe
actually exists and print a download link if not — previously silent failure.
2. Remove the broken pytesseract source-patch block (Python syntax error on the
os.path.join call — it has never run). ocr.py already handles cmd discovery at
import time via PYTESSERACT_TESSERACT_CMD (Bug 14 fix), so patching is moot.
Replace with a pytesseract.get_tesseract_version() smoke test that exercises the
full exe chain and fails clearly if tesseract is missing.
3. Add pytesseract to the dependency verification loop alongside tesserocr.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Standalone script: scans loot columns, tooltips every occupied slot to
identify chipped gems, then right-clicks each to convert in-place.
Uses the same screen/input_layer stack as the main bot (no D2R restart needed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fg_market_scraper.py: broaden user.php href regex to allow a path prefix
before user.php (some topic pages use relative paths like ../user.php).
fg_scrape_pipeline.sh: --days 21 -> --days 60 for a fuller price history.
improve_fg_estimates.py: hoist min_valid_price constant out of the per-file
loop (was a constant being re-assigned every iteration).
fg_daily_estimates.json: refreshed from 853 topics over 60 days (was 21).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New 'count' scenario: read gem counts from GEMS tab via OCR, print plan
- gems_all: accept tier and gem-type filters + --max cap (e.g. 'gems_all flawless diamond')
- gems_all: fast-path ESC + stash-chest click before launching full TownManager
- gems_all: leave stash open after run so re-invocations skip navigation
- stash: stash chest auto-walk if stash not open
- Remove duplicate 'return 0' at end of _gems_all that made dead code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>