Actions

SCHG How-to

Port S3K Object Manager into Sonic 2

From Sonic Retro

Revision as of 21:28, 20 December 2015 by Scarred Sun (talk | contribs) (Text replacement - "</asm>" to "</syntaxhighlight>")

(Guide written by MoDule)

WARNING: This guide is from a forum post dating back to Christmas 2009. [1] This guide may have some inaccuracies, or may use methods to attain certain effects that are obsolete (for lack of a better word). This guide may need to be revised to be more effective and efficient for use in hacks. Any Members or Tech Members that have the knowledge to make said fixes, please feel free to do so.

This is the objects manager used in Sonic 3 & Knuckles ported to Sonic 2 with minor adjustments for compatibility. In Sonic 2, an object is loaded whenever the camera reaches a certain position relative to it and is deleted when at a certain distance. An area in RAM called the respawn table is used to keep track of certain object's states such as monitors which can only be broken once. Only objects that have this property, indicated by the highest bit in the second word of the object's definition being set, have an entry in the respawn table. It was modified slightly in Sonic 3 & Knuckles. The main difference between the two is that S3&K does an additional y-range check (optional) before loading an object. This leads to less objects being loaded at one time and gives a slight improvement to performance. The other difference is that it is now required that every object gets an entry in the object respawn table. This means the respawn table needs more space in RAM which needs to be found somewhere.

At the time of writing, this guide used the SVN disassembly (Revision 125). It's probably safe to say that newer SVN/Mercurial disassembly users shouldn't have too much trouble at all with this... but you've been put on notice.

Part 1: Replacing a LOT of code!!!

With that, let's begin. Replace everything from "ObjectsManager:" (loc_17AA4 in older disassemblies) up to, but not including, "SingleObjLoad:" with the contents of the following code box:

<asm>

---------------------------------------------------------------------------
Objects Manager
Subroutine to load objects whenever they are close to the screen. Unlike in
normal s2, in this version every object gets an entry in the respawn table.
This is necessary to get the additional y-range checks to work.
input variables
-none-
writes
d0, d1, d2
d3 = upper boundary to load object
d4 = lower boundary to load object
d5 = #$FFF, used to filter out object's y position
d6 = camera position
a0 = address in object placement list
a3 = address in object respawn table
a6 = object loading routine
---------------------------------------------------------------------------
loc_17AA4

ObjectsManager: moveq #0,d0 move.b (Obj_placement_routine).w,d0 jmp ObjectsManager_States(pc,d0.w)

============== JUMP TABLE =============================================

ObjectsManager_States: bra.w ObjectsManager_Init ; 0 bra.w ObjectsManager_Main ; 2 bra.w ObjectsManager_Main ; 4 rts

============== END JUMP TABLE =============================================

ObjectsManager_Init: addq.b #4,(Obj_placement_routine).w

lea (Object_Respawn_Table).w,a0 moveq #0,d0 move.w #bytesToLcnt(Object_Respawn_Table_End-Object_Respawn_Table),d1 ; set loop counter - move.l d0,(a0)+ dbf d1,-

move.w (Current_ZoneAndAct).w,d0

ror.b #1,d0 ; this is from s3k
lsr.w #5,d0
lea (Off_Objects).l,a0
movea.l (a0,d0.w),a0

ror.b #1,d0 lsr.w #6,d0 lea (Off_Objects).l,a0 ; load the first pointer in the object layout list pointer index, adda.w (a0,d0.w),a0 ; load the pointer to the current object layout

tst.w (Two_player_mode).w ; skip if not in 2-player vs mode beq.s + cmpi.b #casino_night_zone,(Current_Zone).w ; skip if not Casino Night Zone bne.s + lea (Objects_CNZ1_2P).l,a0 ; CNZ 1 2-player object layout tst.b (Current_Act).w ; skip if not past act 1 beq.s + lea (Objects_CNZ2_2P).l,a0 ; CNZ 2 2-player object layout + ; initialize each object load address with the first object in the layout move.l a0,(Obj_load_addr_right).w move.l a0,(Obj_load_addr_left).w lea (Object_Respawn_Table).w,a3

move.w (Camera_X_pos).w,d6 subi.w #$80,d6 ; look one chunk to the left bcc.s + ; if the result was negative, moveq #0,d6 ; cap at zero + andi.w #$FF80,d6 ; limit to increments of $80 (width of a chunk)

movea.l (Obj_load_addr_right).w,a0 ; get first object in layout

- ; at the beginning of a level this gives respawn table entries to any object that is one chunk ; behind the left edge of the screen that needs to remember its state (Monitors, Badniks, etc.) cmp.w (a0),d6 ; is object's x position >= d6? bls.s + ; if yes, branch addq.w #6,a0 ; next object addq.w #1,a3 ; respawn index of next object going right bra.s -

---------------------------------------------------------------------------

+ move.l a0,(Obj_load_addr_right).w ; remember rightmost object that has been processed, so far (we still need to look forward) move.w a3,(Obj_respawn_index_right).w ; and its respawn table index

lea (Object_Respawn_Table).w,a3 ; reset a3 movea.l (Obj_load_addr_left).w,a0 ; reset a0 subi.w #$80,d6 ; look even farther left (any object behind this is out of range) bcs.s + ; branch, if camera position would be behind level's left boundary

- ; count how many objects are behind the screen that are not in range and need to remember their state cmp.w (a0),d6 ; is object's x position >= d6? bls.s + ; if yes, branch addq.w #6,a0 addq.w #1,a3 ; respawn index of next object going left bra.s - ; continue with next object

---------------------------------------------------------------------------

+ move.l a0,(Obj_load_addr_left).w ; remember current object from the left move.w a3,(Obj_respawn_index_left).w ; and its respawn table index

move.w #-1,(Camera_X_pos_last).w ; make sure ObjectsManager_GoingForward is run

move.w (Camera_Y_pos).w,d0 andi.w #$FF80,d0 move.w d0,(Camera_Y_pos_last).w ; make sure the Y check isn't run unnecessarily during initialization

---------------------------------------------------------------------------

ObjectsManager_Main: ; get coarse camera position move.w (Camera_Y_pos).w,d1 subi.w #$80,d1 andi.w #$FF80,d1 move.w d1,(Camera_Y_pos_coarse).w

move.w (Camera_X_pos).w,d1 subi.w #$80,d1 andi.w #$FF80,d1 move.w d1,(Camera_X_pos_coarse).w

tst.w (Camera_Min_Y_pos).w ; does this level y-wrap? bpl.s ObjMan_Main_NoYWrap ; if not, branch lea (ChkLoadObj_YWrap).l,a6 ; set object loading routine move.w (Camera_Y_pos).w,d3 andi.w #$FF80,d3 ; get coarse value move.w d3,d4 addi.w #$200,d4 ; set lower boundary subi.w #$80,d3 ; set upper boundary bpl.s + ; branch, if upper boundary > 0 andi.w #$7FF,d3 ; wrap value bra.s ObjMan_Main_Cont

---------------------------------------------------------------------------

+ move.w #$7FF,d0 addq.w #1,d0 cmp.w d0,d4 bls.s + ; branch, if lower boundary < $7FF andi.w #$7FF,d4 ; wrap value bra.s ObjMan_Main_Cont

---------------------------------------------------------------------------

ObjMan_Main_NoYWrap: move.w (Camera_Y_pos).w,d3 andi.w #$FF80,d3 ; get coarse value move.w d3,d4 addi.w #$200,d4 ; set lower boundary subi.w #$80,d3 ; set upper boundary bpl.s + moveq #0,d3 ; no negative values allowed

+ lea (ChkLoadObj).l,a6 ; set object loading routine

ObjMan_Main_Cont: move.w #$FFF,d5 ; this will be used later when we load objects move.w (Camera_X_pos).w,d6 andi.w #$FF80,d6 cmp.w (Camera_X_pos_last).w,d6 ; is the X range the same as last time? beq.w ObjectsManager_SameXRange ; if yes, branch bge.s ObjectsManager_GoingForward ; if new pos is greater than old pos, branch

; if the player is moving back move.w d6,(Camera_X_pos_last).w ; remember current position for next time

movea.l (Obj_load_addr_left).w,a0 ; get current object going left movea.w (Obj_respawn_index_left).w,a3 ; and its respawn table index

subi.w #$80,d6 ; look one chunk to the left bcs.s ObjMan_GoingBack_Part2 ; branch, if camera position would be behind level's left boundary

jsr (SingleObjLoad).l ; find an empty object slot bne.s ObjMan_GoingBack_Part2 ; branch, if there are none - ; load all objects left of the screen that are now in range cmp.w -6(a0),d6 ; is the previous object's X pos less than d6? bge.s ObjMan_GoingBack_Part2 ; if it is, branch subq.w #6,a0 ; get object's address subq.w #1,a3 ; and respawn table index jsr (a6) ; load object bne.s + ; branch, if SST is full subq.w #6,a0 bra.s - ; continue with previous object

---------------------------------------------------------------------------

+ ; undo a few things, if the object couldn't load addq.w #6,a0 ; go back to last object addq.w #1,a3 ; since we didn't load the object, undo last change

ObjMan_GoingBack_Part2: move.l a0,(Obj_load_addr_left).w ; remember current object going left move.w a3,(Obj_respawn_index_left).w ; and its respawn table index movea.l (Obj_load_addr_right).w,a0 ; get next object going right movea.w (Obj_respawn_index_right).w,a3 ; and its respawn table index addi.w #$300,d6 ; look two chunks beyond the right edge of the screen

- ; subtract number of objects that have been moved out of range (from the right side) cmp.w -6(a0),d6 ; is the previous object's X pos less than d6? bgt.s + ; if it is, branch subq.w #6,a0 ; get object's address subq.w #1,a3 ; and respawn table index bra.s - ; continue with previous object

---------------------------------------------------------------------------

+ move.l a0,(Obj_load_addr_right).w ; remember next object going right move.w a3,(Obj_respawn_index_right).w ; and its respawn table index bra.s ObjectsManager_SameXRange

---------------------------------------------------------------------------

ObjectsManager_GoingForward: move.w d6,(Camera_X_pos_last).w

movea.l (Obj_load_addr_right).w,a0 ; get next object from the right movea.w (Obj_respawn_index_right).w,a3 ; and its respawn table index addi.w #$280,d6 ; look two chunks forward jsr (SingleObjLoad).l ; find an empty object slot bne.s ObjMan_GoingForward_Part2 ; branch, if there are none

- ; load all objects right of the screen that are now in range cmp.w (a0),d6 ; is object's x position >= d6? bls.s ObjMan_GoingForward_Part2 ; if yes, branch jsr (a6) ; load object (and get address of next object) addq.w #1,a3 ; respawn index of next object to the right beq.s - ; continue loading objects, if the SST isn't full

ObjMan_GoingForward_Part2: move.l a0,(Obj_load_addr_right).w ; remember next object from the right move.w a3,(Obj_respawn_index_right).w ; and its respawn table index movea.l (Obj_load_addr_left).w,a0 ; get current object from the left movea.w (Obj_respawn_index_left).w,a3 ; and its respawn table index subi.w #$300,d6 ; look one chunk behind the left edge of the screen bcs.s ObjMan_GoingForward_End ; branch, if camera position would be behind level's left boundary

- ; subtract number of objects that have been moved out of range (from the left) cmp.w (a0),d6 ; is object's x position >= d6? bls.s ObjMan_GoingForward_End ; if yes, branch addq.w #6,a0 ; next object addq.w #1,a3 ; respawn index of next object to the left bra.s - ; continue with next object

---------------------------------------------------------------------------

ObjMan_GoingForward_End: move.l a0,(Obj_load_addr_left).w ; remember current object from the left move.w a3,(Obj_respawn_index_left).w ; and its respawn table index

ObjectsManager_SameXRange: move.w (Camera_Y_pos).w,d6 andi.w #$FF80,d6 move.w d6,d3 cmp.w (Camera_Y_pos_last).w,d6 ; is the y range the same as last time? beq.w ObjectsManager_SameYRange ; if yes, branch bge.s ObjectsManager_GoingDown ; if the player is moving down

; if the player is moving up tst.w (Camera_Min_Y_pos).w ; does the level y-wrap? bpl.s ObjMan_GoingUp_NoYWrap ; if not, branch tst.w d6 bne.s ObjMan_GoingUp_YWrap cmpi.w #$80,(Camera_Y_pos_last).w bne.s ObjMan_GoingDown_YWrap

ObjMan_GoingUp_YWrap: subi.w #$80,d3 ; look one chunk up bpl.s ObjectsManager_YCheck ; go to y check, if camera y position >= $80 andi.w #$7FF,d3 ; else, wrap value bra.s ObjectsManager_YCheck

---------------------------------------------------------------------------

ObjMan_GoingUp_NoYWrap: subi.w #$80,d3 ; look one chunk up bmi.w ObjectsManager_SameYRange ; don't do anything if camera y position is < $80 bra.s ObjectsManager_YCheck

---------------------------------------------------------------------------

ObjectsManager_GoingDown: tst.w (Camera_Min_Y_pos).w ; does the level y-wrap? bpl.s ObjMan_GoingDown_NoYWrap ; if not, branch tst.w (Camera_Y_pos_last).w bne.s ObjMan_GoingDown_YWrap cmpi.w #$80,d6 bne.s ObjMan_GoingUp_YWrap

ObjMan_GoingDown_YWrap: addi.w #$180,d3 ; look one chunk down cmpi.w #$7FF,d3 bcs.s ObjectsManager_YCheck ; go to check, if camera y position < $7FF andi.w #$7FF,d3 ; else, wrap value bra.s ObjectsManager_YCheck

---------------------------------------------------------------------------

ObjMan_GoingDown_NoYWrap: addi.w #$180,d3 ; look one chunk down cmpi.w #$7FF,d3 bhi.s ObjectsManager_SameYRange ; don't do anything, if camera is too close to bottom

ObjectsManager_YCheck: jsr (SingleObjLoad).l ; get an empty object slot bne.s ObjectsManager_SameYRange ; branch, if there are none move.w d3,d4 addi.w #$80,d4 move.w #$FFF,d5 ; this will be used later when we load objects movea.l (Obj_load_addr_left).w,a0 ; get next object going left movea.w (Obj_respawn_index_left).w,a3 ; and its respawn table index move.l (Obj_load_addr_right).w,d7 ; get next object going right sub.l a0,d7 ; d7 = number of objects between the left and right boundaries * 6 beq.s ObjectsManager_SameYRange ; branch if there are no objects inbetween addq.w #2,a0 ; align to object's y position

- ; check, if current object needs to be loaded tst.b (a3) ; is object already loaded? bmi.s + ; if yes, branch move.w (a0),d1 and.w d5,d1 ; get object's y position cmp.w d3,d1 bcs.s + ; branch, if object is out of range from the top cmp.w d4,d1 bhi.s + ; branch, if object is out of range from the bottom bset #7,(a3) ; mark object as loaded ; load object move.w -2(a0),x_pos(a1) move.w (a0),d1 move.w d1,d2 and.w d5,d1 ; get object's y position move.w d1,y_pos(a1) rol.w #3,d2 andi.w #3,d2 ; get object's render flags and status move.b d2,render_flags(a1) move.b d2,status(a1) move.b 2(a0),id(a1) move.b 3(a0),subtype(a1) move.w a3,respawn_index(a1) jsr (SingleObjLoad).l ; find new object slot bne.s ObjectsManager_SameYRange ; brach, if there are none left + addq.w #6,a0 ; address of next object addq.w #1,a3 ; and its respawn index subq.w #6,d7 ; subtract from size of remaining objects bne.s - ; branch, if there are more

ObjectsManager_SameYRange: move.w d6,(Camera_Y_pos_last).w rts

===========================================================================
---------------------------------------------------------------------------
Subroutines to check if an object needs to be loaded,
with and without y-wrapping enabled.
input variables
d3 = upper boundary to load object
d4 = lower boundary to load object
d5 = #$FFF, used to filter out object's y position
a0 = address in object placement list
a1 = object
a3 = address in object respawn table
writes
d1, d2, d7
---------------------------------------------------------------------------

ChkLoadObj_YWrap: tst.b (a3) ; is object already loaded? bpl.s + ; if not, branch addq.w #6,a0 ; address of next object moveq #0,d1 ; let the objects manager know that it can keep going rts

---------------------------------------------------------------------------

+ move.w (a0)+,d7 ; x_pos move.w (a0)+,d1 ; there are three things stored in this word move.w d1,d2 ; does this object skip y-Checks? bmi.s + ; if yes, branch and.w d5,d1 ; y_pos cmp.w d3,d1 bcc.s LoadObj_YWrap cmp.w d4,d1 bls.s LoadObj_YWrap addq.w #2,a0 ; address of next object moveq #0,d1 ; let the objects manager know that it can keep going rts

---------------------------------------------------------------------------

+ and.w d5,d1 ; y_pos

LoadObj_YWrap: bset #7,(a3) ; mark object as loaded move.w d7,x_pos(a1) move.w d1,y_pos(a1) rol.w #3,d2 ; adjust bits andi.w #3,d2 ; get render flags and status move.b d2,render_flags(a1) move.b d2,status(a1) _move.b (a0)+,id(a1) ; load obj move.b (a0)+,subtype(a1) move.w a3,respawn_index(a1) bra.s SingleObjLoad ; find new object slot

loc_17F36

ChkLoadObj: tst.b (a3) ; is object already loaded? bpl.s + ; if not, branch addq.w #6,a0 ; address of next object moveq #0,d1 ; let the objects manager know that it can keep going rts

---------------------------------------------------------------------------

+ move.w (a0)+,d7 ; x_pos move.w (a0)+,d1 ; there are three things stored in this word move.w d1,d2 ; does this object skip y-Checks? ;*6 bmi.s ++ ; if yes, branch and.w d5,d1 ; y_pos cmp.w d3,d1 bcs.s + ; branch, if object is out of range from the top cmp.w d4,d1 bls.s LoadObj ; branch, if object is in range from the bottom + addq.w #2,a0 ; address of next object moveq #0,d1 rts

---------------------------------------------------------------------------

+ and.w d5,d1 ; y_pos

LoadObj: bset #7,(a3) ; mark object as loaded move.w d7,x_pos(a1) move.w d1,y_pos(a1) rol.w #3,d2 ; adjust bits andi.w #3,d2 ; get render flags and status move.b d2,render_flags(a1) move.b d2,status(a1) _move.b (a0)+,id(a1) ; load obj move.b (a0)+,subtype(a1) move.w a3,respawn_index(a1) ; continue straight to SingleObjLoad

End of function ChkLoadObj
===========================================================================

</syntaxhighlight>

Note: This will disable two player mode's objects manager.

Part 2: Necessary changes

And now a list of all changes needed to be applied to a clean disassembly as of SVN revision 125:

Step 1

The respawn table needs to be extended. In Sonic 2 there are two zones that use more than 255 objects and I'm sure most hackers would like to be relatively free of such a limitation as well. S3k uses $300 bytes for its respawn table, allowing for a total of 768 objects per level. The problem is finding free space. Porting s3k's rings manager frees some space, I believe and there's a tutorial for that. Alternatively, one could use the RAM area used by s1's sound driver. Achtung! This is the part that has been causing people some trouble. If you are using an older disassembly or just don't feel like screwing around with the SST, an alternative solution is below.

Step 2

The SST entry for respawn_index needs to be made a word. This can (and will, if not relocated) cause conflicts with other overlapping SST variables. Either a conflict free allocation must be found or the SST must be extended. For the latter, adding four bytes is easier than just adding two (trust me). If this is done the amount of objects in Dynamic_Object_RAM needs to be reduced. Doing just this can cause some unnamed RAM addresses to become shifted and overlap with other named addresses. Adding a few empty bytes after Object_RAM_End can help avoid this. To find out exactly how many bytes need to be added, compiling the ROM now would give an error message stating the RAM definitions are too long by a certain number of bytes. Reduce the amount of objects in dynamic object RAM so that the number of bytes used is less than what the error message says (but be sure to use even numbers). Any number of bytes left unused need to be defined (ds.b) after Object_RAM_End. If this is done correctly, all other RAM variables should be in the right place. Additionally, all references to respawn_index need to be made word length (so all ".b"s need to be made ".w"s).

Old disassemblies: the addresses for Dynamic_Object_RAM and Dynamic_Object_RAM_End might not exist, yet. Dynamic_Object_RAM = $B000 + object_size * 16 (object_size = next_object). Dynamic_Object_RAM_End = Dynamic_Object_RAM + ($28 + $48) * object_size. Download the latest disassembly, check where these are used and adapt your code accordingly. we do this so the game knows how many objects are in the dynamic object Ram are, since we changed it.

Alternative solution: Based on a suggestion by qiuu, an other way to get by is find an additional $70 words in RAM and use that as a respawn index table. Whenever a respawn index is needed, the address in the table can be calculated based on the object's address in the object table. Here's what you need to do: -Add a Ram address Object_Respawn_Indices somewhere and make it $70 words long. -Change the code after the label LoadObj to this:

<asm> LoadObj: bset #7,(a3) ; mark object as loaded move.w d7,x_pos(a1) move.w d1,y_pos(a1) rol.w #3,d2 ; adjust bits andi.w #3,d2 ; get render flags and status move.b d2,render_flags(a1) move.b d2,status(a1) _move.b (a0)+,id(a1) ; load obj move.b (a0)+,subtype(a1)

move.w a3,respawn_index(a1)

move.w a1,d2 subi.w #Object_RAM,d2 lsr.w #5,d2 andi.w #$7F,d2 lea (Object_Respawn_Indices).w,a1 adda.w d2,a1 move.w a3,(a1) ; continue straight to SingleObjLoad </syntaxhighlight>

-Apply the same changes to ObjectsManager_YCheck and LoadObj_YWrap, also. -Replace anything that looks like this:

<asm> lea (Object_Respawn_Table).w,a2 ; get respawn table's address moveq #0,d0 move.b respawn_index(a0),d0 ; get object's respawn index beq.s + ; if it's zero, don't remember object bclr #7,2(a2,d0.w) ; clear respawn table entry so object can be loaded again + </syntaxhighlight>

with this: <asm> move.w a0,d0 ; get object's address subi.w #Object_RAM,d0 ; get object's index * $40 lsr.w #5,d0 ; get object's index * 2 andi.w #$7F,d0 lea (Object_Respawn_Indices).w,a1 adda.w d0,a1 ; calculate address in table move.w (a1),d0 ; get address in respawn table beq.s + ; if it's zero, don't remember object movea.w d0,a2 ; load address into a2 bclr #7,(a2) ; clear respawn table entry, so object can be loaded again + </syntaxhighlight>

Be careful with which registers you use. Sometimes you can't use exactly these. Also, sometimes the beq is missing. In such a case, the bclr will be different, too. Search for "respawn_index". You can skip step 3, since the code above replaces what we would have done there. You should however still define the markObj_gone macro as follows: <asm> markObj_gone macro move.w a0,d0 ; get object's address subi.w #Object_RAM,d0 ; get object's index * $40 lsr.w #5,d0 ; get object's index * 2 andi.w #$7F,d0 lea (Object_Respawn_Indices).w,a1 adda.w d0,a1 ; calculate address in table move.w (a1),d0 ; get address in table beq.s mdebugObj ; if it's zero, don't remember object movea.w d0,a2 ; load address into a2 bclr #7,(a2) ; clear respawn table entry, so object can be loaded again mdebugObj

   endm

</syntaxhighlight>

That should do the trick. And now to the rest of the guide. Remember, skip step 3.

Step 3

Since respawn_index now contains the address of the object's respawn table entry instead of its index, the address calculation done by some objects is unnecessary. For comparison's sake: Taken from MarkObjGone: <asm> lea (Object_Respawn_Table).w,a2 ; get respawn table's address moveq #0,d0 move.b respawn_index(a0),d0 ; get object's respawn index beq.s + ; if it's zero, don't remember object bclr #7,2(a2,d0.w) ; clear respawn table entry so object can be loaded again + </syntaxhighlight>

Should now be: <asm> move.w respawn_index(a0),d0 ; get address in respawn table beq.s + ; if it's zero, don't remember object movea.w d0,a2 ; load address into a2 bclr #7,(a2) ; clear respawn table entry, so object can be loaded again + </syntaxhighlight>

This code clears an object's respawn table entry. Refer to the wiki to see how object respawning works. This piece of code will be used quite frequently, so it might be preferable to write a short macro for this. Search for "Object_Respawn_Table" to find all these locations. Macro: <asm> markObj_gone macro move.w respawn_index(a0),d0 ; get address in respawn table beq.s mdebugObj ; if it's zero, object was placed in debug mode movea.w d0,a2 ; load address into a2 bclr #7,(a2) ; clear respawn entry, so object can be loaded again mdebugObj

   endm

</syntaxhighlight>

Step 4

Any object that does not call any version of MarkObjGone somewhere at the end of its code handles off screen deletion on its own and needs to be modified in a similar way as above. These are mostly objects that did not get a respawn table entry in s2. The code is very similar to MarkObjGone, search for "Camera_X_pos_coarse". Clearing bit 7 of the object's respawn table entry needs to be done somewhere before object deletion. For comparison's sake: Before: <asm> tst.w (Two_player_mode).w bne.s + ; rts move.w x_pos(a0),d0 ; get object's x position andi.w #$FF80,d0 ; get chunk object is in sub.w (Camera_X_pos_coarse).w,d0 ; compare to camera position cmpi.w #$280,d0 ; is object out of range? bhi.w DeleteObject ; if yes, delete object + ; else, do nothing rts </syntaxhighlight>

After: <asm> tst.w (Two_player_mode).w bne.s + ; rts move.w x_pos(a0),d0 ; get object's x position andi.w #$FF80,d0 ; get chunk object is in sub.w (Camera_X_pos_coarse).w,d0 ; compare to camera position cmpi.w #$280,d0 ; is object out of range? bls.w + ; if not, branch markObj_gone ; else, clear respawn table entry bra.w DeleteObject ; and delete object + rts </syntaxhighlight>

Note: most objects use register a2 for the respawn address, but there is at least one instance where a3 is used, instead. The markObj_gone macro shouldn't be used there. Note also: Collapsing platforms will stay destroyed unless they are modified to clear their entry in the respawn table upon destruction. To those using the alternate solution to step 2: You need to get the respawn address from the respawn index table similar to how it was done before. It shoud be clear how to do this. I can post some example code, if the demand exists.

Step 5

Since it is now assumed respawn_index is word length, the RAM addresses Obj_respawn_index_right and Obj_respawn_index_left need to be extended to word length, as well. The two player mode variables can be overwritten for this. Note: Older disassemblies will have different names: Obj_load_addr_0 and Obj_load_addr_1 are Obj_load_addr_right and Obj_load_addr_left, respectively. Obj_load_addr_2 and Obj_load_addr_3 are the 2P equivalents.

Step 6

A few additional RAM addresses are needed: Camera_Y_pos_coarse and Camera_Y_pos_last. Both word length. If the SST was expanded, there should be some free space after Object_RAM_End. Otherwise, there should still be a few two player mode specific variables no longer used by the objects manager.

Step 7

The high bit in the 2nd word of an object's definition in the object layout no longer indicates that the object should get an entry in the object respawn table. Instead, it now means the object should skip the y-check when being loaded (in other words, it will behave like in s2). This should be used for tall objects, like the elevators in CNZ. Any existing object layouts should be adjusted to reflect this. Btw, it would be nice if someone could write a small program to do this.

Note: The following points only apply to those who decided to expand the SST.

Step 8

Expanding the SST causes Sonic and Tails' interaction with some objects to break, because it is still assumed an object is $40 bytes long, causing an address calculation to go wrong. This needs to be fixed wherever Sonic or Tails's interact is read or written. Relocating interact to $42 and resizing it to a word is the easiest fix. For comparison: In s2 with object size = $40: <asm> move.b interact(a0),d0 lsl.w #6,d0 lea (MainCharacter).w,a1 ; a1=character lea (a1,d0.w),a1 ; a1=object </syntaxhighlight>

For object size != $40: <asm> movea.w interact(a0),a1 ; a1=object </syntaxhighlight>

Note: Most of the time "Object_RAM" is used, instead of "MainCharacter". Both are the same address. Search for both to find all instances of this or similar code. Note: A few objects write something back to interact. Here it should suffice to comment out the address calculation (recognizable by shifting register dx right instead of left. It's basically the above code backwards). Note: Changing the SST can cause problems with the special stages, as they use different SST variables but still have the same names in the disasm. The easiest solution is to rename the old location of any SST value that was moved and replace all instances in the special stage code.

Step 9

SingleObjLoad2 needs to be modified somewhat. Originally it calculated the difference between the current object's address and the end of Dynamic_Object_RAM, then divided the result by $40 using a shift operation to get how many object slots are left after a0. Assuming the new SST has object_size = $44, division through shifting is impossible, but an actual division is also undesirable. S3k uses a table that maps the results of the division by $40 to the result of a division by $4A. Here's what it needs to be with object_size = $44: <asm> SingleObjLoad2: movea.l a0,a1 move.w #Tails_Tails,d0 ; $D000 sub.w a0,d0 ; subtract current object location lsr.w #6,d0 ; divide by $40 move.b Find_First_Sprite_Table(pc,d0.w),d0 bmi.s +

- lea next_object(a1),a1 ; load obj address ; goto next object RAM slot tst.b id(a1) ; is object RAM slot empty? dbeq d0,- ; if yes, branch +

return_18014: rts

===========================================================================

Find_First_Sprite_Table: ; map n*64/64 to n*68/64 dc.b $FF, $0, $1, $2, $3, $4, $5, $6, $7, $8, $9, $A, $b dc.b $C, $D, $E, $F, $F, $10, $11, $12, $13, $14, $15, $16, $17 dc.b $18, $19, $1A, $1B, $1C, $1D, $1E, $1F, $1F, $20, $21, $22, $23 dc.b $24, $25, $26, $27, $28, $29, $2A, $2B, $2C, $2D, $2E, $2F, $2f dc.b $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $3A, $3B, $3c dc.b $3D, $3E, $3F, $3F, $40, $41, $42, $43, $44, $45, $46, $47, $48 dc.b $49, $4A, $4B, $4C, $4D, $4E, $4F, $4F, $50, $51, $52, $53, $54 dc.b $55, $56, $57, $58, $59, $5A, $5B, $5C, $5D, $5E, $5F, $5F, $60 dc.b $61, $62, $63, $64, $65

===========================================================================

</syntaxhighlight>

The table was generated by a small python program I had just wrote. It creates a formatted, copy-and-paste-able table for your convenience and you just need to change the constants at the beginning to adapt it to any size. CODE ob2 = 64 # object size in s2 ob3 = 68 # new object size obn = 102 # objects in dynamic object ram obr = obn * ob3 # size of dynamic object ram

I = obr n = 0 while (I > 0):

 I = I - ob2
 if (n % 13 == 0):        # does assembler directive need to be printed?
   print "    dc.b    ",
 if (n % 13 != 12 and I > 0):    # print with comma?
   print (' $'+hex(((obr - I)/ob3 - 1)%256)[2:].upper()+',')[-4:],
 else:                # else print without
   print (' $'+hex(((obr - I)/ob3 - 1)%256)[2:])[-3:],
 n = n + 1

I think this program is correct, but I'm not entirely sure my math is right.

Done!

Note there are some minor issues: Even though everything works, one of Super Sonic's transformation frames has garbled tiles in it. I don't know what exactly causes this, but if I add a certain amount of padding in front of Sonic's tile data the problem gets fixed. It's kind of hit and miss right now, so I'd appreciate if someone could provide a definitive solution that works every time. Btw, I can reproduce this in an unmodified Sonic 2 by shifting Sonic's tile data slightly.

Other than that, I can't think of anything else that needs saying. Enjoy!

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