Tuesday, November 14, 2017

Return of the scourge

Geek critiquer and long-time reader Josh Wallen asks:
I also recall a glitch where Sonic will very briefly change to a different color, just after turning Super. It _may_ have just been an emulator bug circa 2000, but I've been playing Complete so long that it's hard to remember. Any insight on this?
Chances are it was no emulator bug. There's definitely something wrong with the palette transition during Sonic's super transformation, but it only manifests itself after you've reverted back to normal Sonic at least once in the current level.


Here's the code which handles the palette transition:
SuperHyper_PalCycle_FadeIn:
    ; increment palette frame and update Sonic's palette
    lea     (PalCycle_SuperSonic).l,a0
    move.w  (Palette_frame).w,d0
    addq.w  #6,(Palette_frame).w        ; 1 palette entry = 1 word, Sonic uses 3 shades of blue
    cmpi.w  #$24,(Palette_frame).w              ; has palette cycle reached the 6th frame?
    blo.s   SuperHyper_PalCycle_SonicApply      ; if not, branch
    move.b  #-1,(Super_Hyper_palette_status).w  ; mark fade-in as done
    move.b  #0,(Player_1+object_control).w      ; restore Sonic's movement

SuperHyper_PalCycle_SonicApply:
    lea     (Normal_palette+$4).w,a1
    move.l  (a0,d0.w),(a1)+             ; Write first two palette entries
    move.w  4(a0,d0.w),(a1)             ; Write last palette entry
    ...
The disassembly comments already spell out most of it, so let's focus on that Palette_frame RAM variable. It is a word value used as an offset into the PalCycle_SuperSonic array, which contains Sonic's transformation colors, followed by Super Sonic's color cycle starting at offset $24:
PalCycle_SuperSonic:
    dc.w $E66,$C42,$822     ; 0
    dc.w $E88,$C66,$844     ; 6
    dc.w $EAA,$C88,$A66     ; $C
    dc.w $ECC,$EAA,$C88     ; $12
    dc.w $EEE,$ECC,$EAA     ; $18
    dc.w $EEE,$EEE,$EEE     ; $1E

    dc.w $CEE,$CEE,$AEE     ; $24
    dc.w $AEE,$8EE,$6CC     ; $2A
    dc.w $8EE,$0EE,$0AA     ; $30
Once Palette_frame reaches $24, the transition code stops being called, and the code for the main palette cycle takes over, incrementing Palette_frame every 7 frames and setting it back to $24 when it goes outside the array's bounds:
SuperHyper_PalCycle_SuperSonic:
    ; run frame timer
    subq.b  #1,(Palette_timer).w
    bpl.w   locret_37EC
    move.b  #6,(Palette_timer).w

    ; increment palette frame and update Sonic's palette
    lea     (PalCycle_SuperSonic).l,a0
    move.w  (Palette_frame).w,d0
    addq.w  #6,(Palette_frame).w    ; next frame
    cmpi.w  #$36,(Palette_frame).w  ; is it the last frame?
    blo.s   loc_3898                ; if not, branch
    move.w  #$24,(Palette_frame).w  ; reset frame counter

loc_3898:
    bra.w   SuperHyper_PalCycle_SonicApply
Note how Palette_frame is post-incremented, that is, its value is being copied to d0 before the variable is incremented, so PalCycle_SuperSonic is always being accessed using a stale offset.

Finally, when the rings run out, the code below runs the transition colors in reverse, ending on Sonic's regular palette:
    ; decrement palette frame and update Sonic's palette
    lea     (PalCycle_SuperSonic).l,a0
    move.w  (Palette_frame).w,d0
    subq.w  #6,(Palette_frame).w    ; previous frame
    bhs.s   loc_381E                ; branch, if it isn't the first frame

    ; The following line should be a move.w
    ; This causes the fade-in to be bugged
    move.b  #0,(Palette_frame).w
    move.b  #0,(Super_Hyper_palette_status).w       ; 0 = off

loc_381E:
    bra.s   SuperHyper_PalCycle_SonicApply
Here, a nasty consequence of using stale values rears its ugly head. After d0 has been set to zero, thus pointing at the first set of colors in the PalCycle_SuperSonic color array, Palette_frame is decremented one too many times to -6, or $FFFA, and must be manually set back to zero, so that it's ready next time Sonic transforms.

However, as the disassembly comments point out, the developers made a mistake, and instead of writing a word value of zero, the code writes a single zero byte to the Palette_frame RAM address, overwriting the variable's first byte. This corrupts the value $FFFA into $00FA, and so next time the transition code runs, it will read three colors from offset $FA of PalCycle_SuperSonic, which happens to straddle the color arrays for Hyper Sonic and Super Tails:
    dc.w $EEE,$EEE,$EEE     ; $F6

PalCycle_SuperTails:
    dc.w $0AE,$08E,$46A     ; $FC
    dc.w $4CE,$2AE,$46A     ; $102
The result is that for a couple of frames, Sonic is a mix of white and Tails' oranges, which actually doesn't look that bad:


The transition code continues: Palette_frame is incremented from $FA to $100, which is definitely greater than $24, so the main palette cycle takes over, incrementing Palette_frame every 7 frames and setting it back to $24 when it leaves the array's bounds. $100 is definitely outside the array's bounds, so Palette_frame is reset.

Remember how PalCycle_SuperSonic is always accessed using a stale offset, though? This time around, the offset is $100, meaning that for the following 7 frames, Sonic will use the next three colors from Super Tails' palette cycle, which happens to place dark brown in an unfortunate place:


But wait, it's even better in standalone Sonic 3. Hyper Sonic and Super Tails don't exist yet, and as a result, offsets $FA and $100 just happen to land smack dab in the middle of the Pal_FromBlack function:
    ...
    moveq   #0,d0                       ; $FA
    lea     (Water_palette).w,a0        ; $FC
    lea     (Target_water_palette).w,a1 ; $100
    move.b  (Palette_fade_info).w,d0    ; $104
    ...
Translating the instructions into processor opcodes, we get the following word values:
    ROM:000032BC    7000                ; $FA
    ROM:000032BE    41F8 F080           ; $FC
    ROM:000032C2    43F8 F000           ; $100
    ROM:000032CA    1038 F626           ; $104
Recalling that Mega Drive colors follow the bit pattern 0000BBB0GGG0RRR0, those word values become the colors
    dc.w $000,$0E8,$080                 ; $FA
    dc.w $2E8,$000,$028                 ; $100
which in turn translate to black / lime green / dark green, and lime green / black / dark red.


It's our boy, Ashura!

10 comments:

  1. Can I call him Scourge or Super Scourge instead?

    ReplyDelete
  2. I was going to ask what happens in Sonic 3 but I see you already answered that.

    This is a bit unrelated, but does the RAM address that controls the super emerald count do anything in Sonic 3?

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Most questions of the form "what happens when you do this thing" can be answered by doing the thing and observing the results.

      Delete
  4. Not sure if this is relevant to the blog, but the same bug exists in Sonic 2. Also Sonic's head is sheared off his body in the last few frames of the animation.

    ReplyDelete
    Replies
    1. Yeah, the bug was originally introduced in Sonic 2.

      Concerning Sonic's head, I mentioned this in the preceding post. It's almost like I try to imbue some continuity into this blog. :)

      Delete
  5. Yup. Just my luck. I post the comment, get distracted for a few hours, come back to read the next post, and see it's too late to amend the comment =P.

    ReplyDelete
  6. Thanks for answering! The dark green palette in standalone Sonic 3 was definitely what I was remembering.

    ReplyDelete
  7. So interesting thing, when one of the frames of the transformation is used in launch base underwater, it changes sonic's color to a black with red streaks on his head, ironically it looks like Shadow.

    Maybe a look into how water palettes work with the super palettes?

    ReplyDelete
  8. Scourge in an official Sonic game, done PROPERLY AS AN INTENDED CHARACTER; that would be interesting...

    ReplyDelete