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.

1 comment:

  1. Didn't expect to see a post on this so quickly. Great explanation!

    ReplyDelete