don't click here

Add Chemical Plant Zone to 2P VS Mode

Discussion in 'Engineering & Reverse Engineering' started by E-122-Psi, Nov 19, 2024.

  1. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    It's time for another Sonic 2 VS mode tutorial and it's another level this time. :D

    I'm afraid after Hill Top the level conversions are gonna require a LOT more manual work since none of the others are remotely optimised to work in split screen (a LOT of recoding of the game itself was required to make them run in Sonic 2 VS). However I have at least managed to get the one working that most people will want to race in, Chemical Plant Zone.

    [​IMG]

    Download sample ROM below.

    (NOTE: This tutorial is for the Xenowhirl edit. Most of this is likely compatible with the GitHub version but will need a bit of relabelling.)

    Like Hill Top, I'm going to do this in sequences so I get this right one by one. This one is more finicky so there might be longer delays between posts.

    Step 1: Making it available in the VS menu:
    First we need to go to MenuScreen and change what one of the icons in the 2P menu directs to. This time we'll edit Mystic Cave Zone.

    Go to the directs in word_8E52 and change the second entry to D (Chemical Plant's placement in the game's code):

    Code (Text):
    1. word_8E52:
    2.     dc.w    0
    3.     dc.w  $D00    ; 1
    4.     dc.w  $C00    ; 2
    5.     dc.w $FFFF    ; 3
    Now we need to edit the icon to match it. The data for the level image is in off_8F7E.

    Again to exemplify we'll edit the Mystic Cave icon, so it will be the second three entries, editing the art and palette.

    Code (Text):
    1. off_8F7E:
    2.     dc.l byte_874A
    3.     dc.l byte_878C
    4.     dc.w $4104
    5.     dc.w 3
    6.     dc.w $FF
    7.     dc.w $330
    8.     dc.l byte_8757
    9.     dc.l byte_878C
    10.     dc.w $412C
    11.     dc.w 3
    12.     dc.w $7FF
    13.     dc.w $3D8
    14.     dc.l byte_8764
    15.     dc.l byte_878C
    16.     dc.w $4784
    17.     dc.w 3
    18.     dc.w $6FF
    19.     dc.w $3C0
    20.     dc.l byte_877F
    21.     dc.l byte_8792
    22.     dc.w $47AC
    23.     dc.w 3
    24.     dc.w $CFF
    25.     dc.w $450
    Finally go to word_8732 and then find the entries below reading the 2 player text, and edit accordingly.

    Remember when altering the header to keep it the same number characters listed before it.

    Code (Text):
    1.  
    2.     ; 2-player mode menu text
    3.    
    4. byte_874A:    dc.b  $B,"EMERALD HILL"
    5. byte_8757:    dc.b  $B,"CHEMICAL PLT"
    6. byte_8764:    dc.b  $B,"CASINO NIGHT"
    7. byte_8771:    dc.b  $C,"SPECIAL STAGE"
    8. byte_877F:    dc.b  $B,"   SPECIAL  "
    9. byte_878C:    dc.b   4,"ZONE "
    10. byte_8792:    dc.b   4,"STAGE"
    11. byte_8798:    dc.b   8,"GAME OVER"
    12.         dc.b   8,"TIME OVER"
    13. byte_87AC:    dc.b   6,"NO GAME"
    14. byte_87B4:    dc.b   3,"TIED"
    15. byte_87B9:    dc.b   2," 1P"
    16. byte_87BD:    dc.b   2," 2P"
    17. byte_87C1:    dc.b   3,"    "
    18.  
    19.     charset ; reset character set
    [​IMG]

    And viola, Chemical Plant Zone is now available in place of Mystic Cave.

    Step Two coming soon.
     

    Attached Files:

  2. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Step 2: Making Split screen compatible level art.

    And now we can enter the level and.....hooo boy. That's not a pretty sight.
    [​IMG]

    As stated, CPZ is not remotely optimised to display right in split screen mode.

    To expand the resolution, the split screen interlace loads tiles as 8 by 16 rather than 8 by 8, interchanging between odd and even scanlines of each tile frame by frame.

    This means it needs all pixel art to be loaded as tiles of 8 by 16 pixels, both in level art chunks, and sprites for objects and characters, otherwise it will mix tiles up and the art will often display as an incoherent mess.

    To fix this we need to essentially do the same as with the character and sprite fix tutorial, however we are limited to adjacent blocks in level editing like so.

    [​IMG]
    The top tile must be an even VRAM ID and the tile below must come right after. The tiles besides them don't need to follow right after but must follow the same rules themselves. If the block itself is flipped don't worry, the display can usually handle that, but it's default state must follow this rule.

    [​IMG]

    This also means that the start of the art file needs at least TWO blank tiles to make the empty opening block display properly, again following the same rule.

    Doing such is a tricky process, since this will obviously take up way more VRAM than the normal art file, meaning some sacrifices in detail will need to be made. I have left a ready made asset set below made by our talented Sock Team, albeit edited to work with the VRAM limitations of vanilla Sonic 2. It is still imperfect in areas, but most of the rough spots are down to layering issues, meaning artistic hackers might be able to use tricks to patch them up.

    [​IMG]

    Step Three coming soon.
     

    Attached Files:

    Last edited: Nov 26, 2024
    • Like Like x 3
    • Informative Informative x 1
    • List
  3. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Step Three: Making separate art load for VS mode:

    Well now we have art to load for VS mode but we aren't able to load it. For that we will once again go to Devon's tutorial for segregating level asset loads. If you already used this you can just make the neccessary edits for Chemical Plant Zone and skip this part.

    https://forums.sonicretro.org/index...ique-level-data-per-act-and-in-2p-mode.39478/

    First, rename the CPZ mapping files in both folder like you did the object files and then add back in the original unedited ones into their respective folders.

    Now add your newly renamed 2P mapping folders into the directory:

    Code (Text):
    1. ;-----------------------------------------------------------------------------------
    2. ; CPZ/DEZ 16x16 block mappings (Kosinski compression)
    3. BM16_CPZ:    BINCLUDE    "mappings/16x16/CPZ_DEZ.bin"
    4. ;-----------------------------------------------------------------------------------
    5. ; CPZ/DEZ 16x16 block mappings (Kosinski compression)
    6. BM16_CPZ_2P:    BINCLUDE    "mappings/16x16/CPZ_DEZ_2P_Maps.bin"
    7. ;-----------------------------------------------------------------------------------
    8. ; CPZ/DEZ main level patterns (Kosinski compression)
    9. ; ArtKoz_B6174:
    10. ArtKos_CPZ:    BINCLUDE    "art/kosinski/CPZ_DEZ.bin"
    11. ;-----------------------------------------------------------------------------------
    12. ; CPZ/DEZ main level patterns (Kosinski compression)
    13. ; ArtKoz_B6174:
    14. ArtKos_CPZ_2P:    BINCLUDE    "art/kosinski/CPZ_DEZ_2P_Art.bin"
    15. ;-----------------------------------------------------------------------------------
    16. ; CPZ/DEZ 128x128 block mappings (Kosinski compression)
    17. BM128_CPZ:    BINCLUDE    "mappings/128x128/CPZ_DEZ.bin"
    18. ;-----------------------------------------------------------------------------------
    Now we're gonna go to LevelArtPointers. What we're going to do is duplicate every level so that the rest of the asm will recognise 2 player counterparts as separate (this will handy if you want to edit any other two player levels). Edit your new 2 player directory for Chemical Plant to have the new 2P mappings like so:

    Code (Text):
    1. ; BEGIN SArt_Ptrs Art_Ptrs_Array[17]
    2. ; dword_42594: MainLoadBlocks: saArtPtrs:
    3. LevelArtPointers:
    4.     levartptrs   4,  5,  4, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   0 ; EHZ  ; EMERALD HILL ZONE
    5.     levartptrs   4,  5,  4, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   0 ; EHZ  ; EMERALD HILL ZONE (2 PLAYER)
    6.     levartptrs   6,  7,  5, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   1 ; LEV1 ; LEVEL 1 (UNUSED)
    7.     levartptrs   6,  7,  5, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   1 ; LEV1 ; LEVEL 1 (UNUSED) (2 PLAYER)
    8.     levartptrs   8,  9,  6, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   2 ; LEV2 ; LEVEL 2 (UNUSED)
    9.     levartptrs   8,  9,  6, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   2 ; LEV2 ; LEVEL 2 (UNUSED) (2 PLAYER)
    10.     levartptrs  $A, $B,  7, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   3 ; LEV3 ; LEVEL 3 (UNUSED)
    11.     levartptrs  $A, $B,  7, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   3 ; LEV3 ; LEVEL 3 (UNUSED) (2 PLAYER)
    12.     levartptrs  $C, $D,  8, ArtKos_MTZ, BM16_MTZ, BM128_MTZ ;   4 ; MTZ  ; METROPOLIS ZONE ACTS 1 & 2
    13.     levartptrs  $C, $D,  8, ArtKos_MTZ, BM16_MTZ, BM128_MTZ ;   4 ; MTZ  ; METROPOLIS ZONE ACTS 1 & 2 (2 PLAYER)
    14.     levartptrs  $C, $D,  8, ArtKos_MTZ, BM16_MTZ, BM128_MTZ ;   5 ; MTZ3 ; METROPOLIS ZONE ACT 3
    15.     levartptrs  $C, $D,  8, ArtKos_MTZ, BM16_MTZ, BM128_MTZ ;   5 ; MTZ3 ; METROPOLIS ZONE ACT 3 (2 PLAYER)
    16.     levartptrs $10,$11, $A, ArtKos_SCZ, BM16_WFZ, BM128_WFZ ;   6 ; WFZ  ; WING FORTRESS ZONE
    17.     levartptrs $10,$11, $A, ArtKos_SCZ, BM16_WFZ, BM128_WFZ ;   6 ; WFZ  ; WING FORTRESS ZONE (2 PLAYER)
    18.     levartptrs $12,$13, $B, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   7 ; HTZ  ; HILL TOP ZONE
    19.     levartptrs $12,$13, $B, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   7 ; HTZ  ; HILL TOP ZONE (2 PLAYER)
    20.     levartptrs $14,$15, $C,   BM16_OOZ,   BM16_OOZ,  BM16_OOZ ;   8 ; HPZ  ; HIDDEN PALACE ZONE (UNUSED)
    21.     levartptrs $14,$15, $C,   BM16_OOZ,   BM16_OOZ,  BM16_OOZ ;   8 ; HPZ  ; HIDDEN PALACE ZONE (UNUSED) (2 PLAYER)
    22.     levartptrs $16,$17, $D, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   9 ; LEV9 ; LEVEL 9 (UNUSED)
    23.     levartptrs $16,$17, $D, ArtKos_EHZ, BM16_EHZ, BM128_EHZ ;   9 ; LEV9 ; LEVEL 9 (UNUSED) (2 PLAYER)
    24.     levartptrs $18,$19, $E, ArtKos_OOZ, BM16_OOZ, BM128_OOZ ;  $A ; OOZ  ; OIL OCEAN ZONE
    25.     levartptrs $18,$19, $E, ArtKos_OOZ, BM16_OOZ, BM128_OOZ ;  $A ; OOZ  ; OIL OCEAN ZONE (2 PLAYER)
    26.     levartptrs $1A,$1B, $F, ArtKos_MCZ, BM16_MCZ, BM128_MCZ ;  $B ; MCZ  ; MYSTIC CAVE ZONE
    27.     levartptrs $1A,$1B, $F, ArtKos_MCZ, BM16_MCZ, BM128_MCZ ;  $B ; MCZ  ; MYSTIC CAVE ZONE (2 PLAYER)
    28.     levartptrs $1C,$1D,$10, ArtKos_CNZ, BM16_CNZ, BM128_CNZ ;  $C ; CNZ  ; CASINO NIGHT ZONE
    29.     levartptrs $1C,$1D,$10, ArtKos_CNZ, BM16_CNZ, BM128_CNZ ;  $C ; CNZ  ; CASINO NIGHT ZONE (2 PLAYER)
    30.     levartptrs $1E,$1F,$11, ArtKos_CPZ, BM16_CPZ, BM128_CPZ ;  $D ; CPZ  ; CHEMICAL PLANT ZONE
    31.     levartptrs $1E,$1F,$11, ArtKos_CPZ_2P, BM16_CPZ_2P, BM128_CPZ ;  $D ; CPZ  ; CHEMICAL PLANT ZONE (2 PLAYER)
    32.     levartptrs $20,$21,$12, ArtKos_CPZ, BM16_CPZ, BM128_CPZ ;  $E ; DEZ  ; DEATH EGG ZONE
    33.     levartptrs $20,$21,$12, ArtKos_CPZ, BM16_CPZ, BM128_CPZ ;  $E ; DEZ  ; DEATH EGG ZONE (2 PLAYER)
    34.     levartptrs $22,$23,$13, ArtKos_ARZ, BM16_ARZ, BM128_ARZ ;  $F ; ARZ  ; AQUATIC RUIN ZONE
    35.     levartptrs $22,$23,$13, ArtKos_ARZ, BM16_ARZ, BM128_ARZ ;  $F ; ARZ  ; AQUATIC RUIN ZONE (2 PLAYER)
    36.     levartptrs $24,$25,$14, ArtKos_SCZ, BM16_WFZ, BM128_WFZ ; $10 ; SCZ  ; SKY CHASE ZONE
    37.     levartptrs $24,$25,$14, ArtKos_SCZ, BM16_WFZ, BM128_WFZ ; $10 ; SCZ  ; SKY CHASE ZONE (2 PLAYER)
    Then replace the levartptrs macro above it like so:

    Code (Text):
    1.  
    2. ; declare some global variables to be used by the levartptrs macro
    3. cur_zone_id := 0
    4. cur_zone_str := "0"
    5. cur_zone_2p := 0
    6.  
    7. ; macro for declaring a "main level load block" (MLLB)
    8. levartptrs macro plc1,plc2,palette,art,map16x16,map128x128
    9.   ;  !org LevelArtPointers+zone_id_{cur_zone_str}*24+cur_zone_2p
    10.     dc.l (plc1<<24)|art
    11.     dc.l (plc2<<24)|map16x16
    12.     dc.l (palette<<24)|map128x128
    13. cur_zone_2p := cur_zone_2p+12
    14.     if cur_zone_2p>=24
    15. cur_zone_2p := 0
    16. cur_zone_id := cur_zone_id+1
    17. cur_zone_str := "\{cur_zone_id}"
    18.     endif
    19.     endm
    And have this added after the LevelArtPointers table:
    Code (Text):
    1. if (cur_zone_2p<>0)&&(MOMPASS=1)
    2.     message "Warning: Table LevelArtPointers's last entry does not have a 2P entry"
    3.     endif
    Then, go to Level and go to the comment that says "; multiply d0 by 12, the size of a level art load block". Insert this before "lea (LevelArtPointers).l,a2":

    Code (Text):
    1.     add.w    d0,d0
    2.     tst.w    (Two_player_mode).w
    3.     beq.s    .not_2p_mode
    4.     addi.w    #12,d0
    5.  
    6. .not_2p_mode:
    Then go to both sub_4E98 and loadZoneBlockMaps and add the same code before both instances of "lea (LevelArtPointers).l,a2".

    With that, you should now be able to set up level data pointers for 2P mode for any zone.

    Step Four coming soon.
     
    Last edited: Nov 19, 2024
  4. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Step 4: Making the level scroll properly in split screen:

    Now let's go back to the level. It should load normally in 1P, but double check to be safe. Go to VS mode and....
    [​IMG]

    Well a definite improvement, but still some big issues. The objects are not optimised and Tails' screen doesn't scroll right at all. This is because, unlike Hill Top Zone which conviniently had unused scroll code handy, CPZ is still working off a one player scroll routine. Luckily our talented friend MoDule managed to make a scroll routine for the level. First add this little edit to loc_C372 so VS mode can load a fuller amount of the background:

    Code (Text):
    1.  
    2. loc_C372:    ;InitCam_CPZ
    3.     lsr.w    #2,d0
    4.     move.w    d0,($FFFFEE0C).w
    5.     move.w    d0,($FFFFEE2C).w
    6.     lsr.w    #1,d1
    7.     move.w    d1,($FFFFEE10).w
    8.     move.w    d1,($FFFFEE30).w    ;added BG2 position for P2
    9.     lsr.w    #2,d1
    10.     move.w    d1,($FFFFEE08).w
    11.     move.w    d1,($FFFFEE2C).w    ;added BG1 position for P2
    12.     rts
    Next we're going to edit SwScrl_CPZ with the new VS routine, first put this little branch at the start:

    Code (Text):
    1. Swscrl_CPZ:
    2.     tst.w    (Two_player_mode).w
    3.     bne.w    SwScrl_CPZ_2P
    Then at the end of the routine just before Swscrl_DEZ, add this huge dollop of code:

    Code (Text):
    1. ; ===========================================================================
    2.  
    3. SwScrl_CPZ_2P:
    4.     ; Note: since the plane B nametable is shared between both players, we
    5.     ; can't do any tile loading, so where the single-player routine would
    6.     ; make a call to one of the Set___ScrollFlagsBG routines, we just do
    7.     ; the BG camera position calculations and nothing else.
    8.  
    9.     ; calculate index for ripple animation
    10.     move.b    ($FFFFFE0F).w,d1
    11.     andi.w    #7,d1
    12.     bne.s    .p1
    13.     subq.w    #1,($FFFFA800).w
    14.  
    15. .p1:
    16.     move.w    ($FFFFEEB0).w,d4
    17.     ext.l    d4
    18.     asl.l    #5,d4
    19.     ; SetHorizVertiScrollFlagsBG
    20.     move.l    ($FFFFEE08).w,d0
    21.     add.l    d4,d0
    22.     move.l    d0,($FFFFEE08).w
    23.  
    24.     move.w    ($FFFFEEB0).w,d4
    25.     ext.l    d4
    26.     asl.l    #7,d4
    27.     ; SetHorizScrollFlagsBG2
    28.     move.l    ($FFFFEE10).w,d0
    29.     add.l    d4,d0
    30.     move.l    d0,($FFFFEE10).w
    31.  
    32.     move.w    (Camera_Y_pos).w,d0
    33.     lsr.w    #3,d0
    34.     move.w    d0,($FFFFEE0C).w
    35.     move.w    d0,($FFFFEE14).w
    36.     move.w    d0,($FFFFF618).w
    37.     andi.l    #$FFFEFFFE,(Vscroll_Factor).w
    38.  
    39.     move.w    (Camera_X_pos).w,d0
    40.     move.w    ($FFFFEE0C).w,d3
    41.     move.w    ($FFFFEE08).w,d5
    42.     move.w    ($FFFFEE10).w,d6
    43.     lea    (Horiz_Scroll_Buf).w,a1
    44.     bsr.w    .getBlockIndex
    45.  
    46. .p2:
    47.     move.w    ($FFFFEEB8).w,d4
    48.     ext.l    d4
    49.     asl.l    #5,d4
    50.     ; SetHorizVertiScrollFlagsBG
    51.     move.l    ($FFFFEE28).w,d0
    52.     add.l    d4,d0
    53.     move.l    d0,($FFFFEE28).w
    54.  
    55.     move.w    ($FFFFEEB8).w,d4
    56.     ext.l    d4
    57.     asl.l    #7,d4
    58.     ; SetHorizScrollFlagsBG2
    59.     move.l    ($FFFFEE30).w,d0
    60.     add.l    d4,d0
    61.     move.l    d0,($FFFFEE30).w
    62.  
    63.     move.w    ($FFFFEE24).w,d0
    64.     lsr.w    #3,d0
    65.     move.w    d0,($FFFFEE2C).w
    66.     move.w    d0,($FFFFEE34).w
    67.     move.w    d0,($FFFFF620).w
    68.     subi.w    #224,($FFFFF620).w    ; show on lower screen
    69.     move.w    ($FFFFEE24).w,($FFFFF61E).w
    70.     subi.w    #224,($FFFFF61E).w    ; show on lower screen
    71.     andi.l    #$FFFEFFFE,($FFFFF61E).w
    72.  
    73.     move.w    ($FFFFEE20).w,d0
    74.     move.w    ($FFFFEE2C).w,d3
    75.     move.w    ($FFFFEE28).w,d5
    76.     move.w    ($FFFFEE30).w,d6
    77.     lea    (Horiz_Scroll_Buf+(112)*4).w,a1
    78. ; =============== S U B R O U T I N E =======================================
    79.  
    80. .getBlockIndex:
    81.     move.w    d3,d2
    82.     andi.w    #$3F0,d3
    83.     lsr.w    #4,d3            ; get the index of the first visible block
    84.     move.w    d3,d4            ; d4 = index of first visible block
    85.     move.w    #$F-1,d1        ; prepare to write $F*8 = 120 scroll values
    86.     neg.w    d0
    87.     swap    d0            ; d0 (high word) = FG scroll value
    88.     lsr.w    #1,d2            ; adjust to screen position
    89.     andi.w    #7,d2            ; d2 = number of first visible block's off-screen pixels
    90.     move.w    d5,d0
    91.     cmpi.b    #$12,d4            ; is this the rippling section?
    92.     beq.s    .doRippleFromScreenTop    ; if yes, branch
    93.     blo.s    .doBlockFromScreenTop    ; branch, if this is the upper part of the BG (skyscrapers)
    94.     move.w    d6,d0            ; user other camera for lower part of BG
    95.  
    96. .doBlockFromScreenTop:
    97.     neg.w    d0            ; d0 (low word) = BG scroll value
    98.     add.w    d2,d2            ; d2/2 = number of lines to skip in first visible block
    99.     jmp    .doBlock(pc,d2.w)
    100. ; ---------------------------------------------------------------------------
    101.  
    102. .blockLoop:
    103.     move.w    d5,d0
    104.     cmpi.b    #$12,d4            ; is this the rippling section?
    105.     beq.s    .doRippleFullBlock    ; if yes, branch
    106.     blo.s    .prepareDoBlock        ; branch, if this is the upper part of the BG (skyscrapers)
    107.     move.w    d6,d0            ; user other camera for lower part of BG
    108.  
    109. .prepareDoBlock:
    110.     neg.w    d0            ; d0 (low word) = BG scroll value
    111.  
    112. .doBlock:
    113.     ; Note: because we always write a full block after the top one, it's
    114.     ; possible to overshoot the end of the H-scroll table. This isn't
    115.     ; really a problem, because we have enough unused RAM after it that
    116.     ; we don't end up overwriting anything, but it's still more writes
    117.     ; than necessary.
    118.     rept 8
    119.     move.l    d0,(a1)+        ; set scroll value
    120.     endm
    121.     addq.b    #1,d4            ; d4 = next block index
    122.     dbf    d1,.blockLoop
    123.     rts
    124. ; ---------------------------------------------------------------------------
    125.  
    126. .doRippleFromScreenTop:
    127.     move.w    #8-1,d0            ; number of lines in a full block
    128.     sub.w    d2,d0            ; subtrack off-screen pixels
    129.     move.w    d0,d2            ; d2 = number of lines for top block
    130.     bra.s    .doRipple
    131. ; ---------------------------------------------------------------------------
    132.  
    133. .doRippleFullBlock:
    134.     move.w    #8-1,d2            ; do a full block
    135.  
    136. .doRipple
    137.     move.w    d5,d3
    138.     neg.w    d3
    139.  
    140.     ; get this line's ripple value
    141.     move.w    ($FFFFA800).w,d0
    142.     andi.w    #$1F,d0
    143.     lea    SwScrl_RippleData(pc),a2
    144.     lea    (a2,d0.w),a2
    145.  
    146. .rippleLoop:
    147.     move.b    (a2)+,d0        ; read ripple value into BG-scroll part of d0
    148.     ext.w    d0
    149.     add.w    d3,d0            ; add camera position
    150.     move.l    d0,(a1)+        ; set scroll value
    151.     dbf    d2,.rippleLoop
    152.  
    153.     addq.b    #1,d4            ; d4 = next block index
    154.     dbf    d1,.blockLoop
    155.     rts
    Then make one last edit to loc_E300:

    Code (Text):
    1. loc_E300:
    2.     lea    (VDP_control_port).l,a5
    3.     lea    (VDP_data_port).l,a6
    4.     lea    ($FFFFEE08).w,a3
    5.     lea    ($FFFF8080).w,a4
    6.     move.w    #$6000,d2
    7.     moveq    #0,d4
    8.     cmpi.b    #$C,(Current_Zone).w
    9.     beq.w    loc_E338
    10.     tst.w    (Two_player_mode).w
    11.     beq.w    loc_E336
    12.     cmpi.b    #$B,(Current_Zone).w
    13.     beq.w    loc_E396
    14.     cmpi.b    #$D,(Current_Zone).w    ;Fixes CPZ background to load in full for VS mode
    15.     beq.w    loc_E396
    Now we have to fix the level to skip forced sequences like the boss. Go to LevEvents_CPZ and make this edit to skip most of the routine for a default screen limit in VS mode:

    Code (Text):
    1. loc_F396:
    2.     tst.w    (Two_player_mode).w
    3.     bne.s    loc_F396_VSCPZ1
    4.     cmpi.w    #$2680,(Camera_X_pos).w
    5.     bcs.s    return_F3BA
    6.     move.w    (Camera_X_pos).w,(Camera_Min_X_pos).w
    7.     move.w    (Camera_X_pos).w,(Tails_Min_X_pos).w
    8.     move.w    #$450,(Camera_Max_Y_pos).w
    9.     move.w    #$450,(Tails_Max_Y_pos).w
    10.     addq.b    #2,(Dynamic_Resize_Routine).w
    11.  
    12. return_F3BA:
    13.     rts
    14. ; ===========================================================================
    15. loc_F396_VSCPZ1:
    16.     tst.b    (Current_Act).w
    17.     bne.s    loc_F396_VSCPZ2
    18.     move.w    #$2740,(Camera_Max_X_pos).w
    19.     move.w    #$2740,(Tails_Max_X_pos).w
    20.     rts
    21. ; ===========================================================================
    22. loc_F396_VSCPZ2:
    23.     move.w    #$2860,(Camera_Max_X_pos).w
    24.     move.w    #$2860,(Tails_Max_X_pos).w
    25.     rts
    With that you should have a much better looking CPZ on both screens, but the objects are still optimised and Act 2 should still default to one player mode.

    Step Five coming soon.
     
    Last edited: Nov 19, 2024
  5. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Step 5: Dealing with water in split screen:

    The reason Act 2 won't load in split screen state is because it cannot handle water, so is programmed to switch back to one player state in a level it recognises it. It is possible to make water run in VS mode but requires a TON of recoding thus far.

    Our friend Sonic Hachelle-Bee has made an impressive tutorial detailing how to make water work in split screen mode if you want the full Chemical Plant Zone experience in VS mode. For the sake of streamlining this tutorial and making some VRAM free however, we'll stick to remake rules and have water disabled for two player mode here:

    Code (Text):
    1.  
    2. Go to Level_ClrRam and add a branch for two player mode:
    3.  
    4. Level_ClrRam:
    5.     clearRAM Sprite_Table_Input,$400
    6.     clearRAM Object_RAM,$2400    ; clear object RAM
    7.     clearRAM $FFFFF628,$58
    8.     clearRAM Misc_Variables,$100
    9.     clearRAM $FFFFFE60,$50
    10.     clearRAM $FFFFE700,$100
    11.  
    12.     tst.w    (Two_player_mode).w
    13.     bne.w    +
    14.     cmpi.w    #$D01,(Current_ZoneAndAct).w    ; CPZ 2
    15.     beq.s    Level_InitWater
    16.     cmpi.b    #$F,(Current_Zone).w        ; ARZ
    17.     beq.s    Level_InitWater
    18.     cmpi.b    #8,(Current_Zone).w        ; HPZ
    19.     bne.s    +
    Then go to loc_4114 and edit this part here:

    Code (Text):
    1.  
    2. ; Level_ChkWater:
    3.     tst.b    (Water_flag).w    ; does level have water?
    4.     beq.s    +    ; if not, branch
    5.     move.b    #4,(Object_RAM+$380).w ; load Obj04 (water surface) at $FFFFB380
    6.     move.w    #$60,(Object_RAM+$380+x_pos).w ; set horizontal offset
    7.     move.b    #4,(Object_RAM+$3C0).w ; load Obj04 (water surface) at $FFFFB3C0
    8.     move.w    #$120,(Object_RAM+$3C0+x_pos).w ; set different horizontal offset
    9. +
    10.     cmpi.b    #$D,(Current_Zone).w    ; check if zone == CPZ
    11.     bne.s    +            ; branch if not
    12.     tst.w    (Two_player_mode).w
    13.     bne.s    +
    14.     move.b    #$7C,(Object_RAM+$340).w ; load Obj7C (CPZ pylon) at $FFFFB340
    15. +
    16.     cmpi.b    #$A,(Current_Zone).w    ; check if zone == OOZ
    17.     bne.s    Level_ClrHUD        ; branch if not
    18.     move.b    #7,(Object_RAM+$380).w ; load Obj07 (OOZ oil) at $FFFFB380
    19. ; Level_LoadObj: misnomer now
    While we're here we're gonna also disable the pylon that scrolls in the level's background to save more VRAM.

    Now Act Two should load fine but sans water and pylon.

    Just to note, some parts of the level might now be inaccessible due to objects like the lever springs requiring water physics to run properly. As such you might want to make some layout or routine edits accordingly.

    Step Six coming soon.
     
    Last edited: Dec 22, 2024
  6. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Step 6: Fixing the layout

    Now we're gonna have to refine the object layout for CPZ as certain things don't load right in split screen.

    First of all make backup copies of your CPZ object files for 1P mode.

    Fixing the tubes:

    The tubes are the biggest roadblock right now, once you enter one, you can get stuck and immobilised halfway through the run. This is because 2 player despawns the object's automation routine before the player leaves the tube. To fix this, go to the tubes and make sure their Long Distance and Remember State flags are on.

    [​IMG]

    The moving blocks:

    One of the biggest parasites for object management are the moving blocks since they are made up of tons of objects are often found in clusters, as well as requiring collision blocks to recognise the player being pushed into a wall. Since split screen can't load as many objects as one player, this can cause the manager to crash and stop loading any objects.

    To downplay this, MoDule has once again made a fix. First underneath MarkObjGone_P2's routine and before DeleteObject, add this new routine:

    Code (Text):
    1. ; ===========================================================================
    2. ; input: d0 = the object's x position
    3. ; loc_1640A:
    4. MarkObjGone2_2P:
    5.     tst.w    (Two_player_mode).w
    6.     beq.s    +
    7.     bra.w    DisplaySprite
    8. +
    9.     andi.w    #$FF80,d0
    10.     sub.w    (Camera_X_pos_coarse).w,d0
    11.     cmpi.w    #$80+320+$40+$80,d0
    12.     bhi.w    +
    13.     bra.w    DisplaySprite
    14. +
    15.     lea    (Object_Respawn_Table).w,a2
    16.     moveq    #0,d0
    17.     move.b    respawn_index(a0),d0
    18.     beq.s    +
    19.     bclr    #7,2(a2,d0.w)
    20. +
    21.     bra.w    DeleteObject
    Then go to loc_27E46 in the blocks' (obj6B's) routine and change the branch accordingly:

    Code (Text):
    1. loc_27E46:
    2.     move.w    objoff_34(a0),d0
    3.     jmp    (MarkObjGone2_2P).l ;_2P
    Now go into SonLVL or another editor and set the long distance flag on the moving blocks (note: only the moving ones, the rising staircase are separate objects).

    This should lessen the object load a fair bit, though you'll still likely need to ration things in areas, particularly these two parts.

    [​IMG]
    [​IMG]

    Keep toying with this until you have something that runs smoothly enough. Also remember to add a signpost at the end of Act 2, don't worry it won't load in one player mode.

    [​IMG]

    Once again, a rough cut attempt at a layout is ready made below if you wanna compare and refine.

    Step Seven coming soon.
     

    Attached Files:

  7. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Step 7: Making separate object files for 1p and 2p mode:

    Now we have edits for 2p mode but the problem is these edits are now consistent with 1p mode.

    First rename your object files as "CPZ_1_2P.bin" and "CPZ_2_2P.bin" respectively. Also add back in your original backup object files unchanged.

    Now we're gonna code the game to switch between the two sets of object files depending on mode. Luckily the game already does this for Casino Night so we can just follow suit. Go to loc_17AB8 and add this branch:

    Code (Text):
    1. loc_17AB8:
    2.     addq.b    #2,(Obj_placement_routine).w
    3.     move.w    (Current_ZoneAndAct).w,d0    ; If level == $0F (ARZ)...
    4.     ror.b    #1,d0        ; then this yields $87...
    5.     lsr.w    #6,d0        ; and this yields $0002.
    6.     lea    (Off_Objects).l,a0    ; Next, we load the first pointer in the object layout list pointer index,
    7.     movea.l    a0,a1        ; then copy it for quicker use later.
    8.     adda.w    (a0,d0.w),a0    ; (Point1 * 2) + $0002
    9.     tst.w    (Two_player_mode).w    ; skip if not in 2-player vs mode
    10.     beq.s    loc_17AF0
    11.     cmpi.b    #$C,(Current_Zone).w    ; skip if not Casino Night Zone
    12.     bne.s    loc_17AB8_CPZ
    13.     lea    (Objects_CNZ1_2P).l,a0    ; CNZ 1 2-player object layout
    14.     tst.b    (Current_Act).w        ; skip if not past act 1
    15.     beq.s    loc_17AF0
    16.     lea    (Objects_CNZ2_2P).l,a0    ; CNZ 2 2-player object layout
    17. loc_17AB8_CPZ:
    18.     cmpi.b    #$D,(Current_Zone).w    ; skip if not Chemical Plant Zone
    19.     bne.s    loc_17AF0
    20.     lea    (Objects_CPZ1_2P).l,a0    ; CPZ 1 2-player object layout
    21.     tst.b    (Current_Act).w        ; skip if not past act 1
    22.     beq.s    loc_17AF0
    23.     lea    (Objects_CPZ2_2P).l,a0    ; CPZ 2 2-player object layout
    Now add your 2P object files into the code:

    Code (Text):
    1.  ;---------------------------------------------------------------------------------------
    2. ; CNZ act 1 object layout for 2-player mode (various objects were deleted)
    3. ;---------------------------------------------------------------------------------------
    4. ; byte_1802A;
    5. Objects_CNZ1_2P:    BINCLUDE    "level/objects/CNZ_1_2P.bin"
    6. ;---------------------------------------------------------------------------------------
    7. ; CNZ act 2 object layout for 2-player mode (various objects were deleted)
    8. ;---------------------------------------------------------------------------------------
    9. ; byte_18492:
    10. Objects_CNZ2_2P:    BINCLUDE    "level/objects/CNZ_2_2P.bin"
    11. ;---------------------------------------------------------------------------------------
    12. ; CPZ act 1 object layout for 2-player mode (various objects were deleted)
    13. ;---------------------------------------------------------------------------------------
    14. ;
    15. Objects_CPZ1_2P:    BINCLUDE    "level/objects/CPZ_1_2P.bin"
    16. ;---------------------------------------------------------------------------------------
    17. ; CPZ act 2 object layout for 2-player mode (various objects were deleted)
    18. ;---------------------------------------------------------------------------------------
    19. ;
    20. Objects_CPZ2_2P:    BINCLUDE    "level/objects/CPZ_2_2P.bin"
    With this done, both 1p and 2p should load separate object layouts.

    Step Eight coming soon.
     
    Last edited: Nov 20, 2024
  8. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Step 8: Fixing the art:

    Now the really fun part, editing all the objects in the zone to display properly in split screen. Yay. :P

    Take the files in the download below and insert them into the respective folders in your disassembly. Then add them into your ASM next to their 1P entries (the ones without 'VS' can just replace the existing versions as they are simple tile streamlines that should work fine in 1P without much of any detail sacrifice).

    Code (Text):
    1. ;--------------------------------------------------------------------------------------
    2. ; Nemesis compressed art (48 blocks)
    3. ; Yellow flipping platforms and stuff CPZ    ; ArtNem_82864:
    4.     even
    5. ArtNem_CPZAnimatedBits:    BINCLUDE    "art/nemesis/Small yellow moving platform from CPZ.bin"
    6. ;--------------------------------------------------------------------------------------
    7. ; Nemesis compressed art (48 blocks)
    8. ; Yellow flipping platforms and stuff CPZ    ; ArtNem_82864:
    9.     even
    10. ArtNem_CPZAnimatedBits_VS:    BINCLUDE    "art/nemesis/Small yellow moving platform from CPZ_VS.bin"
    11. ;--------------------------------------------------------------------------------------
    Code (Text):
    1.  ;--------------------------------------------------------------------------------------
    2. ; Nemesis compressed art (32 blocks)
    3. ; Weird crawling badnik from CPZ    ; ArtNem_8B430:
    4.     even
    5. ArtNem_Spiny:    BINCLUDE    "art/nemesis/Weird crawling badnik from CPZ.bin"
    6. ;--------------------------------------------------------------------------------------
    7. ; Nemesis compressed art (32 blocks)
    8. ; Weird crawling badnik from CPZ    ; ArtNem_8B430:
    9.     even
    10. ArtNem_Spiny_VS:    BINCLUDE    "art/nemesis/Spiny from CPZ_VS.bin"
    11. ;--------------------------------------------------------------------------------------
    Code (Text):
    1.  ;--------------------------------------------------------------------------------------
    2. ; Nemesis compressed art (45 blocks)
    3. ; Spider badnik from CPZ     ArtNem_8B6B4:
    4.     even
    5. ArtNem_Grabber:    BINCLUDE    "art/nemesis/Spider badnik from CPZ.bin"
    6. ;--------------------------------------------------------------------------------------
    7. ; Nemesis compressed art (45 blocks)
    8. ; Spider badnik from CPZ     ArtNem_8B6B4:
    9.     even
    10. ArtNem_Grabber_VS:    BINCLUDE    "art/nemesis/Grabber from CPZ_VS.bin"
    11. ;--------------------------------------------------------------------------------------
    Code (Text):
    1.  ; -------------------------------------------------------------------------------
    2. ; sprite mappings
    3. ; -------------------------------------------------------------------------------
    4. Obj0B_MapUnc_201A0:    BINCLUDE "mappings/sprite/obj0B.bin"
    5. ; -------------------------------------------------------------------------------
    6. ; sprite mappings
    7. ; -------------------------------------------------------------------------------
    8. Obj0B_MapUnc_201A0_2P:    BINCLUDE "mappings/sprite/obj0B_VS.bin"
    Code (Text):
    1. ; ------------------------------------------------------------------------------
    2. ; sprite mappings
    3. ; ------------------------------------------------------------------------------
    4. ObjA5_ObjA6_Obj98_MapUnc_38CCA:    BINCLUDE "mappings/sprite/objA6.bin"
    5. ; ------------------------------------------------------------------------------
    6. ; sprite mappings
    7. ; ------------------------------------------------------------------------------
    8. ObjA5_ObjA6_Obj98_MapUnc_VS:    BINCLUDE "mappings/sprite/objA6_2P.bin"
    Code (Text):
    1. ; ----------------------------------------------------------------------------
    2. ; sprite mappings
    3. ; ----------------------------------------------------------------------------
    4. ObjA7_VS:    include "mappings/sprite/objA7_2P.asm"
    5. ; ----------------------------------------------------------------------------
    6. ; sprite mappings - objA7,objA8,objA9
    7. ; ----------------------------------------------------------------------------
    8. ObjA7_ObjA8_ObjA9_Obj98_MapUnc_3921A:
    9.     dc.w word_3923A - ObjA7_ObjA8_ObjA9_Obj98_MapUnc_3921A; 0
    10.     dc.w word_39254 - ObjA7_ObjA8_ObjA9_Obj98_MapUnc_3921A; 1
    11.     dc.w word_3926E - ObjA7_ObjA8_ObjA9_Obj98_MapUnc_3921A; 2
    12.     dc.w word_39278 - ObjA7_ObjA8_ObjA9_Obj98_MapUnc_3921A; 3
    13.     dc.w word_39282 - ObjA7_ObjA8_ObjA9_Obj98_MapUnc_3921A; 4
    14.     dc.w word_3928C - ObjA7_ObjA8_ObjA9_Obj98_MapUnc_3921A; 5
    15.     dc.w word_39296 - ObjA7_ObjA8_ObjA9_Obj98_MapUnc_3921A; 6
    Next we're gonna make a separate pattern load cue entry for all these fixes. Go to the end of your PLC list and add these two routines.

    Code (Text):
    1. ;---------------------------------------------------------------------------------------
    2. ; Pattern load queue
    3. ; CPZ Primary 2p
    4. ;---------------------------------------------------------------------------------------
    5. PLC_16_VS: plrlistheader
    6.     plreq $7300, ArtNem_ConstructionStripes
    7.     plreq $8580, ArtNem_CPZBooster
    8.     plreq $7400, ArtNem_CPZElevator
    9.     plreq $7600, ArtNem_CPZAnimatedBits_VS
    10.     plreq $8000, ArtNem_CPZTubeSpring
    11.     plreq $8300, ArtNem_CPZStairBlock
    12.     plreq $8600, ArtNem_CPZMetalBlock
    13. PLC_16_VS_End
    14. ;---------------------------------------------------------------------------------------
    15. ; Pattern load queue
    16. ; CPZ Secondary 2p
    17. ;---------------------------------------------------------------------------------------
    18. PLC_17_VS: plrlistheader
    19.     plreq $7A80, ArtNem_Grabber_VS
    20.     plreq $6E40, ArtNem_Spiny_VS
    21.     plreq $8680, ArtNem_Spikes
    22.     plreq $8780, ArtNem_CPZDroplet
    23.     plreq $8800, ArtNem_LeverSpring
    24.     plreq $8B80, ArtNem_VrtclSprng
    25.     plreq $8E00, ArtNem_HrzntlSprng
    26. PLC_17_VS_End
    Remember to add them to the end of ArtLoadCues above so they are recognised. If these are your first new PLC entries, they will be $43 and $44 respectively.

    Code (Text):
    1.     dc.w PLC_39 - ArtLoadCues    ; 65
    2.     dc.w PLC_3A - ArtLoadCues    ; 66
    3.     dc.w PLC_16_VS - ArtLoadCues    ; 67    ;$43
    4.     dc.w PLC_17_VS - ArtLoadCues    ; 68    ;$44
    Then add those new PLCs to your VS entry for CPZ in LevelArtPointers:

    Code (Text):
    1.     levartptrs $1E,$1F,$11, ArtKos_CPZ, BM16_CPZ, BM128_CPZ ;  $D ; CPZ  ; CHEMICAL PLANT ZONE
    2.     levartptrs $43,$44,$11, ArtKos_CPZ_2P, BM16_CPZ_2P, BM128_CPZ ;  $D ; CPZ  ; CHEMICAL PLANT ZONE (2 PLAYER)
    Now it comes down to loading the new mapping files for each object, which is a more tedious individual job:

    Tipping pipe section:

    This one is the easiest, just go to Obj0B's entry and add this branch in loc_200B0:

    Code (Text):
    1. loc_200B0:
    2.     addq.b    #2,routine(a0)
    3.     tst.w    (Two_player_mode).w
    4.     beq.w    loc_200B0_1P
    5.     move.l    #Obj0B_MapUnc_201A0_2P,mappings(a0)
    6.     bra.s    loc_200B0_2P
    7. loc_200B0_1P:
    8.     move.l    #Obj0B_MapUnc_201A0,mappings(a0)
    9. loc_200B0_2P:
    10.     move.w    #$E3B0,art_tile(a0)
    11.     bsr.w    JmpTo8_Adjust2PArtPointer
    12.     ori.b    #4,render_flags(a0)
    One-way Barrier door:

    Same for the doors, find the CPZ entry in obj2D:

    Code (Text):
    1.  loc_116E0:
    2.     cmpi.b    #$D,(Current_Zone).w
    3.     bne.s    loc_116F4
    4.     tst.w    (Two_player_mode).w
    5.     beq.w    loc_116E0_1P
    6.     move.w    #$2398,art_tile(a0)
    7.     move.b    #8,width_pixels(a0)
    8.     bra.s    loc_116F4
    9. loc_116E0_1P:
    10.     move.w    #$2394,art_tile(a0)
    11.     move.b    #8,width_pixels(a0)
    Badniks:

    Well these two fellas couldn't be more awkward if they tried. Grabber and Spiny irritatingly don't use proper mapping branches, but do it via subobjdata due to being made of multiple objects.

    First add these new entries at the end of the SubObjData_Index. Note the entry numbers are made based on being the first new entries you make. If not you will have to estimate according to any ones added before:

    Code (Text):
    1. .....
    2.     dc.w ObjC6_SubObjData - SubObjData_Index    ; $AA
    3.     dc.w ObjC8_SubObjData - SubObjData_Index    ; $AC
    4.     dc.w ObjA5_VS_SubObjData - SubObjData_Index    ; $AE
    5.     dc.w ObjA7_VS_SubObjData - SubObjData_Index    ; $B0
    6.     dc.w ObjA7_VS_SubObjData2 - SubObjData_Index    ; $B2
    7.     dc.w ObjA8_VS_SubObjData - SubObjData_Index    ; $B4
    8.     dc.w ObjA8_VS_SubObjData2 - SubObjData_Index    ; $B6
    9.     dc.w ObjA6_VS_SubObjData - SubObjData_Index    ; $B8
    Now we'll add these entries in each objects code:

    First go to the end of obj98 (Spiny's projectile) and add this at the end of the subobjdata entries:

    Code (Text):
    1. ....
    2. ; off_377B4:
    3. ObjAF_SubObjData:
    4.     dc.l ObjAF_Obj98_MapUnc_39E68
    5.     dc.w $2380
    6.     dc.b $84,5,4,$98
    7. ; off_377BE:
    8. ObjB8_SubObjData2:
    9.     dc.l ObjB8_Obj98_MapUnc_3BA46
    10.     dc.w $3AB
    11.     dc.b $84,3,4,$98
    12. ObjA6_VS_SubObjData:
    13.     dc.l ObjA5_ObjA6_Obj98_MapUnc_VS
    14.     dc.w $2372
    15.     dc.b $84,5,4,$98
    (NOTE: If you're using GitHub this will be slightly easier as the entries recognise the artfile, though Xenowhirl requires the VRAM location.)

    Next go to objA5 and 16 (Spiny's floor and wall variants) and edit all the branches for the subobjdata entries:

    Code (Text):
    1. loc_38AFE:
    2.     tst.w    (Two_player_mode).w
    3.     beq.w    loc_36F24_1P
    4.     move.b    #$AE,subtype(a0) ; <== Obj93_SubObjData2
    5. loc_36F24_1P:
    6.     bsr.w    LoadSubObject
    7.     move.w    #-$40,x_vel(a0)
    8.     move.w    #$80,objoff_2A(a0)
    9.     rts
    Code (Text):
    1. loc_38B9A:
    2.     tst.w    (Two_player_mode).w
    3.     beq.w    loc_38B9A_1P
    4.     move.b    #$AE,subtype(a0) ; <== Obj93_SubObjData2
    5. loc_38B9A_1P:
    6.     bsr.w    LoadSubObject
    7.     move.w    #-$40,y_vel(a0)
    8.     move.w    #$80,objoff_2A(a0)
    9.     rts
    Code (Text):
    1. loc_38C22:
    2.     bsr.w    JmpTo25_SingleObjLoad2
    3.     bne.s    return_38C6C
    4.     _move.b    #$98,0(a1) ; load obj98
    5.     move.b    #6,mapping_frame(a1)
    6.     tst.w    (Two_player_mode).w
    7.     beq.w    Missle
    8.     move.b    #$B8,subtype(a1) ; <== ObjA6_SubObjData2
    9.     jmp    Split_Missle
    10. Missle:
    11.     move.b    #$34,subtype(a1) ; <== ObjA6_SubObjData
    12. Split_Missle:
    13.     move.w    x_pos(a0),x_pos(a1)
    14.     move.w    y_pos(a0),y_pos(a1)
    15.     move.w    #-$300,y_vel(a1)...
    Code (Text):
    1. loc_38C6E:
    2.     bsr.w    JmpTo25_SingleObjLoad2
    3.     bne.s    return_38CAC
    4.     _move.b    #$98,0(a1) ; load obj98
    5.     move.b    #6,mapping_frame(a1)
    6.     tst.w    (Two_player_mode).w
    7.     beq.w    Missle_2
    8.     move.b    #$B8,subtype(a1) ; <== ObjA6_SubObjData2
    9.     jmp    Split_Missle_2
    10. Missle_2:
    11.     move.b    #$34,subtype(a1) ; <== ObjA6_SubObjData
    12. Split_Missle_2:
    13.     move.w    x_pos(a0),x_pos(a1)
    14.     move.w    y_pos(a0),y_pos(a1)
    15.     move.w    #$300,d1...
    And then finish it off with adding the subobjdata entries at the end of Spiny's routine:

    Code (Text):
    1. ; off_38CAE:
    2. ObjA5_SubObjData:
    3.     dc.l ObjA5_ObjA6_Obj98_MapUnc_38CCA
    4.     dc.w $252D
    5.     dc.w $404
    6.     dc.w $80B
    7. ObjA5_VS_SubObjData:
    8.     dc.l ObjA5_ObjA6_Obj98_MapUnc_VS
    9.     dc.w $2372
    10.     dc.w $404
    11.     dc.w $80B
    Now we go to Grabber who is even MORE fragmented:

    Code (Text):
    1. loc_38DCC:
    2.     tst.w    (Two_player_mode).w
    3.     beq.w    loc_38DCC_1P
    4.     move.b    #$B0,subtype(a0) ; <== Obj93_SubObjData2
    5. loc_38DCC_1P:
    6.     bsr.w    LoadSubObject
    7.     move.w    #-$40,d0
    8.     btst    #0,render_flags(a0)
    9.     beq.s    loc_38DDE
    10.     neg.w    d0...
    Code (Text):
    1. loc_38F7C:
    2.     tst.w    (Two_player_mode).w
    3.     beq.w    loc_38F7C_1P
    4.     move.b    #$B2,subtype(a0) ; <== Obj93_SubObjData2
    5. loc_38F7C_1P:
    6.     bsr.w    LoadSubObject
    7.     move.b    #3,mapping_frame(a0)
    8.     rts
    Code (Text):
    1. loc_39044:
    2.     tst.w    (Two_player_mode).w
    3.     beq.w    loc_39044_1P
    4.     move.b    #$B4,subtype(a0) ; <== Obj93_SubObjData2
    5. loc_39044_1P:
    6.     bsr.w    LoadSubObject
    7.     move.b    #2,mapping_frame(a0)
    8.     subi.w    #$C,y_pos(a0)
    9.     rts
    Code (Text):
    1. loc_39078:
    2.     tst.w    (Two_player_mode).w
    3.     beq.w    loc_39078_1P
    4.     move.b    #$B6,subtype(a0) ; <== Obj93_SubObjData2
    5. loc_39078_1P:
    6.     bsr.w    LoadSubObject
    7.     subq.w    #8,y_pos(a0)
    8.     rts
    And then we again add in all the subobjdata entries at the end of Grabber's routines:

    Code (Text):
    1. ; off_3920A:
    2. ObjA8_SubObjData2:
    3.     dc.l ObjAA_MapUnc_39228
    4.     dc.w $A500
    5.     dc.b 4,5,4,0
    6. ObjA7_VS_SubObjData:
    7.     dc.l ObjA7_VS
    8.     dc.w $A3D4
    9.     dc.b 4,4,$10,$B
    10. ; off_391F6:
    11. ObjA7_VS_SubObjData2:
    12.     dc.l ObjA7_VS
    13.     dc.w $A3D4
    14.     dc.b 4,1,$10,$D7
    15. ; off_39200:
    16. ObjA8_VS_SubObjData:
    17.     dc.l ObjA7_VS
    18.     dc.w $A3D4
    19.     dc.b 4,4,4,0
    20. ; off_3920A:
    21. ObjA8_VS_SubObjData2:
    22.     dc.l ObjAA_MapUnc_39228
    23.     dc.w $A3D6
    24.     dc.b 4,5,4,0
     

    Attached Files:

    Last edited: Nov 19, 2024
  9. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Step 9: Fixing bugs:

    Object display bugs:

    Both the floating platforms and Grabber have display branches that only work properly for single player mode, and won't spawn for Tails' screen. To fix this, we'll simply add branches in both their routines to skip around it:

    Floating Platform (obj19):

    Code (Text):
    1.  ; loc_220B8:
    2. Obj19_Main:
    3.     move.w    x_pos(a0),-(sp)
    4.     bsr.w    Obj19_Move
    5.     moveq    #0,d1
    6.     move.b    width_pixels(a0),d1
    7.     move.w    #$11,d3
    8.     move.w    (sp)+,d4
    9.     bsr.w    JmpTo4_PlatformObject
    10.     tst.w    (Two_player_mode).w
    11.     bne.s    .displaySprite        ; skip despawn check in 2-player mode
    12.     move.w    objoff_30(a0),d0
    13.     andi.w    #$FF80,d0
    14.     sub.w    (Camera_X_pos_coarse).w,d0
    15.     cmpi.w    #$280,d0
    16.     bhi.w    JmpTo20_DeleteObject
    17.  
    18. .displaySprite:
    19.     bra.w    JmpTo11_DisplaySprite

    Grabber (objA7):

    Code (Text):
    1. loc_39182:
    2.     tst.w    (Two_player_mode).w
    3.     beq.s    +      ; skip despawn check in 2-player mode
    4.     bra.w    JmpTo45_DisplaySprite
    5. ; ---------------------------------------------------------------------------
    6. +    move.w    x_pos(a0),d0
    7.     andi.w    #$FF80,d0
    8.     sub.w    (Camera_X_pos_coarse).w,d0
    9.     cmpi.w    #$280,d0
    10.     bhi.w    +
    11.     bra.w    JmpTo45_DisplaySprite
    Spintube death softlock:

    If you die and fall inside a spintube, it currently will still force you into roll mode and override your player reseting. To make it recognise your player has lost a life, we will simply add a branch for its animation:

    Code (Text):
    1.  loc_225FC:
    2.     tst.w    (Debug_placement_mode).w
    3.     bne.w    return_22718
    4.     cmpi.b    #$18,anim(a1) ;is character dying?
    5.     beq.w    return_22718
    6.     move.w    objoff_2A(a0),d2
    7.     move.w    x_pos(a1),d0
    8.     sub.w    x_pos(a0),d0
    9.     cmp.w    d2,d0
    10.     bcc.w    return_22718
    11.     move.w    y_pos(a1),d1
    12.     sub.w    y_pos(a0),d1
    13.     cmpi.w    #$80,d1
    14.     bcc.w    return_22718
    15.     cmpi.b    #$20,anim(a1)
    16.     beq.w    return_22718
    Spiny projectile direction:

    Spiny technically works fine, but he currently only aims at Sonic due to 1 player. If you want to give him to aim in the direction of both players, do this:

    Code (Text):
    1. loc_38C22:
    2.     bsr.w    JmpTo25_SingleObjLoad2
    3.     bne.s    return_38C6C
    4.     _move.b    #$98,0(a1) ; load obj98
    5.     move.b    #6,mapping_frame(a1)
    6.     move.b    #$34,subtype(a1) ; <== ObjA6_SubObjData
    7.     move.w    x_pos(a0),x_pos(a1)
    8.     move.w    y_pos(a0),y_pos(a1)
    9.     move.w    #-$300,y_vel(a1)
    10.     move.w    #$100,d1
    11.     lea    (MainCharacter).w,a2 ; a2=character
    12.     move.w    x_pos(a0),d0
    13.     cmp.w    x_pos(a2),d0
    14.     bcs.s    loc_38C22_Tails
    15.     neg.w    d1
    16.     jmp    loc_38C60
    17.  
    18. loc_38C22_Tails:
    19.     tst.w    (Two_player_mode).w
    20.     beq.w    loc_38C60
    21.     move.w    x_pos(a2),d0
    22.     sub.w    x_pos(a0),d0
    23.     cmpi.w    #$A0,d0
    24.     bcs.s    loc_38C60
    25.     lea    (Sidekick).w,a2 ; a2=character
    26.     move.w    x_pos(a0),d0
    27.     cmp.w    x_pos(a2),d0
    28.     bcs.s    loc_38C60
    29.     neg.w    d1
    30.  
    31. loc_38C60:
    32.     move.w    d1,x_vel(a1)
    33.     lea    Obj98_SpinyShotFall(pc),a2
    34.     move.l    a2,objoff_2A(a1)
    35.  
    36. return_38C6C:
    37.     rts
     
    Last edited: Nov 19, 2024
  10. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Step 10: Just For Fun:

    If you want to add your own unique track for 2P Chemical Plant for that extra bit of authenticity, just go to 2P Music Playlist.

    This list solely edits what music tracks play for split screen conviniently enough so you can give Chemical Plant whatever theme you want without changing single player's.

    Code (Text):
    1. ;----------------------------------------------------------------------------
    2. ; 2P Music Playlist
    3. ;----------------------------------------------------------------------------
    4. ; byte_3EB2:
    5. MusicList2:
    6.     dc.b  $C+$80    ; 0  ; EHZ 2P
    7.     dc.b   2+$80    ; 1
    8.     dc.b   5+$80    ; 2
    9.     dc.b   4+$80    ; 3
    10.     dc.b   5+$80    ; 4
    11.     dc.b   5+$80    ; 5
    12.     dc.b  $F+$80    ; 6
    13.     dc.b   6+$80    ; 7
    14.     dc.b $10+$80    ; 8
    15.     dc.b  $D+$80    ; 9
    16.     dc.b   4+$80    ; 10
    17.     dc.b   3+$80    ; 11 ; MCZ 2P
    18.     dc.b   8+$80    ; 12 ; CNZ 2P
    19.     dc.b  $E+$80    ; 13 ; CPZ 2P
    20.     dc.b  $A+$80    ; 14
    21.     dc.b   7+$80    ; 15
    22.     dc.b  $D+$80    ; 16
    23.     dc.b   0    ; 17
    24. ; ===========================================================================
    And after all that rigamarole, you should hopefully have Chemical Plant Zone up and running in two player mode.

    This one was rather exhausting so might still need some error checks, if you find anything wrong or something that might be worth refining, please tell me so I can edit this tutorial accordingly.

    Special thanx to Sock Team (especially MoDule for a large amount of the coding, and Markeyjester, whose software wizardry managed to make the split screen optimized level art, also shout out to Zeta_Null who helped work on the earliest attempts of this level in split screen so we could figure a lot of this stuff out).

    Other VS mode tutorials:

    * Add Extra Item Options.
    * Add Hill Top Zone.
    * Make Custom Characters Work in Split Screen.
    * Make Water Work in Split Screen.
     
    Last edited: Dec 22, 2024
  11. Brainulator

    Brainulator

    Regular garden-variety member Member
    Do you mean "CPZ_1_2P.bin" and "CPZ_2_2P.bin"?
     
    • Informative Informative x 1
    • List
  12. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Whoopsie, that's right. Thanx for catching that. Fixed.
     
  13. nineko

    nineko

    I am the Holy Cat Tech Member
    6,377
    531
    93
    italy
    Neat, now do Death Egg Zone, including the two bosses.

    *runs*
     
  14. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Trust me when I say I'd love to make tutorials for the whole game to be split screen friendly, but it's only an uphill battle after these two (and considering how LONG this tutorial is, you can guess how much that says :P).

    A reminder that our team had to practically turn the game inside out to get the other levels running efficiently. Aquatic Ruin is likely the one most plausible on vanilla Sonic 2 after Hill Top and Chemical Plant, but even that has a ton of problematic areas such as water. Even Sonic 2 VS had to downgrade the artwork more drastically to stuff everything into the VRAM.

    (The guys responsible for Sonic 2 Co-op made a mostly accurate Hidden Palace translation however, so maybe you could ask them about that one. :P)

    I intend to make a tutorial for custom levels however, so you guys can try making your own split screen friendly concepts.
     
    Last edited: Nov 20, 2024
  15. Brainulator

    Brainulator

    Regular garden-variety member Member
    Isn't Death Egg Zone's art largely shared with Chemical Plant's?
     
  16. nineko

    nineko

    I am the Holy Cat Tech Member
    6,377
    531
    93
    italy
    Yeah, that was the inspiration for my joke, I suggested to do a Zone which would probably be relatively simple per se, but then I said to include the bosses, which would be near impossible for a variety of reasons.
     
  17. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Well looks like a couple of bugs are left over:

    1. The animated tiles for the background aren't set up right for split screen to display properly. This didn't seem a problem at first since I thought the smaller scroll field for split screen meant they were never offscreen, but they appear JUST at this spot:

    [​IMG]

    2. Also I forgot to map the projectiles for Spiny properly meaning it is just one same frame.

    Since I'm at a limit with VRAM already, we have the choice of fixing the animated background (though this will require some retooling of assets and art locations) and keeping the Spiny projectile as it is by using its extra frame for space, or we can fix Spiny and maybe just place a non-animated block over the error in the background instead. This one would just require a new mapping file. What's the choice here, guys?

    3. Additionally this place is where the lack of water can get the player stuck, since the pressure spring doesn't propell you high enough without water:

    [​IMG]

    For that add this branch to Obj41's routine:

    Code (Text):
    1. loc_26480:
    2.     lea    (byte_26550).l,a3
    3.     move.b    (a3,d0.w),d0
    4.     tst.w    (Two_player_mode).w
    5.     beq.s    loc_26480_Normal
    6.     cmpi.b    #$D,(Current_Zone).w
    7.     bne.s    loc_26480_Normal
    8.     move.w    #-$5A0,y_vel(a1)
    9.     bra.s    loc_26480_2P
    10. loc_26480_Normal:
    11.     move.w    #-$400,y_vel(a1)
    12. loc_26480_2P:
    13.     sub.b    d0,y_vel(a1)
    14.     bset    #0,status(a1)
    15.     btst    #0,status(a0)
    16.     bne.s    loc_264AA
    17.     bclr    #0,status(a1)
    18.     neg.b    d0
    It's not the best fix since it boosts up ALL the springs in CPZ's VS mode, but it's the best way I can think around it besides adding a normal spring or something in place of it.
     
  18. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Okay I've just taken the quicker route with the two fixes. I've changed the tiles for CPZ's background, though that required making a separate chunk file for 2 player to preserve 1 player's setup. Just add the file below underneath the 1 player file and list it in it's respective 2P art pointers list.

    Code (Text):
    1.  ;-----------------------------------------------------------------------------------
    2. ; CPZ/DEZ 128x128 block mappings (Kosinski compression)
    3. BM128_CPZ:    BINCLUDE    "mappings/128x128/CPZ_DEZ.bin"
    4. ;-----------------------------------------------------------------------------------
    5. ; CPZ/DEZ 128x128 block mappings (Kosinski compression)
    6. BM128_CPZ_2P:    BINCLUDE    "mappings/128x128/CPZ_DEZ_2P_128.bin"
    7. ;-----------------------------------------------------------------------------------
    Code (Text):
    1.  levartptrs $1E,$1F,$11, ArtKos_CPZ, BM16_CPZ, BM128_CPZ ;  $D ; CPZ  ; CHEMICAL PLANT ZONE
    2.    levartptrs $1E,$1F,$11, ArtKos_CPZ_2P, BM16_CPZ_2P, BM128_CPZ_2P ;  $D ; CPZ  ; CHEMICAL PLANT ZONE (2 PLAYER)
    The Spiny map file you can simply put in place of the current one.
     

    Attached Files:

  19. E-122-Psi

    E-122-Psi

    Member
    2,532
    728
    93
    Still a couple hurdles right now, but I might manage to squeeze in one more level:
    [​IMG]
    Definitely a compressed state with a bare bones background with no vertical scrolling, but it plays pretty much exactly the same as in 1P.
     
  20. Undying_Star

    Undying_Star

    Member
    8
    5
    3
    Really impressed how these guides have come alone by just using the STOCK engine of sonic 2!