Actions

SCHG How-to

Add Spin Dash to Sonic 1/Part 3

From Sonic Retro

(Original guide by shobiz)
(Guide updated with GitHub additions by JGMR)

While the end result of following Lightning's (and by extension, DikinBaus') and Puto's guides is a near-perfect Spin Dash in Sonic 1, there are still a few things not right about it, so let's fix those.

Hivebrain's 2005 disassembly

This section is designed for users of the legacy Hivebrain 2005 disassembly of Sonic 1. For users of the GitHub disassembly of Sonic 1, head over to the GitHub section below.

Converting Sonic's art buffer to a DMA queue

This isn't exactly a bug per se, but the method used to port over the DMA queue to Sonic 1 in Puto's guide is slightly hack-ish. In stock Sonic 1, Sonic's sprites are copied over into a RAM buffer at $FFFFC800 before being DMAed to VRAM. This buffer occupies $300 bytes, so if we convert it to a DMA queue, besides getting a proper structure we also save $200 bytes of RAM (some of which will be used later on in this guide).

First, let's modify the actual queue routines to use this RAM space. So go to DMA_68KtoVRAM (which you should already have after following Puto's guide) and replace everything from there till the end of Process_DMA with:

; ---------------------------------------------------------------------------
; Subroutine to load VDP commands into the DMA transfer queue
;
; In case you wish to use this queue system outside of the spin dash, this is the
; registers in which it expects data in:
; d1.l: Address to data (In 68k address space)
; d2.w: Destination in VRAM
; d3.w: Length of data
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||


DMA_68KtoVRAM:				; XREF: LoadSonicDynPLC; LoadDustDynPLC
		movea.l	($FFFFC8FC).w,a1
		cmpa.w	#$C8FC,a1	; is the DMA queue full?
		beq.s	DMA_68KtoVRAM_Done ; return if there's no more room in the buffer

		; piece together some VDP commands and store them for later...
		move.w	#$9300,d0	; command to specify DMA transfer length & $00FF
		move.b	d3,d0
		move.w	d0,(a1)+	; store command

		move.w	#$9400,d0	; command to specify DMA transfer length & $FF00
		lsr.w	#8,d3
		move.b	d3,d0
		move.w	d0,(a1)+	; store command

		move.w	#$9500,d0	; command to specify source address & $0001FE
		lsr.l	#1,d1
		move.b	d1,d0
		move.w	d0,(a1)+	; store command

		move.w	#$9600,d0	; command to specify source address & $01FE00
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+	; store command

		move.w	#$9700,d0	; command to specify source address & $FE0000
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+	; store command

		andi.l	#$FFFF,d2	; command to specify destination address and begin DMA
		lsl.l	#2,d2
		lsr.w	#2,d2
		swap	d2
		ori.l	#$40000080,d2 ; set bits to specify VRAM transfer
		move.l	d2,(a1)+	; store command

		move.l	a1,($FFFFC8FC).w ; set the next free slot address
		cmpa.w	#$C8FC,a1	; has the end of the queue been reached?
		beq.s	DMA_68KtoVRAM_Done ; return if there's no more room in the buffer
		move.w	#0,(a1) ; put a stop token at the end of the used part of the buffer

DMA_68KtoVRAM_Done:			; XREF: DMA_68KtoVRAM
		rts
; End of function DMA_68KtoVRAM

; ---------------------------------------------------------------------------
; Process all VDP commands and then reset the DMA queue when it's done
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||


Process_DMA:
		lea	($C00004).l,a5
		lea	($FFFFC800).w,a1

Process_DMA_Loop:			; XREF: Process_DMA
		move.w	(a1)+,d0	; has a stop token been encountered?
		beq.s	Process_DMA_Done ; branch if we reached a stop token

		; issue a set of VDP commands...
		move.w	d0,(a5)		; transfer length
		move.w	(a1)+,(a5)	; transfer length
		move.w	(a1)+,(a5)	; source address
		move.w	(a1)+,(a5)	; source address
		move.w	(a1)+,(a5)	; source address
		move.w	(a1)+,(a5)	; destination
		move.w	(a1)+,(a5)	; destination
		cmpa.w	#$C8FC,a1	; has the end of the queue been reached?
		bne.s	Process_DMA_Loop ; loop if we haven't reached the end of the buffer

Process_DMA_Done:			; XREF: Process_DMA
		move.w	#0,($FFFFC800).w
		move.l	#$FFFFC800,($FFFFC8FC).w
		rts
; End of function Process_DMA

Next, let's modify Sonic's PLC routine to make use of this routine. Go to LoadSonicDynPLC and replace the entire routine with the Sonic 2 equivalent, slightly modified to make use of Sonic 1's PLC format:

LoadSonicDynPLC:			; XREF: Obj01_Control; et al
		moveq	#0,d0
		move.b	$1A(a0),d0	; load frame number
		cmp.b	($FFFFF766).w,d0
		beq.s	locret_13C96
		move.b	d0,($FFFFF766).w
		lea	(SonicDynPLC).l,a2
		add.w	d0,d0
		adda.w	(a2,d0.w),a2
		moveq	#0,d5
		move.b	(a2)+,d5
		subq.w	#1,d5
		bmi.s	locret_13C96
		move.w	#$F000,d4
		move.l	#Art_Sonic,d6

SPLC_ReadEntry:
		moveq	#0,d1
		move.b	(a2)+,d1
		lsl.w	#8,d1
		move.b	(a2)+,d1
		move.w	d1,d3
		lsr.w	#8,d3
		andi.w	#$F0,d3
		addi.w	#$10,d3
		andi.w	#$FFF,d1
		lsl.l	#5,d1
		add.l	d6,d1
		move.w	d4,d2
		add.w	d3,d4
		add.w	d3,d4
		jsr	(DMA_68KtoVRAM).l
		dbf	d5,SPLC_ReadEntry	; repeat for number of entries

locret_13C96:
		rts	
; End of function LoadSonicDynPLC

Finally, we have to make the game actually use these routines. Firstly, go to loc_CD4 and replace

		tst.b	($FFFFF767).w
		beq.s	loc_D50
		lea	($C00004).l,a5
		move.l	#$94019370,(a5)
		move.l	#$96E49500,(a5)
		move.w	#$977F,(a5)
		move.w	#$7000,(a5)
		move.w	#$83,($FFFFF640).w
		move.w	($FFFFF640).w,(a5)
		move.b	#0,($FFFFF767).w

with

		jsr	(Process_DMA).l

Make similar changes at loc_DAE, loc_EEE and loc_FAE. Secondly, go to loc_D50 and remove these two lines:

		move	#$83,($FFFFF640).w
		jsr	Process_DMA

Thirdly, go to Level_ClrVars3 and after the

		move.w	#$8ADF,($FFFFF624).w
		move.w	($FFFFF624).w,(a6)

add

		clr.w	($FFFFC800).w
		move.l	#$FFFFC800,($FFFFC8FC).w

While not necessary, we can find a similar code under End_ClrRam3:

		move.w	#$8ADF,($FFFFF624).w
		move.w	($FFFFF624).w,(a6)

add this code underneath it as well:

		clr.w	($FFFFC800).w
		move.l	#$FFFFC800,($FFFFC8FC).w

Finally, go to loc_47D4, and after

		jsr	Hud_Base

add

		clr.w	($FFFFC800).w
		move.l	#$FFFFC800,($FFFFC8FC).w

After this, the game will use the DMA queue for Sonic's art reloading, and we'll have $200 bytes of free RAM from $FFFFC900 to $FFFFCAFF.

Making the Spin Dash sound rev

This has always annoyed me a lot, but thanks to Xenowhirl's Sonic 2 sound driver disassembly and Tweaker's music guide, we can fix it. We need to use three RAM variables, so we can utilise our newly freed RAM. $FFFFC900 is a flag which is 1 if the Spin Dash sound was the last one played and 0 if it was not. $FFFFC901 is a timer, and $FFFFC902 is the pitch increase value.

First, go to loc_71BC8 and change it from

loc_71BC8:
		lea	$40(a6),a5
		tst.b	(a5)
		bpl.s	loc_71BD4
		jsr	sub_71C4E(pc)

to

loc_71BC8:
		tst.b	($FFFFC901).w
		beq.s	@cont
		subq.b	#1,($FFFFC901).w
		
@cont:
		lea	$40(a6),a5
		tst.b	(a5)
		bpl.s	loc_71BD4
		jsr	sub_71C4E(pc)

Then go to Sound_D1toDF and change if from

Sound_D1toDF:
		tst.b	$27(a6)
		bne.w	loc_722C6
		tst.b	4(a6)
		bne.w	loc_722C6
		tst.b	$24(a6)
		bne.w	loc_722C6
		movea.l	(Go_SoundIndex).l,a0
		sub.b	#$A1,d7
		bra	SoundEffects_Common

to

Sound_D1toDF:
		tst.b	$27(a6)
		bne.w	loc_722C6
		tst.b	4(a6)
		bne.w	loc_722C6
		tst.b	$24(a6)
		bne.w	loc_722C6
		clr.b	($FFFFC900).w
		cmp.b	#$D1,d7		; is this the Spin Dash sound?
		bne.s	@cont3	; if not, branch
		move.w	d0,-(sp)
		move.b	($FFFFC902).w,d0	; store extra frequency
		tst.b	($FFFFC901).w	; is the Spin Dash timer active?
		bne.s	@cont1		; if it is, branch
		move.b	#-1,d0		; otherwise, reset frequency (becomes 0 on next line)
		
@cont1:
		addq.b	#1,d0
		cmp.b	#$C,d0		; has the limit been reached?
		bcc.s	@cont2		; if it has, branch
		move.b	d0,($FFFFC902).w	; otherwise, set new frequency
		
@cont2:
		move.b	#1,($FFFFC900).w	; set flag
		move.b	#60,($FFFFC901).w	; set timer
		move.w	(sp)+,d0
		
@cont3:
		movea.l	(Go_SoundIndex).l,a0
		sub.b	#$A1,d7
		bra	SoundEffects_Common

After this, go to Sound_A0toCF and after

		tst.b	$24(a6)
		bne.w	loc_722C6

add

		clr.b	($FFFFC900).w

Then go to loc_7226E and replace

		movea.l	dword_722EC(pc,d3.w),a5

with

		lea	dword_722EC(pc),a5
		movea.l	(a5,d3.w),a5

Finally, go to loc_72276 and change

		move.w	(a1)+,8(a5)
		move.b	#1,$E(a5)

to

		move.w	(a1)+,8(a5)
		tst.b	($FFFFC900).w	; is the Spin Dash sound playing?
		beq.s	@cont		; if not, branch
		move.w	d0,-(sp)
		move.b	($FFFFC902).w,d0
		add.b	d0,8(a5)
		move.w	(sp)+,d0
		
@cont:
		move.b	#1,$E(a5)

and now the Spin Dash sound revs.

Horizontal scroll delay

Sonic 2's Spin Dash implements a horizontal scroll delay, while the ported one doesn't (yet). The basic idea is to check a certain flag, and if it's non-zero, base scrolling on Sonic's X position a certain number of frames ago rather than his current X position. (Note that this feature can cause some problems with object interaction). So go to loc2_1ACD0 and change

		move.w	d0,($FFFFEED0).w	; move to $EED0

to

		move.w	d0,($FFFFC904).w	; move to $C904

And then go to ScrollHoriz2 and change

ScrollHoriz2:
		move.w	($FFFFD008).w,d0
		sub.w	($FFFFF700).w,d0

to

ScrollHoriz2:				; XREF: ScrollHoriz
		move.w	($FFFFC904).w,d1
		beq.s	@cont1
		sub.w	#$100,d1
		move.w	d1,($FFFFC904).w
		moveq	#0,d1
		move.b	($FFFFC904).w,d1
		lsl.b	#2,d1
		addq.b	#4,d1
		move.w	($FFFFF7A8).w,d0
		sub.b	d1,d0
		lea	($FFFFCB00).w,a1
		move.w	(a1,d0.w),d0
		and.w	#$3FFF,d0
		bra.s	@cont2
		
@cont1:
		move.w	($FFFFD008).w,d0
		
@cont2:
		sub.w	($FFFFF700).w,d0

And then, to fix a bug with backwards horizontal scrolling, go to loc_65F6 and change it from

loc_65F6:				; XREF: ScrollHoriz2
		add.w	($FFFFF700).w,d0
		cmp.w	($FFFFF728).w,d0
		bgt.s	loc_65E4
		move.w	($FFFFF728).w,d0
		bra.s	loc_65E4

to

loc_65F6:				; XREF: ScrollHoriz2
		cmpi.w	#-$10,d0
		bgt.s	@cont
		move.w	#-$10,d0	
		
@cont:
		add.w	($FFFFF700).w,d0
		cmp.w	($FFFFF728).w,d0
		bgt.s	loc_65E4
		move.w	($FFFFF728).w,d0
		bra.s	loc_65E4

to limit the maximum number of pixels the camera can scroll back by in a single frame.

Vertical scroll delay

In stock Sonic 1, the camera scrolls vertically as soon as Sonic looks up or ducks. This can be a bit annoying for the Spin Dash, however, so let's implement the delay present in Sonic 2 by using $FFFFC903 as our delay counter. Firstly, to fix a bit of incorrect code, go to loc2_1AD78 and change

loc2_1AD78:
		addq.l	#4,sp			; Add 4 bytes to the stack return address to skip Sonic_Jump on next rts to Obj01_MdNormal, preventing conflicts with button presses.
		cmpi.w	#$60,($FFFFEED8).w	; $EED8 only ever seems
		beq.s	loc2_1AD8C		; to be used in Spin Dash
		bcc.s	loc2_1AD88
		addq.w	#4,($FFFFEED8).w

loc2_1AD88:
		subq.w	#2,($FFFFEED8).w

loc2_1AD8C:
		bsr.w	Sonic_LevelBound
		bsr.w	Sonic_AnglePos
		move.w	#$60,($FFFFF73E).w	; reset looking up/down
		rts

to

loc2_1AD78:
		addq.l	#4,sp			; Add 4 bytes to the stack return address to skip Sonic_Jump on next rts to Obj01_MdNormal, preventing conflicts with button presses.
		cmpi.w	#$60,($FFFFF73E).w
		beq.s	loc2_1AD8C
		bcc.s	loc2_1AD88
		addq.w	#4,($FFFFF73E).w

loc2_1AD88:
		subq.w	#2,($FFFFF73E).w

loc2_1AD8C:
		bsr.w	Sonic_LevelBound
		bsr.w	Sonic_AnglePos
		;move.w	#$60,($FFFFF73E).w	; reset looking up/down
		rts

Then go to Sonic_LookUp and change

Sonic_LookUp:
		btst	#0,($FFFFF602).w ; is up being pressed?
		beq.s	Sonic_Duck	; if not, branch
		move.b	#7,$1C(a0)	; use "looking up" animation
		cmpi.w	#$C8,($FFFFF73E).w
		beq.s	loc_12FC2
		addq.w	#2,($FFFFF73E).w
		bra.s	loc_12FC2
; ===========================================================================

Sonic_Duck:
		btst	#1,($FFFFF602).w ; is down being pressed?
		beq.s	Obj01_ResetScr	; if not, branch
		move.b	#8,$1C(a0)	; use "ducking"	animation
		cmpi.w	#8,($FFFFF73E).w
		beq.s	loc_12FC2
		subq.w	#2,($FFFFF73E).w
		bra.s	loc_12FC2
; ===========================================================================

Obj01_ResetScr:
		cmpi.w	#$60,($FFFFF73E).w ; is	screen in its default position?
		beq.s	loc_12FC2	; if yes, branch
		bcc.s	loc_12FBE
		addq.w	#4,($FFFFF73E).w ; move	screen back to default

loc_12FBE:
		subq.w	#2,($FFFFF73E).w ; move	screen back to default

to

Sonic_LookUp:
		btst	#0,($FFFFF602).w ; is up being pressed?
		beq.s	Sonic_Duck	; if not, branch
		move.b	#7,$1C(a0)	; use "looking up" animation
		addq.b	#1,($FFFFC903).w
		cmp.b	#$78,($FFFFC903).w
		bcs.s	Obj01_ResetScr_Part2
		move.b	#$78,($FFFFC903).w
		cmpi.w	#$C8,($FFFFF73E).w
		beq.s	loc_12FC2
		addq.w	#2,($FFFFF73E).w
		bra.s	loc_12FC2
; ===========================================================================

Sonic_Duck:
		btst	#1,($FFFFF602).w ; is down being pressed?
		beq.s	Obj01_ResetScr	; if not, branch
		move.b	#8,$1C(a0)	; use "ducking"	animation
		addq.b	#1,($FFFFC903).w
		cmpi.b	#$78,($FFFFC903).w
		bcs.s	Obj01_ResetScr_Part2
		move.b	#$78,($FFFFC903).w
		cmpi.w	#8,($FFFFF73E).w
		beq.s	loc_12FC2
		subq.w	#2,($FFFFF73E).w
		bra.s	loc_12FC2
; ===========================================================================

Obj01_ResetScr:
		move.b	#0,($FFFFC903).w
		
Obj01_ResetScr_Part2:
		cmpi.w	#$60,($FFFFF73E).w ; is	screen in its default position?
		beq.s	loc_12FC2	; if yes, branch
		bcc.s	loc_12FBE
		addq.w	#4,($FFFFF73E).w ; move	screen back to default

loc_12FBE:
		subq.w	#2,($FFFFF73E).w ; move	screen back to default

Finally, to make the screen bias gradually reset during rolling, go to loc_131CC and change

loc_131CC:
		move.b	$26(a0),d0
		jsr	(CalcSine).l

to

loc_131CC:
		cmp.w	#$60,($FFFFF73E).w
		beq.s	@cont2
		bcc.s	@cont1
		addq.w	#4,($FFFFF73E).w
		
@cont1:
		subq.w	#2,($FFFFF73E).w
		
@cont2:
		move.b	$26(a0),d0
		jsr	(CalcSine).l

And that's it! Let me know if I made any mistakes, or if there's anything still not covered. Here's a ROM after all these steps have been done, and here's the source code (with Puto's SEGA sound fix implemented instead of Esrael's for ASM68k compatibility).

GitHub disassembly

This section is designed for those using the GitHub disassembly of Sonic 1. It will assume that you have followed DikinBaus' revamped guide and the GitHub version of Part 2 in that order. The following codes are designed for the AS version of the Sonic 1 GitHub disassembly.

Converting Sonic's art buffer to a DMA queue

This isn't exactly a bug per se, but the method used to port over the DMA queue to Sonic 1 in Puto's guide is slightly hack-ish. In stock Sonic 1, Sonic's sprites are copied over into a RAM buffer at v_sgfx_buffer before being DMAed to VRAM. This buffer occupies $300 bytes, so if we convert it to a DMA queue, besides getting a proper structure we also save $200 bytes of RAM (some of which will be used later on in this guide).

First, let's modify the actual queue routines to use this RAM space. So open up "DMA_68KtoVRAM.asm" under _inc (which you should already have after following Puto's guide) and replace everything from there till the end of Process_DMA with:

; ---------------------------------------------------------------------------
; Subroutine to load VDP commands into the DMA transfer queue
;
; In case you wish to use this queue system outside of the spin dash, this is the
; registers in which it expects data in:
; d1.l: Address to data (In 68k address space)
; d2.w: Destination in VRAM
; d3.w: Length of data
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||


DMA_68KtoVRAM:				; XREF: LoadSonicDynPLC; LoadDustDynPLC
		movea.l	(v_sgfx_buffer+$FC).w,a1
		cmpa.w	#v_sgfx_buffer+$FC,a1	; is the DMA queue full?
		beq.s	DMA_68KtoVRAM_Done ; return if there's no more room in the buffer

		; piece together some VDP commands and store them for later...
		move.w	#$9300,d0	; command to specify DMA transfer length & $00FF
		move.b	d3,d0
		move.w	d0,(a1)+	; store command

		move.w	#$9400,d0	; command to specify DMA transfer length & $FF00
		lsr.w	#8,d3
		move.b	d3,d0
		move.w	d0,(a1)+	; store command

		move.w	#$9500,d0	; command to specify source address & $0001FE
		lsr.l	#1,d1
		move.b	d1,d0
		move.w	d0,(a1)+	; store command

		move.w	#$9600,d0	; command to specify source address & $01FE00
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+	; store command

		move.w	#$9700,d0	; command to specify source address & $FE0000
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+	; store command

		andi.l	#$FFFF,d2	; command to specify destination address and begin DMA
		lsl.l	#2,d2
		lsr.w	#2,d2
		swap	d2
		ori.l	#$40000080,d2 ; set bits to specify VRAM transfer
		move.l	d2,(a1)+	; store command

		move.l	a1,(v_sgfx_buffer+$FC).w ; set the next free slot address
		cmpa.w	#v_sgfx_buffer+$FC,a1	; has the end of the queue been reached?
		beq.s	DMA_68KtoVRAM_Done ; return if there's no more room in the buffer
		move.w	#0,(a1) ; put a stop token at the end of the used part of the buffer

DMA_68KtoVRAM_Done:			; XREF: DMA_68KtoVRAM
		rts
; End of function DMA_68KtoVRAM

; ---------------------------------------------------------------------------
; Process all VDP commands and then reset the DMA queue when it's done
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||


Process_DMA:
		lea	(vdp_control_port).l,a5
		lea	(v_sgfx_buffer).w,a1

Process_DMA_Loop:			; XREF: Process_DMA
		move.w	(a1)+,d0	; has a stop token been encountered?
		beq.s	Process_DMA_Done ; branch if we reached a stop token

		; issue a set of VDP commands...
		move.w	d0,(a5)		; transfer length
		move.w	(a1)+,(a5)	; transfer length
		move.w	(a1)+,(a5)	; source address
		move.w	(a1)+,(a5)	; source address
		move.w	(a1)+,(a5)	; source address
		move.w	(a1)+,(a5)	; destination
		move.w	(a1)+,(a5)	; destination
		cmpa.w	#v_sgfx_buffer+$FC,a1	; has the end of the queue been reached?
		bne.s	Process_DMA_Loop ; loop if we haven't reached the end of the buffer

Process_DMA_Done:			; XREF: Process_DMA
		move.w	#0,(v_sgfx_buffer).w
		move.l	#v_sgfx_buffer,(v_sgfx_buffer+$FC).w
		rts
; End of function Process_DMA

Next, let's modify Sonic's PLC routine to make use of this routine. Open up "Sonic LoadGfx.asm" under _incObj and replace the entire routine with the Sonic 2 equivalent, slightly modified to make use of Sonic 1's PLC format:

; ---------------------------------------------------------------------------
; Sonic	graphics loading subroutine
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B	R O U T	I N E |||||||||||||||||||||||||||||||||||||||


Sonic_LoadGfx:
		moveq	#0,d0
		move.b	obFrame(a0),d0	; load frame number
		cmp.b	(v_sonframenum).w,d0 ; has frame changed?
		beq.s	.nochange	; if not, branch

		move.b	d0,(v_sonframenum).w
		lea	(SonicDynPLC).l,a2 ; load PLC script
		add.w	d0,d0
		adda.w	(a2,d0.w),a2
		moveq	#0,d5
		move.b	(a2)+,d5	; read "number of entries" value
		subq.w	#1,d5
		bmi.s	.nochange	; if zero, branch
		move.w	#$F000,d4
		move.l	#Art_Sonic,d6

.readentry:
		moveq	#0,d1
		move.b	(a2)+,d1
		lsl.w	#8,d1
		move.b	(a2)+,d1
		move.w	d1,d3
		lsr.w	#8,d3
		andi.w	#$F0,d3
		addi.w	#$10,d3
		andi.w	#$FFF,d1
		lsl.l	#5,d1
		add.l	d6,d1		; in Sonic 2, this would house Sonic's art tiles
		move.w	d4,d2
		add.w	d3,d4
		add.w	d3,d4
		jsr	(DMA_68KtoVRAM).l
		dbf	d5,.readentry	; repeat for number of entries

.nochange:
		rts	

; End of function Sonic_LoadGfx

Finally, we have to make the game actually use these routines. Firstly, go back to "sonic.asm" and find the label .waterbelow under VBla_08 and replace

		tst.b	(f_sonframechg).w ; has Sonic's sprite changed?
		beq.s	.nochg		; if not, branch

		writeVRAM	v_sgfx_buffer,ArtTile_Sonic*tile_size ; load new Sonic gfx
		move.b	#0,(f_sonframechg).w

with

		jsr	(Process_DMA).l

Make similar changes at VBla_0A, .waterbelow (under VBla_0C) and VBla_16. Secondly, go to .nochg under VBla_08 and remove these two lines:

		move.w	#$83,(v_vdp_buffer2).w
		jsr	(Process_DMA).l

Thirdly, go to Level_ClrRam and after the

		move.w	#$8A00+223,(v_hbla_hreg).w ; set palette change position (for water)
		move.w	(v_hbla_hreg).w,(a6)

add

		clr.w	(v_sgfx_buffer).w
		move.l	#v_sgfx_buffer,(v_sgfx_buffer+$FC).w

While not necessary, we can find a similar code under GM_Ending:

		move.w	#$8A00+223,(v_hbla_hreg).w ; set palette change position (for water)
		move.w	(v_hbla_hreg).w,(a6)

add this code underneath it as well:

		clr.w	(v_sgfx_buffer).w
		move.l	#v_sgfx_buffer,(v_sgfx_buffer+$FC).w

Finally, go to loc_47D4, and after

		jsr	(Hud_Base).l

add

		clr.w	(v_sgfx_buffer).w
		move.l	#v_sgfx_buffer,(v_sgfx_buffer+$FC).w

After this, the game will use the DMA queue for Sonic's art reloading, and we'll have $200 bytes of free RAM from $FFFFC900 to $FFFFCAFF.

Making the Spin Dash sound rev

This has always annoyed me a lot, but thanks to Xenowhirl's Sonic 2 sound driver disassembly and Tweaker's music guide, we can fix it. We need to use three RAM variables, so we can utilise our newly freed RAM. v_sgfx_buffer+$100 at address $FFFFC900 is a flag which is 1 if the Spin Dash sound was the last one played and 0 if it was not. v_sgfx_buffer+$101 at address $FFFFC901 is a timer, and v_sgfx_buffer+$102 at address $FFFFC902 is the pitch increase value.

First, open up "s1.sounddriver.asm" and go to .nonewsound. Change it from

; loc_71BC8:
.nonewsound:
		lea	SMPS_RAM.v_music_dac_track(a6),a5
		tst.b	SMPS_Track.PlaybackControl(a5)	; Is DAC track playing?
		bpl.s	.dacdone			; Branch if not
		jsr	DACUpdateTrack(pc)

to

; loc_71BC8:
.nonewsound:
		tst.b	(v_sgfx_buffer+$101).w
		beq.s	.cont
		subq.b	#1,(v_sgfx_buffer+$101).w

.cont:
		lea	SMPS_RAM.v_music_dac_track(a6),a5
		tst.b	SMPS_Track.PlaybackControl(a5)	; Is DAC track playing?
		bpl.s	.dacdone			; Branch if not
		jsr	DACUpdateTrack(pc)

Then go to Sound_PlayMoreSFX and change it from

; ---------------------------------------------------------------------------
; Play extra sound effect
; ---------------------------------------------------------------------------
; Sound_D1toDF:
Sound_PlayMoreSFX:
		tst.b	SMPS_RAM.f_1up_playing(a6)	; Is 1-up playing?
		bne.w	clear_sndprio			; Exit is it is
		tst.b	SMPS_RAM.v_fadeout_counter(a6)	; Is music being faded out?
		bne.w	clear_sndprio			; Exit if it is
		tst.b	SMPS_RAM.f_fadein_flag(a6)	; Is music being faded in?
		bne.w	clear_sndprio			; Exit if it is
		movea.l	(Go_ExtSoundIndex).l,a0
		subi.b	#ext__First,d7			; Make it 0-based
		lsl.w	#2,d7			; Convert sfx ID into index
		bra.w	sfx_common

to

; ---------------------------------------------------------------------------
; Play extra sound effect
; ---------------------------------------------------------------------------
; Sound_D1toDF:
Sound_PlayMoreSFX:
		tst.b	SMPS_RAM.f_1up_playing(a6)	; Is 1-up playing?
		bne.w	clear_sndprio			; Exit is it is
		tst.b	SMPS_RAM.v_fadeout_counter(a6)	; Is music being faded out?
		bne.w	clear_sndprio			; Exit if it is
		tst.b	SMPS_RAM.f_fadein_flag(a6)	; Is music being faded in?
		bne.w	clear_sndprio			; Exit if it is
		clr.b	(v_sgfx_buffer+$100).w
		cmp.b	#sfx_SpinDash,d7		; is this the Spin Dash sound?
		bne.s	.sfx_notSDash	; if not, branch
		move.w	d0,-(sp)
		move.b	(v_sgfx_buffer+$102).w,d0	; store extra frequency
		tst.b	(v_sgfx_buffer+$101).w	; is the Spin Dash timer active?
		bne.s	.sfx_timeractive		; if it is, branch
		move.b	#-1,d0		; otherwise, reset frequency (becomes 0 on next line)
		
.sfx_timeractive:
		addq.b	#1,d0
		cmp.b	#$C,d0		; has the limit been reached?
		bcc.s	.sfx_limitreached		; if it has, branch
		move.b	d0,(v_sgfx_buffer+$102).w	; otherwise, set new frequency
		
.sfx_limitreached:
		move.b	#1,(v_sgfx_buffer+$100).w	; set flag
		move.b	#60,(v_sgfx_buffer+$101).w	; set timer
		move.w	(sp)+,d0
		
.sfx_notSDash:
		movea.l	(Go_ExtSoundIndex).l,a0
		subi.b	#ext__First,d7			; Make it 0-based
		lsl.w	#2,d7			; Convert sfx ID into index
		bra.w	sfx_common

After this, go to Sound_PlaySFX and after

		tst.b	SMPS_RAM.f_fadein_flag(a6)	; Is music being faded in?
		bne.w	clear_sndprio			; Exit if it is

add

		clr.b	(v_sgfx_buffer+$100).w

Then go to .sfxoverridedone and replace

		movea.l	SFX_SFXChannelRAM(pc,d3.w),a5

with

		lea	SFX_SFXChannelRAM(pc),a5
		movea.l	(a5,d3.w),a5

Finally, go to .clearsfxtrackram and change

		move.w	(a1)+,SMPS_Track.Transpose(a5)		; load FM/PSG channel modifier
		move.b	#1,SMPS_Track.DurationTimeout(a5)	; Set duration of first "note"

to

		move.w	(a1)+,SMPS_Track.Transpose(a5)		; load FM/PSG channel modifier
		tst.b	(v_sgfx_buffer+$100).w	; is the Spin Dash sound playing?
		beq.s	.cont		; if not, branch
		move.w	d0,-(sp)
		move.b	(v_sgfx_buffer+$102).w,d0
		add.b	d0,8(a5)
		move.w	(sp)+,d0

.cont:
		move.b	#1,SMPS_Track.DurationTimeout(a5)	; Set duration of first "note"

and now the Spin Dash sound revs.

Horizontal scroll delay

Sonic 2's Spin Dash implements a horizontal scroll delay, while the ported one doesn't (yet). The basic idea is to check a certain flag, and if it's non-zero, base scrolling on Sonic's X position a certain number of frames ago rather than his current X position (Note that this feature can cause some problems with object interaction). So open up "Sonic Spindash.asm" under _incObj and find the label Sonic_UpdateSpindash. Change this line

		move.w	d0,($FFFFEED0).w	; move to $EED0

to

		move.w	d0,(v_sgfx_buffer+$104).w	; move to $C904

And then open up "DeformLayers (JP1).asm" (or "DeformLayers.asm" if you have selected REV00 revision) and go to MoveScreenHoriz. Change

MoveScreenHoriz:
		move.w	(v_player+obX).w,d0
		sub.w	(v_screenposx).w,d0 ; Sonic's distance from left edge of screen

to

MoveScreenHoriz:
		move.w	(v_sgfx_buffer+$104).w,d1
		beq.s	.cont1
		sub.w	#$100,d1
		move.w	d1,(v_sgfx_buffer+$104).w
		moveq	#0,d1
		move.b	(v_sgfx_buffer+$104).w,d1
		lsl.b	#2,d1
		addq.b	#4,d1
		move.w	(v_trackpos).w,d0
		sub.b	d1,d0
		lea	(v_tracksonic).w,a1
		move.w	(a1,d0.w),d0
		and.w	#$3FFF,d0
		bra.s	.cont2

	.cont1:
		move.w	(v_player+obX).w,d0

	.cont2:
		sub.w	(v_screenposx).w,d0 ; Sonic's distance from left edge of screen

And then, to fix a bug with backwards horizontal scrolling, go to SH_BehindMid and change it from

SH_BehindMid:
		add.w	(v_screenposx).w,d0
		cmp.w	(v_limitleft2).w,d0
		bgt.s	SH_SetScreen
		move.w	(v_limitleft2).w,d0
		bra.s	SH_SetScreen

to

SH_BehindMid:
		cmpi.w	#-$10,d0
		bgt.s	.cont
		move.w	#-$10,d0

	.cont:
		add.w	(v_screenposx).w,d0
		cmp.w	(v_limitleft2).w,d0
		bgt.s	SH_SetScreen
		move.w	(v_limitleft2).w,d0
		bra.s	SH_SetScreen

to limit the maximum number of pixels the camera can scroll back by in a single frame.

Vertical scroll delay

In stock Sonic 1, the camera scrolls vertically as soon as Sonic looks up or ducks. This can be a bit annoying for the Spin Dash, however, so let's implement the delay present in Sonic 2 by using v_sgfx_buffer+$103 at address $FFFFC903 as our delay counter. Firstly, to fix a bit of incorrect code, go to Sonic_Spindash_ResetScr and change

Sonic_Spindash_ResetScr:
		addq.l	#4,sp		; increase stack ptr
		cmpi.w	#(224/2)-16,($FFFFEED8).w	; $EED8 only ever seems to be used in Spin Dash
		beq.s	loc_1AD8C
		bhs.s	+
		addq.w	#4,($FFFFEED8).w
+		subq.w	#2,($FFFFEED8).w

loc_1AD8C:
		bsr.w	Sonic_LevelBound
		bsr.w	Sonic_AnglePos
		move.w	#$60,(v_lookshift).w	; reset looking up/down
		rts

to

Sonic_Spindash_ResetScr:
		addq.l	#4,sp		; increase stack ptr
		cmpi.w	#(224/2)-16,(v_lookshift).w
		beq.s	loc_1AD8C
		bhs.s	+
		addq.w	#4,(v_lookshift).w
+		subq.w	#2,(v_lookshift).w

loc_1AD8C:
		bsr.w	Sonic_LevelBound
		bsr.w	Sonic_AnglePos
		rts

Then open up "Sonic Move.asm" under _incObj, go to Sonic_LookUp and change

Sonic_LookUp:
		btst	#bitUp,(v_jpadhold2).w ; is up being pressed?
		beq.s	Sonic_Duck	; if not, branch
		move.b	#id_LookUp,obAnim(a0) ; use "looking up" animation
		cmpi.w	#$C8,(v_lookshift).w
		beq.s	loc_12FC2
		addq.w	#2,(v_lookshift).w
		bra.s	loc_12FC2
; ===========================================================================

Sonic_Duck:
		btst	#bitDn,(v_jpadhold2).w ; is down being pressed?
		beq.s	Sonic_ResetScr	; if not, branch
		move.b	#id_Duck,obAnim(a0) ; use "ducking" animation
		cmpi.w	#8,(v_lookshift).w
		beq.s	loc_12FC2
		subq.w	#2,(v_lookshift).w
		bra.s	loc_12FC2
; ===========================================================================

Sonic_ResetScr:
		cmpi.w	#$60,(v_lookshift).w ; is screen in its default position?
		beq.s	loc_12FC2	; if yes, branch
		bcc.s	loc_12FBE
		addq.w	#4,(v_lookshift).w ; move screen back to default

loc_12FBE:
		subq.w	#2,(v_lookshift).w ; move screen back to default

to

Sonic_LookUp:
		btst	#bitUp,(v_jpadhold2).w ; is up being pressed?
		beq.s	Sonic_Duck	; if not, branch
		move.b	#id_LookUp,obAnim(a0) ; use "looking up" animation
		addq.b	#1,(v_sgfx_buffer+$103).w
		cmp.b	#$78,(v_sgfx_buffer+$103).w
		bcs.s	Sonic_ResetScr_Part2
		move.b	#$78,(v_sgfx_buffer+$103).w
		cmpi.w	#$C8,(v_lookshift).w
		beq.s	loc_12FC2
		addq.w	#2,(v_lookshift).w
		bra.s	loc_12FC2
; ===========================================================================

Sonic_Duck:
		btst	#bitDn,(v_jpadhold2).w ; is down being pressed?
		beq.s	Sonic_ResetScr	; if not, branch
		move.b	#id_Duck,obAnim(a0) ; use "ducking" animation
		addq.b	#1,(v_sgfx_buffer+$103).w
		cmpi.b	#$78,(v_sgfx_buffer+$103).w
		bcs.s	Sonic_ResetScr_Part2
		move.b	#$78,(v_sgfx_buffer+$103).w
		cmpi.w	#8,(v_lookshift).w
		beq.s	loc_12FC2
		subq.w	#2,(v_lookshift).w
		bra.s	loc_12FC2
; ===========================================================================

Sonic_ResetScr:
		move.b	#0,(v_sgfx_buffer+$103).w

Sonic_ResetScr_Part2:
		cmpi.w	#$60,(v_lookshift).w ; is screen in its default position?
		beq.s	loc_12FC2	; if yes, branch
		bcc.s	loc_12FBE
		addq.w	#4,(v_lookshift).w ; move screen back to default

loc_12FBE:
		subq.w	#2,(v_lookshift).w ; move screen back to default

Finally, to make the screen bias gradually reset during rolling, open up "Sonic RollSpeed.asm" under _incObj, go to loc_131CC and change

loc_131CC:
		move.b	obAngle(a0),d0
		jsr	(CalcSine).l

to

loc_131CC:
		cmp.w	#$60,(v_lookshift).w
		beq.s	.cont2
		bcc.s	.cont1
		addq.w	#4,(v_lookshift).w

	.cont1:
		subq.w	#2,(v_lookshift).w

	.cont2:
		move.b	obAngle(a0),d0
		jsr	(CalcSine).l

And that's it! Let me know if I made any mistakes, or if there's anything still not covered. Here's a ROM after all these steps have been done.

Final code

Here is the final Spin Dash code for reference:

Hivebrain 2005 disassembly

; ---------------------------------------------------------------------------
; Subroutine to make Sonic perform a spindash
; ---------------------------------------------------------------------------
 
; ||||||||||||||| S U B	R O U T	I N E |||||||||||||||||||||||||||||||||||||||
 
 
Sonic_SpinDash:
		tst.b	$39(a0)			; already Spin Dashing?
		bne.s	loc2_1AC8E		; if set, branch
		cmpi.b	#8,$1C(a0)		; is anim duck
		bne.s	locret2_1AC8C		; if not, return
		move.b	($FFFFF603).w,d0	; read controller
		andi.b	#$70,d0			; pressing A/B/C ?
		beq.w	locret2_1AC8C		; if not, return
		move.b	#$1F,$1C(a0)		; set Spin Dash anim (9 in s2)
		move.w	#$D1,d0			; spin sound ($E0 in s2)
		jsr	(PlaySound_Special).l	; play spin sound
		addq.l	#4,sp			; Add 4 bytes to the stack return address to skip Sonic_Jump on next rts to Obj01_MdNormal, preventing conflicts with button presses.
		move.b	#1,$39(a0)		; set Spin Dash flag
		move.w	#0,$3A(a0)		; set charge count to 0
		cmpi.w	#$C,($FFFFFE14).w	; check air remaining
		bcs.s	loc2_1AC84		; if he's drowning, branch to not make dust
		move.b	#2,($FFFFD1DC).w	; Set the Spin Dash dust animation to $2.

loc2_1AC84:
		bsr.w	Sonic_LevelBound
		bsr.w	Sonic_AnglePos

locret2_1AC8C:
		rts	
; ---------------------------------------------------------------------------

loc2_1AC8E:
		move.b	#$1F,$1C(a0)	; fixes the animation bug when spin-dashing near a monitor
		move.b	($FFFFF602).w,d0	; read controller
		btst	#1,d0			; check down button
		bne.w	loc2_1AD30		; if set, branch
		move.b	#$E,$16(a0)		; $16(a0) is height/2
		move.b	#7,$17(a0)		; $17(a0) is width/2
		move.b	#2,$1C(a0)		; set animation to roll
		addq.w	#5,$C(a0)		; $C(a0) is Y coordinate
		move.b	#0,$39(a0)		; clear Spin Dash flag
		moveq	#0,d0
		move.b	$3A(a0),d0		; copy charge count
		add.w	d0,d0			; double it
		move.w	Dash_Speeds(pc,d0.w),$14(a0) ; get normal speed
		move.w	$14(a0),d0		; get inertia
		subi.w	#$800,d0		; subtract $800
		add.w	d0,d0			; double it
		andi.w	#$1F00,d0		; mask it against $1F00
		neg.w	d0			; negate it
		addi.w	#$2000,d0		; add $2000
		move.w	d0,($FFFFC904).w	; move to $C904
		btst	#0,$22(a0)		; is sonic facing right?
		beq.s	loc2_1ACF4		; if not, branch
		neg.w	$14(a0)			; negate inertia

loc2_1ACF4:
		bset	#2,$22(a0)		; set unused (in s1) flag
		move.b	#0,($FFFFD1DC).w	; clear Spin Dash dust animation.
		move.w	#$BC,d0			; spin release sound
		jsr	(PlaySound_Special).l	; play it!
		bra.s	loc2_1AD78
; ===========================================================================
Dash_Speeds:
		dc.w  $800		; 0
		dc.w  $880		; 1
		dc.w  $900		; 2
		dc.w  $980		; 3
		dc.w  $A00		; 4
		dc.w  $A80		; 5
		dc.w  $B00		; 6
		dc.w  $B80		; 7
		dc.w  $C00		; 8
; ===========================================================================

loc2_1AD30:				; If still charging the dash...
		tst.w	$3A(a0)		; check charge count
		beq.s	loc2_1AD48	; if zero, branch
		move.w	$3A(a0),d0	; otherwise put it in d0
		lsr.w	#5,d0		; shift right 5 (divide it by 32)
		sub.w	d0,$3A(a0)	; subtract from charge count
		bcc.s	loc2_1AD48	; ??? branch if carry clear
		move.w	#0,$3A(a0)	; set charge count to 0

loc2_1AD48:
		move.b	($FFFFF603).w,d0	; read controller
		andi.b	#$70,d0			; pressing A/B/C?
		beq.w	loc2_1AD78		; if not, branch
		move.w	#$1F00,$1C(a0)		; reset spdsh animation
		move.w	#$D1,d0			; was $E0 in sonic 2
		move.b	#2,($FFFFD1DC).w	; Set the Spin Dash dust animation to $2.
		jsr	(PlaySound_Special).l	; play charge sound
		addi.w	#$200,$3A(a0)		; increase charge count
		cmpi.w	#$800,$3A(a0)		; check if it's maxed
		bcs.s	loc2_1AD78		; if not, then branch
		move.w	#$800,$3A(a0)		; reset it to max

loc2_1AD78:
		addq.l	#4,sp			; Add 4 bytes to the stack return address to skip Sonic_Jump on next rts to Obj01_MdNormal, preventing conflicts with button presses.
		cmpi.w	#$60,($FFFFF73E).w
		beq.s	loc2_1AD8C
		bcc.s	loc2_1AD88
		addq.w	#4,($FFFFF73E).w

loc2_1AD88:
		subq.w	#2,($FFFFF73E).w

loc2_1AD8C:
		bsr.w	Sonic_LevelBound
		bsr.w	Sonic_AnglePos
		;move.w	#$60,($FFFFF73E).w	; reset looking up/down
		rts
; End of subroutine Sonic_SpinDash

GitHub disassembly

; ---------------------------------------------------------------------------
; Subroutine to check for starting to charge a spindash
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||


Sonic_SpinDash:
		tst.b	spindash_flag(a0)	; already Spin Dashing?
		bne.s	Sonic_UpdateSpindash	; if set, branch
		cmpi.b	#id_Duck,obAnim(a0)	; is anim duck?
		bne.s	return_1AC8C	; if not, return
		move.b	(v_jpadpress2).w,d0	; read controller
		andi.b	#btnB|btnC|btnA,d0	; pressing A/B/C ?
		beq.w	return_1AC8C	; if not, return
		move.b	#id_Spindash,obAnim(a0)	; set Spin Dash anim (9 in s2)
		move.w	#sfx_SpinDash,d0	; spin sound ($E0 in s2)
		jsr	(PlaySound_Special).l		; play spin sound
		addq.l	#4,sp		; increment stack ptr
		move.b	#1,spindash_flag(a0)	; set Spin Dash flag
		move.w	#0,spindash_counter(a0)	; set charge count to 0
		cmpi.w	#12,(v_air).w	; check air remaining
		blo.s	+			; if he's drowning, branch to not make dust
		move.b	#2,(v_spindust+obAniDust).w ; set Spin Dash dust anim to 2
+
		bsr.w	Sonic_LevelBound
		bsr.w	Sonic_AnglePos

return_1AC8C:
		rts
; End of function Sonic_SpinDash


; ---------------------------------------------------------------------------
; Subroutine to update an already-charging spindash
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||


Sonic_UpdateSpindash:
		move.b	#id_Spindash,obAnim(a0)	; set Spin Dash anim to fix the monitor bug
		move.b	(v_jpadhold2).w,d0	; read controller
		btst	#bitDn,d0			; check down button
		bne.w	Sonic_ChargingSpindash	; if set, branch

		; unleash the charged spindash and start rolling quickly:
		move.b	#$E,obHeight(a0)	; obHeight(a0) is height/2
		move.b	#7,obWidth(a0)		; obWidth(a0) is width/2
		move.b	#id_Roll,obAnim(a0)	; set animation to roll
		addq.w	#5,obY(a0)	; add the difference between Sonic's rolling and standing heights
		move.b	#0,spindash_flag(a0)	; clear Spin Dash flag
		moveq	#0,d0
		move.b	spindash_counter(a0),d0	; copy charge count
		add.w	d0,d0	; double it
		move.w	SpindashSpeeds(pc,d0.w),obInertia(a0)	; get spindash speed

		; Determine how long to lag the camera for.
		; Notably, the faster Sonic goes, the less the camera lags.
		; This is seemingly to prevent Sonic from going off-screen.
		move.w	obInertia(a0),d0	; get inertia
		subi.w	#$800,d0 ; $800 is the lowest spin dash speed
		add.w	d0,d0	; double it
		andi.w	#$1F00,d0 ; This line is not necessary, as none of the removed bits are ever set in the first place
		neg.w	d0		; negate it
		addi.w	#$2000,d0	; add $2000
		move.w	d0,(v_sgfx_buffer+$104).w	; move to $C904

		btst	#0,obStatus(a0)	; is sonic facing right?
		beq.s	+		; if not, branch
		neg.w	obInertia(a0)	; negate inertia
+
		bset	#2,obStatus(a0)	; set unused (in s1) flag
		move.b	#0,(v_spindust+obAniDust).w	; clear Spin Dash dust anim
		move.w	#sfx_Teleport,d0	; spindash zoom sound
		jsr	(PlaySound_Special).l	; play it!
		bra.s	Sonic_Spindash_ResetScr
; ===========================================================================
SpindashSpeeds:
		dc.w  $800	; 0
		dc.w  $880	; 1
		dc.w  $900	; 2
		dc.w  $980	; 3
		dc.w  $A00	; 4
		dc.w  $A80	; 5
		dc.w  $B00	; 6
		dc.w  $B80	; 7
		dc.w  $C00	; 8
; ===========================================================================

Sonic_ChargingSpindash:			; If still charging the dash...
		tst.w	spindash_counter(a0)	; check charge count
		beq.s	+				; if zero, branch
		move.w	spindash_counter(a0),d0	; otherwise put it in d0
		lsr.w	#5,d0			; shift right 5 (divide it by 32)
		sub.w	d0,spindash_counter(a0)	; subtract from charge count
		bcc.s	+				; ??? branch if carry clear
		move.w	#0,spindash_counter(a0)	; set charge count to 0
+
		move.b	(v_jpadpress2).w,d0	; read controller
		andi.b	#btnB|btnC|btnA,d0	; pressing A/B/C?
		beq.w	Sonic_Spindash_ResetScr	; if not, branch
		move.w	#(id_Spindash<<8)|(id_Walk<<0),obAnim(a0)	; reset spindash animation
		move.w	#sfx_SpinDash,d0	; was $E0 in sonic 2
		move.b	#2,(v_spindust+obAniDust).w	; set Spin Dash dust anim to 2
		jsr	(PlaySound_Special).l	; play charge sound
		addi.w	#$200,spindash_counter(a0)	; increase charge count
		cmpi.w	#$800,spindash_counter(a0)	; check if it's maxed
		blo.s	Sonic_Spindash_ResetScr	; if not, then branch
		move.w	#$800,spindash_counter(a0)	; reset it to max

Sonic_Spindash_ResetScr:
		addq.l	#4,sp		; increase stack ptr
		cmpi.w	#(224/2)-16,(v_lookshift).w
		beq.s	loc_1AD8C
		bhs.s	+
		addq.w	#4,(v_lookshift).w
+		subq.w	#2,(v_lookshift).w

loc_1AD8C:
		bsr.w	Sonic_LevelBound
		bsr.w	Sonic_AnglePos
		rts
; End of function Sonic_UpdateSpindash

Continuation

There is one final part added by Mercury that addresses some final camera bugs.

SCHG How-To Guide: Sonic the Hedgehog (16-bit)
Fixing Bugs
Fix Demo Playback | Fix a Race Condition with Pattern Load Cues | Fix the SEGA Sound | Display the Press Start Button Text | Fix the Level Select Menu | Fix the Hidden Points Bug | Fix Accidental Deletion of Scattered Rings | Fix Ring Timers | Fix the Walk-Jump Bug | Correct Drowning Bugs | Fix the Death Boundary Bug | Fix the Camera Follow Bug | Fix Song Restoration Bugs | Fix the HUD Blinking | Fix the Level Select Graphics Bug | Fix a remember sprite related bug
Changing Design Choices
Change Spike Behavior | Collide with Water After Being Hurt | Fix Special Stage Jumping Physics | Improve the Fade In\Fade Out Progression Routines | Fix Scattered Rings' Underwater Physics | Remove the Speed Cap | Port the REV01 Background Effects | Port Sonic 2's Level Art Loader | Retain Rings Between Acts | Add Sonic 2 (Simon Wai Prototype) Level Select | Improve ObjectMove Subroutines | Add Sonic 2 Level Select | Collide with Water After Being Hurt | Smooth Out Rotation in Special Stages
Adding Features
Add Spin Dash ( Part 1 (GitHub)/(Hivebrain) / Part 2 / Part 3 / Part 4 ) | Add Eggman Monitor | Add Super Sonic | Add Extended Camera | Add the Air Roll | Add 6-Button Support
Sound Features
Expand the Sound Index | Play Different Songs Per Act | Port Sonic 2 Final Sound Driver | Port Sonic 3's Sound Driver | Change The SEGA Sound | Correct PAL Music Tempo
Extending the Game
Load Chunks From ROM | Add Extra Characters | Make an Alternative Title Screen | Use Dynamic Tilesets | Make GHZ Load Alternate Art | Make Ending Load Alternate Art | Add a New Zone | Set Up the Goggle Monitor | Add New Moves | Add a Dynamic Collision System | Dynamic Special Stage Walls System | Extend Sprite Mappings and Art Limit | Enigma Credits | Use Dynamic Palettes
Miscellaneous
Convert the Hivebrain 2005 Disassembly to ASM68K
Split Disassembly Guides
Set Up a Split Disassembly | Basic Level Editing | Basic Art Editing | Basic ASM Editing (Spin Dash)

|Add Spin Dash to Sonic 1/Part 3]]