Sunday, March 11, 2018

The many tendrils of a Sonic 3 level, part 1

Apologies for the radio silence during the past month; I've been absolutely swamped with work and haven't had time for either the blog or the hack.

On the subject of loading Sonic & Knuckles stages in Sonic 3, an anonymous commenter wondered whether the bogus entries in the S3A level load block are enough to make the game crash while loading those levels, to which I answered that yes, that should be quite enough.

However, are they the sole cause of errors in this scenario? Assuming we patch up the level load block to point at valid Kosinski data where appropriate, would those stages then boot up properly? Not necessarily.

First off, as we saw before, the level load block itself contains byte pointers which are used as an index to the PalPoint and Offs_PLC arrays. Obviously, these bytes must correspond to valid entries within those arrays, otherwise we'll once again be attempting to decompress garbage data, or trying to copy nonsensical color values to a random location in the Mega Drive's address space.

Beyond this though, there are several other points in the game where code is executed and data is loaded conditionally depending on which level is being played. These points are scattered throughout the ROM without any structure, which is partially why Sonic 3 has a reputation of being a harder game to hack than previous Sonic titles.

Let's start from the top. At $23CA, we have the AnPal_Load function, which is called once every frame in order to run all of the animated palettes within a stage. To accomplish this, it takes the value of the current zone and act and uses it as an index to the OffsAnPal array, which is itself a list of pointers to smaller routines that then handle all the palette animations specific to that stage:
OffsAnPal:      dc.w AnPal_AIZ1-OffsAnPal
                dc.w AnPal_AIZ2-OffsAnPal
                dc.w AnPal_HCZ1-OffsAnPal
                dc.w AnPal_HCZ2-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_CNZ-OffsAnPal
                dc.w AnPal_CNZ-OffsAnPal
                dc.w AnPal_FBZ-OffsAnPal
                dc.w AnPal_FBZ-OffsAnPal
                dc.w AnPal_ICZ-OffsAnPal
                dc.w AnPal_ICZ-OffsAnPal
                dc.w AnPal_LBZ1-OffsAnPal
                dc.w AnPal_LBZ2-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_LRZ1-OffsAnPal
                dc.w AnPal_LRZ2-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_BPZ-OffsAnPal
                dc.w AnPal_BPZ-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_CGZ-OffsAnPal
                dc.w AnPal_CGZ-OffsAnPal
                dc.w AnPal_EMZ-OffsAnPal
                dc.w AnPal_EMZ-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
Interestingly, Flying Battery Zone has its own routine just like in Sonic & Knuckles, except that here it does absolutely nothing. More interestingly, there are also routines defined for both acts of Lava Reef Zone, and although the routine for act 2 is blank, the routine for act 1 is fully functional, and even references some otherwise unused palette data!


The presence of this data, which is identical to the corresponding data in the Sonic & Knuckles ROM, ties squarely into the notion that Lava Reef Zone had already entered production by the time of Sonic 3's release.

Moving right along, at $4680 we have the LevelMusic_Playlist, which as we saw before, dictates which track is played at level load based on the current zone and act. Sonic 3's version of the playlist is identical to the one found in Sonic & Knuckles, except for the following points:

  • The Rolling Jump bonus stage reuses the track for the Gumball bonus stage.
  • Both the ending level slot and the four acts at the end of the level list make use of the special stage track.
  • Sky Sanctuary Zone and Death Egg Zone are bugged. Instead of using the same song for both acts, act 2 of Sky Sanctuary Zone uses the song meant for Death Egg Zone 1, and then both acts of Death Egg Zone use the song meant for Death Egg Zone 2.

At $1A1F4, we find the LevelSizes structure. All of the levels unused in Sonic 3 have the default size of $1000 pixels down and $6000 pixels across. Shortly after, at $1A8A8 we find the LevelResizeArray. Like in Sonic & Knuckles, every stage past Launch Base Zone 2 points at an empty dynamic resize routine.

Then at $26A92, we find the Animate_Tiles function, which is also called once every frame in order to process all the animated elements within a level. Much like the AnPal_Load function, it uses the current zone and act as an index to an array of pointers to smaller routines, which then handle the tile-based animations specific to that level. Interleaved with this array however, is another array containing pointers to the level's "animated PLC", which consists of the ROM address of the uncompressed art, the destination VRAM address, and the duration of each frame of animation.
Offs_AniFunc:   dc.w AnimateTiles_AIZ1-Offs_AniFunc
Offs_AniPLC:    dc.w AniPLC_AIZ1-Offs_AniFunc
                dc.w AnimateTiles_AIZ2-Offs_AniFunc
                dc.w AniPLC_AIZ2-Offs_AniFunc
                dc.w AnimateTiles_HCZ1-Offs_AniFunc
                dc.w AniPLC_HCZ1-Offs_AniFunc
                dc.w AnimateTiles_HCZ2-Offs_AniFunc
                dc.w AniPLC_HCZ2-Offs_AniFunc
                dc.w AnimateTiles_MGZ-Offs_AniFunc
                dc.w AniPLC_MGZ-Offs_AniFunc
                dc.w AnimateTiles_MGZ-Offs_AniFunc
                dc.w AniPLC_MGZ-Offs_AniFunc
                dc.w AnimateTiles_CNZ-Offs_AniFunc
                dc.w AniPLC_CNZ-Offs_AniFunc
                dc.w AnimateTiles_CNZ-Offs_AniFunc
                dc.w AniPLC_CNZ-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ICZ-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ICZ-Offs_AniFunc
                dc.w AnimateTiles_ICZ-Offs_AniFunc
                dc.w AniPLC_ICZ-Offs_AniFunc
                dc.w AnimateTiles_ICZ-Offs_AniFunc
                dc.w AniPLC_ICZ-Offs_AniFunc
                dc.w AnimateTiles_LBZ1-Offs_AniFunc
                dc.w AniPLC_LBZ1-Offs_AniFunc
                dc.w AnimateTiles_LBZ2-Offs_AniFunc
                dc.w AniPLC_LBZ2-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ALZ-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ALZ-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ALZ-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ALZ-Offs_AniFunc
                ...
As with the OffsAnPal array, all of the empty slots in the Offs_AniFunc array point to blank, "null" routines. However, much like the level load block, empty slots in the Offs_AniPLC array are filled with pointers to whatever data follows the previous PLC entry. As a result, Flying Battery Zone points to the PLC for Icecap Zone, all of the Sonic & Knuckles levels point to the PLC for Azure Lake, and every level past Desert Palace (the last stage with animated PLCs) points at the end of the animated PLC block.

Okay, but unlike the data mismatch in the level load block, the Azure Lake PLC is obviously valid PLC data, so loading Sonic & Knuckles levels shouldn't cause the game to crash. And even if the PLC data is invalid, such as with the levels that point at the end of the animated PLC block, the fact that the animation routine is blank means the game doesn't do anything with the invalid PLC data.

So far we haven't found anything that might crash the game. In the next post, we'll look at one aspect which could, and in the process, stumble upon an obscure version difference.

7 comments:

  1. I was wondering if weekend posts were a possibility with the removed schedule. Anyway, may I ask if the level order is arranged differently in S3A than in S&K?

    ReplyDelete
  2. Lava Reef zone has some type of dynamically reloading tiles. Is it just garbage data?

    ReplyDelete
    Replies
    1. Usually I would answer this sort of question in the following post, but the slower post rate means I would leave the question hanging for too long.

      So for now, my answer is probably, but I'll be sure to look into it.

      Delete
  3. This may be a stupid question but are the seasonal changes/palette swap from Mushroom Valley Act 2 still leftover somewhere in the code of S3A? Like the command that tells the game to do that for that Zone.

    ReplyDelete
    Replies
    1. To my knowledge, S3A contains no code at all for Mushroom Hill Zone. There's even a pair of grossly incomplete object layouts that reference an object ID which isn't used in any other level, but even that object has no code associated with it.

      Delete