Fix Ring Timers
From Sonic Retro
(Original guide by redhotsonic)
When you get hurt, you lose rings (duh). All the rings have a timer of $FF which is set to a RAM address. All the scattered rings read from this RAM address. Each frame, that timer counts down by #1. When the timer finally reaches 0, the scattered rings delete themselves. This seems fair enough. Now, imagine you collect some rings, but get hurt again. Some more scattered rings are created. The thing is, these scattered rings set the RAM timer to $FF again, so they can count down. Fine, but them rings you loss earlier, if they haven't been deleted yet, their timer is also set to $FF.
So, for example; you lose rings and the timer sets to $FF. Some frames later, that timer is on $10, so those rings are going to be deleted any moment now, but you get hurt again and lose more rings. Those scattered rings that were about to be deleted, are now going to stay a lot longer.
But wait, it gets better! If you have badniks explode with a ring coming out instead of an animal in your hack, like I do, this will also interrupt with the RAM timer.
That's not meant to happen. What we want is for the rings that are about to be deleted to keep their original timer! So when we lose more rings, the old scattered rings will still have their own timer. I'll note now, that this bug exists in all 3 Mega Drive games, so let's go ahead and fix them all!
Contents
Sonic 1 fix - Hivebrain Disassembly
(Addition by Luigi Hero)
First, open your "Sonic1.asm" file and go to "Obj37_MakeRings:" label. Find this line:
move.b #-1,($FFFFFEC6).w
This is where the RAM timer is set. Although, setting the RAM here is a waste of time. Depending on how many rings are being scattered, this is how many times it's being written. Example, if you lose 10 rings, it writes #-1 to this RAM 10 times. Why? It only needs to be written once. So delete this line.
Next, go to the "Obj37_ResetCounter:" label. Find these line:
move.w #$C6,d0
jsr (PlaySound_Special).l ; play ring loss sound
Just before it, add this:
moveq #-1,d0 ; Move #-1 to d0
move.b d0,$1F(a0) ; Move d0 to new timer
move.b d0,($FFFFFEC6).w ; Move d0 to old timer (for animated purposes)
This is where we're setting our timer. "$1F(a0)" is our brand new timer. "$1F" is a local variable in the object's OST that exists only within the object, instead of a global variable like $FFFFFEC6. This means each scattered ring will now have its own personal timer, instead of following 1 singular global timer. We must still set the old timer though, for animation purposes. Without setting the old timer, the rings won't spin. Anyway, setting the timer here, it will only be written once; saving a bit of time when creating the rings.
So, our new timer has been set to $FF. We still need to make it count down. So, find the "Obj37_ChkDel:" label. See this?
tst.b ($FFFFFEC6).w
beq.w Obj37_Delete
You may delete it! No longer needed, as we're not using this as the main timer anymore. Instead, replace it with this:
subq.b #1,$1F(a0) ; Subtract 1
beq.w DeleteObject ; If 0, delete
Now, the new personal timer will be subtracted every frame. Once it reaches 0, it will delete itself. This timer cannot be interrupted when you lose more rings, so these will delete themselves when they're supposed to!
Sonic 1 fix - SVN Disassembly
First, open your "25 & 37 Rings.asm" file and go to "@makerings:" label. Find this line:
move.b #-1,(v_ani3_time).w
This is where the RAM timer is set. Although, setting the RAM here is a waste of time. Depending on how many rings are being scattered, this is how many times it's being written. Example, if you lose 10 rings, it writes #-1 to this RAM 10 times. Why? It only needs to be written once. So delete this line.
Next, go to the "@resetcounter:" label. Find this line:
sfx sfx_RingLoss ; play ring loss sound
Just before it, add this:
moveq #-1,d0 ; Move #-1 to d0
move.b d0,obDelayAni(a0) ; Move d0 to new timer
move.b d0,(v_ani3_time).w ; Move d0 to old timer (for animated purposes)
This is where we're setting our timer. "obDelayAni(a0)" is our brand new timer. "obDelayAni" is a local variable in the object's OST that exists only within the object, instead of a global variable like v_ani3_time. This means each scattered ring will now have its own personal timer, instead of following 1 singular global timer. We must still set the old timer though, for animation purposes. Without setting the old timer, the rings won't spin. Anyway, setting the timer here, it will only be written once; saving a bit of time when creating the rings.
So, our new timer has been set to $FF. We still need to make it count down. So, find the "@chkdel:" label. See this?
tst.b (v_ani3_time).w
beq.s RLoss_Delete
You may delete it! No longer needed, as we're not using this as the main timer anymore. Instead, replace it with this:
subq.b #1,obDelayAni(a0) ; Subtract 1
beq.w DeleteObject ; If 0, delete
Now, the new personal timer will be subtracted every frame. Once it reaches 0, it will delete itself. This timer cannot be interrupted when you lose more rings, so these will delete themselves when they're supposed to!
Sonic 2 fix - XenoWhirl's Disassembly
First, open your ASM file and go to "loc_120BA:" label. Find this line:
move.b #-1,(Ring_spill_anim_counter).w
This is where the RAM timer is set. Although, setting the RAM here is a waste of time. Depending on how many rings are being scattered, this is how many times it's being written. Example, if you lose 10 rings, it writes #-1 to this RAM 10 times. Why? It only needs to be written once. So delete this line.
Next, go to the "loc_12142:" label. Find this line:
move.w #$C6,d0
jsr (PlaySoundStereo).l
Just before it, add this:
moveq #-1,d0 ; Move #-1 to d0
move.b d0,objoff_1F(a0) ; Move d0 to new timer
move.b d0,(Ring_spill_anim_counter).w ; Move d0 to old timer (for animated purposes)
This is where we're setting our timer. "objoff_1F(a0)" is our brand new timer. "objoff_1F" is a local variable in the object's OST that exists only within the object, instead of a global variable like Ring_spill_anim_counter. This means each scattered ring will now have its own personal timer, instead of following 1 singular global timer. We must still set the old timer though, for animation purposes. Without setting the old timer, the rings won't spin. Anyway, setting the timer here, it will only be written once; saving a bit of time when creating the rings.
So, our new timer has been set to $FF. We still need to make it count down. So, find the "loc_121B8:" label. See this?
tst.b (Ring_spill_anim_counter).w
beq.s BranchTo5_DeleteObject
You may delete it! No longer needed, as we're not using this as the main timer anymore. Instead, replace it with this:
subq.b #1,objoff_1F(a0) ; Subtract 1
beq.w DeleteObject ; If 0, delete
Now, the new personal timer will be subtracted every frame. Once it reaches 0, it will delete itself. This timer cannot be interrupted when you lose more rings, so these will delete themselves when they're supposed to!
Sonic 2 fix - SVN Disassembly
First, open your ASM file and go to "Obj37_Init:" label. On the 3rd + label, find this line:
move.b #-1,(Ring_spill_anim_counter).w
This is where the RAM timer is set. Although, setting the RAM here is a waste of time. Depending on how many rings are being scattered, this is how many times it's being written. Example, if you lose 10 rings, it writes #-1 to this RAM 10 times. Why? It only needs to be written once. So delete this line.
From the "Obj37_Init:", find the 5th + label. Find this line:
move.w #SndID_RingSpill,d0
jsr (PlaySoundStereo).l
Just before it, add this:
moveq #-1,d0 ; Move #-1 to d0
move.b d0,objoff_1F(a0) ; Move d0 to new timer
move.b d0,(Ring_spill_anim_counter).w ; Move d0 to old timer (for animated purposes)
This is where we're setting our timer. "objoff_1F(a0)" is our brand new timer. "objoff_1F" is a local variable in the object's OST that exists only within the object, instead of a global variable like Ring_spill_anim_counter. This means each scattered ring will now have its own personal timer, instead of following 1 singular global timer. We must still set the old timer though, for animation purposes. Without setting the old timer, the rings won't spin. Anyway, setting the timer here, it will only be written once; saving a bit of time when creating the rings.
So, our new timer has been set to $FF. We still need to make it count down. So, find the "loc_121B8:" label. See this?
tst.b (Ring_spill_anim_counter).w
beq.s Obj37_Delete
You may delete it! No longer needed, as we're not using this as the main timer anymore. Instead, replace it with this:
subq.b #1,objoff_1F(a0) ; Subtract 1
beq.w DeleteObject ; If 0, delete
Now, the new personal timer will be subtracted every frame. Once it reaches 0, it will delete itself. This timer cannot be interrupted when you lose more rings, so these will delete themselves when they're supposed to!
Sonic 3 and Knuckles fix - SVN Disassembly
First, open your ASM file and go to "loc_1A6B6:" label. Find this line:
move.b #-1,(Ring_spill_anim_counter).w
This is where the RAM timer is set. Although, setting the RAM here is a waste of time. Depending on how many rings are being scattered, this is how many times it's being written. Example, if you lose 10 rings, it writes #-1 to this RAM 10 times. Why? It only needs to be written once. So delete this line.
Next, go to the "loc_1A738:" label. Find this line:
move.w #$FFB9,d0
jsr (Play_Sound_2).l
Just before it, add this:
moveq #-1,d0 ; Move #-1 to d0
move.b d0,height_pixels(a0) ; Move d0 to new timer
move.b d0,(Ring_spill_anim_counter).w ; Move d0 to old timer (for animated purposes)
This is where we're setting our timer. "anim_frame_timer(a0)" is our brand new timer. "anim_frame_timer(a0)" is a local variable in the object's OST that exists only within the object, instead of a global variable like Ring_spill_anim_counter. This means each scattered ring will now have its own personal timer, instead of following 1 singular global timer. We must still set the old timer though, for animation purposes. Without setting the old timer, the rings won't spin. Anyway, setting the timer here, it will only be written once; saving a bit of time when creating the rings.
So, our new timer has been set to $FF. We still need to make it count down. So, find the "loc_1A79C:" label. See this?
tst.b (Ring_spill_anim_counter).w
beq.s loc_1A7E4
You may delete it! No longer needed, as we're not using this as the main timer anymore. Now, find the "loc_1A7B0:" label. Just below it, add:
subq.b #1,height_pixels(a0) ; Subtract 1
beq.w Delete_Current_Sprite ; If 0, delete
Now, the new personal timer will be subtracted every frame. Once it reaches 0, it will delete itself. This timer cannot be interrupted when you lose more rings, so these will delete themselves when they're supposed to!
BUT WAIT!
Unlike the previous 2 games, you have to do something else to Sonic 3 and Knuckles. You know the lightning shield? When you have rings attracted to you, and you get hurt and lose the shield, the rings that were being attracted are turned into scattered rings. We need to set the new timer for this.
Go to "loc_1A88C:" and find:
move.b #-1,(Ring_spill_anim_counter).w
and just above or below this line, add this line:
move.b #-1,height_pixels(a0) ; Move #$FF to new timer
BUT WAIT!
There's one more thing. Again, this is S3K only. You know the Egg Prisons you can jump on in Flying Battery Zone? Some of them release rings, which are scattered rings again. They need their own timer. So, go to "loc_89D44:" and find the line:
move.b #-1,(Ring_spill_anim_counter).w
and just above or below this line, add this line:
move.b #-1,height_pixels(a0) ; Move #$FF to new timer
NOTE: I do not hack S3K, but I am sure that the height_pixels SST is free for scattered rings. After trying this myself, I see no problems. If there are errors, let me know.
Lost Rings Flash In Sonic 1 SVN
(Addition by KingofHarts)
That should do it. This is especially useful for anyone who uses ReadySonic's ring loss flash effect, which makes lost rings flash during the final frames of their existence.
To add this little feature (For Sonic 1 SVN users, though hackers of other games/disassemblies can easily follow suit) go to "@chkdel:" and change it to this:
@chkdel:
subq.b #1,obDelayAni(a0) ; Subtract 1 ; RHS Ring Timer fix
beq.w DeleteObject ; If 0, delete ; RHS Ring Timer fix
move.w (v_limitbtm2).w,d0
addi.w #$E0,d0
cmp.w obY(a0),d0 ; has object moved below level boundary?
bcs.s RLoss_Delete ; if yes, branch
;Mercury Lost Rings Flash
btst #0, obDelayAni(a0) ; Test the first bit of the timer, so rings flash every other frame.
beq.w DisplaySprite ; If the bit is 0, the ring will appear.
cmpi.b #80,obDelayAni(a0) ; Rings will flash during last 80 steps of their life.
bhi.w DisplaySprite ; If the timer is higher than 80, obviously the rings will STAY visible.
rts
;end Lost Rings Flash
As noted, the ring loss flash is from Mercury's ReadySonic, which I have implemented into Sonic 1 REV C. I have amended this to work with RHS' new fix. That's all there is to it.
Lost Rings Flash In Sonic 1 Hivebrain
(Addition by Luigi Hero)
That should do it. This is especially useful for anyone who uses ReadySonic's ring loss flash effect, which makes lost rings flash during the final frames of their existence.
To add this little feature (For Hivebrain) go to "Obj37_ChkDel:" and change it to this:
Obj37_ChkDel:
subq.b #1,$1F(a0) ; Subtract 1 ; RHS Ring Timer fix
beq.w DeleteObject ; If 0, delete ; RHS Ring Timer fix
move.w ($FFFFF72E).w,d0
addi.w #$E0,d0
cmp.w $C(a0),d0 ; has object moved below level boundary?
bcs.s Obj37_Delete ; if yes, branch
;Mercury Lost Rings Flash
btst #0, $1F(a0) ; Test the first bit of the timer, so rings flash every other frame.
beq.w DisplaySprite ; If the bit is 0, the ring will appear.
cmpi.b #80,$1F(a0) ; Rings will flash during last 80 steps of their life.
bhi.w DisplaySprite ; If the timer is higher than 80, obviously the rings will STAY visible.
rts
;end Lost Rings Flash
As noted, the ring loss flash is from Mercury's ReadySonic, which I have implemented into Sonic 1 REV C. I have amended this to work with RHS' new fix. That's all there is to it.
Lost Rings Flash In Sonic 2
(Addition by TheRetroGuy)
In addition to KingofHarts' note, to do this in Sonic 2 (Xenowhirl) just replace "loc_121B8:" with this:
loc_121B8:
subq.b #1,objoff_1F(a0) ; Subtract 1 ; RHS Ring Timer fix
beq.w DeleteObject ; If 0, delete ; RHS Ring Timer fix
move.w (Camera_Max_Y_pos_now).w,d0
addi.w #$E0,d0
cmp.w y_pos(a0),d0 ; has object moved below level boundary?
bcs.s BranchTo5_DeleteObject
;Mercury Lost Rings Flash
btst #0, objoff_1F(a0) ; Test the first bit of the timer, so rings flash every other frame.
beq.w DisplaySprite ; If the bit is 0, the ring will appear.
cmpi.b #80,objoff_1F(a0) ; Rings will flash during last 80 steps of their life.
bhi.w DisplaySprite ; If the timer is higher than 80, obviously the rings will STAY visible.
rts
|Fix Ring Timers]]