add stealth system, key detection, click recorder, FOHdin, new routes, and tooling updates
This commit is contained in:
@@ -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)
|
||||
@@ -0,0 +1,322 @@
|
||||
"""
|
||||
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
|
||||
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))
|
||||
|
||||
def isListOfPoints(l):
|
||||
if not isinstance(l, list):
|
||||
return False
|
||||
try:
|
||||
isPoint = lambda p: ((len(p) == 2) and isNumeric(p[0]) and isNumeric(p[1]))
|
||||
return all(map(isPoint, l))
|
||||
except (KeyError, TypeError):
|
||||
return False
|
||||
|
||||
class BezierCurve():
|
||||
@staticmethod
|
||||
def binomial(n, k):
|
||||
return math.factorial(n) / float(math.factorial(k) * math.factorial(n - k))
|
||||
|
||||
@staticmethod
|
||||
def bernsteinPolynomialPoint(x, i, n):
|
||||
return BezierCurve.binomial(n, i) * (x ** i) * ((1 - x) ** (n - i))
|
||||
|
||||
@staticmethod
|
||||
def bernsteinPolynomial(points):
|
||||
def bern(t):
|
||||
n = len(points) - 1
|
||||
x = y = 0
|
||||
for i, point in enumerate(points):
|
||||
bern = BezierCurve.bernsteinPolynomialPoint(t, i, n)
|
||||
x += point[0] * bern
|
||||
y += point[1] * bern
|
||||
return x, y
|
||||
return bern
|
||||
|
||||
@staticmethod
|
||||
def curvePoints(n, points):
|
||||
curvePoints = []
|
||||
bernstein_polynomial = BezierCurve.bernsteinPolynomial(points)
|
||||
for i in range(n):
|
||||
t = i / (n - 1)
|
||||
curvePoints += bernstein_polynomial(t),
|
||||
return curvePoints
|
||||
|
||||
class HumanCurve():
|
||||
def __init__(self, fromPoint, toPoint, **kwargs):
|
||||
self.fromPoint = fromPoint
|
||||
self.toPoint = toPoint
|
||||
self.points = self.generateCurve(**kwargs)
|
||||
|
||||
def generateCurve(self, **kwargs):
|
||||
offsetBoundaryX = kwargs.get("offsetBoundaryX", 100)
|
||||
offsetBoundaryY = kwargs.get("offsetBoundaryY", 100)
|
||||
leftBoundary = kwargs.get("leftBoundary", min(self.fromPoint[0], self.toPoint[0])) - offsetBoundaryX
|
||||
rightBoundary = kwargs.get("rightBoundary", max(self.fromPoint[0], self.toPoint[0])) + offsetBoundaryX
|
||||
downBoundary = kwargs.get("downBoundary", min(self.fromPoint[1], self.toPoint[1])) - offsetBoundaryY
|
||||
upBoundary = kwargs.get("upBoundary", max(self.fromPoint[1], self.toPoint[1])) + offsetBoundaryY
|
||||
knotsCount = kwargs.get("knotsCount", 2)
|
||||
distortionMean = kwargs.get("distortionMean", 1)
|
||||
distortionStdev = kwargs.get("distortionStdev", 1)
|
||||
distortionFrequency = kwargs.get("distortionFrequency", 0.4)
|
||||
tween = kwargs.get("tweening", pytweening.easeOutQuad)
|
||||
targetPoints = kwargs.get("targetPoints", 10)
|
||||
|
||||
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):
|
||||
if not (isNumeric(leftBoundary) and isNumeric(rightBoundary) and
|
||||
isNumeric(downBoundary) and isNumeric(upBoundary)):
|
||||
raise ValueError("Boundaries must be numeric")
|
||||
if not isinstance(knotsCount, int) or knotsCount < 0:
|
||||
raise ValueError("knotsCount must be non-negative integer")
|
||||
if leftBoundary > rightBoundary:
|
||||
raise ValueError("leftBoundary must be less than or equal to rightBoundary")
|
||||
if downBoundary > upBoundary:
|
||||
raise ValueError("downBoundary must be less than or equal to upBoundary")
|
||||
|
||||
knotsX = np.random.choice(range(leftBoundary, rightBoundary), size=knotsCount)
|
||||
knotsY = np.random.choice(range(downBoundary, upBoundary), size=knotsCount)
|
||||
return list(zip(knotsX, knotsY))
|
||||
|
||||
def generatePoints(self, knots):
|
||||
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]),
|
||||
2)
|
||||
knots = [self.fromPoint] + knots + [self.toPoint]
|
||||
return BezierCurve.curvePoints(midPtsCnt, knots)
|
||||
|
||||
def distortPoints(self, points, distortionMean, distortionStdev, distortionFrequency):
|
||||
if not (isNumeric(distortionMean) and isNumeric(distortionStdev) and
|
||||
isNumeric(distortionFrequency)):
|
||||
raise ValueError("Distortions must be numeric")
|
||||
if not isListOfPoints(points):
|
||||
raise ValueError("points must be valid list of points")
|
||||
if not (0 <= distortionFrequency <= 1):
|
||||
raise ValueError("distortionFrequency must be in range [0,1]")
|
||||
|
||||
distorted = []
|
||||
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 = [points[0]] + distorted + [points[-1]]
|
||||
return distorted
|
||||
|
||||
def tweenPoints(self, points, tween, targetPoints):
|
||||
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")
|
||||
res = []
|
||||
for i in range(targetPoints):
|
||||
index = int(tween(float(i) / (targetPoints - 1)) * (len(points) - 1))
|
||||
res += points[index],
|
||||
return res
|
||||
|
||||
|
||||
class mouse:
|
||||
"""
|
||||
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 sleep(duration):
|
||||
from utils.misc import wait as _wait
|
||||
_wait(duration, duration * 1.2)
|
||||
|
||||
@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))
|
||||
targetPoints = min(6, max(3, int(0.004 * dist)))
|
||||
if not absolute:
|
||||
x = from_point[0] + x
|
||||
y = from_point[1] + y
|
||||
|
||||
if type(randomize) is int:
|
||||
randomize = int(randomize)
|
||||
if randomize > 0:
|
||||
x = int(x) + random.randrange(-randomize, +randomize)
|
||||
y = int(y) + random.randrange(-randomize, +randomize)
|
||||
else:
|
||||
randomize = (int(randomize[0]), int(randomize[1]))
|
||||
if randomize[1] > 0 and randomize[0] > 0:
|
||||
x = int(x) + random.randrange(-randomize[0], +randomize[0])
|
||||
y = int(y) + random.randrange(-randomize[1], +randomize[1])
|
||||
|
||||
# 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
|
||||
|
||||
distortionMean = 1 * complexity
|
||||
distortionStdev = 1 * complexity
|
||||
distortionFreq = min(0.8, 0.4 * complexity)
|
||||
|
||||
human_curve = HumanCurve(
|
||||
from_point, (x, y),
|
||||
offsetBoundaryX=offsetBoundaryX,
|
||||
offsetBoundaryY=offsetBoundaryY,
|
||||
targetPoints=targetPoints,
|
||||
distortionMean=distortionMean,
|
||||
distortionStdev=distortionStdev,
|
||||
distortionFrequency=distortionFreq
|
||||
)
|
||||
|
||||
duration = min(0.5, max(0.05, dist * 0.0004) * random.uniform(delay_factor[0], delay_factor[1]))
|
||||
delta = duration / len(human_curve.points)
|
||||
|
||||
for point in human_curve.points:
|
||||
_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."""
|
||||
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)
|
||||
try:
|
||||
from utils.stealth import endpoint_wobble
|
||||
wx, wy = endpoint_wobble(rx, ry)
|
||||
_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():
|
||||
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():
|
||||
try:
|
||||
from utils.stealth import apply_click_delay
|
||||
apply_click_delay()
|
||||
except Exception:
|
||||
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():
|
||||
_native_down(button)
|
||||
|
||||
@staticmethod
|
||||
def release(button):
|
||||
_native_up(button)
|
||||
|
||||
@staticmethod
|
||||
def get_position():
|
||||
return _native_get_pos()
|
||||
|
||||
@staticmethod
|
||||
def wheel(delta):
|
||||
_native_wheel(delta)
|
||||
|
||||
@staticmethod
|
||||
def async_move(x, y, absolute=True, randomize=5, delay_factor=[0.4, 0.6]):
|
||||
result = {"_done": False, "_lock": threading.Lock()}
|
||||
|
||||
def _run():
|
||||
try:
|
||||
mouse.move(x, y, absolute=absolute, randomize=randomize, delay_factor=delay_factor)
|
||||
finally:
|
||||
with result["_lock"]:
|
||||
result["_done"] = True
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
def done():
|
||||
with result["_lock"]:
|
||||
return result["_done"]
|
||||
|
||||
def wait(timeout=None):
|
||||
deadline = None if timeout is None else time.monotonic() + timeout
|
||||
while not done():
|
||||
if deadline is not None and time.monotonic() >= deadline:
|
||||
return False
|
||||
from utils.misc import wait as _wait
|
||||
_wait(0.01, 0.012)
|
||||
return True
|
||||
|
||||
return {"done": done, "wait": wait}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
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)
|
||||
Reference in New Issue
Block a user