Tuesday, November 7, 2017

Unsafe cylinder DPLC, part 2: limbo bug

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

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

Holy goodness, what happened here?

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

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

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

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

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

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

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

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


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

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

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


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

1 comment: