Fix Ring Timers
From Sonic Retro
Revision as of 17:57, 25 November 2012 by TheRetroGuy (talk | contribs)
(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 - SVN Disassembly
First, open your "25 & 37 Rings.asm" file and go to "@makerings:" label. Find this line: <asm>
move.b #-1,(v_ani3_time).w
</asm>
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: <asm>
sfx sfx_RingLoss ; play ring loss sound
</asm>
Just before it, add this: <asm>
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)
</asm>
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? <asm>
tst.b (v_ani3_time).w beq.s RLoss_Delete
</asm>
You may delete it! No longer needed, as we're not using this as the main timer anymore. Instead, replace it with this: <asm>
subq.b #1,obDelayAni(a0) ; Subtract 1 beq.w DeleteObject ; If 0, delete
</asm>
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: <asm>
move.b #-1,(Ring_spill_anim_counter).w
</asm>
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: <asm>
move.w #$C6,d0 jsr (PlaySoundStereo).l
</asm>
Just before it, add this: <asm>
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)
</asm>
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? <asm>
tst.b (Ring_spill_anim_counter).w beq.s BranchTo5_DeleteObject
</asm>
You may delete it! No longer needed, as we're not using this as the main timer anymore. Instead, replace it with this: <asm>
subq.b #1,objoff_1F(a0) ; Subtract 1 beq.w DeleteObject ; If 0, delete
</asm>
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: <asm>
move.b #-1,(Ring_spill_anim_counter).w
</asm>
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: <asm>
move.w #SndID_RingSpill,d0 jsr (PlaySoundStereo).l
</asm>
Just before it, add this: </asm>
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)
</asm>
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? <asm>
tst.b (Ring_spill_anim_counter).w beq.s Obj37_Delete
</asm>
You may delete it! No longer needed, as we're not using this as the main timer anymore. Instead, replace it with this: <asm>
subq.b #1,objoff_1F(a0) ; Subtract 1 beq.w DeleteObject ; If 0, delete
</asm>
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: <asm>
move.b #-1,(Ring_spill_anim_counter).w
</asm>
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: <asm>
move.w #$FFB9,d0 jsr (Play_Sound_2).l
</asm>
Just before it, add this: <asm>
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)
</asm>
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? <asm>
tst.b (Ring_spill_anim_counter).w beq.s loc_1A7E4
</asm>
You may delete it! No longer needed, as we're not using this as the main timer anymore. Instead, replace it with this: <asm>
subq.b #1,height_pixels(a0) ; Subtract 1 beq.w Delete_Current_Sprite ; If 0, delete
</asm>
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: <asm>
move.b #-1,(Ring_spill_anim_counter).w
</asm>
and just above or below this line, add this line: <asm>
move.b #-1,height_pixels(a0) ; Move #$FF to new timer
</asm>
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: <asm>
move.b #-1,(Ring_spill_anim_counter).w
</asm>
and just above or below this line, add this line: <asm>
move.b #-1,height_pixels(a0) ; Move #$FF to new timer
</asm>
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.
EXTRA NOTE
(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: <asm> @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
</asm>
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.
(Addition by TheRetroGuy)
In addition to KingofHarts' note, to do this in Sonic 2 (XeonWhirl) just replace "loc_121B8:" with this:
<asm> 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 </asm>