Actions

SCHG How-to

Port S3K Object Manager into Sonic 2

From Sonic Retro

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

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

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:

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

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

	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
+

with this:

	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
+

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:

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

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:

	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
+

Should now be:

	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
+

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:

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

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:

	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

After:

	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

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:

	move.b	interact(a0),d0
	lsl.w	#6,d0
	lea	(MainCharacter).w,a1 ; a1=character
	lea	(a1,d0.w),a1 ; a1=object

For object size != $40:

	movea.w	interact(a0),a1 ; a1=object

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:

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

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