Difference between revisions of "Add Spin Dash to Sonic 1/Part 3"
From Sonic Retro
Scarred Sun (talk | contribs) m (Text replace - '[[Category:SCHG How-tos|' to '{{S1Howtos}} [[Category:SCHG How-tos|') |
Makotoyuki (talk | contribs) |
||
(11 intermediate revisions by 7 users not shown) | |||
Line 6: | Line 6: | ||
This isn't exactly a bug per se, but the method used to port over the DMA queue to Sonic 1 in Puto's guide is slightly hack-ish. In stock ''Sonic 1'', Sonic's sprites are copied over into a RAM buffer at $FFFFC800 before being DMAed to VRAM. This buffer occupies $300 bytes, so if we convert it to a DMA queue, besides getting a proper structure we also save $200 bytes of RAM (some of which will be used later on in this guide). | This isn't exactly a bug per se, but the method used to port over the DMA queue to Sonic 1 in Puto's guide is slightly hack-ish. In stock ''Sonic 1'', Sonic's sprites are copied over into a RAM buffer at $FFFFC800 before being DMAed to VRAM. This buffer occupies $300 bytes, so if we convert it to a DMA queue, besides getting a proper structure we also save $200 bytes of RAM (some of which will be used later on in this guide). | ||
− | Firstly, let's modify the actual queue routines to use this RAM space. So go to DMA_68KtoVRAM and replace everything from there till the end of Process_DMA with: | + | Firstly, let's modify the actual queue routines to use this RAM space. So go to DMA_68KtoVRAM (you'll only have this once you complete part 2, if you haven't just paste this anywhere in the code) and replace everything from there till the end of Process_DMA with: |
− | <asm>; --------------------------------------------------------------------------- | + | <syntaxhighlight lang="asm">; --------------------------------------------------------------------------- |
; Subroutine for queueing VDP commands (seems to only queue transfers to VRAM), | ; Subroutine for queueing VDP commands (seems to only queue transfers to VRAM), | ||
; to be issued the next time ProcessDMAQueue is called. | ; to be issued the next time ProcessDMAQueue is called. | ||
; Can be called a maximum of 18 times before the buffer needs to be cleared | ; Can be called a maximum of 18 times before the buffer needs to be cleared | ||
; by issuing the commands (this subroutine DOES check for overflow) | ; by issuing the commands (this subroutine DOES check for overflow) | ||
+ | ; --------------------------------------------------------------------------- | ||
+ | ; In case you wish to use this queue system outside of the spin dash, this is the | ||
+ | ; registers in which it expects data in: | ||
+ | ; d1.l: Address to data (In 68k address space) | ||
+ | ; d2.w: Destination in VRAM | ||
+ | ; d3.w: Length of data | ||
; --------------------------------------------------------------------------- | ; --------------------------------------------------------------------------- | ||
Line 96: | Line 102: | ||
move.l #$FFFFC800,($FFFFC8FC).w | move.l #$FFFFC800,($FFFFC8FC).w | ||
rts | rts | ||
− | ; End of function ProcessDMAQueue</ | + | ; End of function ProcessDMAQueue</syntaxhighlight> |
Next, let's modify Sonic's PLC routine to make use of this routine. Go to LoadSonicDynPLC and replace the entire routine with the ''Sonic 2'' equivalent, slightly modified to make use of ''Sonic 1''<nowiki>'</nowiki>s PLC format: | Next, let's modify Sonic's PLC routine to make use of this routine. Go to LoadSonicDynPLC and replace the entire routine with the ''Sonic 2'' equivalent, slightly modified to make use of ''Sonic 1''<nowiki>'</nowiki>s PLC format: | ||
− | <asm>LoadSonicDynPLC: ; XREF: Obj01_Control; et al | + | <syntaxhighlight lang="asm">LoadSonicDynPLC: ; XREF: Obj01_Control; et al |
moveq #0,d0 | moveq #0,d0 | ||
move.b $1A(a0),d0 ; load frame number | move.b $1A(a0),d0 ; load frame number | ||
Line 136: | Line 142: | ||
locret_13C96: | locret_13C96: | ||
rts | rts | ||
− | ; End of function LoadSonicDynPLC</ | + | ; End of function LoadSonicDynPLC</syntaxhighlight> |
Third, we have to make the dust object use the new name for the DMA routine. Really simple, just go to loc_1DF0A and replace | Third, we have to make the dust object use the new name for the DMA routine. Really simple, just go to loc_1DF0A and replace | ||
− | <asm> jsr (DMA_68KtoVRAM).l</ | + | <syntaxhighlight lang="asm"> jsr (DMA_68KtoVRAM).l</syntaxhighlight> |
with | with | ||
− | <asm> jsr (QueueDMATransfer).l</ | + | <syntaxhighlight lang="asm"> jsr (QueueDMATransfer).l</syntaxhighlight> |
Finally, we have to make the game actually use these routines. Firstly, go to loc_CD4 and replace | Finally, we have to make the game actually use these routines. Firstly, go to loc_CD4 and replace | ||
− | <asm> tst.b ($FFFFF767).w | + | <syntaxhighlight lang="asm"> tst.b ($FFFFF767).w |
beq.s loc_D50 | beq.s loc_D50 | ||
lea ($C00004).l,a5 | lea ($C00004).l,a5 | ||
Line 157: | Line 163: | ||
move.w #$83,($FFFFF640).w | move.w #$83,($FFFFF640).w | ||
move.w ($FFFFF640).w,(a5) | move.w ($FFFFF640).w,(a5) | ||
− | move.b #0,($FFFFF767).w</ | + | move.b #0,($FFFFF767).w</syntaxhighlight> |
with | with | ||
− | <asm> jsr (ProcessDMAQueue).l</ | + | <syntaxhighlight lang="asm"> jsr (ProcessDMAQueue).l</syntaxhighlight> |
Make similar changes at loc_DAE, loc_EEE and loc_FAE. Secondly, go to loc_D50 and remove these two lines: | Make similar changes at loc_DAE, loc_EEE and loc_FAE. Secondly, go to loc_D50 and remove these two lines: | ||
− | <asm> move #$83,($FFFFF640).w | + | <syntaxhighlight lang="asm"> move #$83,($FFFFF640).w |
− | jsr Process_DMA</ | + | jsr Process_DMA</syntaxhighlight> |
Thirdly, go to Level_ClrVars3 and after the | Thirdly, go to Level_ClrVars3 and after the | ||
− | <asm> move.w #$8ADF,($FFFFF624).w | + | <syntaxhighlight lang="asm"> move.w #$8ADF,($FFFFF624).w |
− | move.w ($FFFFF624).w,(a6)</ | + | move.w ($FFFFF624).w,(a6)</syntaxhighlight> |
add | add | ||
− | <asm> clr.w ($FFFFC800).w | + | <syntaxhighlight lang="asm"> clr.w ($FFFFC800).w |
− | move.l #$FFFFC800,($FFFFC8FC).w</ | + | move.l #$FFFFC800,($FFFFC8FC).w</syntaxhighlight> |
Finally, go to loc_47D4, and after | Finally, go to loc_47D4, and after | ||
− | <asm> jsr Hud_Base</ | + | <syntaxhighlight lang="asm"> jsr Hud_Base</syntaxhighlight> |
add | add | ||
− | <asm> clr.w ($FFFFC800).w | + | <syntaxhighlight lang="asm"> clr.w ($FFFFC800).w |
− | move.l #$FFFFC800,($FFFFC8FC).w</ | + | move.l #$FFFFC800,($FFFFC8FC).w</syntaxhighlight> |
After this, the game will use the DMA queue for Sonic's art reloading, and we'll have $200 bytes of free RAM from $FFFFC900 to $FFFFCAFF. | After this, the game will use the DMA queue for Sonic's art reloading, and we'll have $200 bytes of free RAM from $FFFFC900 to $FFFFCAFF. | ||
== Making the Spin Dash sound rev == | == Making the Spin Dash sound rev == | ||
− | This has always annoyed me a lot, but thanks to [[Xenowhirl|Xenowhirl's | + | This has always annoyed me a lot, but thanks to [[User:Xenowhirl|Xenowhirl]]'s ''Sonic 2'' sound driver [[Disassemblies#Revision_01|disassembly]] and [[User:Tweaker|Tweaker]]'s [[SCHG:Music Hacking|music guide]], we can fix it. We need to use three RAM variables, so we can utilise our newly freed RAM. $FFFFC900 is a flag which is 1 if the Spin Dash sound was the last one played and 0 if it was not. $FFFFC901 is a timer, and $FFFFC902 is the pitch increase value. |
First, go to loc_71BC8 and change it from | First, go to loc_71BC8 and change it from | ||
− | <asm>loc_71BC8: | + | <syntaxhighlight lang="asm">loc_71BC8: |
lea $40(a6),a5 | lea $40(a6),a5 | ||
tst.b (a5) | tst.b (a5) | ||
bpl.s loc_71BD4 | bpl.s loc_71BD4 | ||
− | jsr sub_71C4E(pc)</ | + | jsr sub_71C4E(pc)</syntaxhighlight> |
to | to | ||
− | <asm>loc_71BC8: | + | <syntaxhighlight lang="asm">loc_71BC8: |
tst.b ($FFFFC901).w | tst.b ($FFFFC901).w | ||
beq.s @cont | beq.s @cont | ||
Line 211: | Line 217: | ||
tst.b (a5) | tst.b (a5) | ||
bpl.s loc_71BD4 | bpl.s loc_71BD4 | ||
− | jsr sub_71C4E(pc)</ | + | jsr sub_71C4E(pc)</syntaxhighlight> |
Then go to Sound_D1toDF and change if from | Then go to Sound_D1toDF and change if from | ||
− | <asm>Sound_D1toDF: | + | <syntaxhighlight lang="asm">Sound_D1toDF: |
tst.b $27(a6) | tst.b $27(a6) | ||
bne.w loc_722C6 | bne.w loc_722C6 | ||
Line 224: | Line 230: | ||
movea.l (Go_SoundIndex).l,a0 | movea.l (Go_SoundIndex).l,a0 | ||
sub.b #$A1,d7 | sub.b #$A1,d7 | ||
− | bra SoundEffects_Common</ | + | bra SoundEffects_Common</syntaxhighlight> |
to | to | ||
− | <asm>Sound_D1toDF: | + | <syntaxhighlight lang="asm">Sound_D1toDF: |
tst.b $27(a6) | tst.b $27(a6) | ||
bne.w loc_722C6 | bne.w loc_722C6 | ||
Line 258: | Line 264: | ||
movea.l (Go_SoundIndex).l,a0 | movea.l (Go_SoundIndex).l,a0 | ||
sub.b #$A1,d7 | sub.b #$A1,d7 | ||
− | bra SoundEffects_Common</ | + | bra SoundEffects_Common</syntaxhighlight> |
After this, go to Sound_A0toCF and after | After this, go to Sound_A0toCF and after | ||
− | <asm> tst.b $24(a6) | + | <syntaxhighlight lang="asm"> tst.b $24(a6) |
− | bne.w loc_722C6</ | + | bne.w loc_722C6</syntaxhighlight> |
add | add | ||
− | <asm> clr.b ($FFFFC900).w</ | + | <syntaxhighlight lang="asm"> clr.b ($FFFFC900).w</syntaxhighlight> |
Then go to loc_7226E and replace | Then go to loc_7226E and replace | ||
− | <asm> movea.l dword_722EC(pc,d3.w),a5</ | + | <syntaxhighlight lang="asm"> movea.l dword_722EC(pc,d3.w),a5</syntaxhighlight> |
with | with | ||
− | <asm> lea dword_722EC(pc),a5 | + | <syntaxhighlight lang="asm"> lea dword_722EC(pc),a5 |
− | movea.l (a5,d3.w),a5</ | + | movea.l (a5,d3.w),a5</syntaxhighlight> |
Finally, go to loc_72276 and change | Finally, go to loc_72276 and change | ||
− | <asm> move.w (a1)+,8(a5) | + | <syntaxhighlight lang="asm"> move.w (a1)+,8(a5) |
− | move.b #1,$E(a5)</ | + | move.b #1,$E(a5)</syntaxhighlight> |
to | to | ||
− | <asm> move.w (a1)+,8(a5) | + | <syntaxhighlight lang="asm"> move.w (a1)+,8(a5) |
tst.b ($FFFFC900).w ; is the Spin Dash sound playing? | tst.b ($FFFFC900).w ; is the Spin Dash sound playing? | ||
beq.s @cont ; if not, branch | beq.s @cont ; if not, branch | ||
Line 294: | Line 300: | ||
@cont: | @cont: | ||
− | move.b #1,$E(a5)</ | + | move.b #1,$E(a5)</syntaxhighlight> |
and now the Spin Dash sound revs. | and now the Spin Dash sound revs. | ||
Line 301: | Line 307: | ||
''Sonic 2''<nowiki>'</nowiki>s Spin Dash implements a horizontal scroll delay, while the ported one doesn't (yet). The basic idea is to check a certain flag, and if it's non-zero, base scrolling on Sonic's X position a certain number of frames ago rather than his current X position. (Note that this feature can cause some problems with object interaction). So go to loc2_1ACD0 and change | ''Sonic 2''<nowiki>'</nowiki>s Spin Dash implements a horizontal scroll delay, while the ported one doesn't (yet). The basic idea is to check a certain flag, and if it's non-zero, base scrolling on Sonic's X position a certain number of frames ago rather than his current X position. (Note that this feature can cause some problems with object interaction). So go to loc2_1ACD0 and change | ||
− | <asm> move.w d0,($FFFFEED0).w ; move to $EED0</ | + | <syntaxhighlight lang="asm"> move.w d0,($FFFFEED0).w ; move to $EED0</syntaxhighlight> |
to | to | ||
− | <asm> move.w d0,($FFFFC904).w ; move to $C904</ | + | <syntaxhighlight lang="asm"> move.w d0,($FFFFC904).w ; move to $C904</syntaxhighlight> |
And then go to ScrollHoriz2 and change | And then go to ScrollHoriz2 and change | ||
− | <asm>ScrollHoriz2: | + | <syntaxhighlight lang="asm">ScrollHoriz2: |
move.w ($FFFFD008).w,d0 | move.w ($FFFFD008).w,d0 | ||
− | sub.w ($FFFFF700).w,d0</ | + | sub.w ($FFFFF700).w,d0</syntaxhighlight> |
to | to | ||
− | <asm>ScrollHoriz2: ; XREF: ScrollHoriz | + | <syntaxhighlight lang="asm">ScrollHoriz2: ; XREF: ScrollHoriz |
move.w ($FFFFC904).w,d1 | move.w ($FFFFC904).w,d1 | ||
beq.s @cont1 | beq.s @cont1 | ||
Line 335: | Line 341: | ||
@cont2: | @cont2: | ||
− | sub.w ($FFFFF700).w,d0</ | + | sub.w ($FFFFF700).w,d0</syntaxhighlight> |
And then, to fix a bug with backwards horizontal scrolling, go to loc_65F6 and change it from | And then, to fix a bug with backwards horizontal scrolling, go to loc_65F6 and change it from | ||
− | <asm>loc_65F6: ; XREF: ScrollHoriz2 | + | <syntaxhighlight lang="asm">loc_65F6: ; XREF: ScrollHoriz2 |
add.w ($FFFFF700).w,d0 | add.w ($FFFFF700).w,d0 | ||
cmp.w ($FFFFF728).w,d0 | cmp.w ($FFFFF728).w,d0 | ||
bgt.s loc_65E4 | bgt.s loc_65E4 | ||
move.w ($FFFFF728).w,d0 | move.w ($FFFFF728).w,d0 | ||
− | bra.s loc_65E4</ | + | bra.s loc_65E4</syntaxhighlight> |
to | to | ||
− | <asm>loc_65F6: ; XREF: ScrollHoriz2 | + | <syntaxhighlight lang="asm">loc_65F6: ; XREF: ScrollHoriz2 |
cmpi.w #-$10,d0 | cmpi.w #-$10,d0 | ||
bgt.s @cont | bgt.s @cont | ||
Line 358: | Line 364: | ||
bgt.s loc_65E4 | bgt.s loc_65E4 | ||
move.w ($FFFFF728).w,d0 | move.w ($FFFFF728).w,d0 | ||
− | bra.s loc_65E4</ | + | bra.s loc_65E4</syntaxhighlight> |
to limit the maximum number of pixels the camera can scroll back by in a single frame. | to limit the maximum number of pixels the camera can scroll back by in a single frame. | ||
Line 365: | Line 371: | ||
In stock ''Sonic 1'', the camera scrolls vertically as soon as Sonic looks up or ducks. This can be a bit annoying for the Spin Dash, however, so let's implement the delay present in ''Sonic 2'' by using $FFFFC903 as our delay counter. Firstly, to fix a bit of incorrect code, go to loc2_1AD78 and change | In stock ''Sonic 1'', the camera scrolls vertically as soon as Sonic looks up or ducks. This can be a bit annoying for the Spin Dash, however, so let's implement the delay present in ''Sonic 2'' by using $FFFFC903 as our delay counter. Firstly, to fix a bit of incorrect code, go to loc2_1AD78 and change | ||
− | <asm>loc2_1AD78: | + | <syntaxhighlight lang="asm">loc2_1AD78: |
addq.l #4,sp ; increase stack ptr | addq.l #4,sp ; increase stack ptr | ||
cmpi.w #$60,($FFFFEED8).w ; $EED8 only ever seems | cmpi.w #$60,($FFFFEED8).w ; $EED8 only ever seems | ||
Line 379: | Line 385: | ||
bsr.w Sonic_AnglePos | bsr.w Sonic_AnglePos | ||
move.w #$60,($FFFFF73E).w ; reset looking up/down | move.w #$60,($FFFFF73E).w ; reset looking up/down | ||
− | rts</ | + | rts</syntaxhighlight> |
to | to | ||
− | <asm>loc2_1AD78: | + | <syntaxhighlight lang="asm">loc2_1AD78: |
addq.l #4,sp ; increase stack ptr | addq.l #4,sp ; increase stack ptr | ||
cmpi.w #$60,($FFFFF73E).w | cmpi.w #$60,($FFFFF73E).w | ||
Line 397: | Line 403: | ||
bsr.w Sonic_AnglePos | bsr.w Sonic_AnglePos | ||
;move.w #$60,($FFFFF73E).w ; reset looking up/down | ;move.w #$60,($FFFFF73E).w ; reset looking up/down | ||
− | rts</ | + | rts</syntaxhighlight> |
Then go to Sonic_LookUp and change | Then go to Sonic_LookUp and change | ||
− | <asm>Sonic_LookUp: | + | <syntaxhighlight lang="asm">Sonic_LookUp: |
btst #0,($FFFFF602).w ; is up being pressed? | btst #0,($FFFFF602).w ; is up being pressed? | ||
beq.s Sonic_Duck ; if not, branch | beq.s Sonic_Duck ; if not, branch | ||
Line 428: | Line 434: | ||
loc_12FBE: | loc_12FBE: | ||
− | subq.w #2,($FFFFF73E).w ; move screen back to default</ | + | subq.w #2,($FFFFF73E).w ; move screen back to default</syntaxhighlight> |
to | to | ||
− | <asm>Sonic_LookUp: | + | <syntaxhighlight lang="asm">Sonic_LookUp: |
btst #0,($FFFFF602).w ; is up being pressed? | btst #0,($FFFFF602).w ; is up being pressed? | ||
beq.s Sonic_Duck ; if not, branch | beq.s Sonic_Duck ; if not, branch | ||
Line 470: | Line 476: | ||
loc_12FBE: | loc_12FBE: | ||
− | subq.w #2,($FFFFF73E).w ; move screen back to default</ | + | subq.w #2,($FFFFF73E).w ; move screen back to default</syntaxhighlight> |
Finally, to make the screen bias gradually reset during rolling, go to loc_131CC and change | Finally, to make the screen bias gradually reset during rolling, go to loc_131CC and change | ||
− | <asm>loc_131CC: | + | <syntaxhighlight lang="asm">loc_131CC: |
move.b $26(a0),d0 | move.b $26(a0),d0 | ||
− | jsr (CalcSine).l</ | + | jsr (CalcSine).l</syntaxhighlight> |
to | to | ||
− | <asm>loc_131CC: | + | <syntaxhighlight lang="asm">loc_131CC: |
cmp.w #$60,($FFFFF73E).w | cmp.w #$60,($FFFFF73E).w | ||
beq.s @cont2 | beq.s @cont2 | ||
Line 491: | Line 497: | ||
@cont2: | @cont2: | ||
move.b $26(a0),d0 | move.b $26(a0),d0 | ||
− | jsr (CalcSine).l</ | + | jsr (CalcSine).l</syntaxhighlight> |
And that's it! Let me know if I made any mistakes, or if there's anything still not covered. [[Media:s1spindashrom.zip|Here's]] a ROM after all these steps have been done, and [[Media:s1spindashasm.zip|here's]] the source code (with Puto's SEGA sound fix implemented instead of Esrael's for ASM68k compatibility). | And that's it! Let me know if I made any mistakes, or if there's anything still not covered. [[Media:s1spindashrom.zip|Here's]] a ROM after all these steps have been done, and [[Media:s1spindashasm.zip|here's]] the source code (with Puto's SEGA sound fix implemented instead of Esrael's for ASM68k compatibility). | ||
Line 497: | Line 503: | ||
== Final code == | == Final code == | ||
Here is the final Spin Dash code for references: | Here is the final Spin Dash code for references: | ||
− | <asm>Sonic_SpinDash: | + | <syntaxhighlight lang="asm">Sonic_SpinDash: |
tst.b $39(a0) ; already Spin Dashing? | tst.b $39(a0) ; already Spin Dashing? | ||
bne.s loc2_1AC8E ; if set, branch | bne.s loc2_1AC8E ; if set, branch | ||
Line 621: | Line 627: | ||
rts | rts | ||
; End of function Sonic_SpinDash | ; End of function Sonic_SpinDash | ||
− | </ | + | </syntaxhighlight> |
+ | there is one [[SCHG How-to:Add Spin Dash to Sonic 1/Part 4|final part]] added by [[User:Mercury|Mercury]] that addresses some final camera bugs. | ||
{{S1Howtos}} | {{S1Howtos}} | ||
− | + | |Add Spin Dash to Sonic 1/Part 3]] |
Latest revision as of 02:01, 19 July 2022
(Original guide by shobiz)
While the end result of following Lightning's and Puto's guides is a near-perfect Spin Dash in Sonic 1, there are still a few things not right about it, so let's fix those.
Contents
Converting Sonic's art buffer to a DMA queue
This isn't exactly a bug per se, but the method used to port over the DMA queue to Sonic 1 in Puto's guide is slightly hack-ish. In stock Sonic 1, Sonic's sprites are copied over into a RAM buffer at $FFFFC800 before being DMAed to VRAM. This buffer occupies $300 bytes, so if we convert it to a DMA queue, besides getting a proper structure we also save $200 bytes of RAM (some of which will be used later on in this guide).
Firstly, let's modify the actual queue routines to use this RAM space. So go to DMA_68KtoVRAM (you'll only have this once you complete part 2, if you haven't just paste this anywhere in the code) and replace everything from there till the end of Process_DMA with:
; ---------------------------------------------------------------------------
; Subroutine for queueing VDP commands (seems to only queue transfers to VRAM),
; to be issued the next time ProcessDMAQueue is called.
; Can be called a maximum of 18 times before the buffer needs to be cleared
; by issuing the commands (this subroutine DOES check for overflow)
; ---------------------------------------------------------------------------
; In case you wish to use this queue system outside of the spin dash, this is the
; registers in which it expects data in:
; d1.l: Address to data (In 68k address space)
; d2.w: Destination in VRAM
; d3.w: Length of data
; ---------------------------------------------------------------------------
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; sub_144E: DMA_68KtoVRAM: QueueCopyToVRAM: QueueVDPCommand: Add_To_DMA_Queue:
QueueDMATransfer:
movea.l ($FFFFC8FC).w,a1
cmpa.w #$C8FC,a1
beq.s QueueDMATransfer_Done ; return if there's no more room in the buffer
; piece together some VDP commands and store them for later...
move.w #$9300,d0 ; command to specify DMA transfer length & $00FF
move.b d3,d0
move.w d0,(a1)+ ; store command
move.w #$9400,d0 ; command to specify DMA transfer length & $FF00
lsr.w #8,d3
move.b d3,d0
move.w d0,(a1)+ ; store command
move.w #$9500,d0 ; command to specify source address & $0001FE
lsr.l #1,d1
move.b d1,d0
move.w d0,(a1)+ ; store command
move.w #$9600,d0 ; command to specify source address & $01FE00
lsr.l #8,d1
move.b d1,d0
move.w d0,(a1)+ ; store command
move.w #$9700,d0 ; command to specify source address & $FE0000
lsr.l #8,d1
move.b d1,d0
move.w d0,(a1)+ ; store command
andi.l #$FFFF,d2 ; command to specify destination address and begin DMA
lsl.l #2,d2
lsr.w #2,d2
swap d2
ori.l #$40000080,d2 ; set bits to specify VRAM transfer
move.l d2,(a1)+ ; store command
move.l a1,($FFFFC8FC).w ; set the next free slot address
cmpa.w #$C8FC,a1
beq.s QueueDMATransfer_Done ; return if there's no more room in the buffer
move.w #0,(a1) ; put a stop token at the end of the used part of the buffer
; return_14AA:
QueueDMATransfer_Done:
rts
; End of function QueueDMATransfer
; ---------------------------------------------------------------------------
; Subroutine for issuing all VDP commands that were queued
; (by earlier calls to QueueDMATransfer)
; Resets the queue when it's done
; ---------------------------------------------------------------------------
; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
; sub_14AC: CopyToVRAM: IssueVDPCommands: Process_DMA: Process_DMA_Queue:
ProcessDMAQueue:
lea ($C00004).l,a5
lea ($FFFFC800).w,a1
; loc_14B6:
ProcessDMAQueue_Loop:
move.w (a1)+,d0
beq.s ProcessDMAQueue_Done ; branch if we reached a stop token
; issue a set of VDP commands...
move.w d0,(a5) ; transfer length
move.w (a1)+,(a5) ; transfer length
move.w (a1)+,(a5) ; source address
move.w (a1)+,(a5) ; source address
move.w (a1)+,(a5) ; source address
move.w (a1)+,(a5) ; destination
move.w (a1)+,(a5) ; destination
cmpa.w #$C8FC,a1
bne.s ProcessDMAQueue_Loop ; loop if we haven't reached the end of the buffer
; loc_14CE:
ProcessDMAQueue_Done:
move.w #0,($FFFFC800).w
move.l #$FFFFC800,($FFFFC8FC).w
rts
; End of function ProcessDMAQueue
Next, let's modify Sonic's PLC routine to make use of this routine. Go to LoadSonicDynPLC and replace the entire routine with the Sonic 2 equivalent, slightly modified to make use of Sonic 1's PLC format:
LoadSonicDynPLC: ; XREF: Obj01_Control; et al
moveq #0,d0
move.b $1A(a0),d0 ; load frame number
cmp.b ($FFFFF766).w,d0
beq.s locret_13C96
move.b d0,($FFFFF766).w
lea (SonicDynPLC).l,a2
add.w d0,d0
adda.w (a2,d0.w),a2
moveq #0,d5
move.b (a2)+,d5
subq.w #1,d5
bmi.s locret_13C96
move.w #$F000,d4
move.l #Art_Sonic,d6
SPLC_ReadEntry:
moveq #0,d1
move.b (a2)+,d1
lsl.w #8,d1
move.b (a2)+,d1
move.w d1,d3
lsr.w #8,d3
andi.w #$F0,d3
addi.w #$10,d3
andi.w #$FFF,d1
lsl.l #5,d1
add.l d6,d1
move.w d4,d2
add.w d3,d4
add.w d3,d4
jsr (QueueDMATransfer).l
dbf d5,SPLC_ReadEntry ; repeat for number of entries
locret_13C96:
rts
; End of function LoadSonicDynPLC
Third, we have to make the dust object use the new name for the DMA routine. Really simple, just go to loc_1DF0A and replace
jsr (DMA_68KtoVRAM).l
with
jsr (QueueDMATransfer).l
Finally, we have to make the game actually use these routines. Firstly, go to loc_CD4 and replace
tst.b ($FFFFF767).w
beq.s loc_D50
lea ($C00004).l,a5
move.l #$94019370,(a5)
move.l #$96E49500,(a5)
move.w #$977F,(a5)
move.w #$7000,(a5)
move.w #$83,($FFFFF640).w
move.w ($FFFFF640).w,(a5)
move.b #0,($FFFFF767).w
with
jsr (ProcessDMAQueue).l
Make similar changes at loc_DAE, loc_EEE and loc_FAE. Secondly, go to loc_D50 and remove these two lines:
move #$83,($FFFFF640).w
jsr Process_DMA
Thirdly, go to Level_ClrVars3 and after the
move.w #$8ADF,($FFFFF624).w
move.w ($FFFFF624).w,(a6)
add
clr.w ($FFFFC800).w
move.l #$FFFFC800,($FFFFC8FC).w
Finally, go to loc_47D4, and after
jsr Hud_Base
add
clr.w ($FFFFC800).w
move.l #$FFFFC800,($FFFFC8FC).w
After this, the game will use the DMA queue for Sonic's art reloading, and we'll have $200 bytes of free RAM from $FFFFC900 to $FFFFCAFF.
Making the Spin Dash sound rev
This has always annoyed me a lot, but thanks to Xenowhirl's Sonic 2 sound driver disassembly and Tweaker's music guide, we can fix it. We need to use three RAM variables, so we can utilise our newly freed RAM. $FFFFC900 is a flag which is 1 if the Spin Dash sound was the last one played and 0 if it was not. $FFFFC901 is a timer, and $FFFFC902 is the pitch increase value.
First, go to loc_71BC8 and change it from
loc_71BC8:
lea $40(a6),a5
tst.b (a5)
bpl.s loc_71BD4
jsr sub_71C4E(pc)
to
loc_71BC8:
tst.b ($FFFFC901).w
beq.s @cont
subq.b #1,($FFFFC901).w
@cont:
lea $40(a6),a5
tst.b (a5)
bpl.s loc_71BD4
jsr sub_71C4E(pc)
Then go to Sound_D1toDF and change if from
Sound_D1toDF:
tst.b $27(a6)
bne.w loc_722C6
tst.b 4(a6)
bne.w loc_722C6
tst.b $24(a6)
bne.w loc_722C6
movea.l (Go_SoundIndex).l,a0
sub.b #$A1,d7
bra SoundEffects_Common
to
Sound_D1toDF:
tst.b $27(a6)
bne.w loc_722C6
tst.b 4(a6)
bne.w loc_722C6
tst.b $24(a6)
bne.w loc_722C6
clr.b ($FFFFC900).w
cmp.b #$D1,d7 ; is this the Spin Dash sound?
bne.s @cont3 ; if not, branch
move.w d0,-(sp)
move.b ($FFFFC902).w,d0 ; store extra frequency
tst.b ($FFFFC901).w ; is the Spin Dash timer active?
bne.s @cont1 ; if it is, branch
move.b #-1,d0 ; otherwise, reset frequency (becomes 0 on next line)
@cont1:
addq.b #1,d0
cmp.b #$C,d0 ; has the limit been reached?
bcc.s @cont2 ; if it has, branch
move.b d0,($FFFFC902).w ; otherwise, set new frequency
@cont2:
move.b #1,($FFFFC900).w ; set flag
move.b #60,($FFFFC901).w ; set timer
move.w (sp)+,d0
@cont3:
movea.l (Go_SoundIndex).l,a0
sub.b #$A1,d7
bra SoundEffects_Common
After this, go to Sound_A0toCF and after
tst.b $24(a6)
bne.w loc_722C6
add
clr.b ($FFFFC900).w
Then go to loc_7226E and replace
movea.l dword_722EC(pc,d3.w),a5
with
lea dword_722EC(pc),a5
movea.l (a5,d3.w),a5
Finally, go to loc_72276 and change
move.w (a1)+,8(a5)
move.b #1,$E(a5)
to
move.w (a1)+,8(a5)
tst.b ($FFFFC900).w ; is the Spin Dash sound playing?
beq.s @cont ; if not, branch
move.w d0,-(sp)
move.b ($FFFFC902).w,d0
add.b d0,8(a5)
move.w (sp)+,d0
@cont:
move.b #1,$E(a5)
and now the Spin Dash sound revs.
Horizontal scroll delay
Sonic 2's Spin Dash implements a horizontal scroll delay, while the ported one doesn't (yet). The basic idea is to check a certain flag, and if it's non-zero, base scrolling on Sonic's X position a certain number of frames ago rather than his current X position. (Note that this feature can cause some problems with object interaction). So go to loc2_1ACD0 and change
move.w d0,($FFFFEED0).w ; move to $EED0
to
move.w d0,($FFFFC904).w ; move to $C904
And then go to ScrollHoriz2 and change
ScrollHoriz2:
move.w ($FFFFD008).w,d0
sub.w ($FFFFF700).w,d0
to
ScrollHoriz2: ; XREF: ScrollHoriz
move.w ($FFFFC904).w,d1
beq.s @cont1
sub.w #$100,d1
move.w d1,($FFFFC904).w
moveq #0,d1
move.b ($FFFFC904).w,d1
lsl.b #2,d1
addq.b #4,d1
move.w ($FFFFF7A8).w,d0
sub.b d1,d0
lea ($FFFFCB00).w,a1
move.w (a1,d0.w),d0
and.w #$3FFF,d0
bra.s @cont2
@cont1:
move.w ($FFFFD008).w,d0
@cont2:
sub.w ($FFFFF700).w,d0
And then, to fix a bug with backwards horizontal scrolling, go to loc_65F6 and change it from
loc_65F6: ; XREF: ScrollHoriz2
add.w ($FFFFF700).w,d0
cmp.w ($FFFFF728).w,d0
bgt.s loc_65E4
move.w ($FFFFF728).w,d0
bra.s loc_65E4
to
loc_65F6: ; XREF: ScrollHoriz2
cmpi.w #-$10,d0
bgt.s @cont
move.w #-$10,d0
@cont:
add.w ($FFFFF700).w,d0
cmp.w ($FFFFF728).w,d0
bgt.s loc_65E4
move.w ($FFFFF728).w,d0
bra.s loc_65E4
to limit the maximum number of pixels the camera can scroll back by in a single frame.
Vertical scroll delay
In stock Sonic 1, the camera scrolls vertically as soon as Sonic looks up or ducks. This can be a bit annoying for the Spin Dash, however, so let's implement the delay present in Sonic 2 by using $FFFFC903 as our delay counter. Firstly, to fix a bit of incorrect code, go to loc2_1AD78 and change
loc2_1AD78:
addq.l #4,sp ; increase stack ptr
cmpi.w #$60,($FFFFEED8).w ; $EED8 only ever seems
beq.s loc2_1AD8C ; to be used in Spin Dash
bcc.s loc2_1AD88
addq.w #4,($FFFFEED8).w
loc2_1AD88:
subq.w #2,($FFFFEED8).w
loc2_1AD8C:
bsr.w Sonic_LevelBound
bsr.w Sonic_AnglePos
move.w #$60,($FFFFF73E).w ; reset looking up/down
rts
to
loc2_1AD78:
addq.l #4,sp ; increase stack ptr
cmpi.w #$60,($FFFFF73E).w
beq.s loc2_1AD8C
bcc.s loc2_1AD88
addq.w #4,($FFFFF73E).w
loc2_1AD88:
subq.w #2,($FFFFF73E).w
loc2_1AD8C:
bsr.w Sonic_LevelBound
bsr.w Sonic_AnglePos
;move.w #$60,($FFFFF73E).w ; reset looking up/down
rts
Then go to Sonic_LookUp and change
Sonic_LookUp:
btst #0,($FFFFF602).w ; is up being pressed?
beq.s Sonic_Duck ; if not, branch
move.b #7,$1C(a0) ; use "looking up" animation
cmpi.w #$C8,($FFFFF73E).w
beq.s loc_12FC2
addq.w #2,($FFFFF73E).w
bra.s loc_12FC2
; ===========================================================================
Sonic_Duck:
btst #1,($FFFFF602).w ; is down being pressed?
beq.s Obj01_ResetScr ; if not, branch
move.b #8,$1C(a0) ; use "ducking" animation
cmpi.w #8,($FFFFF73E).w
beq.s loc_12FC2
subq.w #2,($FFFFF73E).w
bra.s loc_12FC2
; ===========================================================================
Obj01_ResetScr:
cmpi.w #$60,($FFFFF73E).w ; is screen in its default position?
beq.s loc_12FC2 ; if yes, branch
bcc.s loc_12FBE
addq.w #4,($FFFFF73E).w ; move screen back to default
loc_12FBE:
subq.w #2,($FFFFF73E).w ; move screen back to default
to
Sonic_LookUp:
btst #0,($FFFFF602).w ; is up being pressed?
beq.s Sonic_Duck ; if not, branch
move.b #7,$1C(a0) ; use "looking up" animation
addq.b #1,($FFFFC903).w
cmp.b #$78,($FFFFC903).w
bcs.s Obj01_ResetScr_Part2
move.b #$78,($FFFFC903).w
cmpi.w #$C8,($FFFFF73E).w
beq.s loc_12FC2
addq.w #2,($FFFFF73E).w
bra.s loc_12FC2
; ===========================================================================
Sonic_Duck:
btst #1,($FFFFF602).w ; is down being pressed?
beq.s Obj01_ResetScr ; if not, branch
move.b #8,$1C(a0) ; use "ducking" animation
addq.b #1,($FFFFC903).w
cmpi.b #$78,($FFFFC903).w
bcs.s Obj01_ResetScr_Part2
move.b #$78,($FFFFC903).w
cmpi.w #8,($FFFFF73E).w
beq.s loc_12FC2
subq.w #2,($FFFFF73E).w
bra.s loc_12FC2
; ===========================================================================
Obj01_ResetScr:
move.b #0,($FFFFC903).w
Obj01_ResetScr_Part2:
cmpi.w #$60,($FFFFF73E).w ; is screen in its default position?
beq.s loc_12FC2 ; if yes, branch
bcc.s loc_12FBE
addq.w #4,($FFFFF73E).w ; move screen back to default
loc_12FBE:
subq.w #2,($FFFFF73E).w ; move screen back to default
Finally, to make the screen bias gradually reset during rolling, go to loc_131CC and change
loc_131CC:
move.b $26(a0),d0
jsr (CalcSine).l
to
loc_131CC:
cmp.w #$60,($FFFFF73E).w
beq.s @cont2
bcc.s @cont1
addq.w #4,($FFFFF73E).w
@cont1:
subq.w #2,($FFFFF73E).w
@cont2:
move.b $26(a0),d0
jsr (CalcSine).l
And that's it! Let me know if I made any mistakes, or if there's anything still not covered. Here's a ROM after all these steps have been done, and here's the source code (with Puto's SEGA sound fix implemented instead of Esrael's for ASM68k compatibility).
Final code
Here is the final Spin Dash code for references:
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 ; increment stack ptr
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?
move.b #2,($FFFFD1DC).w ; Set the Spin Dash dust animation to $2
loc2_1AC84:
jsr Sonic_LevelBound
jsr Sonic_AnglePos
locret2_1AC8C:
rts
; ---------------------------------------------------------------------------
loc2_1AC8E:
move.b #$1F,$1C(a0)
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 spdsh_norm(pc,d0.w),$14(a0) ; get normal speed
tst.b ($FFFFFE19).w ; is sonic super?
beq.s loc2_1ACD0 ; if no, branch
move.w spdsh_super(pc,d0.w),$14(a0) ; get super speed
loc2_1ACD0: ; TODO: figure this out
move.w $14(a0),d0 ; get inertia
subi.w #$800,d0 ; subtract $800
add.w d0,d0 ; double it
andi.w #$1F00,d0 ; mask it against $1F00
neg.w d0 ; negate it
addi.w #$2000,d0 ; add $2000
move.w d0,($FFFFC904).w ; move to $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!
move.b #8,($FFFFFF5B).w ; set afterimage counter to 8
bra.s loc2_1AD78
; ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ
spdsh_norm:
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
spdsh_super:
dc.w $B00 ; 0
dc.w $B80 ; 1
dc.w $C00 ; 2
dc.w $C80 ; 3
dc.w $D00 ; 4
dc.w $D80 ; 5
dc.w $E00 ; 6
dc.w $E80 ; 7
dc.w $F00 ; 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 ; increase stack ptr
cmpi.w #$60,($FFFFF73E).w
beq.s loc2_1AD8C
bcc.s loc2_1AD88
addq.w #4,($FFFFF73E).w
loc2_1AD88:
subq.w #2,($FFFFF73E).w
loc2_1AD8C:
jsr Sonic_LevelBound
jsr Sonic_AnglePos
;move.w #$60,($FFFFF73E).w ; reset looking up/down
rts
; End of function Sonic_SpinDash
there is one final part added by Mercury that addresses some final camera bugs.
|Add Spin Dash to Sonic 1/Part 3]]