Has the bug in VS mode concerning the power up music changes staying on if the utilising player loses a life got a fix?
I could've sworn I made a fix for this at some point, but I can't find it anymore. Anyway, here's the fix: Code (Text): ; loc_3F926: KillSonic: KillCharacter: tst.w (Debug_placement_mode).w bne.s .return ;-------- bugfix -------- ; reset stats and music upon death move.w #1,invincibility_time(a0) ; make invincibility run out move.w #1,speedshoes_time(a0) ; make speed shoes run out cmpa.w #MainCharacter,a0 bne.s .p2 tst.b (Super_Sonic_flag).w beq.s .notSuper move.b #0,(Super_Sonic_flag).w ; remove super status move.b #2,(Super_Sonic_palette).w move.w #$28,(Palette_frame).w .notSuper: jsr (Obj01_ChkInvin).l ; remove player 1's power ups bra.s .kill .p2: jsr (Obj02_ChkInvinc).l ; remove player 2's power ups ;---- end bugfix -------- .kill: clr.b status_secondary(a0) move.b #6,routine(a0) jsrto (Sonic_ResetOnFloor_Part2).l, JmpTo_Sonic_ResetOnFloor_Part2 bset #1,status(a0) move.w #-$700,y_vel(a0) move.w #0,x_vel(a0) move.w #0,inertia(a0) move.b #AniIDSonAni_Death,anim(a0) bset #high_priority_bit,art_tile(a0) move.w #SndID_Hurt,d0 cmpi.b #ObjID_Spikes,id(a2) bne.s .playSound move.w #SndID_HurtBySpikes,d0 .playSound: jsr (PlaySound).l .return: moveq #-1,d0 rts The changes are between the two bugfix comments. The only other thing I changes was replace the nameless temporary symbols with named local symbols. As a bonus, I threw in a Super Sonic detransformation.
If anything, it's hidden away on an old hard drive somewhere. I have at least one old disassembly somewhere with a bunch of bugfixes that I never published.
Here is one more design choice change that can be made. Extending the limit to how many frames Sonic, Tails or Knuckles can have! Now, this is a bit limited due to how the animation subroutine for player characters works, but it does allow for more frames than what the game normally allows. In this example, I will be using Sonic. However, this can be applied to Tails or Knuckles as well. For Sonic, locate the label Code (Text): SAnim_Do2 now, look for a line that reads Code (Text): cmpi.b #$F0,d0 Change this to Code (Text): cmpi.b #$FD,d0 This will allow for 12 more frames of animation! Edit: Actually, change it to $FD, as this is supposed to be the number AFTER the max amount of frames. Therefore, setting to $FD allows for $FC amount of frames to be usable.
Tails on the title screen doesn't have its priority set during the fade-in, meaning a few of his pixels will appear behind Sonic; to fix this, add "move.b #3,priority(a0)" under loc_130A2:.
Counts more as an extra than a fix, but for those who wanna add in Knuckles, there's an INCREDIBLY easy way to add in some exclusive shortcuts for him. The breakable wall from Sonic 1 (obj3C) conveniently still exists in Sonic 2 unused. When you've added it to your level route and mapped it to some new art, go to it's routine and make this edit in loc_15DAE: Code (Text): loc_15DAE: lea (MainCharacter).w,a1 ; a1=character cmp.b #$XX,(a1) ;is player Knuckles? (replace XX with Knuckles' obj number) beq.w Smash_Things ; cmpi.b #2,anim(a1) ; bne.s return_15DAC move.w objoff_30(a0),d0 bpl.s loc_15DC2 neg.w d0 loc_15DC2: cmpi.w #$480,d0 bcs.s return_15DAC Smash_Things: move.w objoff_30(a0),x_vel(a1) addq.w #4,x_pos(a1) This will change so instead of recognising Sonic's ball form, it will just recognise Knuckles only and let him plow through the wall.
Warning! Making objects continue to move after dying causes Sonic to continually die on the lava collision object (i.e. he dies and bounces on it forever). To be safe, add this above the Debug Mode check: Code (Text): cmpi.b #6,(MainCharacter+routine).w ; is Sonic/Knuckles dead? bhs.s + ; if yes, branch cmpi.b #6,(Sidekick+routine).w ; is Tails dead? bhs.s + ; if yes, branch
There is an asymmetry between left and right when braking on all Genesis games (S1, S2, S3, S&K): when you start braking, the skidding animation is triggered when you are above 4.5 pixels/frame going right, or 3.5 pixels/frame when going left. This can become particularly noticeable if you increase the top speed of Sonic and test underwater. So here is the fix for S1, S2, and S&K disassemblies: first, go to the following locations and find the code shown: S1: loc_130BA, loc_13120 Code (ASM): move.b obAngle(a0),d0 addi.b #$20,d0 andi.b #$C0,d0 S2: Sonic_TurnLeft, Sonic_TurnRight, Tails_TurnLeft, Tails_TurnRight Code (ASM): move.b angle(a0),d0 addi.b #$20,d0 andi.b #$C0,d0 S&K: loc_11438, loc_114BE, loc_14C62, loc_14CE8, loc_1746A, loc_174F0, loc_3559E, loc_35620 Code (ASM): move.b $26(a0),d0 addi.b #$20,d0 andi.b #-$40,d0 Now change all instances of d0 to d1 in the code shown above. That is it, the bug is fixed. Both left and right will now trigger skidding animation when braking above 4.5 pixels/frame. What is going on is that the ground speed/inertia is stored in d0. When the data for ground angle is for this fragment, it overwrites part of the value for inertia. Right after this code fragment, there is a check to see if the character should brake (this happens only for shallow angles), and right after this, there is a check for ground speed/inertia for braking. So the braking check is done after the register is partially overwritten. And the reason why it is 4.5 pixels/frame instead of 4 pixels/frame is because 1 frame's worth of deceleration is applied before the comparison.
Another asymmetry, this time between left and right: take a look as these diveboard springs: No matter how hard you try, you can never get a launch off the left-facing diveboard with the same speed you can from the right-facing one. The code that causes this is here: Code (ASM): loc_2645E: move.w x_pos(a0),d0 subi.w #$1C,d0 sub.w x_pos(a1),d0 neg.w d0 btst #0,status(a0) beq.s loc_2647A not.w d0 addi.w #$27,d0 The reason for the bug is that it misplaces the characters relative to the board for computing the power of the launch. The fix is dead easy: take the last line: Code (ASM): addi.w #$27,d0 And change it to this: Code (ASM): addi.w #2*$1C,d0
Hmm, $1C times 2 is $38... but 38 decimal is $26 hex, which is only one off from $27. Wonder what happened there?
You might have figured out what happened. Because Code (Text): not.w d0 == neg.w d0 ; subq.w #1, d0 they might have thought that they needed to add +1 to $38 to get $39. But then they forgot to write the $ in the code, and it became a $27 when reverse engineered. As it turns out, that +1 gives the wrong result -- the last pixel you can stand on when the dive board is flipped will loop back to minimal strength launch, which is not what happens on the unflipped dive board.
Here is a CPZ boss crash: https://www.twitch.tv/videos/47314506 In case you are curious, this bug is of the "how the hell this boss even works" variety. To fix it, find this: Code (ASM): Obj5D_Pipe_Retract_ChkID: moveq #0,d7 move.b #ObjID_CPZBoss,d7 cmp.b id(a1),d7 ; is object a subtype of the CPZ Boss? and change it to this: Code (ASM): Obj5D_Pipe_Retract_ChkID: cmpi.b #ObjID_CPZBoss,id(a1) ; is object a subtype of the CPZ Boss? What is the bug? Register d7 is used by RunObjects routine to keep track of how many objects remain to be processed. Using it without saving before (as this code does) is a recipe for disaster. This piece of code runs when Eggman is retracting his pipe, and whenever an object with the same Y position as the next-to-last pipe segment. When this happens, the following may occur: if there remains more than $5D objects to process (ObjID_CPZBoss is equal to $5D in stock S2), some objects may remain unprocessed; if there remains between $59 and $5D objects, all objects will be processed, and some of the $200 bytes after will be treated as objets; if this area is clear, no ill effects will occur; if there remains less than $59 objects to process, primary collision, then secondary collision, etc., will start to be interpreted as object data, and you can get all sorts of crashes.
A double post, and another bug from the Chemical Plant boss. This time it is a graphical bug which was originally found and fixed by djohe; I am just sharing it. After you beat the CPZ boss and the explosions finish, the boss is supposed to spawn a puff of smoke when blasting off. There is even code for it. However, due to a mistake in the code, it never appears. If you fix this mistake, you will see it appearing with incorrect graphics and palette, so we will fix that too. Open your disassembly and find this code: Code (ASM): loc_2E9A8: movea.l Obj5D_parent(a0),a1 ; a1=object btst #6,Obj5D_status2(a1) bne.s + rts ; =========================================================================== + addq.b #2,routine_secondary(a0) move.l #Obj5D_MapUnc_2EEA0,mappings(a0) move.w #make_art_tile(ArtTile_ArtNem_EggpodJets_1,0,0),art_tile(a0) Change this to: Code (ASM): loc_2E9A8: movea.l Obj5D_parent(a0),a1 ; a1=object btst #6,Obj5D_status2(a1) bne.s + rts ; =========================================================================== + addq.b #2,routine(a0) move.l #Obj5D_MapUnc_2EEA0,mappings(a0) move.w #make_art_tile(ArtTile_ArtNem_BossSmoke_1,1,0),art_tile(a0) This will fix the spawning of the smoke puff (the change from routine_secondary to routine) and set correct graphics and palette (the other change).
I'm bringing attention to these set of unused subroutines that can be found within the data of the Nick Arcade and Simon Wai builds; they seem to be converting some set of data for use in-game (similar to ConvertCollisionArray): Code (Text): ; =========================================================================== ; --------------------------------------------------------------------------- ; Unknown subroutine, presumably converts data for use in-game (which ; would require a dev cart that can write to itself, not normal carts) ; --------------------------------------------------------------------------- ; UnknownSub_1: lea ($FFFF0000).l,a1 move.w #$2EB,d2 loc_3A3A: move.w (a1),d0 move.w d0,d1 andi.w #$F800,d1 andi.w #$7FF,d0 lsr.w #1,d0 or.w d0,d1 move.w d1,(a1)+ dbf d2,loc_3A3A rts ; =========================================================================== ; UnknownSub_2: lea ($FE0000).l,a1 lea ($FE0080).l,a2 lea ($FFFF0000).l,a3 move.w #$3F,d1 loc_3A68: bsr.w UnknownSub_4 bsr.w UnknownSub_4 dbf d1,loc_3A68 lea ($FE0000).l,a1 lea ($FF0000).l,a2 move.w #$3F,d1 loc_3A84: move.w #0,(a2)+ dbf d1,loc_3A84 move.w #$3FBF,d1 loc_3A90: move.w (a1)+,(a2)+ dbf d1,loc_3A90 rts ; =========================================================================== ; UnknownSub_3: lea ($FE0000).l,a1 lea ($FFFF0000).l,a3 moveq #$1F,d0 loc_3AA6: move.l (a1)+,(a3)+ dbf d0,loc_3AA6 moveq #0,d7 lea ($FE0000).l,a1 move.w #$FF,d5 loc_3AB8: lea ($FFFF0000).l,a3 move.w d7,d6 loc_3AC0: movem.l a1-a3,-(sp) move.w #$3F,d0 loc_3AC8: cmpm.w (a1)+,(a3)+ bne.s loc_3ADE dbf d0,loc_3AC8 movem.l (sp)+,a1-a3 adda.w #$80,a1 dbf d5,loc_3AB8 bra.s loc_3AF8 ; --------------------------------------------------------------------------- loc_3ADE: movem.l (sp)+,a1-a3 adda.w #$80,a3 dbf d6,loc_3AC0 moveq #$1F,d0 loc_3AEC: move.l (a1)+,(a3)+ dbf d0,loc_3AEC addq.l #1,d7 dbf d5,loc_3AB8 loc_3AF8: bra.s loc_3AF8 ; ||||||||||||||| S U B R O U T I N E ||||||||||||||||||||||||||||||||||||||| UnknownSub_4: moveq #7,d0 loc_3AFC: move.l (a3)+,(a1)+ move.l (a3)+,(a1)+ move.l (a3)+,(a1)+ move.l (a3)+,(a1)+ move.l (a3)+,(a2)+ move.l (a3)+,(a2)+ move.l (a3)+,(a2)+ move.l (a3)+,(a2)+ dbf d0,loc_3AFC adda.w #$80,a1 adda.w #$80,a2 rts ; End of function UnknownSub_4 EDIT: Actually, the last three subroutines seem to be duplicates of LevelLayout_Convert.
Not sure if this one has been noted before, it's easy to miss, but apparently when you break Buzzer, it's exhaust sprite actually lingers on for a little while longer:
Haha, good catch! A peek at the disassembly should hopefully explain what's going on there: Code (ASM): ; loc_2D090: Obj4B_Flame: movea.l Obj4B_parent(a0),a1 ; a1=object tst.b id(a1) beq.w JmpTo49_DeleteObject ; branch, if object slot is empty. This check is incomplete and very unreliable; check Obj50_Wing to see how it should be done tst.w Obj4B_turn_delay(a1) bmi.s + ; branch, if parent isn't currently turning around rts You might be wondering what exactly is wrong here: when an object is destroyed, the ID of its now-empty RAM slot is set to 0, so this should be a perfectly-adequate way to detect when the Buzzer object is destroyed and to then delete the flame object. Well, here's what Obj50_Wing does: Code (ASM): ; loc_2CDCA: Obj50_Wing: movea.l Obj50_parent(a0),a1 ; a1=object tst.b id(a1) ; is parent object's slot empty? beq.w JmpTo48_DeleteObject ; if yes, branch cmpi.b #ObjID_Aquis,(a1) ; is parent object ObjID_Aquis? bne.w JmpTo48_DeleteObject ; if not, branch btst #7,status(a1) ; is parent object marked as destroyed? bne.w JmpTo48_DeleteObject ; if yes, branch lea (Ani_obj50).l,a1 jsrto (AnimateSprite).l, JmpTo14_AnimateSprite jmpto (DisplaySprite).l, JmpTo32_DisplaySprite As this code suggests, it's absolutely possible for a new object to be loaded into the Buzzer's RAM slot right after it is destroyed, before the flame object ever has a chance to check if the slot is empty. The solution to this is instead not to check if the object slot's ID is 0, but rather if it simply isn't the ID of a Buzzer. Of course, in theory, it is technically possible for the Buzzer's RAM slot to be occupied by another Buzzer object, so this check isn't fool-proof, but it mostly works. I'm pretty sure that the object which gets loaded into the Buzzer's RAM slot is always the points object - notice how both the flame and the points disappear at the same time? (EDIT: I'm dumb: it's the explosion object that gets loaded into the slot, not the points object). Anyway, this new check makes the previous check redundant, so you can safely remove the stuff for checking if the ID is 0 and the code will work just as well.
So can't believe this wasn't noticed before but I only saw it because I thought I broke it before checking S2. Chemical Plant's Pause Water is broken, it doesn't work at all. See here is Rev 2: Game is paused, I checked Rev 1 also. The reason is actually surprisingly simple: Code (Text): ; =========================================================================== ; loc_20930: Obj_WaterSurface_Action: move.w (Water_Level_1).w,d1 move.w d1,y_pos(a0) tst.b objoff_32(a0) bne.s Obj_WaterSurface_Animate btst #button_start,(Ctrl_1_Press).w ; is Start button pressed? beq.s loc_20962 ; if not, branch addq.b #3,mapping_frame(a0) ; use different frames move.b #1,objoff_32(a0) ; stop animation bra.s loc_20962 ; <-- This is branching into the normal animation code Change to BranchTo_JmpTo10_DisplaySprite ; =========================================================================== ; loc_20952: Obj_WaterSurface_Animate: tst.w (Game_paused).w ; is the game paused? bne.s loc_20962 ; if yes, branch <-- This is branching into the normal animation code also change to BranchTo_JmpTo10_DisplaySprite move.b #0,objoff_32(a0) ; resume animation subq.b #3,mapping_frame(a0) ; use normal frames loc_20962: lea (Anim_Obj_WaterSurface).l,a1 moveq #0,d1 move.b anim_frame(a0),d1 move.b (a1,d1.w),mapping_frame(a0) addq.b #1,anim_frame(a0) andi.b #$3F,anim_frame(a0) jmpto (DisplaySprite).l, JmpTo10_DisplaySprite The issue is that the code is branching to loc_20962 even though it should really be branching to "BranchTo_JmpTo10_DisplaySprite" instead causing the pause frames in Chemical Plant (and Hidden Palace Zone) to go completely unused. Figured this out by comparing Obj_WaterSurface_Action to Obj_WaterSurface_Action2 where it does work as intended. Also as a bonus here's how to fix the misaligned pause sprite from happening: Code (Text): ; --------------------------------------------------------------------------- ; Subroutine to move the water or oil surface sprites to where the screen is at ; (the closest match I could find to this subroutine in Sonic 1 is Obj_SpeedBooster_Action) ; --------------------------------------------------------------------------- ; ||||||||||||||| S U B R O U T I N E ||||||||||||||||||||||||||||||||||||||| ; sub_44E4: UpdateWaterSurface: tst.b (Water_flag).w beq.s ++ ; rts move.w (Camera_X_pos).w,d1 btst #0,(Timer_frames+1).w beq.s + addi.w #$20,d1 + ; match obj x-position to screen position move.w d1,d0 addi.w #$60,d0 move.w d0,(WaterSurface1+x_pos).w addi.w #$120,d1 move.w d1,(WaterSurface2+x_pos).w + rts ; End of function UpdateWaterSurface Change this to: Code (Text): ; --------------------------------------------------------------------------- ; Subroutine to move the water surface sprites to where the screen is at ; (the closest match I could find to this subroutine in Sonic 1 is Obj_SpeedBooster_Action) ; --------------------------------------------------------------------------- ; ||||||||||||||| S U B R O U T I N E ||||||||||||||||||||||||||||||||||||||| ; sub_44E4: UpdateWaterSurface: tst.b (Water_flag).w beq.s ++ ; rts move.w (Camera_X_pos).w,d1 btst #button_start,(Ctrl_1_Press).w ; is Start button pressed? bne.s + ; if yes, branch btst #0,(Timer_frames+1).w beq.s + addi.w #$20,d1 + ; match obj x-position to screen position move.w d1,d0 addi.w #$60,d0 move.w d0,(WaterSurface1+x_pos).w addi.w #$120,d1 move.w d1,(WaterSurface2+x_pos).w + rts ; End of function UpdateWaterSurface This Adds a start button check to prevent it from moving over. However if you build now it's still broken what gives? Well you need to move the call to UpdateWaterSurface in Level_MainLoop to be above RunObjects and you'll be golden! Sonic 1 has the same issue which you can find the fix for here Thanks Devon for helping me figure that part out.
Edit: Seems this was fixed already in the thread however that fix is broken, when you vertically wrap downwards back to the top, the rings don't display properly. So keeping this fix here then: Fix rings disappearing too early at the top of the screen in Sonic 2 If you pay close attention, rings that go offscreen at the top will disappear too early. This is due to a small bug with the check. Code (Text): move.w 4(a0),d2 ; get ring Y pos sub.w 4(a3),d2 ; subtract camera Y pos andi.w #$7FF,d2 addi_.w #8,d2 bmi.s BuildRings_NextRing ; dunno how this check is supposed to work cmpi.w #240,d2 bge.s BuildRings_NextRing ; if the ring is not on-screen, branch The "dunno how this check is supposed to work" comment refers to how it ANDs the Y offset with $7FF, adds 8, and then checks if it's negative, in which will never be the case. In the prototype version of the code, that AND wasn't in there, which allowed the upper limit check to work. They most likely added it for Y wrapping in levels, but they forgot to fix the check. To fix it, we can do what S3K does: Code (Text): move.w 4(a0),d2 ; get ring Y pos sub.w 4(a3),d2 ; subtract camera Y pos addq.w #8,d2 andi.w #$7FF,d2 cmpi.w #224+16,d2 bhs.s BuildRings_NextRing ; if the ring is not on-screen, branch The 2 player code also suffers from the bug as well: Code (Text): move.w 4(a0),d2 ; get ring Y pos sub.w 4(a3),d2 ; subtract camera Y pos andi.w #$7FF,d2 addi.w #128+8,d2 bmi.s BuildRings_2P_NextRing cmpi.w #240+128,d2 bge.s BuildRings_2P_NextRing Which calls for a similar fix: Code (Text): move.w 4(a0),d2 ; get ring Y pos sub.w 4(a3),d2 ; subtract camera Y pos addq.w #8,d2 andi.w #$7FF,d2 cmpi.w #224+16,d2 bhs.s BuildRings_2P_NextRing And also, go to BuildRings_P1 and change the "128-8" to a "128+128-8" and, then go to BuildRings_P2 and change the "224+128-8" to a "224+128+128-8". Thanks to devon for making the fix, just posting it here for completion sake.