Files
KittyMacro/kittymacro/routine.py
T
capyb2222 bc09d70516 feat(init): KittyMacro
Roblox macros that play from outside the game: Anime Vanguards, and a
falling-notes piano event.
2026-09-05 12:22:28 -07:00

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()