Actions

SCHG How-to

Add Spin Dash to Sonic 1/Part 2

From Sonic Retro

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

Here's a Sonic 1 + Spin Dash ROM built by following Lightning's guide (as well as DikinBaus' revamped guide). If you try it, you will be able to easily see that there are several things that keep it from being a perfect port.

For example, if you Spin Dash next to a monitor, you won't break it. Also, the sound effect, while fitting, isn't ideal; there's no dust; and if you Spin Dash while on a seesaw (SLZ), Strange Things™ will happen. So let's fix this.

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.

Problem 1: The Monitor Bug

When you try to Spin Dash next to a monitor, you won't be able to do that and simply stop when releasing. It only works if you walk a little bit away from the monitor and do the Spin Dash. To fix that, go to loc_A1EC. You should see something like this:

loc_A1EC:				; XREF: Obj26_Solid
		move.w	#$1A,d1
		move.w	#$F,d2
		bsr.w	Obj26_SolidSides
		beq.w	loc_A25C
		tst.w	$12(a1)
		bmi.s	loc_A20A
		cmpi.b	#2,$1C(a1)	; is Sonic rolling?
		beq.s	loc_A25C	; if yes, branch

By reading this code, you can see that there is a check for the rolling animation there. This is what causes the monitor bug. To fix it, simply add a second check for the Spin Dash animation after this one:

loc_A1EC:				; XREF: Obj26_Solid
		move.w	#$1A,d1
		move.w	#$F,d2
		bsr.w	Obj26_SolidSides
		beq.w	loc_A25C
		tst.w	$12(a1)
		bmi.s	loc_A20A
		cmpi.b	#2,$1C(a1)	; is Sonic rolling?
		beq.s	loc_A25C	; if yes, branch
		cmpi.b	#$1F,$1C(a1)	; is Sonic spin-dashing?
		beq.s	loc_A25C	; if yes, branch

Great, let's try it. It should be working and... oh dear.
This isn't right.

SpindashGuide Pic1.png

We're Spin Dashing, we're not supposed to use the pushing animation. To fix that, go to Sonic_SpinDash, and look at what it does.
These are the first lines:

Sonic_SpinDash:
		tst.b	$39(a0)			; already Spin Dashing?
		bne.s	loc2_1AC8E		; if set, branch

Since we know we're Spin Dashing when $39(a0) is set, let's make sure that the Spin Dash animation is used when that happens.
Therefore, go to loc2_1AC8E, and right after the label, add the following line:

		move.b	#$1F,$1C(a0)

Build the ROM again, and the monitor bug should be permanently taken care of. Here's a ROM after this step was carried out.

Problem 2: Spin Dash Sound Effect

Adding a sound effect to Sonic 1 isn't the most trivial of things to do. So be sure to follow these steps exactly.

First, we need a free slot. Since all the slots reserved for sound effects in Sonic 1 are taken, we therefore need to reserve extra slots for sound effects. The $D1-$DF area is free, so let's use that. Go to Sound_ChkValue, and you should be able to see this line there:

		cmpi.b	#$E0,d7
		bcs.w	Sound_D0toDF	; sound	$D0-$DF

Since only $D0 is actually used here, it's a rather large waste to keep the rest of the slots unused, so change that $E0 to $D1:

		cmpi.b	#$D1,d7
		bcs.w	Sound_D0toDF	; sound	$D0

And after that line, let's add our own comparison:

		cmpi.b	#$DF,d7
		blo.w	Sound_D1toDF	; sound	$D1-$DF

Now, we have to actually create this routine. So go to Sound_A0toCF, and see what it does:

Sound_A0toCF:				; XREF: Sound_ChkValue
		tst.b	$27(a6)
		bne.w	loc_722C6
		tst.b	4(a6)
		bne.w	loc_722C6
		tst.b	$24(a6)
		bne.w	loc_722C6
		cmpi.b	#$B5,d7		; is ring sound	effect played?
		bne.s	Sound_notB5	; if not, branch
		tst.b	$2B(a6)
		bne.s	loc_721EE
		move.b	#$CE,d7		; play ring sound in left speaker

loc_721EE:
		bchg	#0,$2B(a6)	; change speaker

Sound_notB5:
		cmpi.b	#$A7,d7		; is "pushing" sound played?
		bne.s	Sound_notA7	; if not, branch
		tst.b	$2C(a6)
		bne.w	locret_722C4
		move.b	#$80,$2C(a6)

Sound_notA7:
		movea.l	(Go_SoundIndex).l,a0
		subi.b	#$A0,d7
		lsl.w	#2,d7
		movea.l	(a0,d7.w),a3
		movea.l	a3,a1
		moveq	#0,d1
		move.w	(a1)+,d1
		add.l	a3,d1
		move.b	(a1)+,d5
		move.b	(a1)+,d7
		subq.b	#1,d7
		moveq	#$30,d6
		(...)

As you can see, this routine has a lot of checks for special sound effects, like the ring effect and the pushing effect.
We don't need that for our new routine, so trash all of that, and you end up with:

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
		subi.b	#$A0,d7
		lsl.w	#2,d7
		movea.l	(a0,d7.w),a3
		movea.l	a3,a1
		moveq	#0,d1
		move.w	(a1)+,d1
		add.l	a3,d1
		move.b	(a1)+,d5
		move.b	(a1)+,d7
		subq.b	#1,d7
		moveq	#$30,d6
		(...)

There's a problem here: If we subtract $A0 to get the current index, then the next entry in this index will be sound $D0, which is handled by a different routine, forcing us to have an unused entry in an index. So, change that line to subtract $A1 instead:

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
		subi.b	#$A1,d7
		lsl.w	#2,d7
		movea.l	(a0,d7.w),a3
		movea.l	a3,a1
		moveq	#0,d1
		move.w	(a1)+,d1
		add.l	a3,d1
		move.b	(a1)+,d5
		move.b	(a1)+,d7
		subq.b	#1,d7
		moveq	#$30,d6
		(...)

Also, it can be noted that any code after the $A1 line is common to the two routines.
So in the original routine, add a label right before "lsl.w #2,d7":

Sound_notA7:
		movea.l	(Go_SoundIndex).l,a0
		subi.b	#$A0,d7
SoundEffects_Common:
		lsl.w	#2,d7
		movea.l	(a0,d7.w),a3
		movea.l	a3,a1
		moveq	#0,d1
		move.w	(a1)+,d1
		add.l	a3,d1
		move.b	(a1)+,d5
		move.b	(a1)+,d7
		subq.b	#1,d7
		moveq	#$30,d6
		(...)

And we can now greatly reduce the size of our new routine:

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

So, after this is done, put the routine right above Sound_A0toCF.
Now, all we need is the actual ported spin-dash sound effect, which I provide here. Put this file in the sound\ directory of your source code, and go to the SoundD0 label. After the even, add the following lines:

SoundD1:	incbin	sound\soundD1.bin
		even

So the surrounding code should look like this:

SoundCF:	incbin	sound\soundCF.bin
		even
SoundD0:	incbin	sound\soundD0.bin
		even
SoundD1:	incbin	sound\soundD1.bin
		even		
SegaPCM:	incbin	sound\segapcm.bin
		even

And finally, add SoundD1 to the index:

SoundIndex:	dc.l SoundA0, SoundA1, SoundA2
		dc.l SoundA3, SoundA4, SoundA5
		dc.l SoundA6, SoundA7, SoundA8
		dc.l SoundA9, SoundAA, SoundAB
		dc.l SoundAC, SoundAD, SoundAE
		dc.l SoundAF, SoundB0, SoundB1
		dc.l SoundB2, SoundB3, SoundB4
		dc.l SoundB5, SoundB6, SoundB7
		dc.l SoundB8, SoundB9, SoundBA
		dc.l SoundBB, SoundBC, SoundBD
		dc.l SoundBE, SoundBF, SoundC0
		dc.l SoundC1, SoundC2, SoundC3
		dc.l SoundC4, SoundC5, SoundC6
		dc.l SoundC7, SoundC8, SoundC9
		dc.l SoundCA, SoundCB, SoundCC
		dc.l SoundCD, SoundCE, SoundCF
		dc.l SoundD1

Now, go to Sonic_SpinDash, and change all references to sound $BE to reference sound $D1 instead. For reference, here's the complete routine:

; ---------------------------------------------------------------------------
; 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.b	#$C,$28(a0)		; ??? oxygen remaining?
		bcs.s	loc2_1AC84		; ??? branch if carry
		move.b	#2,($FFFFD11C).w	; ??? $D11C is used for
						; the smoke/dust object
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,($FFFFEED0).w	; move to $EED0
		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,($FFFFD11C).w	; clear $D11C (smoke)
		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
		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,($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
; End of subroutine Sonic_SpinDash

So, problem solved. Here's a rom of the output after fixing this problem.

Problem 3: See-Saw Bug (AKA Sonic 2 Spin Dash Bug)

This problem occurs when you're Spin Dashing on a seesaw, and then you get thrown up by the seesaw.
You keep your Spin Dash state, and dash off immediately once you land. This is clearly not the wanted behaviour.
To fix this, go to Obj01_MdJump and Obj01_MdJump2, and add the following line at the beginning of each of the two routines:

		clr.b	$39(a0)

This should fix the see-saw bug. Here's the rom.

Problem 4: Spin Dash Dust

This is a big one. For starters, you need to port the Spin Dash dust object from Sonic 2.
For your convenience, here is the code to the ported object, paste it right before Obj01 (the sonic object):

SpinDash_dust:
Sprite_1DD20:				; DATA XREF: ROM:0001600C?o
		moveq	#0,d0
		move.b	$24(a0),d0
		move	off_1DD2E(pc,d0.w),d1
		jmp	off_1DD2E(pc,d1.w)
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
off_1DD2E:	dc loc_1DD36-off_1DD2E; 0 ; DATA XREF: h+6DBA?o h+6DBC?o ...
		dc loc_1DD90-off_1DD2E; 1
		dc loc_1DE46-off_1DD2E; 2
		dc loc_1DE4A-off_1DD2E; 3
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

loc_1DD36:				; DATA XREF: h+6DBA?o
		addq.b	#2,$24(a0)
		move.l	#MapUnc_1DF5E,4(a0)
		or.b	#4,1(a0)
		move.b	#1,$18(a0)
		move.b	#$10,$19(a0)
		move	#$7A0,2(a0)
		move	#-$3000,$3E(a0)
		move	#$F400,$3C(a0)
		cmp	#-$2E40,a0
		beq.s	loc_1DD8C
		move.b	#1,$34(a0)
;		cmp	#2,($FFFFFF70).w
;		beq.s	loc_1DD8C
;		move	#$48C,2(a0)
;		move	#-$4FC0,$3E(a0)
;		move	#-$6E80,$3C(a0)

loc_1DD8C:				; CODE XREF: h+6DF6?j h+6E04?j
;		bsr.w	sub_16D6E

loc_1DD90:				; DATA XREF: h+6DBA?o
		movea.w	$3E(a0),a2
		moveq	#0,d0
		move.b	$1C(a0),d0
		add	d0,d0
		move	off_1DDA4(pc,d0.w),d1
		jmp	off_1DDA4(pc,d1.w)
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
off_1DDA4:	dc loc_1DE28-off_1DDA4; 0 ; DATA XREF: h+6E30?o h+6E32?o ...
		dc loc_1DDAC-off_1DDA4; 1
		dc loc_1DDCC-off_1DDA4; 2
		dc loc_1DE20-off_1DDA4; 3
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

loc_1DDAC:				; DATA XREF: h+6E30?o
		move	($FFFFF646).w,$C(a0)
		tst.b	$1D(a0)
		bne.s	loc_1DE28
		move	8(a2),8(a0)
		move.b	#0,$22(a0)
		and	#$7FFF,2(a0)
		bra.s	loc_1DE28
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

loc_1DDCC:				; DATA XREF: h+6E30?o
;		cmp.b	#$C,$28(a2)
;		bcs.s	loc_1DE3E
		cmp.b	#4,$24(a2)
		bcc.s	loc_1DE3E
		tst.b	$39(a2)
		beq.s	loc_1DE3E
		move	8(a2),8(a0)
		move	$C(a2),$C(a0)
		move.b	$22(a2),$22(a0)
		and.b	#1,$22(a0)
		tst.b	$34(a0)
		beq.s	loc_1DE06
		sub	#4,$C(a0)

loc_1DE06:				; CODE XREF: h+6E8A?j
		tst.b	$1D(a0)
		bne.s	loc_1DE28
		and	#$7FFF,2(a0)
		tst	2(a2)
		bpl.s	loc_1DE28
		or	#-$8000,2(a0)
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

loc_1DE20:				; DATA XREF: h+6E30?o
loc_1DE28:				; CODE XREF: h+6E42?j h+6E56?j ...
		lea	(off_1DF38).l,a1
		jsr	AnimateSprite
		bsr.w	loc_1DEE4
		jmp	DisplaySprite
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

loc_1DE3E:				; CODE XREF: h+6E5E?j h+6E66?j ...
		move.b	#0,$1C(a0)
		rts	
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

loc_1DE46:				; DATA XREF: h+6DBA?o
		bra.w	DeleteObject
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ



loc_1DE4A:
	movea.w	$3E(a0),a2
	moveq	#$10,d1
	cmp.b	#$D,$1C(a2)
	beq.s	loc_1DE64
	moveq	#$6,d1
	cmp.b	#$3,$21(a2)
	beq.s	loc_1DE64
	move.b	#2,$24(a0)
	move.b	#0,$32(a0)
	rts
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

loc_1DE64:				; CODE XREF: h+6EE0?j
		subq.b	#1,$32(a0)
		bpl.s	loc_1DEE0
		move.b	#3,$32(a0)
		jsr	SingleObjLoad
		bne.s	loc_1DEE0
		move.b	0(a0),0(a1)
		move	8(a2),8(a1)
		move	$C(a2),$C(a1)
		tst.b	$34(a0)
		beq.s	loc_1DE9A
		sub	#4,d1

loc_1DE9A:				; CODE XREF: h+6F1E?j
		add	d1,$C(a1)
		move.b	#0,$22(a1)
		move.b	#3,$1C(a1)
		addq.b	#2,$24(a1)
		move.l	4(a0),4(a1)
		move.b	1(a0),1(a1)
		move.b	#1,$18(a1)
		move.b	#4,$19(a1)
		move	2(a0),2(a1)
		move	$3E(a0),$3E(a1)
		and	#$7FFF,2(a1)
		tst	2(a2)
		bpl.s	loc_1DEE0
		or	#-$8000,2(a1)

loc_1DEE0:				; CODE XREF: h+6EF4?j h+6F00?j ...
		bsr.s	loc_1DEE4
		rts	
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

loc_1DEE4:				; CODE XREF: h+6EC0?p h+6F6C?p
		moveq	#0,d0
		move.b	$1A(a0),d0
		cmp.b	$30(a0),d0
		beq.w	locret_1DF36
		move.b	d0,$30(a0)
		lea	(off_1E074).l,a2
		add	d0,d0
		add	(a2,d0.w),a2
		move	(a2)+,d5
		subq	#1,d5
		bmi.w	locret_1DF36
		move $3C(a0),d4

loc_1DF0A:				; CODE XREF: h+6FBE?j
		moveq	#0,d1
		move	(a2)+,d1
		move	d1,d3
		lsr.w	#8,d3
		and	#$F0,d3	; 'ð'
		add	#$10,d3
		and	#$FFF,d1
		lsl.l	#5,d1
		add.l	#Art_Dust,d1
		move	d4,d2
		add	d3,d4
		add	d3,d4
		jsr	(DMA_68KtoVRAM).l
		dbf	d5,loc_1DF0A
    rts

locret_1DF36:				; CODE XREF: h+6F7A?j h+6F90?j
		rts	
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
off_1DF38:	dc byte_1DF40-off_1DF38; 0 ; DATA XREF: h+6EB4?o h+6FC4?o ...
		dc byte_1DF43-off_1DF38; 1
		dc byte_1DF4F-off_1DF38; 2
		dc byte_1DF58-off_1DF38; 3
byte_1DF40:	dc.b $1F,  0,$FF	; 0 ; DATA XREF: h+6FC4?o
byte_1DF43:	dc.b   3,  1,  2,  3,  4,  5,  6,  7,  8,  9,$FD,  0; 0	; DATA XREF: h+6FC4?o
byte_1DF4F:	dc.b   1, $A, $B, $C, $D, $E, $F,$10,$FF; 0 ; DATA XREF: h+6FC4?o
byte_1DF58:	dc.b   3,$11,$12,$13,$14,$FC; 0	; DATA XREF: h+6FC4?o
; -------------------------------------------------------------------------------
; Unknown Sprite Mappings
; -------------------------------------------------------------------------------
MapUnc_1DF5E:
	dc word_1DF8A-MapUnc_1DF5E; 0
	dc word_1DF8C-MapUnc_1DF5E; 1
	dc word_1DF96-MapUnc_1DF5E; 2
	dc word_1DFA0-MapUnc_1DF5E; 3
	dc word_1DFAA-MapUnc_1DF5E; 4
	dc word_1DFB4-MapUnc_1DF5E; 5
	dc word_1DFBE-MapUnc_1DF5E; 6
	dc word_1DFC8-MapUnc_1DF5E; 7
	dc word_1DFD2-MapUnc_1DF5E; 8
	dc word_1DFDC-MapUnc_1DF5E; 9
	dc word_1DFE6-MapUnc_1DF5E; 10
	dc word_1DFF0-MapUnc_1DF5E; 11
	dc word_1DFFA-MapUnc_1DF5E; 12
	dc word_1E004-MapUnc_1DF5E; 13
	dc word_1E016-MapUnc_1DF5E; 14
	dc word_1E028-MapUnc_1DF5E; 15
	dc word_1E03A-MapUnc_1DF5E; 16
	dc word_1E04C-MapUnc_1DF5E; 17
	dc word_1E056-MapUnc_1DF5E; 18
	dc word_1E060-MapUnc_1DF5E; 19
	dc word_1E06A-MapUnc_1DF5E; 20
	dc word_1DF8A-MapUnc_1DF5E; 21
word_1DF8A:	dc.b 0
word_1DF8C:	dc.b 1
	dc.b $F2, $0D, $0, 0,$F0; 0
word_1DF96:	dc.b 1
	dc.b $E2, $0F, $0, 0,$F0; 0
word_1DFA0:	dc.b 1
	dc.b $E2, $0F, $0, 0,$F0; 0
word_1DFAA:	dc.b 1
	dc.b $E2, $0F, $0, 0,$F0; 0
word_1DFB4:	dc.b 1
	dc.b $E2, $0F, $0, 0,$F0; 0
word_1DFBE:	dc.b 1
	dc.b $E2, $0F, $0, 0,$F0; 0
word_1DFC8:	dc.b 1
	dc.b $F2, $0D, $0, 0,$F0; 0
word_1DFD2:	dc.b 1
	dc.b $F2, $0D, $0, 0,$F0; 0
word_1DFDC:	dc.b 1
	dc.b $F2, $0D, $0, 0,$F0; 0
word_1DFE6:	dc.b 1
	dc.b $4, $0D, $0, 0,$E0; 0
word_1DFF0:	dc.b 1
	dc.b $4, $0D, $0, 0,$E0; 0
word_1DFFA:	dc.b 1
	dc.b $4, $0D, $0, 0,$E0; 0
word_1E004:	dc.b 2
	dc.b $F4, $01, $0, 0,$E8; 0
	dc.b $4, $0D, $0, 2,$E0; 4
word_1E016:	dc.b 2
	dc.b $F4, $05, $0, 0,$E8; 0
	dc.b $4, $0D, $0, 4,$E0; 4
word_1E028:	dc.b 2
	dc.b $F4, $09, $0, 0,$E0; 0
	dc.b $4, $0D, $0, 6,$E0; 4
word_1E03A:	dc.b 2
	dc.b $F4, $09, $0, 0,$E0; 0
	dc.b $4, $0D, $0, 6,$E0; 4
word_1E04C:	dc.b 1
	dc.b $F8, $05, $0, 0,$F8; 0
word_1E056:	dc.b 1
	dc.b $F8, $05, $0, 4,$F8; 0
word_1E060:	dc.b 1
	dc.b $F8, $05, $0, 8,$F8; 0
word_1E06A:	dc.b 1
	dc.b $F8, $05, $0, $C,$F8; 0
	dc.b 0
off_1E074:	dc word_1E0A0-off_1E074; 0
	dc word_1E0A2-off_1E074; 1
	dc word_1E0A6-off_1E074; 2
	dc word_1E0AA-off_1E074; 3
	dc word_1E0AE-off_1E074; 4
	dc word_1E0B2-off_1E074; 5
	dc word_1E0B6-off_1E074; 6
	dc word_1E0BA-off_1E074; 7
	dc word_1E0BE-off_1E074; 8
	dc word_1E0C2-off_1E074; 9
	dc word_1E0C6-off_1E074; 10
	dc word_1E0CA-off_1E074; 11
	dc word_1E0CE-off_1E074; 12
	dc word_1E0D2-off_1E074; 13
	dc word_1E0D8-off_1E074; 14
	dc word_1E0DE-off_1E074; 15
	dc word_1E0E4-off_1E074; 16
	dc word_1E0EA-off_1E074; 17
	dc word_1E0EA-off_1E074; 18
	dc word_1E0EA-off_1E074; 19
	dc word_1E0EA-off_1E074; 20
	dc word_1E0EC-off_1E074; 21
word_1E0A0:	dc 0
word_1E0A2:	dc 1
	dc $7000
word_1E0A6:	dc 1
	dc $F008
word_1E0AA:	dc 1
	dc $F018
word_1E0AE:	dc 1
	dc $F028
word_1E0B2:	dc 1
	dc $F038
word_1E0B6:	dc 1
	dc $F048
word_1E0BA:	dc 1
	dc $7058
word_1E0BE:	dc 1
	dc $7060
word_1E0C2:	dc 1
	dc $7068
word_1E0C6:	dc 1
	dc $7070
word_1E0CA:	dc 1
	dc $7078
word_1E0CE:	dc 1
	dc $7080
word_1E0D2:	dc 2
	dc $1088
	dc $708A
word_1E0D8:	dc 2
	dc $3092
	dc $7096
word_1E0DE:	dc 2
	dc $509E
	dc $70A4
word_1E0E4:	dc 2
	dc $50AC
	dc $70B2
word_1E0EA:	dc 0
word_1E0EC:	dc 1
	dc $F0BA
	even

Now, save and try to build... oops! It seems that there's a routine missing: DMA_68KtoVRAM.
This routine handles the DMA queue present in Sonic 2, but that queue does not exist in Sonic 1.
This queue is used in Sonic 2 to handle the dynamic art for Sonic, Tails, and the dust.
Here's the code to the DMA queue routines in the Sonic 2 Nick Arcade prototype (porting it from the NA disassembly is somewhat... ...easier than porting from the Sonic 2 Final disassembly, so let's use that).

DMA_68KtoVRAM:				; CODE XREF: LoadSonicDynPLC+48?p
					; LoadTailsDynPLC+48?p ...
		movea.l	($FFFFDCFC).w,a1
		cmpa.w	#$DCFC,a1
		beq.s	DMA_68KtoVRAM_NoDMA
		move.w	#$9300,d0
		move.b	d3,d0
		move.w	d0,(a1)+
		move.w	#$9400,d0
		lsr.w	#8,d3
		move.b	d3,d0
		move.w	d0,(a1)+
		move.w	#$9500,d0
		lsr.l	#1,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		move.w	#$9600,d0
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		move.w	#$9700,d0
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		andi.l	#$FFFF,d2
		lsl.l	#2,d2
		lsr.w	#2,d2
		swap	d2
		ori.l	#$40000080,d2
		move.l	d2,(a1)+
		move.l	a1,($FFFFDCFC).w
		cmpa.w	#$DCFC,a1
		beq.s	DMA_68KtoVRAM_NoDMA
		move.w	#0,(a1)

DMA_68KtoVRAM_NoDMA:			; CODE XREF: DMA_68KtoVRAM+8?j
					; DMA_68KtoVRAM+56?j
		rts
; End of function DMA_68KtoVRAM


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


Process_DMA:				; CODE XREF: ROM:00000D9C?p
					; ROM:00000E84?p ...
		lea	($C00004).l,a5
		lea	($FFFFDC00).w,a1

Process_DMA_Loop:			; CODE XREF: Process_DMA+20?j
		move.w	(a1)+,d0
		beq.s	Process_DMA_End
		move.w	d0,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		cmpa.w	#$DCFC,a1
		bne.s	Process_DMA_Loop

Process_DMA_End:			; CODE XREF: Process_DMA+C?j
		move.w	#0,($FFFFDC00).w
		move.l	#$FFFFDC00,($FFFFDCFC).w
		rts
; End of function Process_DMA


Now, a brief explanation(sp?) of how I believe this routine works.
There's a queue that's $100 bytes long, that's stored at $DC00 in 68K RAM.
This queue is where the dynamically loaded art for Sonic, Tails, and the dust gets stored (you send the art to the queue by calling DMA_68KtoVRAM), until it's processed and sent to VRAM, which is done when the Process_DMA routine is called.
To port this to Sonic 1, we need to find an appropriate RAM location to fit it, and call Process_DMA at the right location.
So, where can we find $100 free bytes of RAM in Sonic 1 to fit this queue?

...Oops, we can't. But let's look at this from another point of view.
In Sonic 2, this queue is used to store the art for both Sonic, Tails, AND the dust.
For that reason, it needs $100 bytes. But we only need it to store the dust, and for that, we only actually need $30 bytes.
So, is there any location in RAM where we can stick a $30 bytes-long queue? Yes. We can stick it inside an unused object's SST.

I used the RAM location starting at $FFFFD3C2, if you know any better location, feel free to use it instead.
So we now need to change this routine to point to this new RAM location, as well as reduce its size.
To do that, replace all references to $FFFFDC00 with $FFFFD3C2, and $FFFFDCFC with $FFFFD3EE (and similarly, do the same for the "reduced" versions, $DC00->$D3C2, $DCFC->$D3EE).

Here's the routine after this conversion has been done:

DMA_68KtoVRAM:				; CODE XREF: LoadSonicDynPLC+48?p
					; LoadTailsDynPLC+48?p ...
		movea.l	($FFFFD3EE).w,a1
		cmpa.w	#$D3EE,a1
		beq.s	DMA_68KtoVRAM_NoDMA
		move.w	#$9300,d0
		move.b	d3,d0
		move.w	d0,(a1)+
		move.w	#$9400,d0
		lsr.w	#8,d3
		move.b	d3,d0
		move.w	d0,(a1)+
		move.w	#$9500,d0
		lsr.l	#1,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		move.w	#$9600,d0
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		move.w	#$9700,d0
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		andi.l	#$FFFF,d2
		lsl.l	#2,d2
		lsr.w	#2,d2
		swap	d2
		ori.l	#$40000080,d2
		move.l	d2,(a1)+
		move.l	a1,($FFFFD3EE).w
		cmpa.w	#$D3EE,a1
		beq.s	DMA_68KtoVRAM_NoDMA
		move.w	#0,(a1)

DMA_68KtoVRAM_NoDMA:			; CODE XREF: DMA_68KtoVRAM+8?j
					; DMA_68KtoVRAM+56?j
		rts
; End of function DMA_68KtoVRAM


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


Process_DMA:				; CODE XREF: ROM:00000D9C?p
					; ROM:00000E84?p ...
		lea	($C00004).l,a5
		lea	($FFFFD3C2).w,a1

Process_DMA_Loop:			; CODE XREF: Process_DMA+20?j
		move.w	(a1)+,d0
		beq.s	Process_DMA_End
		move.w	d0,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		cmpa.w	#$D3EE,a1
		bne.s	Process_DMA_Loop

Process_DMA_End:			; CODE XREF: Process_DMA+C?j
		move.w	#0,($FFFFD3C2).w
		move.l	#$FFFFD3C2,($FFFFD3EE).w
		rts
; End of function Process_DMA

Paste it right after ShowVDPGraphics.
Now all that's left to get the queue up and running is to call Process_DMA in the right location.
To do that, go to loc_D50, and add these two lines at the beginning:

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

The queue should now be working. Now we need to do one more thing, call the actual Spin Dash dust object.
To do that, open _inc\Object pointers.asm, and replace the first call to ObjectFall in the second line with Spin Dash_dust.

The outcome should be something like this:

; ---------------------------------------------------------------------------
; Object pointers
; ---------------------------------------------------------------------------
	dc.l Obj01, ObjectFall,	ObjectFall, ObjectFall
	dc.l SpinDash_dust, ObjectFall, ObjectFall, Obj08
	dc.l Obj09, Obj0A, Obj0B, Obj0C
	dc.l Obj0D, Obj0E, Obj0F, Obj10
	dc.l Obj11, Obj12, Obj13, Obj14
	dc.l Obj15, Obj16, Obj17, Obj18
	dc.l Obj19, Obj1A, Obj1B, Obj1C
	dc.l Obj1D, Obj1E, Obj1F, Obj20
	dc.l Obj21, Obj22, Obj23, Obj24
	dc.l Obj25, Obj26, Obj27, Obj28
	dc.l Obj29, Obj2A, Obj2B, Obj2C
	dc.l Obj2D, Obj2E, Obj2F, Obj30
	dc.l Obj31, Obj32, Obj33, Obj34
	dc.l Obj35, Obj36, Obj37, Obj38
	dc.l Obj39, Obj3A, Obj3B, Obj3C
	dc.l Obj3D, Obj3E, Obj3F, Obj40
	dc.l Obj41, Obj42, Obj43, Obj44
	dc.l Obj45, Obj46, Obj47, Obj48
	dc.l Obj49, Obj4A, Obj4B, Obj4C
	dc.l Obj4D, Obj4E, Obj4F, Obj50
	dc.l Obj51, Obj52, Obj53, Obj54
	dc.l Obj55, Obj56, Obj57, Obj58
	dc.l Obj59, Obj5A, Obj5B, Obj5C
	dc.l Obj5D, Obj5E, Obj5F, Obj60
	dc.l Obj61, Obj62, Obj63, Obj64
	dc.l Obj65, Obj66, Obj67, Obj68
	dc.l Obj69, Obj6A, Obj6B, Obj6C
	dc.l Obj6D, Obj6E, Obj6F, Obj70
	dc.l Obj71, Obj72, Obj73, Obj74
	dc.l Obj75, Obj76, Obj77, Obj78
	dc.l Obj79, Obj7A, Obj7B, Obj7C
	dc.l Obj7D, Obj7E, Obj7F, Obj80
	dc.l Obj81, Obj82, Obj83, Obj84
	dc.l Obj85, Obj86, Obj87, Obj88
	dc.l Obj89, Obj8A, Obj8B, Obj8C

This will cause Object ID 05, which was previously unused, to now point to the Spin Dash dust object.

Finally, we need to call the object itself. So go to Obj01_Main, and at the end of the routine (before Obj01_Control), add the following line:

		move.b	#5,$FFFFD1C0.w

This will load the dust object to address $FFFFD1C0, (addresses from $D000 to $D400 are part of the SST).

Finally, we need to have the Spin Dash routine set the dust's animation. To do that, go to Sonic_SpinDash, and change this line:


		move.b	#2,($FFFFD11C).w	; ??? $D11C is used for

to this:

		move.b	#2,($FFFFD1DC).w	; Set the Spin Dash dust animation to $2.


Now we could remove this line:

		bcs.s	loc2_1AC84		; ??? branch if carry

However, there's a better way to do it. Change the following line:

		cmpi.b	#$C,$28(a0)		; ??? oxygen remaining?

to this:

		cmpi.w	#$C,($FFFFFE14).w	; check air remaining


And again, in loc2_1ACF4, change this line:

		move.b	#0,($FFFFD11C).w	; clear $D11C (smoke)

to this:

		move.b	#0,($FFFFD1DC).w	; clear Spin Dash dust animation.

And finally, add the following line in loc2_1AD48, right before the jsr to PlaySound_Special:

		move.b	#2,($FFFFD1DC).w	; Set the Spin Dash dust animation to $2.


For reference, here's the complete Sonic_SpinDash routine:

; ---------------------------------------------------------------------------
; 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,($FFFFEED0).w	; move to $EED0
		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,($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
; End of subroutine Sonic_SpinDash


Now, build the ROM, and the result is... oops, something's missing. Right, we forgot to add the dust art.
So download the Spin Dash dust art here (put it in the artunc\ folder inside your source dir), and at the end of the ROM, after SegaPCM, add the following lines:

Art_Dust	incbin	artunc\spindust.bin


Now compile it and try a Spin Dash:

SpindashGuide Pic2.png

Nice, it seems to be working...
...or not.

SpindashGuide Pic3.png

I'm pretty sure the lamppost wasn't meant to look like THAT.
The problem here seems to be that the dust art is overwriting the lamppost art.
To fix this, we need to move the lamppost art to a different location.
For that, we can use the VRAM location $D800, which is free.
So open the file _inc\Pattern load cues.asm, and search for these lines:

PLC_Main:	dc.w 4
		dc.l Nem_Lamp		; lamppost
		dc.w $F400

Change them to:

PLC_Main:	dc.w 4
		dc.l Nem_Lamp		; lamppost
		dc.w $D800

This will load the lamppost art in $D800 in VRAM, instead of $F400.
Now we must make the lamppost object use that art, instead of the one in $F400.
So now go to Obj79 (lamppost object) and search for this line, which should be in Obj79_Main:

		move.w	#$7A0,2(a0)

Change it to:

		move.w	#($D800/$20),2(a0)

The Obj79_Main routine should then look like this:

Obj79_Main:				; XREF: Obj79_Index
		addq.b	#2,$24(a0)
		move.l	#Map_obj79,4(a0)
		move.w	#($D800/$20),2(a0)
		move.b	#4,1(a0)
		move.b	#8,$19(a0)
		move.b	#5,$18(a0)
		lea	($FFFFFC00).w,a2
		moveq	#0,d0
		move.b	$23(a0),d0
		bclr	#7,2(a2,d0.w)
		btst	#0,2(a2,d0.w)
		bne.s	Obj79_RedLamp
		move.b	($FFFFFE30).w,d1
		andi.b	#$7F,d1
		move.b	$28(a0),d2	; get lamppost number
		andi.b	#$7F,d2
		cmp.b	d2,d1		; is lamppost number higher than the number hit?
		bcs.s	Obj79_BlueLamp	; if yes, branch

Similarly, change this line in Obj79_HitLamp:

		move.w	#$7A0,2(a1)

to this:

		move.w	#($D800/$20),2(a1)

Therefore making Obj79_HitLamp look like this:

Obj79_HitLamp:

		move.w	($FFFFD008).w,d0
		sub.w	8(a0),d0
		addq.w	#8,d0
		cmpi.w	#$10,d0
		bcc.w	locret_16F90
		move.w	($FFFFD00C).w,d0
		sub.w	$C(a0),d0
		addi.w	#$40,d0
		cmpi.w	#$68,d0
		bcc.s	locret_16F90
		move.w	#$A1,d0
		jsr	(PlaySound_Special).l ;	play lamppost sound
		addq.b	#2,$24(a0)
		jsr	SingleObjLoad
		bne.s	loc_16F76
		move.b	#$79,0(a1)	; load twirling	lamp object
		move.b	#6,$24(a1)	; use "Obj79_Twirl" routine
		move.w	8(a0),$30(a1)
		move.w	$C(a0),$32(a1)
		subi.w	#$18,$32(a1)
		move.l	#Map_obj79,4(a1)
		move.w	#($D800/$20),2(a1)
		move.b	#4,1(a1)
		move.b	#8,$19(a1)
		move.b	#4,$18(a1)
		move.b	#2,$1A(a1)
		move.w	#$20,$36(a1)

...and that's it. Compile your ROM and we should now have perfect Spin Dash in Sonic 1.

SpindashGuide Pic4.png

Here's the final result of this thing. Source code available here. Have fun :)

Note that you should look here for instructions on fixing the SEGA sound.

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. The following codes are designed for the AS version of the Sonic 1 GitHub disassembly.

Fixing the monitor bug

When you try to Spin Dash next to a monitor, you won't be able to do that and simply stop when releasing. It only works if you walk a little bit away from the monitor and do the Spin Dash. To fix that, look for file called "_incObj\26 Monitor.asm", and make a search for .normal:. You should see something like this:

.normal:	; 2nd Routine 0
		move.w	#$1A,d1
		move.w	#$F,d2
		bsr.w	Mon_SolidSides
		beq.w	loc_A25C
		tst.w	obVelY(a1)
		bmi.s	loc_A20A
		cmpi.b	#id_Roll,obAnim(a1) ; is Sonic rolling?
		beq.s	loc_A25C	; if yes, branch

By reading this code, you can see that there is a check for the rolling animation there. This is what causes the monitor bug. To fix it, simply add a second check for the Spin Dash animation after this one:

.normal:	; 2nd Routine 0
		move.w	#$1A,d1
		move.w	#$F,d2
		bsr.w	Mon_SolidSides
		beq.w	loc_A25C
		tst.w	obVelY(a1)
		bmi.s	loc_A20A
		cmpi.b	#id_Roll,obAnim(a1) ; is Sonic rolling?
		beq.s	loc_A25C	; if yes, branch
		cmpi.b	#id_Spindash,obAnim(a1)	; is Sonic spin-dashing?
		beq.s	loc_A25C	; if yes, branch

Great, let's try it. It should be working and...

SpindashGuide Pic1.png

Oh dear. This isn't right. We're Spin Dashing, we're not supposed to use the pushing animation.

To fix that, go to "_incObj\Sonic Spindash.asm", and look at the first two lines:

Sonic_SpinDash:
		tst.b	spindash_flag(a0)	; already Spin Dashing?
		bne.s	Sonic_UpdateSpindash	; if set, branch

Since we know we're Spin Dashing when spindash_flag(a0) is set, let's make sure that the Spin Dash animation is used when that happens.
Therefore, go to Sonic_UpdateSpindash, and right after the label, add the following line:

		move.b	#id_Spindash,anim(a0)

Build the ROM again, and the monitor bug should be permanently taken care of. Here's a ROM after this step was carried out.

Adding the Spin Dash sound effect

Adding a sound effect to Sonic 1 isn't the most trivial of things to do. So be sure to follow these steps carefully.

First, we need a free slot. Since all the slots reserved for sound effects in Sonic 1 are taken, we therefore need to reserve extra slots for sound effects. The $D1-$DF area is free, so let's use that.

Go to s1.sounddriver.asm and locate PlaySoundID, and you should be able to see this line there:

		; DANGER! Special SFXes end at $D0, yet this checks until $DF; attempting to
		; play sounds $D1-$DF will cause a crash!
		cmpi.b	#spec__Last+$10,d7	; Is this special sfx ($D0-$DF)?
		blo.w	Sound_PlaySpecial	; Branch if yes

Since only $D0 is actually used here, it's a rather large waste to keep the rest of the slots unused. So change that +$10 to +$1:

		cmpi.b	#spec__Last+$1,d7	; Is this special sfx ($D0-$D0)?
		blo.w	Sound_PlaySpecial	; Branch if yes

And after that line, let's add our own comparison:

		; DANGER! Extra SFXes end at $D1, yet this checks until $DF; attempting to
		; play sounds $D2-$DF will cause a crash!
		cmpi.b	#ext__Last+$E,d7	; Is this extra sfx ($D1-$DF)?
		blo.w	Sound_PlayMoreSFX	; Branch if yes

So now the code should look like this when finished:

		cmpi.b	#spec__Last+$1,d7	; Is this special sfx ($D0-$D0)?
		blo.w	Sound_PlaySpecial	; Branch if yes
		; DANGER! Extra SFXes end at $D1, yet this checks until $DF; attempting to
		; play sounds $D2-$DF will cause a crash!
		cmpi.b	#ext__Last+$E,d7	; Is this extra sfx ($D1-$DF)?
		blo.w	Sound_PlayMoreSFX	; Branch if yes

Then head over to Constants.asm and below spec__Last: equ ((ptr_specend-SpecSoundIndex-4)/4)+spec__First, add this in:

; Extra sound effects
ext__First:	equ $D1
sfx_SpinDash:	equ ((ptr_sndD1-ExtSoundIndex)/4)+ext__First
ext__Last:	equ ((ptr_extend-ExtSoundIndex-4)/4)+ext__First

Now, we have to actually create this routine. So go to Sound_PlaySFX, and see what it does:

; ---------------------------------------------------------------------------
; Play normal sound effect
; ---------------------------------------------------------------------------
; Sound_A0toCF:
Sound_PlaySFX:
		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
		cmpi.b	#sfx_Ring,d7			; is ring sound	effect played?
		bne.s	.sfx_notRing			; if not, branch
		tst.b	SMPS_RAM.v_ring_speaker(a6)	; Is the ring sound playing on right speaker?
		bne.s	.gotringspeaker			; Branch if not
		move.b	#sfx_RingLeft,d7		; play ring sound in left speaker
; loc_721EE:
.gotringspeaker:
		bchg	#0,SMPS_RAM.v_ring_speaker(a6)	; change speaker
; Sound_notB5:
.sfx_notRing:
		cmpi.b	#sfx_Push,d7				; is "pushing" sound played?
		bne.s	.sfx_notPush				; if not, branch
		tst.b	SMPS_RAM.f_push_playing(a6)		; Is pushing sound already playing?
		bne.w	.locret					; Return if not
		move.b	#$80,SMPS_RAM.f_push_playing(a6)	; Mark it as playing
; Sound_notA7:
.sfx_notPush:
		movea.l	(Go_SoundIndex).l,a0
		subi.b	#sfx__First,d7		; Make it 0-based
		lsl.w	#2,d7			; Convert sfx ID into index
		movea.l	(a0,d7.w),a3		; SFX data pointer
		movea.l	a3,a1
		moveq	#0,d1
		move.w	(a1)+,d1		; Voice pointer
		add.l	a3,d1			; Relative pointer
		move.b	(a1)+,d5		; Dividing timing
	if FixBugs
		moveq	#0,d7
	else
		; DANGER! there is a missing 'moveq #0,d7' here, without which SFXes whose
		; index entry is above $3F will cause a crash.
		; This bug is fixed in Ristar's driver.
	endif
		move.b	(a1)+,d7	; Number of tracks (FM + PSG)
		subq.b	#1,d7
		moveq	#SMPS_Track.len,d6
		(...)

As you can see, this routine has a lot of checks for special sound effects, like the ring effect and the pushing effect. We don't need that for our new routine, so trash all of that, and you should end up with this:

; ---------------------------------------------------------------------------
; 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_SoundIndex).l,a0
		subi.b	#sfx__First,d7		; Make it 0-based
		lsl.w	#2,d7			; Convert sfx ID into index
		movea.l	(a0,d7.w),a3		; SFX data pointer
		movea.l	a3,a1
		moveq	#0,d1
		move.w	(a1)+,d1		; Voice pointer
		add.l	a3,d1			; Relative pointer
		move.b	(a1)+,d5		; Dividing timing
		; DANGER! there is a missing 'moveq #0,d7' here, without which SFXes whose
		; index entry is above $3F will cause a crash.
		; This bug is fixed in Ristar's driver.
		move.b	(a1)+,d7	; Number of tracks (FM + PSG)
		subq.b	#1,d7
		moveq	#SMPS_Track.len,d6
		(...)

There's a problem here: If we subtract $A0 (sfx__First) to get the current index, then the next entry in this index will be sound $D0 (spec__First), which is handled by a different routine, forcing us to have an unused entry in an index. So, put a +$1 after sfx__First:

; ---------------------------------------------------------------------------
; 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_SoundIndex).l,a0
		subi.b	#sfx__First+$1,d7		; Make it 0-based
		lsl.w	#2,d7			; Convert sfx ID into index
		movea.l	(a0,d7.w),a3		; SFX data pointer
		movea.l	a3,a1
		moveq	#0,d1
		move.w	(a1)+,d1		; Voice pointer
		add.l	a3,d1			; Relative pointer
		move.b	(a1)+,d5		; Dividing timing
		; DANGER! there is a missing 'moveq #0,d7' here, without which SFXes whose
		; index entry is above $3F will cause a crash.
		; This bug is fixed in Ristar's driver.
		move.b	(a1)+,d7	; Number of tracks (FM + PSG)
		subq.b	#1,d7
		moveq	#SMPS_Track.len,d6
		(...)

Also, it can be noted that any code after the sfx__First+$1 line is common to the two routines.

So go back to Sound_PlaySFX, and add a label right before "lsl.w #2,d7":

; Sound_notA7:
.sfx_notPush:
		movea.l	(Go_SoundIndex).l,a0
		subi.b	#sfx__First,d7		; Make it 0-based
; SoundEffects_Common:	; <-- not required, but here for completeness
sfx_common:				; <-- add this line!
		lsl.w	#2,d7			; Convert sfx ID into index
		movea.l	(a0,d7.w),a3		; SFX data pointer
		movea.l	a3,a1
		moveq	#0,d1
		move.w	(a1)+,d1		; Voice pointer
		add.l	a3,d1			; Relative pointer
		move.b	(a1)+,d5		; Dividing timing
	if FixBugs
		moveq	#0,d7
	else
		; DANGER! there is a missing 'moveq #0,d7' here, without which SFXes whose
		; index entry is above $3F will cause a crash.
		; This bug is fixed in Ristar's driver.
	endif
		move.b	(a1)+,d7	; Number of tracks (FM + PSG)
		subq.b	#1,d7
		moveq	#SMPS_Track.len,d6
		(...)

And we can now greatly reduce the size of our new routine:

; ---------------------------------------------------------------------------
; 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_SoundIndex).l,a0
		subi.b	#sfx__First+$1,d7			; Make it 0-based
		lsl.w	#2,d7			; Convert sfx ID into index
		bra.w	sfx_common

So, after this is done, put the routine right above Sound_PlaySFX.

Now we need to make some modifications to the labels to prevent a building error. Remove the periods before the labels .clear_sndprio and .locret.

Now, all we need is the actual spin-dash sound effect. Go into your Sonic 2 disassembly you have and find "E0 - Spin Dash Rev.asm" under "sound\sfx". Put this file in the "sound\sfx" directory of your source code, and go to the SoundCF label. After the even, add the following lines:

SoundD1:	include "sound/sfx/E0 - Spin Dash Rev.asm"
		even

So the surrounding code should look like this:

SoundCD:	include "sound/sfx/SndCD - Switch.asm"
		even
SoundCE:	include "sound/sfx/SndCE - Ring Left Speaker.asm"
		even
SoundCF:	include "sound/sfx/SndCF - Signpost.asm"
		even
SoundD1:	include "sound/sfx/E0 - Spin Dash Rev.asm"
		even

And finally, add SoundD1 to the index:

; ---------------------------------------------------------------------------
; Extra sound effect pointers
; ---------------------------------------------------------------------------
ExtSoundIndex:
ptr_sndD1:	dc.l SoundD1
ptr_extend

Place this after ptr_specend and before ; Sound effect data. This is how it should look like:

...
ptr_specend

; ---------------------------------------------------------------------------
; Extra sound effect pointers
; ---------------------------------------------------------------------------
ExtSoundIndex:
ptr_sndD1:	dc.l SoundD1
ptr_extend

; ---------------------------------------------------------------------------
; Sound effect data
; ---------------------------------------------------------------------------
...

Now find Go_SpecSoundIndex and copy that line below, but change it to this:

Go_ExtSoundIndex:	dc.l ExtSoundIndex

Here is our result:

...
Go_SpecSoundIndex:	dc.l SpecSoundIndex
Go_ExtSoundIndex:	dc.l ExtSoundIndex	; <-- add this line!
Go_MusicIndex:		dc.l MusicIndex
...

Then, go to Sound_PlayMoreSFX and change Go_SoundIndex to Go_ExtSoundIndex. Here's what you should have:

; ---------------------------------------------------------------------------
; 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	; <-- changed from "Go_SoundIndex"
		subi.b	#ext__First,d7			; Make it 0-based
		lsl.w	#2,d7			; Convert sfx ID into index
		bra.w	sfx_common

Now, open up Sonic Spindash.asm under _incObj (if you haven't yet already), and change all references to sound sfx_Roll to reference sound sfx_SpinDash instead.

For reference, here's the complete routine:

; ---------------------------------------------------------------------------
; Subroutine to make Sonic perform 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,anim(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,anim(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.b	#12,air_left(a0)	; if he's drowning, branch to not make dust
		blo.s	+
		move.b	#2,(v_spindust+anim).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,anim(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,y_radius(a0)	; y_radius(a0) is height/2
		move.b	#7,x_radius(a0)		; x_radius(a0) is width/2
		move.b	#id_Roll,anim(a0)	; set animation to roll
		addq.w	#5,y_pos(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),inertia(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	inertia(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,($FFFFEED0).w	; move to $EED0

		btst	#0,status(a0)	; is sonic facing right?
		beq.s	+		; if not, branch
		neg.w	inertia(a0)	; negate inertia
+
		bset	#2,status(a0)	; set unused (in s1) flag
		move.b	#0,(v_spindust+anim).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),anim(a0)	; reset spindash animation
		move.w	#sfx_SpinDash,d0	; was $E0 in sonic 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

So, problem solved. Here's a ROM of the output after fixing this problem.

Fixing the Spin Dash See-Saw bug

This problem occurs when you're Spin Dashing on a seesaw, and then you get thrown up by the seesaw. You keep your Spin Dash state, and dash off immediately once you land. This is clearly not the intended behaviour.

To fix this, go to Sonic_MdJump and Sonic_MdJump2 under "sonic.asm", and add the following line at the beginning of each of the two routines:

		clr.b	spindash_flag(a0)

This should fix the see-saw bug. Here's the ROM.

Porting the Spin Dash smoke/dust

This is a big one. For starters, you need to port the Spin Dash smoke/dust object from Sonic 2.

For your convenience, here is the code to the ported object:

; ===========================================================================
; ---------------------------------------------------------------------------
; Object 05 - Spindash dust (Water splash in Sonic 2)
; ---------------------------------------------------------------------------
; Sprite_1DD20: Obj08:
SpinDash_dust:
		moveq	#0,d0
		move.b	routine(a0),d0
		move.w	Obj05_Index(pc,d0.w),d1
		jmp	Obj05_Index(pc,d1.w)
; ===========================================================================
; off_1DD2E:
Obj05_Index:	dc.w Obj05_Init-Obj05_Index
		dc.w Obj05_Main-Obj05_Index
		dc.w Obj05_Delete-Obj05_Index
		dc.w Obj05_Skid-Obj05_Index
; ===========================================================================
; loc_1DD36:
Obj05_Init:
		addq.b	#2,routine(a0)
		move.l	#Map_Obj05,mappings(a0)
		ori.b	#4,render_flags(a0)
		move.b	#1,priority(a0)
		move.b	#$10,width_pixels(a0)
		move.w	#$7A0,art_tile(a0)
		move.w	#-$3000,objoff_3E(a0)	; MainCharacter
		move.w	#$F400,objoff_3C(a0)
		cmpa.w	#-$2E40,a0	; Sonic_Dust
		beq.s	Obj05_Main	; was "+"
		move.b	#1,objoff_34(a0)
		; Since Miles "Tails" Prower didn't exist in Sonic 1, this entire code specific to Sonic 2 has been removed.
;		cmpi.w	#2,(Player_mode).w	; is player mode Miles/Tails?
;		beq.s	+			; if yes, branch
;		move.w	#$48C,art_tile(a0)	; make spindust for Tails
;		move.w	#-$4FC0,objoff_3E(a0)	; Sidekick
;		move.w	#-$6E80,objoff_3C(a0)	; make spindust for Tails
;+
;		bsr.w	Adjust2PArtPointer	; doesn't exist in Sonic 1

; loc_1DD90:
Obj05_Main:
		movea.w	objoff_3E(a0),a2 ; a2=character (although an unused/dead code, it's still needed or else dust would not show)
		moveq	#0,d0
		move.b	anim(a0),d0	; use current animation as a secondary routine counter
		add.w	d0,d0
		move.w	Obj05_Modes(pc,d0.w),d1
		jmp	Obj05_Modes(pc,d1.w)
; ===========================================================================
; off_1DDA4: Obj08_DisplayModes:
Obj05_Modes:	dc Obj05_Display-Obj05_Modes
		dc Obj05_MdSplash-Obj05_Modes
		dc Obj05_MdDust-Obj05_Modes
		dc Obj05_MdSkidDust-Obj05_Modes
; ===========================================================================
; This is used for the water splash in Sonic 2. Sonic 1 already has the water
; splash object in place for LZ, but this one uses a different graphic. This
; code is just a duplicate/leftover, however.
; loc_1DDAC:
Obj05_MdSplash:
		move.w	(v_waterpos1).w,y_pos(a0)
		tst.b	next_anim(a0)
		bne.s	Obj05_Display
		move.w	x_pos(a2),x_pos(a0)
		move.b	#0,status(a0)
		andi.w	#$7FFF,art_tile(a0)	; drawing_mask
		bra.s	Obj05_Display
; ===========================================================================
; loc_1DDCC: Obj08_MdSpindashDust:
Obj05_MdDust:
		if Revision=1
		cmpi.w	#$C,(v_air).w	; check air remaining
		blo.s	Obj05_Reset	; if he's drowning, branch to not make dust
		endif
		cmpi.b	#4,routine(a2)
		bhs.s	Obj05_Reset
		tst.b	spindash_flag(a2)
		beq.s	Obj05_Reset
		move.w	x_pos(a2),x_pos(a0)
		move.w	y_pos(a2),y_pos(a0)
		move.b	status(a2),status(a0)
		andi.b	#1,status(a0)
		tst.b	objoff_34(a0)
		beq.s	+
		subi.w	#4,y_pos(a0)
+
		tst.b	next_anim(a0)
		bne.s	Obj05_Display
		andi.w	#$7FFF,art_tile(a0)	; drawing_mask
		tst.w	art_tile(a2)
		bpl.s	Obj05_Display
		ori.w	#-$8000,art_tile(a0)	; high_priority
		bra.s	Obj05_Display
; ===========================================================================
; loc_1DE20:
Obj05_MdSkidDust:
		if Revision=1
		cmpi.w	#$C,(v_air).w	; check air remaining
		blo.s	Obj05_Reset	; if he's drowning, branch to not make dust
		endif

; loc_1DE28:
Obj05_Display:
		lea	(Ani_obj05).l,a1
		jsr	(AnimateSprite).l
		bsr.w	LoadDustDynPLC
		jmp	(DisplaySprite).l
; ===========================================================================
; loc_1DE3E: Obj08_ResetDisplayMode:
Obj05_Reset:
		move.b	#0,anim(a0)
		rts
; ===========================================================================
; BranchTo16_DeleteObject
Obj05_Delete: 
		bra.w	DeleteObject
; ===========================================================================
; This is the routine used for the skidding dust. This is just a leftover, as
; nothing in the game calls for it. Sonic 1 doesn't have a skidding dust in
; the original game.
; loc_1DE4A: Obj08_CheckSkid:
Obj05_Skid:
		movea.w	objoff_3E(a0),a2 ; a2=character (although an unused/dead code, it's still needed or else dust would not show)
		moveq	#$10,d1
		cmpi.b	#id_Stop,anim(a2)	; is Sonic skidding?
		beq.s	Obj05_SkidDust	; if not, branch
		moveq	#$6,d1
		cmp.b	#$3,obColProp(a2)
		beq.s	Obj05_SkidDust
		move.b	#2,routine(a0)
		move.b	#0,objoff_32(a0)
		rts
; ===========================================================================
; loc_1DE64:
Obj05_SkidDust:
		subq.b	#1,objoff_32(a0)
		bpl.s	loc_1DEE0
		move.b	#3,objoff_32(a0)
		jsr	(FindFreeObj).l	; changed from bsr.w
		bne.s	loc_1DEE0
		move.b	0(a0),0(a1) ; load obj08 (leftover code)
		move.w	x_pos(a2),x_pos(a1)
		move.w	y_pos(a2),y_pos(a1)
		addi.w	#$10,y_pos(a1)	; unknown
		tst.b	objoff_34(a0)
		beq.s	+
		subi.w	#4,y_pos(a1)
+
		addi.w	d1,$C(a1)
		move.b	#0,status(a1)
		move.b	#3,anim(a1)
		addq.b	#2,routine(a1)
		move.l	mappings(a0),mappings(a1)
		move.b	render_flags(a0),render_flags(a1)
		move.b	#1,priority(a1)
		move.b	#4,width_pixels(a1)
		move.w	art_tile(a0),art_tile(a1)
		move.w	objoff_3E(a0),objoff_3E(a1)
		andi.w	#$7FFF,art_tile(a1)
		tst.w	art_tile(a2)
		bpl.s	loc_1DEE0
		ori.w	#-$8000,art_tile(a1)

loc_1DEE0:
		bsr.s	LoadDustDynPLC
		rts
; ===========================================================================
; ---------------------------------------------------------------------------
; Spindust pattern loading subroutine
; ---------------------------------------------------------------------------

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

; loc_1DEE4:
LoadDustDynPLC:
		moveq	#0,d0
		move.b	mapping_frame(a0),d0	; load frame number
		cmp.b	objoff_30(a0),d0
		beq.w	return_1DF36
		move.b	d0,objoff_30(a0)
		lea	(DustDynPLC).l,a2 ; load PLC script
		add.w	d0,d0
		adda.w	(a2,d0.w),a2
		move.w	(a2)+,d5
		subq.w	#1,d5
		bmi.w	return_1DF36	; if zero, branch
		move.w	objoff_3C(a0),d4

-		moveq	#0,d1
		move.w	(a2)+,d1	; read "number of entries" value
		move.w	d1,d3
		lsr.w	#8,d3
		andi.w	#$F0,d3
		addi.w	#$10,d3
		andi.w	#$FFF,d1
		lsl.l	#5,d1
		addi.l	#Art_Dust,d1
		move.w	d4,d2
		add.w	d3,d4
		add.w	d3,d4
		jsr	(DMA_68KtoVRAM).l	; labeled as "QueueDMATransfer" in Sonic 2 disassembly nomenclature
		dbf	d5,-	; repeat for number of entries

return_1DF36:
		rts
; ===========================================================================
; ---------------------------------------------------------------------------
; Animation script - Spindash Dust
; ---------------------------------------------------------------------------
; off_1DF38:
Ani_obj05:
		dc.w Obj05Ani_Null-Ani_obj05	; 0
		dc.w Obj05Ani_Splash-Ani_obj05	; 1
		dc.w Obj05Ani_Dash-Ani_obj05	; 2
		dc.w Obj05Ani_Skid-Ani_obj05	; 3
Obj05Ani_Null:	dc.b $1F,  0,$FF
Obj05Ani_Splash:dc.b   3,  1,  2,  3,  4,  5,  6,  7,  8,  9,$FD,  0
Obj05Ani_Dash:	dc.b   1, $A, $B, $C, $D, $E, $F,$10,$FF
Obj05Ani_Skid:	dc.b   3,$11,$12,$13,$14,$FC
		even
; ---------------------------------------------------------------------------
; Sprite mappings - Spindash Dust
; ---------------------------------------------------------------------------
Map_Obj05:
		dc.w word_1DF8A-Map_Obj05	; 0
		dc.w word_1DF8C-Map_Obj05	; 1
		dc.w word_1DF96-Map_Obj05	; 2
		dc.w word_1DFA0-Map_Obj05	; 3
		dc.w word_1DFAA-Map_Obj05	; 4
		dc.w word_1DFB4-Map_Obj05	; 5
		dc.w word_1DFBE-Map_Obj05	; 6
		dc.w word_1DFC8-Map_Obj05	; 7
		dc.w word_1DFD2-Map_Obj05	; 8
		dc.w word_1DFDC-Map_Obj05	; 9
		dc.w word_1DFE6-Map_Obj05	; A
		dc.w word_1DFF0-Map_Obj05	; B
		dc.w word_1DFFA-Map_Obj05	; C
		dc.w word_1E004-Map_Obj05	; D
		dc.w word_1E016-Map_Obj05	; E
		dc.w word_1E028-Map_Obj05	; F
		dc.w word_1E03A-Map_Obj05	; 10
		dc.w word_1E04C-Map_Obj05	; 11
		dc.w word_1E056-Map_Obj05	; 12
		dc.w word_1E060-Map_Obj05	; 13
		dc.w word_1E06A-Map_Obj05	; 14
		dc.w word_1DF8A-Map_Obj05	; 15
word_1DF8A:	dc.b 0
word_1DF8C:	dc.b 1
		dc.b $F2, $0D, $0, 0,$F0
word_1DF96:	dc.b 1
		dc.b $E2, $0F, $0, 0,$F0
word_1DFA0:	dc.b 1
		dc.b $E2, $0F, $0, 0,$F0
word_1DFAA:	dc.b 1
		dc.b $E2, $0F, $0, 0,$F0
word_1DFB4:	dc.b 1
		dc.b $E2, $0F, $0, 0,$F0
word_1DFBE:	dc.b 1
		dc.b $E2, $0F, $0, 0,$F0
word_1DFC8:	dc.b 1
		dc.b $F2, $0D, $0, 0,$F0
word_1DFD2:	dc.b 1
		dc.b $F2, $0D, $0, 0,$F0
word_1DFDC:	dc.b 1
		dc.b $F2, $0D, $0, 0,$F0
word_1DFE6:	dc.b 1
		dc.b $4, $0D, $0, 0,$E0
word_1DFF0:	dc.b 1
		dc.b $4, $0D, $0, 0,$E0
word_1DFFA:	dc.b 1
		dc.b $4, $0D, $0, 0,$E0
word_1E004:	dc.b 2
		dc.b $F4, $01, $0, 0,$E8
		dc.b $4, $0D, $0, 2,$E0
word_1E016:	dc.b 2
		dc.b $F4, $05, $0, 0,$E8
		dc.b $4, $0D, $0, 4,$E0
word_1E028:	dc.b 2
		dc.b $F4, $09, $0, 0,$E0
		dc.b $4, $0D, $0, 6,$E0
word_1E03A:	dc.b 2
		dc.b $F4, $09, $0, 0,$E0
		dc.b $4, $0D, $0, 6,$E0
word_1E04C:	dc.b 1
		dc.b $F8, $05, $0, 0,$F8
word_1E056:	dc.b 1
		dc.b $F8, $05, $0, 4,$F8
word_1E060:	dc.b 1
		dc.b $F8, $05, $0, 8,$F8
word_1E06A:	dc.b 1
		dc.b $F8, $05, $0, $C,$F8
		dc.b 0
		even
; --------------------------------------------------------------------------------
; Dynamic Pattern Loading Cues - Spindash Dust
; --------------------------------------------------------------------------------
DustDynPLC:
		dc word_1E0A0-DustDynPLC	; 0
		dc word_1E0A2-DustDynPLC	; 1
		dc word_1E0A6-DustDynPLC	; 2
		dc word_1E0AA-DustDynPLC	; 3
		dc word_1E0AE-DustDynPLC	; 4
		dc word_1E0B2-DustDynPLC	; 5
		dc word_1E0B6-DustDynPLC	; 6
		dc word_1E0BA-DustDynPLC	; 7
		dc word_1E0BE-DustDynPLC	; 8
		dc word_1E0C2-DustDynPLC	; 9
		dc word_1E0C6-DustDynPLC	; A
		dc word_1E0CA-DustDynPLC	; B
		dc word_1E0CE-DustDynPLC	; C
		dc word_1E0D2-DustDynPLC	; D
		dc word_1E0D8-DustDynPLC	; E
		dc word_1E0DE-DustDynPLC	; F
		dc word_1E0E4-DustDynPLC	; 10
		dc word_1E0EA-DustDynPLC	; 11
		dc word_1E0EA-DustDynPLC	; 12
		dc word_1E0EA-DustDynPLC	; 13
		dc word_1E0EA-DustDynPLC	; 14
		dc word_1E0EC-DustDynPLC	; 15
word_1E0A0:	dc 0
word_1E0A2:	dc 1
		dc $7000
word_1E0A6:	dc 1
		dc $F008
word_1E0AA:	dc 1
		dc $F018
word_1E0AE:	dc 1
		dc $F028
word_1E0B2:	dc 1
		dc $F038
word_1E0B6:	dc 1
		dc $F048
word_1E0BA:	dc 1
		dc $7058
word_1E0BE:	dc 1
		dc $7060
word_1E0C2:	dc 1
		dc $7068
word_1E0C6:	dc 1
		dc $7070
word_1E0CA:	dc 1
		dc $7078
word_1E0CE:	dc 1
		dc $7080
word_1E0D2:	dc 2
		dc $1088
		dc $708A
word_1E0D8:	dc 2
		dc $3092
		dc $7096
word_1E0DE:	dc 2
		dc $509E
		dc $70A4
word_1E0E4:	dc 2
		dc $50AC
		dc $70B2
word_1E0EA:	dc 0
word_1E0EC:	dc 1
		dc $F0BA
		even

Make a new ASM file to put your ported Spindash dust code. I used the name "05 Spindash Dust.asm". Note the "05" in the name, as that's where we'll be putting our object in. Then, head back to "sonic.asm". Paste it right before SonicPlayer (the sonic object) and after Map_WFall. Here's what it should look like:

		...
Map_WFall:	include	"_maps/Waterfalls.asm"
		include	"_incObj/05 Spindash Dust.asm"	; <-- add this line!

; ===========================================================================
; ---------------------------------------------------------------------------
; Object 01 - Sonic
; ---------------------------------------------------------------------------
		...

Now, save and try to build, and... oops! It seems that there's a routine missing: DMA_68KtoVRAM. This routine handles the DMA queue present in Sonic 2, but that queue does not exist in Sonic 1. This queue is used in Sonic 2 to handle the dynamic art for Sonic, Tails, and the dust.

Here's the code to the DMA queue routines in the Sonic 2 Nick Arcade prototype (porting it from the NA disassembly is somewhat... ...easier than porting from the Sonic 2 Final disassembly, so let's use that).

DMA_68KtoVRAM:				; CODE XREF: LoadSonicDynPLC+48?p
					; LoadTailsDynPLC+48?p ...
		movea.l	($FFFFDCFC).w,a1
		cmpa.w	#$DCFC,a1
		beq.s	DMA_68KtoVRAM_NoDMA
		move.w	#$9300,d0
		move.b	d3,d0
		move.w	d0,(a1)+
		move.w	#$9400,d0
		lsr.w	#8,d3
		move.b	d3,d0
		move.w	d0,(a1)+
		move.w	#$9500,d0
		lsr.l	#1,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		move.w	#$9600,d0
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		move.w	#$9700,d0
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		andi.l	#$FFFF,d2
		lsl.l	#2,d2
		lsr.w	#2,d2
		swap	d2
		ori.l	#$40000080,d2
		move.l	d2,(a1)+
		move.l	a1,($FFFFDCFC).w
		cmpa.w	#$DCFC,a1
		beq.s	DMA_68KtoVRAM_NoDMA
		move.w	#0,(a1)

DMA_68KtoVRAM_NoDMA:			; CODE XREF: DMA_68KtoVRAM+8?j
					; DMA_68KtoVRAM+56?j
		rts
; End of function DMA_68KtoVRAM


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


Process_DMA:				; CODE XREF: ROM:00000D9C?p
					; ROM:00000E84?p ...
		lea	($C00004).l,a5
		lea	($FFFFDC00).w,a1

Process_DMA_Loop:			; CODE XREF: Process_DMA+20?j
		move.w	(a1)+,d0
		beq.s	Process_DMA_End
		move.w	d0,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		cmpa.w	#$DCFC,a1
		bne.s	Process_DMA_Loop

Process_DMA_End:			; CODE XREF: Process_DMA+C?j
		move.w	#0,($FFFFDC00).w
		move.l	#$FFFFDC00,($FFFFDCFC).w
		rts
; End of function Process_DMA

Now, a brief explanation(sp?) of how I believe this routine works.

There's a queue that's $100 bytes long, that's stored at $DC00 in 68K RAM. This queue is where the dynamically loaded art for Sonic, Tails, and the dust gets stored (you send the art to the queue by calling DMA_68KtoVRAM), until it's processed and sent to VRAM, which is done when the Process_DMA routine is called.

To port this to Sonic 1, we need to find an appropriate RAM location to fit it, and call Process_DMA at the right location.

So, can we find $100 free bytes of RAM in Sonic 1 to fit this queue?

...Oops, we can't. But let's look at this from another point of view.

In Sonic 2, this queue is used to store the art for both Sonic, Tails, AND the dust. For that reason, it needs $100 bytes. But we only need it to store the dust, and for that, we only actually need $30 bytes.

So, is there any location in RAM where we can stick a $30 bytes-long queue? Yes. We can stick it inside an unused object's SST.

I used the RAM location starting at $FFFFD3C2. If you know any better location, feel free to use it instead.

So we now need to change this routine to point to this new RAM location, as well as reduce its size. To do that, replace all references to $FFFFDC00 with $FFFFD3C2, and $FFFFDCFC with $FFFFD3EE (and similarly, do the same for the "reduced" versions, $DC00->$D3C2, $DCFC->$D3EE).

Here's the routine after this conversion has been done:

DMA_68KtoVRAM:				; CODE XREF: LoadSonicDynPLC+48?p
					; LoadTailsDynPLC+48?p ...
		movea.l	($FFFFD3EE).w,a1
		cmpa.w	#$D3EE,a1
		beq.s	DMA_68KtoVRAM_NoDMA
		move.w	#$9300,d0
		move.b	d3,d0
		move.w	d0,(a1)+
		move.w	#$9400,d0
		lsr.w	#8,d3
		move.b	d3,d0
		move.w	d0,(a1)+
		move.w	#$9500,d0
		lsr.l	#1,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		move.w	#$9600,d0
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		move.w	#$9700,d0
		lsr.l	#8,d1
		move.b	d1,d0
		move.w	d0,(a1)+
		andi.l	#$FFFF,d2
		lsl.l	#2,d2
		lsr.w	#2,d2
		swap	d2
		ori.l	#$40000080,d2
		move.l	d2,(a1)+
		move.l	a1,($FFFFD3EE).w
		cmpa.w	#$D3EE,a1
		beq.s	DMA_68KtoVRAM_NoDMA
		move.w	#0,(a1)

DMA_68KtoVRAM_NoDMA:			; CODE XREF: DMA_68KtoVRAM+8?j
					; DMA_68KtoVRAM+56?j
		rts
; End of function DMA_68KtoVRAM


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


Process_DMA:				; CODE XREF: ROM:00000D9C?p
					; ROM:00000E84?p ...
		lea	($C00004).l,a5
		lea	($FFFFD3C2).w,a1

Process_DMA_Loop:			; CODE XREF: Process_DMA+20?j
		move.w	(a1)+,d0
		beq.s	Process_DMA_End
		move.w	d0,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		move.w	(a1)+,(a5)
		cmpa.w	#$D3EE,a1
		bne.s	Process_DMA_Loop

Process_DMA_End:			; CODE XREF: Process_DMA+C?j
		move.w	#0,($FFFFD3C2).w
		move.l	#$FFFFD3C2,($FFFFD3EE).w
		rts
; End of function Process_DMA

Make a new ASM file called "DMA_68KtoVRAM.asm" and put it in the _inc folder. Then, go to "sonic.asm" and paste it below TilemapToVRAM and above include "_inc/Nemesis Decompression.asm". Here's what you should get:

		...
; End of function TilemapToVRAM

		include	"_inc/DMA_68KtoVRAM.asm"	; <-- add this line!
		include	"_inc/Nemesis Decompression.asm"
		...

Now all that's left to get the queue up and running is to call Process_DMA in the right location. To do that, go to .nochg located under the VBla_08 routine, and add these two lines at the beginning:

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

The queue should now be working.

Now we need to do one more thing, call the actual Spin Dash dust object. To do that, open _inc\Object Pointers.asm, and search for ptr_Obj05. Replace the call to ObjectFall in that line with SpinDash_dust.

The outcome should be something like this:

; ---------------------------------------------------------------------------
; Object pointers
; ---------------------------------------------------------------------------
ptr_SonicPlayer:	dc.l SonicPlayer	; $01
ptr_Obj02:		dc.l NullObject
ptr_Obj03:		dc.l NullObject
ptr_Obj04:		dc.l NullObject
ptr_Obj05:		dc.l SpinDash_dust	; $05
ptr_Obj06:		dc.l NullObject
ptr_Obj07:		dc.l NullObject
ptr_Splash:		dc.l Splash		; $08
ptr_SonicSpecial:	dc.l SonicSpecial
ptr_DrownCount:		dc.l DrownCount
...

This will cause Object ID 05, which was previously unused, to now point to the Spin Dash dust object.

Finally, we need to call the object itself. So go to Sonic_Main, and at the end of the routine (before Sonic_Control), add the following line:

		move.b	#id_Obj05,(v_dustobj).w

This will load the dust object to v_dustobj at address $FFFFD1C0 (addresses from $D000 to $D400 are part of the SST).

Now define v_dustobj in Variables.asm by adding this code between v_shieldobj = v_objspace+object_size*6 and v_starsobj1 = v_objspace+object_size*8:

v_dustobj	= v_objspace+object_size*7	; object variable space for the spin dash dust ($40 bytes)

You should have this:

v_spindust	= v_objspace+object_size*4	; object variable space for the spin dash dust ($40 bytes)
v_shieldobj	= v_objspace+object_size*6	; object variable space for the shield ($40 bytes)
v_dustobj	= v_objspace+object_size*7	; object variable space for the spin dash dust ($40 bytes)
v_starsobj1	= v_objspace+object_size*8	; object variable space for the invincibility stars #1 ($40 bytes)
v_starsobj2	= v_objspace+object_size*9	; object variable space for the invincibility stars #2 ($40 bytes)
v_starsobj3	= v_objspace+object_size*10	; object variable space for the invincibility stars #3 ($40 bytes)
v_starsobj4	= v_objspace+object_size*11	; object variable space for the invincibility stars #4 ($40 bytes)

Finally, we need to have the Spin Dash routine set the dust's animation. To do that, go back to Sonic Spindash.asm, and change the anim under move.b #2,(v_spindust+anim).w to aniDust. You should have this:

		move.b	#2,(v_spindust+aniDust).w	; set Spin Dash dust anim to 2

Now define a new variable under Constants.asm as shown below:

aniDust:		equ $DC	; current animation for spin dash dust

Paste it underneath standonobject: equ $3D. Here's the result:

standonobject:	equ $3D	; object Sonic stands on
aniDust:	equ $DC	; current animation for spin dash dust

Go back to the Spin Dash routine. We could remove the following line below:

		blo.s	+

However, there's a better way to do it. Change the following lines:

		cmpi.b	#12,air_left(a0)	; if he's drowning, branch to not make dust
		blo.s	+

To this:

		cmpi.w	#$C,(v_air).w	; check air remaining
		blo.s	+			; if he's drowning, branch to not make dust

And again, at the "+" label under Sonic_UpdateSpindash, change the anim under move.b #2,(v_spindust+anim).w to aniDust. You should have this:

		move.b	#0,(v_spindust+aniDust).w	; clear Spin Dash dust anim

And finally, add the following line at the "+" label under Sonic_ChargingSpindash, right before the jsr to PlaySound_Special:

		move.b	#2,(v_spindust+aniDust).w	; set Spin Dash dust anim to 2

For reference, here's the complete Sonic_SpinDash routine:

; ---------------------------------------------------------------------------
; Subroutine to make Sonic perform 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,anim(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,anim(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	#$C,(v_air).w	; check air remaining
		blo.s	+			; if he's drowning, branch to not make dust
		move.b	#2,(v_spindust+aniDust).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,anim(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,y_radius(a0)	; y_radius(a0) is height/2
		move.b	#7,x_radius(a0)		; x_radius(a0) is width/2
		move.b	#id_Roll,anim(a0)	; set animation to roll
		addq.w	#5,y_pos(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),inertia(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	inertia(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,($FFFFEED0).w	; move to $EED0

		btst	#0,status(a0)	; is sonic facing right?
		beq.s	+		; if not, branch
		neg.w	inertia(a0)	; negate inertia
+
		bset	#2,status(a0)	; set unused (in s1) flag
		move.b	#0,(v_spindust+aniDust).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),anim(a0)	; reset spindash animation
		move.w	#sfx_SpinDash,d0	; was $E0 in sonic 2
		move.b	#2,(v_spindust+aniDust).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

Now, build the ROM, and the result is... oops, something's missing. Right, we forgot to add the dust art.

So download the Spin Dash dust art here (put it in the artunc\ folder inside your source dir), and at the end of the ROM, after SoundDriver: include "s1.sounddriver.asm", add the following lines:

Art_Dust:	binclude	"artunc/spindust.bin"
		even

Now compile it and try a Spin Dash:

SpindashGuide Pic2.png

Nice. It seems to be working...

SpindashGuide Pic3.png

...or not.

I'm pretty sure the lamppost wasn't meant to look like THAT.

The problem here seems to be that the dust art is overwriting the lamppost art. To fix this, we need to move the lamppost art to a different location. For that, we can use the VRAM location $D800, which is free.

So open the file Constants.asm, and search for this line:

ArtTile_Lamppost:		equ $7A0

Change it to:

ArtTile_Lamppost:		equ ($D800/$20)

This will load the lamppost art in $D800 in VRAM, instead of $F400.

...and that's it. Compile your ROM and we should now have perfect Spin Dash in Sonic 1.

SpindashGuide Pic4.png

Here's the final result of this thing. Have fun :)

Continuation

See this part by shobiz for some additional fixes that are not covered in this guide.

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 | Port Sonic 2 Level Select
Adding Features
Add Spin Dash ( Part 1 (GitHub)/(Hivebrain) / Part 2 / Part 3 / Part 4 ) | Add Eggman Monitor | Add Super Sonic | Add the Air Roll
Sound Features
Expand the Sound Index | Play Different Songs Per Act | Port Sonic 2 Final Sound Driver | Port Sonic 3's Sound Driver | Port Flamewing's Sonic 3 & Knuckles Sound Driver | Change The SEGA Sound
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 2]]