Fix screen boundary spindash bug
From Sonic Retro
(Original guide by Flamewing)
The spindash, as seen in Sonic 2, Sonic 3 and Sonic & Knuckles, comes with an annoying bug: if you spindash when you are at the boundaries of the screen, the spindash will be canceled and you will remain at the edge of the screen. This is easiest to see in Sky Chase Zone at the part where you encounter the Wing Fortress: if you stay at the right edge of the screen, you will be unable to spindash left unless you walk a bit left first. This can also be seen when fighting the Aquatic Ruin Zone boss. Spin dash into the boss from the top of the totem pole as he comes toward you, and you will be rebounded into the screen boundary, and will then encounter the bug if you attempt another spin dash.
Fixing the bug
Locate the word_1AD0C label. In this routine, locate the following lines:
bra.s Obj01_Spindash_ResetScr ; =========================================================================== ; word_1AD0C:
And add code just before it so that it looks like this:
move.b angle(a0),d0 jsr (CalcSine).l muls.w inertia(a0),d1 asr.l #8,d1 move.w d1,x_vel(a0) muls.w inertia(a0),d0 asr.l #8,d0 move.w d0,y_vel(a0) bra.s Obj01_Spindash_ResetScr ; =========================================================================== ; word_1AD0C:
After you have done that, the bug will be fixed. As a bonus, this will also fix a related bug that causes Sonic to jump straight up (losing all speed that would be gained from the spindash) if you jump one frame after releasing the spindash.
Things to keep in mind
This bug will be present in any other player objects as well, that means it needs to be fixed for Tails and Knuckles if they exist.
The bug explained
The bug is quite involved in its inner workings. To understand it, you must understand that, while on ground, Sonic's speed is completely determined by his inertia, as can be seen on routine Obj01_UpdateSpeedOnGround, on label Obj01_Traction:
Obj01_UpdateSpeedOnGround: [...] ; loc_1A630: Obj01_Traction: move.b angle(a0),d0 jsr (CalcSine).l muls.w inertia(a0),d1 ; <= (1) this line asr.l #8,d1 move.w d1,x_vel(a0) ; <= (1) this line muls.w inertia(a0),d0 ; <= (1) this line asr.l #8,d0 move.w d0,y_vel(a0) ; <= (1) this line
This block sets Sonic's X and Y velocities accordingly to the ground angle and his inertia. Obj01_UpdateSpeedOnGround is called by the Sonic_Move routine; this information will become important shortly. If you paid attention, you will notice that this is the same block of code I used above to fix the bug.
The bug then begins its journey at the Sonic_UpdateSpindash routine:
; loc_1AC8E: Sonic_UpdateSpindash: [...] moveq #0,d0 move.b spindash_counter(a0),d0 add.w d0,d0 move.w SpindashSpeeds(pc,d0.w),inertia(a0) ; <= (2) this line tst.b (Super_Sonic_flag).w beq.s + move.w SpindashSpeedsSuper(pc,d0.w),inertia(a0) ; <= (2) this line + [...] btst #0,status(a0) beq.s + neg.w inertia(a0) ; <= (2) this line + bset #2,status(a0) move.b #0,(Sonic_Dust+anim).w move.w #SndID_SpindashRelease,d0 ; spindash zoom sound jsr (PlaySound).l bra.s Obj01_Spindash_ResetScr ; <= (2) this line
The spindash code unleashes the spindash by setting Sonic's inertia, with the intent that Obj01_UpdateSpeedOnGround will correctly set the X and Y components of Sonic's velocity. At the end of the block, the jump to Obj01_Spindash_ResetScr will eventually reach label loc_1AD8C; this information is also going to become important shortly.
The third important part is in the Sonic_LevelBound routine:
; loc_1A974: Sonic_LevelBound: move.l x_pos(a0),d1 move.w x_vel(a0),d0 ; <= (3) this line ext.l d0 ; <= (3) this line asl.l #8,d0 ; <= (3) this line add.l d0,d1 ; <= (3) this line swap d1 move.w (Camera_Min_X_pos).w,d0 addi.w #$10,d0 cmp.w d1,d0 ; has Sonic touched the left boundary? bhi.s Sonic_Boundary_Sides ; if yes, branch move.w (Camera_Max_X_pos).w,d0 addi.w #$128,d0 tst.b (Current_Boss_ID).w bne.s + addi.w #$40,d0 + cmp.w d1,d0 ; has Sonic touched the right boundary? bls.s Sonic_Boundary_Sides ; if yes, branch [...] ; loc_1A9BA: Sonic_Boundary_Sides: move.w d0,x_pos(a0) move.w #0,2+x_pos(a0) ; subpixel x move.w #0,x_vel(a0) ; <= (3) this line move.w #0,inertia(a0) ; <= (3) this line bra.s Sonic_Boundary_CheckBottom
In the marked lines, the X velocity is added to the X coordinate. This is later used to see if the screen boundary will be reached this frame and, if so, kill the horizontal speed.
Now go to the Obj01_MdNormal routine:
Obj01_MdNormal: bsr.w Sonic_CheckSpindash ; <= (2) (3) this line bsr.w Sonic_Jump bsr.w Sonic_SlopeResist bsr.w Sonic_Move ; <= (1) this line bsr.w Sonic_Roll bsr.w Sonic_LevelBound jsr (ObjectMove).l bsr.w AnglePos bsr.w Sonic_SlopeRepel return_1A2DE: rts
This is called when Sonic is on the ground and not rolling. It is the only routine during which you can charge or release a spindash. In fact, the first thing it does is check if the spindash should be/is being charged or is being released. Moreover, there is code inside Sonic_CheckSpindash that will cause it to not to return to this function (thus skipping all following function calls) if a spindash is being charged or released.
In any case, if you are charging or releasing a spindash, the Sonic_CheckSpindash routine eventually calls the Sonic_LevelBound and AnglePos routines. These are necessary when charging the spindash because these functions would be skipped otherwise, but the call to Sonic_LevelBound is harmful when releasing the spindash because Sonic's X speed will never be set from his inertia (which would normally happen in the lines marked (1)), as in Sonic_LevelBound the lines marked (3) will kill Sonic's speed and inertia if he are at the screen boundary. In this case, the important block of code from the call to Sonic_Move is the one I highlighted above, and the one I used to fix the bug. Note that actually calling Sonic_Move will cause a running frame to be displayed for 1 frame when releasing the spindash, among probably many other similar side-effects.
So the sequence is: spindash being unleashed sets your inertia (lines (2), above); Sonic_LevelBound, called from Sonic_CheckSpindash, kills speed and inertia if you are at the screen boundary (lines (3), above) since the horizontal speed is never set from the new inertia (lines (1), above). The result is that you get stuck at the screen boundary.