Tango is fan-made open source rollback netplay for Mega Man Battle Network. Follow the project on Twitter or join the Discord!
Mega Man Battle Network is perhaps one of the less popular Mega Man series today, but back in its heyday sold millions of copies worldwide, spawning both an anime series and oodles licensed merchandise (including, apparently, an official children’s silverware set).
The series features a world where for where terrorists and elementary school children settle their disputes via Tamagotchi battles, which is the main focus of the gameplay: beneath the slightly wacky worldbuilding, the underlying gameplay is a weird mix of a trading card game with an action RPG with… chess…?
The unique gameplay and surprising depth of the game has spawned a competitive scene centered around the sixth installment in the series, widely considered to be the most balanced. Instead of going through all the details, Akshon Esports has put out a video about the history of the game and also the scene. If you’re only here for the technical parts and not this rambling background info, feel free to skip right ahead!
The state of affairs for actually getting this up and running was pretty involved, however:
-
The only emulator that supported netplay in a reasonable way was VBALink 1.8. It’s the only emulator that supports GBA Wireless Adapter emulation, if only barely (if you looked at it funny you would get weird crashes and desyncs), and the source code for its wireless adapter emulation is completely lost to time.
-
Matchmaking with a player could only be done via direct IP connection: in practice, in the absence of port forwarding, it involved using Hamachi or Radmin VPN.
How can this be easier?
From the outset, a few approaches seemed possible.
Fix up the wireless adapter code in VBALink 1.8. I didn’t end up looking into this much at all as the original code for the emulator wasn’t available, and the GBA wireless adapter itself had opaque firmware with code that definitely wasn’t available.
Switch to link cable mode. Battle Network 6 allows connection over both link cable and wireless adapter. The link cable protocol is much more understood and has been relatively well documented by GBATEK. Unlike wireless adapter mode which is able to compensate for delayed packets, link cable I/O clocks a secondary GBA to the primary GBA it’s connected to and I/O is done synchronously. Due to the synchronicity, in a naïve implementation each player will not be able to advance their state until the packet is received. Adding support for rollback would involve understanding the protocol the game used to transmit inputs, which seemed painful.
Emulate two GBAs and just send inputs around. This is actually the generic solution that I like the most and works for every game: we simulate both the primary and the secondary GBA and run both of them at the same time and send their link cable I/O to each other: when an input comes in, we reload to the previous state where both inputs for a given frame were known and apply the input, resending the link cable I/O data. It has some weird quirks in Battle Network 6 though: the link cable mode actually has asymmetric delay! You can test this out in an emulator that supports link cable mode, such as mGBA: MegaMan.EXE on the primary side will start his movement 11 frames in and MegaMan.EXE on the secondary side will start his movement 10 frames in. This is also in contrast to the non-link battle behavior where MegaMan.EXE will start his movement 4 frames in!
Inject the input into the game directly. This is the approach I ended up going with and the approach I was initially super skeptical of due to fear of its complexity. However, this happened to be already some of a well-trodden path, already done in a project that added rollback netplay to a previous installment in the series, Battle Network 3 (go check it out!).
After figuring out a viable enough plan of attack, it was time to get started.
The first part of figuring out where all of this was handled was to figure out a way to get at the code of the game in a reasonable way. Most people swear by no$gba and its debugger, but having never done any of this before I opted to try a different combination dynamic-static analysis approach using:
-
mGBA: mGBA has a built-in debugger for setting read/write watchpoint and code breakpoints, as well as live memory viewer. It also has stack tracing and is able to reconstruct call stacks automatically, which proved to be super useful.
-
Ghidra: Ghidra is a reverse engineering toolkit from the NSA (yes, that NSA!) that has pretty good support for GBA code (ARM32/Thumb2). As a side note, they’re surprisingly responsive to fixing bugs about reverse engineering GBA games so thank you US taxpayers for sponsoring Tango!
Reverse engineering is a lot like smashing your computer onto the floor repeatedly and looking at the parts that come out to figure out where they came from originally. It’s also a bit like being a detective, where you follow the scent of one value in memory changing back to what changed it, and then what changed that.
Here, we have the battle against our good friend ProtoMan.EXE. He’s going to get beaten up a lot, but it’s for the greater good.
First of all, we want to find where our inputs are even going. GBATEK tells us that input is written into the KEYINPUT
MMIO register at 0x04000130
as a 16-bit number.
Sure enough, it’s there: 03FF
is the value we’re looking for. As we press buttons, the value changes: if the down button is held, then it becomes 037F
. A weird quirk is that this is that set bits indicate the button is not held. Moving along, let’s see where this value ends up going: let’s set a read watchpoint for KEYINPUT
and see what reads from it.
Looks like the code at 0x080003F8
is the first thing to touch it. We can now jump over to Ghidra and take a look.
As we can see, we get a good idea of where the KEYINPUT
value is flowing to. In particular, we’re interested in this section:
mov r7,r10
ldr r0,[r7,#0x4]
; ... elided ...
ldr r4,[->KEYINPUT]
ldrh r4,[r4,#0x0]=>KEYINPUT
mvn r4,r4
; ... elided ...
strh r4,[r0,#0x0]
The game:
-
Loads
r10
+0x4
intor0
. Thanks to information from the dism-exe project,r10
i