"""Synthetic input that Roblox actually accepts.""" from __future__ import annotations import ctypes import time from ctypes import wintypes user32 = ctypes.WinDLL("user32", use_last_error=True) # ------------------------------------------------------------------ DPI # Without this, Windows lies about coordinates on any display not at 100% scale. def ensure_dpi_aware() -> None: try: # PER_MONITOR_AWARE_V2. Fails harmlessly if it is already set. user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4)) return except Exception: pass try: user32.SetProcessDPIAware() except Exception: pass ensure_dpi_aware() # --------------------------------------------------------------- structs ULONG_PTR = wintypes.WPARAM INPUT_MOUSE = 0 INPUT_KEYBOARD = 1 KEYEVENTF_EXTENDEDKEY = 0x0001 KEYEVENTF_KEYUP = 0x0002 KEYEVENTF_SCANCODE = 0x0008 MOUSEEVENTF_MOVE = 0x0001 MOUSEEVENTF_LEFTDOWN = 0x0002 MOUSEEVENTF_LEFTUP = 0x0004 MOUSEEVENTF_RIGHTDOWN = 0x0008 MOUSEEVENTF_RIGHTUP = 0x0010 MOUSEEVENTF_MIDDLEDOWN = 0x0020 MOUSEEVENTF_MIDDLEUP = 0x0040 MOUSEEVENTF_WHEEL = 0x0800 MOUSEEVENTF_ABSOLUTE = 0x8000 MOUSEEVENTF_VIRTUALDESK = 0x4000 class MOUSEINPUT(ctypes.Structure): _fields_ = [ ("dx", wintypes.LONG), ("dy", wintypes.LONG), ("mouseData", wintypes.DWORD), ("dwFlags", wintypes.DWORD), ("time", wintypes.DWORD), ("dwExtraInfo", ULONG_PTR), ] class KEYBDINPUT(ctypes.Structure): _fields_ = [ ("wVk", wintypes.WORD), ("wScan", wintypes.WORD), ("dwFlags", wintypes.DWORD), ("time", wintypes.DWORD), ("dwExtraInfo", ULONG_PTR), ] 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): _anonymous_ = ("u",) _fields_ = [("type", wintypes.DWORD), ("u", _INPUTUNION)] user32.SendInput.argtypes = (wintypes.UINT, ctypes.POINTER(INPUT), ctypes.c_int) user32.SendInput.restype = wintypes.UINT user32.MapVirtualKeyW.argtypes = (wintypes.UINT, wintypes.UINT) user32.MapVirtualKeyW.restype = wintypes.UINT user32.GetSystemMetrics.argtypes = (ctypes.c_int,) user32.GetSystemMetrics.restype = ctypes.c_int class InputError(RuntimeError): """SendInput refused the event.""" def _send(*inputs: INPUT) -> None: count = len(inputs) array = (INPUT * count)(*inputs) sent = user32.SendInput(count, array, ctypes.sizeof(INPUT)) if sent != count: err = ctypes.get_last_error() raise InputError(f"SendInput sent {sent}/{count} events (error {err})") # ------------------------------------------------------------------ keys KEYS: dict[str, int] = { "backspace": 0x08, "tab": 0x09, "enter": 0x0D, "return": 0x0D, "shift": 0xA0, "lshift": 0xA0, "rshift": 0xA1, "ctrl": 0xA2, "lctrl": 0xA2, "rctrl": 0xA3, "alt": 0xA4, "lalt": 0xA4, "ralt": 0xA5, "esc": 0x1B, "escape": 0x1B, "space": 0x20, "pageup": 0x21, "pagedown": 0x22, "end": 0x23, "home": 0x24, "left": 0x25, "up": 0x26, "right": 0x27, "down": 0x28, "insert": 0x2D, "delete": 0x2E, "-": 0xBD, "=": 0xBB, "[": 0xDB, "]": 0xDD, ";": 0xBA, "'": 0xDE, ",": 0xBC, ".": 0xBE, "/": 0xBF, "\\": 0xDC, "`": 0xC0, } for _i in range(1, 25): KEYS[f"f{_i}"] = 0x6F + _i for _c in "abcdefghijklmnopqrstuvwxyz": KEYS[_c] = ord(_c.upper()) for _c in "0123456789": KEYS[_c] = ord(_c) # Keys whose scan code needs the extended flag. EXTENDED = { 0x25, 0x26, 0x27, 0x28, # arrows 0x21, 0x22, 0x23, 0x24, 0x2D, 0x2E, # navigation cluster 0xA3, 0xA5, 0x5B, 0x5C, 0x90, # right ctrl/alt, windows keys, numlock } VK_TO_NAME = {} for _name, _vk in KEYS.items(): VK_TO_NAME.setdefault(_vk, _name) held_keys: set[str] = set() held_buttons: set[str] = set() def vk_of(key: str) -> int: try: return KEYS[key.lower()] except KeyError: raise KeyError(f"unknown key name: {key!r}") from None def _key_event(key: str, up: bool) -> None: vk = vk_of(key) scan = user32.MapVirtualKeyW(vk, 0) flags = KEYEVENTF_KEYUP if up else 0 if scan: # wVk stays zero on purpose: with the scan-code flag set. flags |= KEYEVENTF_SCANCODE if vk in EXTENDED: flags |= KEYEVENTF_EXTENDEDKEY ki = KEYBDINPUT(wVk=0, wScan=scan, dwFlags=flags, time=0, dwExtraInfo=0) else: ki = KEYBDINPUT(wVk=vk, wScan=0, dwFlags=flags, time=0, dwExtraInfo=0) _send(INPUT(type=INPUT_KEYBOARD, ki=ki)) def key_down(key: str) -> None: _key_event(key, up=False) held_keys.add(key.lower()) def key_up(key: str) -> None: _key_event(key, up=True) held_keys.discard(key.lower()) def tap(key: str, hold: float = 0.04) -> None: key_down(key) time.sleep(hold) key_up(key) # ----------------------------------------------------------------- mouse _BUTTONS = { "left": (MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP), "right": (MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP), "middle": (MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP), } def _mouse(flags: int, dx: int = 0, dy: int = 0, data: int = 0) -> None: mi = MOUSEINPUT(dx=dx, dy=dy, mouseData=data, dwFlags=flags, time=0, dwExtraInfo=0) _send(INPUT(type=INPUT_MOUSE, mi=mi)) def cursor_pos() -> tuple[int, int]: point = wintypes.POINT() user32.GetCursorPos(ctypes.byref(point)) return point.x, point.y def move_to(x: int, y: int) -> None: """Absolute move, for clicking something on screen.""" vx = user32.GetSystemMetrics(76) vy = user32.GetSystemMetrics(77) vw = max(user32.GetSystemMetrics(78), 1) vh = max(user32.GetSystemMetrics(79), 1) nx = int(round((x - vx) * 65535 / max(vw - 1, 1))) ny = int(round((y - vy) * 65535 / max(vh - 1, 1))) _mouse(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK, nx, ny) def move_rel(dx: int, dy: int) -> None: """Relative move - the only kind the camera reacts to.""" _mouse(MOUSEEVENTF_MOVE, int(dx), int(dy)) def mouse_down(button: str = "left") -> None: down, _ = _BUTTONS[button] _mouse(down) held_buttons.add(button) def mouse_up(button: str = "left") -> None: _, up = _BUTTONS[button] _mouse(up) held_buttons.discard(button) def click(x: int | None = None, y: int | None = None, button: str = "left", settle: float = 0.03, hold: float = 0.04) -> None: """Move, let the cursor land, click.""" if x is not None and y is not None: move_to(x, y) time.sleep(settle) mouse_down(button) time.sleep(hold) mouse_up(button) def scroll(clicks: int) -> None: _mouse(MOUSEEVENTF_WHEEL, data=int(clicks) * 120) def release_all() -> None: """Put everything back up. Safe to call twice, and it never raises.""" for key in list(held_keys): try: key_up(key) except Exception: held_keys.discard(key) for button in list(held_buttons): try: mouse_up(button) except Exception: held_buttons.discard(button) def is_down(key: str) -> bool: """Physical state of a key, regardless of which window has focus.""" return bool(user32.GetAsyncKeyState(vk_of(key)) & 0x8000) MOUSE_VK = {"left": 0x01, "right": 0x02, "middle": 0x04} def mouse_is_down(button: str) -> bool: return bool(user32.GetAsyncKeyState(MOUSE_VK[button]) & 0x8000)