Actions

SCHG How-to

Free up 2 universal SSTs

From Sonic Retro

(Guide written by redhotsonic)

WARNING: Please back-up your whole disassembly before following this guide. I will NOT be held responsible if anything goes wrong when following this guide.

PLEASE NOTE: This guide assumes that all your SST's are equated. If you have made your own objects or ported objects and still have stuff like "move.w #$488,$1E(a0)" instead of "move.w #$488,anim_frame_duration(a0)", you must rename all the SST's so that they use your equates, so they can easily be moved. Otherwise, things will start to go wrong. This is your only warning about this.

There are a few guides out there to free up SST's, but these only seem to be for Sonic and Tails only. This is great if you want to use a SST for a double-jump flag, or a homing-attack flag, whatever. Then you think that you want to do something bold. Like, you want to follow
Sonic Retro
Module's guide on how to port S3K's object manager to S2
or you may want to follow
Sonic Retro
my guide on how to port S3K's priority manager into S2
. I've now got the object manager ported into my hack, but the biggest trouble with the guide, is that you need to free up a universal SST. Module tries to explain that you can expand the SST table, but this is very complicated and annoying, and you have to edit all the other objects, which is tiring. To make his guide a lot easier to follow, freeing a universal SST would be a lot easier and a big time-saver. But how?

Freeing up one of Sonic and/or Tails SST is no good. Because as explained, only Sonic and Tails uses this. All other objects do not. For example, if you had a free SST in Sonic and Tails, say, $34, then move the Priority from $18 to $34, your hack will freeze pretty much immediately when you next start it. That's because the SEGA logo cannot read from Priority anymore, neither can the title screen, neither can any badniks, blah blah blah.

Well, I'm not going to show you how to free up 1, but how to free up 2 of them! Part 1 is not too complicated, Part 2 is very very easy. BUT, if you plan to port the S3K object manager using Module's guide, I highly advice you to do Part 1 first!

If you're only interested in my "How to port S3K Priority Manager into S2" guide, then you can skip step 1 if you want to.


This guide may look really big and you're probably thinking "Oh gawd, this looks tough", but it's actually easy if you read the guide carefully. And I've tried to make it as easy as I can for you and explained as much as I can too!

PLEASE NOTE: This guide is designed for Xenowhirl's 2007 disassembly. You can use this guide if you have the Git disassembly, but you'll have to work around the differences. You definitely cannot do it if you are using Hivebrain's disassembly. This is also for Sonic 2 only!

Part 1: Free up one "universal" SST

Okay, first of all, I must admit, this technically is not universal, but it is "conventions followed by most objects". This is almost as good as universal, and is actually perfect for Module's guide on porting the S3K object manager in. Do not worry, I promise part 2 will free up a universal SST. So, let's get started.

Step 1: Changing the hardest parts of "anim_frame_duration"

anim_frame_duration =	$1E

anim_frame_duration is a word, so we need to convert this to a byte. Doing so is quite easy. If you search through your ASM file (s2.asm) for "anim_frame_duration", you see half of them is set to word, and the others are a byte. So, the ones set to a byte do not need editing. The ones set to a word, obviously do. So, to start, go to "loc_A542:" and you'll see:

loc_A542:
	move.w	d0,y_pos(a1)
	move.w	x_pos(a0),x_pos(a1)
	move.l	#$1000505,mapping_frame(a1)
	move.w	#$100,anim_frame_duration(a1)
	rts

This is part of the object code to trigger the rescue plane and birds from the ending sequence. As you can see, it moves a word ($100) to anim_frame_duration. This needs to be turned into a byte, so change it to this:

loc_A542:
	move.w	d0,y_pos(a1)
	move.w	x_pos(a0),x_pos(a1)
	move.l	#$1000505,mapping_frame(a1)
	move.b	#$FF,anim_frame_duration(a1)
	rts

As you can see, it's now "move.b" and not "move.w". Because of this, it cannot be $100 anymore and must be a byte-length. So, I put it to $FF. It's only $1 byte off, and I checked this in my game, and you won't spot any difference, so this will work fine.

Next, go to "loc_13FE2:" and you'll see this:

loc_13FE2:
	move.w	#$2D0,anim_frame_duration(a0)
	addq.b	#2,routine(a0)
	rts

This is part of the "Game Over" object. When you get a Game Over, and the text appears, $2D0 gets moved to anim_frame_duration. Then every frame, it goes down a byte, and when it finally reaches 0, the game restarts and goes to SEGA (or, it will go to SEGA screen when you press A, B or C). We can't have $2D0 because it's a word. So, change it to this:

loc_13FE2:
	move.b	#$C0,anim_frame_duration(a0)
	addq.b	#2,routine(a0)
	rts

Now, it is set to $C0 (notice again, it's been changed to "move.b"). Now you're thinking "it's going to count down too quick, surely?" Well, that's what we're about to fix next. Go to "loc_13FEE:" and you'll see:

	btst	#0,mapping_frame(a0)
	bne.w	BranchTo17_DisplaySprite
	move.b	(Ctrl_1_Press).w,d0
	or.b	(Ctrl_2_Press).w,d0
	andi.b	#$70,d0
	bne.s	loc_14014
	tst.w	anim_frame_duration(a0)
	beq.s	loc_14014
	subq.w	#1,anim_frame_duration(a0)
	bra.w	DisplaySprite

See that? It's testing the anim_frame_duration. If 0, branch, otherwise, it will subtract one from anim_frame_duration. It will keep doing this until it branches. These both need to changed to .b, but that's not enough. As said, it's counting down too quickly, so change it, to this:

loc_13FEE:
	btst	#0,mapping_frame(a0)
	bne.w	BranchTo17_DisplaySprite
	move.b	(Ctrl_1_Press).w,d0
	or.b	(Ctrl_2_Press).w,d0
	andi.b	#$70,d0
	bne.s	loc_14014
	tst.b	anim_frame_duration(a0)
	beq.s	loc_14014
	move.b	($FFFFFE05).w,d0	; Move Game Frame timer to d0
	andi.b	#3,d0			; andi d0 by 3
	bne.w	DisplaySprite		; if d0 does NOT equal 0, skip subtracting 1 byte from anim_frame_duration
	subq.b	#1,anim_frame_duration(a0)
	bra.w	DisplaySprite

Both anim_frame_duration have been changed from .w to .b again, but there's a bit of extra code here: Before subtracting 1 from anim_frame_duration, it will move the Game Frame Timer to d0 (Game Frame Timer goes up a byte every single frame). The next command will "and" d0 by 3 (so, every frame, d0 will go 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, etc). Then the next command, saying it will branch to DisplaySprite if d0 does NOT equal 0. If it DOES equal 0, it will then subtract 1 from anim_frame_duration and then DisplaySprite. Because of this, it will not subtract 1 from anim_frame_duration so quickly, and therefore, it will take a bit longer for it to reach 0. Instead of subtracting 1 from anim_frame_duration every frame, it subtracts 1 from anim_frame_duration every 4 frames. This works perfectly.

Now, next, is to do with the "End of level" results screen object. There's quite a lot to do with this object: Instead of doing a lot of coding here similar to "Game Over", I changed anim_frame_duration to objoff_34. objoff_34 is not used in the "End of level" results screen and therefore, is free to use for this object. Because objoff_34 will be set as a word, it will be using objoff_35 too, which again, is free for this object. The ending of each level still works perfectly for me after doing this. So, go to "loc_14118:" and you'll see:

loc_14118:

	move.b	d0,mapping_frame(a0)
	bsr.w	sub_13E1C
	move.w	x_pixel(a0),d0
	cmp.w	objoff_30(a0),d0
	bne.w	return_14138
	move.b	#$A,routine(a0)
	move.w	#$B4,anim_frame_duration(a0)

And change it to this:

loc_14118:

	move.b	d0,mapping_frame(a0)
	bsr.w	sub_13E1C
	move.w	x_pixel(a0),d0
	cmp.w	objoff_30(a0),d0
	bne.w	return_14138
	move.b	#$A,routine(a0)
	move.w	#$B4,objoff_34(a0)

Please note: that it's still "move.w" and this is because, further down, it will be doing "subq.w" an they cannot be changed to .b, as they do it for multiple places which uses words. That's another reason why I'm using objoff_34: It's just a lot easier and simpler than doing a lot of calculations, and that might slow down the results screen, which we do not want. For this object, it will always be .w and not .b, so just make sure of that. Anyway...

Next, go to:

loc_1419C:
	subq.w	#1,anim_frame_duration(a0)
	bne.s	BranchTo18_DisplaySprite
	addq.b	#2,routine(a0)


And change it to:

loc_1419C:
	subq.w	#1,objoff_34(a0)
	bne.s	BranchTo18_DisplaySprite
	addq.b	#2,routine(a0)

Next, go to:

loc_141E6:
	add.w	d0,($FFFFFF8E).w
	tst.w	d0
	bne.s	loc_14256
	move.w	#$C5,d0
	jsr	(PlaySound).l
	addq.b	#2,routine(a0)
	move.w	#$B4,anim_frame_duration(a0)
	cmpi.w	#1000,($FFFFFF8E).w
	bcs.s	return_14254
	move.w	#$12C,anim_frame_duration(a0)
	lea	next_object(a0),a1 ; a1=object

And change to:

loc_141E6:
	add.w	d0,($FFFFFF8E).w
	tst.w	d0
	bne.s	loc_14256
	move.w	#$C5,d0
	jsr	(PlaySound).l
	addq.b	#2,routine(a0)
	move.w	#$B4,objoff_34(a0)	; The time the game waits until the total score has totaled and to move on to next level
	cmpi.w	#1000,($FFFFFF8E).w
	bcs.s	return_14254
	move.w	#$12C,objoff_34(a0)	; The time the game waits until the total score has totaled and do the 'continue' jingle and to move on to next level
	lea	next_object(a0),a1 ; a1=object

Next, go to:

loc_14220:
	_move.b	#$3A,0(a1)	; load obj3A (uses screen-space)
	move.b	#$12,routine(a1)
	move.w	#$188,x_pixel(a1)
	move.w	#$118,y_pixel(a1)
	move.l	#Obj3A_MapUnc_14CBC,mappings(a1)
	bsr.w	Adjust2PArtPointer2
	move.b	#0,render_flags(a1)
	move.w	#$3C,anim_frame_duration(a1)
	addq.b	#1,(Continue_count).w

And change to:

loc_14220:
	_move.b	#$3A,0(a1)	; load obj3A (uses screen-space)
	move.b	#$12,routine(a1)
	move.w	#$188,x_pixel(a1)
	move.w	#$118,y_pixel(a1)
	move.l	#Obj3A_MapUnc_14CBC,mappings(a1)
	bsr.w	Adjust2PArtPointer2
	move.b	#0,render_flags(a1)
	move.w	#$3C,objoff_34(a1)
	addq.b	#1,(Continue_count).w

Next, go to:

loc_142B0:
	tst.w	anim_frame_duration(a0)
	beq.s	loc_142BC
	subq.w	#1,anim_frame_duration(a0)
	rts

And change to:

loc_142B0:
	tst.w	objoff_34(a0)
	beq.s	loc_142BC
	subq.w	#1,objoff_34(a0)
	rts

Next, go to:

loc_142CC:
	subq.w	#1,anim_frame_duration(a0)
	bpl.s	loc_142E2
	move.w	#$13,anim_frame_duration(a0)
	addq.b	#1,anim_frame(a0)
	andi.b	#1,anim_frame(a0)

And change to:

loc_142CC:
	subq.w	#1,objoff_34(a0)
	bpl.s	loc_142E2
	move.w	#$13,objoff_34(a0)
	addq.b	#1,anim_frame(a0)
	andi.b	#1,anim_frame(a0)

Done. All these changes are just for the "End of level" results screen. Using objoff_34 instead of anim_frame_duration is perfectly fine and causes no troubles whatsoever. Next, everything will be going back to .b and not .w soo...

Now, there's one more object to deal with: The Tornado (Tails' Plane). Go to "loc_3AB18:" and you'll see:

loc_3AB18:
	clr.w	(Ctrl_1_Logical).w
	lea	(MainCharacter).w,a1 ; a1=character
	move.w	x_pos(a0),x_pos(a1)
	clr.w	x_vel(a1)
	clr.w	y_vel(a1)
	clr.w	inertia(a1)
	bclr	#1,status(a1)
	bclr	#2,status(a1)
	move.l	#$1000505,mapping_frame(a1)
	move.w	#$100,anim_frame_duration(a1)
	move.b	#$13,y_radius(a1)
	cmpi.w	#2,(Player_mode).w
	bne.s	loc_3AB60
	move.b	#$F,y_radius(a1)

Basically, this has the same problem as the first object (triggering the rescue plane and birds from ending sequence): It's moving $100 to anim_frame_duration again, so change it to this:

loc_3AB18:
	clr.w	(Ctrl_1_Logical).w
	lea	(MainCharacter).w,a1 ; a1=character
	move.w	x_pos(a0),x_pos(a1)
	clr.w	x_vel(a1)
	clr.w	y_vel(a1)
	clr.w	inertia(a1)
	bclr	#1,status(a1)
	bclr	#2,status(a1)
	move.l	#$1000505,mapping_frame(a1)
	move.b	#$FF,anim_frame_duration(a1)
	move.b	#$13,y_radius(a1)
	cmpi.w	#2,(Player_mode).w
	bne.s	loc_3AB60
	move.b	#$F,y_radius(a1)

It's now "move.b" instead, and moving $FF instead of $100. $1 byte isn't going to make a difference (I couldn't see a difference).

Next, go to "loc_3AC56:" (still part of the Tornado object) and you'll see:

	lea	(MainCharacter).w,a1 ; a1=character
	move.l	#$1000505,mapping_frame(a1)
	move.w	#$100,anim_frame_duration(a1)
	rts

And change to:

loc_3AC56:
	lea	(MainCharacter).w,a1 ; a1=character
	move.l	#$1000505,mapping_frame(a1)
	move.b	#$FF,anim_frame_duration(a1)
	rts

Same problem as before really... and that's the hardest parts done, and be honest, that wasn't hard, was it? =P

Step 2: Changing .w to .b in all "anim_frame_duration"

This bit is easy, but time consuming. It shouldn't take you any longer than 20 minutes. So, go to the top of your ASM file, or go to "StartOfRom:"

On your notepad (or whatever you're using), use the search function (Edit > Search). In the search box, search for "anim_frame_duration" (without the "quotation marks" of course).

For every result you find, if it is has a .w to it, change it to .b. (Please note: If it is already .b then you can leave it alone and does not need changing)

Example, your first result you come to, should be (at "loc_42E8:):

	move.w	#$2D,anim_frame_duration(a1)

It's extremely simple, you change it to:

	move.b	#$2D,anim_frame_duration(a1)

Simple. Do this for all your search results.

One more incase you're not getting it. The next result (which is actually 2 lines down) is:

	move.w	#$2D,next_object+anim_frame_duration(a1)

and change to:

	move.b	#$2D,next_object+anim_frame_duration(a1)

How hard is that? Do this with all your results.

Step 3: Changing .b to .w back in the ARZ, CNZ and MCZ boss's "anim_frame_duration"

Users of the Git disassembly should not have to do this step. Because of step 2, you've probably change .w to .b's anim_frame_duration in ARZ, CNZ and MCZ's bosses. This is a big problem: The bosses HAVE to use these as a word, otherwise the bosses will not work correctly. You cannot change it to another SST, because these have to use universal SST's, as a word, and unfortunately, the bosses are already using all the universal SST's available. So, go to these locations:

  • loc_304D4:
  • loc_30824:
  • loc_30FB8:
  • loc_3130A:
  • loc_31904:
  • loc_31E76:

At all these locations, where you see a command for anim_frame_duration, change the .b back to .w

Example, at "loc_304D4:", change:

	move.b	#$488,anim_frame_duration(a0)

back to this:

	move.w	#$488,anim_frame_duration(a0)

Do this for all the ones in the list. You MUST change them back to .w at the locations in the above list ONLY.

Step 4: Change "anim_frame_duration" and "respawn_index" around. OPTIONAL, BUT RECOMMENDED

Okay, you've freed an SST that can be used by pretty much any object! Good work, you should be proud. $1E is still anim_frame_duration, but $1F is now free for you to use!

BUT, if you're planning to follow Module's guide on porting the S3K object manager to S2, follow this step. In his guide, he says you need to make the SST respawn_index into a word, instead of a byte. So, we can easily re-arrange this. Search for "anim_frame_duration = $1E" (without the "quotation marks") and change:

anim_frame_duration =	$1E

to this

anim_frame_duration =	$23

Then, look for respawn_index and change:

respawn_index =		$23

to this:

respawn_index =		$1E

Now, anim_frame_duration is using $23 (it won't use $24 because we changed it to a byte), and respawn_index is now $1E, and because we have made $1F free, respawn_index can be used as a word and can now easily use $1E and $1F! BUT, because of this, ARZ, CNZ and MCZ bosses have become affected again. This is because they're moving a word to anim_frame_duration, which is $23. Because it's moving a word, it overwrites $24, which is "routine", causing the game to freeze. Easily fixed. Here's how. You need to go to these labels in the list below and change change anim_frame_duration to respawn_index. Again, Git users should not have to do this.

  • loc_304D4:
  • loc_30824:
  • loc_30FB8:
  • loc_3130A:
  • loc_31904:
  • loc_31E76:

Example, at "loc_304D4:", change:

	move.w	#$488,anim_frame_duration(a0)

back to this:

	move.w	#$488,respawn_index(a0)

Do this for all the ones in the list above. The bosses can use respawn_index freely as it is universal and it is a word. Basically, it's doing the same as it used to do with anim_frame_duration (using $1E and $1F like it used to in the first place).

All done for Part 1. That was easy, hey? Now, for part 2. Now this one is universal, and it a hell of a lot quicker. I freed up this one for my priority manager, but you can use it for whatever you want.

Part 2: Free up the second universal SST

It's so easy. First, go to Sonic's SST's and you'll see

inertia =		$14 ; and $15 ; directionless representation of speed... not updated in the air

Change it to

inertia =		$20 ; and $21 ; directionless representation of speed... not updated in the air

For Sonic and Tails, $20 and $21 is free, so move it there. Why? Simple. $14 is a "convention followed by most objects". I think $15 is still only used by Sonic and Tails, but that doesn't matter. Now, instead of the original $1F, $20 and $21 being free for Sonic and Tails, it's now $15 and $1F that is free for them ($14 is NOT free for Sonic and Tails! About to explain why!)

Next, go to:

width_pixels =		$19

and change to

width_pixels =		$14

width_pixels can be moved to $14, which means $19 is now free, and it's universal! How about that? Told you it was piss easy! I am using $19 as part of priority (in S3K's priority manager, the priority is a word, so I use $18 and $19). And that's the reason why $14 isn't free for Sonic and Tails, because it's being used for width_pixel. Yes, you're going to lose a SST from them but it's better to gain a universal SST, right?

So now, you have $19 free universally. If you did NOT follow step 4 in part 1, then you have $1F free which can be used by almost every object. If you have followed step 4 in part 1, then $1F is still free until you port S3K's object manager in using Module's guide. Also, for Sonic and Tails, you have $15, $1F and $23 free.

WARNING: If you change the equates to use something else, it can cause problems (mainly ARZ, CNZ and MCZ bosses). For example, if you have these bosses set to respawn_index and then you swap respawn_index with routine, respawn_index will use $24 fine, but as the bosses are using it as a word, it will over-write $25, which is routine_secondary. Then, the game will freeze when you come to one of these bosses. Just a word of warning. This is not the case with the Git disassembly

Part 3: Fixing a bug

Applying this guide to a clean Sonic 2 disassembly, the ONLY bug I could find involved Casino Night Zone's pull spring object.

The width_pixels have gone wrong on this. The problem is that part of its height is involved and overwrites the new width_pixels SST. You cannot change the height's location of this object, otherwise the object will never work. But I realized in this object, that its priority is used for something different as well. Instead, its priority is moved to d0 then jumps to DisplaySprite3. So, I made width_pixels do the same thing, and now the object is fine. Anyway, the fix:

Go to "loc_2ABFA:" and you'll see this:

loc_2ABFA:
	bsr.w	JmpTo49_Adjust2PArtPointer
	move.b	#4,render_flags(a0)
	bset	#6,render_flags(a0)
	move.b	#1,objoff_B(a0)
	tst.b	subtype(a0)
	beq.s	loc_2AC54
	addq.b	#2,routine(a0)
	move.b	#$20,objoff_E(a0)
	move.b	#$18,width_pixels(a0)
	move.w	x_pos(a0),objoff_2E(a0)
	move.w	y_pos(a0),objoff_34(a0)
	move.w	x_pos(a0),d2
	move.w	y_pos(a0),d3
	addi.w	#0,d3
	move.b	#1,objoff_F(a0)
	lea	$10(a0),a2
	move.w	d2,(a2)+
	move.w	d3,(a2)+
	move.w	#2,(a2)+
	bra.w	loc_2AE56

Cut out the line "move.b #$18,width_pixels(a0)" so it's not there anymore. We're going to move it.

Next, go to "Obj85:" and you'll see this:

Obj85:
	moveq	#0,d0
	move.b	routine(a0),d0
	move.w	off_2ABCE(pc,d0.w),d1
	jsr	off_2ABCE(pc,d1.w)
	move.w	#$200,d0
	tst.w	(Two_player_mode).w
	beq.s	loc_2ABA0
	bra.w	JmpTo4_DisplaySprite3

Just before the "move.w #$200,d0", paste the width_pixel line there. So, you end up with this:

Obj85:
	moveq	#0,d0
	move.b	routine(a0),d0
	move.w	off_2ABCE(pc,d0.w),d1
	jsr	off_2ABCE(pc,d1.w)
	move.b	#$18,width_pixels(a0)	; Now moved here instead of being at loc_2ABFA
	move.w	#$200,d0
	tst.w	(Two_player_mode).w
	beq.s	loc_2ABA0
	bra.w	JmpTo4_DisplaySprite3

There. That object is now fixed. I've gone through the whole of S2 with Sonic and Tails, and this was the only thing I spotted that went wrong. Everything else seems to be intact. You should now be all set, and able to port over the S3K Object and Priority Managers!

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 Spin Dash Code and Add Spin Dash Speeds | 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
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 | Add beta spindash to Sonic 2 | 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
Port Sonic 1 Sound Driver | Port Sonic 2 Clone Driver | Port Sonic 3 Sound Driver | Expand the Music Index to Start at $00 (Sonic 2 Clone Driver Version)
Extending the Game
Extend the Level Index Past $10 | Extend the Level Select | Extend Water Tables | Add Extra Characters | Free Up 2 Universal SSTs