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.
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:
Nice, it seems to be working...
...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 _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.
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...
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:
Nice. It seems to be working...
...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.
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.
|Add Spin Dash to Sonic 1/Part 2]]