Difference between revisions of "Port S3K Object Manager into Sonic 2"
From Sonic Retro
Brainulator (talk | contribs) m (→Issues: cleanup) |
|||
(2 intermediate revisions by 2 users not shown) | |||
Line 705: | Line 705: | ||
'''Done!''' | '''Done!''' | ||
− | + | ===Issues=== | |
'''Note there are some minor issues:''' | '''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. | 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. | ||
Line 712: | Line 712: | ||
{{S2Howtos}} | {{S2Howtos}} | ||
− |
Latest revision as of 22:27, 27 March 2023
(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.
Contents
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!
Issues
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!