8

Adding rollback netplay to a Game Boy Advance game from 2005: Part 1

 1 year ago
source link: https://tangobattle.substack.com/p/adding-rollback-netplay-to-a-game?s=r
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

15 hr ago

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?

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.

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F67a5b482-e14c-4523-9c29-4f18d449507c_1184x920.png

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.

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F6aeb968b-d7af-4521-bca6-26204a632dbf_1376x1158.png

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.

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2f371c13-48b4-4728-96ba-b1c57eaedf21_1116x986.png

Looks like the code at 0x080003F8 is the first thing to touch it. We can now jump over to Ghidra and take a look.

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ffecab4ee-99dd-4d5f-beb2-798333ae56c3_2826x1950.png

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:

  1. Loads r10+0x4 into r0. Thanks to information from the dism-exe project, r10 is a fixed pointer to the toolkit struct1, which makes this a pointer to the joypad struct.

  2. Loads KEYINPUT into r4, then slices the top 16 bits off.

  3. Negates r4 to turn it into a bitmask of keys being pressed.

  4. Stores r4 into 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!

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F08f9071f-2048-4965-81c4-2d1b54fc6b9b_1376x1158.png

It’s at 0x0200A270 and sure enough, here are our negated joyflags:

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F266a499b-54a2-437d-b9b5-b095215f9f0d_1376x1158.png

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.

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F101b2026-89d5-4b3b-91d9-08d330ed10b7_1184x1280.png

We’ve already seen the code for 0x080003FC and we know it’s not super exciting, so let’s take another step.

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F7418cf1e-4742-4dc8-9d49-dc5d89bd005c_1184x1280.png

Now we’re talking! What’s at 0x0801FF8C?

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F94c631a2-6427-4cb4-9446-b4061791bd9b_2826x1950.png

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:

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2a6d990d-e181-43a9-8d7c-373911068b05_1376x1162.png

There’s our 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?

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F9310e283-98da-4326-a325-8e4d4828fb83_1376x1162.png

Fighting versus ProtoMan.EXE it looks it like this is just duplicated. What if we did an actual link battle?

https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb3692259-8e41-4f6e-8441-44bf793a9ad4_1184x920.png
https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0756343-b46f-4cb5-b5fe-abf3504aebf3_1288x1074.png

We’ve found it! It looks like these are the locations for local and remote input. Whew, that’s it for today!

What’s next

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!

1

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_r5 and 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.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK