Sharing scores is a big part of the rhythm game community. For me, it's been a great way to keep track of my progress and meet similarly skilled players to compete with. On the other hand, taking pictures of the screen with my phone each time got old fast. The process needed some streamlining and I had a plan.

It's been almost two years since my last game hacking post, so how about a new game? Enter beatmania IIDX INFINITAS, the first in the long running series to be released on PC, and a game that quickly became a personal favourite of mine. The gameplay is simple - hit the falling notes as they reach the bottom of the lane. Of course, as you ascend to higher levels it can get quite difficult.

At the end of each chart, the result screen is displayed. This contains a bunch of information such as the music title, difficulty rating, whether you failed or cleared, a letter grade, your final score, how you fared against an optional target score and how well timed each note hit was. You'll see a lot of these being posted around in any rhythm game community chatroom.

A picture of a IIDX result screen posted in Discord A typical example. It's no screenshot, but it's pretty good.

As far as overall quality goes, this is definitely one of the better ones. It's still mostly readable without clicking to view the full size image, it's not too blurry and, despite being partially cropped out, the song title is still legible. The opposite side of the screen is usually populated with the rival ranking leaderboard but some people prefer to play without it, leaving a lot of wasted space.

Now compare that with an official e-amusement mobile scorecard. Sure, it's missing a few things, most notably the graph, but it's compact, consistent and there's very little wasted space. Unfortunately, it's also exclusive to the arcade releases, but hey, that's no reason to give up. If they didn't want to make one for INFINITAS then I'll just have to make one myself.

Today's toolkit consists of Cheat Engine - an excellent memory scanner and my usual starting point with any unfamiliar game, ReClass.NET - an indispensable tool for dissecting structures in memory, and last but not least, IDA Pro for static analysis.

beatmania IIDX INFINITAS result screen Sample score that I'll be referencing throughout the post. Played on keyboard - stop judging!

Here's a quick breakdown of all the data I'll need for my replica scorecard. If the picture above just looks like a bunch of numbers, don't worry, I'll try my best to explain all the non-obvious stuff as it comes up.

With that out of the way, it's time to start up Cheat Engine and attach to the game process: bm2dx.exe.

Pro tip: INFINITAS runs in fullscreen, but you can get it to work in a window with DxWnd and these settings.

How about starting with those five judgement numbers? Considering how they're displayed on the result screen, I'm thinking they might be structured in memory as an array of contiguous 32-bit integers. If this is true, converting each number to hexadecimal, stringing them together and performing an Array of Byte scan in Cheat Engine should give me something to work with.

Five 32-bit integers in HxD Array of Byte scan in Cheat Engine

Off to a good start. As indicated by the green colored text on the left, both of these results are static addresses. This means I could set a new score or completely restart the game and I'd still be able to get to the judgement data using the same address.

The above screenshot doesn't tell the whole story though. I wouldn't reference this in my code using the address 0x34C82D4. When static results are added to Cheat Engine's address list they become relative to the module base. In this case, since the module is bm2dx.exe, which has a base address of 0x1390000, the final address would become bm2dx.exe+0x21382D4 instead.

There's no need for two addresses that point to the same data though, so which one should I use? In these situations, I try to filter the list down to addresses that are reliable, then check each to see if they're close to anything else. Keeping the amount of addresses low helps reduce the amount of maintenance required when an update inevitably breaks something.

A region of memory displayed in Cheat Engine
A region of memory displayed in Cheat Engine
It's a lot easier to see with the data display mode set to 4-byte Integer. (Ctrl+6)

Looks like 0x34C82D4 also has the COMBO BREAK, FAST and SLOW values nearby, making it the clear winner for now. I suspect those zeroes in-between might be used if I were playing on the P2 side, but that's something to confirm later.

Since that first scan worked out so well, why not apply the same technique to the next one?

Array of Byte scan in Cheat Engine Scanning for the best and current EX score values. (decimal: 935, 1116)

Once again, checking the memory region around this address reveals a few extra values of interest, specifically the best and current miss counts. Seems like a good find, but this isn't a static address. This won't necessarily be here again by the time I set another score, and it'll certainly be somewhere completely different if I were to restart the game now.

Fortunately, Cheat Engine can do a bit more than just scan memory. Right-clicking the address from the results window and selecting "Find out what accesses this address" pointed me in the right direction.

(In case you're following along, attempting this will almost certainly trigger anti-debugging protection and crash the game. You'll need to change the debugger method in the Cheat Engine settings dialog to "Use VEH Debugger" first.)

Tracing memory reads in Cheat Engine
Extra info dialog in Cheat Engine

Seems that four instructions are reading the value at this address. Just a guess, but perhaps these are rendering related instructions being called once per frame. In any case, these will make for good starting points when moving over to IDA.

Looking at the disassembly, it appears that the address in the ebx register, 0x216E6F4C, should denote the start of the class.

ReClass.NET dissecting a memory structure

Okay, so here's ReClass.NET. I've attached to bm2dx.exe, set the class address to 0x216E6F4C and named everything known.

Looking back at the result screen image, I can make some assumptions about those first four variables. Assuming that data is always in a pair of best and current values, and that the order is the same as how it appears on the result screen, it's likely that those first four nodes correspond to numeric representations of the clear type and DJ level.

I suppose setting a few different scores with various grades and clear types would confirm this, but there's a better way.

That first instruction was traced to 0x14AFEB4. After subtracting the module base address, 0x1390000, then adding the IDA image base, 0x400000, I finally get to an address that's usable in IDA: 0x51FEB4, which is inside function sub_51FE90.

I won't bother including a screenshot of this function as there were only a few EX score related strings and calls to what appear to be drawing related functions. Repeating the same trace on the miss count ended up at a similar function, sub_5200B0.

Neither as interesting as what they had in common - the single cross-reference to sub_51FC00.

IDA pseudo-code of a rendering related function sub_51FC00

I've left comments on the two functions I found from the previous Cheat Engine traces.

There's not much to go through here so why not take a look around, perhaps at sub_51FDC0? It's the function above the one that draws the EX score, so if my order theory applies, it might be a DJ level rendering function.

IDA pseudo-code of a rendering related function Not a bad guess.

That confirms at least two of the unknown fields in the class were indexes to a string table, specifically this one at off_B6B910.

.data:00B6B910                            ; "level_s_aaa"
.data:00B6B914    dd offset aLevelSAa     ; "level_s_aa"
.data:00B6B918    dd offset aLevelSA      ; "level_s_a"
.data:00B6B91C    dd offset aLevelSB      ; "level_s_b"
.data:00B6B920    dd offset aLevelSC      ; "level_s_c"
.data:00B6B924    dd offset aLevelSD      ; "level_s_d"
.data:00B6B928    dd offset aLevelSE      ; "level_s_e"
.data:00B6B92C    dd offset aLevelSF      ; "level_s_f"

Looking back to the class, the best DJ level has an index of 2, level_s_a, while the current DJ level index is 1, level_s_aa, so that's good. I went back and checked the function above this one, sub_51FCD0. As expected, the clear type string table was there and the indexes lined up once again. The best clear type index being 4, clear_clear, and the current being 5, clear_hard.

.data:00B6B950                            ; "clear_noplay"
.data:00B6B954    dd offset aClearFailed  ; "clear_failed"
.data:00B6B958    dd offset aClearAssist  ; "clear_assist"
.data:00B6B95C    dd offset aClearEasy    ; "clear_easy"
.data:00B6B960    dd offset aClearClear   ; "clear_clear"
.data:00B6B964    dd offset aClearHard    ; "clear_hard"
.data:00B6B968    dd offset aClearEx      ; "clear_ex"
.data:00B6B96C    dd offset aClearFullcombo ; "clear_fullcombo"

Finally, I ran Class Informer, which revealed the true class name to be StageResultDrawFrame. Thinking about it now, I probably should've done that a bit earlier. With the new findings included, the class now looked something like this.

ReClass.NET dissecting a memory structure Not pictured: some extra bytes below for testing later.

Now all that's left is the pacemaker, music information and a couple of things I'll probably need to blindly scan for.

Nothing around the memory region of StageResultDrawFrame resembled anything pacemaker related, but there were a few functions still left in sub_51FC00. Seemed like a safe bet to check the function directly under the miss count one, sub_5202B0.

A target_name string here and a score_no there indicated that this was indeed, a pacemaker related function. A quick glance at the pseudo-code was enough to find the source of the pacemaker data, a certain sub_520A40.

In here, there was a switch with 20 cases. Most of these were identical, but one in particular caught my attention.

IDA pseudo-code of a pacemaker related function

Remember how I mentioned earlier that one of the pacemaker types was a user-defined percentage? If my eyes aren't deceiving me, that looks like a printf format string - and that would make those two percentage symbols an escape sequence.

To confirm my suspicions, I set a breakpoint just after that call to XCgsqzn0000101, then set a new score.

Cheat Engine disassembler view with a fired breakpoint

Sure enough, the breakpoint fired and there was an address in the esi register. Accounting for that +2 from the pseudo-code, I subtracted 8 bytes from the address, resulting in 0x3DFF208, which I then opened up in ReClass for a better look.

ReClass.NET dissecting a memory structure

That first integer matches the one from the case, so that's surely the pacemaker type - most likely an index to another string table. Following that, the target EX score, and finally, the name string - only used when targeting a specific rival or custom percentage.

Fortunately, finding the relevant string table didn't take too long.

.data:00B637D8                            ; "sg_fno"
.data:00B637DC    dd offset aSgMonly      ; "sg_monly"
.data:00B637E0    dd offset aSgRiva1      ; "sg_riva1"
.data:00B637E4    dd offset aSgRiva2      ; "sg_riva2"
.data:00B637E8    dd offset aSgRiva3      ; "sg_riva3"
.data:00B637EC    dd offset aSgRiva4      ; "sg_riva4"
.data:00B637F0    dd offset aSgRiva5      ; "sg_riva5"
.data:00B637F4    dd offset aSgRivaTop    ; "sg_riva_top"
.data:00B637F8    dd offset aSgRivaAve    ; "sg_riva_ave"
.data:00B637FC    dd offset aSgAltop      ; "sg_altop"
.data:00B63800    dd offset aSgAlave      ; "sg_alave"
.data:00B63804    dd offset aSgLotop      ; "sg_lotop"
.data:00B63808    dd offset aSgLoave      ; "sg_loave"
.data:00B6380C    dd offset aSgDantp      ; "sg_dantp"
.data:00B63810    dd offset aSgDanav      ; "sg_danav"
.data:00B63814    dd offset aSgGhost      ; "sg_ghost"
.data:00B63818    dd offset aSgPacemakerAaa ; "sg_pacemaker_aaa"
.data:00B6381C    dd offset aSgPacemakerAa ; "sg_pacemaker_aa"
.data:00B63820    dd offset aSgPacemakerA ; "sg_pacemaker_a"
.data:00B63824    dd offset aSgPacemaker  ; "sg_pacemaker"

As luck would have it, getting to this data programmatically would be easy. I would only need to allocate enough memory to store all the above, meaning 264 bytes, then call sub_520A40 with a pointer to said memory.

With the pacemaker data found, it was time to bid farewell to sub_51FC00. There were a few other cool functions in there related to the graph and background art, but I'll leave that for another time since they weren't particularly useful for the scorecard.

Fortunately, that previous run of Class Informer provided no shortage of avenues to explore next.

IDA Class Informer results window

I'm still missing all the music related information so StageResultDrawMusicInfo seems like the next logical step. Excluding the destructor, only two entries in the virtual function table were unique. May as well start with the first one, sub_523350.

IDA pseudo-code of a music information function
Determining the active player with Cheat Engine

I'm surprised it took this long to find, but sub_544A90 looks like a way to determine whether the current player is on the 1P or 2P play side. Unlike the arcade releases which can have up to two people playing at the same time, INFINITAS is limited to one.

That's all for this function. Nothing resembling music data just yet, but there's still one more to look at.

IDA pseudo-code of a music information function sub_5233B0

I've highlighted some of the immediately interesting stuff, starting with the values returned by sub_546EE0 and sub_4A8560.

Judging by the strings in the two calls to sub_4A8E60, I'm guessing these two contain the current difficulty and play style respectively. The argument in sub_546EE0 most likely being the active player. The string tables all but confirm this.

.data:00B5FF3C 1C D8 B3 00             off_B5FF3C      dd offset aDiffSp       ; DATA XREF: sub_5233B0+34↑r
.data:00B5FF3C                                                                 ; "diff_sp"
.data:00B5FF40 14 D8 B3 00                             dd offset aDiffDp       ; "diff_dp"
.data:00B5FECC 08 D8 B3 00             off_B5FECC      dd offset aDiffNormal   ; DATA XREF: sub_5233B0+47↑r
.data:00B5FECC                                                                 ; "diff_normal"
.data:00B5FED0 FC D7 B3 00                             dd offset aDiffHyper    ; "diff_hyper"
.data:00B5FED4 EC D7 B3 00                             dd offset aDiffAnother  ; "diff_another"

I'll forgo including screenshots of those two functions as they both simply returned fixed addresses. Now for that last highlighted function, sub_545340 - this one led straight to the jackpot, returning a pointer to what appeared to be the current music entry.

ReClass.NET dissecting a memory structure

ReClass to the rescue once again. The first 256 bytes of this class contained the music title (twice?), the artist and the genre. To figure out the size of this structure, I simply added bytes until I saw what appeared to be another title. Very scientific.

After consulting RemyWiki, I was able to distinguish a few more values, including the note count and rating for each difficulty (sometimes easy to miss those 1 byte values when displayed as 4), and a few values that were seemingly related to the BPM.

The song in my test score has a constant BPM throughout, but there are plenty of others in the game that vary. The best example I could think of was ワルツ第17番 ト短調"大犬のワルツ", a rare one where the BPM differs for each chart.

I found it by searching for the artist "virkato" in Cheat Engine and subtracting 192 bytes from the result address.

ReClass.NET dissecting a memory structure

Oh, that's new. Looks like Japanese titles don't fare too well. On the other hand, that title2 looks pretty good. After checking RemyWiki again, it turns out that this is what would be displayed on the 16-segment LED ticker.

When ワルツ第17番 ト短調"大犬のワルツ" is played on beatmania IIDX, the arcade LED ticker displays the title as "VALSE DU GRANDE CHIEN".

The per-chart BPM values were also listed on RemyWiki, demystifying even more of the class.

ReClass.NET dissecting a memory structure

Okay, getting sidetracked a bit here. Nothing after the first 256 bytes is actually used for the scorecard.

Anyway, the final piece of data, the DJ name. Official scorecards show this above a customisable player avatar, known as a Qpro. That's yet another arcade exclusive feature, so I'll only be looking for the name. Nothing fancy, just a simple string search.

Performing a Cheat Engine text scan

That's everything I needed, so it's time to start writing some code now, right?

As much as I'd love to, this is a good point to stop at for now. There's still a lot left to do, so stay tuned for the next post where I'll be implementing all these discoveries into an internal library and looking for some good areas to place some hooks.

Last updated Saturday, 2 May 2020 at 03:52 PM.