Files
my-botty/ARCHITECTURE.md
T
alexpolo1andClaude Opus 5 a2265475d6 Merge origin/main into baalxp
Resolves the 11 conflicting files blocking PR #41. Where both branches fixed
the same problem, the fixes are combined rather than one side dropped:

- a5.py: main's dual WP templates + require_visible pre-scan, bounded by
  baalxp's town_world ROI (excludes the HUD band and party column)
- pindle.py: main's removal of the town-false-positive "already in temple"
  shortcut (also dropped baalxp's post-nudge copy of it); kept baalxp's nudge,
  click tuning and PORTAL_APPROACH node; main's detect-act retry
- town_manager.py: main's act-margin check in wait_for_town_spawn, then
  baalxp's marker->location resolution; baalxp's in_act repair skip before
  main's repair_npc destination table
- bot.py: baalxp's restructured leech cycle, with main's death recovery and
  InGame miss tolerance ported into _baal_xp_hide_and_wait
- game_browser.py, skill_preflight.py, [hammerdin] binds: baalxp (lobby join
  flow; binds verified against the live client 2026-09-04)
- params.ini: baalxp's "leech stays out of order=" rule with order=run_trav
- CLAUDE.md: both bug writeups kept; baalxp's 23-25 renumbered to 32-34 since
  tests/docs reference main's numbering
- test_stealth_config.py: AFK-break invariant exempts the whole baal_xp block
  instead of a 6-line window (the branch grew several handoffs)

Tests: 272 passed. Remaining failures are pre-existing on origin/main
(gems-tab transmute test, smoke_test/test_version_consistency collection).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-15 17:01:34 +02:00

15 KiB
Raw Blame History

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.

Asset conventions — read before adding or recapturing any template:

  • Every .png under the TEMPLATE_PATHS roots is loaded recursively and keyed by its uppercased filename (a5_red_portal.pngA5_RED_PORTAL). Keys are one flat namespace across all roots, so filenames must be globally unique — and never leave a backup or scratch .png anywhere under assets/, or it silently becomes a live template.
  • Keep templates fully opaque (3-channel, or 4-channel with no zero-alpha pixel). alpha_to_mask only produces a mask when the image has 4 channels and contains a fully transparent pixel; that mask is then passed to cv2.matchTemplate(..., TM_CCOEFF_NORMED, mask=...). OpenCV only properly supports masks for TM_SQDIFF and TM_CCORR_NORMED — masked TM_CCOEFF_NORMED returns unreliable scores and wandering match positions. A masked template will appear to "work" in isolation and then fail at random. See CLAUDE.md Bug 23.
  • Crop something structurally stable and unoccluded. Animated or partly hidden features (a swirling portal's interior, a ring occluded by scenery) make poor anchors.
  • Validate on held-out frames: build the crop from one capture, score it against other captures, and — critically — against frames where the subject is absent. A template that scores high on both is matching background, not the subject. Aim for a clear gap straddling the 0.68 default threshold.

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, cold_plains, baal_xp, pindle, shenk, trav, nihlathak, arcane, diablo, vizier, baal, mephisto, andariel, countess

Baal XP leech (baal_xp state, on_run_baal_xp): a self-contained cycle that does NOT use the run_wrapper/approach/battle pattern. It leaves the bot's own game, joins a public Baal game via the online lobby (src/ui/game_browser.py), hides and stands still to collect shared area XP, then leaves and re-enters its own game. The hide-and-wait phase is a pure wait — no maintenance, no runs, no pathing; the health manager thread keeps auto-potioning and the death manager handles the death screen. Config lives in the [baal_xp] section of params.ini; the route is run_baal_xp and re-arms itself between cycles via _baal_xp_rearm.

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.