#!/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())