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,531
    728
    93
    You can thank Markeyjester and MoDule for a lot of that. They turned Sonic 2 VS inside out to make a lot of stuff work, though a lot of their coding tricks manage to still work backported to vanilla Sonic 2.

    Just don't expect an 'Add 60fps Special Stage' tutorial any time soon, that's more a S2 VS engine thing. :P
     
  2. Sonic Hachelle-Bee

    Sonic Hachelle-Bee

    Taking a Sand Shower Tech Member
    823
    218
    43
    Lyon, France
    Sonic 2 Long Version
    You know, E-122-Psy is right. You can manage to make everything work with the stock S2 engine. I have ARZ 2P with water and all working with only H-int enhancements, some tricks, debugging and patience.

    The stock S2 engine is not that bad at 2P game, only under used and feels unfinished. But its code architecture is not that bad. In retrospective, it looks like they were in a hurry to include the new version of CNZ. It appears CNZ was included and polished very late into development. My guess is even after WFZ, DEZ and the development of the final boss.

    Looking at the engine, maybe the initial plan was to have more levels in 2P, but ultimately they had not enough time to do so. They focused on the 1P game. However, EHZ and MCZ were not enough for release, so they included the new CNZ, probably the last designed level, as a last minute addition to the 2P game.

    EDIT: (Now that I have more time) I like this guide, and it provides good advices. Do you plan to add a proper H-int function for water levels in the future? Else, I can share mine with explanations on how it works.
     
    Last edited: Dec 15, 2024
  3. E-122-Psi

    E-122-Psi

    Member
    2,531
    728
    93
    That's an interesting theory. I know they said they weren't satisfied with the original graphics for CNZ, maybe the new layout ended up a convinient case of 'more for less'.

    The beta would imply they wanted split screen to be more intrinsic to the whole game, potentially as a co-op campaign, but ultimately I think they went for making the 1 player experience look as appealing as possible, since split screen optimized levels obviously take up a LOT more VRAM. Hill Top Zone was ALMOST optimized but checking out what needed to be fixed, I can understand why they decided not to bother with the time they had left. The 1 player mode was already taking a lot of cuts in the deadline, 2 player mode as an extra just didn't stand a chance.

    I do agree that split screen is really undervalued however. This is why I wanted to not just make hacks but tutorials for it, to get more people invested in trying things with it. It is approachable if you delve in a bit more (though I owe the investment and talent of Sock Team for demonstrating that more fully). Seeing projects like Sonic 2 Co-op and the most recent Delta update play around with split screen themselves is great to see, though it's jarring it's taken so long for split screen hacking to even be a thing.

    Also, wow you got ARZ working in stock S2 split screen? S2 VS struggled even with its refinements to make it look right, ultimately needing a heavily altered background. I'm afraid if people want a tutorial for that one, they might be looking to you. :P
     
    Last edited: Dec 15, 2024
  4. Sonic Hachelle-Bee

    Sonic Hachelle-Bee

    Taking a Sand Shower Tech Member
    823
    218
    43
    Lyon, France
    Sonic 2 Long Version
    Well, S2 VS got it right, you have to cut hard on graphics to make ARZ working. The level was not designed for 2P game in the first place. But the point is, there is no huge engine rework to make it working. Stock S2 engine is enough.
     
  5. E-122-Psi

    E-122-Psi

    Member
    2,531
    728
    93
    Right now MoDule's earlier guide linked in the tutorial is all we have for adding water to stock Sonic 2. We ended up overhauling the game a whole lot to make stuff work smoothly, so some of it might be too overelaborate for a tutorial, at least one to consider as adding it to 'stock Sonic 2' anymore.

    If you wanna make your own split screen tutorials, I'd be quite eager to see it. Truthfully that's what I wanna see more of, more people experimenting with the mode and sharing their knowledge to make it more approachable.

    Oh definitely. As I mentioned, OOZ barely has a background in the state I'm making it, but I (okay, WE :P) at least got all the objects displaying and running properly. It's the full level, but definitely a bit uglier. If people want that, they're welcome to have it when I've figured it all out.
     
  6. Undying_Star

    Undying_Star

    Member
    8
    5
    3
    I just wonder how you are going to handle the object placement without overflowing the object RAM on oil ocean.
     
  7. Undying_Star

    Undying_Star

    Member
    8
    5
    3
    I mean you can tell that at the beginning of sonic 2’s development they DID want splitscreen co-op, the first stage that was made in splitscreen was green hill zone which was just made in splitscreen as a test, they were figuring out what needed to be stripped out. That why there is NO object art loaded in GHZ nick arcade because green hill’s zone art practically loads over the HUD.
     
  8. E-122-Psi

    E-122-Psi

    Member
    2,531
    728
    93
    Some of the objects working on 'Long Distance' use a modified routine by MoDule to manage them, like the one used for the moving blocks in this tutorial. This means the level can load almost full capacity of objects as in 1 player, though expectedly with some slowdown.
     
  9. Kilo

    Kilo

    Starting new projects every week Tech Member
    1,231
    1,177
    93
    Canada
    Changes with the weather
    They were planning for 2 player even in the earliest stages of Sonic 1:
    Sprite-0002.jpeg
    I'm not entirely sure why it was cut back then. Whether it was due to the devs not quite understanding how the interlacing mode on the Mega Drive worked, a lack of VRAM space due to Sonic 1's poor graphics management (At least compared to 2 and 3), or if they just gave up on the idea.
    It seems like it at least went somewhere, though, because palette line 2 in Sonic 1 matches up with Sonic's main palette but as if it was some bizzaro alien Sonic... Which could be what player 2 was supposed to look like, or at least this is my personal theory, of course you're inclined to disagree on that.
    upload_2024-12-15_12-35-6.png
     
  10. E-122-Psi

    E-122-Psi

    Member
    2,531
    728
    93
    Yeah I noticed the second palette line is nearly always consistent in Sonic 1 save a couple levels where the pinks become turquoise. Dunno if it was just coincidence or not (a lot of badniks use that line too) but we know from the arcade games that they liked toying with those Sonic colour swaps prior to Tails and Knuckles coming to be.
     
  11. Undying_Star

    Undying_Star

    Member
    8
    5
    3
    Yuji naka specifically wanted splitscreen multiplayer on the game, but it never really got past the concepting stage of development, and by the second palette claim, im pretty sure they made it match so they could swap around the palette from 0 to 1 to differentiate the badnik behaviour.
     
  12. Undying_Star

    Undying_Star

    Member
    8
    5
    3
    Speaking of sonic 1 splitscreen i might aswell show that sonic 1 most definitely would have worked in splitscreen if taken the time. I might aswell showcase my hack while im at it
     
  13. Sonic Hachelle-Bee

    Sonic Hachelle-Bee

    Taking a Sand Shower Tech Member
    823
    218
    43
    Lyon, France
    Sonic 2 Long Version
    Make water works in the 2P game with the legacy Sonic 2 engine

    1. Horizontal interrupt in theory

    To add water in the 2P game, you have to fully understand how the horizontal interrupt works. I will not detail how an horizontal interrupt works as it is already documented here for example: https://segaretro.org/Sega_Mega_Drive/Interrupts

    If you don't want to bother and want to do a dirty copy and paste job, skip this theoretical section and jump directly to section 4.

    Just be aware that an horizontal interrupt (H_Int) is triggered at the beginning of horizontal blanking (H_Blank) at the end of a scanline. The H_Int function is therefore called. To avoid calling H_Int at every end of scanline and wasting CPU resources, the VDP H_Int_counter register counts the number of scanlines to skip before actually triggering the interrupt. The VDP H_Int_counter is a countdown counter and triggers once it reaches 0.

    As you may already know, horizontal interrupt is used in 2 places in the Sonic 2 game:
    1. In the 1P game when the level contains water and the water surface is displayed on screen, to change the palette.
    2. In the 2P game, at all times, to manage the screen separator at the middle.

    In case 1, the water surface position on screen is precalculated and variable Hint_counter_reserve is filled with that information. This calculation is done in the WaterEffects function.
    In case 2, the value 107 is assigned to variable Hint_counter_reserve at the beginning of the level, which roughly corresponds to half of the screen.

    Once filled, Hint_counter_reserve is then copied to the VDP H_Int_counter register during the vertical interrupt (V_Int) before the beginning of a new frame display.


    2. The case of the Do_Updates_in_H_int flag

    The V_Int function is a rather long function that takes a lot of CPU time to be executed. It actually takes so many time that it often overflows the V_Blank time and continues its execution at the beginning of the display past the first scanline. Usually, this is not really a problem.

    But there is an issue with the water surface: if the water surface is too high on the screen, near the top, this means the H_Int have to trigger early to change the palette. However, due to the priority of the interrupts, the H_Int will not execute if the V_Int function is still running... Then, if V_Int execution overflows V_Blank time, the H_Int function is not called and the palette is not changed.

    Here comes the Do_Updates_in_H_int flag. This flag is set during V_Int in case the water surface is near the top of the screen (before the 92nd scanline). If this is the case, the V_Int execution is shortened so that it absolutely fit in the V_Blank time without overflowing: the huge Do_Updates function is skipped. This allows the H_Int to be triggered normally.

    Therefore, if the Do_Updates_in_H_int flag is set, at the end of H_Int execution, this is when the Do_Updates function is run, after the palette change.

    This is something to keep in mind for our implementation.


    3. The case of multiple horizontal interrupts and the VDP H_Int_counter

    If we want water in our 2P game, you guess that sometimes, both players will have a water surface displayed on screen. In this case, the H_Int must be triggered several times during display to handle the screen separator, as well as to handle both palette changes. This means we have to modify the VDP H_Int_counter on the fly during H_Int to trigger the next H_Int at the right scanline.

    However, there is a problem. The VDP H_Int_counter will not update its value until its counter reaches 0. Unfortunately, this is already too late once H_Int has been triggered as the counter automatically takes its previous assigned value and immediately restarts counting... This means an update to the VDP H_Int_counter will not take effect for the next H_Int, but for the second next H_Int.

    In Sonic 2, this automatic recount of the VDP H_Int_counter is handled with the Hint_flag variable that is cleared during H_Int. A second call to H_Int does nothing if the flag is cleared.

    This is also something to keep in mind for our implementation.


    4. Precalculate everything in the WaterEffects function

    Now its coding time.

    To avoid issues, the H_Int function must be as quick as possible. Then, everything is precalculated in the WaterEffects function.

    To enable water in the 2P game, we have to handle all these cases:
    1. Both Sonic and Tails screens do not have a water surface.
    2. Sonic has a water surface but not Tails.
    3. Tails has a water surface but not Sonic.
    4. Both Sonic and Tails screens have a water surface.

    Not having a water surface can mean 2 things:
    • Entirely above water.
    • Entirely underwater.

    ... and we have to keep the compatibility with the 1P game.

    The above cases mean a lot of H_Int to trigger, and a variable amount of H_Int depending of the case!

    To reduce drastically the complexity of the code, we will stick with a fixed amount of H_Int. Some H_Int calls may do nothing, but each H_Int call serves an unique purpose. The case with the maximum amount of H_Int needed is case 4. We need 3 distinct H_Int calls:
    • 1st H_Int for the Sonic water surface.
    • 2nd H_Int for the screen separator.
    • 3rd H_Int for the Tails water surface.

    ... actually, we need 2 more H_Int calls to handle the VDP H_Int_counter automatic recount detailed in section 3, so that it doesn't bother us:
    • 1st H_Int at the very first scanline to set the VDP H_Int_counter for the Sonic water surface.
    • 2nd H_Int at the second scanline (automatically triggered due to recount) to set the VDP H_Int_counter for the screen separator.
    • 3rd H_Int at the Sonic water surface to change the palette to underwater and set the VDP H_Int_counter for the Tails water surface.
    • 4th H_Int for the screen separator, changing the palette back to normal and set the VDP H_Int_counter past the end of the display.
    • 5th H_Int at the Tails water surface to change the palette to underwater.

    In case 4, palette is also changed back to normal during V_Int.

    upload_2024-12-22_19-54-54.png

    Now, let's start recoding the WaterEffects function. Replace the old function by this new big one:
    Code (ASM):
    1.  
    2. WaterEffects:
    3.     clr.w    (Hint_flag_precalc).w   ; No water by default
    4.     move.b    #223,(Hint_counter_1).w ; No horizontal interrupt by default
    5.  
    6.     tst.w    (Two_player_mode).w
    7.     beq.s    +
    8.     move.w    #1,(Hint_flag_precalc).w ; Enable H_Int for 2P screen separator
    9.     move.b    #107,(Hint_counter_1).w  ; Set VDP H_Int_counter to trigger at roughly half of the screen
    10. +
    11.     tst.b    (Water_flag).w    ; Does level have water?
    12.     beq.w    NonWaterEffects    ; If not, branch
    13.  
    14.     tst.b    (Deform_lock).w
    15.     bne.s    MoveWater
    16.     cmpi.b    #6,(MainCharacter+routine).w ; Is player dead?
    17.     bhs.s    MoveWater                    ; If yes, branch
    18.     bsr.w    DynamicWater
    19. ; loc_4526: ; LZMoveWater:
    20. MoveWater:
    21.     clr.b    (Water_fullscreen_flag).w
    22.     moveq    #0,d0
    23.     cmpi.b    #aquatic_ruin_zone,(Current_Zone).w ; Is level ARZ?
    24.     beq.s    +                                   ; If yes, branch
    25.     move.b    (Oscillating_Data).w,d0             ; Oscillate water surface
    26.     lsr.w    #1,d0
    27. +
    28.     add.w    (Water_Level_2).w,d0 ; Move water up or down if required
    29.     move.w    d0,(Water_Level_1).w
    30.  
    31.     tst.w    (Two_player_mode).w
    32.     bne.s    MoveWater_2P
    33.  
    34.     ; 1P game water surface management
    35. ;MoveWater_1P:
    36. ;.testFullWater:
    37.     move.w    (Water_Level_1).w,d0 ; Calculate distance between water surface and top of the screen
    38.     sub.w    (Camera_Y_pos).w,d0
    39.     bhs.s    .testNoWater
    40.     tst.w    d0
    41.     bpl.s    .testNoWater
    42.  
    43. ;.fullWater:
    44.     bset    #0,(Water_fullscreen_flag).w ; Change to underwater colors at V-int
    45.     bset    #7,(Water_fullscreen_flag).w ; Screen is full of water
    46.     bra.w    NonWaterEffects
    47.  
    48. .testNoWater:
    49.     cmpi.w    #224-4,d0       ; Remove the 4 scanlines needed to update the palette
    50.     bhs.w    NonWaterEffects ; Branch if water surface is below the bottom of the screen
    51.  
    52. ;.halfWater:
    53.     move.w    #$8000,(Hint_flag_precalc).w ; Change to underwater colors at the water surface
    54.     move.b    d0,(Hint_counter_1).w        ; Set VDP H_Int_counter to trigger at the water surface
    55.     bra.w    NonWaterEffects
    56. ;------------------------------------------------------------------------------
    57.  
    58.     ; 2P game water surface management
    59. MoveWater_2P:
    60.     moveq    #0,d2 ; Just a register to keep track of the case we are in. Case 0: Sonic has a water surface
    61.     bset    #4,(Hint_flag_precalc+1).w ; Enable 1st scanline H_Int
    62.     bset    #5,(Hint_flag_precalc+1).w ; Enable 2nd scanline H_Int
    63.     bset    #7,(Hint_flag_precalc+1).w ; Enable Sonic water surface H_Int
    64.     bset    #0,(Hint_flag_precalc).w   ; Enable Tails water surface H_Int
    65.     move.b    #0,(Hint_counter_1).w      ; Set VDP H_Int_counter to trigger at the 1st scanline (and consequently the 2nd scanline too)
    66.  
    67. ;.sonic_TestFullWater:
    68.     move.w    (Water_Level_1).w,d0 ; Calculate distance between Sonic water surface and top of the screen
    69.     move.w    (Camera_Y_pos).w,d1
    70.     addi.w    #4,d1 ; Remove the 1st and 2nd scanlines which are already triggered
    71.     sub.w    d1,d0
    72.     bhs.s    .sonic_TestNoWater
    73.     tst.w    d0
    74.     bpl.s    .sonic_TestNoWater
    75.  
    76. ;.sonic_FullWater:
    77.     moveq    #1,d2                        ; Case 1: Sonic screen is full of water
    78.     bset    #0,(Water_fullscreen_flag).w ; Change to underwater colors at V-int
    79.     bset    #5,(Water_fullscreen_flag).w ; Sonic screen is full of water
    80.     move.b    #1,(Hint_counter_2).w        ; Set VDP H_Int_counter (Sonic water surface) to trigger after the 2nd scanline
    81.     move.b    #107-4,(Hint_counter_3).w    ; Set VDP H_Int_counter (Screen separator) to 107 minus the 4 scanlines already programmed
    82.     bra.s    .tails_TestFullWater
    83. ;------------------------------------------------------------------------------
    84.  
    85. .sonic_TestNoWater:
    86.     cmpi.w    #204,d0 ; Calculate distance between Sonic water surface and bottom of the Sonic screen
    87.     blo.s    .sonic_HalfWater
    88.  
    89. ;.sonic_NoWater:
    90.     moveq    #2,d2                   ; Case 2: Sonic screen has no water
    91.     move.b    #101,(Hint_counter_2).w ; Set VDP H_Int_counter (Sonic water surface) a bit before the screen separator
    92.     move.b    #3,(Hint_counter_3).w   ; Set VDP H_Int_counter (Screen separator) to 107 minus all the scanlines already skipped before
    93.     bra.s    .tails_TestFullWater
    94. ;------------------------------------------------------------------------------
    95.  
    96. .sonic_HalfWater:
    97.     lsr.b    #1,d0 ; Half resolution
    98.     beq.s    .jmpA ; Edge case
    99.     subi.b    #1,d0
    100. .jmpA:    move.b    d0,(Hint_counter_2).w      ; Set VDP H_Int_counter to trigger at the Sonic water surface
    101.     moveq    #104,d1
    102.     sub.b    d0,d1
    103.     move.b    d1,(Hint_counter_3).w      ; Set VDP H_Int_counter (Screen separator) to 107 minus all the scanlines already skipped before
    104.     bset    #6,(Hint_flag_precalc+1).w ; Change to underwater colors at the Sonic water surface
    105.  
    106. .tails_TestFullWater:
    107.     move.w    (Water_Level_1).w,d0 ; Calculate distance between Tails water surface and top of the Tails screen
    108.     move.w    (Camera_Y_pos_P2).w,d1
    109.     subi.w    #8,d1 ; Remove the scanlines of the screen separator where display is deactivated
    110.     bpl.s    .jmpB
    111.     move.w    #0,d1
    112. .jmpB:    sub.w    d1,d0
    113.     bhs.s    .tails_TestNoWater
    114.     tst.w    d0
    115.     bpl.s    .tails_TestNoWater
    116.  
    117. ;.tails_FullWater:
    118.     ; Branch depending of the Sonic case
    119.     cmpi.b    #1,d2
    120.     beq.s    .both_FullWater
    121.     cmpi.b    #2,d2
    122.     beq.s    .tails_FullWater_sonic_NoWater
    123.  
    124. ;.tails_FullWater_sonic_HalfWater:
    125.     bset    #6,(Water_fullscreen_flag).w ; Tails screen is full of water
    126.     move.b    #5,(Hint_counter_4).w        ; Set VDP H_Int_counter (Tails water surface) to trigger some scanlines after the screen separator
    127.     bra.w    NonWaterEffects
    128. ;------------------------------------------------------------------------------
    129.  
    130. .tails_FullWater_sonic_NoWater:
    131.     bset    #6,(Water_fullscreen_flag).w ; Tails screen is full of water
    132.     move.b    #5,(Hint_counter_4).w        ; Set VDP H_Int_counter (Tails water surface) to trigger some scanlines after the screen separator
    133.     bset    #1,(Hint_flag_precalc+1).w   ; Change to underwater colors during the screen separator
    134.     bra.w    NonWaterEffects
    135. ;------------------------------------------------------------------------------
    136.  
    137. .both_FullWater:
    138.     bset    #6,(Water_fullscreen_flag).w ; Tails screen is full of water
    139.     bset    #7,(Water_fullscreen_flag).w ; Both Sonic and Tails screens are full of water
    140.     move.b    #5,(Hint_counter_4).w        ; Set VDP H_Int_counter (Tails water surface) to trigger some scanlines after the screen separator
    141.     bra.w    NonWaterEffects
    142. ;------------------------------------------------------------------------------
    143.  
    144. .tails_TestNoWater:
    145.     cmpi.w    #228,d0 ; Calculate distance between Tails water surface and bottom of the screen
    146.     blo.s    .tails_HalfWater
    147.  
    148. ;.tails_NoWater:
    149.     ; Branch depending of the Sonic case
    150.     cmpi.b    #1,d2
    151.     beq.s    .tails_NoWater_sonic_FullWater
    152.     cmpi.b    #2,d2
    153.     beq.s    .both_NoWater
    154.  
    155. ;.tails_NoWater_sonic_HalfWater:
    156.     move.b    #112,(Hint_counter_4).w    ; Set VDP H_Int_counter (Tails water surface) to past the bottom of the screen
    157.     bset    #2,(Hint_flag_precalc+1).w ; Change to normal colors during the screen separator
    158.     bra.w    NonWaterEffects
    159. ;------------------------------------------------------------------------------
    160.  
    161. .tails_NoWater_sonic_FullWater:
    162.     move.b    #112,(Hint_counter_4).w    ; Set VDP H_Int_counter (Tails water surface) to past the bottom of the screen
    163.     bset    #2,(Hint_flag_precalc+1).w ; Change to normal colors during the screen separator
    164.     bra.w    NonWaterEffects
    165. ;------------------------------------------------------------------------------
    166.  
    167. .both_NoWater:
    168.     move.b    #112,(Hint_counter_4).w ; Set VDP H_Int_counter (Tails water surface) to past the bottom of the screen
    169.     bra.w    NonWaterEffects
    170. ;------------------------------------------------------------------------------
    171.  
    172. .tails_HalfWater:
    173.     ; Branch depending of the Sonic case
    174.     cmpi.b    #1,d2
    175.     beq.s    .tails_HalfWater_sonic_FullWater
    176.     cmpi.b    #2,d2
    177.     beq.s    .tails_HalfWater_sonic_NoWater
    178.  
    179. ;.both_HalfWater:
    180.     lsr.b    #1,d0 ; Half resolution
    181.     beq.s    .jmpC ; Edge case
    182.     subi.b    #1,d0
    183. .jmpC:    move.b    d0,(Hint_counter_4).w      ; Set VDP H_Int_counter to trigger at the Tails water surface
    184.     bset    #2,(Hint_flag_precalc+1).w ; Change to normal colors during the screen separator
    185.     bset    #1,(Hint_flag_precalc).w   ; Change to underwater colors at the Tails water surface
    186.     bra.w    NonWaterEffects
    187. ;------------------------------------------------------------------------------
    188.  
    189. .tails_HalfWater_sonic_FullWater:
    190.     lsr.b    #1,d0 ; Half resolution
    191.     beq.s    .jmpD ; Edge case
    192.     subi.b    #1,d0
    193. .jmpD:    move.b    d0,(Hint_counter_4).w      ; Set VDP H_Int_counter to trigger at the Tails water surface
    194.     bset    #2,(Hint_flag_precalc+1).w ; Change to normal colors during the screen separator
    195.     bset    #1,(Hint_flag_precalc).w   ; Change to underwater colors at the Tails water surface
    196.     bra.w    NonWaterEffects
    197. ;------------------------------------------------------------------------------
    198.  
    199. .tails_HalfWater_sonic_NoWater:
    200.     lsr.b    #1,d0 ; Half resolution
    201.     beq.s    .jmpE ; Edge case
    202.     subi.b    #1,d0
    203. .jmpE:    move.b    d0,(Hint_counter_4).w    ; Set VDP H_Int_counter to trigger at the Tails water surface
    204.     bset    #1,(Hint_flag_precalc).w ; Change to underwater colors at the Tails water surface
    205.  
    206. ; loc_456A:
    207. NonWaterEffects:
    208.     cmpi.b    #oil_ocean_zone,(Current_Zone).w
    209.     bne.s    +
    210.     bsr.w    OilSlides ; Call slide routine
    211. +
    212.     cmpi.b    #wing_fortress_zone,(Current_Zone).w ; Is the level WFZ?
    213.     bne.s    +                                    ; If not, branch
    214.     bsr.w    WindTunnel                           ; Call wind and block break routine
    215. +
    216.     rts
    217.  
    For the new WaterEffects function to work, you have to create and to update some RAM variables.

    Create these new RAM variables. The best is to create them in free slots between MiscLevelVariables and MiscLevelVariables_End:
    Code (ASM):
    1.  
    2. Hint_flag_precalc:        ds.w    1    ; Same as Hint_flag, computed during display
    3. Hint_counter_1:            ds.b    1    ; Hold the VDP H_Int_counter for the 1st and 2nd scanlines (or water surface in 1P game)
    4. Hint_counter_2:            ds.b    1    ; Hold the VDP H_Int_counter for the Sonic water surface
    5. Hint_counter_3:            ds.b    1    ; Hold the VDP H_Int_counter for the screen separator in the middle of the 2P screen
    6. Hint_counter_4:            ds.b    1    ; Hold the VDP H_Int_counter for the Tails water surface
    7. WaterSurface1_x_pos:        ds.w    1    ; Hold the X pos of the Sonic water surface object
    8. WaterSurface2_x_pos:        ds.w    1    ; Hold the X pos of the Sonic water surface object (second part)
    9. WaterSurface1_x_pos_2P:        ds.w    1    ; Hold the X pos of the Tails water surface object
    10. WaterSurface2_x_pos_2P:        ds.w    1    ; Hold the X pos of the Tails water surface object (second part)
    11.  
    Update these already existing RAM variables:
    Code (ASM):
    1.  
    2. Hint_flag:            ds.w    1    ; unless this is > 0, H-int won't run
    3.                         ; bit 0: Enable H_Int for the 2P screen separator
    4.                         ; bit 1: Change to underwater colors during the screen separator
    5.                         ; bit 2: Change to normal colors during the screen separator
    6.                         ; bit 4: Enable 1st scanline H_Int
    7.                         ; bit 5: Enable 2nd scanline H_Int
    8.                         ; bit 6: Change to underwater colors at the Sonic water surface
    9.                         ; bit 7: Enable Sonic water surface H_Int
    10.                         ; bit 8: Enable Tails water surface H_Int
    11.                         ; bit 9: Change to underwater colors at the Tails water surface
    12.                         ; bit 15: 1P game: change to underwater colors at the water surface
    13.  
    14. Water_fullscreen_flag:        ds.b    1    ; Flags when screen is full of water
    15.                         ; bit 0: Change to underwater colors at V_Int
    16.                         ; bit 5: Sonic screen is full of water
    17.                         ; bit 6: Tails screen is full of water
    18.                         ; bit 7: Whole screen is full of water (1P or 2P game)
    19.  

    5. Re-implement the H_Int function

    It is time to re-implement the H_Int function almost entirely, following the explanations and changes above. Replace the old function by this new one:
    Code (ASM):
    1.  
    2. H_Int:
    3.     tst.w    (Hint_flag).w
    4.     beq.w    H_Int_Ret              ; Branch if all 16 bits are cleared (= 0)
    5.     bmi.w    H_Int_1P_Water_Surface ; Branch if bit 15 is set
    6.  
    7.     tst.b    (Hint_flag+1).w
    8.     beq.w    H_Int_2P_Tails_Water_Surface ; Branch if bit 8 is set and bits 0 to 7 are cleared
    9.     bmi.w    H_Int_2P_Sonic_Water_Surface ; Branch if bit 7 is set
    10.  
    11. ;H_Int_2P_ScreenSeparator:
    12.     move.l    a5,-(sp)
    13.     move.l    d0,-(sp)
    14.  
    15.     ; Wait until we're in the H-blank region
    16. -    move.w    (VDP_control_port).l,d0
    17.     andi.w    #4,d0
    18.     beq.s    -
    19.  
    20.     ; Disable display
    21.     move.w    (VDP_Reg1_val).w,d0
    22.     andi.b    #$BF,d0
    23.     move.w    d0,(VDP_control_port).l
    24.  
    25.     ; Switch to 2P Tails PNT A
    26.     move.w    #$8200|(VRAM_Plane_A_Name_Table_2P/$400),(VDP_control_port).l ; PNT A base: $A000
    27.  
    28.     ; Update Tails screen V-Scroll
    29.     move.l    #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l
    30.     move.l    (Vscroll_Factor_P2_HInt).w,(VDP_data_port).l
    31.  
    32.     stopZ80
    33.     if fixBugs
    34.     ; Like in Sonic 3, the sprite tables are page-flipped in two-player mode.
    35.     ; This fixes a race-condition where incomplete sprite tables can be uploaded
    36.     ; to the VDP on lag frames.
    37.  
    38.     ; Upload the front buffer.
    39.     tst.b    (Current_sprite_table_page).w
    40.     beq.s    +
    41.     dma68kToVDP Sprite_Table_P2,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    42.     bra.s    ++
    43. +
    44.     dma68kToVDP Sprite_Table_P2_Alternate,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    45. +
    46.     else
    47.     dma68kToVDP Sprite_Table_P2,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    48.     endif
    49.  
    50.     ; Change color palette if needed
    51.     cmpi.b    #3,(Hint_flag+1).w
    52.     beq.w    H_Int_2P_ScreenSeparator_UnderwaterColors ; Branch if bit 1 is set
    53.     bhi.w    H_Int_2P_ScreenSeparator_NormalColors     ; Branch if bit 2 is set
    54.     startZ80
    55.  
    56.     ; Wait until we're in the H-blank region
    57.     ; Wait 3 times to cover 3 scanlines
    58. -    move.w    (VDP_control_port).l,d0
    59.     andi.w    #4,d0
    60.     beq.s    -
    61.     nop ; Mandatory to effectively skip a scanline
    62.     nop
    63. -    move.w    (VDP_control_port).l,d0
    64.     andi.w    #4,d0
    65.     beq.s    -
    66.     nop
    67.     nop
    68. -    move.w    (VDP_control_port).l,d0
    69.     andi.w    #4,d0
    70.     beq.s    -
    71.  
    72.     ; Enable display
    73.     move.w    (VDP_Reg1_val).w,d0
    74.     ori.b    #$40,d0
    75.     move.w    d0,(VDP_control_port).l
    76.  
    77.     move.b    #0,(Hint_flag+1).w
    78.  
    79.     move.b    #223,(Hint_counter_reserve+1).w
    80.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    81.  
    82.     move.l    (sp)+,d0
    83.     movea.l    (sp)+,a5
    84.  
    85. H_Int_Ret:
    86.     rte
    87. ; ===========================================================================
    88.  
    89. H_Int_2P_ScreenSeparator_UnderwaterColors:
    90.     dma68kToVDP Underwater_palette,$0000,palette_line_size*4,CRAM
    91.     startZ80
    92.  
    93.     ; Wait until we're in the H-blank region
    94. -    move.w    (VDP_control_port).l,d0
    95.     andi.w    #4,d0
    96.     beq.s    -
    97.  
    98.     ; Enable display
    99.     move.w    (VDP_Reg1_val).w,d0
    100.     ori.b    #$40,d0
    101.     move.w    d0,(VDP_control_port).l
    102.  
    103.     move.b    #0,(Hint_flag+1).w
    104.  
    105.     move.b    #223,(Hint_counter_reserve+1).w
    106.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    107.  
    108.     move.l    (sp)+,d0
    109.     movea.l    (sp)+,a5
    110.     rte
    111. ; ===========================================================================
    112.  
    113. H_Int_2P_ScreenSeparator_NormalColors:
    114.     dma68kToVDP Normal_palette,$0000,palette_line_size*4,CRAM
    115.     startZ80
    116.  
    117.     ; Wait until we're in the H-blank region
    118. -    move.w    (VDP_control_port).l,d0
    119.     andi.w    #4,d0
    120.     beq.s    -
    121.  
    122.     ; Enable display
    123.     move.w    (VDP_Reg1_val).w,d0
    124.     ori.b    #$40,d0
    125.     move.w    d0,(VDP_control_port).l
    126.  
    127.     move.b    #0,(Hint_flag+1).w
    128.  
    129.     move.b    #223,(Hint_counter_reserve+1).w
    130.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    131.  
    132.     move.l    (sp)+,d0
    133.     movea.l    (sp)+,a5
    134.     rte
    135. ; ===========================================================================
    136.  
    137. H_Int_2P_First_Scanline:
    138.     bclr    #4,(Hint_flag+1).w
    139.  
    140.     move.b    (Hint_counter_2).w,(Hint_counter_reserve+1).w ; Sonic water surface
    141.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    142.     rte
    143. ; ===========================================================================
    144.  
    145. H_Int_2P_Second_Scanline:
    146.     bclr    #5,(Hint_flag+1).w
    147.  
    148.     move.b    (Hint_counter_3).w,(Hint_counter_reserve+1).w ; Screen separator
    149.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    150.  
    151.     ; Run routines postponed from V_Int if Sonic water surface is not too high.
    152.     cmpi.b    #52,(Hint_counter_2).w
    153.     bhs.w    V_Int_Postponed
    154.     rte
    155. ; ===========================================================================
    156.  
    157. H_Int_2P_Sonic_Water_Surface:
    158.     ; Handle first 2 interrupts on the first 2 scanlines
    159.     btst    #4,(Hint_flag+1).w
    160.     bne.s    H_Int_2P_First_Scanline
    161.     btst    #5,(Hint_flag+1).w
    162.     bne.s    H_Int_2P_Second_Scanline
    163.  
    164.     bclr    #7,(Hint_flag+1).w
    165.  
    166.     move.b    (Hint_counter_4).w,(Hint_counter_reserve+1).w ; Tails water surface
    167.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    168.  
    169.     ; Change to underwater colors if needed
    170.     btst    #6,(Hint_flag+1).w
    171.     beq.s    H_Int_2P_Sonic_Ret
    172.  
    173.     bclr    #6,(Hint_flag+1).w
    174.  
    175.     movem.l    a0-a1,-(sp)
    176.     lea    (VDP_data_port).l,a1
    177.     lea    (Underwater_palette).w,a0        ; load palette from RAM
    178.     move.l    #vdpComm($0000,CRAM,WRITE),4(a1) ; set VDP to write to CRAM address $00
    179.     rept 32
    180.     move.l    (a0)+,(a1) ; move palette to CRAM (all 64 colors at once)
    181.     endm
    182.     movem.l    (sp)+,a0-a1
    183.  
    184. H_Int_2P_Sonic_Ret:
    185.     ; Run routines postponed from V_Int if it was not performed before.
    186.     tst.b    (Do_Updates_in_H_int).w
    187.     bne.w    V_Int_Postponed
    188.     rte
    189. ; ===========================================================================
    190.  
    191. H_Int_2P_Tails_Water_Surface:
    192.     move    #$2700,sr ; No other interrupts until next waiting for V-Int
    193.     bclr    #0,(Hint_flag).w
    194.  
    195.     move.b    #223,(Hint_counter_reserve+1).w
    196.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    197.  
    198.     ; Change to underwater colors if needed
    199.     btst    #1,(Hint_flag).w
    200.     beq.s    H_Int_2P_Tails_Ret
    201.  
    202.     bclr    #1,(Hint_flag).w
    203.  
    204.     movem.l    a0-a1,-(sp)
    205.     lea    (VDP_data_port).l,a1
    206.     lea    (Underwater_palette).w,a0        ; load palette from RAM
    207.     move.l    #vdpComm($0000,CRAM,WRITE),4(a1) ; set VDP to write to CRAM address $00
    208.     rept 32
    209.     move.l    (a0)+,(a1) ; move palette to CRAM (all 64 colors at once)
    210.     endm
    211.     movem.l    (sp)+,a0-a1
    212.  
    213. H_Int_2P_Tails_Ret:
    214.     rte
    215. ; ===========================================================================
    216. ; loc_1000:
    217. H_Int_1P_Water_Surface:
    218.     move    #$2700,sr ; No other interrupts until next waiting for V-Int
    219.     move.w    #0,(Hint_flag).w
    220.  
    221.     move.b    #223,(Hint_counter_reserve+1).w
    222.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    223.  
    224.     movem.l    a0-a1,-(sp)
    225.     lea    (VDP_data_port).l,a1
    226.     lea    (Underwater_palette).w,a0        ; load palette from RAM
    227.     move.l    #vdpComm($0000,CRAM,WRITE),4(a1) ; set VDP to write to CRAM address $00
    228.     rept 32
    229.     move.l    (a0)+,(a1) ; move palette to CRAM (all 64 colors at once)
    230.     endm
    231.     movem.l    (sp)+,a0-a1
    232.  
    233.     ; Run routines postponed from V_Int if H_Int was triggered early.
    234.     tst.b    (Do_Updates_in_H_int).w
    235.     bne.s    V_Int_Postponed
    236.     rte
    237. ; ===========================================================================
    238.  
    239. V_Int_Postponed:
    240.     clr.b    (Do_Updates_in_H_int).w
    241.     movem.l    d0-a6,-(sp)
    242.     bsr.w    Do_Updates
    243.     movem.l    (sp)+,d0-a6
    244.     rte
    245.  
    You can now remove the PalToCRAM function as it is useless.


    6. Update the V_Int function

    Now that the Water_fullscreen_flag variable is a bitfield, we have to change every test of the old flag. We also have to update the Hint_flag variable with our precalculated Hint_flag_precalc variable. And we have to initialize the VDP H_Int_counter to the first scanline with horizontal interrupt.

    We have to modify 5 functions: Vint_Lag, Vint_Level, Vint_TitleCard, Vint_Fade and Do_ControllerPal (which is called by many other V_Int functions).

    Let's do all of this step by step. Replace the following functions with the new code. Follow the source code to know what changed.

    In function Vint_Lag:
    Code (ASM):
    1.  
    2. Vint_Lag:
    3.     cmpi.b    #GameModeID_TitleCard|GameModeID_Demo,(Game_Mode).w    ; pre-level Demo Mode?
    4.     beq.s    loc_4C4
    5.     cmpi.b    #GameModeID_TitleCard|GameModeID_Level,(Game_Mode).w    ; pre-level Zone play mode?
    6.     beq.s    loc_4C4
    7.     cmpi.b    #GameModeID_Demo,(Game_Mode).w    ; Demo Mode?
    8.     beq.s    loc_4C4
    9.     cmpi.b    #GameModeID_Level,(Game_Mode).w    ; Zone play mode?
    10.     beq.s    loc_4C4
    11.  
    12.     stopZ80            ; stop the Z80
    13.     bsr.w    sndDriverInput    ; give input to the sound driver
    14.     startZ80        ; start the Z80
    15.  
    16.     bra.s    VintRet
    17. ; ---------------------------------------------------------------------------
    18.  
    19. loc_4C4:
    20.     tst.b    (Water_flag).w
    21.     beq.w    Vint0_noWater
    22.  
    23.     move.w    (VDP_control_port).l,d0
    24.     move.l    #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l
    25.     move.l    (Vscroll_Factor).w,(VDP_data_port).l
    26.  
    27.     btst    #6,(Graphics_Flags).w
    28.     beq.s    +
    29.  
    30.     move.w    #$700,d0
    31. -    dbf    d0,- ; do nothing for a while...
    32. +
    33.     stopZ80
    34.  
    35.     btst    #0,(Water_fullscreen_flag).w
    36.     bne.s    loc_526
    37.  
    38.     dma68kToVDP Normal_palette,$0000,palette_line_size*4,CRAM
    39.     bra.s    loc_54A
    40. ; ---------------------------------------------------------------------------
    41.  
    42. loc_526:
    43.     dma68kToVDP Underwater_palette,$0000,palette_line_size*4,CRAM
    44.  
    45. loc_54A:
    46.     move.w    (Hint_flag_precalc).w,(Hint_flag).w           ; Update Hint_flag with our precalculated variable
    47.     move.b    (Hint_counter_1).w,(Hint_counter_reserve+1).w ; Update VDP H_Int_counter with our precalculated variable
    48.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    49.  
    50.     ; Update water surface objects X pos
    51.     move.w    (WaterSurface1_x_pos).w,(WaterSurface1+x_pos).w
    52.     move.w    (WaterSurface2_x_pos).w,(WaterSurface2+x_pos).w
    53.     move.w    (WaterSurface1_x_pos_2P).w,(WaterSurface1_2P+x_pos).w
    54.     move.w    (WaterSurface2_x_pos_2P).w,(WaterSurface2_2P+x_pos).w
    55.  
    56.     move.w    #$8200|(VRAM_Plane_A_Name_Table/$400),(VDP_control_port).l    ; Set scroll A PNT base to $C000
    57.     move.l    (Vscroll_Factor_P2).w,(Vscroll_Factor_P2_HInt).w
    58.  
    59.     if fixBugs
    60.     ; In two-player mode, we have to update the sprite table
    61.     ; even during a lag frame so that the top half of the screen
    62.     ; shows the correct sprites.
    63.     tst.w    (Two_player_mode).w
    64.     beq.s    +
    65.  
    66.     ; Like in Sonic 3, the sprite tables are page-flipped in two-player mode.
    67.     ; This fixes a race-condition where incomplete sprite tables can be uploaded
    68.     ; to the VDP on lag frames, causing corrupted sprites to appear.
    69.  
    70.     ; Upload the front buffer.
    71.     tst.b    (Current_sprite_table_page).w
    72.     bne.s    +
    73.     dma68kToVDP Sprite_Table_Alternate,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    74.     bra.s    ++
    75. +
    76.     endif
    77.     dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    78. +
    79.     bsr.w    sndDriverInput
    80.  
    81.     startZ80
    82.  
    83.     bra.w    VintRet
    84. ; ---------------------------------------------------------------------------
    85.  
    86. Vint0_noWater:
    87.     move.w    (VDP_control_port).l,d0
    88.     move.l    #vdpComm($0000,VSRAM,WRITE),(VDP_control_port).l
    89.     move.l    (Vscroll_Factor).w,(VDP_data_port).l
    90.  
    91.     btst    #6,(Graphics_Flags).w
    92.     beq.s    +
    93.  
    94.     move.w    #$700,d0
    95. -    dbf    d0,- ; do nothing for a while...
    96. +
    97.     stopZ80
    98.  
    99.     move.w    (Hint_flag_precalc).w,(Hint_flag).w           ; Update Hint_flag with our precalculated variable
    100.     move.b    (Hint_counter_1).w,(Hint_counter_reserve+1).w ; Update VDP H_Int_counter with our precalculated variable
    101.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    102.    
    103.     move.w    #$8200|(VRAM_Plane_A_Name_Table/$400),(VDP_control_port).l    ; Set scroll A PNT base to $C000
    104.     move.l    (Vscroll_Factor_P2).w,(Vscroll_Factor_P2_HInt).w
    105.    
    106.     if fixBugs
    107.     ; In two-player mode, we have to update the sprite table
    108.     ; even during a lag frame so that the top half of the screen
    109.     ; shows the correct sprites.
    110.     tst.w    (Two_player_mode).w
    111.     beq.s    +
    112.  
    113.     ; Like in Sonic 3, the sprite tables are page-flipped in two-player mode.
    114.     ; This fixes a race-condition where incomplete sprite tables can be uploaded
    115.     ; to the VDP on lag frames, causing corrupted sprites to appear.
    116.  
    117.     ; Upload the front buffer.
    118.     tst.b    (Current_sprite_table_page).w
    119.     bne.s    +
    120.     dma68kToVDP Sprite_Table_Alternate,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    121.     bra.s    ++
    122. +
    123.     endif
    124.     dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    125. +
    126.     bsr.w    sndDriverInput
    127.    
    128.     startZ80
    129.  
    130.     bra.w    VintRet
    131.  
    In function Vint_Level:
    Code (ASM):
    1.  
    2. Vint_Level:
    3.     stopZ80
    4.  
    5.     bsr.w    ReadJoypads
    6.     tst.b    (Teleport_timer).w
    7.     beq.s    loc_6F8
    8.     lea    (VDP_control_port).l,a5
    9.     tst.w    (Game_paused).w    ; is the game paused?
    10.     bne.w    loc_748        ; if yes, branch
    11.     subq.b    #1,(Teleport_timer).w
    12.     bne.s    +
    13.     move.b    #0,(Teleport_flag).w
    14. +
    15.     cmpi.b    #16,(Teleport_timer).w
    16.     blo.s    loc_6F8
    17.     lea    (VDP_data_port).l,a6
    18.     move.l    #vdpComm($0000,CRAM,WRITE),(VDP_control_port).l
    19.     move.w    #$EEE,d0 ; White.
    20.  
    21.     ; Do two palette lines.
    22.     move.w    #16*2-1,d1
    23. -    move.w    d0,(a6)
    24.     dbf    d1,-
    25.  
    26.     ; Skip a colour.
    27.     move.l    #vdpComm($0042,CRAM,WRITE),(VDP_control_port).l
    28.  
    29.     ; Do the remaining two palette lines.
    30.     if fixBugs
    31.     move.w    #31-1,d1
    32.     else
    33.     ; This does one more colour than necessary: it isn't accounting for
    34.     ; the colour that was skipped earlier!
    35.     move.w    #32-1,d1
    36.     endif
    37. -    move.w    d0,(a6)
    38.     dbf    d1,-
    39.  
    40.     bra.s    loc_748
    41. ; ---------------------------------------------------------------------------
    42.  
    43. loc_6F8:
    44.     btst    #0,(Water_fullscreen_flag).w
    45.     bne.s    loc_724
    46.  
    47.     dma68kToVDP Normal_palette,$0000,palette_line_size*4,CRAM
    48.     bra.s    loc_748
    49. ; ---------------------------------------------------------------------------
    50.  
    51. loc_724:
    52.     dma68kToVDP Underwater_palette,$0000,palette_line_size*4,CRAM
    53.  
    54. loc_748:
    55.     move.w    (Hint_flag_precalc).w,(Hint_flag).w           ; Update Hint_flag with our precalculated variable
    56.     move.b    (Hint_counter_1).w,(Hint_counter_reserve+1).w ; Update VDP H_Int_counter with our precalculated variable
    57.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    58.  
    59.     ; Update water surface objects X pos
    60.     move.w    (WaterSurface1_x_pos).w,(WaterSurface1+x_pos).w
    61.     move.w    (WaterSurface2_x_pos).w,(WaterSurface2+x_pos).w
    62.     move.w    (WaterSurface1_x_pos_2P).w,(WaterSurface1_2P+x_pos).w
    63.     move.w    (WaterSurface2_x_pos_2P).w,(WaterSurface2_2P+x_pos).w
    64.  
    65.     move.w    #$8200|(VRAM_Plane_A_Name_Table/$400),(VDP_control_port).l    ; Set scroll A PNT base to $C000
    66.  
    67.     dma68kToVDP Horiz_Scroll_Buf,VRAM_Horiz_Scroll_Table,VRAM_Horiz_Scroll_Table_Size,VRAM
    68.  
    69.     if fixBugs
    70.     tst.w    (Two_player_mode).w
    71.     beq.s    ++
    72.     ; Like in Sonic 3, the sprite tables are page-flipped in two-player mode.
    73.     ; This fixes a race-condition where incomplete sprite tables can be uploaded
    74.     ; to the VDP on lag frames, causing corrupted sprites to appear.
    75.  
    76.     ; Perform page-flipping.
    77.     tst.b    (Sprite_table_page_flip_pending).w
    78.     beq.s    +
    79.     sf.b    (Sprite_table_page_flip_pending).w
    80.     not.b    (Current_sprite_table_page).w
    81. +
    82.     ; Upload the front buffer.
    83.     tst.b    (Current_sprite_table_page).w
    84.     bne.s    +
    85.     dma68kToVDP Sprite_Table_Alternate,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    86.     bra.s    ++
    87. +
    88.     endif
    89.     dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    90. +
    91.     bsr.w    ProcessDMAQueue
    92.     bsr.w    sndDriverInput
    93.  
    94.     startZ80
    95.  
    96.     movem.l    (Camera_RAM).w,d0-d7
    97.     movem.l    d0-d7,(Camera_RAM_copy).w
    98.     movem.l    (Camera_X_pos_P2).w,d0-d7
    99.     movem.l    d0-d7,(Camera_P2_copy).w
    100.     movem.l    (Scroll_flags).w,d0-d3
    101.     movem.l    d0-d3,(Scroll_flags_copy).w
    102.     move.l    (Vscroll_Factor_P2).w,(Vscroll_Factor_P2_HInt).w
    103.    
    104.     ; V_Int routine can't be executed fully during V-blank.
    105.     ; Postpone execution of V_Int into H_Int in case we need to trigger H_Int early.
    106.     tst.b    (Water_flag).w
    107.     beq.s    Do_Updates
    108.  
    109.     cmpi.b    #GameModeID_SpecialStage,(Game_Mode).w ; Handle a bug in SS results screen
    110.     beq.s    Do_Updates
    111.  
    112.     tst.w    (Two_player_mode).w
    113.     bne.s    +
    114.  
    115.     cmpi.b    #92,(Hint_counter_reserve+1).w
    116.     bhs.s    Do_Updates
    117. +
    118.     move.b    #1,(Do_Updates_in_H_int).w
    119.     rts
    120.  
    In function Vint_TitleCard:
    Code (ASM):
    1.  
    2. Vint_TitleCard:
    3.     stopZ80
    4.  
    5.     bsr.w    ReadJoypads
    6.  
    7.     btst    #0,(Water_fullscreen_flag).w
    8.     bne.s    loc_BB2
    9.  
    10.     dma68kToVDP Normal_palette,$0000,palette_line_size*4,CRAM
    11.     bra.s    loc_BD6
    12. ; ---------------------------------------------------------------------------
    13.  
    14. loc_BB2:
    15.     dma68kToVDP Underwater_palette,$0000,palette_line_size*4,CRAM
    16.  
    17. loc_BD6:
    18.     move.w    (Hint_flag_precalc).w,(Hint_flag).w           ; Update Hint_flag with our precalculated variable
    19.     move.b    (Hint_counter_1).w,(Hint_counter_reserve+1).w ; Update VDP H_Int_counter with our precalculated variable
    20.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    21.  
    22.     dma68kToVDP Horiz_Scroll_Buf,VRAM_Horiz_Scroll_Table,VRAM_Horiz_Scroll_Table_Size,VRAM
    23.  
    24.     if fixBugs
    25.     tst.w    (Two_player_mode).w
    26.     beq.s    ++
    27.     ; Like in Sonic 3, the sprite tables are page-flipped in two-player mode.
    28.     ; This fixes a race-condition where incomplete sprite tables can be uploaded
    29.     ; to the VDP on lag frames, causing corrupted sprites to appear.
    30.  
    31.     ; Perform page-flipping.
    32.     tst.b    (Sprite_table_page_flip_pending).w
    33.     beq.s    +
    34.     sf.b    (Sprite_table_page_flip_pending).w
    35.     not.b    (Current_sprite_table_page).w
    36. +
    37.     ; Upload the front buffer.
    38.     tst.b    (Current_sprite_table_page).w
    39.     bne.s    +
    40.     dma68kToVDP Sprite_Table_Alternate,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    41.     bra.s    ++
    42. +
    43.     endif
    44.     dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    45. +
    46.     bsr.w    ProcessDMAQueue
    47.     jsr    (DrawLevelTitleCard).l
    48.     jsr    (sndDriverInput).l
    49.  
    50.     startZ80
    51.  
    52.     movem.l    (Camera_RAM).w,d0-d7
    53.     movem.l    d0-d7,(Camera_RAM_copy).w
    54.     movem.l    (Scroll_flags).w,d0-d1
    55.     movem.l    d0-d1,(Scroll_flags_copy).w
    56.     move.l    (Vscroll_Factor_P2).w,(Vscroll_Factor_P2_HInt).w
    57.     bsr.w    ProcessDPLC
    58.     rts
    59.  
    In function Vint_Fade:
    Code (ASM):
    1.  
    2. Vint_Fade:
    3.     bsr.w    Do_ControllerPal
    4.  
    5.     move.w    (Hint_flag_precalc).w,(Hint_flag).w           ; Update Hint_flag with our precalculated variable
    6.     move.b    (Hint_counter_1).w,(Hint_counter_reserve+1).w ; Update VDP H_Int_counter with our precalculated variable
    7.     move.w    (Hint_counter_reserve).w,(VDP_control_port).l
    8.  
    9.     bra.w    ProcessDPLC
    10.  
    In function Do_ControllerPal:
    Code (ASM):
    1.  
    2. Do_ControllerPal:
    3.     stopZ80
    4.  
    5.     bsr.w    ReadJoypads
    6.  
    7.     btst    #0,(Water_fullscreen_flag).w
    8.     bne.s    loc_EDA
    9.  
    10.     dma68kToVDP Normal_palette,$0000,palette_line_size*4,CRAM
    11.     bra.s    loc_EFE
    12. ; ---------------------------------------------------------------------------
    13.  
    14. loc_EDA:
    15.     dma68kToVDP Underwater_palette,$0000,palette_line_size*4,CRAM
    16.  
    17. loc_EFE:
    18.     dma68kToVDP Sprite_Table,VRAM_Sprite_Attribute_Table,VRAM_Sprite_Attribute_Table_Size,VRAM
    19.     dma68kToVDP Horiz_Scroll_Buf,VRAM_Horiz_Scroll_Table,VRAM_Horiz_Scroll_Table_Size,VRAM
    20.  
    21.     bsr.w    sndDriverInput
    22.  
    23.     startZ80
    24.  
    25.     rts
    26.  
    And at last, we have to remove an instance of Hint_flag in function V_Int before the switch table:
    Code (ASM):
    1.  
    2.     move.b    (Vint_routine).w,d0
    3.     move.b    #VintID_Lag,(Vint_routine).w
    4. ;    move.w    #1,(Hint_flag).w ; ==> Remove this line
    5.     andi.w    #$3E,d0
    6.     move.w    Vint_SwitchTbl(pc,d0.w),d0
    7.     jsr    Vint_SwitchTbl(pc,d0.w)
    8.  

    7. Fix the water surface objects in the 2P game

    The water surface objects need an update to work properly.

    First of all, we need to create new water surface objects for Tails, that are distinct from Sonic. The water surface is actually 2 objects combined. Both have there reserved RAM slots in the Reserved_Object_RAM zone. I suggest to take 2 additional free RAM slots in the Reserved_Object_RAM zone:
    Code (ASM):
    1.  
    2.                 ; Reserved object RAM, free slots
    3.                 ds.b    object_size
    4.                 ds.b    object_size
    5.                 ds.b    object_size
    6. WaterSurface1_2P:        ; First water surface for 2P
    7.                 ds.b    object_size
    8. WaterSurface2_2P:        ; Second water surface for 2P
    9.                 ds.b    object_size
    10. CPZPylon:            ; Pylon in the foreground in CPZ
    11.                 ds.b    object_size
    12. WaterSurface1:            ; First water surface
    13. Oil:                ; Oil at the bottom of OOZ
    14.                 ds.b    object_size
    15. WaterSurface2:            ; Second water surface
    16.                 ds.b    object_size
    17.  
    Then, we need to actually create the objects at the level initialisation:
    Code (ASM):
    1.  
    2. ; Level_ChkWater:
    3.     tst.b    (Water_flag).w    ; does level have water?
    4.     beq.s    +    ; if not, branch
    5.     move.b    #ObjID_WaterSurface,(WaterSurface1+id).w ; load Obj04 (water surface) at $FFFFB380
    6.     move.w    #$60,(WaterSurface1+x_pos).w ; set horizontal offset
    7.     move.b    #ObjID_WaterSurface,(WaterSurface2+id).w ; load Obj04 (water surface) at $FFFFB3C0
    8.     move.w    #$120,(WaterSurface2+x_pos).w ; set different horizontal offset
    9.     tst.w    (Two_player_mode).w
    10.     beq.s    +
    11.     move.b    #ObjID_WaterSurface,(WaterSurface1_2P+id).w
    12.     move.w    #$60,(WaterSurface1_2P+x_pos).w
    13.     move.b    #ObjID_WaterSurface,(WaterSurface2_2P+id).w
    14.     move.w    #$120,(WaterSurface2_2P+x_pos).w
    15. +
    16.  
    To avoid displaying the water surface of the other player, we need to hack the BuildSprites_P1_ObjLoop function a little. I am not fond of this modification, but this works well:
    Code (ASM):
    1.  
    2. BuildSprites_P1_ObjLoop:
    3.     movea.w    (a4,d6.w),a0 ; a0=object
    4.  
    5.     ; These is a sanity check, to detect invalid objects which should not
    6.     ; have been queued for display. S3K gets rid of this, since it
    7.     ; should not be needed and it just slows this code down.
    8.     tst.b    id(a0)
    9.     beq.w    BuildSprites_P1_NextObj
    10.  
    11.     ; Do not display water surface meant for the other player
    12.     cmpa.w    #WaterSurface1_2P,a0
    13.     beq.w    BuildSprites_P1_NextObj
    14.     cmpa.w    #WaterSurface2_2P,a0
    15.     beq.w    BuildSprites_P1_NextObj
    16.  
    Same thing for the BuildSprites_P2_ObjLoop function:
    Code (ASM):
    1.  
    2. BuildSprites_P2_ObjLoop:
    3.     movea.w    (a4,d6.w),a0 ; a0=object
    4.  
    5.     ; These is a sanity check, to detect invalid objects which should not
    6.     ; have been queued for display. S3K gets rid of this, since it
    7.     ; should not be needed and it just slows this code down.
    8.     tst.b    id(a0)
    9.     beq.w    BuildSprites_P2_NextObj
    10.  
    11.     ; Do not display water surface meant for the other player
    12.     cmpa.w    #WaterSurface1,a0
    13.     beq.w    BuildSprites_P2_NextObj
    14.     cmpa.w    #WaterSurface2,a0
    15.     beq.w    BuildSprites_P2_NextObj
    16.  
    At last, we need to properly update the water surface objects X pos in the UpdateWaterSurface function. Replace the old function with this new one:
    Code (ASM):
    1.  
    2. UpdateWaterSurface:
    3.     tst.b    (Water_flag).w
    4.     beq.s    ++
    5.  
    6.     tst.w    (Two_player_mode).w
    7.     bne.s    UpdateWaterSurface_2P
    8.  
    9. ;UpdateWaterSurface_1P:
    10.     move.w    (Camera_X_pos).w,d1
    11.  
    12.     if fixBugs
    13.     ; This function can cause the water surface's to be cut off at the
    14.     ; left when the game is paused. This is because this function pushes
    15.     ; the water surface sprite to the right every frame. To fix this,
    16.     ; just avoid pushing the sprite to the right when the game is about
    17.     ; to be paused.
    18.     move.b    (Ctrl_1_Press).w,d0 ; is Start button pressed?
    19.     or.b    (Ctrl_2_Press).w,d0 ; (either player)
    20.     andi.b    #button_start_mask,d0
    21.     bne.s    +
    22.     endif
    23.  
    24.     ; Every other frame, shift water surface by $20 pixels to the right
    25.     btst    #0,(Level_frame_counter+1).w
    26.     beq.s    +
    27.     addi.w    #$20,d1
    28.  
    29. +    ; Match obj x-position to screen position
    30.     move.w    d1,d0
    31.     addi.w    #$60,d0
    32.     move.w    d0,(WaterSurface1_x_pos).w
    33.     addi.w    #$120,d1
    34.     move.w    d1,(WaterSurface2_x_pos).w
    35. +
    36.     rts
    37. ; ---------------------------------------------------------------------------
    38.  
    39. UpdateWaterSurface_2P:
    40.     move.w    (Camera_X_pos).w,d1
    41.     move.w    (Camera_X_pos_P2).w,d2
    42.  
    43.     if fixBugs
    44.     ; This function can cause the water surface's to be cut off at the
    45.     ; left when the game is paused. This is because this function pushes
    46.     ; the water surface sprite to the right every frame. To fix this,
    47.     ; just avoid pushing the sprite to the right when the game is about
    48.     ; to be paused.
    49.     move.b    (Ctrl_1_Press).w,d0 ; is Start button pressed?
    50.     or.b    (Ctrl_2_Press).w,d0 ; (either player)
    51.     andi.b    #button_start_mask,d0
    52.     bne.s    +
    53.     endif
    54.  
    55.     ; Every other frame, shift water surface by $20 pixels to the right
    56.     btst    #0,(Level_frame_counter+1).w
    57.     beq.s    +
    58.     addi.w    #$20,d1
    59.     addi.w    #$20,d2
    60.  
    61. +    ; Match obj x-position to screen position
    62.     move.w    d1,d0
    63.     addi.w    #$60,d0
    64.     move.w    d0,(WaterSurface1_x_pos).w
    65.     addi.w    #$120,d1
    66.     move.w    d1,(WaterSurface2_x_pos).w
    67.     move.w    d2,d0
    68.     addi.w    #$60,d0
    69.     move.w    d0,(WaterSurface1_x_pos_2P).w
    70.     addi.w    #$120,d2
    71.     move.w    d2,(WaterSurface2_x_pos_2P).w
    72.     rts
    73.  
    If you want the graphics of the water surface to show properly, add a request to the PlrList of your level:
    Code (ASM):
    1.  
    2.     plreq ArtTile_ArtNem_WaterSurface, ArtNem_WaterSurface
    3.  

    8. Update the level initialisation and other last fixes

    Nothing will work unless you remove this defensive programming line here:
    Code (ASM):
    1.  
    2. Level_InitWater:
    3.     move.b    #1,(Water_flag).w
    4. ;    move.w    #0,(Two_player_mode).w ; ==> Remove this line
    5.  
    You also have to change what is performed at the level initialisation so that the title card display (about) properly:
    Code (ASM):
    1.  
    2. Level_InitWater:
    3.     move.b    #1,(Water_flag).w
    4. +
    5.     lea    (VDP_control_port).l,a6
    6.     move.w    #$8B03,(a6)        ; EXT-INT disabled, V scroll by screen, H scroll by line
    7.     move.w    #$8200|(VRAM_Plane_A_Name_Table/$400),(a6)    ; PNT A base: $C000
    8.     move.w    #$8400|(VRAM_Plane_B_Name_Table/$2000),(a6)    ; PNT B base: $E000
    9.     move.w    #$8500|(VRAM_Sprite_Attribute_Table/$200),(a6)    ; Sprite attribute table base: $F800
    10.     move.w    #$9001,(a6)        ; Scroll table size: 64x32
    11.     move.w    #$8004,(a6)        ; H-INT disabled
    12.     move.w    #$8720,(a6)        ; Background palette/color: 2/0
    13.     move.w    #$8C81,(a6)        ; H res 40 cells, no interlace
    14.     tst.b    (Debug_options_flag).w
    15.     beq.s    ++
    16.     btst    #button_C,(Ctrl_1_Held).w
    17.     beq.s    +
    18.     move.w    #$8C89,(a6)    ; H res 40 cells, no interlace, S/H enabled
    19. +
    20.     btst    #button_A,(Ctrl_1_Held).w
    21.     beq.s    +
    22.     move.b    #1,(Debug_mode_flag).w
    23. +
    24.     clr.w    (Hint_flag_precalc).w
    25.     move.b    #223,(Hint_counter_1).w
    26.     move.w    #$8A00+223,(Hint_counter_reserve).w    ; H-INT every 224th scanline
    27.     tst.w    (Two_player_mode).w
    28.     beq.s    +
    29.     move.w    #1,(Hint_flag_precalc).w
    30.     move.b    #107,(Hint_counter_1).w
    31.     move.w    #$8A00+107,(Hint_counter_reserve).w    ; H-INT every 108th scanline
    32.     move.w    #$8014,(a6)            ; H-INT enabled
    33.     move.w    #$8C87,(a6)            ; H res 40 cells, double res interlace
    34. +
    35.     move.w    (Hint_counter_reserve).w,(a6)
    36.  
    At last, perform a small fix on the Hint_flag variable in the BuildSprites_P2 function:
    Code (ASM):
    1.  
    2. BuildSprites_P2:
    3.     if fixBugs
    4.     ; Like in Sonic 3, the sprite tables are page-flipped in two-player mode.
    5.     ; This fixes a race-condition where incomplete sprite tables can be uploaded
    6.     ; to the VDP on lag frames, causing corrupted sprites to appear.
    7.  
    8.     ; Modify the back buffer.
    9.     lea    (Sprite_Table_P2).w,a2
    10.     tst.b    (Current_sprite_table_page).w
    11.     beq.s    +
    12.     lea    (Sprite_Table_P2_Alternate).w,a2
    13. +
    14.     else
    15.     tst.b    (Hint_flag+1).w ; has H-int occured yet?
    16.     bne.s    BuildSprites_P2    ; if not, wait
    17.     lea    (Sprite_Table_P2).w,a2
    18.     endif
    19.  
    Congratulations! You reached the end of this tutorial.

    You now have a fully functional water in the 2P game! Go check CPZ2 or ARZ in 2P mode, and see the magic.
     
    • Informative Informative x 6
    • List
  14. E-122-Psi

    E-122-Psi

    Member
    2,531
    728
    93
    AMAZING, I'm so glad you're adding more to the VS mode expansion, especially something previously considered impossible in vanilla Sonic 2.

    I'll link this in the other tutorials. :D

    The current CPZ tutorial is kinda crammed in terms of VRAM space, but with the Oil Ocean tutorial I may add a trick to free a couple lines that would likely be handy utilising for the water object art.

    Also minor note, but I appreciate you listing a potential free space to add RAM variables. So few tutorials do that.
     
    Last edited: Dec 22, 2024