Compare commits
18 Commits
b5e77c1e31
...
master
Author | SHA1 | Date | |
---|---|---|---|
ad7d65cd80 | |||
7c01b9a17c | |||
86641f9959 | |||
38915249af | |||
ec3c6a373b | |||
82b4c101c1 | |||
aa78f60ed7 | |||
bdd951afac | |||
98c86659ed | |||
42590642ac | |||
cb97c0d821 | |||
c37262bcfc | |||
ba2dcd2181 | |||
de646da541 | |||
3a7c77c3a2 | |||
3cc391d210 | |||
d86f5dbfb3 | |||
336c4ee36f |
121
README.md
Normal file
121
README.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# autopon
|
||||
|
||||
A python script to automatically drum commands for Patapon games
|
||||
|
||||
## Overview
|
||||
|
||||
Autopon is a python script that automatically issues commands to your troops by specifying a sequence of inputs. It uses high-resolution perf timers to ensure it stays on-beat. It avoids a common footgun of other macro solutions in that I've already done the headache work of figuring out that Patapon is slightly slower than the 120BPM everybody says it is.
|
||||
|
||||
This script's purpose is primarily to aid in grinding, mostly for Patapon 3 class skills but potentially (or eventually) other things as well, like material/hunting missions, experience grinding, etc.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. `ydotool` must be installed, in `$PATH`, and its socket configured appropriately. If necessary, specify the envvar `YDOTOOL_SOCKET` per ydotool(1)
|
||||
|
||||
2. PPSSPP must be installed and running the game (obviously)
|
||||
|
||||
3. Bindings for PPSSPP must be their default: SQUARE on A, TRIANGLE on S, CIRCLE on X, and CROSS on Z
|
||||
|
||||
4. PPSSPP must have focus while the script runs
|
||||
|
||||
## Usage
|
||||
|
||||
### Invocation
|
||||
|
||||
See `./autopon.py -h` for a full list of usage options.
|
||||
|
||||
The short invocation is like so:
|
||||
|
||||
```
|
||||
./autopon.py L-L-L-R-
|
||||
```
|
||||
|
||||
Autopon will then prompt you to hit Enter on a drumbeat, after which you have 1 measure to change focus to the game. You'll need to [tune the startup delay](#startup-delay) as well.
|
||||
|
||||
### Exiting
|
||||
|
||||
Tab over to your terminal on a rest measure and give it Ctrl+C. Additionally, Autopon will exit if it hits 0 remaining iterations: see [Iterations/Repeating](#iterationsrepeating).
|
||||
|
||||
### Iterations/Repeating
|
||||
|
||||
By default, Autopon will repeat a sequence 10,000 times, which is enough for about 11 hours of nonstop usage for a single command. If you would like to increase or decrease this value, use the `--iterations` flag.
|
||||
|
||||
### Song Encoding
|
||||
|
||||
Songs are encoded as a sequence of characters where each character corresponds to an eighth note. The script will automatically provide a full measure of rest at the start of invocation and between every command, keeping up with the call-and-response syntax of Patapon's commands:
|
||||
|
||||
An example sequence of "March of Mobility" looks like this:
|
||||
|
||||
```
|
||||
L-L-L-R-
|
||||
```
|
||||
|
||||
Which will be automatically processed by the program into this looping sequence:
|
||||
|
||||
```
|
||||
--------L-L-L-R-
|
||||
```
|
||||
|
||||
The letters for each drum correspond to their cardinal direction on the face of a PlayStation controller:
|
||||
|
||||
| Drum | Character |
|
||||
| ------- | --------- |
|
||||
| Pata | L |
|
||||
| Pon | R |
|
||||
| Don | D |
|
||||
| Chaka | U |
|
||||
| Rest | - |
|
||||
|
||||
Thus "Aria of Attack" would be "R-R-L-R-", and "Song of Miracles" would be "D-DD-DD-".
|
||||
|
||||
Several commands can be chained in sequence to, for example, automate charge-attacking:
|
||||
|
||||
```
|
||||
./autopon.py R-R-U-U- R-R-L-R-
|
||||
[...]
|
||||
Song sequence: --------R-R-U-U---------R-R-L-R-
|
||||
```
|
||||
|
||||
All default Patapon 1/2/3 command songs are supported. Freestyling is currently prohibited, so using this program to summon Djinns in Patapon 1/2 or power one up in 3 is currently impossible.
|
||||
|
||||
## Tuning
|
||||
|
||||
Several components of this program are timing sensitive.
|
||||
|
||||
### Game BPM
|
||||
|
||||
As it turns out, Patapon (all of them) doesn't actually run at a round 120BPM. Instead, The actual observed BPM is 119.880BPM, and the program is hardcoded to only issue commands at this cadence. This value was refined through testing and ultimately picked by beating up the door at Castle of Justice for over 4 hours and measuring observed deviation, which was zero. There may still be an unknown figure, but 6 digits of precision is enough for most purposes.
|
||||
|
||||
**DO NOT** change the Game BPM if you are running the game at double-speed. To that end, use the `--bpm` argument to add a multiplier.
|
||||
|
||||
### Startup Delay
|
||||
|
||||
The striking rhythm, while consistent, has some level of processing overhead due to needing to call out to an extra binary. Thus there will be a consistent, unavoidable delay between the rhythm that the script keeps and the rhythm observed by the game. In order to sync these two rhythms up, a startup delay will need to be tuned, for which Autopon provides a mechanism.
|
||||
|
||||
The startup delay can be tuned by passing the `--startup-delay` parameter. Adjust this value by gutfeel until hitting Enter when prompted to sync the game up nails a perfect measure.
|
||||
|
||||
### Key Delay
|
||||
|
||||
The default value is such that you're unlikely to need to tune it, but if you need to, you can adjust how long a key is held by specifying `--key-delay`. Adjust this value if your game drops inputs -- Patapon is typically picky about anything under 50ms. Values too high may result in mistimed inputs.
|
||||
|
||||
## Glossary
|
||||
|
||||
| Term | Definition |
|
||||
| --------------- | ---------- |
|
||||
| command [song] | A four-drum sequence that corresponds to a directive for the Patapon army. For a list of all command songs in the games, [see here](https://patapon.fandom.com/wiki/List_of_Command_Songs). |
|
||||
| drum | One of four face buttons on the PlayStation controller. The act of hitting a drum is often called "striking" it. |
|
||||
| sequence | A sequence of eighth-note characters that is specified by the user and later decoded by this program to determine what drums to hit. Is intended to represent, but is not necessarily, a command song. |
|
||||
|
||||
## TODO
|
||||
|
||||
Features I'd like to add:
|
||||
|
||||
* Specifying a separate set of setup commands, so you could gain fever, charge-defend, and then defend forever to continue a hero mode or something
|
||||
|
||||
* Set up pre-defined sequences for common tasks, like running P3's defense minigame
|
||||
|
||||
* Creating a table of Djinn minigame answers for Patapon 1/2 and letting the user drop them in easily after a Djinn command
|
||||
|
||||
* Automatic return-to-hideout, re-run functionality for Patapon 3
|
||||
|
||||
* Figure out how the fuck I'm supposed to do Patapon 3's Djinn and its faster BPM on-the-fly (the answer is probably "don't")
|
92
autopon.py
92
autopon.py
@@ -3,17 +3,17 @@ import argparse
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
def ydotool(key, multiplier=1):
|
||||
# This runs with a 100ms delay (by default, modulated by multiplier) because
|
||||
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 any lower.
|
||||
# if it's too low.
|
||||
subprocess.run([
|
||||
"ydotool",
|
||||
"key",
|
||||
str(key) + ":1",
|
||||
str(key) + ":0",
|
||||
"--key-delay",
|
||||
str(100 / multiplier)
|
||||
str(delay / multiplier)
|
||||
])
|
||||
|
||||
def main():
|
||||
@@ -49,27 +49,50 @@ def main():
|
||||
"R": "45"
|
||||
}
|
||||
drummapping = {
|
||||
"U": '\033[32mCHAKA\033[0m',
|
||||
"D": '\033[33mDON\033[0m',
|
||||
"L": '\033[31mPATA\033[0m',
|
||||
"R": '\033[34mPON\033[0m'
|
||||
"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 Naiive, does not work
|
||||
# 120.000 Naiive, does not work
|
||||
# 119.905 Still fast
|
||||
# 119.900 Still fast
|
||||
# 119.890
|
||||
bpm_constant = 119.890
|
||||
# 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}")
|
||||
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)
|
||||
@@ -77,23 +100,40 @@ def main():
|
||||
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()
|
||||
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(button, multiplier=args.bpm)
|
||||
print(drummapping.get(key, '-'), end="", flush=True)
|
||||
else:
|
||||
print(" ", end="", flush=True)
|
||||
remaining_iterations -= 1
|
||||
print(f"~ ({remaining_iterations} remaining)")
|
||||
try:
|
||||
while remaining_iterations > 0:
|
||||
print(ghoststring, end="\r", flush=True)
|
||||
for i, key in enumerate(sequence):
|
||||
while time.perf_counter() < synctime + lastbeat:
|
||||
time.sleep(0.01)
|
||||
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("")
|
||||
print(f"Total iterations: {args.iterations - remaining_iterations}")
|
||||
return 0
|
||||
|
||||
main()
|
||||
|
Reference in New Issue
Block a user