Actions

SCHG How-to

Fix Ring Timers

From Sonic Retro

Revision as of 09:10, 30 September 2012 by KingofHarts (talk | contribs) (Added a RHS fix, with a small addendum by myself.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Original Guide by redhotsonic

Hello, guys! I discovered a bug with the Scattered Rings object, and yet again, it exists in all 3 classic games: Sonic 1, Sonic 2 and Sonic 3 and Knuckles! So, I made a fix.

The Bug

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. Let's fix this!

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.