don't click here

Some changes and fixes for Sonic 2

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

  1. Clownacy

    Clownacy

    Tech Member
    1,053
    581
    93
    Keep in mind that, despite being based on the then-current Hg disasm, ReadySonic became woefully outdated as Hg evolved into the Git disasm. Guides can be interpreted, and used on any disasm. As for BOOM, I don't wanna use something that's ultimately a disasm from 2007, even if it comes with some extra features.
     
  2. Colton

    Colton

    Member
    9
    0
    0
    Is it possible that someone, somewhere, could at least make a ROM (or make a custom disassembly or something) with all these bugfixes you guys have been making? I just can't seem to find SSHG to find it for the life of me & these bugs frustrate me now that I know about them... :argh:
     
  3. Clownacy

    Clownacy

    Tech Member
    1,053
    581
    93
    A lot of these fixes aren't in the SCHG. Also, wasn't this brought up like two posts ago?
     
  4. Colton

    Colton

    Member
    9
    0
    0
    4 to be exact. Blame it on Knucklez.
     
  5. Clownacy

    Clownacy

    Tech Member
    1,053
    581
    93
    http://www.youtube.com/watch?v=2BWjrd1tWaY

    Thanks to this guy, I learned of another bug: starting the game through the options menu doesn't clear your emerald count, meaning you can soft-reset and replay EHZ over and over, and collect all the emeralds within the first act. If I remember right, I once read of a similar bug, involving emeralds not being cleared in 2-player mode, allowing Sonic to go Super.

    Here's the problematic code:
    Code (ASM):
    1. ; loc_909A:
    2. OptionScreen_Select:
    3.     move.b  (Options_menu_box).w,d0
    4.     bne.s   OptionScreen_Select_Not1P
    5.     ; Start a single player game
    6.     moveq   #0,d0
    7.     move.w  d0,(Two_player_mode).w
    8.     move.w  d0,(Two_player_mode_copy).w
    9.     move.w  d0,(Current_ZoneAndAct).w   ; emerald_hill_zone_act_1
    10.     move.b  #GameModeID_Level,(Game_Mode).w ; => Level (Zone play mode)
    11.     rts
    12.  
    And here's the code that does the same thing, only on the Title Screen:
    Code (ASM):
    1. TitleScreen_Loop:
    2.     ; ...
    3.     moveq   #0,d0
    4.     move.b  (Title_screen_option).w,d0
    5.     bne.s   TitleScreen_CheckIfChose2P  ; branch if not a 1-player game
    6.  
    7.     moveq   #0,d0
    8.     move.w  d0,(Two_player_mode_copy).w
    9.     move.w  d0,(Two_player_mode).w
    10.     if emerald_hill_zone_act_1=0
    11.     move.w  d0,(Current_ZoneAndAct).w ; emerald_hill_zone_act_1
    12.     else
    13.     move.w #emerald_hill_zone_act_1,(Current_ZoneAndAct).w
    14.     endif
    15.     tst.b   (Level_select_flag).w   ; has level select cheat been entered?
    16.     beq.s   +           ; if not, branch
    17.     btst    #button_A,(Ctrl_1_Held).w ; is A held down?
    18.     beq.s   +           ; if not, branch
    19.     move.b  #GameModeID_LevelSelect,(Game_Mode).w ; => LevelSelectMenu
    20.     rts
    Look similar? I imagine Sonic Team copied directly from this code while working on the Options menu. But what's that underneath it?

    Code (ASM):
    1.     tst.b   (Level_select_flag).w   ; has level select cheat been entered?
    2.     beq.s   +           ; if not, branch
    3.     btst    #button_A,(Ctrl_1_Held).w ; is A held down?
    4.     beq.s   +           ; if not, branch
    5.     move.b  #GameModeID_LevelSelect,(Game_Mode).w ; => LevelSelectMenu
    6.     rts
    7. ; ---------------------------------------------------------------------------
    8. +
    9.     move.w  d0,(Current_Special_StageAndAct).w
    10.     move.w  d0,(Got_Emerald).w
    11.     move.l  d0,(Got_Emeralds_array).w
    12.     move.l  d0,(Got_Emeralds_array+4).w
    13.     rts
    Uh oh...

    So, as you can see, it looks like Sonic Team just missed this bit off the end. This also seems to be case with the Level Select, though it's debatable whether that was intentional or not, being a debugging menu, and all.

    The fix is simple: just append those last clears to the Options code, for both 1 Player and 2 Player mode.
     
  6. TheLastWhiteFlame

    TheLastWhiteFlame

    Member
    13
    0
    0
    Earth
    S1 & S2 Restored
    Time for another bug-fix. In sonic 2, if you get invincibility then turn super, you will have stars around you while super. I agree it may look good but it's not intended. It's a simple fix, just add checks for super sonic around obj35, so in "loc_1DA0C" change this
    Code (ASM):
    1.     movea.w parent(a0),a1 ; a1=character
    2.     btst    #status_sec_isInvincible,status_secondary(a1)
    3.     beq.w   DeleteObject
    to this

    Code (ASM):
    1.     movea.w parent(a0),a1 ; a1=character
    2.     btst    #status_sec_isInvincible,status_secondary(a1)
    3.     beq.w   DeleteObject
    4.     tst.b   (Super_Sonic_flag).w
    5.     beq.s   +
    6.     jmp DeleteObject
    7. +
    and then at "loc_1DA80" change this

    Code (ASM):
    1.     movea.w parent(a0),a1 ; a1=character
    2.     btst    #status_sec_isInvincible,status_secondary(a1)
    3.     beq.w   DeleteObject
    to this

    Code (ASM):
    1.     movea.w parent(a0),a1 ; a1=character
    2.     btst    #status_sec_isInvincible,status_secondary(a1)
    3.     beq.w   DeleteObject
    4.     tst.b   (Super_Sonic_flag).w
    5.     beq.s   +
    6.     jmp DeleteObject
    7. +
    And now when you get invincibility, it checks for super sonic thus ridding the bug.
     
  7. Clownacy

    Clownacy

    Tech Member
    1,053
    581
    93
    Since the code above your checks can reach DeleteObject with a word-sized branch, your checks probably can too. Just invert the conditions and branch to DeleteObject instead. Also, S3K places the checks above the code you posted.
     
  8. Clownacy

    Clownacy

    Tech Member
    1,053
    581
    93
    Thanks to a heads-up from djohe, I learned that the multisprite feature is broken in 2-Player mode: X- and Y-flipping don't work. You can see this in MCZ, by looking at a Crawlton's body segments. They should be X-flipped, much like its head, if Sonic is facing its opposite side, but they only do so in single-player.

    To fix this, look at ChkDrawSprite, to see how single-player does it:

    Code (ASM):
    1. ; sub_1680A:
    2. ChkDrawSprite:
    3.     cmpi.b  #80,d5      ; has the sprite limit been reached?
    4.     blo.s   DrawSprite_Cont ; if it hasn't, branch
    5.     rts ; otherwise, return
    6. ; End of function ChkDrawSprite
    7.  
    8.  
    9. ; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
    10.  
    11. ; sub_16812:
    12. DrawSprite:
    13.     movea.w art_tile(a0),a3
    14.     cmpi.b  #80,d5
    15.     bhs.s   DrawSprite_Done
    16. ; loc_1681C:
    17. DrawSprite_Cont:
    18.     btst    #0,d4   ; is the sprite to be X-flipped?
    19.     bne.s   DrawSprite_FlipX    ; if it is, branch
    20.     btst    #1,d4   ; is the sprite to be Y-flipped?
    21.     bne.w   DrawSprite_FlipY    ; if it is, branch
    22. ; loc__1682A:
    23. DrawSprite_Loop:
    As you can see, when ChkDrawSprite is called, it branches to DrawSprite_Cont, where the X/Y-flip checks are performed.

    Now let's see how 2P does it:

    Code (ASM):
    1. ; sub_16DA6:
    2. ChkDrawSprite_2P:
    3.     cmpi.b  #80,d5
    4.     blo.s   DrawSprite_2P_Loop
    5.     rts
    6. ; End of function ChkDrawSprite_2P
    7.  
    8.  
    9. ; ||||||||||||||| S U B R O U T I N E |||||||||||||||||||||||||||||||||||||||
    10.  
    11. ; copy sprite art to VRAM, in 2-player mode
    12.  
    13. ; sub_16DAE:
    14. DrawSprite_2P:
    15.     movea.w art_tile(a0),a3
    16.     cmpi.b  #80,d5
    17.     bhs.s   DrawSprite_2P_Done
    18.     btst    #0,d4
    19.     bne.s   DrawSprite_2P_FlipX
    20.     btst    #1,d4
    21.     bne.w   DrawSprite_2P_FlipY
    22. ; loc_16DC6:
    23. DrawSprite_2P_Loop:
    Oh dear.

    The issue's pretty obvious: the wrong label is called, skipping the checks. Just add a DrawSprite_2P_Cont before the checks, and make ChkDrawSprite_2P use that instead.
     
  9. Clownacy

    Clownacy

    Tech Member
    1,053
    581
    93
    Don't you hate it when you Spin Dash next to a moving object, and it takes you out of your Spin Dash animation, and into your walking one?

    [​IMG]

    Well, S3K has the fix just for you! The code responsible for this issue is SolidObject_TestClearPush (loc_19AC4 in old disasms). The S3K version, loc_1E0A2, checks for if the Spin Dash animation is playing, and if so, it doesn't interrupt it. Simple, huh?

    S2:
    Code (ASM):
    1. SolidObject_TestClearPush:
    2.     move.l  d6,d4
    3.     addq.b  #pushing_bit_delta,d4
    4.     btst    d4,status(a0)
    5.     beq.s   loc_19AEA
    6.     cmpi.b  #AniIDSonAni_Roll,anim(a1)
    7.     beq.s   loc_19ADC
    8.     move.w  #AniIDSonAni_Run,anim(a1)
    S3K:
    Code (ASM):
    1. loc_1E0A2:
    2.         move.l  d6,d4
    3.         addq.b  #2,d4
    4.         btst    d4,$2A(a0)
    5.         beq.s   loc_1E0D0
    6.         cmpi.b  #2,$20(a1)
    7.         beq.s   sub_1E0C2
    8.         cmpi.b  #9,$20(a1)
    9.         beq.s   sub_1E0C2
    10.         move.w  #1,$20(a1)
    As djohe would tell you, it's also worth checking for the drowning and death animations. This prevents issues like entering the walking animation when drowning next to a solid object like a monitor.

    He'd also recommend checking for the 'in air' status bit, and the spin dash flag, instead of the animation IDs for drowing and Spin Dashing. Saves cycles, more thorough, etc.

    How about another bug?

    [​IMG]

    This is silly. As it turns out, S3K fixes this as well. The cause is... a little harder to grasp than the last one, especially since the code in question was largely rewritten in S3K.

    Code (ASM):
    1. TAnim_WalkRunZoom: ; a0=character
    2.     ; note: for some reason SAnim_WalkRun doesn't need to do this here...
    3.     subq.b  #1,anim_frame_duration(a0)  ; subtract 1 from Tails' frame duration
    4.     bpl.s   TAnim_Delay         ; if time remains, branch
    The comment was right to be suspicious: this is the cause. It simply occurs too early.

    You see, if you look at Sonic's animation code, you can see that check multiple times. It occurs way later in SAnim_WalkRun than it does in TAnim_WalkRunZoom, and it appears in SAnim_Roll and SAnim_Push. What's significant about these subroutines? The animations they control - walking, running, rolling, and pushing - are all dependant on the player's Inertia value.

    So what's the problem? Tails' code, by placing the check early into TAnim_WalkRunZoom, 'kills two birds with one stone', so to say: instead of having multiple checks for each piece of code, this does it with one. The problem with this is that the 'tumbling' animation also runs this code, even though that particular animation isn't Inertia-dependant.

    So what do we do? Simple, do what Sonic's code does: move the check at TAnim_WalkRunZoom down to under TAnim_SpeedSelected, then add the same check to the start of TAnim_Roll, TAnim_Push, and TAnim_GetTailFrame.

    And with that, the bug is fixed. Hoora- wait what's that?

    [​IMG]

    Oh great, now Tails' tails are messing up.

    Once again, S3K provides a fix:

    Obj_Tails_Tail_Main:
    Code (ASM):
    1.         moveq   #0,d0
    2.         move.b  anim(a2),d0
    3.         btst    #5,status(a2)
    4.         beq.s   loc_1612C
    5.         tst.b   ($FFFFF7C9).w
    6.         bne.s   loc_1612C
    7.         ; This is checking if parent (Tails) is in its pushing animation
    8.         cmpi.b  #$A9,mapping_frame(a2)
    9.         blo.s   loc_1612C
    10.         cmpi.b  #$AC,mapping_frame(a2)
    11.         bhi.s   loc_1612C
    12.         moveq   #4,d0
    13. loc_1612C:
    The game technically lacks a 'pushing' animation ID, so the Tails' tails object (Obj05) relies on the 'this object is pushing something' status bit to determine if it should use its own 'pushing' animation. That flag doesn't necessarily mean Tails is in his pushing animation, however. To counter this, the S3K version of the object manually checks Tails' mapping frames. This eliminates all chance of the object acting funny if the 'pushing' bit and 'pushing' animation are not set at the same time. The S2 equivalent of this code would be:

    Obj05_Main:
    Code (ASM):
    1.     moveq   #0,d0
    2.     move.b  anim(a2),d0
    3.     btst    #5,status(a2)
    4.     beq.s   +
    5.     ; This is checking if parent (Tails) is in its pushing animation
    6.     cmpi.b  #$63,mapping_frame(a2)
    7.     blo.s   +
    8.     cmpi.b  #$66,mapping_frame(a2)
    9.     bhi.s   +
    10.     moveq   #4,d0
    11. +
     
  10. Clownacy

    Clownacy

    Tech Member
    1,053
    581
    93
    Okay, another fix for Tails... kinda. The fix in the above post isn't complete, causing Tails' sprite to visibly glitch when walking around CPZ's tubes. As mentioned before, Tails' animation code in TAnim_WalkRunZoom is very different to Sonic's SAnim_WalkRun. I'm still not sure why that is, but S3K wound up reverting a lot of it anyway, and managed to fix some bugs in the process. So, for the (hopefully) last fix in this little series, we'll be replacing some of Tails' code with Sonic's.

    Find SAnim_WalkRun, and find this block of code (the Git disasm is always changing, so be sure to find your own copy):

    Code (ASM):
    1.     moveq   #0,d1
    2.     move.b  anim_frame(a0),d1
    3.     move.b  1(a1,d1.w),d0
    4.     cmpi.b  #-1,d0
    5.     bne.s   +
    6.     move.b  #0,anim_frame(a0)
    7.     move.b  1(a1),d0
    8. +
    9.     move.b  d0,mapping_frame(a0)
    10.     add.b   d3,mapping_frame(a0)
    11.     subq.b  #1,anim_frame_duration(a0)
    12.     bpl.s   return_1B4AC
    13.     neg.w   d2
    14.     addi.w  #$800,d2
    15.     bpl.s   +
    16.     moveq   #0,d2
    17. +
    18.     lsr.w   #8,d2
    19.     move.b  d2,anim_frame_duration(a0)  ; modify frame duration
    20.     addq.b  #1,anim_frame(a0)       ; modify frame number
    Copy that, then go to your TAnim_SpeedSelected, and replace everything after the label up until the 'rts' instruction with the code you just copied.
     
  11. Caverns 4

    Caverns 4

    Member
    346
    0
    16
    Sonic: Retold
    If my memory serves me, this isn't a "bug", but was intended behavior. I don't really have the place where that was said on hand though, and it would take me a while to find it.
    It's also not really worth debating over, since it also could be considered a design preference depending on the scope of your hack.
     
  12. Clownacy

    Clownacy

    Tech Member
    1,053
    581
    93
    ...then why bring it up? It seems obvious that Sonic Team were duplicating code (which is considered bad practice for leading to bugs just like this), and simply stopped at the checks, not noticing the additional clears after it. What reason would this be intentional?
     
  13. Fred

    Fred

    Taking a break Oldbie
    1,563
    117
    43
    Portugal
    Sonic 3 Unlocked
    It's weird inconsistent behavior to be sure, but it's also the only way to play EHZ1 as Super Sonic without using cheats.

    I'd rather have the emerald count clear when you reset the game or run out of continues, though.
     
  14. Clownacy

    Clownacy

    Tech Member
    1,053
    581
    93
    So, apparently the DEZ music was meant to resume between the Silver Sonic and Final Boss fights.

    Code (ASM):
    1. loc_39BA4:
    2.     move.w  #$1000,(Camera_Max_X_pos).w
    3.     addq.b  #2,(Dynamic_Resize_Routine).w
    4.     move.b  (Level_Music).w,d0
    5.     jsrto   (PlayMusic).l, JmpTo5_PlayMusic
    6.     bra.w   JmpTo65_DeleteObject
    The problem here is that Level_Music is actually a word long, meaning all that 'move.b' does is try to play Sound 0, which doesn't do anything, thus the Boss music just continues until the Final Boss music interrupts it. To fix this, just change the 'move.b' to 'move.w'
     
  15. Overlord

    Overlord

    Now playable in Smash Bros Ultimate Moderator
    19,218
    965
    93
    Long-term happiness
    That track is still underused though. Even in a scenario where it'd be working, you'd hear it again while playing normally for what, 5 seconds? 10 at most?
     
  16. DigitalDuck

    DigitalDuck

    Arriving four years late. Member
    5,338
    412
    63
    Lincs, UK
    TurBoa, S1RL
    Yeah, you wouldn't even get to the melody in normal gameplay. There are people who've completed the game lots of times that don't even know it has a melody.
     
  17. Dark Sonic

    Dark Sonic

    Member
    14,631
    1,610
    93
    Working on my art!
    I always thought it would have been better if you fought Mecha Sonic to the Death Egg melody rather than the boss music. Actually give the song a chance to be heard.
     
  18. Mercury

    Mercury

    His Name Is Sonic Tech Member
    1,740
    21
    18
    Location Location
    AeStHete
    Was having a conversation about the minutiae of how 1-ups are awarded in the classic games and discovered this:

    Flags are set when you get a life at 100 and 200 rings, so you can't re-get those lives when your rings count down from being Super Sonic (they didn't want you to e.g. collect 100 rings, wait a second for them to count down to 99, grab one more ring and get a free life, rinse and repeat).

    However, these flags are not cleared when you revert to normal (by dropping to 0 rings). They are only cleared when Sonic is hit and loses his rings, or when the level is loaded. (Search "Sonic_RevertToNormal" and "Extra_life_flags" in the github disasm for the relevant stuff.)
     
  19. DigitalDuck

    DigitalDuck

    Arriving four years late. Member
    5,338
    412
    63
    Lincs, UK
    TurBoa, S1RL
    Design pondering:

    Should they even be cleared when Sonic is hit? If you wanted to farm lives from rings, you could collect 100, get hit, and then start afresh with up to 32 (meaning you only need to hunt for another 68 rings instead of another 100). Doesn't make that much of a difference, but it's food for thought.
     
  20. Devon

    Devon

    Down you're going... down you're going... Tech Member
    1,218
    1,373
    93
    your mom
    So, notice how bridges in Sonic 2 can only have an even amount of segments? Well, here's how you can allow for odd number of segments.

    In "Obj11_Init", there's this little segment of code:

    [68k]
    lsr.w #1,d0
    lsl.w #4,d0 ; (d0 div 2) * 16
    [/68k]
    This basically forces the number of segments to be even and then shifts it to what it needs to be. What you need to do is just change that to:

    [68k]
    lsl.w #3,d0 ; d0 * 8
    [/68k]
    But, we aren't done yet. There's a little segment of code that appears to be an incorrect calculation for calculating the X position of the second set of bridge segments (for when it's needed) (although, this could be intentional, but I'm not quite sure). Take a look at this in the same routine:

    [68k]
    move.w d4,d0
    add.w d0,d0
    add.w d4,d0 ; d0*3
    move.w sub2_x_pos(a1,d0.w),d0
    [/68k]
    For one thing, it's mutliplying the offset used to get the X position value in the sub sprite data by 3, which is wrong. Sub sprite data each take up 6 bytes, not 3. So, what we need to do is just make it:

    [68k]
    move.w d4,d0
    add.w d0,d0
    add.w d4,d0
    add.w d0,d0 ; d0*6
    move.w sub2_x_pos(a1,d0.w),d0
    [/68k]
    But there's still one more thing: because the segment count is based on 1 instead of 0 (meaning 1 = 1 segment, instead of 0 = 1 segment), it's getting the wrong data. For example, with 1 segment, it will multiply 1 by 6, and get the data from sub3_x_pos, since sub2_x_pos+6 = sub3_x_pos, which isn't used with only 1 segment. So, all we need to do is this, so that it will point to the correct data:

    [68k]
    move.w d4,d0
    add.w d0,d0
    add.w d4,d0
    add.w d0,d0 ; d0*6
    move.w sub2_x_pos-next_subspr(a1,d0.w),d0
    [/68k]
    And that's it! I can't seem to find any issues with doing this, but if any of you find any, let me know.