Mysterious Balloons

Those of you that have done the out-of-bounds tricks in Sonic R are probably aware that there are uncollectable blue balloons hidden in really weird, out-of-the way places.

Balloon outside of Resort Island

The even fewer of you who have decided to swap the files for different tracks around (so that, for instance, Regal Ruin has the track and collision data of Radical City) will have noticed that even more balloons appear, in totally random places. And if you play this franken-level in the Balloon Time Attack mode, you'll notice that these balloons aren't even used by anything!

Mutliple balloons? But how?

So, what are these mysterious blue balloons? Simple: the in-bound ones are leftovers from the Sega Saturn version of the game. In that version, the balloons didn't animate or have random colors like they do in the PC version. They're just static blue balloons, placed exactly where they are in the PC version's track files.

Saturn screenshot vs. model dump shows same position for both.

I imagine the out of bound ones were used by the devs as a template to copy/paste from, and placed far out of bounds where nobody can see them. This is done with many objects in the game. For instance, take this screenshot from far beyond the outskirts of Regal Ruin.

Many random objects way, way in the outskirts of Regal Ruins.

So now two question arises: where do the real balloons come from, and why can't I see (some of) the fake ones? I'll answer the second one first, because why not.


Where do the balloons go?

Here's the relevant code, in all it's glory.

BEGTEXT:0047E5D6 loc_47E5D6:                             ; CODE XREF: Level_InitBalloons+83
BEGTEXT:0047E5D6                 mov     eax, ds:Game_Course
BEGTEXT:0047E5DB                 mov     esi, ds:TrackGeometryOffset
BEGTEXT:0047E5E1                 mov     eax, ds:off_500A18[eax*4]
BEGTEXT:0047E5E8                 mov     ecx, 0FFFFFFFFh
BEGTEXT:0047E5ED
BEGTEXT:0047E5ED loc_47E5ED:                             ; CODE XREF: Level_InitBalloons+B5
BEGTEXT:0047E5ED                 mov     edx, [eax]      ; Hide fake balloons
BEGTEXT:0047E5EF                 add     eax, 4
BEGTEXT:0047E5F2                 cmp     edx, 0FFFFFFFFh
BEGTEXT:0047E5F5                 jz      short loc_47E603
BEGTEXT:0047E5F7
BEGTEXT:0047E5F7                 imul    ebx, edx, 44h
BEGTEXT:0047E5FA                 mov     edx, esi
BEGTEXT:0047E5FC                 mov     [edx+ebx+2Ch], cx
BEGTEXT:0047E601                 jmp     short loc_47E5ED
BEGTEXT:0047E603 ; ---------------------------------------------------------------------------
BEGTEXT:0047E603
BEGTEXT:0047E603 loc_47E603:                             ; CODE XREF: Level_InitBalloons+A9
                                 ...

		

So, what are we looking at, exactly? I'll walk you through this, because it's not that hard. First off, the current course is loaded into register EAX. The current course is a DWORD (4-byte) value from 1 to 5, 1 being Resort Island and 5 being Radiant Emerald. ESI is loaded with a pointer to the track's geometry section in memory. (That is, a memory address we can dereference later to get us the first byte of the track information, whatever that may be.)

Once EAX and ESI are loaded, we multiply EAX by 4 and add 0x500A18 to it. This makes EAX now a pointer to some area in memory. In this particular case, it points to a location in SONICR.EXE, specifically offset 0xEBA18 + (course ID * 4). This means you could open a hex editor, look at addresss 0xEBA18, and edit this data. And ECX is loaded with 0x0FFFFFFFF, which is used as a "end of data" indicator.

Once all our registers are set up, we enter the main loop. This repeats while EDX != 0x0FFFFFFFF. EDX, in this case, is whatever EAX (our pointer to the location in the EXE) points to. Every go through the loop, we increment EAX by 4 so that we don't perpetually repeat ourselves.

Once we've comfirmed that we're not at the end of the fake balloon data, we can start hiding balloons. To do this, we multiply the value of EDX by 0x44, and store it in EBX. We then set the value of (TrackGeometryOffset + FakeBalloonData + 0x2C) to 0xFFFF. That is, it sets a word in the in-memory representation of the track's part so that the part doesn't get rendered. And, because it's a loop, it jumps back to the start where we dereference EAX again.

Here's some C pseudocode to clear that up:

long *badTrackParts = 0x500A18 + Game_Course;
long *currPart = badTrackParts;
while(*currPart != 0x0FFFFFFF) {
    short *renderFlag = 0x00712D44 + (*currPart * 0x44) + 0x2C;
    *renderFlag = 0xFFFF;
    currPart++;
}

In summary, the game reads a list of parts and then marks those parts as "do not render". ...I made that sound really complicated, didn't I? Also, I'm not really sure why only some of the balloons are hidden or why they weren't just removed from the game altogether. I suppose whatever level editor tool they used had a "is part visible?" checkbox that makes this super duper easy. But I can only speculate.


Where do the balloons come from?

I'll skip the ASM analysis for this one, because the ASM here is much, much messier and not really worth explaining. Each course in Sonic R has 17 balloons defined. These are used in the Balloon mode for Time Attack and Multiplayer.

Of these 17 possible balloons, (X << 2) + 1 are randomly selected to be visible, where X is the number of players. (That is, for 1, 2, 3, and 4 players, there will be 5, 9, 13, and 17 balloons in play, respectively.)

Each balloon has 0x24 bytes of data associated in memory. The only relevant bytes in game are the X, Y and Z coordinates and the red, blue, and green coloring. The X, Y, and Z are loaded from the memory address 0x004FF924 + (Game_Course - 1)*0xCC in memory, which is equal to 0xEA924 + (Game_Course - 1)*0xCC in the EXE. This means each course gets a 0xCC sized chunk. (For 17 balloons, that's 12 bytes per balloon, 4 bytes for the 3 coordinates.) The coordinates are 4096 times those used for the characters (so moving a balloon 1 unit would be equivelent to moving a charater 4096 units.)

The colors are randomly generated, where R, G, and B are set to multiples of 0x40 up to a maximum of 0xFF. (I'm not sure on this, though. I didn't really look into it much.)

To help you visualize (and to help me make sure all my info is accurate), here's an interactive balloon randomizer. Note that it will probably be hideously broken on phones and really old browsers. Also the balloon positions might not be 100% accurate, just due to how I got the map pictures.



1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
X: Y: Z: