Friday, September 29, 2017

HyperTouch_Special

Hyper Sonic and Hyper Knuckles both have special abilities which destroy all enemies currently on screen. From what we've learned so far about responsive object collision, it should be clear that at some point these abilities must process the collision response list, identify all objects with a collision type of 00, and run a handler to increase the current bonus chain, add the appropriate number of points to the player's score, and make the enemy explode. In fact, hyper abilities have their own version of the Touch_ChkValue routine, which looks like this:
HyperTouch_ChkValue:
    tst.b   render_flags(a1)        ; Is object on-screen?
    bpl.s   locret_10524            ; If not, return (screen-nuke only affects what's on-screen)
    andi.b  #$C0,d0                 ; Get collision_flags type data
    beq.s   HyperTouch_Enemy        ; If 00, enemy, branch
    cmpi.b  #$C0,d0
    beq.w   HyperTouch_Special      ; If 11, "special thing for starpole", branch
    tst.b   d0                              
    bmi.s   HyperTouch_Harmful      ; If 10, "harmful", branch

locret_10524:
    rts
Right off the bat, there's an additional check done on the object's render_flags attribute: when the object is off-screen, the high bit of this attribute is cleared, and the object is ignored. Objects with a collision type of 01 are also ignored, but surprisingly enough, all other types are still processed. To find out why, let's take a look at each collision handler.

It should be no surprise that HyperTouch_Enemy exists. After all, the goal of the hyper ability is to destroy all enemies currently on screen! The first major difference relative to Touch_Enemy is that it doesn't affect "special" enemies, such as bosses or enemies that take multiple hits, such as the Relief enemies in Marble Garden Zone. As for the other major difference, I'll just let the disassembly comments do the talking:
HyperTouch_Enemy:
    tst.b   collision_property(a1)          ; Is this a special enemy?
    beq.s   HyperTouch_DestroyEnemy         ; If not, branch
    rts
; ---------------------------------------------------------------------------

; Similar to other enemy destruction subroutines, but this one doesn't make the player bounce

HyperTouch_DestroyEnemy:
    ...
Next up we have HyperTouch_Harmful. All this does is look for enemy projectiles, and make them fly away similarly to how they bounce off elemental barriers:
HyperTouch_Harmful:
    move.b  shield_reaction(a1),d0
    andi.b  #8,d0                           ; Should the object be bounced away by a shield?
    bne.w   Touch_ChkHurt_Bounce_Projectile ; If so, branch
    rts

Finally, the namesake of this post, HyperTouch_Special. Why does it need to exist? It's time to learn the truth.

The truth is that not all enemies uses the enemy collision type. Touch_Enemy is quite strict about what happens to the enemy when it touches a player: it either damages the player without being notified that the collision even happened, or it is suddenly and unexpectedly turned into an explosion, preventing it from reacting to the collision in any way.

The enemies in Hydrocity Zone are particularly fond of that kind of behavior, such as the Blastoid enemy, which causes underlying walkways to collapse when it's defeated, or the Jawz enemy, which kills itself when it runs into the player.


Thus, we are in the unfortunate situation where, in order to have the hyper ability affect every enemy, it must also affect every single object tagged with the special collision type. Here's what the handler looks like:
HyperTouch_Special:
    ori.b   #3,collision_property(a1)
    cmpi.w  #3,(Player_mode).w              ; Are we in Knuckles Alone mode?
    bne.s   loc_105B6                       ; If not, branch
    move.w  x_pos(a1),(Player_2+x_pos).w    ; ???
    move.w  y_pos(a1),(Player_2+y_pos).w    ; ???

loc_105B6:
    move.b  #2,(Player_2+anim).w            ; Put sidekick in his rolling animation
    bset    #Status_InAir,(Player_2+status).w
    rts
Here's the rationale for that first line of code: in order to make sure the enemy is defeated, the collision flag for player 1 is set. Player 1 is currently hyper, meaning they're invincible, meaning there will never be a scenario where the enemy damages the player and survives the collision.

As a consequence to this action, whenever player 1 uses their hyper ability, they get bounced around by every bumper currently on screen. I guess the developers didn't want player 1 to feel lonely, so they also set the flag for player 2, and added the two lines at the bottom to make sure player 2 bounces off in their rolling animation.

I am just as confused as the disassembly comments are about the code in the middle.


Unfortunately, the developers didn't consider that player 2 may be riding an object which overrides the player's controls, so that last line of code causes them to enter a sort of limbo where they're riding the object, but also in mid-air.

Here's a saner approach to the problem:
HyperTouch_Special:
    ori.b   #3,collision_property(a1)
    tst.w  (Player_mode).w                  ; Are we in Sonic and Tails mode?
    bne.s   .return                         ; If not, branch
    clr.b   (Player_2+object_control).w
    move.b  #2,(Player_2+anim).w            ; Put sidekick in his rolling animation
    bset    #Status_InAir,(Player_2+status).w
.return:
    rts
Here we make sure to also clear player 2's object_control attribute, releasing them from their ride, and for the love of god, only do any of this in a Sonic and Tails game. Build the ROM, and verify that player 2 bounces off properly now.


Of course, this does nothing to alleviate other woes incurred from processing special collision in the first place, such as hyper abilities activating the warp stars atop a star post, or bumpers causing Knuckles to be crushed into walls.

Thursday, September 28, 2017

Rings are fundamentally broken in 2P mode

I know what you're thinking. "There are rings in Competition mode!?" Yes, starting from lap 4, a ring appears as an item on the item wheel, and on lap 5, there's even an enemy there which can cause you to take damage and drop it!

If you actually do so though, you'll find that the ring you drop is massively hosed. First off, it takes on the appearance of a garbled goal post. Then, even if player 2 happens to pick it up, the ring will go right back into player 1's item slot.


It's even worse when player 2 drops it. The ring doesn't disappear from player 2's item slot, and when someone picks it back up, it once again goes right into player 1's item slot, effectively duplicating it.


So what's going on? Well, several things, really. The bouncing ring object used in 2P mode is the same one used in 1P mode, which does not adequately account for a second player. As a change of pace, I thought it might be interesting to not only explain where things go wrong, but also to work out an adequate solution. Let's get to it!

When deciding how many ring objects to spawn, the ring-spawning object runs the following code fragment:
loc_1A68C:
    movea.l a0,a1
    moveq   #0,d5
    move.w  (Ring_count).w,d5
    tst.b   $3F(a0)
    beq.s   loc_1A69E
    move.w  (Ring_count_P2).w,d5
    ...
Apparently, when the ring-spawning object is initialized, byte $3F of its SST is set depending on which player took a hit, and the above code uses that to determine whose ring count should be considered. Surprisingly, a short while later, the same object uses this code to clear out the player's ring count:
loc_1A738:
    move.w  #$FFB9,d0
    jsr     (Play_Sound_2).l
    move.w  #0,(Ring_count).w
    ...
This explains why when player 2 takes damage, the ring doesn't disappear from their item slot. The game is attempting to remove it from player 1's item slot! Collecting the ring (thus duplicating it) and then taking damage again (say, on the underside of Chrome Gadget's moving platform) confirms this.

We can fix it by simply checking byte $3F again, as was done before:
Obj_2PRing_Done:
    move.w  #$FFB9,d0
    jsr     (Play_Sound_2).l
    tst.b   $3F(a0)
    beq.s   .notP2
    move.w  #0,(Ring_count_P2).w
    bra.s   Obj_2PRing_Main
; ---------------------------------------------------------------------------
.notP2:
    move.w  #0,(Ring_count).w
    ...
The reason dropped rings look like a garbled mess is because in Competition mode, the ring art is loaded to a different VRAM address. Furthermore, in all of the Competition levels, the ring colors are actually in palette line 3, since line 1 is already holding Knuckles' palette.

We can fix that by changing the line which sets the object's art_tile from this:
    move.w  #$A6BC,$A(a1)
to this:
    move.w  #$E3C6,art_tile(a1)
Then, as we muck around in the object's init code, we come across this line:
    move.b  #$47,$28(a1)
At offset $28 is the object's collision_flags attribute, and if we look at the collision type encoded in the top two bits, we find that it's set to 01, just as expected.

When the bouncing ring object is touched, it will be set to routine 4, which looks like this:
loc_1A7C2:
    addq.b  #2,5(a0)
    move.b  #0,$28(a0)
    move.w  #$80,8(a0)
    bsr.w   GiveRing
    ...
The GiveRing function gives player 1 a single ring, adding in extra logic to also award pending 1ups, and to make sure the counter doesn't overflow. Once again, the object is blindly affecting player 1's ring count, without taking into account who actually collected the ring. To fix this, we can change the object's collision type to one which distinguishes between players. Touch_Special seems to fit the bill.
    move.b  #$C7,collision_flags(a1)
Then, in the object's main routine we check (and clear) the collision_property attribute via the canonical method. Note that we are now forced to add the invulnerability timer check on the object's side of things:
Obj_2PRing_Main:
    bclr    #1,collision_property(a0)
    beq.s   .notP2
    cmpi.b  #90,(Player_2+invulnerability_timer).w
    bhs.s   .notP2
    bsr.w   GiveRing_P2
    bra.s   .collect
; ---------------------------------------------------------------------------
.notP2:
    bclr    #0,collision_property(a0)
    beq.s   .keepbouncing
    cmpi.b  #90,(Player_1+invulnerability_timer).w
    bhs.s   .keepbouncing
    bsr.w   GiveRing

.collect:
    addq.b  #2,routine(a0)
    move.b  #0,collision_flags(a0)
    move.w  #$80,priority(a0)
    ...
Having done the necessary changes, we build the ROM, load it up on our emulator of choice, and verify that everything works properly now.


Also note that there is now a race condition: if both players touch the ring on the exact same frame, the above code will award the ring to player 2. Curious, I decided to check out how Sonic 2 handles this scenario. Here is the relevant code from Sonic 2's version of Touch_ChkValue:
    move.w  (MainCharacter+invulnerable_time).w,d0
    tst.w   (Two_player_mode).w
    beq.s   +
    move.w  invulnerable_time(a0),d0
+
    cmpi.w  #90,d0
    bhs.w   +
    move.b  #4,routine(a1)  ; set the object's routine counter
    move.w  a0,parent(a1)
+
    rts
WOW, that's a lot simpler than what I came up with. The RAM address of the player who touched the ring is copied into the "parent" attribute of the ring object's SST. Later, all the ring has to do is check the attribute to find out grabbed it:
CollectRing:
    tst.b   parent+1(a0)            ; did Tails collect the ring?
    bne.s   CollectRing_Tails       ; if yes, branch
    ...
What's interesting is how the race condition is still there, and happens to have the same exact outcome. When player 2 processes the collision response list, they will overwrite the "parent" attribute set by player 1, laying claim to the ring.

Wednesday, September 27, 2017

Responsive object collisions, part 3

Picking back up from where we left off, when the collision type is 11, the code branches to Touch_Special, which looks something like this in the current disassembly:
Touch_Special:
    ...
    move.w  a0,d1                           ; Get RAM address of what object hit this
    subi.w  #Object_RAM,d1
    beq.s   loc_10406                       ; If the main character hit it, branch
    addq.b  #1,collision_property(a1)       ; Otherwise, it seems everything else does double

loc_10406:
    addq.b  #1,collision_property(a1)       ; So hitting a boss with your Tails sidekick does double damage?
    rts

This is a bit obfuscated, so it's no wonder the disassembly comments get it wrong. Here's what's going on: at this point, a0 holds the address of the player that's processing the collision response list. The above code takes that address and subtracts the address of the start of object RAM, and takes the branch if the result is zero, which can only happen if a0 is pointing at the start of object RAM.

It just so happens that the player 1 object is always loaded to the start of object RAM, so this is just a confusing way to check which player touched the object. If it was player 1, then the branch is taken, and the collision_property attribute is incremented once; otherwise, the branch is not taken, and it is incremented twice. Assuming that it was initially set to zero, this means the object's collision_property attribute is set to 1 when player 1 touches it, 2 when player touches it, and 3 when both players touch it.

So what does this mean? Next time the object runs, its collision_property attribute will be a bitfield composed of flags that indicate which players are touching it right now. This collision type is what allows objects such as bumpers to affect players separately from one another.

Finally, when the collision type is 01, none of the previous branches are taken, and so control flows into the code which directly follows...
    ; If 01...
    move.b  collision_flags(a1),d0                  ; Get collision_flags
    andi.b  #$3F,d0                                 ; Get only collision size
    cmpi.b  #6,d0                                   ; Is touch response $46 ?
    beq.s   Touch_Monitor                           ; If yes, branch
...where immediately there's a hardwired check that looks for collision size 6. If this check passes, the code branches to Touch_Monitor, which handles, uh, monitor collision. This would take us far afield, so let's skip it for now.
    move.b  (Player_1+invulnerability_timer).w,d0   ; Get the main character's invulnerability_timer
    tst.w   (Competition_mode).w                    ; Is the competition mode?
    beq.s   loc_1000A                               ; If not, branch
    move.b  invulnerability_timer(a0),d0            ; Get invulnerability_timer from whoever branched to TouchResponse

loc_1000A:
    cmpi.b  #90,d0                                  ; Is there more than 90 frames on the timer remaining?
    bhs.w   locret_10018                            ; If so, branch
    move.b  #4,routine(a1)                          ; Set target object's routine to 4 (must be reserved for collision response)

locret_10018:
    rts

Next, there's code that looks at the invulnerability_timer attribute of player 1 (in a 2P game, the attribute of the player currently processing the collision response list) and if it's lower than 90, sets the routine of the touched object to 4.

Meanwhile, the invulnerability_timer attribute is set to 120 when the player lands after taking damage, and decreases once per frame until it reaches zero, causing the player to blink for two seconds before becoming vulnerable again. The check at loc_1000A prevents the player from messing with the routine of touched objects during the first half-second of the invulnerability period.

This collision type is used by the bouncing ring object, preventing a player from picking rings back up immediately after dropping them. Note that in a 2P game, the other player could potentially collect all the rings the moment they spawn.


The placement of the check at loc_1000A annoys me greatly. Reminder that to get this far, we had to go through all the rigamole of performing bound checks on the player and the ring's hitboxes, for a response we already know in advance isn't actually going to happen. Wouldn't it have made more sense to do the check on the ring object's side of things and only call Add_SpriteToCollisionResponseList once the 30 frames are up?

It's particularly annoying because the start of the invulnerability period is exactly when the player is likely to be touching a lot of rings, which means more of them will pass all the bound checks only to then fail the timer check. Luckily, it's not as bad as it could possibly be: at the point where they're guaranteed to be touching all the rings, the player is still falling back from taking damage, and the collision response list isn't processed when the player object is in routine 4.

Tuesday, September 26, 2017

Responsive object collisions, part 2

Last time, we saw how responsive object collision is further split into four collision types, depending on the high bits of the object's collision_flags attribute. Each collision type elicits a different type of response from the object, hence the term "responsive collision".
Touch_ChkValue:
    move.b  collision_flags(a1),d1      ; Get its collision_flags
    andi.b  #$C0,d1                     ; Get only collision type bits
    beq.w   Touch_Enemy                 ; If 00, enemy, branch
    cmpi.b  #$C0,d1
    beq.w   Touch_Special               ; If 11, "special thing for starpole", branch
    tst.b   d1
    bmi.w   Touch_ChkHurt               ; If 10, "harmful", branch
    ...                                 ; If 01...
Let's start with an easy one. When the collision type is 10, the code branches to Touch_ChkHurt, which is responsible for collision with most harmful objects: things like enemy projectiles, fireballs, lasers. This comes with built-in checks for stuff like invincibility and immunity granted by an elemental barrier.
Touch_ChkHurt:
    move.b  status_secondary(a0),d0
    andi.b  #$73,d0                     ; Does player have any shields or is invincible?
    beq.s   Touch_ChkHurt_NoPowerUp     ; If not, branch
    and.b   shield_reaction(a1),d0      ; Does one of the player's shields grant immunity to this object??
    bne.s   Touch_ChkHurt_Return        ; If so, branch
    ...

When the collision type is 00, the code branches to Touch_Enemy, which handles collision with most enemies, as well as bosses. This comes with all the rigamole necessary to determine whether the player can currently damage their foe. In the event that none of these checks pass, the code defaults to calling Touch_ChkHurt.
Touch_Enemy:
    btst    #2,status_secondary(a0)     ; Is player invincible?
    bne.s   .checkhurtenemy             ; If so, branch
    cmpi.b  #9,anim(a0)                 ; Is player in their spin dash animation?
    beq.s   .checkhurtenemy             ; If so, branch
    cmpi.b  #2,anim(a0)                 ; Is player in their rolling animation?
    beq.s   .checkhurtenemy             ; If so, branch

    cmpi.b  #2,character_id(a0)         ; Is player Knuckles?
    bne.s   .notknuckles                ; If not, branch
    cmpi.b  #1,double_jump_flag(a0)     ; Is Knuckles gliding?
    beq.s   .checkhurtenemy             ; If so, branch
    cmpi.b  #3,double_jump_flag(a0)     ; Is Knuckles sliding across the ground after gliding?
    beq.s   .checkhurtenemy             ; If so, branch
    bra.w   Touch_ChkHurt
; ---------------------------------------------------------------------------

.notknuckles:
    cmpi.b  #1,character_id(a0)             ; Is player Tails
    bne.w   Touch_ChkHurt                   ; If not, branch
    tst.b   double_jump_flag(a0)            ; Is Tails flying ("gravity-affected")
    beq.w   Touch_ChkHurt                   ; If not, branch
    btst    #Status_Underwater,status(a0)   ; Is Tails underwater
    bne.w   Touch_ChkHurt                   ; If not, branch
    ...
However, if any of the checks pass, this code goes on to increase the current bonus chain, add the appropriate number of points to the player's score, and make the enemy explode. How it achieves the latter is quite interesting: it overwrites the enemy's own code pointer with the address for the explosion object, and resets the object's routine counter to zero, ensuring the explosion is initialized properly, and that it spawns both a small animal and a score popup.
    move.w  (Chain_bonus_counter).w,d0      ; Get copy of chain bonus counter
    addq.w  #2,(Chain_bonus_counter).w      ; Add 2 to chain bonus counter
    cmpi.w  #6,d0                           ; Has the counter already surpassed 5?
    blo.s   loc_101C4                       ; If not, branch
    moveq   #6,d0                           ; Cap counter at 6

loc_101C4:
    move.w  d0,objoff_3E(a1)
    move.w  Enemy_Points(pc,d0.w),d0        ; Get appropriate number of points
    cmpi.w  #16*2,(Chain_bonus_counter).w   ; Have 16 enemies been destroyed?
    blo.s   loc_101DE                       ; If not, branch
    move.w  #1000,d0                        ; Fix bonus to 10000 points
    move.w  #$A,objoff_3E(a1)

loc_101DE:
    movea.w a0,a3
    bsr.w   HUD_AddToScore
    move.l  #Obj_Explosion,(a1)             ; Create enemy destruction explosion
    move.b  #0,routine(a1)
    ...
Now, obviously, the explosion object doesn't need collision, so it never calls the Add_SpriteToCollisionResponseList function. This ensures that Touch_Enemy only runs once per enemy... or does it?


Remember how I mentioned that in a Sonic and Tails game, the collision response list is actually processed twice, once by each player? If Sonic and Tails happen to defeat the same enemy on the same exact frame, Touch_Enemy actually runs twice, the bonus chain is increased twice and the player is awarded points as if two enemies were defeated.

Next time, we'll take a look at the remaining two collision types.

Monday, September 25, 2017

Responsive object collisions, part 1

Previously, I talked about how there are two kinds of object collisions, which I named solid and responsive collision. We then saw how objects can opt into solid collision by calling a function such as SolidObjectFull, which does all the work of performing bound checks on the player and the solid obstacle, as well as affecting both objects' SSTs appropriately.

Responsive collision, on the other hand, can be attained by calling the Add_SpriteToCollisionResponseList function:
Add_SpriteToCollisionResponseList:
    lea     (Collision_response_list).w,a1
    cmpi.w  #$7E,(a1)           ; Is list full?
    bhs.s   locret_1041C        ; If so, return
    addq.w  #2,(a1)             ; Count this new entry
    adda.w  (a1),a1             ; Offset into right area of list
    move.w  a0,(a1)             ; Store RAM address in list

locret_1041C:
    rts
Much like with the Draw_Sprite function, responsive collision works in a subscription-based model: calling the function simply adds the object to a list of items to be processed in the future. The actual work of performing bound checks and producing the appropriate effects is deferred to the next frame, where it is done by the player object.

(Note that the collision response list is actually processed twice in a Sonic and Tails game, once per player object. This will become relevant later. Foreshadowing: the sign of a quality blog.)

For this reason, the collision parameters cannot be passed through registers; instead, a single byte of the solid object's SST is reserved for this purpose:
    collision_flags = $28 ; byte ; TT SSSSSS ; TT = collision type, SSSSSS = size
The decision to stuff everything in a single byte, presumably to save space on the SST, has a couple of consequences. First off, the collision_flags attribute carries information about both the collision size and the collision type. As a result, each time the game needs to read either value, it must clear out the remaining bits.
Touch_Loop:
    movea.w (a4)+,a1                    ; Get address of first object's RAM
    move.b  collision_flags(a1),d0      ; Get its collision_flags
    bne.s   Touch_Width                 ; If it actually has collision, branch
    ...

Touch_Width:
    andi.w  #$3F,d0                     ; Get only collision size
    add.w   d0,d0                       ; Turn into index
    lea     Touch_Sizes(pc,d0.w),a2
    moveq   #0,d1
    move.b  (a2)+,d1                    ; Get width value from Touch_Sizes
    move.w  x_pos(a1),d0                ; Get object's x_pos
    sub.w   d1,d0                       ; Subtract object's width
    ...
Then, because six bits are obviously not enough to store the actual dimensions of the object's hitbox, the collision size is really just an index to a lookup table of hardcoded size definitions, stored in pairs of width by height.
Touch_Sizes:
    dc.b    4,   4
    dc.b  $14, $14
    dc.b   $C, $14
    dc.b  $14,  $C
    dc.b    4, $10
    dc.b   $C, $12
    dc.b  $10, $10
    ...
Collision type, on the other hand, is fairly straightforward. There are four collision types, and the appropriate behavior is selected by a series of branches; no fancypants arithmetic involved.
Touch_ChkValue:
    move.b  collision_flags(a1),d1      ; Get its collision_flags
    andi.b  #$C0,d1                     ; Get only collision type bits
    beq.w   Touch_Enemy                 ; If 00, enemy, branch
    cmpi.b  #$C0,d1
    beq.w   Touch_Special               ; If 11, "special thing for starpole", branch
    tst.b   d1
    bmi.w   Touch_ChkHurt               ; If 10, "harmful", branch
    ...                                 ; If 01...
Next time, we'll continue our foray into responsive collision by taking a look at the implementation of each collision type.

Friday, September 22, 2017

Out of sight, out of mind

Previously, I mentioned how when an object calls the Draw_Sprite function, bit 7 of its render_flags is set depending on whether or not the object is currently on-screen. Solid objects actually use this information for an optimization pass: if the object is off-screen, it will not bother processing collision with either player. This can be abused by using the spin dash or Hyper Sonic's double jump attack to temporarily outrun the camera, as seen here.
loc_1DF88:
    tst.b   4(a0)
    bpl.w   loc_1E0A2
    ...
In practice however, the SolidObjectFull function must keep running, because if it doesn't clear the player's "standing on object" flag properly, then we might just end up triggering the slope glitch again. The solution is to only skip over to the on-screen test once the appropriate bit on the object's status bitfield has been cleared, signaling that the player did not collide with the object on the previous frame.
SolidObjectFull:
    lea     (Player_1).w,a1
    moveq   #3,d6
    movem.l d1-d4,-(sp)
    bsr.s   sub_1DC74
    movem.l (sp)+,d1-d4
    ...

sub_1DC74:
    btst    d6,$2A(a0)
    beq.w   loc_1DF88
    move.w  d1,d2
    add.w   d2,d2
    btst    #1,$2A(a1)
    bne.s   loc_1DC98
    move.w  $10(a1),d0
    sub.w   $10(a0),d0
    add.w   d1,d0
    bmi.s   loc_1DC98
    cmp.w   d2,d0
    blo.s   loc_1DCAC
    ...
That's not the end of it, though: the second player object receives an additional optimization. If player 2 is offscreen, the main body of the SolidObjectFull function does not run at all. The result is that 2P Tails does not interact with any solid objects whenever he is off-screen, as seen here.
SolidObjectFull:
    lea     (Player_1).w,a1
    moveq   #3,d6
    movem.l d1-d4,-(sp)
    bsr.s   sub_1DC74
    movem.l (sp)+,d1-d4
    lea     (Player_2).w,a1
    tst.b   4(a1)
    bpl.w   locret_1DCB4
    addq.b  #1,d6

sub_1DC74:
    btst    d6,$2A(a0)
    beq.w   loc_1DF88
    ...
This extra optimization turns out to be overkill, because if Tails happened to be standing on a solid object when he got scrolled offscreen, then the object will once again fail to clear his "standing on object" flag, triggering the slope glitch all over again, as seen here.

Thursday, September 21, 2017

Solid object collisions

There are two kinds of collisions that can be registered between the player and other objects, which we might call solid collision and responsive collision. As I've previously mentioned, an object can obtain solid collision by calling a function such as SolidObjectFull.

A couple of important things happen when the player touches the top of one of these objects. First, the object sets bit 3 of the status bitfield, at offset $2A of the player's SST. This flag signals the player object that it's presently standing on top of an object, so it should skip over the code that checks the level blocks at the player's feet and determines whether the player should be falling or not.

The next thing the object does is copy the address of its own SST to offset $42 in the player's SST. This is done so the player object can later consult the state of the object it's standing on and react accordingly. In practice, this is only used to look up the object's x_pos and width_pixels values, to determine whether the player is close enough to the edge of the object that one of the teetering animations should play.

Lastly, the object also sets bit 3 or 4 of its own status bitfield, depending on which player is standing on it. This is sort of redundant, because the two player SSTs are both always at the same RAM address, meaning the object could just look up the previous two offsets to obtain the same information, but there you go.

It's important to note that the player object puts no effort into ever clearing its own "standing on object" flag: it's the solid object's responsibility to keep calling the SolidObjectFull function, check whether the players are still there, and if not, clear their "standing on object" flags depending on the value of its status bitfield. Breakable objects should take care to also clear the flags when they're destroyed.

Armed with this knowledge, you should be able to understand why the "slope glitch" behaves as it does.


Due to an oversight in the object's code, when Tails breaks the ice block, Sonic's "standing on object" flag isn't cleared. The code that checks the level blocks at his feet continues to be skipped, causing him to float, and since the block has been destroyed, it will never detect Sonic running off the edge, and so he'll stay up there until he jumps, which sets his "airborne" flag.

When he lands on the ground, Sonic is affected by the floor's angle for a single frame. Afterwards, his "airborne" flag is once again cleared, whereas his "standing on object" flag is still set, so the code that checks the level blocks at his feet gets skipped again, making him float once more.

Because the block's SST got zeroed out when it was destroyed, its x_pos and width_pixels report an infinitely narrow object whose center is all the way at the start of the level, activating the teetering animation.

The only way out of this mess is to jump on a different object, and then either run or jump off of that one. If nothing else goes wrong, the second object will clear the "standing on object" flag properly and disable the glitch.

Wednesday, September 20, 2017

Three more goofs in the Competition levels

Speaking of oddities present in the Competition levels, here are three more goofs I've come to notice over the years:


In Azure Lake, one of the blocks in this overhanging ledge doesn't have any collision assigned to it. The height mask is actually set properly, it's just that the block doesn't have either solidity bit set.


Balloon Park has a bunch of identical-looking flags waving in the background, and exactly one of them is blowing in the opposite direction of the others. This probably happened accidentally when mirroring a bunch of level blocks in order to fill out the rest of the background plane.

Finally, also in Balloon Park, my favorite. First off, in Competition mode, Sonic's palette is loaded to the would-be player palette in line 0, whereas Knuckles' palette is loaded to the would-be enemy palette in line 1.


Meanwhile, although the bumper objects in Balloon Park are set to use colors from palette line 1 (left below), I find that they end up looking much better when they're changed to use line 0 (right):


Here's my theory. The bumper's graphics were originally designed to use Sonic's palette, but the object ended up being programmed to use the enemy palette, like most stage-specific objects do. The error was never caught because unlike most situations, Sonic's palette and the enemy palette are almost completely identical, and as such the bumper doesn't look particularly wrong when the wrong one is applied.

Tuesday, September 19, 2017

Wall running woes, part 2

In standalone Sonic 3, the design flaws in the level collision code are even more pronounced than in Sonic & Knuckles. While running on the ceiling, or a wall on the left side, level collisions are completely ignored, so you'll sail right through solid parts of the level. Obviously, this can easily cause you to become irreparably stuck in a wall- er, I mean, watch out for Dr. Robotnik's diabolical traps.


Unfortunately, although otherwise quite welcome, the changes to the algorithm in Sonic & Knuckles caused a breaking change in one of the Competition stages. (left: Sonic 3 alone; right: Sonic 3 & Knuckles)


In Sonic 3 & Knuckles, you can no longer backtrack through the loop in Desert Palace after you cross it. The reason for this becomes evident once we open up the level in SonLVL and take a gander at the collision data:


The loop is comprised entirely of blocks solid from all sides! The only reason the thing worked in the first place was due to Sonic 3's lax attitude while running on left-hand side walls. Changing the collision on the walkway threading the loop from fully solid to top-only would fix this issue.

Monday, September 18, 2017

Wall running woes, part 1

There's a design flaw in the code for level collisions when you're running on a wall. In addition to raw X and Y velocity, the player object tracks an extra value, ground velocity, which is your speed along whichever surface you're running on. Later, this value gets multiplied by the sine and cosine of the player's current angle in order to calculate the actual displacement along the X and Y axes.

Normally, this value is cleared when you run into a wall. However, when you bump into a solid level block while running up or down a wall, your ground velocity isn't cleared, because you're not actually running into a wall, you're running into the floor or the ceiling!


When you run into the floor, the solid blocks underneath prevent your character from actually moving. However, due to gravity, your ground velocity continues increasing and eventually, the displacement experienced in a single frame is so large, your character's bounding box travels beyond the range of the solid blocks, sending you right through the floor.


The developers knew of this limitation in the collision engine, which is why the issue doesn't occur in the more obvious places. For instance, in Carnival Night Zone, you can easily get yourself running on the walls by simply jumping at the rounded corners in the ceiling. If you run down the wall, though, your character will land properly on the floor.


The trick is to introduce an invisible "GTGT" collision marker object right where the wall meets the floor. Object collision is completely indifferent to the player's running direction, forcing them to stand up when the object is stood upon.

Friday, September 15, 2017

Moonwalker

Sonic Retro forum user and all-around sweetheart Josh posted about a curious behavior in regards to touching springs while falling back from taking damage. Wow, I can't believe it's almost been a year since then.


This is a consequence of how routine 4, the hurt state, works. While in routine 4, the player continuously falls backward until a) something sets their routine back to 2, thus restoring player control, or b) they land on a solid surface. When the latter occurs, the player object resets its own routine, but also takes care to clear its horizontal velocity.

If it didn't do this, the backward momentum from the fall would force the player into a moonwalk upon landing. Those of you who have played Sonic Mania may already know how annoying that would be, since it occasionally happens when you touch one of Metallic Madness Zone's shrink/growth rays, delaying your entry into the next room for no reason.


What's happening with the horizontal spring up there is that Sonic got within range to activate it, except without actually landing on anything. This sent him hurtling off to the left while still in routine 4, but it was only when he touched the floor at the very edge that his routine was reset and his horizontal velocity killed.

---

Reader NES Boy points out this is the blog's 100th post. Big deal. Raymond Chen has over five thousand posts across fourteen years under his belt and he's still going strong. I'll break out the fireworks if I manage to do this for a solid year without missing a beat.

But seriously, big thanks to everyone following this blog, and even bigger thanks to those of you who have added to the discussion in the comments section. I have many more stories yet to tell.

Thursday, September 14, 2017

Can't catch me if I'm already gone

Like Hydrocity Zone, Carnival Night Zone too features a one-way obstacle you wouldn't expect to fall back through, but which the layout accounts for anyway. This time, it's the hover fans scattered throughout both acts of the level.


In the above scenario, when you take damage over a row of fans, they won't catch you, and you'll fall into an otherwise empty room. There's a red spring in the corner to make sure you can get back out, which is kind of overkill, considering you can get within range of the fans simply by jumping.

I don't know what they were so worried about, because if you do this close to the bottom of the stage, you'll fall straight into Knuckles' path, where you'll likely get stuck anyway due to all the Knuckles-only breakable walls.

As for why it happens: although the fans are part of the level's blocks much like the water slides in Hydrocity Zone, they actually have no collision and are instead controlled by invisible objects placed directly over them. In turn, these objects don't affect the player when their routine counter is set to 4 or above. Previously, we saw how routines 6 and above are used when the player has died; routine 4 occurs when the player is falling back from taking damage.
sub_31E96:
    ...
    cmpi.b  #4,5(a1)
    bhs.w   locret_31F2E
    tst.b   $2E(a1)
    bne.s   locret_31F2E
    ...
I'm not sure why the object behaves that way, but there you go.

Something similar happens in Flying Battery Zone, where none of the screen event code runs if the player is dead. This leads to some visual weirdness if you save yourself from death by entering debug mode: since the screen events aren't running, the foreground plane isn't redrawn as you move around the stage.


This one I can explain: the screen event code is responsible for checking whether you're inside or outside the ship, and triggering all the different effects, such as loading palettes, changing backgrounds and doing the necessary level layout modifications. It does this by loading player 1's X and Y coordinates into the d0 and d1 registers upfront and comparing them against a bunch of different ranges.
FBZ1_ScreenEvent:
    cmpi.b  #6,($FFFFB005).w
    blo.s   loc_5242E
    rts                                     ; Don't do any special events while Sonic is dying
; ---------------------------------------------------------------------------

loc_5242E:
    lea     FBZ1_LayoutModRange(pc),a1
    move.w  ($FFFFB010).w,d0
    move.w  ($FFFFB014).w,d1
    move.w  ($FFFFEED2).w,d2
    jmp     loc_52442(pc,d2.w)
; ---------------------------------------------------------------------------

loc_52442:
    bra.w   FBZ1SE_Normal
; ---------------------------------------------------------------------------
    bra.w   FBZ1SE_LayoutMod1
; ---------------------------------------------------------------------------
    bra.w   FBZ1SE_LayoutMod2
; ---------------------------------------------------------------------------
    ...
If this code ran while during the death animation, the player could potentially enter one of those ranges while falling off the screen, causing the background to change while the camera is stuck where the player originally died. The simplest solution is to check for routine 6 or above before doing anything, and although it fudges up debug mode a bit, it has no visible effect when the camera freezes normally.

Wednesday, September 13, 2017

Why is there an invisible solid object rising along with the floor beam?

Beats me. The best way to answer this kind of question is to go in the disassembly, dummy out the relevant bit of code and see what happens. First off, the solid background flag isn't set until the thing starts moving, so that sucks.


That would be easily fixable, though, so let's move on. If you let the beam carry you through the one-way platform, your character won't have any vertical momentum, so they'll get caught on the top-only block and fall behind the background scroll, eventually getting crushed by the sideways collision on the beam as it plows on ahead.


This could be fixed by removing the sideways collision on the beam chunks. Lastly though, and perhaps most damning, is the fact that the beam does not carry you horizontally as it moves diagonally up the screen.


This is likely why every other use of the solid background flag only has the background plane scroll vertically. Even the crushing wall at the start of Hydrocity Zone 2, which moves horizontally, does not let you stand on top of it.

Tuesday, September 12, 2017

That sinking feeling

It's worth pointing out that the only thing preventing the quirk we saw last time from occurring with completely solid level blocks is wall collision: as you may recall, whether a block is solid from the top or from other directions is determined by two separate flags, and the associated behavior is also defined separately. As such, just like top-only blocks, if you're in the air and have any upward momentum, you will not register any collision with the top of fully solid blocks.

Ordinarily, this isn't an issue, because the player will always be falling when they touch the ground. The problem occurs when a level event sets the solid background flag and begins scrolling the background plane upward. In this scenario, it is possible for the floor to rise faster than the player, and since the player is moving upward, no collision is registered.


This can happen during the rising sand sequences in Sandopolis Zone, as well as directly after the Carnival Night Zone act 1 boss, since during this sequence, the foreground plane contains the boss' breakable blocks, where as the entirety of the shaft, including the floor below, is part of the rising background plane.

It doesn't happen in Marble Garden Zone because the player gets pushed up along with the background plane, even in mid-air, and it also doesn't happen at the very end of Flying Battery Zone because there's an invisible solid object rising along with the long floor beam.

Monday, September 11, 2017

What goes up won't come down

Some time ago, I mentioned how level blocks have two different collision flags: one makes them solid from the top, the other makes them solid from everywhere else, and they can be combined in order to make a block completely solid.

Blocks which are only solid from the top have some interesting properties, and I think it's worth pointing them out. First off, since you can only interact with the top surface, their undersides are often quite rough.


This generally has no negative effect, but when there are two such blocks stacked on top of one another, occasionally your character may end up standing on the lower one instead of the top one as intended.


More important is how these blocks work, though. They're supposed to allow you through if you jump from underneath them, but then catch you when you're falling back down, right? So the way the developers programmed them is that if you're in the air, and you have any upward momentum, then the blocks are completely intangible.


This leads to a pretty obscure mechanic, which is nonetheless acknowledged by the game's level design. If you have a sloped surface made out of top-only blocks, you can pass through it horizontally so long as you're in the air and moving upward. For instance, early on in Hydrocity Zone 2, as seen above, if you chain a spindash into a long jump, the speed will carry you right through the water slide and onto a ledge with some hidden goodies below.


Most places where this is applicable are noticeably barren, however, save for the occasional spring to get you back out. Notably, in one of the dumbest layout failures in the game, this trick allows quick and easy access to Hydrocity Zone 2's sewers, as demonstrated by the legendary GoldS in his glitches and oversights series.

Friday, September 8, 2017

After boss, cleanup!

Alright, one last detail related to act transitions. When the end sign object spawns at the end of act 1, it calls a function named AfterBoss_Cleanup. Similar to the dynamic resize routines, this function doesn't actually do anything for most levels, but it still plays an important role for the ones where the transition occurs prior to the act 1 boss.
AfterBoss_Cleanup:
    moveq   #0,d0
    lea     (Current_zone_and_act).w,a1
    move.b  (a1)+,d0
    add.b   d0,d0
    add.b   (a1)+,d0
    add.b   d0,d0
    move.w  off_83C1C(pc,d0.w),d0
    jmp     off_83C1C(pc,d0.w)
; ---------------------------------------------------------------------------
off_83C1C:
    dc.w loc_83C7C-off_83C1C
    dc.w loc_83C90-off_83C1C
    dc.w locret_83CA6-off_83C1C
    dc.w locret_83CA6-off_83C1C
    dc.w loc_83CA8-off_83C1C
    dc.w loc_83CA8-off_83C1C
    dc.w locret_83CB2-off_83C1C
    dc.w locret_83CB2-off_83C1C
    dc.w locret_83CB2-off_83C1C
    dc.w locret_83CB2-off_83C1C
    dc.w locret_83CB2-off_83C1C
    dc.w loc_83CB4-off_83C1C
    dc.w loc_83CC0-off_83C1C
    dc.w loc_83CC0-off_83C1C
    dc.w loc_83CC0-off_83C1C
    dc.w loc_83CC0-off_83C1C
    dc.w locret_83CCC-off_83C1C
    dc.w locret_83CCC-off_83C1C
    dc.w locret_83CCC-off_83C1C
    dc.w locret_83CCC-off_83C1C
    ...
As you may recall, when a boss spawns, it typically loads its palette over the level's enemy palette. This will temporarily screw up all objects that use it, such as um, enemies. That is, if the boss doesn't outright load its own graphics over the enemy art. Regardless, we can rely on the eventual act transition to load act 2's palette and PLCs and fix everything.

...Unless the boss takes place in the act 2 layout, in which case the act transition has already happened, and act 2 will continue using the same art and palette! In this scenario, the function ensures everything is properly reloaded.
loc_83C90:
    lea     (Pal_AIZFire).l,a1
    jsr     (PalLoad_Line1).l
    lea     PLC_83CCE(pc),a1
    jmp     (Load_PLC_Raw).l
If that were the whole story though, only AIZ2 and ICZ2 would have cleanup functions, which is obviously not the case. First off, no other act 2 code is ever called, because no other act 2 spawns an end sign object.

Next, although Mushroom Hill Zone's act transition does occur during the level results, it does not immediately load the autumn-esque act 2 palette, instead keeping the act 1 colors around a little while longer. As such, the cleanup function reloads the enemy palette, in order to correctly display the blue mushrooms in the intro area.
loc_83CC0:
    lea     (Pal_MHZ2).l,a1
    jmp     (PalLoad_Line1).l
Marble Garden Zone's cleanup function reloads the monitor, spike and spring art, which seems useless and is undone by the end sign loading its own graphics. Angel Island Zone 1's cleanup function reloads the entire level palette, and is called by the cutscene Knuckles object in order to scrub away the intro colors, I assume.

Once again, however, there's something weird going on:
    dc.w loc_83CC0-off_83C1C    ; LBZ1
    dc.w loc_83CC0-off_83C1C    ; LBZ2
    dc.w loc_83CC0-off_83C1C    ; MHZ1
    dc.w loc_83CC0-off_83C1C    ; MHZ2
Like the dynamic resize routines before it, the AfterBoss_Cleanup function's jump table clearly contains some incorrect pointers. This time, both acts of Launch Base Zone are pointing at palette cleanup code meant for Mushroom Hill Zone. When I first saw this, I figured it must be impossible to observe it in practice, because the act transition will come along and fix everything before anything using the enemy palette comes up on screen. Wrong!


In Knuckles' route, one of the floor chunks happens to include a piece of the large rotating drums prevalent throughout the stage. Those drums happen to use the enemy palette, and so sure enough...


...when the end sign object spawns, the drums' colors switch over to those of MHZ's blue mushrooms, and then, when the act transition is triggered, they finally take on their proper act 2 colors.

Thursday, September 7, 2017

Blue Knuckles

In my previous post I just casually mentioned Blue Knuckles, which is a fairly well-known glitch. But who exactly is Blue Knuckles? Is he the remnants of a fourth playable character? Well, not really. He's just the consequence of how various character-specific stuff is programmed.

You see, the game uses three different RAM variables to keep track of which character you're currently playing as. The first two are the Player_mode and Player_option words at $FF08 and $FF0A. Player_mode is the one that's actually used for game logic, whereas Player_option is the character you have selected in the menus. However, Player_mode gets overwritten with the value of Player_option on each level load.
Player_mode =   ramaddr( $FFFFFF08 ) ; word ; 0 = Sonic and Tails, 1 = Sonic alone, 2 = Tails alone, 3 = Knuckles alone
Player_option = ramaddr( $FFFFFF0A ) ; word ; option selected on level select, data select screen or Sonic & Knuckles title screen

The third variable is the character_id byte at offset $38 in the player object's SST, which we learned about back when we looked at the breakable wall object. It is set to a specific, hardcoded value by each of the Sonic, Tails, and Knuckles objects, and is mostly used in object to object interactions.
character_id =  $38 ; byte ; 0 for Sonic, 1 for Tails, 2 for Knuckles
The Blue Knuckles glitch occurs when the Player_option variable, through RAM corruption or otherwise, gets set to an invalid value. You can force this by using the PAR code FFFF0A:0004.


When this happens, the game spawns a Knuckles object, but loads Sonic's palette, resulting in... well, a blue Knuckles. It also loads Sonic's lives icon and puts the player at Sonic's spawn coordinates, but without Sonic's level intro objects, such as the snowboard at the start of Icecap Zone, or Tails at the start of Carnival Night Zone and Mushroom Hill Zone.

But why a blue Knuckles rather than a red Sonic? Why Sonic's coordinates rather than Knuckles'? What are the rules?

There are no rules. It's completely arbitrary depending on how each case was programmed. For instance, consider the case in which the player objects are spawned:
    move.w  (Player_mode).w,d0
    bne.s   loc_6B1E
    move.l  #Obj_Sonic,(Player_1).w
    move.l  #Obj_DashDust,(Dust).w
    move.l  #Obj_Insta_Shield,(Shield).w
    move.w  #$B000,($FFFFCD2A).w
    move.l  #Obj_Tails,(Player_2).w
    move.w  ($FFFFB010).w,($FFFFB05A).w
    move.w  ($FFFFB014).w,($FFFFB05E).w
    subi.w  #$20,($FFFFB05A).w
    addi.w  #4,($FFFFB05E).w
    move.l  #Obj_DashDust,(Dust_P2).w
    move.w  #0,($FFFFF708).w
    rts
; ---------------------------------------------------------------------------

loc_6B1E:
    subq.w  #1,d0
    bne.s   loc_6B42
    move.l  #Obj_Sonic,(Player_1).w
    move.l  #Obj_DashDust,(Dust).w
    move.l  #Obj_Insta_Shield,(Shield).w
    move.w  #$B000,($FFFFCD2A).w
    rts
; ---------------------------------------------------------------------------

loc_6B42:
    subq.w  #1,d0
    bne.s   loc_6B64
    move.l  #Obj_Tails,(Player_1).w
    move.l  #Obj_DashDust,(Dust_P2).w
    addi.w  #4,($FFFFB014).w
    move.w  #0,($FFFFF708).w
    rts
; ---------------------------------------------------------------------------

loc_6B64:
    move.l  #Obj_Knuckles,(Player_1).w
    move.l  #Obj_DashDust,(Dust).w
    rts
; ---------------------------------------------------------------------------
Here's what this code says: if Player_mode is 0, spawn Sonic and Tails. Otherwise, if it's 1, spawn Sonic. Otherwise, if it's 2, spawn Tails. Otherwise, spawn Knuckles. All invalid values will spawn a Knuckles object. On the other hand, here's the code that loads the player palette:
    moveq   #3,d0
    cmpi.w  #3,(Player_mode).w
    bne.s   loc_61DA
    moveq   #5,d0

loc_61DA:
    bsr.w   LoadPalette_Immediate
    ...
If Player_mode is 3, load Knuckles' palette. Otherwise, load Sonic's palette. This time, all invalid values will instead load Sonic's palette. Knuckles is the exception here, even Tails uses Sonic's palette.
    moveq   #5,d0
    cmpi.w  #3,(Player_mode).w
    beq.s   loc_60DA
    moveq   #1,d0
    cmpi.w  #2,(Player_mode).w
    bne.s   loc_60DA
    moveq   #7,d0
    tst.b   (Graphics_flags).w
    bmi.s   loc_60DA
    moveq   #$52,d0

loc_60DA:
    bsr.w   Load_PLC
    ...
The lives icon behaves similarly. If Player_mode is 3, load Knuckles' lives icon. Otherwise, if it's 2, load Tails' lives icon. Otherwise, load Sonic's lives icon. Note the extra check in the Tails branch, though: if the sign bit on the Graphics_flags byte is set, the game instead picks the Miles lives icon.

The level results object swaps the logic on a few branches, and consequently we get a Miles/Tails results screen:
    lea     (ArtKosM_ResultsSONIC).l,a1
    cmpi.w  #1,(Player_mode).w
    bls.s   loc_2DB58
    lea     (ArtKosM_ResultsKNUCKLES).l,a1
    cmpi.w  #3,(Player_mode).w
    beq.s   loc_2DB58
    lea     (ArtKosM_ResultsMILES).l,a1
    tst.b   (Graphics_flags).w
    bpl.s   loc_2DB58
    lea     (ArtKosM_ResultsTAILS).l,a1

loc_2DB58:
    ...

loc_2DB58:
    jsr     (Queue_Kos_Module).l
    ...
And so on. By exploring different instances in which there's character-specific behavior, you can get a feel for whether the game is doing something like Player_mode == 3, Player_mode != 3, or sometimes even Player_mode < 3.