#! /usr/bin/env python3 import argparse import subprocess import time def ydotool(key, delay=60, multiplier=1): # This runs with a bit of a delay (by default, modulated by multiplier) because # Patapon -- the game, not the emulator or the input layer -- will drop inputs # if it's too low. subprocess.run([ "ydotool", "key", str(key) + ":1", str(key) + ":0", "--key-delay", str(100 / multiplier) ]) def main(): # A list of all possible songs, expressed as their cardinal directions # We'll map these to keystrokes later songs = [ # March of Mobility "L-L-L-R-", # Aria of Attack "R-R-L-R-", # Lament of Defense "U-U-L-R-", # Hold-Tight Hoe-Down/Concerto of Charge "R-R-U-U-", # Melody with a Bounce/Jingle of Jump "D-D-U-U-", # Ballad of 1999/Pizzicato of Party "L-R-D-U-", # Song of Miracles (Djinn) "D-DD-DD-", # Leisurely Lullaby # Not sure why you'd want to cast this on repeat, but you can I guess "L-R-L-R-", # Step Back Strut "U-L-U-L-" ] # A mapping of characters in our encoded songs to keys to send to ydotool # These should be the default bindings for PPSSPP keymapping = { "U": "31", "D": "44", "L": "30", "R": "45" } drummapping = { "U": '\033[32mCHAKA\033[0m', "D": '\033[33mDON\033[0m', "L": '\033[31mPATA\033[0m', "R": '\033[34mPON\033[0m' } parser = argparse.ArgumentParser( description="Play a sequence of Patapon commands on repeat" ) parser.add_argument('song',default="R-R-L-R-",nargs="+",choices=songs,help="The song to play. Defaults to Aria of Attack. When expressing a song, use eighth notes, dashes, and cardinal directions to designate the drums. For example, party would be \"L-R-D-U-\", and djinn would be \"D-DD-DD-\"") parser.add_argument('--bpm',type=float,default=1,help="Multiplier for the BPM. Change if you're running the game at a higher speed") parser.add_argument('--iterations',type=int,default=10000,help="Number of iterations of the sequence to run. Defaults to 10,000") parser.add_argument('--key-delay',type=int,default=60,help="Number of milliseconds to hold each key down for. Defaults to 100. Tune if you're getting dropped inputs or if the program can't keep up") parser.add_argument('--startup-delay',type=int,default=0,help="Number of milliseconds by which we should \"start early\". Adjust this value if, despite syncing on-beat, the program is late. The program will never be early -- if it feels like it is, it's just WAY late") args = parser.parse_args() # Schedule out our beat interval # 120.000 Naiive, does not work # 119.905 Still fast # 119.900 Still fast # 119.890 Just baaaaarely too fast # 119.880 # 119.000 Wicked slow, outpaces in like 4 measures bpm_constant = 119.880 print(f"Game BPM: {bpm_constant} BPM * {args.bpm}") beat_interval = 60 / (bpm_constant * args.bpm * 2) print(f"Beat interval: {beat_interval * 1000}ms per eighth note") # This sanity check of min() ensures that we'll never delay by more than a # beat. Honestly, delaying by a beat makes no sense, but delaying by more # than one makes even less. startup_delay = min(beat_interval * 2, args.startup_delay / 1000) print(f"Startup delay: {startup_delay * 1000}ms") # Set up the environment sequence='--------' + '--------'.join(args.song) print(f"Song sequence: {sequence}") remaining_iterations = args.iterations lastbeat = 0 # Wait for user confirmation input("Press enter on-beat to sync up with Patapon...") synctime = time.perf_counter() + startup_delay # Play da notes while remaining_iterations > 0: for i, key in enumerate(sequence): while time.perf_counter() < synctime + lastbeat: pass lastbeat += beat_interval button = keymapping.get(key, '-') if button != '-': ydotool(key=button, delay=args.key_delay, multiplier=args.bpm) print(drummapping.get(key, '-'), end="", flush=True) else: print(" ", end="", flush=True) remaining_iterations -= 1 print(f"~ ({remaining_iterations} remaining)") main()