138 lines
5.4 KiB
Python
Executable File
138 lines
5.4 KiB
Python
Executable File
#! /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(delay / 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": {
|
|
"string": "CHAKA",
|
|
"color": 32
|
|
},
|
|
"D": {
|
|
"string": "DON",
|
|
"color": 33
|
|
},
|
|
"L": {
|
|
"string": "PATA",
|
|
"color": 31
|
|
},
|
|
"R": {
|
|
"string": "PON",
|
|
"color": 34
|
|
},
|
|
}
|
|
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('--dry-run',action='store_true',help="Don't call out to ydotool, just pretend to")
|
|
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
|
|
|
|
# Get a ghost string to use for fancy input display stuffs
|
|
ghoststring="\033[37m"
|
|
for i, key in enumerate(sequence):
|
|
ghoststring += drummapping.get(key,{}).get('string',' ')
|
|
ghoststring+="\033[0m"
|
|
|
|
# Wait for user confirmation
|
|
input("Press enter on-beat to sync up with Patapon...")
|
|
synctime = time.perf_counter() + startup_delay
|
|
|
|
# Play da notes
|
|
try:
|
|
while remaining_iterations > 0:
|
|
print(ghoststring, end="\r", flush=True)
|
|
for i, key in enumerate(sequence):
|
|
while time.perf_counter() < synctime + lastbeat:
|
|
pass
|
|
lastbeat += beat_interval
|
|
button = keymapping.get(key, '-')
|
|
if button != '-':
|
|
if not args.dry_run:
|
|
ydotool(key=button, delay=args.key_delay, multiplier=args.bpm)
|
|
print(f"\033[{drummapping.get(key,{}).get('color',0)}m{drummapping.get(key,{}).get('string','-')}\033[0m", end="", flush=True)
|
|
else:
|
|
print(" ", end="", flush=True)
|
|
remaining_iterations -= 1
|
|
print(f"~ ({remaining_iterations} remaining)" + " " * len(str(args.iterations)), end="\r")
|
|
except KeyboardInterrupt as e:
|
|
print("Interrupt")
|
|
pass
|
|
|
|
print(f"Total iterations: {args.iterations - remaining_iterations}")
|
|
return 0
|
|
|
|
main()
|