Friday, December 29, 2017

Roll height bugs, part 3

Modifying the player's hitbox when they curl into a ball can sometimes lead to unfortunate consequences. For example, they might be able to roll underneath badly-placed collision change objects and break the level collision. Also, because jumping makes the hitbox return to its regular size, players can get crushed in situations in which they would usually be fine, such as while breaking through Icecap Zone's stacked ice blocks.


However, there are times in which the game shines through. If Sonic breaks into a roll while upside down, the game will actually shift him up by five pixels rather than down, in order to keep him attached to the ceiling.


As we saw before though, when Sonic is rolling, the camera is programmed to aim five pixels above his actual position. Together with the change to his Y coordinate, this causes the camera to awkwardly jerk up a total of 10 pixels.

Thursday, December 28, 2017

Roll height bugs, part 2

Objects that take control away from the player generally fail to handle the case in which Sonic is spinning. For instance, with the tube elevators I talked about last time, when you spindash directly into one, Sonic's feet will dig into the bottom of the capsule because his hitbox size and Y position aren't reset to their usual values.


This sort of oversight is normally harmless because Sonic only sinks into the object by about five pixels, a short enough distance that he ends up getting pushed out in the same direction he came from.

Play around with fire however, and eventually you'll get burned. When the player rides on one of Marble Garden Zone's spinning tops, it executes this interesting snippet of code:
loc_35048:
    move.b  $44(a1),d0
    addi.b  #$18,d0
    move.b  d0,$1E(a1)
    ...
Offsets $44 and $45 of the player's SST hold the default values of their y_radius and x_radius attributes. They are set once when the player object is first initialized and henceforth only read from, to restore the player's hitbox after rolling.

The spinning top object isn't messing with these values, which is good. But it is taking the default Y radius, adding 24 to it, and writing the new value to the player's y_radius attribute, which effectively changes the size of Sonic's hitbox from the usual 20×40 pixels to 20×88 pixels. This way lies madness.


Why does the object do this? To trick the solid object code into thinking the bottom of Sonic's hitbox is all the way at the bottom of the spinning top. You see, the game is written to process two kinds of solid collisions: object-to-level collision, and player-to-object collision. It is not equipped to process solidity between any two arbitrary objects.

However, by hijacking the player's own hitbox, the spinning top object can simulate floor collision between itself and the tops of other objects, which is all it really needs, without having to mess around with any of the underlying code.

Except the underlying code just so happens to contain this fragment (relevant bits in bold):
Sonic_TouchFloor:
    move.b  $1E(a0),d0
    move.b  $44(a0),$1E(a0)
    move.b  $45(a0),$1F(a0)
    btst    #2,$2A(a0)
    beq.s   loc_121D8
    bclr    #2,$2A(a0)
    move.b  #0,$20(a0)
    sub.b   $44(a0),d0
    ext.w   d0
    tst.b   (Reverse_gravity_flag).w
    beq.s   loc_121C4
    neg.w   d0

loc_121C4:
    move.w  d0,-(sp)
    move.b  $26(a0),d0
    addi.b  #$40,d0
    bpl.s   loc_121D2
    neg.w   (sp)

loc_121D2:
    move.w  (sp)+,d0
    add.w   d0,$14(a0)

loc_121D8:
    ...
When bit 2 of the player's status bitfield (aka their rolling flag) is set, the above code must shift their Y position up a few pixels in order to account for the change in their hitbox size. It calculates the number of pixels by subtracting the default Y radius from the current collision height, which is usually 30 pixels while rolling.

The current collision height is 88 pixels.


Here's the rundown: if Sonic starts riding the spinning top while rolling, then landing on a solid object will make the floor collision code attempt to shift him up by five pixels. However, since his current Y radius is an insane value, this ends up shifting him down by 24 pixels, which is enough to clear the sinking mud object and send him right through the floor.


It doesn't stop there. Because Sonic never dismounted the spinning top properly, his hitbox is still abnormally tall, which causes him to touch the floor much sooner than he normally would. Fortunately, the floor collision code restores Sonic's hitbox size whenever he lands, so there's no long-term damage.

Wednesday, December 27, 2017

Roll height bugs, part 1

The size of a player's hitbox is determined by the values at offsets $1E and $1F of their SST, respectively known as the y_radius and x_radius attributes in the current disassembly:
y_radius =              $1E ; byte ; collision height / 2
x_radius =              $1F ; byte ; collision width / 2
As their labels imply, these attributes denote the player's radius in pixels across the vertical and horizontal axes, and as such, their values must first be doubled in order to obtain the actual collision size. Furthermore, since a radius of 0 isn't particularly helpful, the values stored in these attributes are actually off by a single pixel, so therefore we must also add 1 to each value before doubling it.

For instance, the Sonic object is initialized with a y_radius of $13 and an x_radius of 9. These values are off by one, so the true radii are 10 for the horizontal radius and $14, or 20 for the vertical one. Doubling these values gives us Sonic's final collision size of 20×40 pixels.


When Sonic curls into a ball, his hitbox gets reduced to the adequately smaller size of 16×30 pixels. However, since the precise location of the hitbox's edges is determined from Sonic's X and Y coordinates, which in turn define the center of the object, the game must also make sure to move Sonic down by five pixels in order to keep him grounded:


As one might expect, this easily lends itself to lots of tiny oversights. For example, when a player jumps directly out of a roll, the larger hitbox is incorrectly applied, causing them to hit the ceiling sooner than intended.

In Sonic 2, this also caused the player to suddenly jolt up after landing, thanks to the game subtracting five pixels from their Y position in order to prevent their hitbox from digging into the floor when it expands back to its normal size (which has no effect due to the previous bug).


These changes to the player's position also take their toll on the camera. Because Sonic suddenly moves down by five pixels when he curls into a ball, the camera counteracts this by aiming itself five pixels above his actual position so as to prevent the entire screen from jerking down awkwardly. However, this code does not account for Tails' shorter hitbox, and so whenever Tails curls into a ball, the screen awkwardly jerks up instead:


Over the next couple of posts, I'll talk about some of the more interesting height-related bugs. Not all of them; there are far too many to count.

Tuesday, December 26, 2017

You had it in you all along

In Launch Base Zone 1, you may have occasionally experienced a bug where you ride one of the tube elevator capsule thingies, and when you arrive at the other end, the door is already shut and you're immediately spat out of the lift.


So what's going on? First, let's get the basics of the tube elevator down. Even when you take another route through the level, or run far away and then come back, you'll always find a closed elevator sitting at the back end of the tube.


This suggests that the closed elevators are a permanent fixture of the stage, a fact confirmed by looking at the contents of the level's object layout: at the end of every tube is a second elevator object with bit 6 of its subtype set. A look at the object's code confirms that this causes it to spawn directly in the closed state:
Obj_LBZTubeElevator:
    move.l  #Map_LBZTubeElevator,$C(a0)
    move.w  #$2455,$A(a0)
    move.b  #$18,7(a0)
    move.b  #$30,6(a0)
    ori.b   #4,4(a0)
    move.w  #$80,8(a0)
    move.w  $10(a0),$44(a0)
    move.w  $14(a0),$46(a0)
    btst    #6,$2C(a0)
    beq.s   loc_29CEC
    move.b  #0,$22(a0)                  ; If BIT 6 of subtype set, the elevator remains closed
    move.b  #0,$26(a0)
    move.l  #Obj_LBZTubeElevatorClosed,(a0)
    bra.s   Obj_LBZTubeElevatorClosed

loc_29CEC:
    ...
Now we have a problem, though. When we do take the elevator, we don't want that second capsule waiting at the other end of the tube. This is accounted for by the closed elevator's code, which starts off with a rather complicated check:
Obj_LBZTubeElevatorClosed:
    lea     (Player_1).w,a1
    btst    #3,$2A(a1)
    beq.s   loc_29D8E
    tst.b   $2E(a1)
    beq.s   loc_29D8E
    movea.w $42(a1),a2
    cmpi.l  #Obj_LBZTubeElevatorActive,(a2)
    bne.s   loc_29D8E
    move.w  #$7FF0,$10(a0)

loc_29D8E:
    ...
Alright, so three things are being determined here. First, the object retrieves player 1's SST and checks whether bit 3 of its status bitfield is set: this is player 1's standing on object flag. If so, it then checks whether the player's object_control bitfield has any of its bits set: this is a sign that the player's movement is being controlled by another object.

If both those checks pass, then the elevator pulls out the big guns. It retrieves the word value stored at offset $42 of the player's SST, which we saw before contains the RAM address of the last object the player stood on. Since the player is currently standing on an object, this is the RAM address of the object the player is standing on right now.

It's time for the final check: the elevator looks at the code pointer of the object the player is currently standing on. If that object happens to be another elevator that isn't closed, then the first elevator assumes the second one is about to drop off the player at the back end of the tube, so it moves itself all the way to the end of the level, where it'll be collected by the object deletion routine.

So, does it work? Most of the time, yes. The elevator object bobs up and down inside the tube, which usually lifts Sonic off the floor, setting up the appropriate values in his SST.


However, when the elevator is at its lowest point, it is actually far enough away that Sonic will not snap onto its surface, which results in the tube ride starting while the player is still standing on solid ground.


When this happens, the player's object_control bitfield is set, but their status bitfield isn't, making the above checks fail. This causes the second elevator to spawn normally, and it is this elevator which squeezes Sonic out of the tube. (If you look closely, you'll be able to see both elevators bobbing up and down out of sync.)

Monday, December 25, 2017

Christmas corrections

Today is Christmas Day, a holiday which is typically celebrated by showering the people you love the most with copious amounts of gifts. Keeping with tradition, then, I thought this would be the perfect opportunity to celebrate three gifts that my readers have graciously offered me through the comments section of this blog.

(Stuttering Craig voice) This is Sonic 3 Unlocked's 2017 Top 3 Christmas Corrections!



Number Three!

As part of my short series on Lock-on Technology, I pointed out a difference with Knuckles' climbing animation between Knuckles in Sonic 2 and Sonic & Knuckles: exclusively in the latter, whenever Knuckles stands still on a wall, he reverts back to the first frame of the climbing animation.


I chalked this up to a feature introduced in the S&K version of the Knuckles object, but later, an anonymous commenter performed their own analysis of the source code, which I present below. Turns out, it's not actually a feature, it's a bug:
I think the second behaviour quirk you mentioned in this post is the result of a bug. There's some code in S3K that isn't in KiS2, at loc_16E10 in the current S3K Git disasm. The equivalent label in KiS2's Git disasm is loc_315B04.

What I think this new code does is handle floor collision, because Knuckles still seems to move briefly after the player stops pressing the D-Pad. The issue is, this new code overwrites d1 with the distance Knuckles is from the floor. d1 is checked immediately afterwards, has Knuckles's frame ID added to it, and is then used to calculate which frame Knuckles should display.

d1 will always be a positive number, usually a large one depending on how far Knuckles is from the ground. This means, when Knuckles's frame ID is added to it, it goes well beyond the ceiling value of $BC, causing the game to reset it to $B7, making Knuckles display the first frame of his animation. Chances are the number could overflow, too, causing him to display his last frame instead.

Safe to say, editing the code to properly back up d1 causes it to behave like KiS2 instead.
Let's take a look at the code mentioned. Knuckles in Sonic 2 to the left, Sonic & Knuckles to the right. Changes in bold:
loc_315B04:                                     loc_16E10:
                                                    move.b  (Ctrl_1_logical).w,d0
                                                    andi.b  #3,d0
                                                    bne.s   loc_16E34
                                                    move.b  $46(a0),d5
                                                    move.w  $14(a0),d2
                                                    addi.w  #9,d2
                                                    move.w  $10(a0),d3
                                                    bsr.w   sub_F828
                                                    tst.w   d1
                                                    bmi.w   loc_16D6E

                                                loc_16E34:
    tst.w   d1                                      tst.w   d1
    beq.s   loc_315B30                              beq.s   loc_16E60
    subq.b  #1,$1F(a0)                              subq.b  #1,$25(a0)
    bpl.s   loc_315B30                              bpl.s   loc_16E60
    move.b  #3,$1F(a0)                              move.b  #3,$25(a0)
    add.b   $1A(a0),d1                              add.b   $22(a0),d1
    cmp.b   #$B7,d1                                 cmpi.b  #$B7,d1
    bcc.s   loc_315B22                              bhs.s   loc_16E52
    move.b  #$BC,d1                                 move.b  #$BC,d1

loc_315B22:                                     loc_16E52:
    cmp.b   #$BC,d1                                 cmpi.b  #$BC,d1
    bls.s   loc_315B2C                              bls.s   loc_16E5C
    move.b  #$B7,d1                                 move.b  #$B7,d1

loc_315B2C:                                     loc_16E5C:
    move.b  d1,$1A(a0)                              move.b  d1,$22(a0)
In Sonic 2, when loc_315B04 is reached, the d1 register is set to 1, -1, or 0 depending on whether Knuckles is moving up, moving down, or standing still. Assuming neither branch to loc_315B30 is taken, Knuckles' current mapping frame is added to the value in d1, and then two bound checks are made before writing the resulting value back into Knuckles' mapping frame: if the value is less than $B7, d1 is set to $BC, and if it's greater than $BC, d1 is set to $B7.

The gist of it is: while Knuckles is climbing up a wall, his mapping frame gets progressively incremented, but when he's climbing down, it gets decremented instead. And if the mapping frame ever steps outside of the $B7-$BC range, it gets wrapped around to the other end of the range, in order to loop the animation.

In Sonic & Knuckles though, a call to sub_F828 was introduced, causing the FindFloor function to be called whenever the player is holding neither up nor down on the directional pad. The FindFloor function calculates an object's distance to the floor directly below it, and stores the result in register... d1.

The inevitable result follows: when the player lets go of the directional pad, sub_F828 is called and the value in d1 gets overwritten with the distance between the center of the Knuckles object and the floor. Knuckles' current mapping frame is then added to this value, which always produces a value greater than $BC. This triggers the bounds check, resetting Knuckles' mapping frame back to $B7, the first frame of the climbing animation.

In other words, the anonymous commenter's analysis is 100% correct. Good work!



Number Two!

On the subject of triggering slope glitch in Ice Cap Zone by having Tails break an ice block while Sonic is standing on it, Brainulator9 asked whether Tails could get slope glitch by instead breaking the block as Sonic. In Sonic 3 & Knuckles, this is impossible because player 2's status bits always get set, regardless of who breaks the blocks, and regardless of whether the Tails object is even present in the player 2 slot.


However, as Brainulator9 pointed out, the same isn't true of standalone Sonic 3, in which Sonic can indeed break Tails' gravity. Below is the relevant code: Sonic 3 to the left, Sonic & Knuckles to the right, once again changes in bold.
loc_58B3C:                                      loc_8B384:
    move.b  ($FFFFB020).w,$3A(a0)                   move.b  ($FFFFB020).w,$3A(a0)
    move.b  ($FFFFB06A).w,$3B(a0)                   move.b  ($FFFFB06A).w,$3B(a0)
    moveq   #$23,d1                                 moveq   #$23,d1
    moveq   #$10,d2                                 moveq   #$10,d2
    moveq   #$10,d3                                 moveq   #$10,d3
    move.w  $10(a0),d4                              move.w  $10(a0),d4
    jsr     (SolidObjectFull).l                     jsr     (SolidObjectFull).l
    bsr.w   sub_58B62                               bsr.w   sub_8B3AA
    jmp     (Sprite_OnScreen_Test).l                jmp     (Sprite_OnScreen_Test).l

sub_58B62:                                      sub_8B3AA:
    move.b  $2A(a0),d0                              move.b  $2A(a0),d0
    btst    #3,d0                                   btst    #3,d0
    beq.s   loc_58B78                               beq.s   loc_8B3C0
    lea     (Player_1).w,a1                         lea     (Player_1).w,a1
    cmpi.b  #2,$3A(a0)                              cmpi.b  #2,$3A(a0)
    beq.s   loc_58B8A                               beq.s   loc_8B3D2

loc_58B78:                                      loc_8B3C0:
    btst    #4,d0                                   btst    #4,d0
    beq.s   locret_58BD0                            beq.s   locret_8B430
    lea     (Player_1).w,a2                         lea     (Player_2).w,a1
    cmpi.b  #2,$3B(a0)                              cmpi.b  #2,$3B(a0)
    bne.s   locret_58BD0                            bne.s   locret_8B430

loc_58B8A:                                      loc_8B3D2:
    bset    #2,$2A(a1)                              bset    #2,$2A(a1)
    move.b  #$E,$1E(a1)                             move.b  #$E,$1E(a1)
    move.b  #7,$1F(a1)                              move.b  #7,$1F(a1)
    move.b  #2,$20(a1)                              move.b  #2,$20(a1)
    move.w  #-$300,$1A(a1)                          move.w  #-$300,$1A(a1)
    bset    #1,$2A(a1)                              bset    #1,$2A(a1)
    bclr    #3,$2A(a1)                              bclr    #3,$2A(a1)
    move.b  #2,5(a1)                                move.b  #2,5(a1)
                                                    btst    #4,$2A(a0)
                                                    beq.s   loc_8B41A
                                                    lea     (Player_2).w,a1
                                                    bset    #1,$2A(a1)
                                                    bclr    #3,$2A(a1)

                                                loc_8B41A:
    lea     ChildObjDat_58C20(pc),a2                lea     ChildObjDat_8B480(pc),a2
    jsr     CreateChild1_Normal(pc)                 jsr     CreateChild1_Normal(pc)
    moveq   #$6E,d0                                 moveq   #$6E,d0
    jsr     (Play_Sound_2).l                        jsr     (Play_Sound_2).l
    jsr     (Go_Delete_Sprite).l                    jsr     (Go_Delete_Sprite).l

locret_58BD0:                                   locret_8B430:
    rts                                             rts
Both versions of the code call the SolidObjectFull function, and then check bits 3 and 4 of the status bitfield along with the animation of the corresponding player, which is previously backed up to offsets $3A and $3B, in order to determine whether the player landed on the object whilst in their rolling animation.

Note how thoroughly botched the checks for player 2 are in Sonic 3, though: player 1's RAM address is loaded instead of player 2's, and it gets loaded to register a2 rather than register a1. The only reason this code works at all is because the SolidObjectFull function itself sets a1 to player 2's RAM address during the course of its execution, and then exits without overwriting the contents of the register with something else:
SolidObjectFull:
    lea     (Player_1).w,a1
    moveq   #3,d6
    movem.l d1-d4,-(sp)
    bsr.s   sub_1BA2A
    movem.l (sp)+,d1-d4
    lea     (Player_2).w,a1
    tst.b   4(a1)
    bpl.w   locret_1BA6A
    addq.b  #1,d6

sub_1BA2A:
    ...
That isn't the problem in and of itself, however: the problem is that the code at loc_58B8A only runs for a single player, which leaves the other player hanging if they happened to also be standing on the ice block at the time. Rather than fix this properly, Sonic 3 & Knuckles simply forces player 2 to fall off the block either way, resulting in the strange, lopsided behavior where player 1 can get slope glitch but not player 2.



Number One!

Finally, regarding the Japanese characters in the slot machine bonus stage, another anonymous commenter points out that if you read them vertically, top to bottom, then left to right, they make up the first sixteen letters of the Iroha.


Now, what is the Iroha? It is an ancient Japanese poem, which has the unique characteristic of using every single kana character exactly once. (The title refers to the first three characters used in the poem.)

イロハニホヘト iro ha nihoheto
チリヌルヲ   chirinuru wo
ワカヨタレソ  wa ka yo tare so
ツネナラム   tsune naramu
ウヰノオクヤマ uwi no okuyama
ケフコエテ   kefu koete
アサキユメミシ asaki yume mishi
ヱヒモセス   wehi mo sesu

Since each character only appears once, the Iroha serves as an alternative to the usual gojūon ordering, but both work equally well as placeholder graphics for a level's animated PLCs.



That's all I've got. Thank you all so much for the valuable feedback; I hope every single one of you has a terrific holiday season, and don't forget:

Friday, December 22, 2017

That's cheating, but I'll let it slide

Reader Silver Sonic 1992 asks:
A bit unrelated, but is there a reason there is a duplicate of the Sonic's falling animation?
First, a little background. All three characters have three distinct animations that make use of their falling sprites. These animations take up IDs $19 through $1B. Here are the definitions for Sonic's animations:
byte_12C0A:     dc.b    9, $D7, $D8, $FF

byte_12C0E:     dc.b  $40, $8D, $FF

byte_12C11:     dc.b    9, $8C, $8D, $FF
The second one, $1A, is the animation which plays when falling back from taking damage, and consists of only a single frame. The third one, $1B, is used at the start of a few levels, and alternates between the damage frame and a second one to show Sonic flailing his arms as he falls into the stage.

Now the third animation, $19, is used while sliding on stuff like the icy slopes in Icecap Zone 1, and the sand streams in Sandopolis Zone. As it turns out, if the regular falling animation is used for these obstacles, Sonic's sprite actually floats a fair bit off the floor:


The developers' solution was to duplicate the mapping frames used by the falling animation, shift them diagonally down towards the slope, make a copy of the falling anim that points to those frames, and have slides use that anim instead:


Not all slides, however. If the water slides in Hydrocity Zone 2 used the proper sliding anim, you wouldn't be able to see much of your character due to the high walls of the waterway:


In this instance, the developers cheated and made the water slide call the falling anim instead, which means that Sonic is technically hovering in mid-air again.


And that's why there's a duplicate of Sonic's falling animation. Unless you were talking about mapping frames $D0 and $D1. I think those are just unused.

Thursday, December 21, 2017

The underside of a hero

Not even the Special Stage sprites are exempt from historical oversights. Aside from a really bad misalignment with the standing frame, each sprite features a highly-detailed head that uses a bunch of different colors, whereas the torso and the legs seem much rougher in comparison, using just two shades of blue, at times resembling basic 3D polygons.


It would appear that once again, some of Sonic's sprites were left half-unfinished. But that isn't the only oversight these sprites share with the monkey bar animation.


In both Sonic 1 and Sonic 2, the soles of Sonic and Tails' shoes were colored the same as the rest of the shoe: red. For Sonic 3, the developers changed it so all three characters have black soles, but this redesign was never reflected in the Special Stage sprites for any of them.


Given their close tie to Sonic 2, it should be no surprise that Sonic's monkey bar sprites also feature red soles. They're not alone, though: red soles are also present in some rotation sprites used in Hydrocity Zone, Mushroom Hill Zone and Lava Reef Zone, as well as the horizontal "corkscrew" animation, which is lifted directly from Sonic 2.

EDIT: This was pointed out by commenter greenknight9000, which I somehow missed the first time around. My bad!

Wednesday, December 20, 2017

The face of a hero, part 3

In mapping frames $80-$85, all three characters have a set of unique sprites that's only used while swinging across the monkey bars in Flying Battery Zone. Despite clearly sharing the same base as the standing rotation sprites however, at least Sonic's sprites appear to be noticeably less detailed, looking almost unfinished at times:


These sprites are included in standalone Sonic 3, which isn't too surprising when you consider how Flying Battery Zone was originally meant to appear earlier in the game. What's surprising is how they're identical to the sprites later found in Sonic & Knuckles. It seems like a rough version of the animation was first cobbled together, but then the stage itself got canned before the sprites were polished up, and eventually they just forgot about the whole thing.

Particularly interesting is the first frame, which doesn't share much of a likeness with the rest of the Sonic 3 sprites, but is more or less a perfect match for the standing sprite from Sonic 2.


This is probably holdover from the days where Sonic was still using adapted versions of his Sonic 2 sprites, suggesting that at one point they weren't just placeholders, but actually part of the game's design.

Tuesday, December 19, 2017

The face of a hero, part 2

So how come frame $56 looks different from the regular standing sprite? Well, one possibility is that the differences are unintentional: at some point the developers went back and touched up the standing sprite, and frame $56 didn't receive those tweaks by simple virtue of being a separate frame.

Another possibility is that the differences are intentional, arguably to serve as a better inbetween frame for the standing rotation anim. There's a hole in that theory though, which is that the same cross-eyed gaze can be found in a couple of other sprites, such as the hanging animation:


Now, you could argue that the second theory still applies: the developers may have simply based the hanging sprite on frame $56 instead of the correct standing frame. The first theory seems more plausible though, if only for the fact that it has happened on multiple other occasions.

However, as an anonymous commenter pointed out:
Anyhow, what I find interesting about that presumably-early standing sprite is that the shine on Sonic's shoe matches his ducking sprites, where it spontaneously moves.

To me, that is the most convincing argument.

Monday, December 18, 2017

The face of a hero, part 1

Sonic's sprites in Sonic 3 have always been a divisive subject. Some people like them, but most people hate them, and they will go out of their way to make fun of his "baseball mits" and "clown shoes" without prior provocation.

With the release of Sonic Mania, these lovely individuals have seemingly been vindicated, as Sonic's sprites in that title are based off the ones in Sonic 1 and 2. This important design decision then led to fan-made comparisons such as this, to which the True Elite immediately respond by pointing out how cross-eyed the Sonic 3 sprites look.


Except that if you bother playing the game, you realize this is what Sonic's standing sprite actually looks like:


So where did the other sprite come from? We've actually seen it before on this very blog. It's one of the rotation sprites used when standing on rotating objects, such as the barrels in Carnival Night Zone.


This sprite just happens to be slightly different from the regular standing sprite, namely with respect to Sonic's left shoe, his mouth, and his gaze, which does indeed look a bit cross-eyed.

Okay, but how did this relatively obscure sprite end up in the comparison?

Well, as I pointed out back then, the standing rotation sprites take up mapping frames $55 through $5B. Meanwhile, as we recently saw, the actual standing pose is all the way over at mapping frame $BA. Someone comes along to make a rudimentary sprite rip, finds frame $56 long before reaching frame $BA, so they push it to the top of their sprite sheet.

Much later, someone naïvely takes the first sprite in this sheet, blows the pixels up to the size of Legos, and uses them to win an argument on the Internet.

So how come the other characters don't suffer from this kind of mix-up?

Well, as for Tails, frame $56 is significantly different from his standing sprite, which is nearly unchanged from the one in Sonic 2. This makes it unlikely that anyone would confuse the two.


As for Knuckles, he actually uses frame $56 in his main standing animation, in no short part due to his sheer number of unique sprites, which struggle to fit within the 252 mapping frame limit.

Friday, December 15, 2017

The rollback

Sonic isn't the only one with two rolling animations; Tails and Knuckles have them as well. Tails' animations in particular consist of just three frames each, repeating at a constant rate of 30 frames per second regardless of the current ground velocity value.
AniTails02:     dc.b    1, $96, $97, $98, $FF

AniTails03:     dc.b    0, $96, $97, $98, $FF
Now, although judging from the animation scripts, the mapping frames appear to be in the correct order, they're actually in reverse order in the mappings themselves. As a result, Tails' fur rotates backwards whenever he curls into a ball.


The absurdity of this oversight, together with the fact that it doesn't occur in Sonic 2, makes it a strong contender for my coveted Top 5 Stupidest Bugs in Sonic 3 list. Maybe someday I'll post about the other entrants in this here blog thing.

Plenty more oversights exist in the player sprites, so I guess I'll talk about them next week. Have a great day, everyone!

Thursday, December 14, 2017

Over the threshold

Not far into Sonic's animation routines, we come across the following snippet:
loc_1270A:
    tst.b   (Super_Sonic_Knux_flag).w
    bne.s   loc_12766
    lea     (byte_12AF8).l,a1
    cmpi.w  #$600,d2
    bhs.s   loc_12724
    lea     (byte_12AEE).l,a1
They're a little obfuscated due to a lack of human-readable labels, but byte_12AEE and byte_12AF8 are the scripts for Sonic's walking and running animations, which can be found in the Anim - Sonic.asm file in the current disassembly:
byte_12AEE:     dc.b  $FF,   7,   8,   1,   2,   3,   4,   5,   6, $FF

byte_12AF8:     dc.b  $FF, $21, $22, $23, $24, $FF, $FF, $FF, $FF, $FF
As we can tell from the above code, rather than use the current animation value at byte $20 of its SST, the Sonic object switches between walking and running by loading the appropriate animation script, based on the value of register d2. It does so without resetting its own animation frame value, so the shorter running animation is padded to the length of the walking animation to ensure the value is kept within the bounds of either animation.

Meanwhile, register d2 contains Sonic's current ground velocity, which serves as a threshold for when the running anim should kick in. Ground velocity is measured in increments of 1/256 pixels per frame, and so the above code causes the running anim to kick in when Sonic is traveling at a speed higher or equal to 6 pixels per frame.

Every time the current animation frame changes, ground velocity is also used to inform the initial value of the animation frame timer; the first byte in either animation script is for all purposes unused.

When the Super_Sonic_Knux_flag is set however, the code jumps to loc_12766, which looks like this:
loc_12766:
    lea     (byte_12C84).l,a1
    cmpi.w  #$800,d2
    bhs.s   loc_1277E
    lea     (byte_12C7A).l,a1
Not only does the code now point at Super Sonic's walking and running animations, which have separate definitions for some reason, but the speed threshold is raised from 6 to 8 pixels per frame. The reason for this is obvious: the running animation is meant to kick in when Sonic reaches his maximum speed (on flat ground, anyway), and since Super Sonic has a higher maximum speed, the threshold is moved forward to compensate.

This detail is so fundamental, it pains me that neither the mobile remakes nor Sonic Mania implement it, causing Super Sonic to break into his flying pose much sooner than intended.


Anyway, if we keep scrolling down, we'll eventually find a third spot where a similar speed check is done:
loc_12A4C:
    lea     (byte_12B0C).l,a1
    cmpi.w  #$600,d2
    bhs.s   loc_12A5E
    lea     (byte_12B02).l,a1
So what are the byte_12B02 and byte_12B0C animation scripts? Turns out, they're byte-for-byte identical, and both of them define Sonic's rolling animation:
byte_12B02:     dc.b  $FE, $96, $97, $96, $98, $96, $99, $96, $9A, $FF

byte_12B0C:     dc.b  $FE, $96, $97, $96, $98, $96, $99, $96, $9A, $FF
Rather unintuitively, while Sonic is jumping or spinning, the above code causes his animation to switch between a "slow roll" anim and a "fast roll" anim, according to the same threshold as the running code. It just so happens that since both animations are identical, there is no way to actually observe this change.

Wednesday, December 13, 2017

The lost idle animation

Okay, the last post ended up way longer than I was anticipating, so I'll keep this one short. Sonic has one more unused animation, but the script itself is gone, so we can only guess the intended order and duration of each frame.

So what's the animation? It's an alternate ending to the idle animation in which Sonic falls asleep while tapping his foot.


Curiously, the sprites for this alternate sequence start at mapping frame $BF, directly following the regular standing and foot-tapping sprites at $BA-$BE. Meanwhile, the sequence shown in-game, in which Sonic urges the player to continue moving, uses mapping frames $AD-$AF, with the last sprite all on its own at frame $D9.

This suggests the sleeping sprites were added first, and the final sprites were later squeezed into any available slots. It would explain why no animation scripts use the sleeping sprites: they were likely part of the idle animation at one point, and then got thrown out when that animation was retooled.

Tuesday, December 12, 2017

The lost helper item

While on the subject of weird animations, I figured I should talk about this. Apart from the character-specific animations, Sonic has an unused animation in slot $A, which has the following definition:
byte_12BA8:     dc.b    9, $BA, $C5, $C6, $C6, $C6, $C6, $C6, $C6, $C7, $C7, $C7, $C7, $C7, $C7,
                dc.b  $C7, $C7, $C7, $C7, $C7, $C7, $FD,   0
What's interesting is that, beyond the regular standing sprite at mapping frame $BA, the animation is comprised entirely of otherwise unused sprites! Sonic is really the only character with unused graphics like that. This is what the animation looks like, which shouldn't surprise most of you lot:


Here we can see Sonic whistling. Calling someone, perhaps? Anyway, back when these sprites were first discovered, a few observant individuals made a connection between them and a certain debug element in standalone Sonic 3.

In Sonic 3, when using an S monitor, a whistling sound will be heard instead of the regular Super transformation sound:


What is the significance of this? Well, back in February 1994, German magazine Sega Magazin published their hands-on impressions of the game then known as Sonic 3 Part One. Specifically, in page 97 they describe the new power-ups in the game, providing descriptions of the three elemental barriers as well as a fourth item not found in the final game:

Water shield: Sonic can now breathe under water, and he also can use the Squat-Attack.
Fire shield: Is very effective against fire attacks. Sonic can use the Flying Attack with it.
Lightning-Shield: Sonic can electrocute near enemies.
Help!-Item: Tails helps him in dangerous situations.
Translation: Oerg866 (emphasis added)
And so a theory was born: originally, the S monitor wasn't a debug-only item that granted Sonic his super form, it was a helper item that triggered the whistling animation, which would summon a flying Tails to carry Sonic out of a jam. That's why the S monitor plays the whistling sound in Sonic 3: it's a leftover from the helper item the developers forgot to fix.

This is a reasonable hypothesis. It would certainly explain how precious VRAM got wasted on a monitor icon which can never be seen outside of debug mode.

I think that last point might be a red herring, though. The whistling sound also shows up in Sonic & Knuckles, where it's used in The Doomsday Zone. It is equally likely that the developers originally repurposed the whistling sound for Super transformations, but later changed their minds and forgot to update the S monitor. At least until Sonic & Knuckles.

Rather, looking at the order of the sounds in the Sound Test tells a different story:

IDDescription
$3EFlame Barrier item box
$3FAqua Barrier item box
$40(unused)
$41Thunder Barrier item box
$42Double Spin Attack
$43Flame Barrier Attack
$44Aqua Barrier Attack
$45Thunder Barrier Attack
$46Whistle

Note how the whistling sound appears directly after the sounds used for Sonic's elemental barrier attacks, which in turn are ordered the same as their respective item box sounds. Except for the unused sound effect in slot $40, that is, which actually sounds considerably more "electric" than the strange "ding" used by the Thunder Barrier item box.

My guess is that sound effect $40 was originally meant for the Thunder Barrier item box, whereas sound effect $41 was meant for the helper item's item box. The whistling sound (and matching animation) would only play later, once the item was actually used. Whether that was automated like the Super monitor, or something that could be stored for later, we'll probably never know.

I should point that Sonic Generations uses sound $40 for the Thunder Barrier item box, but whether that was by design or due to lack of attention to the source material is up for grabs.

Monday, December 11, 2017

Sonic Eraser

Following up on the topic of Hydrocity Zone's water jets, there's something that's always bugged me. Just before you're flung up in the air by a vertical jet, it sends you through one of these: a translucent white tube that crosses over another path through the stage.


These things are level chunks, and they're usually flanked by collision change objects that set Sonic's priority flag. This is because the level blocks themselves have their priority flag set too, presumably so they overlap Sonic as he rides up the tube. Using debug mode to avoid the collision change object shows how this would look.


However, for some unfathomable reason, once the vertical water jet object spawns, it sets both Sonic and Tails' current animation to $1C:
    move.b  #$1C,($FFFFB020).w
    move.b  #$1C,($FFFFB06A).w
    move.l  #loc_30338,(a0)
So what does animation $1C do? It simply displays mapping frame 0 for 120 frames, or two seconds. Mapping frame 0 is a blank frame which is usually reserved for when the player is blinking from having taken damage.
byte_12C15:     dc.b  $77,   0, $FF
In other words, right before Sonic goes through the tube, the water jet makes him invisible for two seconds, completely destroying the point of making the tube translucent and giving it high priority.


I have no explanation for this; the event appears to play out fine if you remove the offending lines of code. Maybe there was a bug here that was hastily covered up, before being fixed properly?

Friday, December 8, 2017

Hydrocity Zone intro area: the water jet

Finally, we arrive at the fourth and final element of the Hydrocity Zone intro sequence: the large jet of water that propels you out of the flooded tunnel.


The water jet is an object, and it's actually placed directly within the level's object layout. There are two subtypes of the jet object, one vertical and the other horizontal, and both can be manually placed using debug mode.


Now, it doesn't look like much until you place it because it loads its own art when it spawns, overwriting the graphics for the Turbo Spiker enemy. And although the object deletes itself and reloads the enemy art the instant it goes off-screen, there's still a known edge case in which Super Sonic can accelerate fast enough to bring a couple of Turbo Spikers into view before their art has finished decompressing.


As you play around with debug mode, you may notice that sometimes, you can't seem to place any horizontal jets. This is because the init code for the horizontal jet contains a clause which deletes the object if player 1's Y coordinate is less than $500; apparently a hack to prevent the jet from spawning when you take the top path out of the intro area.

However, this creates an oversight. If like before we take the long way around to the back of the tunnel, but avoid going near the object until we're below the $500 mark, then the water jet will shoot out for no reason as we climb up the wall.


Note how when we do this, the rotating palette remains in its slow setting. This is because the horizontal jet counts on it having been sped up by the tunnel background object, so its only responsibility it to slow the palette back down.

Which leads us to our final oversight. Possibly to avoid shooting out for no reason, when the horizontal jet deletes itself, it doesn't set its flag in the object respawn table. As a result, the jet will shoot out once and then never reappear, even if we take the long way around back to the intro area and use the tunnel exit again.


Now, this is hardly an oversight in and of itself, since it's pretty obvious that you were never meant to return to the intro area. The oversight is that since the water jet doesn't respawn, there's nobody around to slow the rotating palette back down, which means all the waterfalls are once again animating at a ridiculous speed.


The only way out of this mess is to trigger one of the vertical water jets, which respawn every single time.