Fix crashes in Sonic 3 & Knuckles

Discussion in 'Tutorials' started by Natsumi, Jun 22, 2018.

  1. Natsumi

    Natsumi Markey's Member

    Joined:
    Oct 7, 2011
    Messages:
    645
    Location:
    Otter's lap
    So, as you might know, there are many bugs in Sonic 3 (& Knuckles), that cause the game to crash, for seemingly no reason. Indeed, many people, including myself, are really annoyed at these, because they can totally fuck you up and ruin your playthrough. Luckily, most of them are on the Sonic 3 side, so you at least have a save system to have your back. I have found out why these occur, and how to fix them, once and for all! These bugs appear because of the new mechanism for updating plane mappings in v-blank, while the plane update code itself is outside of it. The data to be transferred to VDP is stored in a buffer in working memory, and then very quickly loaded into VRAM during v-blank. There is one flaw with it; No bounds checking. The plane buffer can happily overrun itself, into very sensitive memory, such as the game mode, which is the main culprit for crashing the game! Bear in mind, this tutorial will also cause visual bugs from time to time! This tutorial will be based on the Git disassembly, but changes may be applicable to other disassemblies.

    So first of all, lets fix the MGZ2 moving up section near the boss! Open up Sonic3k.asm and go to loc_51994, and change this line:
    Code:
            bpl.w    loc_51926
    To this:
    Code:
            bpl.w    loc_51936        ; NAT: Change lable
    Then open up Sonic3k.constants.asm, and change:
    Code:
    Target_water_palette =        ramaddr( $FFFFF000 ) ; $80 bytes ; used by palette fading routines
    Water_palette =            ramaddr( $FFFFF080 ) ; $80 bytes ; this is what actually gets displayed
    Water_palette_line_2 =        ramaddr( $FFFFF0A0 ) ; $20 bytes
    Water_palette_line_3 =        ramaddr( $FFFFF0C0 ) ; $20 bytes
    Water_palette_line_4 =        ramaddr( $FFFFF0E0 ) ; $20 bytes
    
    Plane_buffer =            ramaddr( $FFFFF100 ) ; $480 bytes ; used by level drawing routines
    VRAM_buffer =            ramaddr( $FFFFF580 ) ; $80 bytes ; used to temporarily hold data while it is being transferred from one VRAM location to another
    Into this:
    Code:
    Plane_buffer =            ramaddr( $FFFFF000 ) ; $480 bytes ; used by level drawing routines
    VRAM_buffer =            ramaddr( $FFFFF480 ) ; $80 bytes ; used to temporarily hold data while it is being transferred from one VRAM location to another
    
    Target_water_palette =        ramaddr( $FFFFF500 ) ; $80 bytes ; used by palette fading routines
    Water_palette =            ramaddr( $FFFFF580 ) ; $80 bytes ; this is what actually gets displayed
    Water_palette_line_2 =        ramaddr( $FFFFF5A0 ) ; $20 bytes
    Water_palette_line_3 =        ramaddr( $FFFFF5C0 ) ; $20 bytes
    Water_palette_line_4 =        ramaddr( $FFFFF5E0 ) ; $20 bytes
    Then we need to fix the water palette routines. Go to LoadPalette2, and replace:
    Code:
            suba.w    #$B80,a3
    With:
    Code:
            suba.w    #Normal_palette-Target_water_palette,a3    ; NAT: Fixed equation
    And go to LoadPalette2_Immediate, and change:
    Code:
            suba.w    #$C00,a3
    To:
    Code:
            suba.w    #Normal_palette-Water_palette,a3    ; NAT: Fixed equation
    What this will do, is swap the positions of Plane_buffer and Water_palette. This will fix MGZ2, and was the only way I found to fix it reliably. However, few other levels with water will break, so we might as well and go the full treatment to fix them proper. So! Lets go fix HCZ2. Open up Sonic3k.asm again, and navigate to loc_50FB8, and change
    Code:
            bpl.s    loc_50FCE
    Into:
    Code:
            bpl.s    loc_50FDE        ; NAT: Change lable
    Next up, AIZ2. Go to loc_50280, and change:
    Code:
            bpl.s    loc_50298
    To:
    Code:
            bpl.s    loc_502A8        ; NAT: Change lable
    Then at loc_50204, change:
    Code:
            bpl.s    loc_50260
    To this:
    Code:
            bpl.s    loc_50270        ; NAT: Change lable
    Next up for ICZ1, in loc_53E84, change:
    Code:
            bpl.s    loc_53E3E
    Into:
    Code:
            bpl.s    loc_53E4E        ; NAT: Change lable
    Then LBZ2, go to loc_54488, and change:
    Code:
            bpl.s    loc_544A4
    To:
    Code:
            bpl.s    loc_544B4        ; NAT: Change lable
    To do SOZ1, go to loc_55B08, and change:
    Code:
            bpl.w    loc_55BA2
    Into:
    Code:
            bpl.w    loc_55B74        ; NAT: Change lable
    And SOZ2 as well. At loc_56206 change:
    Code:
            bpl.s    loc_5621A
    To:
    Code:
            bpl.s    loc_5622A        ; NAT: Change lable
    And also at loc_56304:
    Code:
            bpl.w    loc_563C0
    Change that to:
    Code:
            bpl.w    loc_563D0        ; NAT: Change lable
    And finally, go to loc_56676:
    Code:
            bpl.s    loc_566A8
    Change that into:
    Code:
            bpl.s    loc_566BC        ; NAT: Change lable
    To Fix LRZ1, go to loc_56C8C, and change:
    Code:
            bpl.s    loc_56C30
    Into:
    Code:
            bpl.s    loc_56C40        ; NAT: Change lable
    To do LRZ2, At loc_57044, change:
    Code:
            bpl.s    loc_5705C
    To:
    Code:
            bpl.s    loc_5706C        ; NAT: Change lable
    To fix SSZ, go to loc_578F0, and move the lable below:
    Code:
            jsr    (DrawBGAsYouMove).l
    So the routine will look like this:
    Code:
    loc_578EC:
            jsr    sub_579F0(pc)
            jsr    (DrawBGAsYouMove).l
    
    loc_578F0:        ; NAT: Move lable
            jsr    (PlainDeformation).l
            jsr    (ShakeScreen_Setup).l
            tst.w    ($FFFFEE98).w
            bne.s    loc_57916
            lea    word_577B2(pc),a4
            lea    ($FFFFA8BE).w,a5
            jmp    (Apply_FGVScroll).l
    Then go to loc_5799A, and move the lable below:
    Code:
            jsr    (Draw_TileRow).l
    So the routine will look like:
    Code:
    loc_57996:
            jsr    sub_57A60(pc)
            lea    (Camera_Y_pos_BG_copy).w,a6
            lea    (Camera_Y_pos_BG_rounded).w,a5
            move.w    #$1C00,d1
            moveq    #$20,d6
            jsr    (Draw_TileRow).l
    
    loc_5799A:        ; NAT: Move lable
            move.w    ($FFFFEEDE).w,d0
            beq.s    loc_579C4
            move.w    d0,(Camera_X_pos_BG_copy).w
            move.w    ($FFFFEEE0).w,(Camera_Y_pos_BG_copy).w
            jmp    (PlainDeformation).l
    Now, you might have noticed me change some levels that really dont cause issues. This is because these levels can cause issues, if you are able to move the came >16 pixels diagonally in a single frame. As this is very unlikely, its not going to happen very often. I provided a fix for that anyway, for completeness sake. What the fix actually is, I am skipping some code for scrolling when the screen is being redrawn. Most of the time this wont cause an issue, but can occasionally cause a column of tiles not be drawn correctly. But without the fix, this will crash the game, so pick y our poison I guess. Of course, when we moved the plane buffer, this could probably be fine now, but again, for sake of completeness, I wanted to provide a solution.
    This tutorial was created from changes made to a hack I am working on, and I might have missed a level! If there is any other level causing crashes you know of, please let me know and I will have a look in my source.
     
    Last edited: Jun 25, 2018