Actions

SCHG How-to

Port S3K Priority Manager into Sonic 2

From Sonic Retro

(Guide written by redhotsonic)

Preface

WARNING: Please back-up your disassembly before attempting to use this guide. I will not be held responsible for anything that goes wrong. Alternatively, use a version control system such as Mercurial (used for the Sonic Retro repositories) or git, which will not only allow you to back up your repository before making this change but at every step and in all future steps of whatever changes you make, making finding errors easier.

This guide is mainly following Xenowhirl's 2007 disassembly. But, if you're an SVN user, I have tried to make this guide as friendly as possible for you. Anything marked as such is only for SVN or Xenowhirl disassemblies. PAY ATTENTION!

Anything in red is for Xenowhirl's 2007 disassembly users only.
Anything in green is for SVN disassembly users only.

Also, EVERY SST MUST BE EQUATED! So, if you're using an old disassembly, you won't be able to follow this. Also, for any objects you've made yourself or have ported, make sure that it's all equated (so instead of $18(a0), it should be priority(a0).) Anything that has not been equated may start to cause problems; you have been warned!

What is "Priority"?

In all Sonic games, there is a universal SST called Priority. Each sprite has one. The higher the priority number, the lower priority it has. Then the objects can be displayed accordingly. For example, if Sonic has a priority of 2, and Dr. Eggman's priority is 3, then when both sprites are displayed over each other, Sonic will ALWAYS be displayed in front of him. It is the equivalent of what other programming languages (Such as Game Maker) would call "depth".

Differences between S2 & S3 Managers

In Sonic 2, the priority's universal SST is a byte. It's start's from #0 (highest) up to #7 (lowest). When each object jumps to the "DisplaySprite" subroutine, it firsts, converts the priority byte into a word. And then it can check whether to display the sprite, and if so, display it. It has to do this every frame for every object for the sprite to be displayed.

In Sonic 3 and Knuckles, priority is already a word, again, it's start's from #0 (highest) but goes upto #$380 (lowest). That doesn't mean there's $380 types of priority. It goes up in $80's (#0, $80, $100, $180, etc). Because it's already set as a word, when it jumps to the "DisplaySprite" subroutine, it doesn't have to do them calculations, and can just check whether to display the sprite or not and if so, display it.

So basically, S3K's "DisplaySprite" has shaven a few commands away. Because S3K does not have to do them calculations changing from a byte to a word like S2 does on every single frame, it can save a lot of time, and can use the processor on other things instead. When you see a lot of sprites on screen, this can make a huge difference to gameplay.

Porting S3K's Priority Manager into S2 can help get rid of some of the lag your hack may be experiencing. If you follow this guide step-by-step carefully, and make sure you have some time on your hands because it can take a little while, everything should go fine.

Please be aware, once you've started this, you cannot rebuild your ROM until you've finished. Otherwise, you'll get errors and crashes.

Step 1: Getting a free Universal SST

The biggest problem is freeing a universal SST, seeming as they are all being used. Luckily, quite a while back, I showed a guide on how to free two universal SST's! [1] If you have already done part 2 of this guide, you do not need to carry on with this step. Part 2 is the essential SST we need for this guide. Part 1 isn't as important.

If you haven't done this already, you will need to do so. Below is the second step from the SST guide. Follow this step before continuing. First, go to Sonic's SST's (This will be in s2constants.asm for SVN users) 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, therefore priority can now be used as a word ($18 and $19)!

Step 2: Setting up DisplaySprite and DisplaySprite2

You should now have width_pixels at $14 instead of $19. Because priority is at $18, and $19 is now free, theoretically, the priority SST can now be used as a word.

Next, we need to change DisplaySprite and DisplaySprite2. Do NOT edit/change or replace DisplaySprite3! Leave that as it is! At "DisplaySprite:", you'll see this:

        move.w  priority(a0),d0
        lsr.w   #1,d0
        andi.w  #$380,d0
        adda.w  d0,a1

These are the lines that does the calculations every single frame. This is what converts the byte into a word. Like said in S3K, as priority is already a word, these lines are now useless. As this is what we're trying to achieve, we can do the same thing.

So, at "DisplaySprite:", change this:

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||

; sub_164F4:
DisplaySprite:
        lea     (Sprite_Table_Input).w,a1
        move.w  priority(a0),d0
        lsr.w   #1,d0
        andi.w  #$380,d0
        adda.w  d0,a1
        cmpi.w  #$7E,(a1)
        bcc.s   return_16510
        addq.w  #2,(a1)
        adda.w  (a1),a1
        move.w  a0,(a1)

return_16510:
        rts
; End of function DisplaySprite

; ---------------------------------------------------------------------------
; Subroutine to display a sprite/object, when a1 is the object RAM
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||

; sub_16512:
DisplaySprite2:
        lea     (Sprite_Table_Input).w,a2
        move.w  priority(a1),d0
        lsr.w   #1,d0
        andi.w  #$380,d0
        adda.w  d0,a2
        cmpi.w  #$7E,(a2)
        bcc.s   return_1652E
        addq.w  #2,(a2)
        adda.w  (a2),a2
        move.w  a1,(a2)

return_1652E:
        rts
; End of function DisplaySprite2

To this

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||

; sub_164F4:
DisplaySprite:
        lea     (Sprite_Table_Input).w,a1
        adda.w  priority(a0),a1
        cmpi.w  #$7E,(a1)
        bcc.s   return_16510
        addq.w  #2,(a1)
        adda.w  (a1),a1
        move.w  a0,(a1)

return_16510:
        rts
; End of function DisplaySprite

; ---------------------------------------------------------------------------
; Subroutine to display a sprite/object, when a1 is the object RAM
; ---------------------------------------------------------------------------

; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||

; sub_16512:
DisplaySprite2:
        lea     (Sprite_Table_Input).w,a2
        adda.w  priority(a1),a2
        cmpi.w  #$7E,(a2)
        bcc.s   return_1652E
        addq.w  #2,(a2)
        adda.w  (a2),a2
        move.w  a1,(a2)

return_1652E:
        rts
; End of function DisplaySprite2

Both of these subroutines are now shorter. May not look by much, but remember, as this is getting repeated all the time for EACH object for each frame, this will make a lot of difference.

DisplaySprite3 does not need changing. This is because it already does a similar thing. Some objects move a priority word to d0, then DisplaySprite3 uses that d0. As it's already a word, nothing needs changing.

Step 3: Set objects that uses tables for priority to the new manager

Now, a lot of objects just move a byte to the object's priority. But some objects use a table for priority, width, mappings, etc.

Now, editing the table can cause problems, it can start making the objects code out of line, or even the rest of the game, which will cause the game to randomly crash at certain times. But it has to be a word, it cannot stay as a byte.

So, the best thing I found it to actually use them calculations from Sonic 2's DisplaySprite to convert them into a word. So, when creating the object, it will have the priority as a byte, and then do Sonic 2's calculations to convert it into a word. Priority will then remain as a word for the rest of the time that object is there for. Because of this, it will only need to do this calculation once, then carry on the S3K way. Doing it once then the S3K way, is better then doing them calculations all the time, right?

So, first, go to "loc_112A4:" ("Obj1C_Init:" on SVN) This is "Object 1C - Bridge stake in Emerald Hill Zone and Hill Top Zone, falling oil in Oil Ocean Zone" object. You can see it's moving a lot of data to mappings and etc. It does this because there's so many subtypes to the object, and this is the best and quickest way for that object to load.

So, underneath "move.b (a1)+,priority(a0)", add this:

        move.w  priority(a0),d0
        lsr.w   #1,d0
        andi.w  #$380,d0
        move.w  d0,priority(a0)

So you have something like this (SVN user, your code will look slightly different, this is only a reference):

loc_112A4:
        addq.b  #2,routine(a0)
        moveq   #0,d0
        move.b  subtype(a0),d0
        move.w  d0,d1
        lsl.w   #3,d0
        lea     dword_111E6(pc),a1
        lea     (a1,d0.w),a1
        move.b  (a1),mapping_frame(a0)
        move.l  (a1)+,mappings(a0)
        move.w  (a1)+,art_tile(a0)
        bsr.w   Adjust2PArtPointer
        ori.b   #4,render_flags(a0)
        move.b  (a1)+,width_pixels(a0)
        move.b  (a1)+,priority(a0)
        move.w  priority(a0),d0
        lsr.w   #1,d0
        andi.w  #$380,d0
        move.w  d0,priority(a0)
        lea     byte_1128E(pc),a1
        move.b  (a1,d1.w),d1
        beq.s   BranchTo_MarkObjGone
        move.b  d1,y_radius(a0)
        bset    #4,render_flags(a0)

Whatever data is grabbed from the table, it's now converted it to a word, and it will never do it again. It will always remain as a word. So, when it goes to DisplaySprite over and over, it can just get on with it.

Next, go to "loc_1131A:" ("Obj71_Init:" for SVN) (Object 71 - Bridge stake and pulsing orb from Hidden Palace Zone) and do the same thing, so you end up with this (SVN users, reference only):

loc_1131A:
        addq.b  #2,routine(a0)
        move.b  subtype(a0),d0
        andi.w  #$F,d0
        lsl.w   #3,d0
        lea     dword_11302(pc),a1
        lea     (a1,d0.w),a1
        move.b  (a1),mapping_frame(a0)
        move.l  (a1)+,mappings(a0)
        move.w  (a1)+,art_tile(a0)
        bsr.w   Adjust2PArtPointer
        ori.b   #4,render_flags(a0)
        move.b  (a1)+,width_pixels(a0)
        move.b  (a1)+,priority(a0)
        move.w  priority(a0),d0
        lsr.w   #1,d0
        andi.w  #$380,d0
        move.w  d0,priority(a0)
        move.b  subtype(a0),d0
        andi.w  #$F0,d0
        lsr.b   #4,d0
        move.b  d0,anim(a0)

Go to "loc_3F228:" (Object 3E - Egg prison) and almost do the same thing again. Just look at the registers though, as they are different (it's a1 instead of a0). You should end up with this (SVN users, reference only):

loc_3F228:
        _move.b 0(a0),0(a1) ; load obj
        move.w  x_pos(a0),x_pos(a1)
        move.w  y_pos(a0),y_pos(a1)
        move.w  y_pos(a0),objoff_30(a1)
        move.l  #Obj3E_MapUnc_3F436,mappings(a1)
        move.w  #$2680,art_tile(a1)
        move.b  #$84,render_flags(a1)
        moveq   #0,d0
        move.b  (a2)+,d0
        sub.w   d0,y_pos(a1)
        move.w  y_pos(a1),objoff_30(a1)
        move.b  (a2)+,routine(a1)
        move.b  (a2)+,width_pixels(a1)
        move.b  (a2)+,priority(a1)
        move.w  priority(a1),d0
        lsr.w   #1,d0
        andi.w  #$380,d0
        move.w  d0,priority(a1)
        move.b  (a2)+,mapping_frame(a1)

And finally, go to "LoadSubObject_Part3:". Now this loads all the priorities, widths, mappings, etc, to all objects past Obj8C. This is a life saver. So this will save us a lot of time from doing it to a lot of other objects.

So, change this:

LoadSubObject_Part3:
        move.l  (a1)+,mappings(a0)
        move.w  (a1)+,art_tile(a0)
        jsr     Adjust2PArtPointer
        move.b  (a1)+,d0
        or.b    d0,render_flags(a0)
        move.b  (a1)+,priority(a0)
        move.b  (a1)+,width_pixels(a0)
        move.b  (a1),collision_flags(a0)
        addq.b  #2,routine(a0)
        rts

to this:

LoadSubObject_Part3:
        move.l  (a1)+,mappings(a0)
        move.w  (a1)+,art_tile(a0)
        jsr     Adjust2PArtPointer
        move.b  (a1)+,d0
        or.b    d0,render_flags(a0)
        move.b  (a1)+,priority(a0)
        move.w  priority(a0),d0
        lsr.w   #1,d0
        andi.w  #$380,d0
        move.w  d0,priority(a0)
        move.b  (a1)+,width_pixels(a0)
        move.b  (a1),collision_flags(a0)
        addq.b  #2,routine(a0)
        rts

That's half the objects done already!

Step 4: Set the rest of the objects to the new manager

Now, this step is easy, but time consuming. You will be a while doing this. You need to search through the whole of your asm file, and change all priority's to a word. Here is an example.

At "Obj5E:", you'll see this:

        move.b  #0,priority(a0)

Change it to this:

        move.w  #0,priority(a0)

The move command has now changed from .b to .w as priority is now a word. The number will also need changing, but #0 in S2 is the same as S3K. So 0 doesn't need changing here.

Another example. Go to "loc_7158:" ("Obj5F_Init:" for SVN) and change this:

        move.b  #1,priority(a0)

to this:

        move.w  #$80,priority(a0)

Again, the .b has change to .w (this must be done ALL the time, even if the number is 0). This time, the number is 1. After calculations (or in S3K), the number will be $80. So it's been changed to $80. Remember, in S2, the numbers were only #. With the S3K way (unless 0), it will need to be #$.

You need to find all these and change all .b to .w and change the numbers to the relavant number it would have become after the original calculations.

To help, I've made this table for reference:


===============================
TABLE CONVERSION FROM S2 TO S3K
===============================
| | S 2 | S3K |
|---|-----|-----|
| ~ | # | #$ |
| P | 0 | 0 |
| R | 1 | 80 |
| I | 2 | 100 |
| O | 3 | 180 |
| R | 4 | 200 |
| I | 5 | 280 |
| T | 6 | 300 |
| Y | 7 | 380 |
===============================


Want one more example? Okay. Go to "ObjDB_Sonic_Init:" and change this:

        move.b  #2,priority(a0)

to this:

        move.w  #$100,priority(a0)

It's that simple. Now go! Convert all them priorities!

HINT: To make this step miles quicker, you can do a search and replace. Just be careful when doing this. Make sure you do it right. Remember that not all priorities use a0. Some use a1, or a2. You could do something similar to this:

Search:

move.b  #2,priority(a

Replace with:

move.w  #$100,priority(a

...and etc. That way, it will replace them all, no matter what register it has. Do the same from #0 - #7.

Be warned though! If you've edited the tabs (spaces) on any coding, or on your own made objects, the "Search and Replace" may not be the best way to go, and you may have to do this manually. If you do do the "search and replace" idea, I'd highly reccommend you still search through the ASM file after to check you've done it right.

Step 5: Make sure the copiers are copying correctly

At these locations (Xenowhirl 2007 users):

  • loc_10B9E:
  • loc_15E46:
  • Obj79_MakeSpecialStars:
  • loc_25C24:
  • loc_28A6E:

SVN users ONLY: For first label, do a search for this line:

        _move.b d4,id(a1) ; load obj1F

Second label is "BreakObjectToPieces_InitObject:"
Third label is "Obj79_MakeSpecialStars:"
Fourth Label is "loc_25C24:"
And fifth label is "Obj73_LoadSubObject:"

Under each of these labels, you should see this line:

        move.b  priority(a0),priority(a1)

Some objects like these, move the priority to a1 from a0. As you can see, it's only moving a byte. Change the line at all these locations to this:

        move.w  priority(a0),priority(a1)

All the registers at these locations are the same, so the above line is fine to use.

Step 6: Make sure some objects are calculating correctly

Similar to step 5, some objects copy the priority to another address register or data register, then does a small subtraction, then moves it back. As it's going by S2's priority, this needs changing also.

Go to "loc_34864:" (Tails in Special Stage) and change this bit only:

        move.b  priority(a0),priority(a1)
        subi.b  #1,priority(a1)

to this:

        move.w  priority(a0),priority(a1)
        subi.w  #$80,priority(a1)

So you have this (SVN users, reference only):

loc_34864:
        move.w  #$400,objoff_32(a0)
        move.b  #$40,angle(a0)
        move.b  #1,($FFFFF7DE).w
        clr.b   collision_property(a0)
        clr.b   respawn_index(a0)
        bsr.w   loc_349C8
        movea.l #Object_RAM+$180,a1
        move.b  #$63,(a1) ; load obj63 (shadow) at $FFFFB180
        move.w  x_pos(a0),x_pos(a1)
        move.w  y_pos(a0),y_pos(a1)
        addi.w  #$18,y_pos(a1)
        move.l  #Obj63_MapUnc_34492,mappings(a1)
        move.w  #$623C,art_tile(a1)
        move.b  #4,render_flags(a1)
        move.w  #$200,priority(a1)
        move.l  a0,objoff_38(a1)
        movea.l #Object_RAM+$1C0,a1
        move.b  #$88,(a1) ; load obj88
        move.w  x_pos(a0),x_pos(a1)
        move.w  y_pos(a0),y_pos(a1)
        move.l  #Obj88_MapUnc_34DA8,mappings(a1)
        move.w  #$4316,art_tile(a1)
        move.b  #4,render_flags(a1)
        move.w  priority(a0),priority(a1)
        subi.w  #$80,priority(a1)
        move.l  a0,objoff_38(a1)
        movea.l a1,a0
        move.b  #1,($FFFFF7DF).w
        clr.b   respawn_index(a0)
        movea.l objoff_38(a0),a0 ; load 0bj address
        rts

Now, it's moving a WORD to a1, then subtracting $80 instead of 1. So now it's going how S3K would do it.

Similar story here. Go to "Obj88:" (Tails' tails in Special Stage) and change this bit only:

        move.b  priority(a1),d0
        subq.b  #1,d0
        move.b  d0,priority(a0)

to this:

        move.w  priority(a1),d0
        subi.w  #$80,d0
        move.w  d0,priority(a0)

One more. Go to "loc_3C1F4:" (Breakable plating from WFZ) and change this bit only:

        move.b  priority(a0),d4
        subq.b  #1,d4

to this:

        move.w  priority(a0),d4
        subi.w  #$80,d4

There's a little more to this object. Next, go to "loc_3C20E:" and change this bit only:

        move.b  d4,priority(a1)

to this:

        move.w  d4,priority(a1)

These objects will now display correctly.

Step 7: Make sure EHZ's boss uses the right compares

At these locations:

  • loc_2F714:
  • loc_2F77E:
  • loc_2F7A6:

The EHZ boss is showing these commands:

        cmpi.b  #2,priority(a0)

Change them to this:

        cmpi.w  #$100,priority(a0)

They were still treating priority as a byte, so we just needed to change these to a word.

Step 8: Fix ARZ, CNZ, MCZ and OOZ's boss's priority

These are a pain in the ass to fix! These bosses use the priority and (the old) width_pixel's SST's together for a complete different reason (The hammer, drills and catchers). It's moves odd numbers to the priority SST (well, it's actually positions, but the number is odd to DisplaySprite) and because of this, it cannot display the word any longer and therefore, the game freezes pretty much instantly when you approach one of these bosses.

Luckily, there are many objects that do a similar thing; using priority for a complete different reason. This is where "DisplaySprite3" comes into play. Objects that use priority SST for other reasons, instead, moves a word to d0, then jumps to DisplaySprite3. So, we're going to do something similar for ARZ, CNZ, MCZ and OOZ's bosses.

ARZ boss

Go to "loc_304D4:" and delete the line:

        move.w  #$100,priority(a0)

This is no longer needed, as it's going to be replaced with other things later in the ARZ boss code anyway.

Still at this label, you should also see these commands:

        move.w  #$200,priority(a1)
        move.w  #$100,priority(a1)

You might as well get rid of them too.

Make sure you do NOT delete this line:

        move.w  #$488,priority(a0)

This is for the y_pos of the hammer, one of the reason why the priority needs changing of the boss.

Next, go to "loc_30BC8:" and find and delete the line:

        move.w  #$200,priority(a0)

Again, no point in this!

Next, find the label "JmpTo37_DisplaySprite". Only the ARZ boss jumps to this location for displaying a sprite, so we know we can freely edit this for our ARZ boss without affecting anything else. Anyway, you should see this (SVN user, you're will look ever so slightly different, but you can still copy and paste my fix that I'm about to show):

JmpTo37_DisplaySprite 
        jmp     DisplaySprite

Now, because them odd numbers are set as priority, when it jumps to DisplaySprite, it will try to process it and freeze. So, this is what we're going to do. Change it to this:

JmpTo37_DisplaySprite 
        move.w  #$200,d0        ; move $200 to ARZ boss' priority
        jmp     DisplaySprite3  ; Dispaly it

Now, everytime the boss needs displaying, instead of grabbing the data from the priority SST, it will process what we've just moved to d0. #$200 seems good, I use that.

I would have tried changing these priority commands to a different universal SST, but the ARZ boss unfortunately uses them all, so there's nothing free. So, the priority for Eggman, arrows, hammer and totem poles now have to be the same priority, but really, you won't notice any different to the way the boss displays it's sprites.

Viola! ARZ boss is sorted and will no longer freeze.

CNZ boss

The problem with this boss is the exact same problem as ARZ. Priority is being used for something else. Same sort of fix applies here.

Go to "loc_31904:" and delete the line:

        move.w  #$180,priority(a0)

Also, go to "loc_31F48:" and delete the line:

        move.w  #$380,priority(a0)

Next, find the label "JmpTo39_DisplaySprite". Only the CNZ boss jumps to this location for displaying a sprite, so again, we know it's safe to edit. You should see this (SVN user, yours will look ever so slightly different, but you can still copy and paste my fix that I'm about to show):

JmpTo39_DisplaySprite 
        jmp     DisplaySprite

Change it to this:

JmpTo39_DisplaySprite 
        move.w  #$180,d0        ; move $180 to CNZ boss' priority
        jmp     DisplaySprite3  ; Dispaly it

CNZ boss should no longer freeze. #$180 seems good here. Again, CNZ boss shares the same problem as ARZ boss, so Eggman, the catchers and spikeball have to be the same priority, and you shouldn't notice any difference.

MCZ boss

Same issue. Priority is being used for something else. Same sort of fix applies here. But luckily, all the priorities are #$180, so at least the priority won't change after this edit unlike the previous two bosses we've edited.

Go to "loc_30FB8:" and delete the line:

        move.w  #$180,priority(a0)

Also, go to "loc_313DA:" ("Obj57_LoadStoneSpike:" for SVN) and delete the same(ish) line (I say 'ish' because it's using a1 instead of a0):

        move.w  #$180,priority(a1)

Next, find the label "JmpTo38_DisplaySprite". Only the MCZ boss jumps to this location for displaying a sprite, so again, we know it's safe to edit. You should see this (SVN user, you're will look ever so slightly different, but you can still copy and paste my fix that I'm about to show):

JmpTo38_DisplaySprite 
        jmp     DisplaySprite

Change it to this:

JmpTo38_DisplaySprite 
        move.w  #$180,d0        ; move $180 to MCZ boss' priority
        jmp     DisplaySprite3  ; Dispaly it

Seeming as both priority lines we just deleted was #$180, we know that we can use #$180 here. MCZ boss should no longer freeze.

OOZ boss

This boss uses Priority as a x_pos for the sub objects. Again, we're going to have to do a similar thing like we did with the other bosses.

Go to "loc_32FA8:" ("Obj55_Init:" for SVN) and delete the line:

        move.w  #$180,priority(a0)

Also, go to "loc_33586:" ("Obj55_Laser_Init:" for SVN) and delete the line:

        move.w  #$200,priority(a0)

Also, go to "loc_33640:" ("Obj55_Laser_CreateWave:" for SVN) and delete the line:

        move.w  #$100,priority(a1)

Next, find the label "JmpTo41_DisplaySprite". Only the OOZ boss jumps to this location for displaying a sprite, so again, we know it's safe to edit. You should see this (SVN user, yours will look ever so slightly different, but you can still copy and paste my fix that I'm about to show):

JmpTo41_DisplaySprite
        jmp     DisplaySprite

Change it to this:

JmpTo41_DisplaySprite 
        move.w  #$180,d0        ; move $180 to OOZ boss' priority
        jmp     DisplaySprite3  ; Dispaly it

OOZ boss should no longer freeze. #$180 seems good here. Again, all boss objects have to be the same priority, and you shouldn't notice any difference.

Notice: I have compared the priorities to the original Sonic 2's priorities for these bosses with our new "set" priorities (that we've moved to d0) and I saw no difference whatsover. So all the above is fine. For example, with Sonic 2's original priorities set, Sonic was always in front of the ARZ boss, hammer, totem poles and arrows. He was never behind them. The way we have set up the bosses above, this is still the case. That's why you shouldn't notice any difference.

Step 9: Fix MTZ's boss's priority

You're probably thinking, "What? This boss gets it's very own step?" Yup, and the reason why, is because there is a free universal SST! Hoozah! That means we can fix this boss without making everything use the same priority!

Like the others, the boss uses priority for other things (you know the little flame on Eggman's ship? Priority is used for that y_pos. But luckily for us, anim_frame_duration's SST is completely free. Also, the SST after it (objoff_1F) is also free! So we can use this as a word!

Unfortunately, it seems that I couldn't make the flame use "anim_frame_duration" and I'm not sure why. So instead, we're going to make the DisplaySprite read from anim_frame_duration. Okay, let's fix this damn boss...

Go to "loc_3229E:" ("Obj54_Init:" for SVN) and change this:

        move.w  #$180,priority(a0)

to this:

        move.w  #$180,anim_frame_duration(a0)

Still at this location, change:

        move.w  #$300,priority(a1)

to this:

        move.w  #$300,anim_frame_duration(a1)

Make sure you do NOT edit/change this line:

        move.w  y_pos(a0),priority(a0)

This is for the y_pos of the flame. DO NOT EDIT


Go to these locations (Xenowhirl 2007 users):

  • loc_32966:
  • loc_32B1A:
  • loc_32B34:
  • loc_32B42:
  • loc_32B56:
  • loc_32CC0:

SVN users ONLY: For first label, do a search for this line:

        move.l  objoff_34(a0),objoff_34(a1)

Second label is "Obj53_SetAnimPriority:"
Third label is directly underneath the "Obj53_SetAnimPriority:" label
Fourth Label is directly underneath the previous label we were at
Fifth label is directly underneath the previous label we were at
And sixth label is "Obj54_Laser_Init:"

And change the priority to anim_frame_duration. For example (SVN users, reference only):

loc_32B34:
        move.b  #4,mapping_frame(a0)
        move.w  #$100,anim_frame_duration(a0)
        rts

Next, find the label "JmpTo40_DisplaySprite". Only the MTZ boss jumps to this location for displaying a sprite, so again, we know it's safe to edit. You should see this (SVN user, you're will look ever so slightly different, but you can still copy and paste my fix that I'm about to show):

JmpTo40_DisplaySprite
        jmp     DisplaySprite

Change it to this:

JmpTo40_DisplaySprite 
        move.w  anim_frame_duration(a0),d0
        jmp     DisplaySprite3

There we go! This boss is now fixed and will no longer freeze. And the priorities haven't been changed! Hooray! If only the other bosses we had to edit would play ball.

Step 10: Fix the priority for when the main character is dead

As previously stated, some objects use the priority SST for other purposes. That's why a lot of them use DisplaySprite3 instead.

The problem is, when Sonic (main character) dies, all objects freeze (well, most of them anyway). When they've frozen, they all jump to "RunObjectDisplayOnly:"

RunObjectDisplayOnly:
        moveq   #0,d0
        move.b  (a0),d0 ; get the object's ID
        beq.s   +       ; if it's obj00, skip it
        tst.b   render_flags(a0)        ; should we render it?
        bpl.s   +                       ; if not, skip it
        bsr.w   DisplaySprite
+
        lea     next_object(a0),a0 ; load 0bj address
        dbf     d7,RunObjectDisplayOnly
        rts
; End of function RunObjectDisplayOnly

As you can see, ALL objects will jump to DisplaySprite. This wasn't a problem before because it just took a byte from the priority and then convert it to a word, which was always even. But now, it doesn't with our new manager. It just takes the word. So, if the priority SST is odd, it will still try to display it, and then freeze.

Most of the time, when Sonic dies, everything will be fine. But if Sonic dies near the EHZ bridge, CNZ spring, ARZ boss, most stuff that use DisplaySprite3, the game will freeze. Because instead of displaying whatever is at d0, it's jumping to DisplaySprite, and taking it from priority SST instead, which is more-than-likely odd (because it was used for something else).

What we need to do, is set a new priority for all objects when Sonic is dead. So, we're going to change the branch to DisplaySprite to something else. So, go to "RunObjectDisplayOnly:" and change it to this:

RunObjectDisplayOnly:
        moveq   #0,d0
        move.b  (a0),d0 ; get the object's ID
        beq.s   +       ; if it's obj00, skip it
        tst.b   render_flags(a0)        ; should we render it?
        bpl.s   +                       ; if not, skip it
        move.w  #$200,d0
        bsr.w   DisplaySprite3
+
        lea     next_object(a0),a0 ; load 0bj address
        dbf     d7,RunObjectDisplayOnly
        rts
; End of function RunObjectDisplayOnly

So now, when Sonic dies, all objects that are frozen will have their priority set to #$200. This is what most objects use anyway. I didn't actually notice any difference to when Sonic normally dies.

Because of this now, objects that were originally DisplaySprite3, when Sonic dies, it won't try taking the priority SST anymore, and just use the #$200 we've set. If you enter debug when in the middle of dying, then exit so you're back alive, all objects will start using their orignial priority (or d0 if they were using DisplaySprite3).

The "Game Over" object is one of the few objects that doesn't freeze when Sonic dies, so that object will still use it's own priority (which is 0). So you don't have to worry about the "Game Over" text being behind any objects.

Now, A couple issues occur, because, when you die, all objects jump to the "RunObjectDisplayOnly:". As they're all using the same priority, a newer one will overlap. Take the EHZ boss. His drill gets displayed first, then his wheels (so they overlap), then the cockpit (so they overlap the rest), then finally Eggman (which overlaps all). Another example of this is during the final boss, where the final boss overlaps his arms.

To fix this, it's simple, and I should have thought of it before (Sonic 2 Recreation has different coding and doesn't suffer this). Anyway, go to "RunObjectDisplayOnly:", and change from this:

RunObjectDisplayOnly:
        moveq   #0,d0
        move.b  (a0),d0 ; get the object's ID
        beq.s   +       ; if it's obj00, skip it
        tst.b   render_flags(a0)        ; should we render it?
        bpl.s   +                       ; if not, skip it
        move.w  #$200,d0
        bsr.w   DisplaySprite3
+
        lea     next_object(a0),a0 ; load 0bj address
        dbf     d7,RunObjectDisplayOnly
        rts
; End of function RunObjectDisplayOnly

to this:

RunObjectDisplayOnly:
        moveq   #0,d0                   ; Clear d0 quickly
        move.b  (a0),d0                 ; get the object's ID
        beq.s   ++                      ; if it's obj00, skip it
        tst.b   render_flags(a0)        ; should we render it?
        bpl.s   ++                      ; if not, skip it
        move.w  priority(a0),d0         ; move object's priority to d0
        btst    #6,render_flags(a0)     ; is the compound sprites flag set?
        beq.s   +                       ; if not, branch
        move.w  #$200,d0                ; move $200 to d0
+
        bsr.w   DisplaySprite3          ; Display the object with whatever is set at d0
+
        lea     next_object(a0),a0      ; load 0bj address
        dbf     d7,RunObjectDisplayOnly
        rts
; End of function RunObjectDisplayOnly

So now, when all objects jump here, it will ask first, does it have the compound sprite flag set? If not, branch and continue, and use the object's original priority. If it IS set, it means that the current object's status table also contains information about other child sprites which need to be drawn using the current object's mappings. In other words, for our sake, it's using priority for something different. Therefore, it will NOT branch and it will move $200 to d0 instead.

Step 11: Fix the priority for Special Stages

This took me fucking forever to fix! But luckily for you, I've figured it out without having to change any of the priorities (unlike the bosses we did earlier). It's going to be easy for you!

Playing the special stages as they are now will make the game freeze within 2 seconds. That can't be good, right? We will need to fix this.

Go to "Obj87:". This is the "Number of rings in Special Stage" object. This is what cause the special stage to freeze in a matter of seconds.

You may notice that it's not moving anything to a priority SST. That's because the priority for this object is 0. The problem is, it's moving numbers from SST $19(a0) onwards. Mainly at $19(a0) is #$20. Now, because we've got the new priority manager, it will grab it's priority (which is 0), but as it's now a word, it will grab that #$20. So, it will have the data #$0020. DisplaySprite cannot process that, and crashes. We need to make it grab #$0000 and not #$0020, but we can't change anything at SST $19(a0), otherwise the special stage will start to misbehave. The fix? It's quite simple.

At these locations (Xenowhirl 2007 users):

  • loc_7536:
  • loc_758C:
  • loc_75BC:
  • loc_75C8:
  • loc_7648:

SVN users ONLY: First label is "loc_7536:"
For second label, do a search for this line:

move.b  d2,sub2_mapframe-sub2_x_pos(a1) ; sub2_mapframe

Third label is directly underneath the previous label we were at
Fourth Label is directly underneath the previous label we were at
And, for the fifth label, do a search for this line:

        move.w  #$D8,(a1)       ; sub?_x_pos

and it's the + label directly underneath it

You will see the code branching (in some way) to "JmpTo_DisplaySprite". We're going to create a new label for this object to jump to instead. So, at these locations, change all:

JmpTo_DisplaySprite

to this:

JmpTo_DisplaySpriteSpecial

Example (SVN users, reference only):

loc_7536:
        move.b  d3,objoff_F(a0)
        bra.w   JmpTo_DisplaySpriteSpecial

Make sure you only change it to the new label at these locations!

Next, go to the label itself "JmpTo_DisplaySprite" to see this (SVN user, you're will look ever so slightly different, but you can still copy and paste my fix that I'm about to show):

JmpTo_DisplaySprite 
        jmp     DisplaySprite.l

Directly underneath it, add this:

JmpTo_DisplaySpriteSpecial
        move.w  #0,d0
        jmp     DisplaySprite3.l

So you end up with this (SVN users, reference only):

JmpTo_DisplaySprite 
        jmp     DisplaySprite.l
        
JmpTo_DisplaySpriteSpecial
        move.w  #0,d0
        jmp     DisplaySprite3.l

Now, obj87 only, will jump here for displaying the sprite. It will force #$0000 to be displayed instead. And it doesn't affect any of the object itself. We couldn't edit the "JmpTo_DisplaySprite" label itself because other objects are jumping to that label, and we don't want to edit other object's priority.

The special stage will no longer freeze within seconds...

But hold on there!

I'm sorry, the special stage can still freeze halfway through. Yeah, another object does not want to play ball. The "Messages/checkpoint from Special Stage" object.

This object displays the "Rings to go!" text and the "Collect 40 rings" text and to make them fly apart from each other. It also displays the "Cool" and "Not enough rings" text and the hand with the blue emblem, etc. To be totally honest, this object is almost perfect, it's the "Rings to go" that's causing the freezing. The problem is pretty much the same as "Number of rings in Special Stage" object.

Go to "loc_357B2:" ("Obj5A_FlashMessage:" for SVN) And you should see (SVN users, reference only):

loc_357B2:
        tst.b   ($FFFFDBA0).w
        bne.w   return_357D0
        tst.b   ($FFFFDBA6).w
        bne.s   return_357D0
        move.b  ($FFFFFE0F).w,d0
        andi.b  #7,d0
        cmpi.b  #6,d0
        bcs.w   JmpTo44_DisplaySprite

That branch to "JmpTo44_DisplaySprite" is the problem. Looking at the object, this bit's priority is always 0. So, change the "JmpTo44_DisplaySprite" to "JmpTo_DisplaySpriteSpecial2", so you have this (SVN users, reference only):

loc_357B2:
        tst.b   ($FFFFDBA0).w
        bne.w   return_357D0
        tst.b   ($FFFFDBA6).w
        bne.s   return_357D0
        move.b  ($FFFFFE0F).w,d0
        andi.b  #7,d0
        cmpi.b  #6,d0
        bcs.w   JmpTo_DisplaySpriteSpecial2

Yes, we're creating another new label! It's the best way to do it. Next, go to "loc_35F76:". Right above that label, paste this:

JmpTo_DisplaySpriteSpecial2:
        move.w  #0,d0
        jmp     Displaysprite3.l

So you end up with this (SVN users, reference only):

JmpTo_DisplaySpriteSpecial2:
        move.w  #0,d0
        jmp     Displaysprite3.l

loc_35F76:
        add.w   d0,d0
        move.w  d0,d1
        add.w   d0,d0
        add.w   d1,d0
        move.w  word_35F92(pc,d0.w),(Normal_palette_line4+$16).w
        move.w  word_35F92+2(pc,d0.w),(Normal_palette_line4+$18).w
        move.w  word_35F92+4(pc,d0.w),(Normal_palette_line4+$1A).w
        rts

Fixed! That part of the object will be forced to use #$0000 again and to display it. The rest of the object is completely fine though, and does not need editing (plus, the rest of it isn't using 0).

Step 11.b: EXTRA STEP for SVN users

ATTENTION SVN USERS! You've got one more thing to do. Fortunately it is piss easy. Now, if you're a Xenowhirl user, you're safe and can ignore this part and move along to Step 12.

Seems that if you've followed this guide thus far as an SVN user, there's a glitch with the Special Stages. Sonic and Tails act like they're being hit by the bombs object over and over, making it impossible to retain any rings in the special stages. The reason? inertia is being used for something else in the special stages. This doesn't seem to be the case in Xenowhirl's disassembly.

So, in your SST table, you're better off switching inertia with something else. invulnerable_time seems to be the best to swap it with. So change them so you end up with this:

invulnerable_time =     $20 ; and $21 ; time remaining until you stop blinking
inertia =               $30 ; and $31 ; directionless representation of speed... not updated in the air

Step 12: Fix the CNZ pull-spring

This CNZ spring has lost it's width_pixels SST. Similar problem to what the priorty manager has done, it's width_pixel is being over-written by something else in the objects code.

If you've already applied this fix to your hack from my "How to free up 2 universal SST's" guide, you can skip this step. Otherwise, here is an excerpt:

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.

Step 13: Fix the priority for Tails' tails

An example of the error with Tails' tails.

Uh oh, an observant eye would notice that Tails' tails' priority has been affected, despite the fact that Tails' priority, and his tails' priority are both $100. Now it seems that his tails are not as much as a priority anymore. Anyway, flamewing had a solution, which he uses for his hack; Sonic 2 Heroes. The fix:

First, you're going to have to use some RAM. Only a word. So, go to your list of equates. I used $FFFFF5C0 (it's free whether you use the S1 sound driver or not). Call the RAM "Tails_Tails_ptr"

Tails_Tails_ptr =               ramaddr( $FFFFF5C0 )

Our new RAM is ready for use. First, go to "InitPlayers:" and under the line:

        move.b  #2,(Sidekick).w ; load Obj02 Tails object at $FFFFB040

SVN users, yours will say:

        move.b  #ObjID_Tails,(Sidekick+id).w ; load Obj02 Tails object at $FFFFB040

Insert this:

        move.w  #Tails_Tails,(Tails_Tails_ptr).w

So you have something like this (SVN users, reference only):

        move.b  #2,(Sidekick).w ; load Obj02 Tails object at $FFFFB040
        move.w  #Tails_Tails,(Tails_Tails_ptr).w
        move.w  (MainCharacter+x_pos).w,(Sidekick+x_pos).w
        move.w  (MainCharacter+y_pos).w,(Sidekick+y_pos).w
        subi.w  #$20,(Sidekick+x_pos).w
        addi.w  #4,(Sidekick+y_pos).w
        move.b  #8,(Tails_Dust).w ; load Obj08 Tails' spindash dust/splash object at $FFFFD140

Do the same at label "InitPlayers_TailsAlone:", so you have something like this (SVN users, reference only):

InitPlayers_TailsAlone:
        move.b  #2,(MainCharacter).w ; load Obj02 Tails object at $FFFFB000
        move.w  #Tails_Tails,(Tails_Tails_ptr).w
        move.b  #8,(Tails_Dust).w ; load Obj08 Tails' spindash dust/splash object at $FFFFD100
        addi.w  #4,(MainCharacter+y_pos).w
        rts
; End of function InitPlayers

Next, go to "loc_A2F2:". This is for when you're at the cutscene at the end of the game.

XenoWhirl users, Change this:

loc_A2F2:
        moveq   #$E,d0
        move.b  #2,(a1) ; load Tails object
        move.b  #$81,$2A(a1)
        move.b  #5,(Object_RAM+$80).w ; load Obj05 (Tails' tails) at $FFFFB080
        move.w  a1,(Object_RAM+$80+parent).w
        rts

to this:

loc_A2F2:
        moveq   #$E,d0
        move.b  #2,(a1) ; load Tails object
        move.b  #$81,$2A(a1)
        move.w  #Object_RAM+$80,(Tails_Tails_ptr).w
        rts

SVN users, change this:

loc_A2F2:
        moveq   #$E,d0
        move.b  #ObjID_Tails,id(a1) ; load Tails object
        move.b  #$81,obj_control(a1)
        move.b  #ObjID_TailsTails,(Tails_Tails_Cutscene+id).w ; load Obj05 (Tails' tails) at $FFFFB080
        move.w  a1,(Tails_Tails_Cutscene+parent).w
        rts

to this:

loc_A2F2:
        moveq   #$E,d0
        move.b  #ObjID_Tails,id(a1) ; load Tails object
        move.b  #$81,obj_control(a1)
        move.w  #Tails_Tails_Cutscene,(Tails_Tails_ptr).w ; Tails' tails at $FFFFB080
        rts

Next, go to "Obj02_Init_Continued:", and change the last few lines.

Xenowhirl users, change this:

        move.b  #5,(Tails_Tails).w ; load Obj05 (Tails' Tails) at $FFFFD000
        move.w  a0,(Tails_Tails+parent).w ; set its parent object to this

And change it to this:

        movea.w (Tails_Tails_ptr).w,a1
        move.b  #5,0(a1) ; load Obj05 (Tails' Tails) at $FFFFD000
        move.w  a0,parent(a1) ; set its parent object to this

SVN users, yours will say:

        move.b  #ObjID_TailsTails,(Tails_Tails+id).w ; load Obj05 (Tails' Tails) at $FFFFD000
        move.w  a0,(Tails_Tails+parent).w ; set its parent object to this

Change it to this:

        movea.w  (Tails_Tails_ptr).w,a1
        move.b  #ObjID_TailsTails,id(a1) ; load Obj05 (Tails' Tails)
        move.w  a0,parent(a1) ; set its parent object to this

One more step! Go to "Obj02:" See the command "jmp Obj02_States(pc,d1.w)"/"jmp Obj02_Index(pc,d1.w)"? Change the "jmp" to a "jsr". And directly underneath it, add this:

        movea.w (Tails_Tails_ptr).w,a1
        tst.b   routine(a1)
        beq.s   +
        jmp     (DisplaySprite2).l
+
        rts

So you have something like this (SVN users, reference only):

Obj02:
        ; a0=character
        cmpi.w  #2,(Player_mode).w
        bne.s   +
        move.w  (Camera_Min_X_pos).w,(Tails_Min_X_pos).w
        move.w  (Camera_Max_X_pos).w,(Tails_Max_X_pos).w
        move.w  (Camera_Max_Y_pos_now).w,(Tails_Max_Y_pos).w
+
        moveq   #0,d0
        move.b  routine(a0),d0
        move.w  Obj02_States(pc,d0.w),d1
        jsr     Obj02_States(pc,d1.w)
        movea.w (Tails_Tails_ptr).w,a1
        tst.b   routine(a1)
        beq.s   +
        jmp     (DisplaySprite2).l
+
        rts

Done! Tails' tails should now work with the right priority! Tails will now queue his tails for drawing right after himself. Without this, the tails would be queued for drawing after all objects between Tails and the tails that have the same priority. And that's why before, Tails' tails had less priority.

Step 14: Done

That's it guys! All done! You have now successfully ported Sonic 3 and Knuckles' Priority Manager into Sonic 2. Your hack should now be experiencing less lag! Please be aware though, this will not eliminate all lag from your hack. It just helps to get rid of some of it. To get rid of more lag, you need to do more; like Object Managers and etc.

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
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