Adding rollback netplay to a Game Boy Advance game from 2005: Part 1
Getting started by poking around the game blindly until something hopefully starts making sense
Tango is fan-made open source rollback netplay for Mega Man Battle Network. Follow the project on Twitter (@tangobattle) or join the N1GP 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?
Figuring out what to do
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.
Assembling the tools
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!
Inspecting the game
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
0x080003f6 (addresses reported in the stack trace are always the instruction after the executed one) 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]
Loads, which makes this a pointer to the joypad struct.
r0. Thanks to information from the dism-exe project,
r10is a fixed pointer to the toolkit struct
r4, then slices the top 16 bits off.
r4to turn it into a bitmask of keys being pressed.
r4into the first field of the joypad struct.
Not much else is done with
KEYINPUT afterwards, so we’ll have to continue the hunt and look at the joypad struct now. For the purposes of this blog post not being super tedious I won’t be relabeling any variables or defining structs, but at this point you really should!
0x0200a270 and sure enough, here are our negated joyflags:
There’s some other joyflags here but we can just ignore the other two for now, unless something gets more interesting. Let’s set a read watchpoint on the joyflag field we wrote to.
We’ve already seen the code for
0x080003fa and we know it’s not super exciting, so let’s take another step.
Now we’re talking! What’s at
This is a bunch of code, but we can see the familiar read from
r10+0x4 being copied to a fixed offset in memory,
0x02036782. Sure enough:
FC00 again. Now, some other parts this function are kind of interesting as well: in particular, let’s look at this code:
ldrh r1,[r5,#0x2]=>DAT_020399f2 mov r2,#0xfc lsl r2,r2,#0x8 and r2,r1 ; ... elided ... ldrh r1,[r5,#offset DAT_02039a02] mov r2,#0xfc lsl r2,r2,#0x8 and r2,r1
Two sets of joyflags? Could this be what we’re looking for?
Fighting versus ProtoMan.EXE it looks it like this is just duplicated. What if we did an actual link battle?
We’ve found it! It looks like these are the locations for local and remote input. Whew, that’s it for today!
Now that we’ve found what we think is the location of inputs in memory, we can try messing around with it. Until next time, though!
Battle Network 6 has a lot of handrolled assembly, so the code doesn’t use a standard calling convention a lot of the time. In particular,
r10 is usually fixed to be a pointer to the toolkit and
r5 (not seen here) is often used to pass pointers to “relevant” structs such as battle objects or battle state. In Ghidra, these end up showing up with names like
unaff_r10 to indicate that an unaffected register value is being used. You can edit the storage by right clicking a function prototype and selecting Edit Function Signature.