Not intended!

What is Blogvent? (tl;dr I am going to write one post a day for all of December as a way to practice writing.)

On the last episode…

So yesterday I was playing with the game VVVVVV and giving the game pre-programmed keyboard inputs to play the game.

Today I wanted to run an actual real world Tool Assisted Speedrun input file. From a library called libTAS.

The file format

The libTAS input file looks something like this:

|K|M0:0:A:.....|
|K|M0:0:A:.....|
|K76|M0:0:A:.....|
|K76|M0:0:A:.....|
|K73:76|M0:0:A:.....|
|K73:76|M0:0:A:.....|
|K|M0:0:A:.....|
|K|M0:0:A:.....|
|Kff0d|M0:0:A:.....|

This file is describing 9 frames of input, for the first two there is no keyboard key pressed, mouse is at 0,0 and no analogue input (controller).

After that we have 2 frames of K76 and then 2 frames of K73:76, then 2 frames of nothing and then 1 frame of Kff0d.

What is going on here? K76 is key 76 in hex, or the lowercase letter v in ASCII. K73:76 is both 73 and 76 at the same time, or the s and v keys. Then we’re back to nothing. So you hold v for 4 frames and after holding v for 2 frames you hold v and s for 2 frames.

Then what is Kff0d, well ff0d is one of the Xlib KeySim definitions because libTAS is a linux based library. ff0d is the Enter key.

The exploit

There is a Tool Assisted Speedrun of VVVVVV that is short (33 seconds) and uses quite a few glitches to finish the game. The video is over 6 minutes but only the first 30 seconds actually matter, the rest is credits.

The creator of this speedrun Elomavi has a great writeup of what is going on and they also share the input file.

So what I wanted to do today was to parse the input file and try to recreate the video.

Parsing

I read the input file line by line and I want to find out what buttons I should be pressing for each frame. I notice that there are never more than 3 buttons being pressed at a time so that is my output [u32; 3].

fn parse_line(line: &str) -> [u32; 3] {
    let mut result = [0, 0, 0];

    // split the line on | and return if it failed
    // all lines should have at least 1 |
    let segments: Vec<&str> = line.split('|').collect();
    if segments.is_empty() {
        return result;
    }

    // return empty when the segment is just |K|
    let keys = &segments[1][1..];
    if keys.is_empty() {
        return result;
    }

    // if it just contains 1 number, convert it to int
    // assign it to the first value and return
    if !keys.contains(':') {
        result[0] = u32::from_str_radix(keys, 16).unwrap();
        return result;
    }

    // otherwise split on : and for each entry, convert and assign
    let key_segments: Vec<&str> = keys.split(':').collect();
    for (index, key) in key_segments.iter().enumerate() {
        result[index] = u32::from_str_radix(key, 16).unwrap();
    }

    result
}

Then for each array of key presses, I need to figure out what key I should press and what key I should release. Because if there are 2 frames of a key being pressed, I want to keep holding it down until the file tells me to release.

So I have a function that takes in the current keys for this frame and the keys from the last frame and find the difference between the two. Looks like this.

fn keys_to_press(old_inputs: &[u32; 3], new_inputs: &[u32; 3]) -> [u32; 3] {
    let mut to_press: [u32; 3] = [0, 0, 0];
    let mut to_press_index = 0;

    for new_key in new_inputs {
        if *new_key == 0 {
            continue;
        }

        // if the new key isn't in the array of
        // previous keys, then it's a key we need to press
        if !old_inputs.contains(new_key) {
            to_press[to_press_index] = *new_key;
            to_press_index += 1;
        }
    }

    to_press
}

Then it’s just a matter of pressing the keys in the keys_to_press array and releasing the keys from a keys_to_release array and then sleeping for 1 frame.

But there’s a problem. I’ve done some work, so I can’t sleep for exactly 16.666 milliseconds. So I have a timer that calculates how long this has taken and subtracts that from 16.666 and sleeps for that amount.

let now = Instant::now();

// ... the work

let elapsed = now.elapsed().as_micros();
let sleep_time = 16667 - elapsed;
sleep(Duration::from_micros(sleep_time as u64));

Test

Now it’s just a matter of testing it… and it doesn’t work. I’ve been working on this for the past few hours and the best I’ve gotten is the intro skip that the TAS does but then it desyncs soon after that. You can see that around 13 seconds in the video that it should hit the teleporter but it doesn’t.

The website says it runs at 58.8 fps so perhaps I need to try that tomorrow. I think I also need to read a bit how libTAS works, but until then enjoy my little TAS bot trying to run an existing tool assisted speedrun.

Comments

If you want to chat about this, Twitter or Mastodon