Showing posts with label Lava Reef Zone. Show all posts
Showing posts with label Lava Reef Zone. Show all posts

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.

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.

Thursday, October 26, 2017

Non-standard Eggheads, part 2

On the subject of the unused FBZ2 boss head, reader Silver Sonic 1992 asked:
Was this the intended use for that frame?
I guess I should've went into more detail regarding the nature of the bug. When the boss object begins swinging around the platform, the code at loc_7078A sets bit 2 of the bitfield at offset $38 of its SST:
loc_7078A:
    move.b  #8,5(a0)
    bset    #2,$38(a0)
    move.l  #loc_707EC,$34(a0)
    ...
Meanwhile, the head object tries to keep tabs on this bit by loading the RAM address for the boss' SST into register a1, which it has stored away at offset $44 of its own SST, and then checking the contents of offset $38. However, due to an oversight, it ends up checking offset $38 of register a0, which is its own SST, rather than register a1:
loc_67BFC:
    clr.b   $22(a0)
    movea.w $44(a0),a1
    btst    #2,$38(a0)
    beq.s   loc_67C12
    move.b  #1,$22(a0)
Okay, now for today's bug. Lava Reef Zone's act 2 boss has a unique head object with only one idle frame, in which the art has been redrawn to give Eggman's whiskers extra lighting from the lava below.


However, due to an oversight, only two of the three frames are ever seen. When the boss is defeated, it simply displays the second frame, which is the regular damage frame. The oversight is hard to notice due to how quickly the boss sinks back into the lava.

The code that picks which mapping frame to display is at loc_79C1C:
loc_79C1C:
    jsr     (Refresh_ChildPositionAdjusted).l
    movea.w $46(a0),a1
    move.b  #$F,$22(a0)
    btst    #6,$2A(a1)
    beq.s   loc_79C3E
    move.b  #$10,$22(a0)
    bra.w   loc_79C4C
; ---------------------------------------------------------------------------

loc_79C3E:
    btst    #7,$2A(a1)
    beq.s   loc_79C4C
    move.b  #$11,$22(a0)

loc_79C4C:
    jmp     (Child_Draw_Sprite2).l
Much like before, the head object is keeping tabs on the boss object, except this time the boss object's RAM address is at offset $46, and the bitfield the head object checks is the status bitfield at offset $2A.

Here's how this code works: it defaults to mapping frame $F, switches to frame $10 if bit 6 is set, and switches to frame $11 if bit 7 is set. Bit 6 is set when the boss takes damage, and bit 7 is set when it is defeated. However, when the boss takes its final hit, both bits are set, and the bra.w loc_79C4C instruction skips over the bit 7 check if bit 6 is already set.

To fix the bug, simply remove the bra.w instruction.

Monday, September 4, 2017

Act transitions, part 6: deferred execution

In the lower route through Lava Reef Zone 1, there's a section where the path is overlapped by a pool of lava that rises and lowers periodically. This effect is accomplished by temporarily hijacking the background plane, as we've previously seen in other levels. Yet again, however, the legendary GoldS manages to find a way to break it.

You see, there's three triggers to enable or disable the rising lava, one on each side you might approach the area: from the left, from the right, and from above. However, he developers didn't expect that you might leave the area through the bottom, which is possible if the lava rises above your position while you're frozen in place from a super transformation.


When you do this, the rising lava effect keeps running throughout the rest of the level, as evidenced by the blanked-out background. If you clear the level under these circumstances, the act 2 title card shows up, act 2 music begins playing, the palette changes, the camera unlocks, the whole shebang... except you're still in the act 1 layout.


So what's going on? Well, act 2 music starts playing because the level results object changes the apparent act to act 2.
loc_2DD06:
    move.b  #1,(Apparent_act).w     ; Change to act 2 if in act 1
    ...
The act 2 palette gets loaded because when the act 1 boss is defeated, it leaves behind an object that makes the rocks turn blue once the player progresses beyond a certain point.
loc_78B08:
    cmpi.w  #$2C0,(Camera_X_pos).w
    blo.w   locret_78536
    move.w  #$2C0,(Camera_min_X_pos).w
    lea     (Pal_LRZ2).l,a1
    lea     (Normal_palette_line_2).w,a2
    moveq   #7,d6

loc_78B24:
    move.l  (a1)+,(a2)+
    dbf     d6,loc_78B24
    ...
At the end of act 1, the camera position is clearly past $2C0. For this reason, the object doesn't actually spawn until the title card goes away, at which point we're presumably in act 2. But since we're still in act 1, the camera position remains the same, which explains why the rocks turn blue as soon as the title card disappears.

It should be obvious by this point: all we experienced were the side effects. The act 2 layout wasn't loaded because the main event, the act transition, didn't actually run. The reason for that is simple:
LRZ1_BackgroundEvent:
    move.w  ($FFFFEEC2).w,d0
    jmp     loc_56BC2(pc,d0.w)
; ---------------------------------------------------------------------------

loc_56BC2:
    bra.w   loc_56BD2           ; Normal
; ---------------------------------------------------------------------------
    bra.w   loc_56C6E           ; Rising lava #1
; ---------------------------------------------------------------------------
    bra.w   loc_56C88           ; Rising lava #2
; ---------------------------------------------------------------------------
    bra.w   loc_56CAA           ; Transition
; ---------------------------------------------------------------------------

loc_56BD2:
    tst.w   ($FFFFEEC6).w
    beq.s   loc_56C28
    clr.w   ($FFFFEEC6).w
    movem.l d7-a0/a2-a3,-(sp)
    lea     (LRZ2_128x128_Secondary_Kos).l,a1
    lea     ($FFFF0180).l,a2
    jsr     (Queue_Kos).l
    lea     (LRZ2_16x16_Secondary_Kos).l,a1
    lea     ($FFFF9128).w,a2
    jsr     (Queue_Kos).l
    lea     (ArtKosM_LRZ2_Secondary).l,a1
    move.w  #$1200,d2
    jsr     (Queue_Kos_Module).l
    moveq   #$30,d0
    jsr     (Load_PLC).l
    movem.l (sp)+,d7-a0/a2-a3
    move.w  #$C,($FFFFEEC2).w
    bra.w   loc_56D16
The act transition, like in any other level, is handled by the background event routine. More specifically, it is handled by the normal background event routine, which is only running when the rising lava isn't active. The level results object did set the transition flag at $EEC6 just like it usually does, it's just that no one was listening.

This can be verified by going back to the rising lava area, and then leaving through one of the intended exits. The rising lava is disabled, the background event routine returns to normal, sees the transition flag is set, and starts loading act 2 right there on the spot. Then, because you're far earlier in the level than expected, the camera position is again set to a negative value, promptly causing the game to lock up once more.

Thursday, August 17, 2017

Contextual graphics, part 3: frozen palette

When the end level sign lands in Lava Reef Zone 1, the palette animation on the foreground rocks suddenly freezes on whichever color it was at the time. Unlike previous cases however, this is not mandated by any particular line of code: it happens because the game silently transitions to act 2, which features no such animated palette!


It looks particularly dumb when it freezes on the bright purple color, which in turn is exacerbated by the fact that, at the start of the boss fight, the palette used by the foreground rocks is dimmed significantly. This was likely done in order to make the boss' giant hand stand out more as it emerges from the bottom of the screen.

Tuesday, July 18, 2017

Door Into Bummer

Not far into act 1 of Lava Reef Zone, we come across this simple obstacle: a wall-mounted cannon over a closed door, shooting an endless barrage of teeny tiny pellets. Once the thing is put out of its misery, the door slides open, revealing the path to the rest of the stage.


When the cannon object is punched in the face, it does a couple of important things. First, it sets one of the flags in the level trigger array, based on the four lowest bits of its subtype. The trigger array is a set of general-purpose, temporary flags which get cleared whenever a level loads. In particular, the flag set by the cannon is under the watchful eye of the nearby door object. As soon as the flag is set, the door wakes up and does its thing.
loc_42E84:
    ...
    move.b  $2C(a0),d0
    andi.w  #$F,d0
    lea     (Level_trigger_array).w,a3
    lea     (a3,d0.w),a3
    moveq   #0,d3
    ...

sub_42EC0:
    ...
    bset    d3,(a3)
    move.l  #Obj_Explosion,(a0)
    move.b  #2,5(a0)
    ...
The second thing the cannon does is overwrite its own code pointer with the address for the enemy explosion object. It also sets the routine byte to 2 in order to skip over the initial bit which spawns a small animal along with a score popup. The key takeaway though, is that once the explosion object does its thing, it calls Delete_Current_Sprite, which deletes the object and ensures it doesn't spawn again lest the player wander off and then come back.

So far everything sounds good. But there's a wrinkle to this plan. Remember how I said the level trigger array is cleared whenever a level loads? Well, there's a star post right before this area. If you destroy the cannon, then hit the star post with enough rings to enter a bonus stage, and then come back...


Congratulations, you have screwed yourself over.

When you enter a bonus stage, the object respawn table for the current stage is not cleared. This is what prevents you from going back and tagging all the rings and enemies again. However, the level trigger array is cleared, putting you in a situation where the door is very much closed, but the only object that can do anything about it has been permanently removed from the map. The only way out of this mess is to lose a life or reload your save file.

Tuesday, July 4, 2017

Boss flash bloopers

The boss of Sandopolis Zone 2 uses two different palettes: one for the metallic interior, loaded over the enemy palette, and another for the stone shell, loaded over the palette for the stage's background, which is obscured at this point. It's actually this second palette that flashes when the boss takes damage.


Something seems a bit off, though: the two darkest colors of the shell don't flash, resulting in this strange outline effect. A quick look at the code reveals the cause:
loc_7820A:
    moveq   #0,d0
    btst    #0,$20(a0)
    bne.s   loc_78218
    addi.w  #$A,d0

loc_78218:
    bsr.w   sub_7825E
    subq.b  #1,$20(a0)
    ...
; ---------------------------------------------------------------------------

sub_7825E:
    lea     word_7826C(pc),a1
    lea     word_78276(pc,d0.w),a2
    jmp     (CopyWordData_3).l
; ---------------------------------------------------------------------------
word_7826C:
    dc.w  $FC6C, $FC6E, $FC70, $FC72, $FC74
word_78276:
    dc.w    $6E,  $24C,   $28,     4,     2
    dc.w   $AAA,  $CCC,  $CCC,  $EEE,  $EEE
There are five addresses, and two sets of five colors, but the copy function being called is CopyWordData_3. As such, only the first three colors will get pushed to CRAM. Replacing it with a call to CopyWordData_5 resolves the issue.


Lava Reef Zone 1 and Sky Sanctuary Zone also have something strange about their boss flashes. Some of the flash colors are actually much darker than the regular colors; that can't possibly be right.


Below is the the code used by the Lava Reef boss. Can you spot the defect?
loc_78C3A:
    moveq   #0,d0
    btst    #0,$20(a0)
    bne.s   loc_78C48
    addi.w  #4,d0

loc_78C48:
    bsr.w   sub_78C98
    subq.b  #1,$20(a0)
    ...
; ---------------------------------------------------------------------------

sub_78C98:
    lea     word_78CA6(pc),a1
    lea     word_78CB2(pc,d0.w),a2
    jmp     (CopyWordData_6).l
; ---------------------------------------------------------------------------
word_78CA6:
    dc.w  $FC26, $FC28, $FC30, $FC38, $FC3A, $FC3C
word_78CB2:
    dc.w    $2A,     6,     2,  $644,  $422,     0
    dc.w   $888,  $AAA,  $CCC,  $AAA,  $CCC,  $EEE
There are six addresses, two sets of six colors, and the copy function being called is CopyWordData_6. At first glance, everything seems to check out.

Look closer. When $20(a0) is even, d0 is incremented to skip the first set of colors and read from the second. But here, it's being incremented by 4, skipping only the first two colors. CopyWordData_6 will then dutifully copy over the next six colors, which are the last four entries from the regular set, and the first two entries from the flashing set.

A similar defect occurs in the Sky Sanctuary boss. Setting d0 to the correct value resolves the issue:


These defects clearly originate from human error. Each boss has its own flash code, so later bosses probably had their code copied and adapted from existing ones, but the programmer forgot to change the function call or the value of d0.

What's interesting to me is how both oversights are self-correcting: once the boss stops flashing, the normal colors are always restored, so it's hard to notice the problem. You have to be paying close attention to the boss while it's flashing, and you need to know the colors are wrong in advance.

Monday, June 26, 2017

Unlike Sonic 3, I don't chuckle

User muteKi posted over at the Sonic Retro forums:
One of the rather defining bits of Sonic 3 is the many moments where Knuckles comes around to throw a curveball into your path through a stage, with a distinctive giggle animation. (...) Lava Reef, however? (...) He just stands there, like a schmuck. No giggle there.

In none of the S&K cutscenes does he giggle, despite it being such a distinctive animation in Sonic 3.

Knuckles isn't playable in standalone Sonic 3, so there's no reason to include all of his sprites in the Sonic 3 cartridge, especially since player graphics are uncompressed and thus take up a lot of space. Instead, the game only stores the sprites used in each of his cutscene appearances, including the trademark giggle.


In Sonic & Knuckles, however, the game has access to nearly all of these poses from Knuckles' main art block, so the cutscene block isn't included. As such, the giggle animation fell by the wayside.

The interesting bit is that in the earliest prototypes we have, he does giggle in Lava Reef.


In the April and May prototypes, the LRZ2 scene uses the cutscene art block, along with the punching animation from Launch Base 2. Starting from the 0606 prototype, it instead uses the player art block and a custom animation. Rather unsurprisingly, the 0606 prototype is also the first build which no longer includes the Sonic 3 half of the game.

My guess is, when it came time to remove all the Sonic 3 dependencies, it didn't make sense to keep the cutscene art block around for just one level, and it was too late in development to do something like split off the giggling sprites and their mappings, and rewrite the object so it switches art and mappings mid-execution.