don't click here

Some changes and fixes for Sonic 2

Discussion in 'Engineering & Reverse Engineering' started by Esrael, Jun 7, 2012.

  1. Esrael

    Esrael

    Neto Tech Member
    304
    257
    63
    Brazil, São Paulo, Guarulhos
    Neto Assembler Editor / Sonic 2 Delta / Neto MD-DOS
    Edit: An user in the discord pointed me that the previous changes break some collisions.
    An extra check is necessary to avoid this, but instead of increasing the processing, a
    table will be used to increase the speed.

    "The 45 to 0 degree bug".
    Why can´t CPz have loops with slanted corners (You can't go from 45 to 0 degrees)?
    Did you noticed that if you try to walk in the right side of the slanted corner the collision will behave wonk.
    Why this occur? Goto to routine Sonic_Angle and look in the code of loc_1E380:
    Code (Text):
    1.  
    2. loc_1E380:    
    3.    move.b   angle(a0),d2
    4.    addi.b   #$20,d2
    5.    andi.b   #$C0,d2
    6.    move.b   d2,angle(a0)
    7.    rts
    8.  
    The issue is caused by the line "addi.b #$20,d2".
    If you are at 135 degree the value in D2 is $E0 and adding $20 will result in 0, but if you are at 45 degree the D2 have the value $20 and adding $20 will result in $40, that are incorrect.

    How to fix?

    replace the code after loc_1E380: with this
    Code (Text):
    1.  
    2. loc_1E380:
    3.                 moveq   #$00, D2
    4.                 move.b  Obj_Angle(A0), D2
    5.                 move.b  Next_Angle_Table_Data(PC, D2), Obj_Angle(A0)          
    6.                 rts
    7. ;-------------------------------------------------------------------------------
    8. Next_Angle_Table_Data:              
    9.                 dc.b    $00, $00, $00, $00, $00, $00, $00, $00
    10.                 dc.b    $00, $00, $00, $00, $00, $00, $00, $00
    11.              
    12.                 dc.b    $00, $00, $00, $00, $00, $00, $00, $00
    13.                 dc.b    $00, $00, $00, $00, $00, $00, $00, $00
    14.  
    15.                 dc.b    $00, $40, $40, $40, $40, $40, $40, $40
    16.                 dc.b    $40, $40, $40, $40, $40, $40, $40, $40
    17.              
    18.                 dc.b    $40, $40, $40, $40, $40, $40, $40, $40
    19.                 dc.b    $40, $40, $40, $40, $40, $40, $40, $40
    20.              
    21.                 dc.b    $40, $40, $40, $40, $40, $40, $40, $40
    22.                 dc.b    $40, $40, $40, $40, $40, $40, $40, $40
    23.              
    24.                 dc.b    $40, $40, $40, $40, $40, $40, $40, $40
    25.                 dc.b    $40, $40, $40, $40, $40, $40, $40, $40
    26.              
    27.                 dc.b    $80, $80, $80, $80, $80, $80, $80, $80
    28.                 dc.b    $80, $80, $80, $80, $80, $80, $80, $80
    29.              
    30.                 dc.b    $80, $80, $80, $80, $80, $80, $80, $80
    31.                 dc.b    $80, $80, $80, $80, $80, $80, $80, $80
    32.              
    33.                 dc.b    $80, $80, $80, $80, $80, $80, $80, $80
    34.                 dc.b    $80, $80, $80, $80, $80, $80, $80, $80
    35.              
    36.                 dc.b    $80, $80, $80, $80, $80, $80, $80, $80
    37.                 dc.b    $80, $80, $80, $80, $80, $80, $80, $80
    38.              
    39.                 dc.b    $80, $C0, $C0, $C0, $C0, $C0, $C0, $C0
    40.                 dc.b    $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0
    41.              
    42.                 dc.b    $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0
    43.                 dc.b    $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0
    44.              
    45.                 dc.b    $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0
    46.                 dc.b    $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0
    47.              
    48.                 dc.b    $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0
    49.                 dc.b    $C0, $C0, $C0, $C0, $C0, $C0, $C0, $C0
    50.              
    51.                 dc.b    $00, $00, $00, $00, $00, $00, $00, $00
    52.                 dc.b    $00, $00, $00, $00, $00, $00, $00, $00
    53.              
    54.                 dc.b    $00, $00, $00, $00, $00, $00, $00, $00
    55.                 dc.b    $00, $00, $00, $00, $00, $00, $00, $00
    56. ;-------------------------------------------------------------------------------
    57.  
    Build and now you can have slopes with 45 to 0 degree in both directions.


    And if you are using Sonic and Knuckles like Sonic Delta 40, the code is at $ED7A
     
    Last edited: Sep 14, 2023
    • Like Like x 3
    • Informative Informative x 1
    • List
  2. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,249
    1,420
    93
    your mom
    There is a possible slight oversight with how the push sensor is positioned. I wrote about it for Sonic 1 here.
    I will add that Tails in general is more suspect to this oversight, because his natural and rolling hitbox heights are almost the same, and thus the threshold for him is always quite low.

    [​IMG]

    In s2.asm, go to loc_1EBE6, and add this right under addq.w #8,d2:
    Code (Text):
    1.         btst    #2,status(a0)          ; Is the Player rolling?
    2.         bne.s   +                      ; If not, branch
    3.         cmpi.b  #ObjID_Tails,id(a0)    ; Is this Tails?
    4.         bne.s   ++                     ; If not, branch
    5.         addq.w  #1,d2                  ; Offset push sensor movement when not rolling
    6. +
    7.         subq.w  #5,d2                  ; Move the push sensor up

    Then, change the "+" in this to a "++":
    Code (Text):
    1.       andi.b  #$38,d1
    2.       bne.s   +

    And you'll get this:

    [​IMG]

    You can adjust the numbers to your liking if you please. I will also note that if you make it so that the push sensor is enabled for other directions (like S3K does when running straight up/down on a wall or running on a flat ceiling), then modifications to account for that will need to be made.
     
    Last edited: Aug 10, 2023
    • Like Like x 5
    • Informative Informative x 1
    • List
  3. saxman

    saxman

    Oldbie Tech Member
    I haven't had a chance to try this, but from what you describe, this sounds like the reason they may have changed the loops. Nice to know you have a fix for it though. 20+ years later, you're still posting interesting stuff!
     
  4. OrionNavattan

    OrionNavattan

    Tech Member
    166
    164
    43
    Oregon
    A sharp eye while observing the behavior of some badniks in frame advance mode revealed two that are not setting their display width correctly: the Turtloid and the Grabber's head/body. The Turtloid's sprite is $38 pixels across, but the width is set as if it's only $30 pixels wide, causing it to pop in abruptly at the right of the screen:
    Code (ASM):
    1. ; Sonic 2 Git
    2. Obj9A_SubObjData:
    3.    subObjData Obj9A_Obj98_MapUnc_37B62,make_art_tile(ArtTile_ArtNem_Turtloid,0,0),4,5,$18,0 ; that $18 should be $1C!
    4. ; ===========================================================================
    5. ; Sonic 2 Orion
    6. SubData_Turt:
    7.        subobjdata   Map_Turt,tile_Nem_Turtloid,render_rel,5,$30/2,id_col_null ; that $30 should be $38!

    Likewise, the Grabber's widest sprite is $2C pixels wide, but the width is set as if it's $20, causing it to both pop in and disappear prematurely at the edges of the screen:
    Code (ASM):
    1. ; Sonic 2 Git
    2. ObjA7_SubObjData:
    3.    subObjData ObjA7_ObjA8_ObjA9_Obj98_MapUnc_3921A,make_art_tile(ArtTile_ArtNem_Grabber,1,1),4,4,$10,$B ; that $10 should be $16!
    4. ; ===========================================================================
    5. ; Sonic 2 Orion
    6. SubData_Grabber:
    7.        subobjdata Map_Grab,tile_Nem_Grabber+tile_pal2+tile_hi,render_rel,4,$20/2,id_col_8x8 ; that $20 should be $2C!

    Changing the width values to the ones noted above fixes both of these bugs, however small they may be.
     
    • Like Like x 4
    • Informative Informative x 1
    • List
  5. Esrael

    Esrael

    Neto Tech Member
    304
    257
    63
    Brazil, São Paulo, Guarulhos
    Neto Assembler Editor / Sonic 2 Delta / Neto MD-DOS
    Last edited: Sep 14, 2023
  6. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,249
    1,420
    93
    your mom
    Avoiding CalcAngle When Performing Collision in the Air

    The way Sonic handles collision while he is airborne involves calculating the general direction that he is moving, and then jumping to the appropriate code for handling collision for that specific direction. If he is moving downwards, he would check collision with the walls and the floor. If he is moving left or right, he would check collision with the wall he is moving towards and the ceiling and floor. If he is moving upwards, he would check collision with the walls and ceiling. The way it checks that direction is to take the angle he is moving at by using CalcAngle with his X and Y speeds as the parameters, and the output angle value determines the direction he is moving.

    [​IMG]

    That works and all, but we can definitely use a less intensive calculation to handle this.

    What we can do is actually just directly check the speed values ourselves. What we can then deduce from this is that we can basically determine Sonic's direction depending on if he is moving more horizontally than vertically and the direction of the larger speed value. We know this, because the angle values that separate the different directions are all angles where the absolute values of X and Y are the same.

    Here is a chart showing the new logic with the conditions that should be checked:

    [​IMG]

    Here's an example implementation.

    Replace:
    Code (Text):
    1. Sonic_DoLevelCollision:
    2.         move.l  #Primary_Collision,(Collision_addr).w
    3.         cmpi.b  #$C,top_solid_bit(a0)
    4.         beq.s   +
    5.         move.l  #Secondary_Collision,(Collision_addr).w
    6. +
    7.         move.b  lrb_solid_bit(a0),d5
    8.         move.w  x_vel(a0),d1
    9.         move.w  y_vel(a0),d2
    10.         jsr     (CalcAngle).l
    11.         subi.b  #$20,d0
    12.         andi.b  #$C0,d0
    13.         cmpi.b  #$40,d0
    14.         beq.w   Sonic_HitLeftWall
    15.         cmpi.b  #$80,d0
    16.         beq.w   Sonic_HitCeilingAndWalls
    17.         cmpi.b  #$C0,d0
    18.         beq.w   Sonic_HitRightWall

    with:
    Code (Text):
    1. Sonic_DoLevelCollision:
    2.         move.l  #Primary_Collision,(Collision_addr).w
    3.         cmpi.b  #$C,top_solid_bit(a0)
    4.         beq.s   +
    5.         move.l  #Secondary_Collision,(Collision_addr).w
    6. +
    7.         move.b  lrb_solid_bit(a0),d5
    8.  
    9.         move.w  x_vel(a0),d0                            ; Get X speed
    10.         move.w  y_vel(a0),d1                            ; Get Y speed
    11.         bpl.s   SonAirCol_PosY                          ; If it's positive, branch
    12.  
    13.         cmp.w   d0,d1                                   ; Are we moving towards the left?
    14.         bgt.w   Sonic_HitLeftWall                       ; If so, branch
    15.  
    16.         neg.w   d0                                      ; Are we moving towards the right?
    17.         cmp.w   d0,d1
    18.         bge.w   Sonic_HitRightWall                      ; If so, branch
    19.  
    20.         bra.w   Sonic_HitCeilingAndWalls                ; We are moving upwards
    21.  
    22. SonAirCol_PosY:
    23.         cmp.w   d0,d1                                   ; Are we moving towards the right?
    24.         blt.w   Sonic_HitRightWall                      ; If so, branch
    25.  
    26.         neg.w   d0                                      ; Are we moving towards the left?
    27.         cmp.w   d0,d1
    28.         ble.w   Sonic_HitLeftWall                       ; If so, branch

    Also do this for Tails_DoLevelCollision.
     
    • Like Like x 4
    • Agree Agree x 1
    • List
  7. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,249
    1,420
    93
    your mom
    Full Position Alignment With Block Collision

    The way stage block collision works is that a function is called to determine the distance away or inside a block. Typically, if an object is inside an block, their speed is set to 0 and their position is aligned against the block. However, only the integer half of the position value tends to be affected, but not the fraction half.

    For example, let's say your Y position is 38.75, and you are inside a block 6 pixels deep downwards. The alignment would subtract your Y position by 6 pixels upwards, and your final Y position would end up being 32.75. In most cases, not clearing out the fraction part doesn't really seem to affect anything, but can still creep in some edge cases when it comes to movement on the ground, and mess up the collision.

    So, if you so desire to do a full alignment where the fraction part is cleared out (in the previous example, the final Y position ends up just being 32), it's rather easy: just search for the calls for the various collision detection functions, and in the part where the position value is aligned, also clear out the fraction part.

    For example:
    Code (Text):
    1. Sonic_HitRightWall:
    2.         bsr.w   CheckRightWallDist
    3.         tst.w   d1
    4.         bpl.s   Sonic_HitCeiling2
    5.         add.w   d1,x_pos(a0)
    6.         clr.w   x_pos+2(a0)             ; <-- Add this
    7.         move.w  #0,x_vel(a0) ; stop Sonic since he hit a wall
    8.         move.w  y_vel(a0),inertia(a0)
    9.         rts

    The functions to look for are Sonic_CheckFloor, CheckRightWallDist, Sonic_CheckCeiling, CheckLeftWallDist, ChkFloorEdge, ChkFloorEdge_Part2, ChkFloorEdge2, ObjCheckFloorDist, FireCheckFloorDist, RingCheckFloorDist, ObjCheckRightWallDist, ObjCheckCeilingDist, ObjCheckLeftWallDist, and CalcRoomInFront. Also FindFloor and FindWall calls throughout AnglePos.

    Also, it might be worth capping Sonic's top ground speed at something like 15.75 ($FC0) in both directions and replace the rolling speed cap (that is only done to the X speed) to prevent further collision errors.
     
    Last edited: Dec 22, 2023
  8. GoldS

    GoldS

    Oldbie
    239
    50
    28
    USA
    Metropolis Zone Art Fixes
    No real programming changes here, just a couple of fixes to Metropolis Zone's graphics.

    The first: The lava bubble graphic, which uses the first Metropolis Zone FG/BG palette, mistakenly uses color index F, which is used by the color-cycling lights in the background:
    [​IMG]
    This has been a problem since the Simon Wai prototype, but since the bottom of the lava bubble is partially obscured by the lava itself, it's a bit hard to see. We can tell from the recently shared digitizer printouts that this color is supposed to be #0008. Fortunately, that color is already included in the first Metropolis Zone FG/BG palette at color index 8, so we can just redraw the sprite, like so:
    [​IMG]
    To fix this, either replace the "Lava bubble from MTZ.nem" art file with the one included in the attached ZIP, or change the art yourself by changing all instances of color index F in the sprite with color index 8.

    The second: One of the colors in MTZ's second BG/FG palette was changed after the August 21st prototype, seemingly in error. This only affects a few of the zone's graphics...
    [​IMG]
    ...which is likely why no one noticed it. The orange color in those graphics was originally a dark red, as seen below:
    [​IMG]
    To fix this, either replace the MTZ.bin palette file with the one found in the attached ZIP file, or search for the following hex string in either MTZ.bin or whatever ROM you're editing...
    Code (Text):
    1. 00 20 00 4E 00 0E
    ...and replace it with this:
    Code (Text):
    1. 00 20 00 08 00 0E
     

    Attached Files:

    Last edited: Dec 23, 2023
    • Like Like x 8
    • Informative Informative x 4
    • List
  9. OrionNavattan

    OrionNavattan

    Tech Member
    166
    164
    43
    Oregon
    Deprecated, see revised fix here.
    Fixing Wing Fortress Zone's Clouds


    This has been fixed in at least one hack (Sonic Classic Heroes), but I don't think a fix for this has been shared publicly.

    The clouds in Wing Fortress Zone have a slight flaw as noted by a comment in the Git disassembly: the scroll values are based solely on the cloud speeds and don't take into account the position of the background, causing them to slow down when the camera moves right and speed up when it moves left. The fix for this couldn't be simpler however: we just need to add the background's x pos to the scroll values before writing them to the HScroll buffer (which is essentially what Sonic 1 Revision 1 does with the scrolling clouds in GHZ).

    Near the end of SwScrl_WFZ/Deform_WFZ, there are two instances of this:

    Code (ASM):
    1. ; Sonic 2 GitHub
    2.   move.w   (a2,d3.w),d0       ; Fetch scroll value for this row
    3.   neg.w   d0           ; ...and flip sign for VDP
    4. ; ======================================
    5. ; Sonic 2 ASM68K
    6.     move.w   (a2,d3.w),d0               ; d0 = base scroll value for this segment
    7.     neg.w   d0                   ; negate to make bg scroll value

    Change both of them to this:

    Code (ASM):
    1. ; Sonic 2 GitHub
    2.    move.w   (a2,d3.w),d0       ; Fetch scroll value for this row
    3.    cmpi.b   #8,d3                   ; clouds use indices 8, $A, and $10
    4.    bcs.s   +              ; branch if this row isn't clouds
    5.    add.w   (Camera_BG_X_pos).w,d0           ; add bg x pos so clouds scroll correctly
    6.  
    7. +  neg.w   d0           ; ...and flip sign for VDP
    8. ; ======================================
    9. ; Sonic 2 ASM68K
    10.      move.w   (a2,d3.w),d0               ; d0 = base scroll value for this segment
    11.      cmpi.b   #8,d3                   ; clouds use indices 8, $A, and $10
    12.      bcs.s   .notclouds               ; branch if this segment isn't clouds
    13.      add.w   (v_bg1_x_pos).w,d0           ; add bg x pos so clouds scroll correctly
    14.  
    15.   .notclouds:
    16.      neg.w   d0                   ; negate to make bg scroll value
    (The second instance of .notclouds will need to be changed to something else in Sonic 2 ASM68K, though this should not be an issue as the fix is already included in a bugfix conditional.)
     
    Last edited: Feb 4, 2024
  10. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,249
    1,420
    93
    your mom
    Here's a fun little oddity. The rolling speed cap only applies horizontally, but not vertically. If you don't want that, and instead would rather have it be consistent with the regular ground movement speed cap, just go to Sonic_RollSpeed, and change:
    Code (Text):
    1.         move.b  angle(a0),d0
    2.         jsr     (CalcSine).l
    3.         muls.w  inertia(a0),d0
    4.         asr.l   #8,d0
    5.         move.w  d0,y_vel(a0)    ; set y velocity based on $14 and angle
    6.         muls.w  inertia(a0),d1
    7.         asr.l   #8,d1
    8.         cmpi.w  #$1000,d1
    9.         ble.s   +
    10.         move.w  #$1000,d1       ; limit Sonic's speed rolling right
    11. +
    12.         cmpi.w  #-$1000,d1
    13.         bge.s   +
    14.         move.w  #-$1000,d1      ; limit Sonic's speed rolling left
    15. +
    16.         move.w  d1,x_vel(a0)    ; set x velocity based on $14 and angle

    to:
    Code (Text):
    1.         move.b  angle(a0),d0
    2.         jsr     (CalcSine).l
    3.         move.w  inertia(a0),d2
    4.         cmpi.w  #$1000,d2
    5.         ble.s   +
    6.         move.w  #$1000,d2       ; limit Sonic's speed rolling
    7. +
    8.         cmpi.w  #-$1000,d2
    9.         bge.s   +
    10.         move.w  #-$1000,d2      ; limit Sonic's speed rolling
    11. +
    12.         muls.w  d2,d0
    13.         asr.l   #8,d0
    14.         move.w  d0,y_vel(a0)    ; set y velocity based on $14 and angle
    15.         muls.w  d2,d1
    16.         asr.l   #8,d1
    17.         move.w  d1,x_vel(a0)    ; set x velocity based on $14 and angle
     
    • Like Like x 4
    • Informative Informative x 1
    • List
  11. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,249
    1,420
    93
    your mom
    Properly Removing the Roll Jump Movement Lock

    So, some of you of course know about how when you jump after rolling, your movement is locked. Removing that lock is as simple as removing this line that's in Sonic_Jump:
    Code (Text):
    1.     bset    #4,status(a0)   ; set the rolling+jumping flag

    However, there's one issue that needs to be addressed. You see, when you hold left or right in the air when your control isn't locked, there's a speed cap that gets applied. When the lock is active, moving left or right is prevented, which in turn, prevents that speed cap from kicking in. With that, you can jump and keep your momentum, if you are rolling down a hill, for instance:

    [​IMG]

    But, when we remove the lock and allow the speed cap to kick in...

    [​IMG] [​IMG]

    Yeaaaah, that's not ideal. Luckily, there's already a guide that makes it so that if you are moving faster than the maximum speed, it doesn't cap it (referred to as "removing the speed cap"). Apply those changes, and you'll be able to maintain your momentum and also be able to control yourself in the air.
     
    Last edited: Jan 25, 2024
    • Like Like x 3
    • Informative Informative x 1
    • List
  12. OrionNavattan

    OrionNavattan

    Tech Member
    166
    164
    43
    Oregon
    Fixing Wing Fortress Zone's Clouds, Redux

    Some feedback from djohe on the Blastem Discord led me to completely rewrite the fix for the issue in this post. As a refresher, the clouds in Wing Fortress Zone have a slight flaw as noted by a comment in the Git disassembly: the code increments a longword accumulator for each cloud speed and then uses the high word as the scroll value, not accounting for the background position at all. This causes the clouds to slow down when the camera moves right and speed up when it moves left. The fix for this is fairly straightforward: we need to add the background's x pos to the scroll values, similar to what Sonic 1 Revision 1 does with the scrolling clouds in GHZ. However, as djhoe pointed out to me, simply adding the bg x pos to the scroll values causes the clouds to move at nearly the same speed as the foreground, which doesn't really line up with the notion of "distant background clouds". At the suggestion of Sik on the Blastem Discord, dividing the x pos by 4 before adding it to the scroll values produces far more reasonable results.

    My original fix worked by checking the index value pulled from the scroll arrays (SwScrl_WFZ_Transition/Normal_Array), and adding the bg x pos to it if it was one of the cloud indices (8 or higher). This revised fix is a bit more sophisticated: we're going to seperate the cloud accumulators from the scroll values and calculate pre-adjusted values to write to the buffer, allowing us to write the scroll values without a time-comsuming index check. Starting with unmodified code, find this block in Swscrl_WFZ/Deform_WFZ:
    Code (ASM):
    1.    ; Sonic 2 GitHub
    2.    lea   (TempArray_LayerDef).w,a2
    3.    move.l   d0,(a2)+               ; Static parts of BG (generally no clouds in them)
    4.    move.l   d1,(a2)+               ; Eggman's getaway ship
    5.    ; Note: this is bugged: this tallies only the cloud speeds. It works fine
    6.    ; if you are standing still, but makes the clouds move faster when going
    7.    ; right and slower when going left. This is exactly the opposite of what
    8.    ; should happen.
    9.    addi.l   #$8000,(a2)+           ; Larger clouds
    10.    addi.l   #$4000,(a2)+           ; Medium clouds
    11.    addi.l   #$2000,(a2)+           ; Small clouds
    12. ; ======================================
    13.        ; Sonic 2 ASM68K
    14.        lea   (v_bgscroll_buffer).w,a2
    15.        move.l   d0,(a2)+               ; static sections of background (empty sky with no clouds)
    16.        move.l   d1,(a2)+               ; Eggman's getaway ship
    17.        addi.l   #$8000,(a2)+               ; large clouds
    18.        addi.l   #$4000,(a2)+               ; accumulator for medium clouds
    19.        addi.l   #$2000,(a2)+               ; accumulator for small clouds

    ...and replace it with the following:
    Code (ASM):
    1.     ; Sonic 2 GitHub
    2.    lea   (TempArray_LayerDef+$14).w,a2       ; a2 = cloud accumulators (located after scroll values)
    3.    movea.l   a2,a1
    4.    ; Increment the cloud accumulators.
    5.    addi.l   #$8000,(a2)+               ; Accumulator for large clouds
    6.    addi.l   #$4000,(a2)+               ; Accumulator for medium clouds
    7.    addi.l   #$2000,(a2)+               ; Accumulator for small clouds
    8.  
    9.    lea   (TempArray_LayerDef).w,a2
    10.    move.l   d0,(a2)+               ; Static parts of BG (generally no clouds in them)
    11.    move.l   d1,(a2)+               ; Eggman's getaway ship
    12.  
    13.    swap   d0                   ; Swap bg x pos to low word
    14.    lsr.w   #2,d0                   ; d0 = bg x pos / 4
    15.  
    16.    rept 2
    17.    move.w   (a1)+,d1               ; Get high word of accumulator
    18.    add.w   d0,d1                   ; Add bg x pos / 4
    19.    move.w   d1,(a2)+               ; Write to scroll buffer (large and medium clouds)
    20.    addq.w   #2,a1                   ; Skip unused low word
    21.    addq.w   #2,a2
    22.    endm
    23.  
    24.    move.w   (a1),d1                   ; Get high word of accumulator
    25.    add.w   d0,d1                   ; Add bg x pos / 4
    26.    move.w   d1,(a2)                   ; Write to scroll buffer (small clouds)
    27.  
    28. ; ======================================
    29.        ; Sonic 2 ASM68K
    30.        lea   (v_bgscroll_buffer+$14).w,a2       ; a2 = cloud accumulators (located after scroll values)
    31.        movea.l   a2,a1
    32.        addi.l   #$8000,(a2)+               ; accumulator for large clouds
    33.        addi.l   #$4000,(a2)+               ; accumulator for medium clouds
    34.        addi.l   #$2000,(a2)+               ; accumulator for small clouds
    35.  
    36.        lea   (v_bgscroll_buffer).w,a2
    37.        move.l   d0,(a2)+               ; static sections of background (empty sky with no clouds)
    38.        move.l   d1,(a2)+               ; Eggman's getaway ship
    39.  
    40.        swap   d0                   ; swap bg x pos to low word
    41.        lsr.w   #2,d0                   ; d0 = bg x pos / 4
    42.  
    43.        rept 2
    44.        move.w   (a1)+,d1               ; get high word of accumulator
    45.        add.w   d0,d1                   ; add bg x pos / 4
    46.        move.w   d1,(a2)+               ; write to scroll buffer (large and medium clouds)
    47.        addq.w   #2,a1                   ; skip unused low word
    48.        addq.w   #2,a2
    49.        endr
    50.  
    51.        move.w   (a1),d1                   ; get high word of accumulator
    52.        add.w   d0,d1                   ; add bg x pos / 4
    53.        move.w   d1,(a2)                   ; write to scroll buffer (small clouds)
    54.  

    EDIT: djohe gave me permission to share his own solution to this problem, which is even better. Replace the three addi.l #xxxx,(a2)+ instructions with the following:
    Code (ASM):
    1.    ; Sonic 2 GitHub
    2.    moveq   #0,d5
    3.    move.w   (Camera_X_pos_diff).w,d5    ; get camera x-diff
    4.    ext.l   d5
    5.    asl.l   #5,d5                
    6.    add.l   d5,(a2)                   ; add to high word of accumulator
    7.    addi.l   #$8000,(a2)+        ; Larger clouds    
    8.    add.l   d5,(a2)
    9.    addi.l   #$4000,(a2)+        ; Medium clouds
    10.    add.l   d5,(a2)
    11.    addi.l   #$2000,(a2)+        ; Small clouds
    12.  
    13. ; ======================================
    14.        ; Sonic 2 ASM68K
    15.        moveq   #0,d5
    16.        move.w   (v_camera_x_diff).w,d5    ; get camera x-diff
    17.        ext.l   d5
    18.        asl.l   #5,d5                
    19.        add.l   d5,(a2)                   ; add to high word of accumulator
    20.        addi.l   #$8000,(a2)+        ; large clouds
    21.        add.l   d5,(a2)
    22.        addi.l   #$4000,(a2)+        ; medium clouds
    23.        add.l   d5,(a2)
    24.        addi.l   #$2000,(a2)+        ; small clouds
     
    Last edited: Feb 9, 2024
  13. MoDule

    MoDule

    Tech Member
    327
    24
    18
    Procrastinating from writing bug-fix guides
    Metropolis Zone's shellcracker badnik is infamous for its messy collisions. While it's attacking the player, the arm has such a large hitbox that it becomes very difficult to avoid taking damage. A more obscure oddity, pointed out to me by D.A.Garden, is that while not attacking, the claw has no collisions. The following guide addresses both issues.

    Part 1: Removing the arm's hitbox

    The claw and arm segments are initialized with the same data, including the large hitbox. To make the arm harmless, clear the collision_flags during each piece's initialization:
    Code (ASM):
    1. ; loc_38170:
    2. ObjA0_Init:
    3.    bsr.w   LoadSubObject
    4.    movea.w   objoff_2C(a0),a1 ; a1=object
    5.    move.b   render_flags(a1),d0
    6.    andi.b   #1,d0
    7.    or.b   d0,render_flags(a0)
    8.    move.w   objoff_2E(a0),d0
    9.    beq.s   loc_38198
    10.    move.b   #4,mapping_frame(a0)
    11.    addq.w   #6,x_pos(a0)
    12.    addq.w   #6,y_pos(a0)
    13.    ; <--- remove arm's hitbox here
    14.  
    15. loc_38198:
    16.    lsr.w   #1,d0
    17.    move.b   byte_381A4(pc,d0.w),objoff_2A(a0)
    18.    jmpto   MarkObjGone, JmpTo39_MarkObjGone

    with the following instruction:
    Code (ASM):
    1.     move.b    #0,collision_flags(a0)

    With that, the first bug is already fixed. The next bug will take a little more doing.

    Part 2: Making the claw damaging at all times

    While not easy to notice because of how quickly shellcracker attacks, its claw is actually only damaging during its attack. That is because the extending claw is a separate object that is created at the beginning of shellcracker's attack and then deleted after it's done. The rest of the time, the visible claw is baked into shellcracker's sprite frames.

    For the claw to always be damaging, its object needs to be spawned along and kept aligned with the shellcracker object.

    Claw's new idle state

    To make the claw follow the shellcracker object, add an extra state ObjA0_Idle to the end of the claw's secondary state table:
    Code (ASM):
    1. off_381C8:   offsetTable
    2.        offsetTableEntry.w loc_381E0   ; 0
    3.        offsetTableEntry.w loc_3822A   ; 2
    4.        offsetTableEntry.w loc_38244   ; 4
    5.        offsetTableEntry.w loc_38258   ; 6
    6.        offsetTableEntry.w ObjA0_Idle   ; 8   ; <---

    and add its code directly above loc_381E0:
    Code (ASM):
    1. ObjA0_Idle:
    2.    cmpi.b   #3,mapping_frame(a1)   ; is parent using clawless frame?
    3.    beq.s   .startAttack       ; if yes, branch
    4.  
    5.    ; align claw with parent
    6.    move.w   x_pos(a1),x_pos(a0)
    7.    move.w   #-$14,d2
    8.    btst   #0,render_flags(a1)
    9.    beq.s   .offsetPos
    10.    neg.w   d2
    11.  
    12. .offsetPos:
    13.    add.w   d2,x_pos(a0)
    14.    move.w   y_pos(a1),y_pos(a0)
    15.    subi_.w   #8,y_pos(a0)
    16.    addq.w   #4,sp
    17.    jmp   MarkObjGone3
    18. ; ===========================================================================
    19.  
    20. .startAttack:
    21.    move.b   #0,routine_secondary(a0)
    22.    ; fall through to next state
    23. ; ===========================================================================

    Increasing claw's movement delay

    There is a short delay before the claw and arm segments start moving during the attack. Because the arm is already spawned when the attack starts and thus doesn't use up a frame running its Init routine, we need to add a short delay to its movement, or else it will move ahead of the arm segments.

    This is done by changing the first value in the following table:
    Code (ASM):
    1. byte_381A4:
    2.     dc.b   2    ; 0    ; <---
    3.     dc.b   3    ; 1
    4.     dc.b   5    ; 2
    5.     dc.b   7    ; 3
    6.     dc.b   9    ; 4
    7.     dc.b  $B    ; 5
    8.     dc.b  $D    ; 6
    9.     dc.b  $F    ; 7
    10.     even

    Resetting the claw

    After the attack animation is done, instead of deleting the claw, it needs to return to its idle state. This is done here:
    Code (ASM):
    1. loc_38266:
    2.    tst.w   objoff_2E(a0)
    3.    bne.s   loc_3827A
    4.    movea.w   objoff_2C(a0),a1 ; a1=object
    5.    move.b   #0,mapping_frame(a1)
    6.    st.b   objoff_2C(a1)
    7.    ; <--- reset claw's state here
    8.  
    9. loc_3827A:
    10.    addq.w   #4,sp
    11.    bra.w   JmpTo65_DeleteObject

    with the following:
    Code (ASM):
    1.    move.b   #8,routine_secondary(a0)   ; return to idle state
    2.    move.b   #2,objoff_2A(a0)       ; reset wait counter
    3.    addq.w   #4,sp
    4.    jmp   MarkObjGone3

    Spawning the claw and arms separately

    To use the claw for its hitbox, it needs to be spawned during shellcracker's initialization:
    Code (ASM):
    1. ; loc_38022:
    2. Obj9F_Init:
    3.    bsr.w   LoadSubObject
    4.    btst   #0,render_flags(a0)
    5.    beq.s   +
    6.    bset   #0,status(a0)
    7. +
    8.    move.w   #-$40,x_vel(a0)
    9.    move.b   #$C,y_radius(a0)
    10.    move.b   #$18,x_radius(a0)
    11.    move.w   #$140,objoff_2A(a0)
    12.  
    13.    ; <--- spawn claw here
    14.    rts

    Spawn the claw with the following:
    Code (ASM):
    1.    moveq   #0,d1       ; spawn just the claw
    2.    moveq   #0,d6       ; skip loop
    3.    bsr.w   loc_38296
    4.    move.b   #8,routine_secondary(a1) ; => ObjA0_Idle

    Since the claw is already spawned, skip over it when spawning the rest of the arm by adjusting the loop initialization:
    Code (ASM):
    1. loc_38292:
    2.    moveq   #2,d1   ; <--- start with the arm segments
    3.    moveq   #6,d6   ; <--- spawn one fewer object

    That takes care of keeping the claw spawned so that its hitbox is always active. The next section contains changes that are not strictly necessary, but may improve the player's experience.

    Optional: Post-destruction behavior

    Now that the claw is always spawned, it will also always fly off when its parent shellcracker object is destroyed. When not attacking, it will fall straight down. It will also still be able to hurt the player.

    The code that puts the claw and arm segments into their falling state is this:
    Code (ASM):
    1. loc_381D0:
    2.    move.b   #4,routine(a0)
    3.    move.w   #$40,objoff_2A(a0)
    4.    ; <--- edits go here
    5.    jmpto   MarkObjGone, JmpTo39_MarkObjGone

    Removing claw's hitbox

    The claw's hitbox can be removed the same way it was done for the arm segments, by adding to the place indicated above the following line:
    Code (ASM):
    1.    move.b   #0,collision_flags(a0)   ; remove claw's hitbox

    Fixing the claw's and arm's odd fall direction

    When the claw and arm fly off, they keep whatever speed they had during the attack. This can lead to the arm's segments going in different directions.

    This is the most optional fix and can be done in other ways. The following code will make the claw and arm always fly away from the destroyed shellcracker:
    Code (ASM):
    1.    ; set flying away speed
    2.    move.w   #-$200,d2
    3.    btst   #0,render_flags(a0)
    4.    beq.s   .setSpeed
    5.    neg.w   d2
    6.  
    7. .setSpeed:
    8.    move.w   d2,x_vel(a0)

    Again, this is added in the same place in loc_381D0 as indicated above.

    And with that, the shellcracker has gone from a really unfair enemy to just a major nuisance.