add stealth system, key detection, click recorder, FOHdin, new routes, and tooling updates
This commit is contained in:
+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