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.