Wii U Replay Device

16 Jul 2021

In spring of 2020, I set out with the goal of making a replay device for the Wii U and advancing the state of TAS console verification on modern Nintendo consoles. My original demonstration left a lot to be desired, but now I have a much more promising result to share.

If that sounded like a bunch of gibberish, here’s a bit of background. A speedrun is a play-through of a game, or a portion of a game, done with the added goal of going fast. Essentially, a speedrun is accomplishing some goal as quickly as possible. Speedruns often take advantage of small bugs in the game, or just unintended strategies, that can be exploited to skip portions of a level, skip whole levels, skip cut-scenes, or really any part of a game in between the start and the end. A TAS is a tool-assisted speedrun, which is a speedrun that uses additional tools to play the game with a precision and consistency above that of a human. Some of those aforementioned tricks are so specific, it’s almost impossible for a human to execute them. A TAS can do them with ease. A TAS is often done within an emulator, where the game is not played on a real console, but played within a software recreation of the console on a computer. Emulators often have built-in TAS tools that help the TAS creator dial in on the timings and inputs needed to successfully pull off these amazing tricks.

While a TAS is already extremely impressive and fun to watch, wouldn’t it be cool if it was done on a real console instead? Enter console verification. A console verified TAS makes use of additional hardware to replay the inputs to a TAS into a real console.

For my replay device, I took advantage of the extension port on the bottom of the Wiimote. This port interfaces with the nunchuk, classic controller, or any other accessory through an encrypted 400Hz I²C bus. The encryption method for this interface was reverse engineered long ago, and now a microcontroller and some code can convince the Wiimote that there’s a classic controller plugged in. I used the 3V 12MHz version of the Adafruit Pro Trinket for my implementation. This microcontroller was a great fit for this project for a number of reasons. First, it is based on the tried and true ATmega328. This meant great documentation and plenty of existing code for me to build off of. It also can run off of a 3V supply, which happens to be the voltage provided by the Wiimote to any extension plugged in. This meant I could power the replay device directly off of the Wiimote instead of relying on an additional power source. Lastly, it runs at 12MHz. This probably isn’t necessary for this project, but most 3V Atmega328 boards run at 8MHz, and that extra speed can’t hurt when working with the time sensitive code here.

I was able to find a Wiimote library for the Atmega328 and take advantage of some existing code for the communication portion of this device. On the hardware side, I simply connected pin 1 of the extension port to A5, pin 3 to GND, pins 4 and 5 to BAT+, and pin 6 to A4. After that I just added some prerecorded inputs to the code and more or less had a replay device.

But it’s not that simple. While the replay device communicates with the Wiimote at a fixed rate, the Wiimote is still communicating with the Wii U over bluetooth. Not only does wireless communication introduce some latency variation, but the Wii U doesn’t even poll the Wiimote at a consistent interval either. Back in 2020, my workaround to this problem was… nothing. Wishful thinking and some misapplied theory had me hoping that the law of large numbers would take care of this problem. What I mean by this is, if my character incorrectly turns a little too far to the right at some point, that error would eventually be corrected by my character turning a little too far to the left later. Unfortunately, this theory really does not apply in 3D games the way I had hoped for. See, if my character turns a little too far, and then walks forward, making up for my rotation later does not account for the fact that where my character has translated to is still completely wrong. For my original demonstration, where I tried to run one track in Mario Kart 8, this issue can be seen in full effect. It’s not long before the character is driving into a wall and getting completely desynced with the inputs.

So just pick a game where the law of large numbers does apply, duh! But really, it was actually that simple.

I identified Excitebike within NES Remix Pack as a great candidate for the law of large numbers actually applying in the way I’d hoped for. There are three independent problems in Excitebike, the character’s position, the character’s pitch when landing a jump, and the character’s speed. These three issues are essentially independent. The position is not influenced by pitch like position would be influenced by rotation in a 3D game since they are not happening in the same plane.

With that in mind, I recorded some inputs to Excitebike, dumped them onto the replay device, and gave it a shot. Sure enough, it works like a charm. I was able to finish stage four in 19.1 seconds, beating the human world record by 0.3 seconds.

The demonstration can be seen on YouTube.

Now things are still not perfect. Since this is on a real console, I still have small problems plaguing me like inconsistent load times. When the stage is selected, it’s not always the same amount of time before the stage begins. In Mario Kart 8, I was able to account for that by connecting the replay device while the stage was loading and using the controller selection pop up to ensure that my inputs were synced with the beginning of the race. In Excitebike, I don’t get the same luxury. Instead, I have to hardcode a delay for the approximate time it takes the console to load the stage from the disc. Unfortunately, this means that the run is not 100% repeatable.

At some point it would be great to identify a game like excitebike where I can also take advantage of the controller selection pop up. I feel pretty confident that the repeatability would near 100% in that situation.

For now, that’s all! As always the code is also available on GitHub.