Roblox macros that play from outside the game: Anime Vanguards, and a falling-notes piano event.
270 lines
7.5 KiB
Python
270 lines
7.5 KiB
Python
"""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)
|