Friday, October 6, 2017

Sonic 3 stages in S3&K: level layouts

Okay, so object and ring layouts can't be reused because they're not amenable to being patched, and they have to get patched for Knuckles. What about level layouts? Are they amenable to being patched?

Well, as we saw before, a level layout is pretty much just a flat array of chunk IDs, which is copied to RAM on level load to allow crazy stuff like copying chunks from one part of the layout to another in the middle of gameplay. They are most definitely amenable to being patched, so the ones in the Sonic 3 cartridge do just fine.

Do they require patching? Yes. For example, Launch Base Zone 1 has the following code at the start of its background init function. Note that during execution of screen and background event functions, the a3 register contains the address of the first row pointer for the respective level layout plane.
LBZ1_BackgroundInit:
    cmpi.w  #3,(Player_mode).w
    bne.s   loc_541C6
    movea.w (a3),a1
    addq.w  #2,a1
    move.b  #$E4,(a1)+
    move.b  #$E6,(a1)+
    move.b  #$E0,(a1)+
    movea.w 4(a3),a1
    addq.w  #2,a1
    move.b  #$EC,(a1)+
    move.b  #$EE,(a1)+
    move.b  #$E8,(a1)+
    ...
So what this code does is load the pointer to the first row of background chunks into the a1 register, skip over the first two chunks in the row, and then replace the next three chunks with different chunk IDs. Then it loads the pointer to the second row of chunks and does the same thing.

In practice, the chunks being replaced are the ones that put the Death Egg on the background plane. Since Knuckles' story takes place after Sonic and Tails', during the course of which the Death Egg is launched back into outer space, it wouldn't make sense for it to appear in the background when playing as Knuckles.

As such, this:


becomes this:


Something similar happens at the very end of the level, after defeating Knuckles' boss. Here's the code, this time from the screen event function:
loc_53F50:
    movea.w $4C(a3),a1
    move.b  $78(a1),d0
    movea.w $50(a3),a1
    lea     $78(a1),a1
    move.b  d0,(a1)+
    move.b  d0,(a1)+
    move.b  d0,(a1)+
    jmp     Refresh_PlaneScreenDirect(pc)
This one is cute. First, it loads the pointer to a specific chunk row to register a1, then copies a specific chunk ID from that row into register d0. It's this one:


Next, it loads a different chunk row into a1, skips over the first 120 chunks, and pastes the previously copied chunk into the three slots that follow, erasing the checkerboard pipe from the level layout. Note how the background pattern isn't a perfect match; it's that way in-game as well:


Something doesn't seem right about the above two images, though. The twin boxes appear way too low. Well, if you go and check, they're actually like that in the Sonic 3 layout. It was Sonic & Knuckles that patched them.


The code responsible for doing so is in the screen init function for LBZ1:
LBZ1_ScreenInit:
    ...
    movea.w $48(a3),a1
    movea.w $4C(a3),a5
    move.b  $79(a1),$79(a5)
    move.b  #$DB,$79(a1)
    jsr     LBZ1_RotateChunks(pc)
    ...
Okay, so the first thing this code does is load the pointers to two chunk rows into registers a1 and a5. Then, it copies a chunk from the first row...


...and pastes it over the same chunk on the second row, also replacing the one on the first row with a chunk ID of $DB:


We seem to have placed a strange chunk up there. This is actually correct: the boss object displays the outside of the boxes using sprites, and the chunk is only used to show the inside of the boxes once they open.

Astute viewers will notice that the box is now too high. Argh. Best look inside that LBZ1_RotateChunks function:
LBZ1_RotateChunks:
    moveq   #$17,d2

loc_54148:
    lea     ($FF6E00).l,a1
    lea     -2(a1),a5
    move.w  (a5),d0
    moveq   #$3E,d1

loc_54156:
    move.w  -(a5),-(a1)
    dbf     d1,loc_54156
    move.w  d0,(a5)
    dbf     d2,loc_54148
    rts
Holy goodness, what is happening here?

What this function is actually doing is patching the chunk definition for chunk $DB. That is, it is changing the pattern of 16x16 blocks which make up the 128x128 chunk. More specifically, it's taking the bottom three rows of blocks...


...and throwing them on top of the chunk, bringing the remaining rows down. It's "rotating" the chunk in the sense that it's sliding all the blocks down by three rows:


This isn't the only instance of chunk definition patching in the game, though. In Angel Island Zone 1, it's used to remove the horizon line from the background while playing as Knuckles. Again, that's because Knuckles' story takes place after Sonic and Tails', in which the Master Emerald is returned to Angel Island, causing it to resume floating in the sky.

Note also how once again, some of the replacement blocks aren't a perfect match:


That's it for our brief look into Sonic 3 & Knuckles. Next up is the game born out of circumstance: Knuckles in Sonic 2.

3 comments:

  1. The Twin hammer boxes had their art changed slightly between Sonic 3 and Sonic 3 & Knuckles?

    ReplyDelete
    Replies
    1. Good eye. Your conclusion isn't exactly right though, but I'm sure it'll be the subject of a future post.

      Delete
    2. Let me guess: the sprites themselves are unaltered, but the mappings are different between S3 and S3K.

      Delete