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!