Add the Air Roll/Flying Spin Attack
From Sonic Retro
NOTE: This guide isn't just copy-paste. This is intended to teach more than just how to port the Air Roll.
EDIT (SSRG, 12/6/19): Removed a mention of how putting the bsr just anywhere in Obj01_MdJump/Obj01_MdJump2 would screw up the jump, and moved the placement of the bsr, based off of Iso Kilo's observations.
EDIT (3rd edit total, 12/9/2019): Fixed a bug linked to the first edit.
As you likely know, whenever you jump on a spring in a Sonic game, you enter a vulnerable state. However, in Sonic Triple Trouble, you could press a jump button while in the spring animation to curl up into a ball:
Now, this isn't in Sonic 1, 2, or 3 & Knuckles. However, this guide can show you how to add this move to Sonic 1's Hivebrain disasm, so that you can do this in Sonic 1.
NOTE: For those not using the S1 Hivebrain disasm or S1 at all, porting should be easy. Make sure to look for the equivalent addresses and routines.
Now, we want the code to be gone through whenever Sonic is setting his jumping data. As such, putting it in Obj01_MdNormal wouldn't work for our purposes. Instead, we should put it in Obj01_MdJump and Obj01_MdJump2.
This is Obj01_MdJump:
Obj01_MdJump: ; XREF: Obj01_Modes bsr.w Sonic_JumpHeight bsr.w Sonic_ChgJumpDir bsr.w Sonic_LevelBound jsr ObjectFall btst #6,$22(a0) beq.s loc_12E5C subi.w #$28,$12(a0) loc_12E5C: bsr.w Sonic_JumpAngle bsr.w Sonic_Floor rts
And this is Obj01_MdJump2:
Obj01_MdJump2: ; XREF: Obj01_Modes bsr.w Sonic_JumpHeight bsr.w Sonic_ChgJumpDir bsr.w Sonic_LevelBound jsr ObjectFall btst #6,$22(a0) beq.s loc_12EA6 subi.w #$28,$12(a0) loc_12EA6: bsr.w Sonic_JumpAngle bsr.w Sonic_Floor rts
As Tanman Tanner's reply on the SSRG tutorial indicates, placing the branch to the code just anywhere will cause issues. I've had one spot where there aren't issues. So, place this:
Right before the RTS' in loc_12E5C and loc_12EA6.
Now, bsr and jsr are a special kind of branch. After the code it branches to is finished, it'll automatically return to the point where we branched from and continue from right below it. This will be important for the format of our Air Roll code.
So, we've created a branch to a new subroutine. The problem is, it doesn't exist! If you try to build right now, you'll get an error. Now, we have to create the subroutine itself.
Put the proper label within Sonic's code, I've personally put it below Sonic_JumpHeight's function.
Now, we have to actually code the action. We want the code to only execute if A/B/C is pressed, right?
Now, this tutorial should show you how to utilize button presses, so check it out real quick, because I'd rather not copy what Selbi had to say.
Now that you are back, you should have created something like this:
Sonic_AirRoll: move.b ($FFFFF603).w,d0 ; Move $FFFFF603 to d0 andi.b #$70,d0 ; Has A/B/C been pressed? bne.w AirRoll_Checks ; If so, branch.
Now, we don't want this code being all there is, and we don't want it moving on to whatever is below it, especially if that's what is being branched to by the button press, correct? This is where rts (Return To Subroutine) comes in handy. The name should hint at it's function. Now, rts returns to the point where the current routine/subroutine was branched from, and continues code execution from there, much like what bsr and jsr automatically do. So, in order to keep the game from executing code it shouldn't, we should add a rts to the bottom of Sonic_AirRoll:
Sonic_AirRoll: move.b ($FFFFF603).w,d0 ; Move $FFFFF603 to d0 andi.b #$70,d0 ; Has A/B/C been pressed? bne.w AirRoll_Checks ; If so, branch. rts ; Return.
Now, there's another new subroutine being branched to! So, create that label, AirRoll_Checks, right below the rts. This is how the Sonic_AirRoll function should look:
Sonic_AirRoll: move.b ($FFFFF603).w,d0 ; Move $FFFFF603 to d0 andi.b #$70,d0 ; Has A/B/C been pressed? bne.w AirRoll_Checks ; If so, branch. rts ; Return. AirRoll_Checks:
Now, we don't want the code to execute if Sonic's already in a ball, right? Luckily, in RAM, there's a SST which stores the current object's animation, which just so happens to be Sonic! Sonic's animation ID for rolling in Sonic 1 is #$2. The SST in question is $1C. IF you know what's about to happen, good job, you already know of the cmp (Compare) instruction. This instruction compares one thing to another thing. Using this, we can perform a branch using beq (Branch if Equal) and bne (Branch if Not Equal). This is what the end result should look like:
AirRoll_Checks: cmpi.b #2,$1C(a0) ; Is animation 2 active? bne.s AirRoll_Set ; If not, branch.
If you want the move to, just like in Triple Trouble, only be able to occur if Sonic's in his spring animation, change the animation ID to the spring animation (#$10, IIRC) and invert the branch (beq -> bne, bne -> beq).
Now, we don't want Sonic to be able to Air Roll if he isn't in the air, correct? Luckily, there's a bitfield in the SSTs often known as status. If bit one, in this case, is enabled, that means Sonic's in the air. Now, you may just think, "I can do what I just did with $1C!" That isn't the case. Since $22/status is a bitfield, we have to use a command known as btst (Bitest). This checks if a bit has 0 stored in it. If we set it to use beq, the move will only trigger if Sonic isn't in the air. That's the opposite of what we want, so the branch must be inverted. This is what the final result should be:
btst #1,$22(a0) ; Is bit 1 in the status bitfield enabled? bne.s AirRoll_Set ; If so, branch.
Now, again, we don't want the code to continue executing what is below it. So, add another rts. This is what the Sonic_AirRoll function should look like currently:
Sonic_AirRoll: move.b ($FFFFF603).w,d0 ; Move $FFFFF603 to d0 andi.b #$70,d0 ; Has A/B/C been pressed? bne.w AirRoll_Checks ; If so, branch. rts ; Return. AirRoll_Checks: cmpi.b #2,$1C(a0) ; Is animation 2 active? bne.s AirRoll_Set ; If not, branch. btst #1,$22(a0) ; Is bit 1 in the status bitfield enabled? bne.s AirRoll_Set ; If so, branch. rts ; Return.
Now, do you notice how AirRoll_Checks is branching to a new subroutine called AirRoll_Set? Add that label below AirRoll_Checks' rts.
Now, do you remember how $1C in the SST has the current object's animation stored to it? We are about to use that information again. There's a handy instruction called move (move), which moves a thing to a location. We can move #$2 to $1C in order to set Sonic's animation to be the rolling animation!
So, this is what should be under AirRoll_Checks now:
AirRoll_Set: move.b #2,$1C(a0) ; Set Sonic's animation to the rolling animation.
With that, the Air Roll should be functional. But, wait, you may be saying, "Won't the code just move into the code below it?" Not so fast. Remember this line that we inserted at the beginning of the tutorial?
Hopefully you remember what I said bsr and jsr do. bsr and jsr are branches which have the function of rts built-in. They automatically return to the point where we branched to the main routine, and continues code execution from there. Therefore, our code is self-contained and functional.
Have fun with the Air Roll!
- Inferno Gear (creating the guide, coding the Air Roll)
- Iso Kilo (Answering my question when I hit a snag while coding this for my own hack, pointing out how the bsr could be at the top of Obj01_MdJump/Obj01_MdJump2.)