Friday, March 23, 2018

The many tendrils of a Sonic 3 level, part 2.5

Before we proceed any further in our analysis, a brief digression. On the subject of the Animate_Tiles function, reader Silver Sonic 1992 commented:
Lava Reef zone has some type of dynamically reloading tiles. Is it just garbage data?
I completely missed this the first time around. Amidst all the pointers to null routines, the Offs_AniFunc table actually contains a pointer to a properly-defined animation routine for Lava Reef Zone 1:
                 dc.w AnimateTiles_NULL-Offs_AniFunc
                 dc.w AniPLC_ALZ-Offs_AniFunc
                 dc.w AnimateTiles_LRZ1-Offs_AniFunc
                 dc.w AniPLC_ALZ-Offs_AniFunc
                 dc.w AnimateTiles_NULL-Offs_AniFunc
                 dc.w AniPLC_ALZ-Offs_AniFunc
Notably, apart from some tweaks to the target VRAM offsets to make the routine work also for act 2, as well as the fact that ArtUnc_AniLRZ__BG2 seemingly grew to 1.5x its size sometime after Sonic 3's release, the bulk of the routine is quite similar to the code found in Sonic & Knuckles:
AnimateTiles_LRZ1:                              AnimateTiles_LRZ1:
                                                    move.w  #$6400,d4
                                                    move.w  #$6880,d6
                                                    bra.s   loc_282D0
                                                ; ---------------------------------------------

                                                AnimateTiles_LRZ2:
                                                    move.w  #$6400,d4
                                                    move.w  #$6880,d6

                                                loc_282D0:
    lea     (Anim_Counters).w,a3                    lea     (Anim_Counters).w,a3
    moveq   #0,d0                                   moveq   #0,d0
    move.w  ($FFFFEEE4).w,d0                        move.w  ($FFFFEEE4).w,d0
    sub.w   (Camera_X_pos_BG_copy).w,d0             sub.w   (Camera_X_pos_BG_copy).w,d0
                                                    subq.w  #1,d0
    divu.w  #$30,d0                                 divu.w  #$30,d0
    swap    d0                                      swap    d0
    cmp.b   1(a3),d0                                cmp.b   1(a3),d0
    beq.s   loc_27440                               beq.s   loc_2833C
    move.b  d0,1(a3)                                move.b  d0,1(a3)
    moveq   #0,d1                                   moveq   #0,d1
    move.w  d0,d2                                   move.w  d0,d2
    andi.w  #7,d0                                   andi.w  #7,d0
    lsl.w   #7,d0                                   lsl.w   #7,d0
    move.w  d0,d1                                   move.w  d0,d1
    lsl.w   #3,d0                                   lsl.w   #3,d0
    add.w   d0,d1                                   add.w   d0,d1
    move.l  d1,d5                                   move.l  d1,d5
    andi.w  #$38,d2                                 andi.w  #$38,d2
    move.w  d2,d0                                   move.w  d2,d0
    lsl.w   #3,d2                                   lsl.w   #3,d2
    add.w   d2,d1                                   add.w   d2,d1
    add.w   d2,d2                                   add.w   d2,d2
    add.w   d2,d1                                   add.w   d2,d1
    lsr.w   #1,d0                                   lsr.w   #1,d0
    lea     word_27446(pc,d0.w),a4                  lea     word_2834C(pc,d0.w),a4
    lea     (ArtUnc_AniALZ).l,a0                    lea     (ArtUnc_AniLRZ__BG).l,a0
    move.w  #$6020,d4
    add.l   a0,d1                                   add.l   a0,d1
    move.w  d4,d2                                   move.w  d4,d2
    move.w  (a4)+,d3                                move.w  (a4)+,d3
    add.w   d3,d4                                   add.w   d3,d4
    add.w   d3,d4                                   add.w   d3,d4
    jsr     (Add_To_DMA_Queue).l                    jsr     (Add_To_DMA_Queue).l
    move.l  d5,d1                                   move.l  d5,d1
    add.l   a0,d1                                   add.l   a0,d1
    move.w  d4,d2                                   move.w  d4,d2
    move.w  (a4)+,d3                                move.w  (a4)+,d3
    beq.s   loc_27440                               beq.s   loc_2833C
    jsr     (Add_To_DMA_Queue).l                    jsr     (Add_To_DMA_Queue).l

loc_27440:                                      loc_2833C:
                                                    cmpi.b  #$16,(Current_zone).w
                                                    beq.s   locret_2834A
    addq.w  #2,a3                                   addq.w  #2,a3
    bra.w   loc_2745E                               bra.w   loc_28364
; --------------------------------------------- ; ---------------------------------------------

                                                locret_2834A:
                                                    rts
                                                ; ---------------------------------------------
word_27446:                                     word_2834C:
    dc.w  $240,     0                               dc.w  $240,     0
    dc.w  $1E0,   $60                               dc.w  $1E0,   $60
    dc.w  $180,   $C0                               dc.w  $180,   $C0
    dc.w  $120,  $120                               dc.w  $120,  $120
    dc.w   $C0,  $180                               dc.w   $C0,  $180
    dc.w   $60,  $1E0                               dc.w   $60,  $1E0
; --------------------------------------------- ; ---------------------------------------------

loc_2745E:                                      loc_28364:
    moveq   #0,d0                                   moveq   #0,d0
    move.w  ($FFFFEEE2).w,d0                        move.w  ($FFFFEEE2).w,d0
    sub.w   (Camera_X_pos_BG_copy).w,d0             sub.w   (Camera_X_pos_BG_copy).w,d0
    andi.w  #$1F,d0                                 andi.w  #$1F,d0
    cmp.b   1(a3),d0                                cmp.b   1(a3),d0
    beq.s   locret_274BE                            beq.s   loc_283CC
    move.b  d0,1(a3)                                move.b  d0,1(a3)
    moveq   #0,d1                                   moveq   #0,d1
    move.w  d0,d2                                   move.w  d0,d2
    andi.w  #7,d0                                   andi.w  #7,d0
    lsl.w   #8,d0                                   lsl.w   #7,d0
                                                    move.w  d0,d1
                                                    add.w   d0,d0
                                                    add.w   d1,d0
    move.w  d0,d1                                   move.w  d0,d1
    move.l  d1,d5                                   move.l  d1,d5
    andi.w  #$18,d2                                 andi.w  #$18,d2
    move.w  d2,d0                                   move.w  d2,d0
    lsl.w   #3,d2                                   lsl.w   #2,d2
                                                    add.w   d2,d1
                                                    add.w   d2,d2
    add.w   d2,d1                                   add.w   d2,d1
    lsr.w   #1,d0                                   lsr.w   #1,d0
    lea     word_274C0(pc,d0.w),a4                  lea     word_283D2(pc,d0.w),a4
    lea     (ArtUnc_AniALZ).l,a0                    lea     (ArtUnc_AniLRZ__BG2).l,a0
    move.w  #$64A0,d4                               move.w  d6,d4
    add.l   a0,d1                                   add.l   a0,d1
    move.w  d4,d2                                   move.w  d4,d2
    move.w  (a4)+,d3                                move.w  (a4)+,d3
    add.w   d3,d4                                   add.w   d3,d4
    add.w   d3,d4                                   add.w   d3,d4
    jsr     (Add_To_DMA_Queue).l                    jsr     (Add_To_DMA_Queue).l
    move.l  d5,d1                                   move.l  d5,d1
    add.l   a0,d1                                   add.l   a0,d1
    move.w  d4,d2                                   move.w  d4,d2
    move.w  (a4)+,d3                                move.w  (a4)+,d3
    beq.s   locret_274BE                            beq.s   loc_283CC
    jsr     (Add_To_DMA_Queue).l                    jsr     (Add_To_DMA_Queue).l

locret_274BE:                                   loc_283CC:
                                                    addq.w  #2,a3
    rts                                             bra.w   loc_286E8
; --------------------------------------------- ; ---------------------------------------------
word_274C0:                                     word_283D2:
    dc.w   $80,     0                               dc.w   $C0,     0
    dc.w   $60,   $20                               dc.w   $90,   $30
    dc.w   $40,   $40                               dc.w   $60,   $60
    dc.w   $20,   $60                               dc.w   $30,   $90
; --------------------------------------------- ; ---------------------------------------------
Much like what happened with the level load block though, the uncompressed art used by this routine was completely wiped from the Sonic 3 ROM, leaving the code pointing at whatever data came next. In this case it's ArtUnc_AniALZ, which is uncompressed art normally used by Azure Lake's animation routine.

Friday, March 16, 2018

The many tendrils of a Sonic 3 level, part 2: the animal exchange

Picking up from where we left off, at $2BD98 we come across the Obj_Animal object, which as we learned before, is the small animal that spawns both from a defeated enemy and from an act 2 end capsule. And right at the start of its code, we find this seemingly innocuous byte array:
byte_2BDDA: dc.b 5, 1
            dc.b 0, 3
            dc.b 5, 1
            dc.b 0, 5
            dc.b 6, 5
            dc.b 2, 3
            dc.b 6, 1
            dc.b 6, 5
            dc.b 6, 5
            dc.b 6, 5
            dc.b 6, 5
            dc.b 6, 5
            dc.b 6, 5
This is actually a very dangerous byte array! When the animal object is first initialized, it uses the value of the current zone, plus the value in register d0 (which is randomly set to either 0 or 1), to index the byte_2BDDA array, and put the resulting byte into offset $30 of its own SST:
loc_2BF4A:
    moveq   #0,d1
    move.b  (Current_zone).w,d1
    add.w   d1,d1
    add.w   d0,d1
    lea     byte_2BDDA,a1
    move.b  (a1,d1.w),d0
    move.b  d0,$30(a0)
Then at loc_2BFEA, after the animal first lands on the floor, it feeds this value into the routine counter at offset 5 of its own SST, in order to determine whether it should hop or fly away:
    move.b  $30(a0),d0
    add.b   d0,d0
    addq.b  #4,d0
    move.b  d0,5(a0)
It is therefore vital that the byte_2BDDA array contain two entries for each level in the game, as these determine which two animals may spawn in any given stage. With Sonic 3 in particular, every stage past Launch Base Zone reuses the same animal combination as Flying Battery Zone. In Sonic & Knuckles, only Doomsday Zone retains this property.
byte_2BDDA: dc.b 5, 1  ; AIZ                    byte_2C7BA: dc.b 5, 1  ; AIZ
            dc.b 0, 3  ; HCZ                                dc.b 0, 3  ; HCZ
            dc.b 5, 1  ; MGZ                                dc.b 5, 1  ; MGZ
            dc.b 0, 5  ; CNZ                                dc.b 0, 5  ; CNZ
            dc.b 6, 5  ; FBZ                                dc.b 6, 5  ; FBZ
            dc.b 2, 3  ; ICZ                                dc.b 2, 3  ; ICZ
            dc.b 6, 1  ; LBZ                                dc.b 5, 1  ; LBZ
            dc.b 6, 5  ; MHZ                                dc.b 6, 1  ; MHZ
            dc.b 6, 5  ; SOZ                                dc.b 0, 1  ; SOZ
            dc.b 6, 5  ; LRZ                                dc.b 5, 1  ; LRZ
            dc.b 6, 5  ; SSZ                                dc.b 0, 5  ; SSZ
            dc.b 6, 5  ; DEZ                                dc.b 6, 1  ; DEZ
            dc.b 6, 5  ; DDZ                                dc.b 6, 5  ; DDZ
                                                            dc.b 5, 1  ; Ending
                                                            dc.b 5, 1  ; ALZ
                                                            dc.b 5, 1  ; BPZ
                                                            dc.b 5, 1  ; DPZ
                                                            dc.b 5, 1  ; CGZ
                                                            dc.b 5, 1  ; EMZ
                                                            dc.b 5, 1  ; Gumball
                                                            dc.b 5, 1  ; Pachinko
                                                            dc.b 5, 1  ; Slots
                                                            dc.b 5, 1  ; LRZ Boss
                                                            dc.b 5, 1  ; DEZ Boss
Interestingly, in Sonic & Knuckles, the array was extended to cover every level slot, rather than stopping at Doomsday Zone. This was probably done for the sake of the Lava Reef Zone boss act, in which animals spawn from the capsule at the very end of the stage. Indeed, if we look at the numbers, all of the new entries in the array use the same animal combination as Lava Reef Zone.

The length of this array in Sonic 3 suggests that attempting to load any level slot beyond Doomsday Zone is inherently dangerous. But in reality, it only becomes a problem if an animal is spawned while playing the stage; it shouldn't affect whether the stage can load or not.

What does affect it are the animals' graphics, which are loaded by the PLCLoad_AnimalsAndExplosion function at $543F4 when the level starts. Much like the animal object, it uses the value of the current zone as an offset to an array, which contains pointers to the PLCs for each stage's combination of animals.

And therein lies the rub, because in Sonic 3, this array only goes up to Launch Base Zone:
off_54414:  dc.w PLC_54422-off_54414  ; AIZ     off_85FFE:  dc.w PLC_8602E-off_85FFE  ; AIZ
            dc.w PLC_54430-off_54414  ; HCZ                 dc.w PLC_8603C-off_85FFE  ; HCZ
            dc.w PLC_5443E-off_54414  ; MGZ                 dc.w PLC_8604A-off_85FFE  ; MGZ
            dc.w PLC_5444C-off_54414  ; CNZ                 dc.w PLC_86058-off_85FFE  ; CNZ
            dc.w PLC_5445A-off_54414  ; FBZ                 dc.w PLC_86066-off_85FFE  ; FBZ
            dc.w PLC_54468-off_54414  ; ICZ                 dc.w PLC_86074-off_85FFE  ; ICZ
            dc.w PLC_54476-off_54414  ; LBZ                 dc.w PLC_86082-off_85FFE  ; LBZ
                                                            dc.w PLC_86090-off_85FFE  ; MHZ
                                                            dc.w PLC_8609E-off_85FFE  ; SOZ
                                                            dc.w PLC_860AC-off_85FFE  ; LRZ
                                                            dc.w PLC_860BA-off_85FFE  ; SSZ
                                                            dc.w PLC_860C8-off_85FFE  ; DEZ
                                                            dc.w PLC_860D6-off_85FFE  ; DDZ
                                                            dc.w PLC_860E4-off_85FFE  ; Ending
                                                            dc.w PLC_860E4-off_85FFE  ; ALZ
                                                            dc.w PLC_860E4-off_85FFE  ; BPZ
                                                            dc.w PLC_860E4-off_85FFE  ; DPZ
                                                            dc.w PLC_860E4-off_85FFE  ; CGZ
                                                            dc.w PLC_860E4-off_85FFE  ; EMZ
                                                            dc.w PLC_860E4-off_85FFE  ; Gumball
                                                            dc.w PLC_860E4-off_85FFE  ; Pachinko
                                                            dc.w PLC_860E4-off_85FFE  ; Slots
                                                            dc.w PLC_860E4-off_85FFE  ; LRZ Boss
                                                            dc.w PLC_860E4-off_85FFE  ; DEZ Boss
PLC_54422:  dc.w 1                              PLC_8602E:  dc.w 1
            dc.l ArtNem_BlueFlicky                          dc.l ArtNem_BlueFlicky
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Chicken                             dc.l ArtNem_Chicken
            dc.w $B240                                      dc.w $B240
PLC_54430:  dc.w 1                              PLC_8603C:  dc.w 1
            dc.l ArtNem_Rabbit                              dc.l ArtNem_Rabbit
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Seal                                dc.l ArtNem_Seal
            dc.w $B240                                      dc.w $B240
PLC_5443E:  dc.w 1                              PLC_8604A:  dc.w 1
            dc.l ArtNem_BlueFlicky                          dc.l ArtNem_BlueFlicky
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Chicken                             dc.l ArtNem_Chicken
            dc.w $B240                                      dc.w $B240
PLC_5444C:  dc.w 1                              PLC_86058:  dc.w 1
            dc.l ArtNem_Rabbit                              dc.l ArtNem_Rabbit
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_BlueFlicky                          dc.l ArtNem_BlueFlicky
            dc.w $B240                                      dc.w $B240
PLC_5445A:  dc.w 1                              PLC_86066:  dc.w 1
            dc.l ArtNem_Squirrel                            dc.l ArtNem_Squirrel
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_BlueFlicky                          dc.l ArtNem_BlueFlicky
            dc.w $B240                                      dc.w $B240
PLC_54468:  dc.w 1                              PLC_86074:  dc.w 1
            dc.l ArtNem_Penguin                             dc.l ArtNem_Penguin
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Seal                                dc.l ArtNem_Seal
            dc.w $B240                                      dc.w $B240
PLC_54476:  dc.w 1                              PLC_86082:  dc.w 1
            dc.l ArtNem_Squirrel                            dc.l ArtNem_BlueFlicky
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Chicken                             dc.l ArtNem_Chicken
            dc.w $B240                                      dc.w $B240
                                                PLC_86090:  dc.w 1
                                                            dc.l ArtNem_Squirrel
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_8609E:  dc.w 1
                                                            dc.l ArtNem_Rabbit
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_860AC:  dc.w 1
                                                            dc.l ArtNem_BlueFlicky
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_860BA:  dc.w 1
                                                            dc.l ArtNem_Rabbit
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_860C8:  dc.w 1
                                                            dc.l ArtNem_Squirrel
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_860D6:  dc.w 1
                                                            dc.l ArtNem_Squirrel
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_860E4:  dc.w 1
                                                            dc.l ArtNem_BlueFlicky
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
So even if we were to fix up the level load block for the Sonic & Knuckles stages, the PLCLoad_AnimalsAndExplosion function would then take the value of the current zone, go past the end of the pointer array straight into the actual PLC definitions, and eventually attempt to load a nonsensical PLC, which would most definitely crash the game. Huzzah!
PLC_54476:  dc.w 1                              PLC_86082:  dc.w 1
            dc.l ArtNem_Squirrel                            dc.l ArtNem_BlueFlicky
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Chicken                             dc.l ArtNem_Chicken
            dc.w $B240                                      dc.w $B240
It was only by looking at the animal data side-by-side like this that I stumbled upon one of the most obscure differences between Sonic 3 and Sonic & Knuckles that I know of. In Sonic 3, the animals which appear in Launch Base Zone are Ricky (the squirrel) and Cucky (the chicken). In Sonic & Knuckles however, Ricky was replaced by Flicky (the flicky).

This wasn't an accident; if you check the byte_2BDDA array at the top of this post, you'll see that the animal type was changed from 6 to 5 there, too.


Finally, on the subject of animal types, here's something that always bugged me. In Sonic 1, there were seven different animals. From left to right, they are: Pocky, Cucky, Pecky, Rocky, Picky, Flicky and Ricky.


Sonic 2 then added five more, whose names I can't find a source for: an eagle, a mouse, a monkey, a turtle and a bear.


When it came time to make Sonic 3, the developers decided to scale back to the original cast of animals. Indeed, if you look at the byte_2BDDA array, you'll find that they're numbered 0 through 6... but animal 4 is nowhere to be seen.


Picky appears to have disappeared; not even his graphics can be found within the ROM. Maybe he's still hiding in the Casino Night Zone?

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.