Roblox macros that play from outside the game: Anime Vanguards, and a falling-notes piano event.
151 lines
5.3 KiB
Python
151 lines
5.3 KiB
Python
"""Recording what you do, and doing it again."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
from . import screen, sound, winput
|
|
from .hotkeys import Hotkeys
|
|
from .runner import Runner
|
|
|
|
# Everything worth catching in a Roblox game.
|
|
WATCH_KEYS = [
|
|
*"abcdefghijklmnopqrstuvwxyz",
|
|
*"0123456789",
|
|
"space", "shift", "ctrl", "tab", "esc", "enter",
|
|
"-", "=", "[", "]", ",", ".", "/",
|
|
]
|
|
|
|
|
|
def record(name: str, keys: dict | None = None, watch_keys=None, extra=None) -> list[dict]:
|
|
"""Watch the physical mouse and keyboard until you stop, and write it down."""
|
|
keys = keys or {}
|
|
control = Hotkeys(
|
|
toggle=keys.get("pause", "f7"),
|
|
quit=keys.get("stop", "f8"),
|
|
)
|
|
watch = list(watch_keys or WATCH_KEYS)
|
|
|
|
print(f"recording into {name!r}")
|
|
print(f" [{keys.get('pause', 'f7').upper()}] start / finish"
|
|
f" [{keys.get('stop', 'f8').upper()}] discard and quit")
|
|
print(" clicks and key presses are captured while recording is on.")
|
|
print(" focus Roblox, press start, play your build order, press finish.")
|
|
|
|
steps: list[dict] = []
|
|
recording = False
|
|
last_at = None
|
|
|
|
# Seeded as held, so a button already down is not recorded when it comes up.
|
|
button_was = {button: winput.mouse_is_down(button) for button in ("left", "right", "middle")}
|
|
key_was = {key: winput.is_down(key) for key in watch}
|
|
|
|
try:
|
|
while True:
|
|
for pressed in control.poll() + (extra.poll() if extra else []):
|
|
if pressed == "quit":
|
|
sound.alarm()
|
|
print("discarded.")
|
|
return []
|
|
if pressed == "toggle":
|
|
recording = not recording
|
|
if recording:
|
|
# After the beep, not before: the beep is blocking.
|
|
sound.started()
|
|
last_at = time.monotonic()
|
|
print("recording…")
|
|
else:
|
|
sound.stopped()
|
|
print(f"finished — {len(steps)} steps.")
|
|
return steps
|
|
|
|
now = time.monotonic()
|
|
|
|
for button in ("left", "right", "middle"):
|
|
down = winput.mouse_is_down(button)
|
|
fired = down and not button_was[button]
|
|
button_was[button] = down
|
|
if fired and recording:
|
|
x, y = winput.cursor_pos()
|
|
steps.append({
|
|
"do": "click",
|
|
"at": [x, y],
|
|
"button": button,
|
|
"after": round(now - last_at, 3),
|
|
"color": list(screen.pixel(x, y) or ()),
|
|
})
|
|
last_at = now
|
|
print(f" click {button} at {x},{y}")
|
|
|
|
for key in watch:
|
|
down = winput.is_down(key)
|
|
fired = down and not key_was[key]
|
|
key_was[key] = down
|
|
if fired and recording:
|
|
steps.append({"do": "key", "key": key, "after": round(now - last_at, 3)})
|
|
last_at = now
|
|
print(f" key {key}")
|
|
|
|
time.sleep(0.005)
|
|
except KeyboardInterrupt:
|
|
print("interrupted.")
|
|
return steps
|
|
|
|
|
|
def play(runner: Runner, steps: list[dict], speed: float = 1.0) -> None:
|
|
"""Replay a recorded routine once."""
|
|
speed = max(float(speed), 0.05)
|
|
for index, step in enumerate(steps, start=1):
|
|
runner.wait(float(step.get("after", 0)) / speed)
|
|
kind = step.get("do")
|
|
if kind == "click":
|
|
x, y = step["at"]
|
|
winput.click(x, y, button=step.get("button", "left"))
|
|
elif kind == "key":
|
|
winput.tap(step["key"])
|
|
elif kind == "wait":
|
|
runner.wait(float(step.get("seconds", 0)) / speed)
|
|
elif kind == "hold":
|
|
winput.key_down(step["key"])
|
|
runner.wait(float(step.get("seconds", 0.2)))
|
|
winput.key_up(step["key"])
|
|
else:
|
|
runner.log(f"step {index}: unknown action {kind!r}, skipped")
|
|
|
|
|
|
def capture_point(prompt: str, keys: dict | None = None) -> dict | None:
|
|
"""Point the cursor at something and press the capture key."""
|
|
keys = keys or {}
|
|
control = Hotkeys(
|
|
take=keys.get("capture", "f6"),
|
|
skip=keys.get("stop", "f8"),
|
|
)
|
|
print(f"\n{prompt}")
|
|
print(f" hover it, then [{keys.get('capture', 'f6').upper()}] to capture"
|
|
f" [{keys.get('stop', 'f8').upper()}] to skip")
|
|
while True:
|
|
for pressed in control.poll():
|
|
if pressed == "skip":
|
|
print(" skipped.")
|
|
return None
|
|
if pressed == "take":
|
|
x, y = winput.cursor_pos()
|
|
colour = screen.pixel(x, y)
|
|
sound.tick()
|
|
print(f" captured {x},{y} colour {colour}")
|
|
return {"at": [x, y], "color": list(colour or ())}
|
|
time.sleep(0.01)
|
|
|
|
|
|
def watch_cursor() -> None:
|
|
"""Live cursor position and colour, for calibrating by hand. Ctrl+C quits."""
|
|
print("cursor readout — Ctrl+C to stop")
|
|
try:
|
|
while True:
|
|
x, y = winput.cursor_pos()
|
|
colour = screen.pixel(x, y)
|
|
print(f"\r {x:>5},{y:<5} rgb{colour} ", end="", flush=True)
|
|
time.sleep(0.08)
|
|
except KeyboardInterrupt:
|
|
print()
|