Thursday, November 30, 2017

VDP sprite masking

Particularly astute readers will notice there's something off about my previous post. If the Super gumball is a sprite, and it's being displayed behind the level blocks...


...then that means the level blocks must have their priority bit set. If that's the case, though, then the only way to display other sprites over them is to set their priority bit as well, which causes them to be displayed over all the level blocks:


So how do we get around this? It's time to break out the big guns.

Remember how the VDP implements the sprite attribute table as a linked list of sprite data? There's an obscure feature where if, in the process of rendering a row of pixels, the VDP encounters a sprite whose X coordinate is exactly zero, it will not render any sprites that appear further down the list.


The trick is to push the player sprites and the machine's handle into the table first, then push a garbage sprite with an X coordinate of zero, and finally push the gumball sprites which make up the contents of the machine. Coordinate zero is far left of the visible region of the screen, so the garbage sprite is never actually drawn, but it masks the gumball sprites because they appear further down the list.


The same trick is used in Sonic 2's title screen animation. When Sonic and Tails rise from inside the wing emblem, their sprites are actually fully constructed from the start and are simply moved up in order to gradually reveal them. A couple of garbage sprites at X coordinate zero prevent them from peeking out from the underside of the emblem.


Sonic 1's title screen behaves very similarly, but without actually using the sprite masking trick. Instead, it just places an absurd amount of garbage sprites immediately off screen, causing the sprite limit to kick in early, which produces a nigh indistinguishable effect.

Wednesday, November 29, 2017

Featuring S from the Gumball series

After reading yesterday's post, you might be under the impression that normally, the graphics for the Super gumball are completely unused. Well, you're half right.


If we get rid of the high priority sprites lining the top half of the gumball machine, we can get a better look at the generic placeholder gumballs which make up the foreground plane:


Oh, it seems we have a low priority sprite hiding behind the foreground layer. You would be forgiven for thinking that it's just a second Thunder Barrier, but we know better by now.


Hey, at least they managed to find a use for one of the sprites!

Tuesday, November 28, 2017

Super Gum

If you examine the gumball object's setup code at sub_612A8, you may notice how the object's mapping frame is at no point determined by its subtype value. Instead, the code copies the ROM address of one of several animation scripts to offset $30 of its SST, which the object later processes by calling the Animate_Raw function:
word_61466: dc.w $7F08
            dc.w $8FC
word_6146A: dc.w $7F09
            dc.w $9FC
word_6146E: dc.w $7F0A
            dc.w $AFC
word_61472: dc.w $7F0B
            dc.w $BFC
word_61476: dc.w $7F0C
            dc.w $CFC
word_6147A: dc.w $7F0D
            dc.w $DFC
word_6147E: dc.w $7F0E
            dc.w $EFC
word_61482: dc.w $7F0F
            dc.w $FFC
word_61486: dc.w $110
            dc.w $11FC
Hmm, it doesn't look like an animation script? Blame the formatting in the disassembly. It should start looking a bit more familiar if we break it into bytes:
word_61466: dc.b  $7F,   8,   8, $FC
word_6146A: dc.b  $7F,   9,   9, $FC
word_6146E: dc.b  $7F,  $A,  $A, $FC
word_61472: dc.b  $7F,  $B,  $B, $FC
word_61476: dc.b  $7F,  $C,  $C, $FC
word_6147A: dc.b  $7F,  $D,  $D, $FC
word_6147E: dc.b  $7F,  $E,  $E, $FC
word_61482: dc.b  $7F,  $F,  $F, $FC
word_61486: dc.b    1, $10, $11, $FC
Ah, that's more like it. Unsurprisingly, the animations have nothing to them: the same mapping frame is displayed twice, and the animation starts over. It seems pointless for the gumball object to use animations at all, doesn't it? That is, until you realize there are nine scripts, and that the last one alternates between two distinct mapping frames:


Given the letter printed on its surface, and its flashing coloration reminiscent of Super Sonic, it think it's fair to say we're looking at an unused ninth gumball, which would change the player into their super form. Indeed, if the gumball object's subtype is manually set to 8, it will use a unique item collection routine along with this animation.


Unfortunately, the item collection routine doesn't do much: all it does is set bit 7 of the Saved_status_secondary RAM address, which has nothing to do with super transformations and whose bits are all cleared on level load except for the three elemental shield bits, anyway.
loc_61264:
    bset    #7,(Saved_status_secondary).w
    rts
My guess is that at some point, bit 7 would spawn the player directly in their super form. It would be pretty dumb for the gumball's effect to wear off as soon as you left the bonus stage!

Monday, November 27, 2017

Aww, no grapes? What a rip-off.

In the Gumball Machine bonus stage, each time the player rotates the handle, a new gumball object is spawned, with a random subtype. This is achieved by calling the Random_Number function, which produces pseudo-random longword values along a uniform distribution.


The code at sub_612A8 whittles this value down to its four lower bits, generating a random number between 0 and 15, which is then used as an index into a table containing the actual subtype values, which in turn range from 0 to 7:
byte_612E0:
    dc.b 0  ; 1-Up Ball
    dc.b 3  ; Clear
    dc.b 1  ; Replace Ball
    dc.b 4  ; Bumper
    dc.b 2  ; Ring Ball
    dc.b 4  ; Bumper
    dc.b 5  ; Flame Barrier
    dc.b 4  ; Bumper
    dc.b 6  ; Aqua Barrier
    dc.b 3  ; Clear
    dc.b 7  ; Thunder Barrier
    dc.b 4  ; Bumper
    dc.b 5  ; Flame Barrier
    dc.b 6  ; Aqua Barrier
    dc.b 7  ; Thunder Barrier
    dc.b 2  ; Ring Ball
This effectively converts the uniform distribution of the randomly-generated number into a weighted probability function: for instance, the Thunder Barrier appears twice in the table, so it's twice as likely as a Replace Ball, which only appears once. Meanwhile, it's only half as likely as a bumper, which appears four times in the table.


There's a trick, though. Whenever the resulting gumball is a 1-Up Ball, a RAM flag is set which causes any subsequent 1-Ups to be replaced with the fourth entry from the table, producing a bumper:
sub_612A8:
    jsr     (Random_Number).l
    andi.w  #$F,d0
    bne.s   loc_612C2
    movea.w $46(a0),a1
    bset    #7,$38(a1)
    beq.s   loc_612C2
    moveq   #3,d0

loc_612C2:
    move.b  byte_612E0(pc,d0.w),d0
    move.b  d0,$2C(a0)
    ...
As a result, only a single 1-Up Ball will ever spawn per bonus stage, and once it does, the random distribution becomes even more skewed toward bumpers. Here's all of this information compiled into a table, because people like tables:

Gumball TypeLikelyhood (before 1-up)Likelyhood (after 1-up)
1-Up Ball1/160/16
Replace Ball1/161/16
Ring Ball2/162/16
Clear2/162/16
Bumper4/165/16
Flame Barrier2/162/16
Aqua Barrier2/162/16
Thunder Barrier2/162/16

Friday, November 24, 2017

Unused graphics: Beam Rocket's bomb chute

Okay, one last unused sprite to round out the week. Here are the full contents of the art file for the Launch Base Zone 2 boss, Beam Rocket, unchanged all the way from standalone Sonic 3. Notice anything weird at the end?


By applying the correct mapping data, we find the following two sprites: the bomb from Knuckles' end of level cutscene, and a narrow, unrecognizable, unused sprite:


Now, it's strange to find the bomb in this art file because the end cutscene only plays after fighting Big Arm, which loads its own graphics over Beam Rocket's. The logical explanation is that originally Knuckles wasn't planned to fight Big Arm and as such, the bomb graphics would still be loaded by the time the cutscene takes place.

As it turns out, this is the behavior present in every build up to and including the 0525 prototype, and playing any one of them sheds further light on the narrow sprite's original purpose: it's a chute to release the bomb from!


When the developers added the Big Arm boss to this sequence, they hit a snag: the bomb chute graphics are no longer loaded once Big Arm is breaking apart, and Beam Rocket's graphics can't be reloaded because Big Arm's parts are still on screen. So they did the only thing they could do, and removed the bomb chute sprite entirely.


Once the boss is offscreen, the game can reload Beam Rocket's graphics in order to display the bomb sprite. Curiously though, it keeps the Big Arm palette around, and so the sprite is drawn a little redder than was originally intended.

Thursday, November 23, 2017

Unused graphics: ice rock and ice block

Two more unused sprites, this time in Icecap Zone. One of the PLCs from act 1 contains what appears to be a large ice boulder. No object in the final game uses the sprite, but the mappings are still in the ROM and... this thing is huge.


The mappings cleverly reuse the same pieces in the top and bottom halves of the sprite, as to maximize its size without using up too many tiles. It fits snugly in act 1's narrow corridors, so maybe that's where it was meant to be used.


The second sprite is present in both acts, and has a fully-functional object associated with it, which can be placed using debug mode. It's a square block that breaks when you jump on it, similar to an obstacle used in Sonic 1's Marble Zone.


Its size and appearance suggest it was meant to be placed in the chutes which lead into act 2's underground area. The final game plugs up the top of these chutes using a different object, which is in turn lifted from Sonic 2's Hill Top Zone.

Wednesday, November 22, 2017

Unused graphics: shattered lightbulb

Carnival Night Zone is also home to some unused button sprites. However, unlike the ones in Angel Island Zone, these have fully programmed objects associated with them, which can even be placed in the level using debug mode!


The only two buttons in the entire level, which control the lights in act 2, simply use the same generic button sprite seen elsewhere in the game. I guess the developers didn't want Knuckles to have a different button for just this one level.

Speaking of the lights in act 2, the area where Knuckles kills them features a unique object which isn't placed anywhere else in the stage: a pair of lightbulbs, seemingly for decoration. Peeking into the object's art file, though, reveals it has a second frame of animation, in which the lightbulb has shattered!


That's not even the shocking part, though. The lightbulb object actually contains logic which causes it to shatten when it becomes submerged in water!

Given the placement of the objects, along with the fact that the water rises once Knuckles presses the button, I think it's fair to assume that originally the button would raise the water high enough to shatter the lightbulbs, causing the lights to go out. Here's a video illustrating what that may have looked like, as well as the unused button graphics:


Note how Knuckles becomes discolored once the water level crosses over his sprite. This is because his palette is only loaded to the normal palette slot and not the water palette slot, and it could have contributed to the scene being altered for the final version of the game.

Tuesday, November 21, 2017

Unused graphics: Knuckles' button?

Here's another one from Angel Island Zone. One of act 2's PLCs loads the following graphics to VRAM address $8700:


The first eight tiles are used by the background trees at the very end of the stage. The last seven tiles, however, cannot be seen in-game because they're instantly overwritten with the graphics for breakable floors.

No sprite mappings for these tiles seem to have survived in the final game, so we can only speculate about the object's intended appearance. However, by applying the player palette and arranging the tiles into sensible shapes, we arrive at the most likely configuration: a button.


Now, since these graphics are loaded at the start of the stage, they could have been meant for anything. But taking into account that they're included in the same art file as a tree sprite which only appears at the very end of the level, we can probably assume the button was also meant to be used near the end of the level.

Another clue comes from the graphics themselves, namely the blades of grass at the bottom, which are rendered using Tails' colors, no less. Normally, it would be unnecessary to bake the grass into the sprite like this: so long as the sprite's priority flag is clear, the object should naturally be drawn behind the level blocks.


The exception is at the very end of the level, where the background waterfall blends directly into the grassy floor. In this scenario, when a sprite's priority flag is clear, it gets drawn behind both the grass and the waterfall, and when it is set, it gets drawn in front of both. Baking the sprite is the only way to make it show up in front of the water and behind the tips of the grass which, you guessed it, perfectly match Tails' colors at this spot.

So the button was likely meant to be placed at the waterfall, possibly making it a precursor to the one used by Knuckles to drop Sonic and Tails into Hydrocity Zone. Here's how that could have looked:

Monday, November 20, 2017

Unused graphics: Tornado's shadow

As a change of pace, I thought it'd be nice to look at some of the graphics which go unused in the game. Below are the full contents of the Tornado art file used in Angel Island Zone's intro. Yes, Tails' head is baked directly onto the plane.


Surprisingly, the landing gear and rocket are separate sprites just like in Sonic 2, even though both of them are always attached to the plane in Sonic 3. More surprisingly, however, are the black oblong shapes at the end of the art file.

No objects in the game's code use these sprites, so one can only speculate about their intended use. My guess is they were meant to form a shadow, cast by the plane onto the ocean surface below. Here's a mock-up animation illustrating what it might have looked like:


Why this effect wasn't used, we will likely never know. Perhaps the developers weren't happy with how it looked. It does look rather strange; no other objects in the game cast any shadows, and it may have been too strange that Sonic didn't cast a shadow once he jumped off the plane.

Friday, November 17, 2017

The missing animation

In his glitches and oversights series, GoldS shows us that when Tails carries Super Sonic into the Knuckles cutscene in Launch Base Zone 1, Sonic does some fancy air surfing. But what exactly is going on behind the scenes?

Well first off, Sonic is floating in mid-air because the cutscene sets Sonic's object_control attribute, in order to prevent him from jumping out of the teacup lift he usually rides in. This forces Tails to let go of Sonic, which normally triggers an obscure animation where Sonic frantically does mid-air crunches for some reason.


Now, player characters have around thirty-something animations each, and the first 31 are shared between all of them, using up IDs 0 through $1E. The "falling off Tails" animation is ID $22, making it a character-specific animation.

This makes sense considering that normally, Sonic is the only character who can be carried by Tails. However, if Sonic and Tails' Marble Garden Zone 2 boss is fought as Knuckles, Tails will carry Knuckles, and when he drops him, we can see that ID $22 corresponds to Knuckles' "getting up from glide-landing" animation:


Sonic doesn't have any special abilities like flying or climbing, so most of his character-specific animations are unused. Other than the "falling off Tails" animation, he only has the super transformation sequence, which was Sonic-exclusive until Sonic & Knuckles!
        dc.w byte_12C1C-AniSonic_       ; $1E  Unused
        dc.w byte_12CA4-AniSonic_       ; $1F  Super transformation
        dc.w byte_12C24-AniSonic_       ; $20  Unused
        dc.w byte_12C28-AniSonic_       ; $21  Unused
        dc.w byte_12C2C-AniSonic_       ; $22  Falling off Tails
        dc.w byte_12C32-AniSonic_       ; $23  Unused
Super Sonic goes a step further: his animation table ends at the super transformation sequence, leaving the "falling off Tails" animation dangling:
        dc.w byte_12C1C-AniSuperSonic_  ; $1E  Unused
        dc.w byte_12CA4-AniSuperSonic_  ; $1F  Super transformation

byte_12C7A:     dc.b  $FF,   1,   2,   3,   4,   5,   6,   7,   8, $FF
When an animation ID greater than $1F is requested, arbitrary bytes from Super Sonic's walking animation definition at byte_12C7A are interpreted as an offset to apply to the AniSuperSonic_ table at ROM address $12C3A. When the ID is $22, the bytes chosen are 4 and 5, which are interpreted as an offset of $405, causing the animation to be read all the way from ROM address $1303F!

This address is way beyond the end of Super Sonic's animation table: it actually goes right past the Sonic_Load_PLC and Tails_Carry_LoadPLC functions, past the animation code for Competition mode characters, and ends smack dab in the middle of Competition mode Sonic's idle animation.
                                                          |
                                                          V
byte_13038:     dc.b    7, $1B, $1B, $1B, $1B, $1B, $1B, $1B, $1B, $1C, $1C, $1C, $1C, $1C, $1C,
                dc.b  $1D, $1E, $1D, $1E, $1D, $1E, $1D, $1E, $1D, $1E, $FE, $10
If you recall how the animation system works, the first byte of each animation defines how many frames to wait before switching mapping frames, so this animation will wait $1B or 27 frames between each mapping frame. The frames the animation will display, on the other hand, are $1B... $1C... $1C... $1C...


...which is essentially a very bad tour of Sonic's diagonal ceiling walking frames.

Thursday, November 16, 2017

The unused idle frame

An anonymous commenter asked:
Do you plan to show off Super Sonic's unused idle frame? Not even TCRF seems to know about it.
Not even I knew about it until you pointed it out!

Check it out, this is pretty cool. Super Sonic has a third, unused mapping frame in his standing animation, which reuses graphics from the first frame. However, the sprite pieces have different shapes, so his head comes out all crumpled:


Looking through Sonic's art, however, we find that the proper head graphics are right where you'd expect them: directly after the graphics for the other two frames. The third frame is simply using the DPLC entry meant for the first frame.


And so, if we fix up the DPLC script, edit the standing animation so it uses the third frame, restore the truncated palette cycle, and while we're at it, shift Super Sonic's misaligned head a single pixel to the right, we can finally see what could have been. Final sprite on the left for comparison purposes:


Which one do you guys prefer? Let me know in the comments, and thank you so much for the great question. This post would not have happened if it weren't for your valuable feedback.

Wednesday, November 15, 2017

The truncated palette cycle

Last time, we saw that the code for Super Sonic's palette cycle sets the Palette_frame RAM variable back to $24 when it's outside the PalCycle_SuperSonic array's bounds. It does so by checking if the variable's value is $36 or higher:
    ; 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
This code causes the Palette_frame variable to loop between the values $24, $2A, and $30, giving Super Sonic a nice three-step strobe light effect:


What I didn't mention before is how the PalCycle_SuperSonic array actually has enough entries for a four-step palette cycle, the last of which are never used due to the above check:
    dc.w $CEE,$CEE,$AEE     ; $24
    dc.w $AEE,$8EE,$6CC     ; $2A
    dc.w $8EE,$0EE,$0AA     ; $30
    dc.w $AEE,$8EE,$6CC     ; $36
Applying the full cycle grants Super Sonic a smoother, pulsating light effect, closer to the other characters' Super forms:


It is unclear whether the extra colors were unused intentionally, or due to an oversight. In Super Sonic's regular palette, the last set of colors is identical to the second, producing the pulsating effect seen above. With the underwater palettes however, the last set of colors is identical to the the third, which would cause the palette cycle to hang awkwardly on its darkest step before resetting.

Whichever it was, my guess is that the developers decided the truncated version was good enough, so when the water palettes were later produced, they were only made to conform to the three-step cycle, with the last set of colors serving as padding.

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!

Monday, November 13, 2017

Misalignments

Have you ever noticed that the Pointdexter enemy from Hydrocity Zone contains a slight error in its animation?


Each of Pointdexter's sprites are comprised of three sprite pieces: the front, the rear, and the torso. The torso has three different variations, one for each frame of animation: deflated, partially inflated, and fully inflated.


In practice, though, the arrangement of sprite pieces used in the partially inflated frame leaves a lot to be desired, when compared to the other two frames:


Here's what happened. The graphics for the partially inflated torso were squeezed into the smallest sprite they would fit in, a 3x3 square, in order to consume as little VRAM as possible. However, the sprite mappings applied to the graphics do not properly center the torso piece with the front and rear pieces. This is what it should look like:


This is an infrequent yet recurring error, which isn't limited to inbetween frames of an enemy's animation. Below are the sprites used for Sonic's super transformation. Pay special attention to the last two frames. Notice anything funny?


Here the developers once again identified an opportunity to squeeze Sonic's head into a 3x3 square. Why they only did so with the last two frames and not the grayscale counterpart before them is beyond me, but it just makes the oversight that much more obvious: the head piece is misaligned, apparently coerced into an 8x8 grid as with Pointdexter above.

So, what's going on? My theory is that the graphics were chopped up by hand, but whatever tool they used to generate sprite mappings could only output simple mappings broken up along an 8x8 grid, and these are simply instances where the developers forgot to go in and touch them up by hand.

Friday, November 10, 2017

The limbo bug simplified

One last post to wrap up the subject. What is the easiest way of triggering the limbo bug? Simple, just enter the vertical cylinder while standing on top of a solid object.


We saw before that the cylinder object calls the RideObject_SetRide function, which unsets bit 3 or 4 of the object that the player was previously standing on. It just turns out, solid object collision functions such as SolidObjectFull also call the RideObject_SetRide function, which in turn will clear bit 3 or 4 of the cylinder object, triggering the limbo bug.

You might imagine this leading to an endless tug of war between both objects, but the solid object only steals the player back after the cylinder object sets bit 6 of the player's object_control attribute, disabling certain object collisions, which include the cylinder object itself, causing the player to just fall through it.

Since the bug was triggered by standing on a solid object, you unfortunately don't get the effects of slope glitch, but you do get a frozen sprite and can go through walls all the same.

Thursday, November 9, 2017

The limbo bug explained

A couple of days ago, I said I would look into the method speedrunners use to trigger the limbo bug. Now, I'm back with the results. Surprisingly, it all has to do with the camera.

First, I'll post the video again for your convenience. Here's Flying Battery Zone 1 in 0:55 by Zurggriff, on June 16, 2017:


Okay, at the very start of the level, there's a long vertical shaft. When Sonic enters this shaft,
he's forced into a spin by one of the two objects at either end, pictured to the right.

This object forces Sonic into a spin by doing two things. One, it sets Sonic's spin dash flag at byte $3D of his SST. Two, in case he wasn't already spinning, it changes his animation to the spinning animation, and plays the spin sound effect for good measure.

The spin dash flag induces two distinct behaviors, depending on Sonic's current animation. If Sonic is in his spinning animation, then it will give Sonic a small boost of speed when he's moving too slowly. If that sounds familiar, it's because this behavior was used extensively in Sonic 2's Casino Night Zone.

However, if Sonic isn't in his spinning animation, the flag indicates he is currently charging a spin dash, which is released as soon as the player stops holding the down button.

The intended behavior is that when Sonic passes through the bottom object, his spin dash flag is set, forcing him into a spin, and then when he passes through the top one, the flag is cleared again. At the start of the level, though, the top object is far enough from the camera that the object manager elects not to spawn it.

When looking up or down, the camera's max vertical speed is temporarily reduced so that it will gently slide over to its target position. By stepping on the booster pad during this period, Sonic is able to zoom past the object at the top of the shaft before the camera can scroll up enough for it to spawn. This causes Sonic's spin dash flag to remain set.

Another special behavior of the camera is that the game is constantly recording the last few player positions to a buffer. When a spin dash is released, the camera temporarily uses values from this buffer rather than the player's live position, so that the player will briefly outrun the camera.

All of these aspects collide once Sonic reaches the vertical cylinder object. The object sets Sonic's object_control and status bitfields, and bumps him out of his spinning animation. Sonic's spin dash flag is still set, but since he isn't in his spinning animation any more, a spin dash release is triggered, assuming of course the player isn't holding down.

The cylinder object sets player 1's ride flag in own its status bitfield but at the same time, the spin dash release causes the camera to start reading from the player position buffer. The camera quickly snaps to where it was a short while ago, which was quite a ways to the left. So far left, in fact, that the cylinder object scrolls far enough off screen to be deleted.

Of course, when the camera scrolls back to the right, the object is loaded from the object layout again. But the damage is done. The object respawns with a clean status bitfield, and once again has no idea that player 1 was riding it.

Do the limbo, baby.

Wednesday, November 8, 2017

Unsafe cylinder DPLC, part 3: flipping the tables

Okay, this is a good one. While writing yesterday's post, I had to look at the code for the RideObject_SetRide function, and I came across this gem, right at the start:
RideObject_SetRide:
    btst    #3,$2A(a1)
    beq.s   loc_1E4A0
    movea.w $42(a1),a3
    bclr    d6,$2A(a3)

loc_1E4A0:
    move.w  a0,$42(a1)
    ...
We've seen this before. Bit 3 of the player's status bitfield is the "standing on object" flag. When the player lands on an object that's using the SolidObjectFull function, the object sets this bit and copies the address of its own SST to offset $42 of the player's SST. It also sets bit 3 or 4 of its own SST, depending on which player landed on it.

The code at loc_1E4A0 above wants to do the same, but before it can do so, it needs to check whether the player was already standing on another object, and if so, clear bit 3 or 4 of its SST. Which bit it clears is indicated by register d6.

You can already tell where this is going.

If both players are riding the same vertical cylinder object, and the object changes player 1's mapping frame, player 2's handler runs with d6 erroneously set to 1, which corresponds to the vertical flip flag in the object's status bitfield.

Time to put the pieces together. If player 2 is standing on a solid object, player 1 is riding a cylinder, and player 2 starts riding the cylinder at the exact same time that the cylinder object changes player 1's mapping frame, player 2's handler will call RideObject_SetRide with d6 set to 1, clearing the vertical flip flag of the object player 2 was standing on.

At least that was the theory. If the object player 2 was standing on just happened to be upside down...


I love this game so damn much.

Tuesday, November 7, 2017

Unsafe cylinder DPLC, part 2: limbo bug

In yesterday's post, we saw how due to an oversight in the mesh tube object from Flying Battery Zone, each time Sonic changes to a different mapping frame, the object uses the wrong bit of its status bitfield to track Tails' state, causing him to periodically stop riding the object.

The bit used in this scenario is 1, obtained by incrementing from the low byte of Sonic's art address, which thanks to an alignment directive, is always zero. But what if we shifted that address forward by, say, two bytes?
    align $8000
    ds.b 2
ArtUnc_Sonic:
    binclude "General/Sprites/Sonic/Art/Sonic.bin"

Holy goodness, what happened here?

Let's go back to the fundamentals. The mesh tube object uses bit 3 of its status bitfield to track player 1's state, and the bit obtained by incrementing d6 to track player 2's state:
loc_3A252:
    lea     (Player_1).w,a1
    lea     $30(a0),a2
    moveq   #3,d6
    bsr.s   sub_3A270
    lea     (Player_2).w,a1
    lea     $38(a0),a2
    addq.b  #1,d6
    bsr.s   sub_3A270
    jmp     (Delete_Sprite_If_Not_In_Range).l
Normally, this is bit 4, but when Sonic's mapping frame changes, d6 gets overwritten with the ROM address for Sonic's art. Thanks to the realignment we did, the low byte of this address is now 2, which incremented becomes... 3. Uh oh.

We are now using bit 3 to track the state of both players. This way lies madness. Let's try jotting down the basic logic for the sub_3A270 function when it handles one of the players:

  • If the player's ride bit is clear:
    • If the player is running on the ground, within the object's range:
      • Set the player's ride bit
      • Override the player's controls
      • Move and animate the player along the mesh tube
  • If the player's ride bit is set:
    • If the player is jumping, moving slowly or outside the object's range:
      • Clear the player's ride bit
      • Restore the player's controls
    • Otherwise:
      • Move and animate the player along the mesh tube

When player 1 gets within range of the object, the function takes the top path, setting bit 3 of the object's status bitfield, overriding player 1's controls, and animating their sprite, which changes their mapping frame:

  • If player 1's ride bit is clear:
    • If player 1 is running on the ground, within the object's range:
      • Set player 1's ride bit
      • Override player 1's controls
      • Move and animate player 1 along the mesh tube

The function then immediately goes on to handle player 2. However, because player 1's mapping frame changed, d6 is accidentally set to 3 again. The function checks player 1's ride bit again, which is set. Player 2 is not within the object's range, so the function takes the middle path:

  • If player 1's ride bit is set:
    • If player 2 is jumping, moving slowly or outside the object's range:
      • Clear player 1's ride bit
      • Restore player 2's controls

The result is that in one fell swoop, the mesh tube object sets player 1's ride bit, overrides their controls, then clears the ride bit without actually restoring them. Worse, the ride bit is now off so the object has no idea any of this happened; for all it knows, the player exited the mesh tube normally. The player is now stuck in limbo, which is why when I first ran across this bug, I called it the limbo bug:


So, what exactly is the limbo bug? When the mesh tube object overrides the player's controls, it does two things: first, it calls the RideObject_SetRide function, which among other things sets the player's "standing on object" flag, activating slope glitch. Then, it also sets bits 1 and 6 of the player's object_control attribute, at offset $2E of its SST.
loc_3A2F0:
    move.l  #0,(a2)
    move.b  d2,4(a2)
    move.w  d3,6(a2)
    bset    #1,$2A(a1)
    jsr     (RideObject_SetRide).l
    move.b  #$42,$2E(a1)
    bra.s   loc_3A314
As we saw last time, bit 1 disables the player's animation routines so that another object can impose its own animation; in this case, it's the 3D rotations used by the mesh tube. Meanwhile, bit 6 seems to disable various collisions: walls are intangible except when airborne, and objects such as fans and quicksand have no effect.

Okay, but we had to go in the ROM and intentionally misalign Sonic's art just to get here, right? It should be impossible to cause this bug in an unmodified game.

Not so fast. For a while now, speedrunners have used the limbo bug in order to skip most of Flying Battery Zone 1:


This method of triggering the bug is slightly different, and one which I do not fully understand yet, but I'll be sure to post about it sooner rather than later.