Roblox macros that play from outside the game: Anime Vanguards, and a falling-notes piano event.
264 lines
9.3 KiB
Python
264 lines
9.3 KiB
Python
#!/usr/bin/env python3
|
|
"""KittyMacro — command line.
|
|
|
|
python macro.py gui all of the below, in a window
|
|
python macro.py check is any of this going to work here?
|
|
python macro.py mouse live cursor position and colour
|
|
python macro.py record build record a build order
|
|
python macro.py play build replay it once, to check it
|
|
python macro.py calibrate teach it your buttons
|
|
python macro.py run farm Anime Vanguards
|
|
python macro.py routines what has been recorded
|
|
|
|
python macro.py calibrate --game piano set up a falling-notes event
|
|
python macro.py run --game piano and play it
|
|
python macro.py run --game piano --session the whole event, menus and all
|
|
python macro.py walk into walk the route into the event, once
|
|
|
|
Nothing needs installing: it is the standard library and ctypes.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ctypes
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
# Some Windows consoles are still on a code page that cannot spell "-".
|
|
for _stream in (sys.stdout, sys.stderr):
|
|
try:
|
|
_stream.reconfigure(encoding="utf-8", errors="replace")
|
|
except Exception:
|
|
pass
|
|
|
|
from kittymacro import __version__, config, routine, screen, winput # noqa: E402
|
|
from kittymacro.macros import animevanguards, piano # noqa: E402
|
|
from kittymacro.runner import Runner # noqa: E402
|
|
|
|
# Recording and playing routines belong to the tower defence macro; the rest takes --game.
|
|
MACRO = animevanguards
|
|
GAMES = {"animevanguards": animevanguards, "piano": piano}
|
|
|
|
|
|
def game_of(args):
|
|
return GAMES[getattr(args, "game", "animevanguards")]
|
|
|
|
|
|
# ------------------------------------------------------------------ check
|
|
def cmd_check(args) -> int:
|
|
print(f"KittyMacro {__version__}")
|
|
print(f"python {sys.version.split()[0]} ({'64' if sys.maxsize > 2**32 else '32'}-bit)")
|
|
|
|
width, height = screen.screen_size()
|
|
print(f"screen {width} x {height}")
|
|
|
|
x, y = winput.cursor_pos()
|
|
print(f"cursor {x},{y} rgb{screen.pixel(x, y)}")
|
|
|
|
hwnd = screen.find_roblox()
|
|
if hwnd:
|
|
rect = screen.window_rect(hwnd)
|
|
print(f"roblox found — window {rect}")
|
|
print(f" in front: {'yes' if screen.roblox_focused() else 'no'}")
|
|
else:
|
|
print("roblox NOT FOUND — open the game, then run this again")
|
|
|
|
admin = False
|
|
try:
|
|
admin = bool(ctypes.windll.shell32.IsUserAnAdmin())
|
|
except Exception:
|
|
pass
|
|
print(f"elevated {'yes' if admin else 'no'}")
|
|
|
|
# A relative move of nothing.
|
|
try:
|
|
winput.move_rel(0, 0)
|
|
print("sendinput accepted")
|
|
except winput.InputError as error:
|
|
print(f"sendinput REFUSED — {error}")
|
|
print(" if Roblox is running as administrator, run this the same way")
|
|
return 1
|
|
|
|
if not args.live:
|
|
print("\nlooks fine. `check --live` will make your character jump in game "
|
|
"to prove input really lands.")
|
|
return 0
|
|
|
|
if not hwnd:
|
|
print("\ncannot run the live test without Roblox open.")
|
|
return 1
|
|
|
|
print("\nlive test: focusing Roblox and pressing space in 3 seconds…")
|
|
screen.focus_roblox()
|
|
for remaining in (3, 2, 1):
|
|
print(f" {remaining}…", flush=True)
|
|
time.sleep(1)
|
|
if not screen.roblox_focused():
|
|
print(" Roblox is still not in front — click it and try again.")
|
|
return 1
|
|
winput.tap("space", hold=0.08)
|
|
time.sleep(0.4)
|
|
winput.tap("space", hold=0.08)
|
|
print(" sent two jumps. If your character jumped, everything below works.")
|
|
return 0
|
|
|
|
|
|
# ------------------------------------------------------------------ tools
|
|
def cmd_mouse(args) -> int:
|
|
routine.watch_cursor()
|
|
return 0
|
|
|
|
|
|
def cmd_record(args) -> int:
|
|
cfg = config.load(MACRO.NAME, MACRO.DEFAULTS)
|
|
steps = routine.record(args.name, cfg["hotkeys"])
|
|
if not steps:
|
|
return 1
|
|
path = config.save_routine(args.name, steps, args.note or "")
|
|
print(f"saved {len(steps)} steps to {path}")
|
|
if args.name not in (cfg["routines"].get("build"), cfg["routines"].get("loop")):
|
|
print(f"point the macro at it with: routines.build = \"{args.name}\" "
|
|
f"in {config.config_path(MACRO.NAME)}")
|
|
return 0
|
|
|
|
|
|
def cmd_play(args) -> int:
|
|
steps = config.load_routine(args.name)
|
|
cfg = config.load(MACRO.NAME, MACRO.DEFAULTS)
|
|
runner = Runner(f"routine {args.name}", cfg["hotkeys"])
|
|
|
|
def tick():
|
|
routine.play(runner, steps, args.speed)
|
|
runner.stopped = True
|
|
|
|
runner.log(f"{len(steps)} steps — focus Roblox and press "
|
|
f"{cfg['hotkeys'].get('pause', 'f7').upper()} to play")
|
|
runner.loop(tick)
|
|
return 0
|
|
|
|
|
|
def cmd_calibrate(args) -> int:
|
|
macro = game_of(args)
|
|
cfg = config.load(macro.NAME, macro.DEFAULTS)
|
|
cfg = macro.calibrate(cfg)
|
|
path = config.save(macro.NAME, cfg)
|
|
print(f"\nsaved to {path}")
|
|
return 0
|
|
|
|
|
|
def cmd_run(args) -> int:
|
|
macro = game_of(args)
|
|
try:
|
|
if macro is piano:
|
|
macro.run(mode="session" if args.session else
|
|
("once" if args.once else "loop"))
|
|
else:
|
|
macro.run(once=args.once)
|
|
except FileNotFoundError as error:
|
|
print(f"{error}")
|
|
print("record one first: python macro.py record build")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
def cmd_walk(args) -> int:
|
|
"""Walk one of the piano routes once, without playing anything."""
|
|
cfg = config.load(piano.NAME, piano.DEFAULTS)
|
|
steps = (cfg.get("routes") or {}).get(args.which) or []
|
|
if not piano.picked(steps):
|
|
print(f"nothing picked in the {args.which!r} route — the window "
|
|
f"(python macro.py gui) has a Pick button per step")
|
|
return 1
|
|
runner = Runner(f"route {args.which}", cfg["hotkeys"])
|
|
|
|
def tick():
|
|
piano.walk(runner, steps, f"walking the {args.which} route —")
|
|
runner.stopped = True
|
|
|
|
runner.log(f"{len(steps)} steps — focus Roblox and press "
|
|
f"{cfg['hotkeys'].get('pause', 'f7').upper()} to walk it")
|
|
runner.loop(tick)
|
|
return 0
|
|
|
|
|
|
def cmd_gui(args) -> int:
|
|
import gui
|
|
return gui.main()
|
|
|
|
|
|
def cmd_routines(args) -> int:
|
|
names = config.list_routines()
|
|
if not names:
|
|
print("nothing recorded yet — python macro.py record build")
|
|
return 0
|
|
for name in names:
|
|
steps = config.load_routine(name)
|
|
clicks = sum(1 for step in steps if step.get("do") == "click")
|
|
keys = sum(1 for step in steps if step.get("do") == "key")
|
|
seconds = sum(float(step.get("after", 0)) for step in steps)
|
|
print(f" {name:<20} {len(steps):>3} steps ({clicks} clicks, {keys} keys, {seconds:.0f}s)")
|
|
return 0
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
parser = argparse.ArgumentParser(prog="macro.py", description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
subs = parser.add_subparsers(dest="command", required=True)
|
|
|
|
window = subs.add_parser("gui", help="open the window")
|
|
window.set_defaults(func=cmd_gui)
|
|
|
|
check = subs.add_parser("check", help="is any of this going to work here?")
|
|
check.add_argument("--live", action="store_true",
|
|
help="also press space in game to prove input lands")
|
|
check.set_defaults(func=cmd_check)
|
|
|
|
mouse = subs.add_parser("mouse", help="live cursor position and colour")
|
|
mouse.set_defaults(func=cmd_mouse)
|
|
|
|
record = subs.add_parser("record", help="record clicks and keys into a routine")
|
|
record.add_argument("name")
|
|
record.add_argument("--note", help="what this routine is for")
|
|
record.set_defaults(func=cmd_record)
|
|
|
|
play = subs.add_parser("play", help="replay a recorded routine once")
|
|
play.add_argument("name")
|
|
play.add_argument("--speed", type=float, default=1.0,
|
|
help="scale the gaps between steps (default 1.0)")
|
|
play.set_defaults(func=cmd_play)
|
|
|
|
calibrate = subs.add_parser("calibrate", help="capture the points a macro watches")
|
|
calibrate.add_argument("--game", choices=sorted(GAMES), default="animevanguards")
|
|
calibrate.set_defaults(func=cmd_calibrate)
|
|
|
|
run = subs.add_parser("run", help="play the game")
|
|
run.add_argument("--once", action="store_true",
|
|
help="one match, or one song, then stop")
|
|
run.add_argument("--session", action="store_true",
|
|
help="piano: the whole event — in through the menus, then every "
|
|
"song at every difficulty, resuming where it left off")
|
|
run.add_argument("--game", choices=sorted(GAMES), default="animevanguards")
|
|
run.set_defaults(func=cmd_run)
|
|
|
|
walk = subs.add_parser("walk", help="walk a piano route once, to check it")
|
|
walk.add_argument("which", nargs="?", default="into", choices=["into", "back"],
|
|
help="into the event, or back to the song list")
|
|
walk.set_defaults(func=cmd_walk)
|
|
|
|
routines = subs.add_parser("routines", help="list recorded routines")
|
|
routines.set_defaults(func=cmd_routines)
|
|
|
|
args = parser.parse_args(argv)
|
|
return args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if sys.platform != "win32":
|
|
print("KittyMacro drives Windows input directly — it only runs on Windows.")
|
|
raise SystemExit(2)
|
|
raise SystemExit(main())
|