Showing posts with label Bugs. Show all posts
Showing posts with label Bugs. Show all posts

Tuesday, October 1, 2019

One Steve Shield Limit

Reader Emi asked:
There's something that always bothered me in sonic 3 with super sonic
Why SS can't use the insta shield? Or why you can't use the insta shield with normal invicibility.
I'm not sure if that's a bug but it feels odd since the rest still can use their special moves as super but sonic can't
The reason for this is twofold.

First off, the insta-shield is implemented as regular power-up -- it is actually a separate object which spawns into the player 1 shield shot when playing as Sonic, and Sonic is also awarded one whenever he loses any other shield. This is because unlike other characters' special moves, the insta-shield must animate independently from Sonic, and it loads its art to the same VRAM slot reserved for shields and invincibility stars.

This is probably why Sonic can't use any shield moves while invincible. Since shields and invincibility share a single VRAM slot, they can't be displayed at the same time (thank goodness), and it would be pretty bad design to let players use shield moves without knowing ahead of time which move Sonic would actually perform. As such, all shield moves are disabled while Sonic's shield is hidden, and this includes Super Sonic.

Of course, you could reasonably argue that since Sonic's shield is hidden, the insta-shield should just override all other shield moves for the duration of the Super form. After all, it's what Hyper Sonic's jump dash does. That would probably work, if not for another VRAM conflict: similar to the insta-shield, the large sparks that trail behind Super characters at high speeds load their art to the same VRAM slot as shields and invincibility stars.


Actually, this is probably why they fixed that Sonic 2 bug where invincibility and Super would stack! It wasn't a problem in Sonic 2 because shields and invincibility used separate VRAM slots, since players could potentially have both at the same time in a 2P game. Anyway, it was probably too much trouble to mediate VRAM usage between the insta-shield and the sparks, so Super Sonic specifically has no jump moves. That much is intentional:
    bclr    #Status_RollJump,status(a0)             bclr    #Status_RollJump,status(a0)
    tst.b   (Super_Sonic_Knux_flag).w               tst.b   (Super_Sonic_Knux_flag).w
    beq.s   Sonic_FireShield                        beq.s   Sonic_FireShield
                                                    bmi.w   Sonic_HyperDash
    move.b  #1,double_jump_flag(a0)                 move.b  #1,double_jump_flag(a0)
    rts                                             rts
; --------------------------------------------- ; ---------------------------------------------

Sonic_FireShield:                               Sonic_FireShield:
    btst    #Status_Invincible,$2B(a0)              btst    #Status_Invincible,$2B(a0)
    bne.w   locret_12A20                            bne.w   locret_11A14
    btst    #Status_FireShield,$2B(a0)              btst    #Status_FireShield,$2B(a0)
    beq.s   Sonic_LightningShield                   beq.s   Sonic_LightningShield
Now, what probably wasn't intentional is how Super Sonic can trigger the bubble shield's bounce effect upon touching the ground in S3A!


Note how in the above bit of code, Sonic's double_jump_flag is still set upon a successful double jump, even when the Super_Sonic_Knux_flag is on. If Sonic happens to have a bubble shield, that will cause the Player_TouchFloor routine to trigger a bubble bounce, despite the initial downward plunge bit never actually taking place.

S&K adds a check for Super_Sonic_Knux_flag to this code, as well as a clumsy character ID check since characters other than Sonic sometimes run through this code too I guess:
    tst.b   double_jump_flag(a0)                    tst.b   double_jump_flag(a0)
    beq.s   locret_130BC                            beq.s   locret_12230
                                                    tst.b   character_id(a0)
                                                    bne.s   loc_1222A
                                                    tst.b   (Super_Sonic_Knux_flag).w
                                                    bne.s   loc_1222A
    btst    #Status_BublShield,$2B(a0)              btst    #Status_BublShield,$2B(a0)
    beq.s   loc_130B6                               beq.s   loc_1222A
    bsr.s   BubbleShield_Bounce                     bsr.s   BubbleShield_Bounce

loc_130B6:                                      loc_1222A:
    move.b  #0,double_jump_flag(a0)                 move.b  #0,double_jump_flag(a0)
What baffles me is, why include that code path at all? If you just remove the Super_Sonic_Knux_flag check, then the invincibility check in Sonic_FireShield will also catch Super Sonic and bail without setting the double_jump_flag. Yes, the game will continue checking the A, B and C buttons once every frame until Sonic lands back on the floor, but guess what? The game already does that for invincibility!

Okay, so the insta-shield is just a regular old object in the shield slot. Does that mean that if we award it to characters other than Sonic, they too can benefit from its unique properties? Not really.


You see, all the interesting stuff is done by the Sonic object itself. So long as his double_jump_flag is set to 1, Sonic's attack radius is increased beyond its normal range. The insta-shield simply plays its animation whenever the flag jumps from 0 to 1, and then at the end of the animation, it increases the flag to 2, returning Sonic's attack radius to normal.

Incidentally, this is the source of another S3A bug: the double_jump_flag is only cleared when Sonic touches the floor, so if the insta-shield's animation ends after Sonic has already landed, the flag will remain stuck at 2 until Sonic touches the floor again, and any double jumps prior to that will be considered to have been "already spent", as it were.

S&K fixes this by checking whether the double_jump_flag has already been cleared before attempting to set it to 2.


In my upcoming ROM hack, which I *swear* isn't cancelled, you can have characters other than Tails as your support character. Shields can still be awarded to player 1, though, so what do we do about the insta-shield art conflict?

Well, the spindash dust object is already working overtime as skid dust, water splashes and the drowning timer, so now it also serves as an insta-shield for player 2. Yeah, it disappears when player 2 jumps in/out of water and fails to show up at all when they're drowning, but as reader Emi notes, blocking the insta-shield altogether would just feel... odd.

Monday, January 29, 2018

Drowning during bosses

An anonymous reader asks:
I don't think you've talked about it yet so I'd like to ask a few things related to the Hydrocity Act 1 boss in S3A.
Sure, go ahead.
Why does it use the Act 2 boss BGM instead?
loc_47DBA:                                      loc_69F64:
    move.l  #Obj_HCZ_MinibossLoop,(a0)              move.l  #Obj_HCZ_MinibossLoop,(a0)
    moveq   #$19,d0                                 moveq   #$2E,d0
    jsr     (Play_Sound).l                          jsr     (Play_Sound).l
                                                    move.b  #$2E,($FFFFFF91).w

locret_47DC8:                                   locret_69F78:
    rts                                             rts
Because for some reason, in S3A the act 1 boss requests the act 2 boss music.
Why, if you let the air countdown start and then get out of the water, does the S&K Act 1 boss BGM start to play? It's seems to be the only time where track 18 (I think it's 18) is used. (Apparently this glitch also happens in Act 2 but I haven't tried)
Because, to put it mildly, the code that's responsible for resuming a level's music after having previously switched to the drowning theme is a brittle piece of crap:
Player_ResetAirTimer:
    cmpi.b  #$C,$2C(a1)
    bhi.s   loc_1744C
    cmpa.w  #Player_1,a1
    bne.s   loc_1744C
    move.w  (Level_music).w,d0
    btst    #1,$2B(a1)
    beq.s   loc_17430
    move.w  #$2C,d0

loc_17430:
    tst.b   (Super_Sonic_Knux_flag).w
    beq.w   loc_1743C
    move.w  #$2C,d0

loc_1743C:
    tst.b   (Boss_flag).w
    beq.s   loc_17446
    move.w  #$18,d0

loc_17446:
    jsr     (Play_Sound).l

loc_1744C:
    move.b  #$1E,$2C(a1)
    rts
Let's go over what this code does. First, the current level's music is loaded from the Level_music RAM variable, which as we saw before, is pulled from the master playlist, based on the current zone and act. This value is then stored in the d0 register, from which it will eventually be read by the Play_Sound function called at loc_17446.

Before it can reach loc_17446 though, the value must survive a gauntlet, which begins by checking bit 1 of the player's status_secondary bitfield, which is set when the player is invincible. If the player is currently invincible, the contents of the d0 register are replaced with the value $2C, which is the ID for the invincibility music track.

Next, if the Super_Sonic_Knux_flag is set, the value of the d0 register is once again replaced with $2C, the ID for the invincibility music track. Note that this check is redundant: the player is always invincible when in their Super form.

Finally, the Boss_flag is checked. If the player is currently fighting a boss, then the game should continue playing boss music, and therefore the d0 register is overwritten with the value $18, which as the commenter duly notes, is the theme used by act 1 bosses in Sonic & Knuckles. No effort is made to pick specific themes for each boss, despite the fact that there are only two bosses in the entire game where the player can trigger the countdown music.

This is annoying for several reasons. In Sonic 3, both bosses play the act 2 boss theme, so they could've just made the code set d0 to $19 and be done with it: at least it would always resume the right track! Meanwhile, as we can see from the code near the start of this post, Sonic & Knuckles actually saves the current boss track to RAM address $FF91, but nothing in the game ever makes use of this value.

Why complicate matters, though? Just check the current act and use that information to pick one song or the other.
It's possible to drown during the score tally but in S3&K this was apparently fixed. Why does it happen and how it was fixed?
The question presupposes a falsehood: the only way to drown during the level results is by running out of air just as the results object slides into view, which works equally well in both Sonic 3 and Sonic & Knuckles.


In practice, however, the level results object prevents this by restoring both player objects' air reserves (at offset $2C of their SSTs) right as the stage clear theme begins playing:
Obj_LevelResultsWait:
    tst.w   $2E(a0)
    beq.s   loc_2CF4C
    subq.w  #1,$2E(a0)
    cmpi.w  #$121,$2E(a0)
    bne.s   locret_2CF8E                        ; Play after eh, a second or so
    move.b  #$1E,($FFFFB02C).w                  ; Reset air for Hydrocity
    move.b  #$1E,($FFFFB076).w
    moveq   #$29,d0
    jmp     (Play_Sound).l                      ; Play level complete theme
The title card object then repeats the process, ensuring the player always begins the second act with a full set of lungs.
Thanks in advance.
Thank you for the questions!

Friday, January 26, 2018

Too many tails

Reader muteKi asks:
Fun little thing I've been at least a little curious about (and got to thinking about it because of the recent posts on Big Arm / LBZ's end sequence) -- for some reason there's a regression with Tails's tails not getting removed at the end of the stage when the Death Egg falls, specifically in Tails alone mode in S3K.
So, to clarify: at the end of Launch Base Zone 2, the player automatically turns to face the Death Egg, which is falling in the background. Exclusively in Sonic 3 & Knuckles though, Tails' namesake appendages aren't hidden as his animation changes from looking up to the standing rotation, granting him an extra set of tails for the duration of the sequence.


In order to understand what's going on, let's first take a brief look at how Tails' tails operate. Each frame, the tails object looks at Tails' animation to determine which animation it should play.

For instance, when Tails is in his looking up animation (7), his tails will quickly flick up and down. Meanwhile, when he's in his walking animation (0), the object blanks itself out, because Tails' walking cycle already has the tails baked in.
; animation master script table for the tails
; chooses which animation script to run depending on what Tails is doing
Obj_Tails_Tail_AniSelection:
    dc.b    0,0     ; TailsAni_Walk,Run     -> Blank
    dc.b    3       ; TailsAni_Roll         -> Directional
    dc.b    3       ; TailsAni_Roll2        -> Directional
    dc.b    9       ; TailsAni_Push         -> Pushing
    dc.b    1       ; TailsAni_Wait         -> Swish
    dc.b    0       ; TailsAni_Balance      -> Blank
    dc.b    2       ; TailsAni_LookUp       -> Flick
    dc.b    1       ; TailsAni_Duck         -> Swish
    dc.b    7       ; TailsAni_Spindash     -> Spindash
    ...
Alright, now let's look at the changes made to the cutscene object between Sonic 3 (left) and Sonic & Knuckles (right):
loc_5117A:                                      loc_72C3C:
    move.l  #locret_511CC,(a0)                      move.l  #loc_72C68,(a0)
    clr.b   ($FFFFFA88).w                           clr.b   ($FFFFFA88).w
    clr.w   $1C(a1)                                 jsr     (Stop_Object).l
    clr.w   $18(a1)
    clr.w   $1A(a1)
    bclr    #0,4(a1)                                bclr    #0,4(a1)
    bclr    #0,$2A(a1)                              bclr    #0,$2A(a1)
    move.w  #$101,(Ctrl_1_logical).w                move.w  #$101,(Ctrl_1_logical).w
    st      (Ctrl_1_locked).w
    jsr     Create_New_Sprite
    bne.s   loc_511C4
    move.l  #loc_5182E,(a1)
    lea     (Player_1).w,a2
    move.w  $10(a2),$10(a1)
    move.w  $14(a2),$14(a1)

loc_511C4:
    lea     ChildObjDat_52010(pc),a2                lea     ChildObjDat_73806(pc),a2
    jmp     CreateChild6_Simple(pc)                 jmp     (CreateChild6_Simple).l
Okay, a lot of structural differences right off the bat. First, S3A stops the player by clearing their three velocity values in-line; S&K instead opts to call the Stop_Object function, which does the same exact thing.

The other big structural change is that the S3A object actually spawns another object to run the next part of the code at loc_5182E, while setting its own code pointer to a stub location. The S&K object cuts the middle man by setting its own code pointer directly to the next bit of code, at loc_72C68.

Beyond those points, the only difference so far is that S3A locks the player's controls by setting the Ctrl_1_locked flag.
loc_5182E:                                      loc_72C68:
    btst    #0,($FFFFFA88).w                        btst    #0,($FFFFFA88).w
    beq.w   locret_50DD2                            beq.s   locret_72C9C
    move.l  #loc_5185A,(a0)                         move.l  #loc_72C9E,(a0)
    move.l  #loc_51868,$34(a0)                      move.l  #loc_72CBE,$34(a0)
    clr.b   (Ctrl_1_locked).w
    lea     (Player_1).w,a1                         lea     (Player_1).w,a1
                                                    bsr.w   sub_72C8E
                                                    lea     (Player_2).w,a1
                                                    clr.b   $20(a0)

                                                sub_72C8E:
    move.b  #$53,$2E(a1)                            move.b  #$83,$2E(a1)
    move.b  #0,$20(a1)                              clr.b   $24(a1)
                                                    clr.b   $23(a1)

                                                locret_72C9C:
                                                    rts
Here we go. Apart from S3A now clearing the flag it had just previously set, and different flags being set on the player's object_control bitfield, the code was altered to account for player 2, which now joins player 1 during the Beam Rocket fight in a Sonic and Tails game.

Note how S3A sets the player's current animation (at offset $20 of their SST) to 0, walking, just as the standing rotation frames kick in. This has the effect of blanking out Tails' tails, according to the rules in the Obj_Tails_Tail_AniSelection lookup table. Note further how S&K tries to do the same thing just to player 2, but ends up pointing at register a0 rather than a1, clearing offset $20 of the cutscene object, rather than Tails' current animation.

Okay, so that line of code is wrong, but fixing it wouldn't solve our problem: when playing as Tails alone, the Tails object is in the player 1 slot, not the player 2 slot, and the code very specifically avoids clearing player 1's animation for some reason. (Why? Who knows.)

The weird part is that when you play as Sonic and Tails, the tails object IS blanked out properly.


The reason for this lies in the code which runs through the standing rotation frames:
loc_5185A:                                      loc_72C9E:
                                                   lea     (Player_1).w,a1
    lea     byte_52070(pc),a1                      lea     byte_7386A(pc),a2
    bsr.w   Animate_ExternalPlayerSprite           jsr     (Animate_ExternalPlayerSprite).l
    jmp     (Player_Load_PLC).l                    lea     (Player_2).w,a1
                                                   clr.b   $20(a1)
                                                   lea     byte_73874(pc),a2
                                                   jmp     (Animate_ExternalPlayerSprite).l
Clearly the developers also had no clue why Tails' tails weren't going away, so they just made it so Tails' animation gets cleared every frame from then on. Now that's class.

The correct solution is to fix the clr.b $20(a0) line and move it inside the sub_72C8E function so it affects both players.

Wednesday, January 24, 2018

Have Tails activate star posts as quickly as possible.

Reader Brainulator9 asked:
Do you plan to discuss avoiding Tails getting stuck in passageways or getting stuck in traps by activating a Star Post or entering a Special Stage as quickly as possible?
Brainulator9 is talking about a passage that appears in page 27 of the US Sonic 3 manual, directly before the infamous "diabolical traps" line. Here's what it reads:
In the IceCap Zone and throughout the game, avoid having Tails get stuck in passageways or get caught in traps. Have Tails activate Starposts or enter into a Special Stage as quickly as possible.
What could this oddly specific phrasing be referring to? If the "diabolical traps" thing is any indication, this is likely a bug acknowledgment and its suggested workaround.

Well, there is indeed an S3A-exclusive bug regarding Tails in Icecap Zone, which occurs roughly 100% of the time. You see, Icecap Zone 1 is normally a vertically-wrapping level. However, when you first start the level as Tails, the level size is modified in order to have Tails fall from the top of the screen.


Evidently, the developers never bothered playing past this sequence, because at no point is the level size ever restored back to its regular state. As a result, when Tails reaches the point where the level should wrap upon itself, he's instantly killed by a mysterious, unseen force.


The workaround? Well, the level size is only supposed to change when Tails first enters the stage, so the relevant code only runs if the Last_star_post_hit variable is zero. The variable is set to a non-zero value when the player activates a star post or enters a special stage, so doing either will prevent the bug from reoccurring after the player respawns.

Thursday, January 18, 2018

Keep your arms inside at all times

One version difference that a lot of people latch onto is Launch Base Zone's final boss, Big Arm, and the behavior of its grabbing attack when the player is Super. In Sonic & Knuckles, this just bounces the player off the ground with a spring sound, but in Sonic 3, it causes Super Sonic to drop all of his rings and thus revert back to normal Sonic.


The reason for this is boring: in Sonic 3, regardless of whether the player is Super or not, the HurtCharacter function is called, which uh, hurts the character. Meanwhile, Sonic & Knuckles added a check for invincibility (not Super!) that calls up a separate code path if the appropriate bit is set. This suggests the original behavior was just an oversight.
loc_510AE:                          loc_74664:
    move.w  d0,$18(a0)                  move.w  d0,$18(a0)
    move.w  #-$600,$1A(a0)              move.w  #-$600,$1A(a0)
    move.w  #$3F,$2E(a0)                move.w  #$3F,$2E(a0)
                                        btst    #Status_Invincible,(Player_1+status_secondary).w
                                        bne.s   loc_7468C
    movea.l a0,a2                       movea.l a0,a2
    lea     (Player_1).w,a0             lea     (Player_1).w,a0
    jsr     (HurtCharacter).l           jsr     (HurtCharacter).l
    movea.l a2,a0                       movea.l a2,a0
    rts                                 rts
                                    
                                    loc_7468C:
                                        lea     (Player_1).w,a1
                                        clr.b   $2E(a1)
                                        neg.w   d0
                                        move.w  d0,$18(a1)
                                        move.w  #-$400,$1A(a1)
                                        bset    #1,$2A(a1)
                                        bclr    #3,$2A(a1)
                                        clr.b   $40(a1)
                                        clr.b   $3D(a1)
                                        move.b  #2,$20(a1)
                                        move.b  #2,5(a1)
                                        moveq   #-$4F,d0
                                        jmp     (Play_Sound_2).l
There's another, more interesting oversight with the Big Arm boss, which wasn't fixed in Sonic & Knuckles. It requires a second player, however in Sonic 3, the Tails object is deleted during the Egg Mobile ride, and in Sonic & Knuckles, only Knuckles fights the boss!

We can force the bug to occur in Sonic 3 by using the PAR code 05A8CC:6010, which prevents Tails from despawning at the end of Launch Base 2. Get the boss down to 1 HP, then let it grab you while having Tails deal the final hit.


When this happens, Sonic gets stuck in mid-air, at least until the ending sequence starts. The effects are more heinous in Sonic & Knuckles because a regular stage clear sequence was added to the level, which can't start because Sonic is not touching the floor.

What's interesting about this oversight is that both Sonic 3 and Sonic & Knuckles have code in place to prevent it. If the byte at offset $30 of Big Arm's SST is clear, then it calls the Restore_PlayerControl function, which clears the player's object_control flag and resets their animation:
loc_51D78:                                  loc_7506E:
    tst.b   $30(a0)                             tst.b   $30(a0)
    bne.s   loc_51D82                           bne.s   loc_7507A
    jsr     Restore_PlayerControl(pc)           jsr     (Restore_PlayerControl).l
                                            
loc_51D82:                                  loc_7507A:
    jmp     (SaveGame).l                        lea     ChildObjDat_75186(pc),a2
                                                jmp     (CreateChild1_Normal).l
Problem is, the byte at offset $30 is set when the boss is holding the player, which means the Restore_PlayerControl function is only called in the exact wrong situation: when the boss isn't holding the player.


This can be seen in both Sonic 3 and Sonic & Knuckles: upon dealing the final hit to the boss, the player's animation is reset, which results in them falling to the ground in their standing frame.

Wednesday, January 17, 2018

They just didn't care, part 2

Sonic 3 somehow managed to revive a bug from the prototype versions of Sonic 2: holding down a jump button while a level is loading causes the button press to be buffered until the player gains control of the character, making them jump as soon as the level starts.


As reader Brainulator9 points out, this works even when the player object spawns in mid-air, allowing you to do things you were never supposed to, such as turning into Super Sonic within a bonus stage.

Luckily, this was fixed in Sonic & Knuckles for all three player objects. However, there's one instance in which the game doesn't spawn the regular player objects. ...Yep, you know it.


Don't you just love Competition mode?

Tuesday, January 16, 2018

They just didn't care

There are three separate RAM flags which the game uses to track the state of debug mode: the debug cheat flag, the debug mode flag and debug placement mode.

  • The debug cheat flag is set when the player correctly inputs the debug cheat code, and is only cleared by a hard reset.
  • The debug mode flag is set when the player holds down the A button during level load, but only after the debug cheat flag has been set. It is cleared at the SEGA screen.
  • Finally, debug placement mode is set when the player presses the B button during a level, but only after the debug mode flag has been set. It is cleared when the player presses the B button again.

Sonic 3 and Sonic & Knuckles disagree on a few things regarding these flags. In Sonic 3 for instance, the debug cheat flag is set at the same time the level select is unlocked, but it has a separate cheat code in Sonic & Knuckles.

In particular though, just as with past Sonic games, Sonic 3 activates the debug HUD as soon as the debug mode flag is on, whereas Sonic & Knuckles waits until debug placement mode is also on, something which I've used extensively in the past to easily differentiate between S3A and S&K screenshots.


Of course, once the player exits object placement mode, the game must now restore the regular HUD. It can't just draw the score counter over the debug display, because any empty digits in the score counter would leave behind part of the debug display; it needs to wipe the whole thing clean first and draw the counters back in afterwards.

To that effect, the object placement exit code calls the HUD_DrawInitial function, which is used to initialize the HUD at level load. This is why the timer temporarily reverts to 0:00 until the next second rolls over.
    moveq   #0,d0
    move.w  d0,(Debug_placement_mode).w
    move    #$2700,sr
    jsr     (HUD_DrawInitial).l
    move.b  #1,(Update_HUD_score).w
    move.b  #-$80,(Update_HUD_ring_count).w
    move    #$2300,sr
    lea     (Player_1).w,a1
    move.l  ($FFFFFFCA).w,$C(a1)
    move.w  ($FFFFFFCE).w,$A(a1)
Unfortunately, nobody bothered to test this change in Competition mode. The regular HUD is never meant to be shown here, and so when HUD_DrawInitial is called, something goes wrong, and the player object is never properly restored, permanently removing them from the game.


This oversight definitely makes my Top 5 Stupidest Bugs in Sonic 3 list, since it makes debug mode totally useless in the Competition stages. Not only can you never interact with the items you place, but you can't even use debug mode's most basic functionality, free movement, because you can't place yourself back at the new position. Sheesh.

Monday, January 15, 2018

No fun allowed

In S3A only, if you defeat the Blastoid enemies that are placed directly over the special stage rings in Hydrocity Zone 1, you can actually fall through a hole in the floor directly into the ring's secret room.


For whatever reason, in Sonic 3 & Knuckles, these holes were hastily patched up with invisible collision objects, and as a result, Knuckles can't glide-land or climb up on this ledge properly.


Then again, in S3A you can defeat Blastoids by just walking into them, likely because they don't use the regular enemy collision type. So uh, pick your poison, I guess.

Friday, January 12, 2018

Can't escape!

Reader Silver Sonic 1992 asks:
Also, do you know what the "Special Stage 1" is for in the level select? Is it remnant of the super emerald special stages?
Doesn't look like it, but it's interesting nonetheless.

The special stage entries in the level select actually refer to an invalid zone ID, zone $40. When this zone is selected, a special handler prevents the invalid zone from getting loaded, and instead modifies the value of the Game_mode RAM variable, setting it to $2C for act 1, and $34 for act 2:
LevelSelect_PressStart:
    move.w  ($FFFFFF82).w,d0
    add.w   d0,d0
    move.w  LS_Level_Order(pc,d0.w),d0
    bmi.w   LevelSelect_Return
    cmpi.w  #$5555,d0                   ; This is used for the S&K zones
    beq.w   LevelSelect_Main
    cmpi.w  #$4001,d0                   ; Is Special Stage 2 selected?
    beq.w   LevelSelect_SpecialStage    ; If so, branch
    cmpi.w  #$4000,d0                   ; Is Special Stage 1 selected?
    bne.w   LevelSelect_StartZone       ; If not, branch
    move.b  #$2C,(Game_mode).w
    rts
; ---------------------------------------------------------------------------

LevelSelect_SpecialStage:
    move.b  #$34,(Game_mode).w
    rts
Once this variable is changed, the game instantly switches away from the current mode, $28 (level select) to whichever one got picked above. In Sonic & Knuckles, mode $2C refers to the title screen for the Blue Sphere bonus game, but in S3A, both it and $34 point at the special stage game mode.
GameModes:
    dc.l Sega_Screen                ;   0
    dc.l Title_Screen               ;   4
    dc.l Level                      ;   8
    dc.l Level                      ;  $C
    dc.l JumpToSegaScreen           ; $10
    dc.l ContinueScreen             ; $14
    dc.l JumpToSegaScreen           ; $18
    dc.l LevelSelect_S2Options      ; $1C
    dc.l S3Credits                  ; $20
    dc.l LevelSelect_S2Options      ; $24
    dc.l LevelSelect_S2Options      ; $28
    dc.l SpecialStage               ; $2C
    dc.l SpecialStage               ; $30
    dc.l SpecialStage               ; $34
    dc.l Competition_Menu           ; $38
    dc.l Competition_PlayerSelect   ; $3C
    dc.l Competition_LevelSelect    ; $40
    dc.l Competition_Results        ; $44
    dc.l SpecialStage_Results       ; $48
    dc.l SaveScreen                 ; $4C
    dc.l TimeAttack_Records         ; $50
At the end of the special stage mode's main loop, the Game_mode RAM variable is checked. If it's anything other than $34, then instead of jumping back to the start of the loop, execution progresses into a second loop, in which the screen progressively fades to white. The equivalent code can be found at loc_851A in the S&K disassembly.
    cmpi.b  #$34,(Game_mode).w
    beq.s   loc_77D2
    tst.w   (Demo_mode_flag).w
    beq.s   loc_7828
    move.b  #0,(Game_mode).w

loc_7828:
    move.w  #$3C,(Demo_timer).w
    move.w  #$3F,(Palette_fade_info).w
    clr.w   ($FFFFF794).w

loc_7838:
    move.b  #$1C,(V_int_routine).w
    bsr.w   Wait_VSync
    jsr     (Process_Sprites).l
    bsr.w   Animate_SSRings
    bsr.w   sub_8C1A
    jsr     (Render_Sprites).l
    jsr     Draw_SSSprites(pc)
    bsr.w   sub_8B9A
    bsr.w   sub_89E2
    bsr.w   Process_Nem_Queue_Init
    jsr     (Process_Kos_Module_Queue).l
    subq.w  #1,($FFFFF794).w
    bpl.s   loc_787C
    move.w  #2,($FFFFF794).w
    bsr.w   Pal_ToWhite

loc_787C:
    tst.w   (Demo_timer).w
    bne.s   loc_7838
Now, if the debug cheat is enabled, then pressing the A button while the game is paused makes the game jump directly to the level select. This is achieved by setting the Game_mode variable to $28, the value for the level select mode:
Pause_Loop:
    move.b  #$10,(V_int_routine).w
    bsr.w   Wait_VSync
    tst.b   (Slow_motion_flag).w
    beq.s   Pause_NoSlowMo
    btst    #6,(Ctrl_1_pressed).w
    beq.s   Pause_ChkFrameAdvance   ; branch if A isn't pressed
    move.b  #$28,(Game_mode).w
    nop
    bra.s   Pause_ResumeMusic
And that's what that check in the special stage mode was for. When the Game_mode variable changes from its regular value of $34, the game knows that it has to exit the special stage and go someplace else (for instance, the level select) so it begins fading to white, and once the fade is over, it jumps to the new game mode.

Except that, when we choose the "special stage 1" option, the Game_mode variable starts off with the value $2C. This calls up the special stage mode all the same, except the check fails immediately, triggering a fade out. Once the fade is over, the game jumps to the "new" game mode, $2C... which calls up the special stage mode all over again.

The cherry on top of the cake is this code at the end of that second loop:
loc_787C:
    tst.w   (Demo_timer).w
    bne.s   loc_7838
    addq.b  #1,($Current_special_stage).w
    cmpi.b  #7,(Current_special_stage).w
    bcs.s   locret_7894
    move.b  #0,(Current_special_stage).w

locret_7894:
    rts
This code ensures that the current stage value is properly incremented when the player exits the stage using the debug shortcut. That way, next time a special stage is accessed, the game will advance to the next stage in line, just like if the previous one had completed normally.

Inadvertently, this causes the "special stage 1" bug to cycle through each stage in an infinite loop. In Sonic & Knuckles, not only was the bug fixed, but the debug shortcut now leads to the SEGA screen, so the above code was removed.

Wednesday, January 3, 2018

His and hers act 2 capsules

One of S3A's little idiosyncrasies is that, unlike the ones in which it descends from the top of the screen, any levels that feature stationary act 2 end capsules have them placed directly into the object layout, instead of having the boss object spawn them at the end of the fight.

In S&K, this was changed so that stationary capsules are also spawned by the boss, which unfortunately led to this bug where the capsule in Hydrocity Zone 2 is not properly aligned with the screen lock:


Why the developers changed this is a mystery; perhaps they were trying to remove any possibility that the player might somehow miss the screen lock and proceed to the next stage without fighting the boss, as can be seen here.

Surprisingly, such a minor detail can actually give us insight on an unrelated aspect of the game's development. It turns out, the object layout for Flying Battery Zone 2 contains one such stationary capsule, at coordinates $3210, $660.
FBZ2_Sprites:   dc.w   $80,   $80,     0, $3130, $86D8, $B400, $3210, $8660, $C600
                dc.w $FFFF,     0,     0
The reason this is interesting is because the S3A placement of the capsule is a stone's throw away from its final resting place in Sonic & Knuckles. This suggests that by the time it was removed from S3A, the level layout for Flying Battery 2 was already quite similar to its final layout in Sonic & Knuckles.


Astute readers will notice the second object present in the layout placed slightly to the left of the capsule at coordinates $3130, $6D8. Foreshadowing: the sign of a quality blog.

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 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!

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.