Actions

SCHG How-to

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!

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


SCHG How-To Guide: Sonic the Hedgehog (16-bit)
Fixing Bugs
Fix Demo Playback | Fix a Race Condition with Pattern Load Cues | Fix the SEGA Sound | Display the Press Start Button Text | Fix the Level Select Menu | Fix the Hidden Points Bug | Fix Accidental Deletion of Scattered Rings | Fix Ring Timers | Fix the Walk-Jump Bug | Correct Drowning Bugs | Fix the Death Boundary Bug | Fix the Camera Follow Bug | Fix Song Restoration Bugs | Fix the HUD Blinking | Fix the Level Select Graphics Bug | Fix a remember sprite related bug
Changing Design Choices
Change Spike Behavior | Collide with Water After Being Hurt | Fix Special Stage Jumping Physics | Improve the Fade In\Fade Out Progression Routines | Fix Scattered Rings' Underwater Physics | Remove the Speed Cap | Port the REV01 Background Effects | Port Sonic 2's Level Art Loader | Retain Rings Between Acts | Add Sonic 2 (Simon Wai Prototype) Level Select | Improve ObjectMove Subroutines | Port Sonic 2 Level Select
Adding Features
Add Spin Dash ( Part 1 / Part 2 / Part 3 / Part 4 ) | Add Eggman Monitor | Add Super Sonic | Add the Air Roll
Sound Features
Expand the Sound Index | Play Different Songs Per Act | Port Sonic 2 Final Sound Driver | Port Sonic 3's Sound Driver | Port Flamewing's Sonic 3 & Knuckles Sound Driver | Change The SEGA Sound
Extending the Game
Load Chunks From ROM | Add Extra Characters | Make an Alternative Title Screen | Use Dynamic Tilesets | Make GHZ Load Alternate Art | Make Ending Load Alternate Art | Add a New Zone | Set Up the Goggle Monitor | Add New Moves | Add a Dynamic Collision System | Dynamic Special Stage Walls System | Extend Sprite Mappings and Art Limit | Enigma Credits | Use Dynamic Palettes
Miscellaneous
Convert the Hivebrain 2005 Disassembly to ASM68K
Split Disassembly Guides
Set Up a Split Disassembly | Basic Level Editing | Basic Art Editing | Basic ASM Editing (Spin Dash)
SCHG How-To Guide: Sonic the Hedgehog 2 (16-bit)
Fixing Bugs
Fix Demo Playback | Fix a Race Condition with Pattern Load Cues | Fix Super Sonic Bugs | Use Correct Height When Roll Jumping | Fix Jump Height Bug When Exiting Water | Fix Screen Boundary Spin Dash Bug | Correct Drowning Bugs | Fix Camera Y Position for Tails | Fix Tails Subanimation Error | Fix Tails' Respawn Speeds | Fix Accidental Deletion of Scattered Rings | Fix Ring Timers | Fix Rexon Crash | Fix Monitor Collision Bug | Fix EHZ Deformation Bug | Correct CPZ Boss Attack Behavior | Fix Bug in ARZ Boss Arrow's Platform Behavior | Fix ARZ Boss Walking on Air Glitch | Fix ARZ Boss Sprite Behavior | Fix Multiple CNZ Boss Bugs | Fix HTZ Background Scrolling Mountains | Fix OOZ Launcher Speed Up Glitch | Fix DEZ Giant Mech Collision Glitch | Fix Boss Deconstruction Behavior | Fix Speed Bugs | Fix 14 Continues Cheat | Fix Debug Mode Crash | Fix 99+ Lives | Fix Sonic 2's Sega Screen
Design Choices
Remove the Air Speed Cap | Disable Floor Collision While Dying | Modify Super Sonic Transformation Methods & Behavior | Enable/Disable Tails in Certain Levels | Collide with Water After Being Hurt | Retain Rings When Returning at a Star Post | Improve the Fade In\Fade Out Progression Routines | Fix Scattered Rings' Underwater Physics | Insert LZ Water Ripple Effect | Restore Lost CPZ Boss Feature | Prevent SCZ Tornado Spin Dash Death | Improve ObjectMove Subroutines | Port S3K Rings Manager | Port S3K Object Manager | Port S3K Priority Manager | Edit Level Order with ASM‎ | Alter Ring Requirements in Special Stages | Make Special Stage Characters Use Normal DPLCs | Speed Up Ring Loss Process | Change spike behaviour in Sonic 2
Adding Features
Create Insta-kill and High Jump Monitors | Create Clone and Special Stage Monitors | Port Knuckles
Sound Features
Expand Music Index to Start at $00 | Port Sonic 1 Sound Driver | Port Sonic 2 Clone Driver | Port Sonic 3 Sound Driver | Port Flamewing's Sonic 3 & Knuckles Sound Driver | Expand the Music Index to Start at $00 (Sonic 2 Clone Driver Version) | Play Different Songs Per Act
Extending the Game
Extend the Level Index Past $10 | Extend the Level Select | Extend Water Tables | Add Extra Characters | Free Up 2 Universal SSTs

|Fix Ring Timers]]