Compare commits

..

20 Commits

Author SHA1 Message Date
ad7d65cd80 More minor fixes to formatting 2025-01-30 14:49:39 -06:00
7c01b9a17c Minor fix to output formatting 2025-01-30 14:48:37 -06:00
86641f9959 Wait less busily
This resolution is still fine for perfect inputs as it's a much higher frequency than the polling rate of the game
2025-01-30 14:47:31 -06:00
38915249af Fancy output displays 2025-01-30 14:46:13 -06:00
ec3c6a373b Add dry-run argument 2025-01-30 14:36:35 -06:00
82b4c101c1 Add more to the README 2025-01-30 14:00:52 -06:00
aa78f60ed7 Add note about timing inputs 2025-01-30 13:44:13 -06:00
bdd951afac Fix not actually using key-delay 2025-01-30 13:43:13 -06:00
98c86659ed More README improvements 2025-01-30 13:40:23 -06:00
42590642ac Polish README 2025-01-30 13:32:40 -06:00
cb97c0d821 Fix link 2025-01-30 13:24:47 -06:00
c37262bcfc Add README 2025-01-30 13:24:20 -06:00
ba2dcd2181 Minor log tweak 2025-01-30 07:16:09 -06:00
de646da541 Add interrupt handler and finishing logic 2025-01-30 05:50:43 -06:00
3a7c77c3a2 Add startup delay, adjust logging 2025-01-30 04:43:48 -06:00
3cc391d210 Minor, minor minor tuning based on running this for 10 minutes grinding 4 spears
I had a BPM calculator going for 10 minutes and it KNOWS we're in the 119.88 range, maybe a high 119.87 but probably closer to mid-.88
2025-01-30 04:31:58 -06:00
d86f5dbfb3 Tuning 2025-01-30 04:05:18 -06:00
336c4ee36f Clarify constant, try a lower BPM 2025-01-30 04:03:20 -06:00
b5e77c1e31 Modulate ydotool delay, fix bug, comment 2025-01-30 04:02:44 -06:00
c14f537a88 Tweaking, tuning, and cleanup
Apparently we need to be using keycodes for this for reliability. Also, the 100ms delay is required, otherwise Patapon (the game, not the emulator or the input layer) won't pick up on it
2025-01-30 04:00:14 -06:00
2 changed files with 201 additions and 29 deletions

121
README.md Normal file
View 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")

View File

@@ -3,11 +3,17 @@ import argparse
import subprocess
import time
def ydotool(key):
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",
"type",
str(key)
"key",
str(key) + ":1",
str(key) + ":0",
"--key-delay",
str(delay / multiplier)
])
def main():
@@ -37,28 +43,56 @@ def main():
# A mapping of characters in our encoded songs to keys to send to ydotool
# These should be the default bindings for PPSSPP
keymapping = {
"U": "s",
"D": "z",
"L": "a",
"R": "x"
"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'
"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 forever"
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=int,default=120,help="The BPM of Patapon. Change if you're running the game at a higher speed")
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
beat_interval = 60 / (args.bpm * 2)
print(f"Beat interval: {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)
@@ -66,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
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 != '-':
ydotool(button)
print(drummapping.get(key, '-'), end="", flush=True)
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)")
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()