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.

No comments:

Post a Comment