#!/usr/bin/env python
# vim: set expandtab tabstop=4 shiftwidth=4:

import argparse
import subprocess

# Stupid little script to auto-run the Cannon challenge from the game Owlboy.
# Attempts to acquire all rings on the course, rather than trying to get any
# kind of speed score.  Uses the `xdotool` utility to actually handle the
# input - this just exists for an easy way to manage xdotool arguments.
#
# It expects the player to start right on top of the "Ranking" sign, on the
# ground to the right of the cannon, as if the player's just respawned after
# an unsuccessful attempt.
#
# Limitations!
#
#    1) Whether due to accuracy threshholds on 'sleep' commands, multiprocess
#       timing issues, xdotool, Xorg, or possibly the game itself, this utility
#       is NOT entirely consistent.  There's one point at which you'll run into
#       the wall maybe 20% of the time, and the very end is quite inconsistent
#       as well (there's various fail conditions there).  It appears to work
#       enough of the time, though, and is certainly more consistent than *I*
#       am.  It may take a few runs to entirely make it through.
#
#    2) I started this after a few manual attempts, when I'd already gotten a
#       few of the initial groups of rings, so I'm not 100% sure that it does
#       get all of them.  Specifically, it looks to me like we might be a little
#       too high for the "bottom" rings on the initial circle.  Also the very
#       last rings may end up getting missed 'cause I rejiggered the end after
#       realizing I'd missed all the ones on the far wall, so I'm not sure if
#       they're being hit properly anymore.
# 

parser = argparse.ArgumentParser(
    description="Cheat at Owlboy's Boguin Cannon - get all rings")

parser.add_argument('-v', '--verbose',
    action='store_true',
    help='Show full xdotool command')

args = parser.parse_args()

commands = ['xdotool']
LEFT=1
RIGHT=3
initial_pause=3
total_time=0.0

def mouse(button, length=0.1):
    global commands
    global total_time
    total_time += float(length)
    commands.extend([
        'mousedown', str(button),
        'sleep', str(length),
        'mouseup', str(button),
        ])

def key(button, length):
    global commands
    global total_time
    total_time += float(length)
    commands.extend([
        'keydown', button,
        'sleep', str(length),
        'keyup', button,
        ])

def sleep(length):
    global commands
    global total_time
    total_time += float(length)
    commands.extend([
        'sleep', str(length),
        ])

# Give time to focus the game
sleep(initial_pause)

# Fly, get up to cannon height
key('w', 0.1)
sleep(0.1)
key('w', 0.5)

# Move left to cannon
key('a', 0.35)

# Exit flying
mouse(RIGHT)

# Get in cannon
sleep(0.2)
mouse(RIGHT)

# Launch
sleep(0.3)
mouse(LEFT)

# Reset our total_time - it takes maybe three seconds
# to reach the point where the official timer starts.
# That's off by a bit (I think it makes us think we're
# taking longer than we actually are) but whatever, it's
# just an estimate.
total_time = -3.0

# Veer right, then straighten out
sleep(5.8)
key('d', 1.3)
sleep(.5)
key('a', .3)

# Loop back up again and go down the tunnel
sleep(.5)
key('a', 1.5)

# Veer off to the left at the bottom of the tunnel
sleep(3.75)
key('a', 1.1)

# Gain a bit more height on that climb - this is where
# we may run into a wall, if fate isn't being kind to us.
sleep(0.2)
key('a', .4)
sleep(.1)
key('a', .4)
sleep(.1)
key('a', .4)

# Start the initial loop-de-loop
sleep(1.7)
key('a', 1.0)

# Maintain a bit of height at the top of the loop
sleep(.4)
key('a', .3)

# Veer back to the right on the bottom of the loop
sleep(1.0)
key('d', 1.2)

# Veer more to the right as we fall down the opening
sleep(1.1)
key('d', 0.7)

# More veering right down the chasm
sleep(1.0)
key('d', 0.9)

# And a bit of adjustment so that we make it into the opening
sleep(0.5)
key('d', 0.3)

# Veer off into the corridor to the right
sleep(1.7)
key('d', 0.8)

# Keep some height to get to the other side of the room
sleep(0.5)
key('d', 0.4)
sleep(0.2)
key('d', 0.4)

# Now swerve left to get the last few rings and exit - From here
# on out we're pretty susceptible to all the little imperfections
# of previous timings; sometimes we might not pull up in time,
# others we might go too early.
sleep(1.8)
key('a', 0.6)

# Maintain a little bit of height for the last bit
sleep(0.4)
key('a', 0.4)
sleep(0.4)
key('a', 0.4)

# Run!
print('')
print('xdotool arg count: {}'.format(len(commands)-1))
if args.verbose:
    print('Full command:')
    print('   {}'.format(' '.join(commands)))
    print('')
print('Total time logged by commands: {:0.2f} sec'.format(total_time))
print('Focus the Owlboy game (unpaused) within {} seconds!'.format(
    initial_pause))
subprocess.run(commands)
print('')
print('Done!')
