Lost Ring Bugfixes - RetroKoH/S1Fixed GitHub Wiki

(Original guides by redhotsonic)
Sources and Commits noted in each section

Lost Rings are an interesting mechanic. Get hit, and you lose all of your rings on hand, with a chance to recollect up to 32 of them. Be that as it may, there are many bugs and issues involving the lost ring object. This guide is going to attempt to fix a few of the bugs connected to this object and its mechanic.

Part 1: Fix Accidental Deletion of Scattered Rings

Source: SCHG Page
Original Commit: 28cd1e2

In the lost rings code, there is a check to see if rings have reached the bottom of the level and if so, to delete themselves. It has this check because when rings reach to the bottom of the level, they delete themselves, so as not to spawn at the top of the level. This is a good thing, as we don't want rings looping, right? But there is an issue with levels that are y-wrapped enabled. In stock Sonic 1, there are two such levels: Labyrinth Zone Act 3, and Scrap Brain Zone Act 2, the former being more prevalent. If you edit these levels or add y-wrapping elsewhere, this bug becomes more prevalent, so let's fix it. Everything we need to do is going to be in _incObj/25 & 37 Rings.asm, so let's go there and find the label .chkdel. It should look something like this:

	tst.b	(v_ani3_time).w
	beq.s	RLoss_Delete
	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
	bra.w	DisplaySprite

Right after the second line: beq.s RLoss_Delete, we're going to make an addition:

	tst.b	(v_ani3_time).w
	beq.s	RLoss_Delete
+	cmpi.w	#$FF00,(v_limittop2).w	; is vertical wrapping enabled?
+	beq.w	DisplaySprite		; if so, branch
	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
	bra.w	DisplaySprite

Basically, whenever rings fall to the bottom of the level, they delete themselves to prevent looping back to the top of the screen (like Sonic does on y-wrapped levels). But with levels with y-wrap enabled, the rings would still delete themselves when they reached those same co-ordinates. In levels such as Labyrinth Act 3, those rings would reach the co-ordinates and delete themselves, which we don't want, as there is no bottom to the level and you want your rings back! So, all we've done here is made it so that if y-wrap is enabled, we do not delete the rings if they've reached the bottom boundary coordinate. The rings will still delete themselves after a certain amount of time though, you do not need to worry about them being around forever if y-wrap is enabled.

NOTE: There is still an error with rings deleting themselves if they go over the top boundary (specifically, if that boundary is 0000, which it usually is) in non-wrapping levels. I am investigating a fix for this.

Part 2: Fix Ring Timers

Source: SCHG Page
Original Commit: aa0974a

There is another interesting quirk with lost rings. Imagine you are playing, and you accidentally land on spikes. Oops! You lose all of your rings, and they fly everywhere. There is a timer in RAM that is set to $FF, and all of the scattered rings read from this one timer as it counts down each frame. When the timer finally reaches 0, all of the scattered rings delete themselves. This seems fair enough, but imagine you collect some rings, but get hurt again immediately afterward. Some more scattered rings are created. The thing is, these scattered rings set the RAM timer to $FF again, so they can count down, and those rings you lost earlier??? If they haven't been deleted yet, their timer is also reset to $FF, because all lost rings read off of the same one timer!

But wait, it gets better! If you have badniks, or other objects, explode with a ring coming out instead of an animal in your hack, like I do, this will also interrupt with the RAM timer. If you have S3K Shields backported and you lose the lightning shield mid-game, rings that were following you take off, and THEY also disrupt that RAM timer. That's obviously not meant to happen. What we want is for the rings to have individual timers, so that when we lose more rings, the old scattered rings will still have their own timers and delete when they are supposed to. To do this, we are going back into the same file we were in earlier: _incObj/25 & 37 Rings.asm, and we are going to the .makerings label. If we look just past where (a1)'s OSTs are set up (the last one being obActWid), we'll see the first line we need to remove:

.makerings:
; ...
; ...
	move.b	#$47,obColType(a1)
	move.b	#8,obActWid(a1)
-	move.b	#-1,(v_ani3_time).w
	tst.w	d4
	bmi.s	.loc_9D62
	move.w	d4,d0

This is where the RAM timer is set. Setting the RAM here is a complete waste of time because it's going to be set once for every scattered ring that's being spawned. 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 timer, instead of following a 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 and take a look at the first two lines:

	tst.b	(v_ani3_time).w
	beq.s	RLoss_Delete

Since we are no longer using the global timer, replace it with this:

	subq.b	#1,obDelayAni(a0)	; Subtract 1
	beq.s	RLoss_Delete		; If 0, delete

Now, the new individual timers will be subtracted every frame. Once they reach 0, their respective ring will delete itself. This timer cannot be interrupted when you lose more rings, so they will delete themselves when they're supposed to!