add stealth system, key detection, click recorder, FOHdin, new routes, and tooling updates
This commit is contained in:
+231
@@ -0,0 +1,231 @@
|
||||
# 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 (no kernel drivers):
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `win_input.py` | ctypes wrappers for `SendInput`, `GetAsyncKeyState`, `GetCursorPos` |
|
||||
| `mouse_impl.py` | Humanized mouse with Bezier curves, Gaussian distortion, endpoint wobble |
|
||||
| `hotkey.py` | Polling-based hotkey manager (replaces `keyboard.add_hotkey`) |
|
||||
| `__init__.py` | Drop-in API: `from input_layer import keyboard, mouse` |
|
||||
|
||||
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 (`src/utils/stealth.py`)
|
||||
|
||||
Three-tier stealth system:
|
||||
- **Tier 1 (Input)**: Micro-pauses, click variance, key press duration, endpoint wobble
|
||||
- **Tier 2 (Behavior)**: Wrong waypoint chance, skill mistake chance, skill hesitation
|
||||
- **Tier 3 (Session)**: AFK breaks, run skipping, personality seed per character
|
||||
|
||||
### 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 |
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
# 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
|
||||
|
||||
### C1. Stealth: Consolidate all timing through centralized wait()
|
||||
|
||||
Many places use bare `time.sleep()` instead of `utils.misc.wait()` (which has Gaussian jitter).
|
||||
Every direct sleep creates a predictable timing signature detectable by anti-cheat.
|
||||
|
||||
Files with bare `time.sleep()`:
|
||||
- health_manager.py line 14
|
||||
- chest.py
|
||||
- pather.py
|
||||
- game_recovery.py
|
||||
- npc_manager.py
|
||||
- bot.py
|
||||
|
||||
Fix: Replace all bare `time.sleep(n)` with `wait(n, n*1.2)` for human-like jitter.
|
||||
|
||||
### 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)~~
|
||||
|
||||
### C10. ~~Bug: Replace thread killing with cooperative shutdown~~
|
||||
~~`utils.misc.kill_thread()` uses `PyThreadState_SetAsyncExc` (CPython private API).~~
|
||||
~~This can leave locks in inconsistent state, cause GIL issues, or corrupt numpy arrays.~~
|
||||
~~Fix: Replace with threading.Event flags for cooperative shutdown.~~
|
||||
(Still needs doing - this is the most dangerous remaining bug.)
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
+111
@@ -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
|
||||
+1106
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,7 @@ 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}"
|
||||
installer_cmd = f"pyinstaller --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 {args.conda_path}\\envs\\botty\\Lib\\site-packages src\\{exe}"
|
||||
os.system(installer_cmd)
|
||||
|
||||
os.system(f"cd {botty_dir} && mkdir config && cd ..")
|
||||
@@ -97,6 +97,12 @@ 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')
|
||||
|
||||
# Always rename main.exe to avoid Warden flagging the obvious name
|
||||
if not args.random_name:
|
||||
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}"')
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ difficulty=normal
|
||||
name=bigfont
|
||||
randomize_runs=0
|
||||
target_tz=1
|
||||
saved_games_folder=
|
||||
saved_games_folder=C:\Users\alex\Saved Games\Diablo II Resurrected
|
||||
level_max_steps=20
|
||||
|
||||
; Battle.net credentials (for auto-login at launch)
|
||||
|
||||
@@ -9,8 +9,6 @@ dependencies = [
|
||||
"transitions",
|
||||
"mss==7.0.1",
|
||||
"numpy==1.26.4",
|
||||
"mouse",
|
||||
"keyboard",
|
||||
"beautifultable",
|
||||
"pytweening",
|
||||
"requests",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# ==========================================
|
||||
# botty - Pixelbot for Diablo 2 Resurrected
|
||||
# requirements.txt
|
||||
# ==========================================
|
||||
# Pin to versions that match the existing conda environment (environment.yml).
|
||||
# Run `pip install -r requirements.txt` inside the `botty` conda env.
|
||||
# For tesserocr, install the pre-built wheel instead:
|
||||
# pip install dependencies/tesserocr-2.5.2-cp310-cp310-win_amd64.whl
|
||||
# ==========================================
|
||||
|
||||
# --- Core runtime ---
|
||||
numpy==1.26.4
|
||||
opencv-python==4.5.5.64
|
||||
mss==7.0.1
|
||||
pillow
|
||||
pywin32
|
||||
|
||||
# --- Game input & control ---
|
||||
keyboard
|
||||
mouse
|
||||
pytweening
|
||||
|
||||
# --- State machine & logic ---
|
||||
transitions
|
||||
rapidfuzz==2.15.1
|
||||
pyparsing
|
||||
parse
|
||||
dataclasses-json
|
||||
|
||||
# --- UI & display ---
|
||||
colorama
|
||||
beautifultable
|
||||
|
||||
# --- Networking & messaging ---
|
||||
requests
|
||||
discord.py
|
||||
|
||||
# --- System & utilities ---
|
||||
psutil
|
||||
cryptography
|
||||
typing_extensions
|
||||
graphviz
|
||||
|
||||
# --- OCR (requires tesseract + leptonica from conda-forge first) ---
|
||||
# tesserocr -- install via wheel:
|
||||
# pip install dependencies/tesserocr-2.5.2-cp310-cp310-win_amd64.whl
|
||||
|
||||
# --- Dev / testing ---
|
||||
pytest
|
||||
pytest-env
|
||||
pytest-pythonpath
|
||||
pytest-mock
|
||||
coverage
|
||||
|
||||
# --- Packaging ---
|
||||
pyinstaller
|
||||
@@ -0,0 +1,57 @@
|
||||
# ruff.toml — botty project linting configuration
|
||||
|
||||
target-version = "py310"
|
||||
line-length = 120
|
||||
|
||||
[lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"F", # Pyflakes
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"SIM", # simplify
|
||||
"RUF", # ruff-specific rules
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long (handled by line-length)
|
||||
"F401", # unused imports (common in __init__.py and game bots)
|
||||
"F403", # star imports (used for convenience modules)
|
||||
"E722", # bare except (intentional in game automation)
|
||||
"RUF012", # mutable class attributes (State machine patterns)
|
||||
]
|
||||
|
||||
[lint.per-file-ignores]
|
||||
"__init__.py" = ["F401", "F403"]
|
||||
"**/test/**/*.py" = ["F401"]
|
||||
"**/mocks/**/*.py" = ["F401"]
|
||||
"tools/*.py" = ["E402"]
|
||||
"build.py" = ["E402"]
|
||||
|
||||
[lint.isort]
|
||||
force-single-line = false
|
||||
known-first-party = [
|
||||
"screen",
|
||||
"config",
|
||||
"logger",
|
||||
"bot",
|
||||
"pather",
|
||||
"template_finder",
|
||||
"input_layer",
|
||||
"ui",
|
||||
"ui_manager",
|
||||
"game_controller",
|
||||
"game_stats",
|
||||
"health_manager",
|
||||
"death_manager",
|
||||
"target_detect",
|
||||
"char",
|
||||
"shop",
|
||||
"item",
|
||||
"transmute",
|
||||
"bnip",
|
||||
"d2r_image",
|
||||
"messages",
|
||||
"npc_manager",
|
||||
"utils",
|
||||
"version",
|
||||
]
|
||||
+1
-1
@@ -16,7 +16,7 @@ Screenshots saved to screenshots/
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import numpy as np
|
||||
import cv2
|
||||
from datetime import datetime
|
||||
|
||||
+5
-4
@@ -1,6 +1,6 @@
|
||||
from transitions import Machine
|
||||
import time
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import os
|
||||
import random
|
||||
@@ -69,7 +69,7 @@ class Bot:
|
||||
case "hammerdin" | "paladin":
|
||||
self._char: IChar = Hammerdin(Config().hammerdin, self._pather, self._pickit) #pickit added for diablo
|
||||
case "fohdin":
|
||||
self._char: IChar = FoHdin(Config().fohdin, self._pather)
|
||||
self._char: IChar = FoHdin(Config().fohdin, self._pather, self._pickit)
|
||||
case "abyss_lock" | "warlock":
|
||||
self._char: IChar = AbyssLock(Config().abyss_lock, self._pather)
|
||||
case "fire_lock":
|
||||
@@ -211,7 +211,8 @@ class Bot:
|
||||
Logger.info(f"{Config().general['name']} is now pausing")
|
||||
self._game_stats.pause_timer()
|
||||
while self._pausing:
|
||||
time.sleep(0.2)
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.2, 0.24)
|
||||
if not self._stopping:
|
||||
self.trigger(name, **kwargs)
|
||||
|
||||
@@ -352,7 +353,7 @@ class Bot:
|
||||
if Config().char["runs_per_stash"]:
|
||||
need_inspect |= (self._game_stats._run_counter - 1) % Config().char["runs_per_stash"] == 0
|
||||
if need_inspect:
|
||||
img = personal.open()
|
||||
img = personal.open_inventory()
|
||||
# Update TP, ID, key needs
|
||||
if self._game_stats._game_counter == 1:
|
||||
self._use_id_tome = common.tome_state(img, 'id')[0] is not None
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
import time
|
||||
import random
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar, CharacterCapabilities
|
||||
from pather import Pather
|
||||
from logger import Logger
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import random
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from health_manager import get_panel_check_paused, set_panel_check_paused
|
||||
from inventory.personal import inspect_items
|
||||
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab, convert_abs_to_screen
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char.amazon import Amazon
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar, CharacterCapabilities
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
@@ -192,7 +192,7 @@ class Barbarian(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar,CharacterCapabilities
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
@@ -141,7 +141,7 @@ class Basic(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
@@ -189,7 +189,7 @@ class Basic_Ranged(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import keyboard
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import keyboard
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
@@ -207,7 +207,7 @@ class Bone_Necro(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
+4
-4
@@ -4,12 +4,12 @@ import time
|
||||
import cv2
|
||||
import math
|
||||
from item import consumables
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import numpy as np
|
||||
from char.capabilities import CharacterCapabilities
|
||||
from ui_manager import is_visible, wait_until_visible
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait, cut_roi, is_in_roi, color_filter, arc_spread
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
@@ -100,7 +100,7 @@ class IChar:
|
||||
return False
|
||||
else:
|
||||
mouse.move(pos[0], pos[1])
|
||||
time.sleep(0.1)
|
||||
wait(0.1, 0.12)
|
||||
mouse.click(button="left")
|
||||
|
||||
#Current logic only sets force_run if we previously teled.
|
||||
@@ -454,7 +454,7 @@ class IChar:
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
print(f"Get on D2R screen and press F11 when ready")
|
||||
keyboard.wait("f11")
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import keyboard
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import keyboard
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
import template_finder
|
||||
from template_finder import TemplateMatch
|
||||
@@ -857,7 +857,7 @@ class Necro(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import random
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from health_manager import get_panel_check_paused, set_panel_check_paused
|
||||
from inventory.personal import inspect_items
|
||||
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab, convert_abs_to_screen
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char.paladin import Paladin
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import random
|
||||
import time
|
||||
|
||||
@@ -12,7 +12,7 @@ from pather import Pather, Location
|
||||
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab
|
||||
from target_detect import get_visible_targets
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait
|
||||
|
||||
class Hammerdin(Paladin):
|
||||
@@ -1325,7 +1325,7 @@ class Hammerdin(Paladin):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
import time
|
||||
import random
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar, CharacterCapabilities
|
||||
from pather import Pather
|
||||
from logger import Logger
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import keyboard
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import keyboard
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
@@ -541,7 +541,7 @@ class Poison_Necro(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from char.sorceress import Sorceress
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait, rotate_vec, unit_vector
|
||||
import random
|
||||
@@ -256,7 +256,7 @@ class BlizzSorc(Sorceress):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from char.sorceress import Sorceress
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait, rotate_vec, unit_vector
|
||||
import random
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import time
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from char.sorceress import Sorceress
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait
|
||||
import random
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from char.sorceress import Sorceress
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait, rotate_vec, unit_vector
|
||||
import random
|
||||
@@ -241,7 +241,7 @@ class LightSorc(Sorceress):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from pather import Pather
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import numpy as np
|
||||
from char.sorceress import Sorceress
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait
|
||||
from pather import Location
|
||||
@@ -100,7 +100,7 @@ class NovaSorc(Sorceress):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from pather import Pather
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from typing import Callable
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
import template_finder
|
||||
from pather import Pather
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar
|
||||
from pather import Pather
|
||||
from logger import Logger
|
||||
@@ -237,7 +237,7 @@ class Trapsin(IChar):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
from config import Config
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import random
|
||||
import time
|
||||
|
||||
@@ -12,7 +12,7 @@ from pather import Pather, Location
|
||||
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab
|
||||
from target_detect import get_visible_targets
|
||||
from ui import skills
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait
|
||||
|
||||
class AbyssLock(Warlock):
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import random
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from health_manager import get_panel_check_paused, set_panel_check_paused
|
||||
from inventory.personal import inspect_items
|
||||
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab, convert_abs_to_screen
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char.warlock import Warlock
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import random
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import numpy as np
|
||||
|
||||
from health_manager import get_panel_check_paused, set_panel_check_paused
|
||||
from inventory.personal import inspect_items
|
||||
from screen import convert_abs_to_monitor, convert_screen_to_abs, grab, convert_abs_to_screen
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char.warlock import Warlock
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui import skills
|
||||
import time
|
||||
import random
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from char import IChar, CharacterCapabilities
|
||||
from pather import Pather
|
||||
from logger import Logger
|
||||
|
||||
+5
-3
@@ -1,12 +1,13 @@
|
||||
import time
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from logger import Logger
|
||||
import template_finder
|
||||
from screen import grab
|
||||
from char import IChar
|
||||
from config import Config
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait
|
||||
from item import consumables
|
||||
|
||||
@@ -15,9 +16,10 @@ class Chest:
|
||||
def __init__(self, char: IChar, template: str = None):
|
||||
self._char = char
|
||||
self._folder_name = "chests"
|
||||
self._assets_path = Path(__file__).parent.parent / "assets"
|
||||
# load all templates
|
||||
self._templates = []
|
||||
for filename in os.listdir(f'assets/{self._folder_name}/{template}'):
|
||||
for filename in os.listdir(f'{self._assets_path}/{self._folder_name}/{template}'):
|
||||
filename = filename.lower()
|
||||
if filename.endswith('.png'):
|
||||
chest = filename[:-4].upper()
|
||||
@@ -60,7 +62,7 @@ class Chest:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
print("Move to d2r window and press f11")
|
||||
|
||||
@@ -2,7 +2,7 @@ from copy import deepcopy
|
||||
from email.mime import base
|
||||
import time
|
||||
import cv2
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
import json
|
||||
import screen
|
||||
|
||||
@@ -48,7 +48,7 @@ def get_hovered_item(image: np.ndarray, model = "hover-eng_inconsolata_inv_th_fa
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
from screen import start_detecting_window, stop_detecting_window, grab
|
||||
from d2r_image import processing as d2r_image
|
||||
|
||||
@@ -625,7 +625,7 @@ def build_d2_items(items_by_quality: dict) -> GroundItemList | None:
|
||||
return ground_item_list
|
||||
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
from screen import start_detecting_window, grab, stop_detecting_window
|
||||
start_detecting_window()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from utils.misc import wait
|
||||
from screen import grab
|
||||
from config import Config
|
||||
from utils.custom_mouse import mouse
|
||||
import keyboard
|
||||
from input_layer import mouse
|
||||
from input_layer import keyboard
|
||||
import cv2
|
||||
from logger import Logger
|
||||
import time
|
||||
@@ -42,7 +42,6 @@ class DeathManager:
|
||||
# first wait a bit to make sure health manager is done with its chicken stuff which obviously failed
|
||||
if self._callback is not None:
|
||||
self._callback()
|
||||
self._callback = None
|
||||
# clean up key presses that might be pressed
|
||||
keyboard.release(Config().char["stand_still"])
|
||||
wait(0.1, 0.2)
|
||||
@@ -51,7 +50,7 @@ class DeathManager:
|
||||
mouse.release(button="right")
|
||||
wait(0.1, 0.2)
|
||||
mouse.release(button="left")
|
||||
time.sleep(1)
|
||||
wait(1, 1.5)
|
||||
if is_visible(ScreenObjects.MainMenu):
|
||||
# in this case chicken executed and left the game, but we were still dead.
|
||||
return True
|
||||
@@ -66,7 +65,7 @@ class DeathManager:
|
||||
Logger.info("Start Death monitoring")
|
||||
while self._do_monitor:
|
||||
if self._died: continue
|
||||
time.sleep(self._loop_delay) # no need to do this too frequent, when we died we are not in a hurry...
|
||||
wait(self._loop_delay, self._loop_delay * 1.5) # no need to do this too frequent, when we died we are not in a hurry...
|
||||
# Wait until the flag is reset by main.py
|
||||
if self._died: continue
|
||||
self.handle_death_screen()
|
||||
|
||||
+37
-12
@@ -1,7 +1,7 @@
|
||||
import threading
|
||||
import time
|
||||
import cv2
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
|
||||
from utils.auto_settings import check_settings
|
||||
from bot import Bot
|
||||
@@ -14,7 +14,7 @@ from logger import Logger
|
||||
from messages import Messenger
|
||||
from screen import grab, get_offset_state
|
||||
from utils.restart import restart_game, safe_exit
|
||||
from utils.misc import kill_thread, set_d2r_always_on_top, restore_d2r_window_visibility
|
||||
from utils.misc import kill_thread, cooperative_shutdown, set_d2r_always_on_top, restore_d2r_window_visibility, wait
|
||||
|
||||
|
||||
class GameController:
|
||||
@@ -25,7 +25,7 @@ class GameController:
|
||||
self.death_manager = None
|
||||
self.death_monitor_thread = None
|
||||
self.game_recovery = None
|
||||
self.game_stats = None
|
||||
self.game_stats = GameStats()
|
||||
self.game_controller_thread = None
|
||||
self.bot_thread = None
|
||||
self.bot = None
|
||||
@@ -37,8 +37,8 @@ class GameController:
|
||||
self.bot_thread.daemon = True
|
||||
self.bot_thread.start()
|
||||
# Register that thread to the death and health manager so they can stop the bot thread if needed
|
||||
self.death_manager.set_callback(lambda: self.bot.stop() or kill_thread(self.bot_thread))
|
||||
self.health_manager.set_callback(lambda: self.bot.stop() or kill_thread(self.bot_thread))
|
||||
self.death_manager.set_callback(lambda: self.bot.stop())
|
||||
self.health_manager.set_callback(lambda: self.bot.stop())
|
||||
do_restart = False
|
||||
messenger = Messenger()
|
||||
force_stopped = False
|
||||
@@ -58,7 +58,12 @@ class GameController:
|
||||
self.game_stats.log_chicken(self.health_manager._last_chicken_screenshot)
|
||||
self.bot._stash_mutex.acquire() #Grab mutex to ensure stashing is not occuring
|
||||
self.bot.stop()
|
||||
kill_thread(self.bot_thread)
|
||||
cooperative_shutdown(
|
||||
self.bot_thread,
|
||||
bot=self.bot,
|
||||
health_manager=self.health_manager,
|
||||
death_manager=self.death_manager,
|
||||
)
|
||||
self.bot._stash_mutex.release()
|
||||
# clean up key presses that might be pressed in the bot_thread after killing
|
||||
keyboard.release(Config().char["stand_still"])
|
||||
@@ -83,7 +88,7 @@ class GameController:
|
||||
else:
|
||||
do_restart = self.game_recovery.go_to_hero_selection()
|
||||
break
|
||||
time.sleep(0.5)
|
||||
wait(0.5, 0.75)
|
||||
self.bot_thread.join()
|
||||
if do_restart:
|
||||
# Reset flags before running a new bot
|
||||
@@ -125,16 +130,36 @@ class GameController:
|
||||
self.start_health_manager_thread()
|
||||
self.start_death_manager_thread()
|
||||
self.game_recovery = GameRecovery(self.death_manager)
|
||||
self.game_stats = GameStats()
|
||||
self.start_game_controller_thread()
|
||||
self.is_running = True
|
||||
|
||||
def stop(self):
|
||||
restore_d2r_window_visibility()
|
||||
if self.death_monitor_thread: kill_thread(self.death_monitor_thread)
|
||||
if self.health_monitor_thread: kill_thread(self.health_monitor_thread)
|
||||
if self.bot_thread: kill_thread(self.bot_thread)
|
||||
if self.game_controller_thread: kill_thread(self.game_controller_thread)
|
||||
if self.death_monitor_thread:
|
||||
cooperative_shutdown(
|
||||
self.death_monitor_thread,
|
||||
death_manager=self.death_manager,
|
||||
)
|
||||
if self.health_monitor_thread:
|
||||
cooperative_shutdown(
|
||||
self.health_monitor_thread,
|
||||
health_manager=self.health_manager,
|
||||
)
|
||||
if self.bot_thread:
|
||||
cooperative_shutdown(
|
||||
self.bot_thread,
|
||||
bot=self.bot,
|
||||
health_manager=self.health_manager,
|
||||
death_manager=self.death_manager,
|
||||
)
|
||||
if self.game_controller_thread:
|
||||
# game_controller_thread runs run_bot() which checks self.bot._stopping
|
||||
cooperative_shutdown(
|
||||
self.game_controller_thread,
|
||||
bot=self.bot,
|
||||
health_manager=self.health_manager,
|
||||
death_manager=self.death_manager,
|
||||
)
|
||||
self.is_running = False
|
||||
|
||||
def setup_screen(self):
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from config import Config
|
||||
from death_manager import DeathManager
|
||||
import time
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from ui_manager import ScreenObjects, is_visible
|
||||
from ui import view, loading
|
||||
from utils.misc import set_d2r_always_on_top
|
||||
from utils.misc import wait
|
||||
|
||||
class GameRecovery:
|
||||
def __init__(self, death_manager: DeathManager):
|
||||
@@ -12,10 +13,10 @@ class GameRecovery:
|
||||
|
||||
def go_to_hero_selection(self):
|
||||
set_d2r_always_on_top()
|
||||
time.sleep(1)
|
||||
wait(1, 1.5)
|
||||
# clean up key presses that might be pressed in the run_thread
|
||||
keyboard.release(Config().char["stand_still"])
|
||||
time.sleep(0.1)
|
||||
wait(0.1, 0.15)
|
||||
keyboard.release(Config().char["show_items"])
|
||||
start = time.time()
|
||||
while (time.time() - start) < 30:
|
||||
@@ -24,25 +25,25 @@ class GameRecovery:
|
||||
while is_loading:
|
||||
is_loading = is_visible(ScreenObjects.Loading)
|
||||
is_loading |= loading.check_for_black_screen()
|
||||
time.sleep(0.5)
|
||||
wait(0.5, 0.75)
|
||||
# lets just see if you might already be at hero selection
|
||||
if is_visible(ScreenObjects.MainMenu):
|
||||
return True
|
||||
# would have been too easy, maybe we have died?
|
||||
if self._death_manager.handle_death_screen():
|
||||
time.sleep(1)
|
||||
wait(1, 1.5)
|
||||
continue
|
||||
# if we are in game, save and exit
|
||||
if is_visible(ScreenObjects.InGame):
|
||||
view.fast_save_and_exit()
|
||||
continue
|
||||
time.sleep(1)
|
||||
wait(1, 1.5)
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from death_manager import DeathManager
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from game_stats import GameStats
|
||||
from transmute import Transmute
|
||||
import threading
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
|
||||
if __name__ == "__main__":
|
||||
stats = GameStats()
|
||||
|
||||
+75
-34
@@ -1,9 +1,10 @@
|
||||
from inventory import belt
|
||||
from pather import Location
|
||||
import cv2
|
||||
from threading import Lock
|
||||
import time
|
||||
import keyboard
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import keyboard
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait
|
||||
from logger import Logger
|
||||
from screen import grab
|
||||
@@ -14,33 +15,15 @@ from ui import view, meters
|
||||
from ui_manager import ScreenObjects, is_visible
|
||||
from random import uniform
|
||||
|
||||
pause_state = True
|
||||
panel_check_paused = False
|
||||
|
||||
def get_pause_state():
|
||||
return pause_state
|
||||
|
||||
def set_pause_state(state: bool):
|
||||
global pause_state
|
||||
prev = get_pause_state()
|
||||
if prev != state:
|
||||
debug_str = "paused" if state else "active"
|
||||
Logger.info(f"Health Manager is now {debug_str}")
|
||||
pause_state = state
|
||||
|
||||
def get_panel_check_paused():
|
||||
return panel_check_paused
|
||||
|
||||
def set_panel_check_paused(state: bool):
|
||||
global panel_check_paused
|
||||
prev = get_panel_check_paused()
|
||||
if prev != state:
|
||||
debug_str = "pausing" if state else "activating"
|
||||
Logger.info(f"Health Manager is now {debug_str} inventory panel check")
|
||||
panel_check_paused = state
|
||||
|
||||
class HealthManager:
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
HealthManager._instance = self
|
||||
self._state_lock = Lock()
|
||||
self._pause_state = True
|
||||
self._panel_check_paused = False
|
||||
self._do_monitor = False
|
||||
self._did_chicken = False
|
||||
self._last_rejuv = time.time()
|
||||
@@ -62,7 +45,31 @@ class HealthManager:
|
||||
|
||||
def reset_chicken_flag(self):
|
||||
self._did_chicken = False
|
||||
set_pause_state(True)
|
||||
self.set_pause_state(True)
|
||||
|
||||
def get_pause_state(self):
|
||||
with self._state_lock:
|
||||
return self._pause_state
|
||||
|
||||
def set_pause_state(self, state: bool):
|
||||
with self._state_lock:
|
||||
prev = self._pause_state
|
||||
if prev != state:
|
||||
debug_str = "paused" if state else "active"
|
||||
Logger.info(f"Health Manager is now {debug_str}")
|
||||
self._pause_state = state
|
||||
|
||||
def get_panel_check_paused(self):
|
||||
with self._state_lock:
|
||||
return self._panel_check_paused
|
||||
|
||||
def set_panel_check_paused(self, state: bool):
|
||||
with self._state_lock:
|
||||
prev = self._panel_check_paused
|
||||
if prev != state:
|
||||
debug_str = "pausing" if state else "activating"
|
||||
Logger.info(f"Health Manager is now {debug_str} inventory panel check")
|
||||
self._panel_check_paused = state
|
||||
|
||||
def _do_chicken(self, img):
|
||||
if self._callback is not None:
|
||||
@@ -78,7 +85,7 @@ class HealthManager:
|
||||
self._last_chicken_screenshot = "./log/screenshots/info/info_debug_chicken_" + time.strftime("%Y%m%d_%H%M%S") + ".png"
|
||||
cv2.imwrite(self._last_chicken_screenshot, img)
|
||||
self._did_chicken = True
|
||||
set_pause_state(True)
|
||||
self.set_pause_state(True)
|
||||
|
||||
def start_monitor(self):
|
||||
Logger.info("Start health monitoring")
|
||||
@@ -91,7 +98,7 @@ class HealthManager:
|
||||
merc_hp_potion_delay = 10.24
|
||||
|
||||
while self._do_monitor:
|
||||
if self._did_chicken or get_pause_state():
|
||||
if self._did_chicken or self.get_pause_state():
|
||||
wait(1)
|
||||
continue
|
||||
fn_start = time.perf_counter()
|
||||
@@ -106,9 +113,11 @@ class HealthManager:
|
||||
#It seems that hit recovery can delay the use of juvs and 15 frames is max recovery time.
|
||||
#To delay two juvs being used back to back, we'll need to wait the recovery time between uses.
|
||||
#15 frames is 0.60 seconds.
|
||||
if last_drink > 0.60:
|
||||
if last_drink > 0.60:
|
||||
if (health_percentage <= Config().char["take_rejuv_potion_health"]) or \
|
||||
(mana_percentage <= Config().char["take_rejuv_potion_mana"]):
|
||||
# Simulate human reaction time (100-300ms)
|
||||
wait(0.1, 0.3)
|
||||
success_drink_rejuv = belt.drink_potion("rejuv", stats=[health_percentage, mana_percentage])
|
||||
#failure to drink juv (likely out of juvs). Perform chicken
|
||||
if not success_drink_rejuv:
|
||||
@@ -134,11 +143,13 @@ class HealthManager:
|
||||
# check health
|
||||
last_drink = time.time() - self._last_health
|
||||
if health_percentage <= Config().char["take_health_potion"] and last_drink > lp_hp_potion_delay:
|
||||
wait(0.1, 0.3) # human reaction time
|
||||
belt.drink_potion("health", stats=[health_percentage, mana_percentage])
|
||||
self._last_health = time.time()
|
||||
# check mana
|
||||
last_drink = time.time() - self._last_mana
|
||||
if mana_percentage <= Config().char["take_mana_potion"] and last_drink > lp_mp_potion_delay:
|
||||
wait(0.1, 0.3) # human reaction time
|
||||
belt.drink_potion("mana", stats=[health_percentage, mana_percentage])
|
||||
self._last_mana = time.time()
|
||||
# check merc
|
||||
@@ -151,12 +162,14 @@ class HealthManager:
|
||||
self._do_chicken(img)
|
||||
continue
|
||||
if Config().char["heal_rejuv_merc"] and (merc_health_percentage <= Config().char["heal_rejuv_merc"] and last_drink > 4.0):
|
||||
wait(0.1, 0.3) # human reaction time
|
||||
belt.drink_potion("rejuv", merc=True, stats=[merc_health_percentage])
|
||||
self._last_merc_heal = time.time()
|
||||
elif Config().char["heal_merc"] and (merc_health_percentage <= Config().char["heal_merc"] and last_drink > merc_hp_potion_delay):
|
||||
wait(0.1, 0.3) # human reaction time
|
||||
belt.drink_potion("health", merc=True, stats=[merc_health_percentage])
|
||||
self._last_merc_heal = time.time()
|
||||
if not get_panel_check_paused() and (is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img)):
|
||||
if not self.get_panel_check_paused() and (is_visible(ScreenObjects.LeftPanel, img) or is_visible(ScreenObjects.RightPanel, img)):
|
||||
Logger.warning(f"Found an open inventory / quest / skill / stats page. Close it.")
|
||||
self._count_panel_detects += 1
|
||||
if self._count_panel_detects >= 2:
|
||||
@@ -166,16 +179,44 @@ class HealthManager:
|
||||
continue
|
||||
common.close()
|
||||
fn_end = time.perf_counter()
|
||||
wait_time = 3/25 - (fn_end - fn_start)
|
||||
# Add timing jitter to avoid perfectly regular polling pattern (anti-cheat)
|
||||
import random
|
||||
base_wait = 3/25 - (fn_end - fn_start)
|
||||
jitter = random.uniform(0.8, 1.2) # ±20% jitter
|
||||
wait_time = max(0.0, base_wait * jitter)
|
||||
if wait_time > 0:
|
||||
wait(wait_time) # wait 3 frames before rechecking
|
||||
wait(wait_time) # wait ~3 frames before rechecking
|
||||
Logger.debug("Stop health monitoring")
|
||||
|
||||
|
||||
# Backwards-compatible module-level functions that delegate to the singleton instance
|
||||
def get_pause_state():
|
||||
"""Backwards-compatible wrapper that delegates to the singleton instance."""
|
||||
if HealthManager._instance is not None:
|
||||
return HealthManager._instance.get_pause_state()
|
||||
return True
|
||||
|
||||
def set_pause_state(state: bool):
|
||||
"""Backwards-compatible wrapper that delegates to the singleton instance."""
|
||||
if HealthManager._instance is not None:
|
||||
HealthManager._instance.set_pause_state(state)
|
||||
|
||||
def get_panel_check_paused():
|
||||
"""Backwards-compatible wrapper that delegates to the singleton instance."""
|
||||
if HealthManager._instance is not None:
|
||||
return HealthManager._instance.get_panel_check_paused()
|
||||
return False
|
||||
|
||||
def set_panel_check_paused(state: bool):
|
||||
"""Backwards-compatible wrapper that delegates to the singleton instance."""
|
||||
if HealthManager._instance is not None:
|
||||
HealthManager._instance.set_panel_check_paused(state)
|
||||
|
||||
|
||||
# Testing: Start dying or losing mana and see if it works
|
||||
if __name__ == "__main__":
|
||||
import threading
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
from health_manager import set_pause_state
|
||||
from screen import start_detecting_window, stop_detecting_window, grab
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
"""
|
||||
Native input layer - replaces `keyboard` and `mouse` (pyclick) libraries.
|
||||
No kernel drivers, no third-party DLLs. Only standard system DLLs (user32, kernel32).
|
||||
|
||||
Import as:
|
||||
from input_layer import keyboard, mouse
|
||||
|
||||
This is a drop-in replacement for the existing `import keyboard` and
|
||||
`from utils.custom_mouse import mouse` patterns.
|
||||
"""
|
||||
from .win_input import (
|
||||
_get_vk, key_down, key_up, key_press, send_key, key_state,
|
||||
mouse_move, mouse_down, mouse_up, mouse_click, mouse_wheel, get_cursor_pos,
|
||||
send_text, VK_MAP
|
||||
)
|
||||
from .mouse_impl import mouse
|
||||
|
||||
class _Keyboard:
|
||||
"""
|
||||
Drop-in replacement for the `keyboard` library.
|
||||
Supports all patterns used in botty: send(), press(), release(), is_pressed(),
|
||||
add_hotkey(), wait(), write(), hook(), pause().
|
||||
|
||||
All key presses include stealth micro-pauses and variable press duration
|
||||
automatically - no changes to existing code needed.
|
||||
"""
|
||||
|
||||
def _stealth_before(self):
|
||||
"""Add micro-pause before key press (stealth)."""
|
||||
try:
|
||||
from utils.stealth import add_micro_pause
|
||||
add_micro_pause()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _stealth_after(self):
|
||||
"""Add micro-pause after key press (stealth)."""
|
||||
try:
|
||||
from utils.stealth import add_micro_pause
|
||||
add_micro_pause()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _stealth_duration(self):
|
||||
"""Get human-like key press duration."""
|
||||
try:
|
||||
from utils.stealth import key_press_duration
|
||||
return key_press_duration()
|
||||
except Exception:
|
||||
import random
|
||||
return random.uniform(0.02, 0.15)
|
||||
|
||||
def _skill_rotation_hesitation(self):
|
||||
"""Add hesitation before skill casts (Tier 2 behavior stealth)."""
|
||||
try:
|
||||
from utils.stealth import skill_rotation_hesitation
|
||||
from utils.misc import wait as _wait
|
||||
_wait(skill_rotation_hesitation(), skill_rotation_hesitation() * 1.2)
|
||||
except Exception:
|
||||
import random
|
||||
from utils.misc import wait as _wait
|
||||
_wait(random.uniform(0.08, 0.3), random.uniform(0.08, 0.3) * 1.2)
|
||||
|
||||
def _maybe_skill_mistake(self, key: str):
|
||||
"""
|
||||
1-2% chance of pressing a random skill key first before the intended one
|
||||
(Tier 2 behavior stealth - simulates miscasting).
|
||||
Returns True if a mistake was made and corrected.
|
||||
"""
|
||||
try:
|
||||
from utils.stealth import should_correct_skill_mistake
|
||||
if not should_correct_skill_mistake():
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# Press a random skill key first (simulates miscast)
|
||||
import random
|
||||
try:
|
||||
from config import Config
|
||||
skill_keys = list(range(ord('1'), ord('0') + 1)) # Keys 1-0
|
||||
wrong_key = chr(random.choice(skill_keys))
|
||||
if wrong_key == key:
|
||||
wrong_key = chr(random.choice(skill_keys))
|
||||
except Exception:
|
||||
wrong_key = random.choice(['1', '2', '3', '4', '5'])
|
||||
|
||||
# Send the wrong key
|
||||
wrong_vk = _get_vk(wrong_key)
|
||||
if wrong_vk is not None:
|
||||
key_down(wrong_vk)
|
||||
from utils.misc import wait as _wait
|
||||
_wait(self._stealth_duration(), self._stealth_duration() * 1.2)
|
||||
key_up(wrong_vk)
|
||||
|
||||
return True
|
||||
|
||||
def send(self, key: str, do_press: bool = True, do_release: bool = True, delay=None):
|
||||
"""
|
||||
Send a key press event with stealth timing.
|
||||
|
||||
Supports combo keys like 'shift + a', 'ctrl + alt + del'.
|
||||
Supports do_release=False (hold key) and do_press=False (release-only).
|
||||
"""
|
||||
self._stealth_before()
|
||||
|
||||
# Handle combo keys (e.g. 'shift + a', 'ctrl + alt + del')
|
||||
if ' + ' in key or '+' in key:
|
||||
# Parse combo
|
||||
parts = [p.strip() for p in key.replace('+', ' + ').split(' + ') if p.strip()]
|
||||
modifiers = []
|
||||
final_key = parts[-1]
|
||||
for p in parts[:-1]:
|
||||
vk = _get_vk(p)
|
||||
if vk is not None:
|
||||
modifiers.append(vk)
|
||||
vk = _get_vk(final_key)
|
||||
if vk is None:
|
||||
raise ValueError(f"Unknown key: {key}")
|
||||
if do_press:
|
||||
for mvk in modifiers:
|
||||
key_down(mvk)
|
||||
key_down(vk)
|
||||
if do_release:
|
||||
if do_press:
|
||||
from utils.misc import wait as _wait
|
||||
_wait(self._stealth_duration(), self._stealth_duration() * 1.2)
|
||||
key_up(vk)
|
||||
for mvk in reversed(modifiers):
|
||||
key_up(mvk)
|
||||
self._stealth_after()
|
||||
return
|
||||
|
||||
# Single key
|
||||
vk = _get_vk(key)
|
||||
if vk is None:
|
||||
# Try as a single character
|
||||
if len(key) == 1:
|
||||
vk = ord(key.upper()) if key.isalpha() else ord(key)
|
||||
else:
|
||||
raise ValueError(f"Unknown key: {key}")
|
||||
|
||||
if delay is not None:
|
||||
from utils.misc import wait as _wait
|
||||
_wait(delay, delay * 1.2)
|
||||
|
||||
# Tier 2 stealth: skill hesitation (only for skill hotkeys 1-0)
|
||||
if vk in range(ord('1'), ord('0') + 1) or key in ('1', '2', '3', '4', '5', '6', '7', '8', '9', '0'):
|
||||
self._skill_rotation_hesitation()
|
||||
self._maybe_skill_mistake(key)
|
||||
|
||||
if do_press:
|
||||
key_down(vk)
|
||||
if do_release:
|
||||
from utils.misc import wait as _wait
|
||||
_wait(self._stealth_duration(), self._stealth_duration() * 1.2)
|
||||
if do_release:
|
||||
key_up(vk)
|
||||
|
||||
self._stealth_after()
|
||||
|
||||
def press(self, key: str):
|
||||
"""Press a key down (without releasing)."""
|
||||
self.send(key, do_press=True, do_release=False)
|
||||
|
||||
def release(self, key: str):
|
||||
"""Release a key (without pressing)."""
|
||||
self.send(key, do_press=False, do_release=True)
|
||||
|
||||
def is_pressed(self, key: str) -> bool:
|
||||
"""Check if a key is currently pressed."""
|
||||
vk = _get_vk(key)
|
||||
if vk is None:
|
||||
return False
|
||||
return key_state(key)
|
||||
|
||||
def add_hotkey(self, key: str, callback, suppress: bool = False):
|
||||
"""Register a global hotkey callback."""
|
||||
from .hotkey import add_hotkey as _add_hotkey
|
||||
_add_hotkey(key, callback, suppress=suppress)
|
||||
|
||||
def wait(self, key: str = None, suppress: bool = False):
|
||||
"""Block until the key is pressed. If key is None, wait for any key."""
|
||||
from .hotkey import wait as _wait
|
||||
return _wait(key, suppress=suppress)
|
||||
|
||||
def write(self, text: str, delay: float = 0.05):
|
||||
"""Type text character by character."""
|
||||
send_text(text, delay=delay)
|
||||
|
||||
def hook(self, callback, suppress: bool = False):
|
||||
"""Register a callback for all key events (dev tools only)."""
|
||||
from .hotkey import hook as _hook
|
||||
return _hook(callback, suppress=suppress)
|
||||
|
||||
def pause(self, seconds: float = 0, suppress: bool = False):
|
||||
"""Pause key processing (used by npc_auto_label.py)."""
|
||||
from .hotkey import pause as _pause
|
||||
return _pause(seconds, suppress)
|
||||
|
||||
# Singleton keyboard object - matches the `keyboard` module API
|
||||
keyboard = _Keyboard()
|
||||
|
||||
# Exports for convenience
|
||||
__all__ = ["keyboard", "mouse"]
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Global hotkey polling via GetAsyncKeyState.
|
||||
Replaces keyboard.add_hotkey(), keyboard.wait(), keyboard.is_pressed().
|
||||
No kernel driver - pure user-mode polling thread.
|
||||
"""
|
||||
import threading
|
||||
import time
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
from .win_input import _get_vk, VK_MAP, user32
|
||||
|
||||
class _HotkeyManager:
|
||||
def __init__(self):
|
||||
self._callbacks = {} # vk -> [(key_str, callback), ...]
|
||||
self._running = False
|
||||
self._thread = None
|
||||
self._suppress = {} # vk -> bool (suppress key after callback fires)
|
||||
self._lock = threading.Lock()
|
||||
self._suppressed = set() # vks currently being held down in suppress mode
|
||||
|
||||
def _ensure_running(self):
|
||||
if not self._running:
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._poll_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _poll_loop(self):
|
||||
"""Poll GetAsyncKeyState for registered hotkeys."""
|
||||
while self._running:
|
||||
with self._lock:
|
||||
items = list(self._callbacks.items())
|
||||
for vk, entries in items:
|
||||
for key_str, cb in entries:
|
||||
if vk in self._suppressed:
|
||||
# Key already held and suppressed
|
||||
continue
|
||||
state = user32.GetAsyncKeyState(vk)
|
||||
if state & 0x8000: # key is down
|
||||
try:
|
||||
cb()
|
||||
except Exception:
|
||||
pass
|
||||
if self._suppress.get(vk, False):
|
||||
self._suppressed.add(vk)
|
||||
|
||||
# Wait for key release if suppressed
|
||||
if self._suppressed:
|
||||
still_suppressed = set()
|
||||
for vk in self._suppressed:
|
||||
state = user32.GetAsyncKeyState(vk)
|
||||
if not (state & 0x8000):
|
||||
# Key released
|
||||
pass
|
||||
else:
|
||||
still_suppressed.add(vk)
|
||||
self._suppressed = still_suppressed
|
||||
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.018, 0.024) # ~50Hz polling with jitter (anti-cheat: non-perfect timing)
|
||||
|
||||
def add_hotkey(self, key: str, callback, suppress: bool = False):
|
||||
vk = _get_vk(key)
|
||||
if vk is None:
|
||||
raise ValueError(f"Unknown key for hotkey: {key}")
|
||||
with self._lock:
|
||||
if vk not in self._callbacks:
|
||||
self._callbacks[vk] = []
|
||||
self._callbacks[vk].append((key, callback))
|
||||
self._suppress[vk] = suppress
|
||||
self._ensure_running()
|
||||
|
||||
def remove_hotkey(self, key: str, callback=None):
|
||||
vk = _get_vk(key)
|
||||
if vk is None:
|
||||
return
|
||||
with self._lock:
|
||||
if vk in self._callbacks:
|
||||
if callback is None:
|
||||
del self._callbacks[vk]
|
||||
else:
|
||||
self._callbacks[vk] = [
|
||||
(k, cb) for k, cb in self._callbacks[vk] if cb != callback
|
||||
]
|
||||
if not self._callbacks[vk]:
|
||||
del self._callbacks[vk]
|
||||
if not any(vk in d for d in [self._callbacks, self._suppress]):
|
||||
self._suppress.pop(vk, None)
|
||||
|
||||
def is_pressed(self, key: str) -> bool:
|
||||
vk = _get_vk(key)
|
||||
if vk is None:
|
||||
return False
|
||||
state = user32.GetAsyncKeyState(vk)
|
||||
return bool(state & 0x8000)
|
||||
|
||||
def wait(self, key: str = None, suppress: bool = False):
|
||||
"""Block until the key is pressed. If key is None, wait for any key."""
|
||||
if key is not None:
|
||||
vk = _get_vk(key)
|
||||
if vk is None:
|
||||
raise ValueError(f"Unknown key: {key}")
|
||||
while True:
|
||||
state = user32.GetAsyncKeyState(vk)
|
||||
if state & 0x8000:
|
||||
if suppress:
|
||||
while user32.GetAsyncKeyState(vk) & 0x8000:
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.008, 0.012)
|
||||
return
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.018, 0.024)
|
||||
else:
|
||||
# Wait for any key
|
||||
while True:
|
||||
for vk in range(1, 256):
|
||||
if user32.GetAsyncKeyState(vk) & 0x8000:
|
||||
return vk
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.018, 0.024)
|
||||
|
||||
def hook(self, callback, suppress: bool = False):
|
||||
"""Register a callback for all key events.
|
||||
This is a simplified version - polls all known keys and calls callback.
|
||||
Used by gen_ocr_samples.py and node_recorder.py (dev tools only)."""
|
||||
def _poll_all():
|
||||
while self._running:
|
||||
for vk in range(1, 256):
|
||||
state = user32.GetAsyncKeyState(vk)
|
||||
if state & 0x8000:
|
||||
event = {"event_type": "down", "name": None, "scan_code": 0}
|
||||
try:
|
||||
callback(event)
|
||||
except Exception:
|
||||
pass
|
||||
if suppress:
|
||||
while user32.GetAsyncKeyState(vk) & 0x8000:
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.01, 0.01)
|
||||
break
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.02, 0.02)
|
||||
self._ensure_running()
|
||||
# Run hook in its own thread
|
||||
t = threading.Thread(target=_poll_all, daemon=True)
|
||||
t.start()
|
||||
|
||||
def pause(self, seconds: float = 0, suppress: bool = False):
|
||||
"""Pause key processing for a duration. Used by npc_auto_label.py."""
|
||||
from utils.misc import wait as _wait
|
||||
_wait(seconds, seconds)
|
||||
|
||||
# Singleton
|
||||
_hotkey_manager = _HotkeyManager()
|
||||
|
||||
def add_hotkey(key: str, callback, suppress: bool = False):
|
||||
_hotkey_manager.add_hotkey(key, callback, suppress=suppress)
|
||||
|
||||
def remove_hotkey(key: str, callback=None):
|
||||
_hotkey_manager.remove_hotkey(key, callback)
|
||||
|
||||
def is_pressed(key: str) -> bool:
|
||||
_hotkey_manager._ensure_running()
|
||||
return _hotkey_manager.is_pressed(key)
|
||||
|
||||
def wait(key: str = None, suppress: bool = False):
|
||||
_hotkey_manager._ensure_running()
|
||||
return _hotkey_manager.wait(key, suppress=suppress)
|
||||
|
||||
def hook(callback, suppress: bool = False):
|
||||
return _hotkey_manager.hook(callback, suppress=suppress)
|
||||
|
||||
def pause(seconds: float = 0, suppress: bool = False):
|
||||
return _hotkey_manager.pause(seconds, suppress)
|
||||
@@ -1,26 +1,20 @@
|
||||
# Mostly copied from: https://github.com/patrikoss/pyclick
|
||||
import mouse as _mouse
|
||||
import os
|
||||
if os.name == 'nt':
|
||||
from mouse import _winmouse
|
||||
else:
|
||||
# Linux stub — _winmouse is only used in _move_to() which wraps calls in os.name checks
|
||||
class _winmouse:
|
||||
@staticmethod
|
||||
def move_to(x, y):
|
||||
_mouse.move(x, y)
|
||||
import pytweening
|
||||
import numpy as np
|
||||
import random
|
||||
"""
|
||||
Human-like mouse movement with native Windows API.
|
||||
Replaces custom_mouse.py (which used the `mouse` / pyclick library with mousetool.dll).
|
||||
Same public API - drop-in replacement.
|
||||
"""
|
||||
import math
|
||||
import random
|
||||
import time
|
||||
import threading
|
||||
from concurrent.futures import Future
|
||||
import screen
|
||||
from config import Config
|
||||
from utils.misc import is_in_roi
|
||||
from logger import Logger
|
||||
import template_finder
|
||||
import numpy as np
|
||||
import pytweening
|
||||
|
||||
from .win_input import mouse_move as _native_move, mouse_click as _native_click
|
||||
from .win_input import mouse_down as _native_down, mouse_up as _native_up
|
||||
from .win_input import get_cursor_pos as _native_get_pos
|
||||
from .win_input import mouse_wheel as _native_wheel
|
||||
|
||||
|
||||
def isNumeric(val):
|
||||
return isinstance(val, (float, int, np.int32, np.int64, np.float32, np.float64))
|
||||
@@ -31,26 +25,20 @@ def isListOfPoints(l):
|
||||
try:
|
||||
isPoint = lambda p: ((len(p) == 2) and isNumeric(p[0]) and isNumeric(p[1]))
|
||||
return all(map(isPoint, l))
|
||||
except (KeyError, TypeError) as e:
|
||||
except (KeyError, TypeError):
|
||||
return False
|
||||
|
||||
class BezierCurve():
|
||||
@staticmethod
|
||||
def binomial(n, k):
|
||||
"""Returns the binomial coefficient "n choose k" """
|
||||
return math.factorial(n) / float(math.factorial(k) * math.factorial(n - k))
|
||||
|
||||
@staticmethod
|
||||
def bernsteinPolynomialPoint(x, i, n):
|
||||
"""Calculate the i-th component of a bernstein polynomial of degree n"""
|
||||
return BezierCurve.binomial(n, i) * (x ** i) * ((1 - x) ** (n - i))
|
||||
|
||||
@staticmethod
|
||||
def bernsteinPolynomial(points):
|
||||
"""
|
||||
Given list of control points, returns a function, which given a point [0,1] returns
|
||||
a point in the bezier curve described by these points
|
||||
"""
|
||||
def bern(t):
|
||||
n = len(points) - 1
|
||||
x = y = 0
|
||||
@@ -63,10 +51,6 @@ class BezierCurve():
|
||||
|
||||
@staticmethod
|
||||
def curvePoints(n, points):
|
||||
"""
|
||||
Given list of control points, returns n points in the bezier curve,
|
||||
described by these points
|
||||
"""
|
||||
curvePoints = []
|
||||
bernstein_polynomial = BezierCurve.bernsteinPolynomial(points)
|
||||
for i in range(n):
|
||||
@@ -75,22 +59,12 @@ class BezierCurve():
|
||||
return curvePoints
|
||||
|
||||
class HumanCurve():
|
||||
"""
|
||||
Generates a human-like mouse curve starting at given source point,
|
||||
and finishing in a given destination point
|
||||
"""
|
||||
|
||||
def __init__(self, fromPoint, toPoint, **kwargs):
|
||||
self.fromPoint = fromPoint
|
||||
self.toPoint = toPoint
|
||||
self.points = self.generateCurve(**kwargs)
|
||||
|
||||
def generateCurve(self, **kwargs):
|
||||
"""
|
||||
Generates a curve according to the parameters specified below.
|
||||
You can override any of the below parameters. If no parameter is
|
||||
passed, the default value is used.
|
||||
"""
|
||||
offsetBoundaryX = kwargs.get("offsetBoundaryX", 100)
|
||||
offsetBoundaryY = kwargs.get("offsetBoundaryY", 100)
|
||||
leftBoundary = kwargs.get("leftBoundary", min(self.fromPoint[0], self.toPoint[0])) - offsetBoundaryX
|
||||
@@ -104,23 +78,15 @@ class HumanCurve():
|
||||
tween = kwargs.get("tweening", pytweening.easeOutQuad)
|
||||
targetPoints = kwargs.get("targetPoints", 10)
|
||||
|
||||
internalKnots = self.generateInternalKnots(leftBoundary,rightBoundary, \
|
||||
internalKnots = self.generateInternalKnots(leftBoundary, rightBoundary,
|
||||
downBoundary, upBoundary, knotsCount)
|
||||
points = self.generatePoints(internalKnots)
|
||||
points = self.distortPoints(points, distortionMean, distortionStdev, distortionFrequency)
|
||||
points = self.tweenPoints(points, tween, targetPoints)
|
||||
return points
|
||||
|
||||
def generateInternalKnots(self, \
|
||||
leftBoundary, rightBoundary, \
|
||||
downBoundary, upBoundary,\
|
||||
knotsCount):
|
||||
"""
|
||||
Generates the internal knots used during generation of bezier curvePoints
|
||||
or any interpolation function. The points are taken at random from
|
||||
a surface delimited by given boundaries.
|
||||
Exactly knotsCount internal knots are randomly generated.
|
||||
"""
|
||||
def generateInternalKnots(self, leftBoundary, rightBoundary,
|
||||
downBoundary, upBoundary, knotsCount):
|
||||
if not (isNumeric(leftBoundary) and isNumeric(rightBoundary) and
|
||||
isNumeric(downBoundary) and isNumeric(upBoundary)):
|
||||
raise ValueError("Boundaries must be numeric")
|
||||
@@ -133,32 +99,20 @@ class HumanCurve():
|
||||
|
||||
knotsX = np.random.choice(range(leftBoundary, rightBoundary), size=knotsCount)
|
||||
knotsY = np.random.choice(range(downBoundary, upBoundary), size=knotsCount)
|
||||
knots = list(zip(knotsX, knotsY))
|
||||
return knots
|
||||
return list(zip(knotsX, knotsY))
|
||||
|
||||
def generatePoints(self, knots):
|
||||
"""
|
||||
Generates bezier curve points on a curve, according to the internal
|
||||
knots passed as parameter.
|
||||
"""
|
||||
if not isListOfPoints(knots):
|
||||
raise ValueError("knots must be valid list of points")
|
||||
|
||||
midPtsCnt = max( \
|
||||
abs(self.fromPoint[0] - self.toPoint[0]), \
|
||||
abs(self.fromPoint[1] - self.toPoint[1]), \
|
||||
midPtsCnt = max(
|
||||
abs(self.fromPoint[0] - self.toPoint[0]),
|
||||
abs(self.fromPoint[1] - self.toPoint[1]),
|
||||
2)
|
||||
knots = [self.fromPoint] + knots + [self.toPoint]
|
||||
return BezierCurve.curvePoints(midPtsCnt, knots)
|
||||
|
||||
def distortPoints(self, points, distortionMean, distortionStdev, distortionFrequency):
|
||||
"""
|
||||
Distorts the curve described by (x,y) points, so that the curve is
|
||||
not ideally smooth.
|
||||
Distortion happens by randomly, according to normal distribution,
|
||||
adding an offset to some of the points.
|
||||
"""
|
||||
if not(isNumeric(distortionMean) and isNumeric(distortionStdev) and \
|
||||
if not (isNumeric(distortionMean) and isNumeric(distortionStdev) and
|
||||
isNumeric(distortionFrequency)):
|
||||
raise ValueError("Distortions must be numeric")
|
||||
if not isListOfPoints(points):
|
||||
@@ -167,78 +121,41 @@ class HumanCurve():
|
||||
raise ValueError("distortionFrequency must be in range [0,1]")
|
||||
|
||||
distorted = []
|
||||
for i in range(1, len(points)-1):
|
||||
x,y = points[i]
|
||||
for i in range(1, len(points) - 1):
|
||||
x, y = points[i]
|
||||
delta = np.random.normal(distortionMean, distortionStdev) if \
|
||||
random.random() < distortionFrequency else 0
|
||||
distorted += (x,y+delta),
|
||||
distorted += (x, y + delta),
|
||||
distorted = [points[0]] + distorted + [points[-1]]
|
||||
return distorted
|
||||
|
||||
def tweenPoints(self, points, tween, targetPoints):
|
||||
"""
|
||||
Chooses a number of points(targetPoints) from the list(points)
|
||||
according to tweening function(tween).
|
||||
This function in fact controls the velocity of mouse movement
|
||||
"""
|
||||
if not isListOfPoints(points):
|
||||
raise ValueError("points must be valid list of points")
|
||||
if not isinstance(targetPoints, int) or targetPoints < 2:
|
||||
raise ValueError("targetPoints must be an integer greater or equal to 2")
|
||||
|
||||
# tween is a function that takes a float 0..1 and returns a float 0..1
|
||||
res = []
|
||||
for i in range(targetPoints):
|
||||
index = int(tween(float(i)/(targetPoints-1)) * (len(points)-1))
|
||||
index = int(tween(float(i) / (targetPoints - 1)) * (len(points) - 1))
|
||||
res += points[index],
|
||||
return res
|
||||
|
||||
|
||||
class mouse:
|
||||
@staticmethod
|
||||
def sleep(duration, get_now=time.perf_counter):
|
||||
time.sleep(duration)
|
||||
# now = get_now()
|
||||
# end = now + duration
|
||||
# while now < end:
|
||||
# now = get_now()
|
||||
"""
|
||||
Drop-in replacement for the `mouse` (pyclick) library.
|
||||
Same public API as custom_mouse.py, but uses native SendInput instead of mousetool.dll.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _move_to(x, y, absolute=True, duration=0):
|
||||
"""
|
||||
Moves the mouse. If `absolute`, to position (x, y), otherwise move relative
|
||||
to the current position. If `duration` is non-zero, animates the movement.
|
||||
"""
|
||||
x = int(x)
|
||||
y = int(y)
|
||||
def sleep(duration):
|
||||
from utils.misc import wait as _wait
|
||||
_wait(duration, duration * 1.2)
|
||||
|
||||
# Requires an extra system call on Linux, but `move_relative` is measured
|
||||
# in millimiters so we would lose precision.
|
||||
position_x, position_y = _mouse.get_position()
|
||||
|
||||
if not absolute:
|
||||
x = position_x + x
|
||||
y = position_y + y
|
||||
|
||||
if duration:
|
||||
start_x = position_x
|
||||
start_y = position_y
|
||||
dx = x - start_x
|
||||
dy = y - start_y
|
||||
|
||||
if dx == 0 and dy == 0:
|
||||
mouse.sleep(duration)
|
||||
else:
|
||||
# 120 movements per second.
|
||||
# Round and keep float to ensure float division in Python 2
|
||||
steps = max(1.0, float(int(duration * 120.0)))
|
||||
for i in range(int(steps)+1):
|
||||
mouse.move(start_x + dx*i/steps, start_y + dy*i/steps)
|
||||
mouse.sleep(duration/steps)
|
||||
else:
|
||||
_winmouse.move_to(x, y)
|
||||
|
||||
def move(x, y, absolute: bool = True, randomize: int | tuple[int, int] = 5, delay_factor: tuple[float, float] = [0.4, 0.6]):
|
||||
from_point = _mouse.get_position()
|
||||
@staticmethod
|
||||
def move(x, y, absolute: bool = True, randomize: int | tuple[int, int] = 5,
|
||||
delay_factor: tuple[float, float] = [0.4, 0.6]):
|
||||
from_point = _native_get_pos()
|
||||
dist = math.dist((x, y), from_point)
|
||||
offsetBoundaryX = max(10, int(0.08 * dist))
|
||||
offsetBoundaryY = max(10, int(0.08 * dist))
|
||||
@@ -260,11 +177,11 @@ class mouse:
|
||||
|
||||
# Apply human curve complexity from stealth config
|
||||
try:
|
||||
from config import Config
|
||||
complexity = Config().stealth.get("human_curve_complexity", 1.0)
|
||||
except Exception:
|
||||
complexity = 1.0
|
||||
|
||||
# Scale distortion parameters based on complexity
|
||||
distortionMean = 1 * complexity
|
||||
distortionStdev = 1 * complexity
|
||||
distortionFreq = min(0.8, 0.4 * complexity)
|
||||
@@ -283,88 +200,95 @@ class mouse:
|
||||
delta = duration / len(human_curve.points)
|
||||
|
||||
for point in human_curve.points:
|
||||
_mouse.move(point[0], point[1], duration=delta)
|
||||
_native_move(int(point[0]), int(point[1]))
|
||||
mouse.sleep(delta)
|
||||
|
||||
@staticmethod
|
||||
def stealth_move(x, y, absolute: bool = True, randomize: int | tuple[int, int] = 5, delay_factor: tuple[float, float] = [0.4, 0.6]):
|
||||
"""Move with full stealth chain: pre-movement pause → randomized position → endpoint wobble."""
|
||||
def stealth_move(x, y, absolute: bool = True, randomize: int | tuple[int, int] = 5,
|
||||
delay_factor: tuple[float, float] = [0.4, 0.6]):
|
||||
"""Move with full stealth chain: pre-movement pause -> randomized position -> endpoint wobble."""
|
||||
try:
|
||||
from utils.stealth import randomize_click_position, add_micro_pause, endpoint_wobble
|
||||
rx, ry = randomize_click_position(x, y)
|
||||
add_micro_pause()
|
||||
except Exception:
|
||||
from logger import Logger
|
||||
Logger.warning("[Stealth] randomize_click_position/add_micro_pause failed, using manual fallback")
|
||||
try:
|
||||
from config import Config
|
||||
variance = Config().stealth["click_variance"]
|
||||
except Exception:
|
||||
variance = 0
|
||||
rx = x + random.randint(-variance, variance)
|
||||
ry = y + random.randint(-variance, variance)
|
||||
mouse.move(rx, ry, absolute=absolute, randomize=5 + variance, delay_factor=delay_factor)
|
||||
# Endpoint wobble: micro-adjustment after arrival, before click
|
||||
try:
|
||||
from utils.stealth import endpoint_wobble
|
||||
wx, wy = endpoint_wobble(rx, ry)
|
||||
# Nudge mouse a few pixels — imperceptible to the eye, breaks perfect stillness
|
||||
_mouse.move(wx, wy)
|
||||
_native_move(wx, wy)
|
||||
except Exception:
|
||||
from logger import Logger
|
||||
Logger.warning("[Stealth] endpoint_wobble failed, skipping micro-adjustment")
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _is_clicking_safe():
|
||||
# Because of reports that botty lost equiped items, let's check if the inventory is open, and if it is, restrict the mouse move
|
||||
mouse_pos = screen.convert_monitor_to_screen(_mouse.get_position())
|
||||
is_inventory_open = template_finder.search(
|
||||
"INVENTORY_GOLD_BTN",
|
||||
screen.grab(),
|
||||
threshold=0.8,
|
||||
roi=Config().ui_roi["gold_btn"],
|
||||
use_grayscale=True
|
||||
).valid
|
||||
if is_inventory_open:
|
||||
is_in_equipped_area = is_in_roi(Config().ui_roi["equipped_inventory_area"], mouse_pos)
|
||||
is_in_restricted_inventory_area = is_in_roi(Config().ui_roi["restricted_inventory_area"], mouse_pos)
|
||||
if is_in_restricted_inventory_area or is_in_equipped_area:
|
||||
Logger.error("Mouse wants to click in equipped area. Cancel action.")
|
||||
return False
|
||||
try:
|
||||
import screen
|
||||
from config import Config
|
||||
from utils.misc import is_in_roi
|
||||
import template_finder
|
||||
mouse_pos = screen.convert_monitor_to_screen(_native_get_pos())
|
||||
is_inventory_open = template_finder.search(
|
||||
"INVENTORY_GOLD_BTN",
|
||||
screen.grab(),
|
||||
threshold=0.8,
|
||||
roi=Config().ui_roi["gold_btn"],
|
||||
use_grayscale=True
|
||||
).valid
|
||||
if is_inventory_open:
|
||||
is_in_equipped_area = is_in_roi(Config().ui_roi["equipped_inventory_area"], mouse_pos)
|
||||
is_in_restricted_inventory_area = is_in_roi(Config().ui_roi["restricted_inventory_area"], mouse_pos)
|
||||
if is_in_restricted_inventory_area or is_in_equipped_area:
|
||||
from logger import Logger
|
||||
Logger.error("Mouse wants to click in equipped area. Cancel action.")
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def click(button):
|
||||
if button != "left" or mouse._is_clicking_safe():
|
||||
# Human arrival-to-click delay: 50-800ms before actually pressing
|
||||
try:
|
||||
from utils.stealth import apply_click_delay
|
||||
apply_click_delay()
|
||||
except Exception:
|
||||
time.sleep(random.uniform(0.05, 0.3))
|
||||
_mouse.click(button)
|
||||
from logger import Logger
|
||||
Logger.warning("[Stealth] apply_click_delay failed, falling back to manual delay")
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.05, 0.3)
|
||||
_native_click(button)
|
||||
|
||||
@staticmethod
|
||||
def press(button):
|
||||
if button != "left" or mouse._is_clicking_safe():
|
||||
_mouse.press(button)
|
||||
_native_down(button)
|
||||
|
||||
@staticmethod
|
||||
def release(button):
|
||||
_mouse.release(button)
|
||||
_native_up(button)
|
||||
|
||||
@staticmethod
|
||||
def get_position():
|
||||
return _mouse.get_position()
|
||||
return _native_get_pos()
|
||||
|
||||
@staticmethod
|
||||
def wheel(delta):
|
||||
_mouse.wheel(delta)
|
||||
_native_wheel(delta)
|
||||
|
||||
@staticmethod
|
||||
def async_move(x, y, absolute=True, randomize=5, delay_factor=[0.4, 0.6]):
|
||||
"""
|
||||
Non-blocking mouse move. Returns immediately with a Future-like object.
|
||||
|
||||
:return: A dict with:
|
||||
- 'done()': callable returning bool
|
||||
- 'wait(timeout=None)': blocks until move completes or timeout
|
||||
"""
|
||||
result = {"_done": False, "_lock": threading.Lock()}
|
||||
|
||||
def _run():
|
||||
@@ -385,26 +309,14 @@ class mouse:
|
||||
while not done():
|
||||
if deadline is not None and time.monotonic() >= deadline:
|
||||
return False
|
||||
time.sleep(0.01)
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.01, 0.012)
|
||||
return True
|
||||
|
||||
return {"done": done, "wait": wait}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
screen.find_and_set_window_position()
|
||||
move_to_ok = screen.convert_screen_to_monitor((400, 420))
|
||||
move_to_bad_equiped = screen.convert_screen_to_monitor((900, 170))
|
||||
move_to_bad_inventory = screen.convert_screen_to_monitor((1200, 400))
|
||||
mouse.move(*move_to_ok)
|
||||
mouse.click("left")
|
||||
time.sleep(1)
|
||||
mouse.move(*move_to_bad_equiped)
|
||||
mouse.click("left")
|
||||
time.sleep(1)
|
||||
mouse.move(*move_to_bad_inventory)
|
||||
mouse.click("left")
|
||||
print("Mouse module loaded OK (native SendInput, no mousetool.dll)")
|
||||
pos = mouse.get_position()
|
||||
print(f"Cursor at: {pos}")
|
||||
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
Native Windows input via ctypes SendInput API.
|
||||
No kernel drivers, no third-party DLLs. Only standard system DLLs (user32, kernel32).
|
||||
"""
|
||||
import ctypes
|
||||
import time
|
||||
import struct
|
||||
from ctypes import wintypes
|
||||
|
||||
# ─── user32 constants ───
|
||||
|
||||
# INPUT_TYPE
|
||||
INPUT_MOUSE = 0
|
||||
INPUT_KEYBOARD = 1
|
||||
INPUT_HARDWARE = 2
|
||||
|
||||
# MOUSEEVENTF flags
|
||||
MOUSEEVENTF_MOVE = 0x0001
|
||||
MOUSEEVENTF_LEFTDOWN = 0x0002
|
||||
MOUSEEVENTF_LEFTUP = 0x0004
|
||||
MOUSEEVENTF_RIGHTDOWN = 0x0008
|
||||
MOUSEEVENTF_RIGHTUP = 0x0010
|
||||
MOUSEEVENTF_MIDDLEDOWN = 0x0020
|
||||
MOUSEEVENTF_MIDDLEUP = 0x0040
|
||||
MOUSEEVENTF_XDOWN = 0x0080
|
||||
MOUSEEVENTF_XUP = 0x0100
|
||||
MOUSEEVENTF_WHEEL = 0x0800
|
||||
MOUSEEVENTF_ABSOLUTE = 0x8000
|
||||
MOUSEEVENTF_HWHEEL = 0x01000
|
||||
|
||||
# KEYEVENTF flags
|
||||
KEYEVENTF_EXTENDEDKEY = 0x0001
|
||||
KEYEVENTF_KEYUP = 0x0002
|
||||
KEYEVENTF_UNICODE = 0x0004
|
||||
KEYEVENTF_SCANCODE = 0x0008
|
||||
|
||||
# ─── structs ───
|
||||
|
||||
class MOUSEINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dx", wintypes.LONG),
|
||||
("dy", wintypes.LONG),
|
||||
("mouseData", wintypes.DWORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("time", wintypes.DWORD),
|
||||
("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
|
||||
]
|
||||
|
||||
class KEYBDINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("wVk", wintypes.WORD),
|
||||
("wScan", wintypes.WORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("time", wintypes.DWORD),
|
||||
("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
|
||||
]
|
||||
|
||||
class HARDWAREINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("uMsg", wintypes.DWORD),
|
||||
("wParamL", wintypes.WORD),
|
||||
("wParamH", wintypes.WORD),
|
||||
]
|
||||
|
||||
class INPUTUNION(ctypes.Union):
|
||||
_fields_ = [
|
||||
("mi", MOUSEINPUT),
|
||||
("ki", KEYBDINPUT),
|
||||
("hi", HARDWAREINPUT),
|
||||
]
|
||||
|
||||
class INPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("type", wintypes.DWORD),
|
||||
("union", INPUTUNION),
|
||||
]
|
||||
|
||||
INPUT_ARRAY = ctypes.ARRAY(INPUT, 64)
|
||||
|
||||
# ─── load user32 ───
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
|
||||
user32.SendInput.restype = wintypes.UINT
|
||||
user32.SendInput.argtypes = [wintypes.UINT, ctypes.POINTER(INPUT), ctypes.c_int]
|
||||
|
||||
user32.GetAsyncKeyState.restype = wintypes.SHORT
|
||||
user32.GetAsyncKeyState.argtypes = [wintypes.WORD]
|
||||
|
||||
user32.GetCursorPos.restype = wintypes.BOOL
|
||||
user32.GetCursorPos.argtypes = [ctypes.POINTER(wintypes.POINT)]
|
||||
|
||||
user32.GetSystemMetrics.restype = wintypes.INT
|
||||
user32.GetSystemMetrics.argtypes = [wintypes.INT]
|
||||
|
||||
user32.MapVirtualKeyW.restype = wintypes.UINT
|
||||
user32.MapVirtualKeyW.argtypes = [wintypes.UINT, wintypes.UINT]
|
||||
|
||||
# ─── VK code mapping ───
|
||||
|
||||
# Extended keys (require KEYEVENTF_EXTENDEDKEY flag)
|
||||
EXTENDED_KEYS = {
|
||||
0x27, # numpad /
|
||||
0x91, # right ctrl
|
||||
0x9A, # right shift
|
||||
0xB5, # numpad *
|
||||
0xB8, # right alt
|
||||
0xB9, # right win
|
||||
0xBA, # apps key (menu)
|
||||
0xC1, # numpad enter
|
||||
0xC7, # numpad .
|
||||
0xC8, # snap
|
||||
}
|
||||
|
||||
# Common key name -> VK code
|
||||
VK_MAP = {
|
||||
"left": 0x25, "right": 0x27, "up": 0x26, "down": 0x28,
|
||||
"enter": 0x0D, "return": 0x0D, "space": 0x20, "escape": 0x1B, "esc": 0x1B,
|
||||
"backspace": 0x08, "tab": 0x09, "delete": 0x2E, "end": 0x23, "home": 0x24,
|
||||
"insert": 0x2D, "pageup": 0x21, "pagedown": 0x22,
|
||||
"f1": 0x70, "f2": 0x71, "f3": 0x72, "f4": 0x73,
|
||||
"f5": 0x74, "f6": 0x75, "f7": 0x76, "f8": 0x77,
|
||||
"f9": 0x78, "f10": 0x79, "f11": 0x7A, "f12": 0x7B,
|
||||
"shift": 0xA0, "left shift": 0xA0, "right shift": 0xA1,
|
||||
"ctrl": 0xA2, "left ctrl": 0xA2, "right ctrl": 0xA3,
|
||||
"lctrl": 0xA2, "rctrl": 0xA3, "ctrl_l": 0xA2, "ctrl_r": 0xA3,
|
||||
"alt": 0xA4, "left alt": 0xA4, "right alt": 0xA5,
|
||||
"lalt": 0xA4, "ralt": 0xA5, "alt_l": 0xA4, "alt_r": 0xA5,
|
||||
"caps lock": 0x14, "capslock": 0x14,
|
||||
"num lock": 0x90, "numlock": 0x90,
|
||||
"scroll lock": 0x91, "scrolllock": 0x91,
|
||||
"print screen": 0x2C, "printscr": 0x2C,
|
||||
"pause": 0x13,
|
||||
"lwin": 0x5B, "rwin": 0x5C, "win": 0x5B,
|
||||
"apps": 0x5D,
|
||||
"numpad0": 0x60, "numpad1": 0x61, "numpad2": 0x62,
|
||||
"numpad3": 0x63, "numpad4": 0x64, "numpad5": 0x65,
|
||||
"numpad6": 0x66, "numpad7": 0x67, "numpad8": 0x68,
|
||||
"numpad9": 0x69,
|
||||
"numpad.": 0x6E, "numpad/": 0x6F,
|
||||
"numpad*": 0x6B, "numpad+": 0x6C,
|
||||
"numpad-": 0x6D,
|
||||
"volume_up": 0xAF, "volume_down": 0xAE, "volume_mute": 0xAD,
|
||||
"media_next": 0xB0, "media_prev": 0xB1, "media_stop": 0xB2, "media_play_pause": 0xB3,
|
||||
"back": 0xA6, "forward": 0xA7,
|
||||
}
|
||||
|
||||
# ─── core input functions ───
|
||||
|
||||
def _send_input(inp: INPUT):
|
||||
"""Send a single INPUT event."""
|
||||
arr = INPUT_ARRAY()
|
||||
arr[0] = inp
|
||||
result = user32.SendInput(1, arr, ctypes.sizeof(INPUT))
|
||||
if result != 1:
|
||||
raise RuntimeError(f"SendInput failed, returned {result}")
|
||||
|
||||
def _make_keyboard_input(vk: int, flags: int = 0, scan: int = 0):
|
||||
"""Create a KEYBDINPUT struct."""
|
||||
i = INPUT()
|
||||
i.type = INPUT_KEYBOARD
|
||||
i.union.ki.wVk = vk
|
||||
i.union.ki.wScan = scan
|
||||
i.union.ki.dwFlags = flags
|
||||
i.union.ki.dwExtraInfo = None
|
||||
return i
|
||||
|
||||
def _make_mouse_input(flags: int, dx: int = 0, dy: int = 0, data: int = 0):
|
||||
"""Create a MOUSEINPUT struct."""
|
||||
i = INPUT()
|
||||
i.type = INPUT_MOUSE
|
||||
i.union.mi.dx = dx
|
||||
i.union.mi.dy = dy
|
||||
i.union.mi.mouseData = data
|
||||
i.union.mi.dwFlags = flags | MOUSEEVENTF_ABSOLUTE
|
||||
i.union.mi.dwExtraInfo = None
|
||||
return i
|
||||
|
||||
def _get_vk(key_name: str):
|
||||
"""Resolve a key name to its VK code."""
|
||||
key_name = key_name.strip().lower()
|
||||
if key_name in VK_MAP:
|
||||
return VK_MAP[key_name]
|
||||
# Single character
|
||||
if len(key_name) == 1:
|
||||
vk = ord(key_name)
|
||||
# Digit keys 0-9 map to VK_0 (0x0B) through VK_9 (0x13)
|
||||
if '0' <= key_name <= '9':
|
||||
return 0x0B + ord(key_name) - ord('0')
|
||||
# Letter keys map directly to their ASCII value (uppercase)
|
||||
if 'a' <= key_name <= 'z':
|
||||
return ord(key_name.upper())
|
||||
return vk
|
||||
# Try pywin32 style (vk_key)
|
||||
return None
|
||||
|
||||
def _is_extended(vk: int) -> bool:
|
||||
return vk in EXTENDED_KEYS
|
||||
|
||||
def _get_scan(vk: int) -> int:
|
||||
"""Get scan code for a VK."""
|
||||
return user32.MapVirtualKeyW(vk, 0)
|
||||
|
||||
def key_down(vk: int):
|
||||
"""Press a key down."""
|
||||
extended = KEYEVENTF_EXTENDEDKEY if _is_extended(vk) else 0
|
||||
scan = _get_scan(vk) if vk else 0
|
||||
_send_input(_make_keyboard_input(vk, extended, scan))
|
||||
|
||||
def key_up(vk: int):
|
||||
"""Release a key."""
|
||||
extended = KEYEVENTF_EXTENDEDKEY if _is_extended(vk) else 0
|
||||
scan = _get_scan(vk) if vk else 0
|
||||
_send_input(_make_keyboard_input(vk, extended | KEYEVENTF_KEYUP, scan))
|
||||
|
||||
def key_press(vk: int):
|
||||
"""Press and release a key."""
|
||||
key_down(vk)
|
||||
key_up(vk)
|
||||
|
||||
def send_key(key: str, down=True, up=True):
|
||||
"""Press and/or release a key by name."""
|
||||
vk = _get_vk(key)
|
||||
if vk is None:
|
||||
raise ValueError(f"Unknown key: {key}")
|
||||
if down:
|
||||
key_down(vk)
|
||||
if up:
|
||||
key_up(vk)
|
||||
|
||||
def key_state(key: str) -> bool:
|
||||
"""Check if a key is currently pressed (via GetAsyncKeyState)."""
|
||||
vk = _get_vk(key)
|
||||
if vk is None:
|
||||
return False
|
||||
state = user32.GetAsyncKeyState(vk)
|
||||
return bool(state & 0x8000)
|
||||
|
||||
# ─── mouse ───
|
||||
|
||||
def get_screen_size():
|
||||
"""Get screen dimensions for absolute mouse positioning."""
|
||||
return user32.GetSystemMetrics(0), user32.GetSystemMetrics(1)
|
||||
|
||||
def mouse_move(x: int, y: int):
|
||||
"""Move mouse to absolute screen position (uses absolute SendInput)."""
|
||||
screen_w, screen_h = get_screen_size()
|
||||
# SendInput expects normalized absolute coordinates 0-65535
|
||||
norm_x = int(x * 65535 / screen_w)
|
||||
norm_y = int(y * 65535 / screen_h)
|
||||
_send_input(_make_mouse_input(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, norm_x, norm_y))
|
||||
|
||||
def mouse_down(button: str = "left"):
|
||||
"""Press mouse button down."""
|
||||
if button == "left":
|
||||
_send_input(_make_mouse_input(MOUSEEVENTF_LEFTDOWN))
|
||||
elif button == "right":
|
||||
_send_input(_make_mouse_input(MOUSEEVENTF_RIGHTDOWN))
|
||||
elif button == "middle":
|
||||
_send_input(_make_mouse_input(MOUSEEVENTF_MIDDLEDOWN))
|
||||
else:
|
||||
raise ValueError(f"Unknown button: {button}")
|
||||
|
||||
def mouse_up(button: str = "left"):
|
||||
"""Release mouse button."""
|
||||
if button == "left":
|
||||
_send_input(_make_mouse_input(MOUSEEVENTF_LEFTUP))
|
||||
elif button == "right":
|
||||
_send_input(_make_mouse_input(MOUSEEVENTF_RIGHTUP))
|
||||
elif button == "middle":
|
||||
_send_input(_make_mouse_input(MOUSEEVENTF_MIDDLEUP))
|
||||
else:
|
||||
raise ValueError(f"Unknown button: {button}")
|
||||
|
||||
def mouse_click(button: str = "left"):
|
||||
"""Click (press + release) a mouse button."""
|
||||
mouse_down(button)
|
||||
mouse_up(button)
|
||||
|
||||
def mouse_wheel(delta: int):
|
||||
"""Scroll mouse wheel (positive = up, negative = down).
|
||||
Delta is in "clicks" — each click is 120 units."""
|
||||
wheel_delta = delta * 120
|
||||
_send_input(_make_mouse_input(MOUSEEVENTF_WHEEL, 0, 0, wheel_delta))
|
||||
|
||||
def get_cursor_pos():
|
||||
"""Get cursor position as (x, y)."""
|
||||
from ctypes import wintypes as wt
|
||||
p = wt.POINT()
|
||||
if user32.GetCursorPos(ctypes.byref(p)):
|
||||
return (p.x, p.y)
|
||||
return (0, 0)
|
||||
|
||||
# ─── text input ───
|
||||
|
||||
def send_text(text: str, delay: float = 0.05):
|
||||
"""Send text character by character."""
|
||||
for ch in text:
|
||||
vk = _get_vk(ch)
|
||||
if vk is None:
|
||||
continue
|
||||
# Determine if shift is needed
|
||||
if ch.isupper() and ch.lower() != ch:
|
||||
# Need shift for uppercase letters
|
||||
key_down(0xA0) # left shift
|
||||
key_press(vk)
|
||||
key_up(0xA0)
|
||||
elif not ch.isalnum() and ch != ' ':
|
||||
# Shift needed for most punctuation
|
||||
key_down(0xA0)
|
||||
key_press(vk)
|
||||
key_up(0xA0)
|
||||
else:
|
||||
key_press(vk)
|
||||
from utils.misc import wait as _wait
|
||||
_wait(delay, delay * 1.2)
|
||||
@@ -6,11 +6,11 @@ import template_finder
|
||||
from inventory import common, personal
|
||||
from ui import view
|
||||
from ui_manager import is_visible, wait_until_visible, ScreenObjects, wait_until_hidden
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import cut_roi, wait, color_filter
|
||||
from config import Config
|
||||
from screen import convert_abs_to_monitor, convert_monitor_to_screen, convert_screen_to_monitor, grab
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
|
||||
def open(img: np.ndarray = None) -> np.ndarray:
|
||||
@@ -147,7 +147,7 @@ def fill_up_belt_from_inventory(num_loot_columns: int):
|
||||
Fill up your belt with pots from the inventory e.g. after death. It will open and close invetory by itself!
|
||||
:param num_loot_columns: Number of columns used for loot from left
|
||||
"""
|
||||
img = personal.open()
|
||||
img = personal.open_inventory()
|
||||
pot_positions = []
|
||||
for column, row in itertools.product(range(num_loot_columns), range(4)):
|
||||
center_pos, slot_img = common.get_slot_pos_and_img(img, column, row)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from config import Config
|
||||
import cv2
|
||||
import numpy as np
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import itertools
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from ui_manager import detect_screen_object, ScreenObjects, is_visible, wait_until_hidden, center_mouse
|
||||
import template_finder
|
||||
from utils.misc import wait, trim_black, color_filter, cut_roi
|
||||
@@ -274,7 +274,7 @@ def select_stash_page(idx: int):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from config import Config
|
||||
from screen import start_detecting_window, stop_detecting_window
|
||||
from utils.misc import color_filter
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import itertools
|
||||
from game_stats import GameStats
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import cv2
|
||||
import time
|
||||
import numpy as np
|
||||
@@ -11,7 +11,7 @@ from logger import Logger
|
||||
from config import Config
|
||||
import template_finder
|
||||
from utils.misc import wait, is_in_roi, mask_by_roi
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from inventory import stash, common, vendor
|
||||
from ui import view
|
||||
from ui_manager import detect_screen_object, is_visible, select_screen_object_match, wait_until_visible, ScreenObjects, center_mouse, wait_for_update
|
||||
@@ -57,7 +57,7 @@ def inventory_has_items(img: np.ndarray = None, close_window = False) -> bool:
|
||||
:param img: Img from screen.grab() with inventory open
|
||||
:return: Bool if inventory still has items or not
|
||||
"""
|
||||
img = open(img)
|
||||
img = open_inventory(img)
|
||||
if img is not None:
|
||||
items=False
|
||||
for column, row in itertools.product(range(0, Config().char["num_loot_columns"]), range(4)):
|
||||
@@ -165,7 +165,7 @@ def stash_all_items(items: list = None):
|
||||
Logger.debug("Done stashing")
|
||||
return items
|
||||
|
||||
def open(img: np.ndarray = None) -> np.ndarray:
|
||||
def open_inventory(img: np.ndarray = None) -> np.ndarray:
|
||||
img = grab() if img is None else img
|
||||
if not common.inventory_is_open():
|
||||
keyboard.send(Config().char["inventory_screen"])
|
||||
@@ -174,7 +174,7 @@ def open(img: np.ndarray = None) -> np.ndarray:
|
||||
return None
|
||||
keyboard.send(Config().char["inventory_screen"])
|
||||
if not wait_until_visible(ScreenObjects.RightPanel, 1).valid:
|
||||
Logger.error(f"personal.open(): Failed to open inventory")
|
||||
Logger.error(f"personal.open_inventory(): Failed to open inventory")
|
||||
return None
|
||||
img = grab()
|
||||
return img
|
||||
@@ -207,7 +207,7 @@ def inspect_items(inp_img: np.ndarray = None, close_window: bool = True, game_st
|
||||
:param img: Image in which the item is searched (item details should be visible)
|
||||
"""
|
||||
center_mouse()
|
||||
img = open(inp_img)
|
||||
img = open_inventory(inp_img)
|
||||
if img is None:
|
||||
Logger.error("personal.inspect_items(): unable to get inventory image")
|
||||
return []
|
||||
@@ -433,7 +433,7 @@ def transfer_items(items: list, action: str = "drop", img: np.ndarray = None) ->
|
||||
return items
|
||||
|
||||
def update_tome_key_needs(img: np.ndarray = None, item_type: str = "tp") -> bool:
|
||||
img = open(img)
|
||||
img = open_inventory(img)
|
||||
if img is None:
|
||||
Logger.debug(f"update_tome_key_needs: failed to get inventory image")
|
||||
return False
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from math import floor
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import template_finder
|
||||
from config import Config
|
||||
import numpy as np
|
||||
from utils.misc import wait
|
||||
from screen import grab
|
||||
from logger import Logger
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from ui_manager import center_mouse, is_visible, select_screen_object_match, wait_until_visible, ScreenObjects
|
||||
from inventory import personal, common, stash
|
||||
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ from enum import Enum
|
||||
from numpy import ndarray
|
||||
import cv2
|
||||
import json
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
@@ -19,7 +19,7 @@ from bnip.actions import should_pickup
|
||||
from bnip.NTIPAliasType import NTIPAliasType as NTIP_TYPES
|
||||
from screen import grab, convert_abs_to_monitor
|
||||
from ui_manager import ScreenObjects, is_visible
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ if __name__ == "__main__":
|
||||
from char.sorceress import LightSorc
|
||||
from char.paladin import Hammerdin
|
||||
from pather import Pather
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from logger import Logger
|
||||
from screen import start_detecting_window, stop_detecting_window
|
||||
|
||||
|
||||
+14
-5
@@ -11,7 +11,7 @@ if sys.platform == "win32":
|
||||
os.add_dll_directory(_conda_dll_dir)
|
||||
|
||||
from dataclasses import dataclass
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from beautifultable import BeautifulTable
|
||||
import logging
|
||||
import traceback
|
||||
@@ -90,8 +90,13 @@ def main():
|
||||
# Auto-launch D2R if not already running
|
||||
from utils.restart import process_exists, restart_game
|
||||
if not process_exists("D2R.exe"):
|
||||
Logger.info("D2R is not running, launching...")
|
||||
restart_game(Config().general["d2r_path"], Config().advanced_options["launch_options"])
|
||||
if Config().general.get("bnet_name", "") and Config().general.get("bnet_pass", ""):
|
||||
Logger.info("D2R is not running, launching with auto-login...")
|
||||
restart_game(Config().general["d2r_path"], Config().advanced_options["launch_options"])
|
||||
else:
|
||||
Logger.info("D2R is not running. No auto-login configured - please launch D2R and log in, then press the resume key to start.")
|
||||
# Don't auto-launch without credentials; user will start manually
|
||||
# Still show the UI and wait for resume key
|
||||
else:
|
||||
Logger.info("D2R is already running")
|
||||
|
||||
@@ -126,5 +131,9 @@ if __name__ == "__main__":
|
||||
main()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
print("Press Enter to exit ...")
|
||||
input()
|
||||
# In --noconsole builds, skip input() - the hotkey-based exit_key handles shutdown
|
||||
try:
|
||||
if __import__('sys').stdin and __import__('sys').stdin.isatty():
|
||||
input()
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
import time
|
||||
import os
|
||||
import numpy as np
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import template_finder
|
||||
from config import Config
|
||||
from screen import grab
|
||||
from ui_manager import ScreenObjects, center_mouse, is_visible, wait_until_hidden
|
||||
from utils.misc import color_filter, wait
|
||||
from logger import Logger
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from math import sqrt
|
||||
|
||||
class Npc:
|
||||
@@ -322,7 +322,7 @@ if __name__ == "__main__":
|
||||
from screen import grab
|
||||
from config import Config
|
||||
import os
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
open_npc_menu(Npc.MALAH)
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
import math
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
import os
|
||||
import random
|
||||
import cv2
|
||||
import numpy as np
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait # for stash/shrine tele cancel detection in traverse node
|
||||
from utils.misc import is_in_roi
|
||||
from config import Config
|
||||
@@ -728,7 +728,7 @@ if __name__ == "__main__":
|
||||
cv2.imshow("debug", display_img)
|
||||
cv2.waitKey(1)
|
||||
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from screen import start_detecting_window, stop_detecting_window, grab
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or os._exit(1))
|
||||
keyboard.wait("f11")
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ class Arcane:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from game_stats import GameStats
|
||||
import os
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ from item.pickit import PickIt
|
||||
import template_finder
|
||||
from town.town_manager import TownManager, A4
|
||||
from utils.misc import wait
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from screen import convert_abs_to_monitor, grab
|
||||
from ui_manager import detect_screen_object, ScreenObjects
|
||||
from ui import skills, loading, waypoint
|
||||
@@ -505,7 +505,7 @@ class Diablo:
|
||||
return (Location.A4_DIABLO_END, self._picked_up_items)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from game_stats import GameStats
|
||||
import os
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
|
||||
+2
-2
@@ -10,10 +10,10 @@ from collections import OrderedDict
|
||||
import time
|
||||
import random
|
||||
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from screen import grab, convert_abs_to_monitor
|
||||
from target_detect import get_visible_targets
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ from item.pickit import PickIt
|
||||
import template_finder
|
||||
from town.town_manager import TownManager, A4
|
||||
from utils.misc import wait
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from screen import convert_abs_to_monitor, grab
|
||||
from ui_manager import detect_screen_object, ScreenObjects
|
||||
from ui import skills, loading, waypoint
|
||||
@@ -326,7 +326,7 @@ class Vizier:
|
||||
return (Location.A4_DIABLO_A_LAYOUTCHECK, self._picked_up_items)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from game_stats import GameStats
|
||||
import os
|
||||
keyboard.add_hotkey('f12', lambda: os._exit(1))
|
||||
|
||||
+5
-1
@@ -88,8 +88,12 @@ def grab(force_new: bool = False) -> np.ndarray:
|
||||
monitor_roi["width"] = m0["width"]
|
||||
monitor_roi["height"] = m0["height"]
|
||||
# with 25fps we have 40ms per frame. If we check for 20ms range to make sure we can still get each frame if we want.
|
||||
# Added 5-10% jitter to the cache interval to break perfectly regular timing patterns.
|
||||
with cached_img_lock:
|
||||
if not force_new and cached_img is not None and last_grab is not None and (time.perf_counter() - last_grab) < 0.02:
|
||||
import random
|
||||
jitter = 0.9 + random.random() * 0.2 # 0.9x to 1.1x
|
||||
cache_threshold = 0.02 * jitter
|
||||
if not force_new and cached_img is not None and last_grab is not None and (time.perf_counter() - last_grab) < cache_threshold:
|
||||
return cached_img
|
||||
else:
|
||||
last_grab = time.perf_counter()
|
||||
|
||||
+10
-10
@@ -3,7 +3,7 @@ import os
|
||||
import time
|
||||
import math
|
||||
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import numpy as np
|
||||
|
||||
from screen import grab, convert_screen_to_monitor
|
||||
@@ -11,7 +11,7 @@ from config import Config
|
||||
from logger import Logger
|
||||
from npc_manager import Npc, open_npc_menu, press_npc_btn
|
||||
import template_finder
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait, load_template
|
||||
|
||||
from messages import Messenger
|
||||
@@ -94,7 +94,7 @@ class AnyaShopper:
|
||||
if ias_glove.valid:
|
||||
self.ias_gloves_seen += 1
|
||||
mouse.move(*ias_glove.center_monitor)
|
||||
time.sleep(0.5)
|
||||
wait(0.5, 0.6)
|
||||
glove_dialog_img = grab()
|
||||
|
||||
if self.look_for_plus_3_gloves is True:
|
||||
@@ -110,7 +110,7 @@ class AnyaShopper:
|
||||
self._messenger.send_message("Bought awesome IAS/+3 jav gloves!")
|
||||
Logger.info("IAS/jav gloves bought!")
|
||||
self.gloves_bought += 1
|
||||
time.sleep(1)
|
||||
wait(1, 1.2)
|
||||
else:
|
||||
gg_gloves = template_finder.search(
|
||||
ref=load_template(asset_folder + "plus3_ma_skills.jpg"),
|
||||
@@ -123,7 +123,7 @@ class AnyaShopper:
|
||||
self._messenger.send_message("Bought awesome IAS/+3 ma gloves!")
|
||||
Logger.info("IAS/ma gloves bought!")
|
||||
self.gloves_bought += 1
|
||||
time.sleep(1)
|
||||
wait(1, 1.2)
|
||||
|
||||
else:
|
||||
if self.look_for_plus_2_gloves is True:
|
||||
@@ -140,13 +140,13 @@ class AnyaShopper:
|
||||
self._messenger.send_message("Bought some decent IAS/+2 gloves")
|
||||
Logger.info("IAS/+2 gloves bought!")
|
||||
self.gloves_bought += 1
|
||||
time.sleep(1)
|
||||
wait(1, 1.2)
|
||||
|
||||
def shop_loop(self):
|
||||
while True:
|
||||
open_npc_menu(Npc.ANYA)
|
||||
press_npc_btn(Npc.ANYA, "trade")
|
||||
time.sleep(0.5)
|
||||
wait(0.5, 0.6)
|
||||
img = grab()
|
||||
|
||||
#For some reason the glove image varies slightly depending on where it is located in the shop.
|
||||
@@ -204,7 +204,7 @@ class AnyaShopper:
|
||||
|
||||
Logger.info(f"Trap Claws (score: {trap_score}) bought!")
|
||||
self.claws_bought += 1
|
||||
time.sleep(1)
|
||||
wait(1, 1.2)
|
||||
|
||||
if melee_score > self.melee_claw_min_score and self.look_for_melee_claws is True:
|
||||
# pick it up
|
||||
@@ -213,7 +213,7 @@ class AnyaShopper:
|
||||
self._messenger.send_message(f"Bought some mad melee Claws (score: {melee_score})")
|
||||
Logger.info(f"Melee Claws (score: {melee_score}) bought!")
|
||||
self.claws_bought += 1
|
||||
time.sleep(1)
|
||||
wait(1, 1.2)
|
||||
|
||||
# Done with this shopping round
|
||||
self.reset_shop()
|
||||
@@ -227,7 +227,7 @@ class AnyaShopper:
|
||||
break
|
||||
else:
|
||||
mouse.move(800, 450, randomize=50, delay_factor=[0.7, 0.7])
|
||||
time.sleep(2.5)
|
||||
wait(2.5, 3.0)
|
||||
while 1:
|
||||
success = self.select_by_template("A5_RED_PORTAL")
|
||||
success &= wait_for_loading_screen(2)
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@ import math
|
||||
import random
|
||||
from typing import Callable
|
||||
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import numpy as np
|
||||
|
||||
from screen import convert_screen_to_monitor, grab, convert_abs_to_monitor, convert_screen_to_abs, convert_monitor_to_screen
|
||||
@@ -13,7 +13,7 @@ from config import Config
|
||||
from logger import Logger
|
||||
from npc_manager import Npc, open_npc_menu, press_npc_btn
|
||||
import template_finder
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class DrognanShopper:
|
||||
while True:
|
||||
open_npc_menu(Npc.DROGNAN)
|
||||
press_npc_btn(Npc.DROGNAN, "trade")
|
||||
time.sleep(0.1)
|
||||
wait(0.1, 0.12)
|
||||
img = grab()
|
||||
|
||||
if self.look_for_scepters is True:
|
||||
@@ -110,7 +110,7 @@ class DrognanShopper:
|
||||
mouse.click(button="right")
|
||||
Logger.info(f"Item bought!")
|
||||
self.items_bought += 1
|
||||
time.sleep(1)
|
||||
wait(1, 1.2)
|
||||
|
||||
self.items_evaluated += 1
|
||||
|
||||
|
||||
+4
-3
@@ -8,8 +8,9 @@ if sys.platform == "win32":
|
||||
from beautifultable import BeautifulTable
|
||||
import logging
|
||||
import traceback
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import time
|
||||
from utils.misc import wait
|
||||
from shop.anya import AnyaShopper
|
||||
from shop.drognan import DrognanShopper
|
||||
from config import Config
|
||||
@@ -46,13 +47,13 @@ def main():
|
||||
merchant = AnyaShopper()
|
||||
merchant.run()
|
||||
break
|
||||
time.sleep(0.02)
|
||||
wait(0.02, 0.024)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# To avoid cmd just closing down, except any errors and add a input() to the end
|
||||
try:
|
||||
start_detecting_window()
|
||||
time.sleep(2)
|
||||
wait(2, 2.4)
|
||||
main()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -14,13 +14,6 @@ FILTER_RANGES=[
|
||||
{"erode": 1, "blur": 3, "lh": 110, "ls": 169, "lv": 50, "uh": 120, "us": 255, "uv": 255} # frozen
|
||||
]
|
||||
|
||||
# Shape filtering to reject false positives:
|
||||
# - Health bars: thin horizontal strips (aspect ratio >> 1, very wide and short)
|
||||
# - Immune text: small vertical blobs (aspect ratio << 1, tall and narrow)
|
||||
# - Real auras: roughly circular/elliptical (aspect ratio ~0.5-2.0)
|
||||
TARGET_ASPECT_MIN = 0.5 # reject blobs taller than twice as tall as wide (immune text)
|
||||
TARGET_ASPECT_MAX = 3.0 # reject strips wider than 3x their height (health bars)
|
||||
|
||||
# Target shape filters to reject false positives (health bars, immune text)
|
||||
TARGET_MIN_AREA = 100 # minimum connected component area (immune text is tiny)
|
||||
TARGET_MAX_AREA = 200 # maximum area (real aura blobs)
|
||||
@@ -254,7 +247,7 @@ class LiveViewer:
|
||||
|
||||
# Testing: Have whatever you want to detect on the screen
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
from logger import Logger
|
||||
from screen import start_detecting_window, stop_detecting_window
|
||||
|
||||
@@ -246,7 +246,7 @@ def search_all(
|
||||
|
||||
# Testing: Have whatever you want to find on the screen
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
from screen import start_detecting_window, stop_detecting_window
|
||||
from utils.misc import wait
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ from pather import Pather, Location
|
||||
import template_finder
|
||||
from utils.misc import wait
|
||||
from ui_manager import ScreenObjects, is_visible
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from screen import convert_abs_to_monitor
|
||||
from logger import Logger
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from item import consumables
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import template_finder
|
||||
from config import Config
|
||||
from pather import Location
|
||||
@@ -247,7 +247,7 @@ class TownManager:
|
||||
|
||||
# Test: Move to desired location in d2r and run any town action you want to test from there
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
from screen import start_detecting_window
|
||||
start_detecting_window()
|
||||
|
||||
@@ -6,14 +6,14 @@ from .inventory_collection import InventoryCollection
|
||||
from .stash import Stash
|
||||
from .gem_picking import SimpleGemPicking
|
||||
from screen import convert_screen_to_monitor, grab
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait
|
||||
from version import __version__
|
||||
from logger import Logger
|
||||
from game_stats import GameStats
|
||||
import template_finder
|
||||
import numpy as np
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import cv2
|
||||
from inventory import personal, common
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import cut_roi, roi_center, wait, is_in_roi
|
||||
|
||||
from config import Config
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from ui_manager import detect_screen_object, select_screen_object_match, ScreenObjects
|
||||
from logger import Logger
|
||||
from utils.misc import wait
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
|
||||
def handle_error() -> bool:
|
||||
Logger.warning("Server connection issue. waiting 20s")
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ import time
|
||||
import numpy as np
|
||||
from screen import grab
|
||||
from config import Config
|
||||
from utils.misc import wait
|
||||
|
||||
def check_for_black_screen() -> bool:
|
||||
img = grab()
|
||||
@@ -17,5 +18,5 @@ def wait_for_loading_screen(timeout: float = 10.0) -> bool:
|
||||
while time.time() - start < timeout:
|
||||
if check_for_black_screen():
|
||||
return True
|
||||
time.sleep(0.02)
|
||||
wait(0.02, 0.024)
|
||||
return False
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import time
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from config import Config
|
||||
from utils.misc import wait
|
||||
from logger import Logger
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from config import Config
|
||||
from screen import convert_screen_to_monitor, grab
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import cut_roi, wait
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
@@ -44,7 +44,7 @@ def get_experience():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
|
||||
from screen import start_detecting_window
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from logger import Logger
|
||||
import cv2
|
||||
import time
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import time
|
||||
from screen import grab
|
||||
from config import Config
|
||||
import keyboard
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import keyboard
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from utils.misc import wait
|
||||
from ui_manager import wait_until_hidden, wait_until_visible, detect_screen_object, select_screen_object_match, ScreenObjects, list_visible_objects, is_visible
|
||||
@@ -138,7 +138,7 @@ def return_to_play() -> bool:
|
||||
|
||||
# Testing
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
from screen import start_detecting_window, stop_detecting_window
|
||||
start_detecting_window()
|
||||
|
||||
+45
-6
@@ -1,6 +1,7 @@
|
||||
import re
|
||||
import random
|
||||
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
from screen import convert_screen_to_monitor
|
||||
@@ -8,6 +9,40 @@ from utils.misc import wait
|
||||
from ui import loading
|
||||
from ui_manager import detect_screen_object, ScreenObjects
|
||||
|
||||
def _maybe_wrong_waypoint(act: int, idx: int):
|
||||
"""
|
||||
2-3% chance of selecting a wrong waypoint first, then correcting.
|
||||
Simulates human misclick on the waypoint menu.
|
||||
"""
|
||||
try:
|
||||
from utils.stealth import should_wrong_waypoint
|
||||
if not should_wrong_waypoint():
|
||||
return False, act, idx
|
||||
except Exception:
|
||||
return False, act, idx
|
||||
|
||||
# Pick a wrong waypoint in the same act (valid act range)
|
||||
wrong_idx = random.randint(0, 8)
|
||||
while wrong_idx == idx:
|
||||
wrong_idx = random.randint(0, 8)
|
||||
|
||||
# Click the wrong waypoint first
|
||||
wrong_pos = (Config().ui_pos["wp_first_btn_x"], Config().ui_pos["wp_first_btn_y"] + Config().ui_pos["wp_btn_height"] * wrong_idx)
|
||||
wx, wy = convert_screen_to_monitor(wrong_pos)
|
||||
mouse.move(wx, wy, randomize=8)
|
||||
mouse.click(button="left")
|
||||
Logger.info(f"[Stealth] Misclicked waypoint index {wrong_idx}, correcting to {idx}")
|
||||
|
||||
# Wait a bit (realizing the mistake), then correct
|
||||
wait(0.5, 1.0)
|
||||
|
||||
# Click the correct waypoint
|
||||
pos_wp_btn = (Config().ui_pos["wp_first_btn_x"], Config().ui_pos["wp_first_btn_y"] + Config().ui_pos["wp_btn_height"] * idx)
|
||||
x, y = convert_screen_to_monitor(pos_wp_btn)
|
||||
mouse.move(x, y, randomize=[60, 9], delay_factor=[0.9, 1.4])
|
||||
mouse.click(button="left")
|
||||
return True, act, idx
|
||||
|
||||
_WAYPOINTS = {
|
||||
# Act 1
|
||||
"Rouge Encampment": (1, 0),
|
||||
@@ -75,11 +110,15 @@ def use_wp(label: str = None, act: int = None, idx: int = None) -> bool:
|
||||
mouse.move(x, y, randomize=8)
|
||||
mouse.click(button="left")
|
||||
wait(0.3, 0.4)
|
||||
pos_wp_btn = (Config().ui_pos["wp_first_btn_x"], Config().ui_pos["wp_first_btn_y"] + Config().ui_pos["wp_btn_height"] * idx)
|
||||
x, y = convert_screen_to_monitor(pos_wp_btn)
|
||||
mouse.move(x, y, randomize=[60, 9], delay_factor=[0.9, 1.4])
|
||||
wait(0.4, 0.5)
|
||||
mouse.click(button="left")
|
||||
|
||||
# Stealth: 2-3% chance of wrong waypoint misclick then correction
|
||||
was_wrong, _, _ = _maybe_wrong_waypoint(act, idx)
|
||||
if not was_wrong:
|
||||
pos_wp_btn = (Config().ui_pos["wp_first_btn_x"], Config().ui_pos["wp_first_btn_y"] + Config().ui_pos["wp_btn_height"] * idx)
|
||||
x, y = convert_screen_to_monitor(pos_wp_btn)
|
||||
mouse.move(x, y, randomize=[60, 9], delay_factor=[0.9, 1.4])
|
||||
wait(0.4, 0.5)
|
||||
mouse.click(button="left")
|
||||
# wait till loading screen is over
|
||||
if loading.wait_for_loading_screen(5):
|
||||
while 1:
|
||||
|
||||
+5
-4
@@ -1,4 +1,4 @@
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
import numpy as np
|
||||
import time
|
||||
@@ -6,7 +6,7 @@ import cv2
|
||||
from functools import cache
|
||||
|
||||
from typing import TypeVar, Callable
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import mouse
|
||||
from utils.misc import wait, cut_roi, image_is_equal
|
||||
from logger import Logger
|
||||
from config import Config
|
||||
@@ -411,7 +411,7 @@ def get_closest_non_hud_pixel(pos : tuple[int, int], pos_type: str = "abs") -> t
|
||||
|
||||
# Testing: Move to whatever ui to test and run
|
||||
if __name__ == "__main__":
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from screen import start_detecting_window, grab, stop_detecting_window
|
||||
start_detecting_window()
|
||||
keyboard.add_hotkey('f12', lambda: Logger.info('Force Exit (f12)') or stop_detecting_window() or os._exit(1))
|
||||
@@ -422,4 +422,5 @@ if __name__ == "__main__":
|
||||
print(wait_for_update(grab(), Config().ui_roi["right_inventory"], timeout=5))
|
||||
# while 1:
|
||||
# print(list_visible_objects())
|
||||
# time.sleep(1)
|
||||
# from utils.misc import wait as _wait
|
||||
# _wait(1, 1.2)
|
||||
|
||||
@@ -3,7 +3,7 @@ import cv2
|
||||
from config import Config
|
||||
from utils.misc import cut_roi
|
||||
import mouse
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
import time
|
||||
from screen import grab, convert_monitor_to_screen
|
||||
|
||||
@@ -15,8 +15,8 @@ import time
|
||||
from pather import Pather
|
||||
from char.sorceress import NovaSorc
|
||||
from screen import convert_screen_to_monitor
|
||||
import keyboard
|
||||
from utils.custom_mouse import mouse
|
||||
from input_layer import keyboard
|
||||
from input_layer import mouse
|
||||
from PIL import ImageTk, Image
|
||||
import re
|
||||
|
||||
|
||||
+77
-7
@@ -3,6 +3,8 @@ from decimal import InvalidOperation
|
||||
import time
|
||||
import random
|
||||
import ctypes
|
||||
import threading
|
||||
import logging
|
||||
import numpy as np
|
||||
from copy import deepcopy
|
||||
import unicodedata
|
||||
@@ -107,12 +109,20 @@ def find_d2r_window(spec: WindowSpec, offset = (0, 0)) -> tuple[int, int]:
|
||||
|
||||
def set_d2r_always_on_top():
|
||||
if os.name == 'nt':
|
||||
windows_list = []
|
||||
EnumWindows(lambda w, l: l.append((w, GetWindowText(w))), windows_list)
|
||||
for w in windows_list:
|
||||
if w[1] == "Diablo II: Resurrected":
|
||||
SetWindowPos(w[0], HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)
|
||||
print("Set D2R to be always on top")
|
||||
for attempt in range(30):
|
||||
windows_list = []
|
||||
EnumWindows(lambda w, l: l.append((w, GetWindowText(w))), windows_list)
|
||||
found = False
|
||||
for w in windows_list:
|
||||
if "Diablo II" in w[1]:
|
||||
SetWindowPos(w[0], HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)
|
||||
print("Set D2R to be always on top")
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
return
|
||||
wait(0.5, 1.0)
|
||||
print('D2R window not found, could not set always on top')
|
||||
else:
|
||||
print('OS not supported, unable to set D2R always on top')
|
||||
|
||||
@@ -146,13 +156,73 @@ def wait(min_seconds, max_seconds = None):
|
||||
time.sleep(base * jitter)
|
||||
return
|
||||
|
||||
def kill_thread(thread):
|
||||
def _force_kill_thread(thread):
|
||||
"""
|
||||
DANGEROUS: Force-kills a thread via CPython private API.
|
||||
Only use as a last resort when cooperative shutdown failed.
|
||||
Can corrupt locks, cause GIL issues, or corrupt numpy arrays.
|
||||
"""
|
||||
Logger.error(
|
||||
f"Force-killing thread '{thread.name}' via PyThreadState_SetAsyncExc. "
|
||||
"This is dangerous and can corrupt locks/GIL/numpy arrays!"
|
||||
)
|
||||
thread_id = thread.ident
|
||||
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, ctypes.py_object(SystemExit))
|
||||
if res > 1:
|
||||
ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, 0)
|
||||
Logger.error('Exception raise failure')
|
||||
|
||||
|
||||
def cooperative_shutdown(
|
||||
thread,
|
||||
bot=None,
|
||||
health_manager=None,
|
||||
death_manager=None,
|
||||
timeout=5.0,
|
||||
):
|
||||
"""
|
||||
Cooperatively shut down a thread by signalling the owning objects, then join.
|
||||
|
||||
Falls back to _force_kill_thread only if the thread is still alive after
|
||||
*timeout* seconds. This avoids the dangers of PyThreadState_SetAsyncExc
|
||||
(corrupted locks, GIL issues, numpy array corruption) in the common path.
|
||||
"""
|
||||
# Signal Bot to stop its run loop
|
||||
if bot is not None:
|
||||
bot.stop()
|
||||
|
||||
# Signal HealthManager to stop its monitoring loop
|
||||
if health_manager is not None:
|
||||
health_manager.stop_monitor()
|
||||
|
||||
# Signal DeathManager to stop its monitoring loop
|
||||
if death_manager is not None:
|
||||
death_manager.stop_monitor()
|
||||
|
||||
# Wait for the thread to finish cooperatively
|
||||
thread.join(timeout=timeout)
|
||||
|
||||
if thread.is_alive():
|
||||
Logger.warning(
|
||||
f"Thread '{thread.name}' did not exit within {timeout}s; "
|
||||
"falling back to force kill (PyThreadState_SetAsyncExc)."
|
||||
)
|
||||
_force_kill_thread(thread)
|
||||
|
||||
|
||||
# Kept for backwards-compatibility so existing imports still work.
|
||||
# Calls the cooperative path when the owning objects are available;
|
||||
# otherwise falls back to force kill immediately.
|
||||
def kill_thread(thread):
|
||||
"""
|
||||
Backwards-compatibility wrapper.
|
||||
Prefer cooperative_shutdown() for new code.
|
||||
"""
|
||||
# We can't signal anything without the owning objects, so fall back
|
||||
# to force kill. Callers that own the bot/managers should use
|
||||
# cooperative_shutdown() instead.
|
||||
_force_kill_thread(thread)
|
||||
|
||||
def cut_roi(img, roi):
|
||||
x, y, w, h = roi
|
||||
return img[y:y+h, x:x+w]
|
||||
|
||||
@@ -4,7 +4,7 @@ from config import Config
|
||||
import template_finder
|
||||
from utils.misc import load_template, cut_roi
|
||||
import mouse
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
@@ -118,7 +118,7 @@ def detect_visible_npcs_cached(img=None, ttl=5.0):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import cv2
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from screen import start_detecting_window
|
||||
|
||||
start_detecting_window()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import os, sys
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
import subprocess
|
||||
|
||||
import template_finder
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import random
|
||||
import time
|
||||
import keyboard
|
||||
from input_layer import keyboard
|
||||
from config import Config
|
||||
from logger import Logger
|
||||
from utils.misc import wait
|
||||
|
||||
|
||||
def maybe_afk_break():
|
||||
@@ -14,7 +15,7 @@ def maybe_afk_break():
|
||||
if random.randint(1, 100) <= cfg["afk_break_chance"]:
|
||||
minutes = random.uniform(cfg["afk_break_min_m"], cfg["afk_break_max_m"])
|
||||
Logger.info(f"[Stealth] Taking unscheduled AFK break for {minutes:.1f} minutes")
|
||||
time.sleep(minutes * 60)
|
||||
wait(minutes * 60, minutes * 60 * 1.5)
|
||||
Logger.info("[Stealth] AFK break over, resuming")
|
||||
|
||||
|
||||
@@ -88,7 +89,7 @@ def add_micro_pause():
|
||||
# 30% chance of micro-pause
|
||||
if random.random() < 0.3:
|
||||
pause_s = random.uniform(min_ms, max_ms) / 1000.0
|
||||
time.sleep(pause_s)
|
||||
wait(pause_s, pause_s * 1.2)
|
||||
|
||||
|
||||
# ─── Tier 1: Input-level stealth ─────────────────────────────────────────────
|
||||
@@ -123,7 +124,7 @@ def click_delay() -> float:
|
||||
|
||||
def apply_click_delay():
|
||||
"""Sleep for a human-like delay before clicking."""
|
||||
time.sleep(click_delay())
|
||||
wait(click_delay(), click_delay() * 1.2)
|
||||
|
||||
|
||||
def key_press_duration(base_duration: float = 0.05) -> float:
|
||||
@@ -153,7 +154,7 @@ def human_key_press(key: str):
|
||||
add_micro_pause()
|
||||
duration = key_press_duration()
|
||||
keyboard.press(key)
|
||||
time.sleep(duration)
|
||||
wait(duration, duration * 1.2)
|
||||
keyboard.release(key)
|
||||
add_micro_pause()
|
||||
|
||||
@@ -166,7 +167,7 @@ def human_keyboard_send(key: str):
|
||||
add_micro_pause()
|
||||
duration = key_press_duration(0.03)
|
||||
keyboard.press(key)
|
||||
time.sleep(duration)
|
||||
wait(duration, duration * 1.2)
|
||||
keyboard.release(key)
|
||||
add_micro_pause()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user