[Sonic 1 2005] Make Sonic (near) impossible to kill after defeating a boss

Discussion in 'Tutorials' started by giovanni.gen, Sep 20, 2022.

  1. giovanni.gen

    giovanni.gen It's still Joe-vanni, not Geo-vanni. Member

    Joined:
    Apr 16, 2015
    Messages:
    313
    Location:
    Italy
    DISCLAIMER: Not all of the code needed to implement this is present and ready to be copy-pasted.
    Make sure you read the guide properly and have a basic grasp on 68K assembly.

    Has this ever happened to you?

    painful.gif

    Let's face it, the fact that Sonic can just die after successfully defeating a boss fight is frustrating. However, this can be fixed!

    Step 1: Change Sonic's hurting routine

    There just so happens to be a RAM byte that tracks down at what stage of a boss we are at: $FFFFF7A7. So, what is $FFFFF7A7?

    Well, we can use this information to our advantage. If the byte is anything other than zero, then it means we've finished a boss fight.

    The function that governs how Sonic should respond to damage is called HurtSonic. We can create a similar check to what the game already uses to check for the shield. We check if the byte for the boss fight flags is zero, and if it isn't, we prevent Sonic from getting any damage at all. Something along these lines:

    Code:
            tst.b   ($FFFFF7A7).w   ; GIO: have we defeated the boss?
            bne.s   Hurt_AfterBoss  ; GIO: if yes, branch
    
    This way, Sonic will never get hurt if $FFFFF7A7 is anything but zero. Now, if you have good eyes, you may have noticed something weird about this "Hurt_AfterBoss" I am referencing. It is the fact that this label doesn't actually exist. The plan, of course, is to make the label in question. Right below the line that removes the shield from Sonic in Hurt_Shield, we add the label in question. Should look something like this:

    Code:
    Hurt_Shield:
            move.b    #0,($FFFFFE2C).w ; remove shield
    Hurt_AfterBoss:
            move.b    #4,$24(a0)
            ...
    Now we save, build, and test out our changes. The best grounds for testing is Marble zone, so let's test it out!

    stillpainful.gif

    Wait, what!?

    We've just beaten the boss, and yet, with the changes made, Sonic still got hurt! What happened here?

    Let's run that down again.

    notsopainful.gif

    Huh, this time it worked! So... What's up with that?

    Step 2: Slightly tweaking the boss fights

    Here's the explanation as to what's happening. Turns out that $FFFFF7A7 is only set to 1 after a boss fight is finished exploding. If you're fine with it being that way, just refer to the Spring Yard Zone, Labyrinth Zone and Final Zone parts, but I personally would go all the way in making sure that Sonic can't get hurt the moment the boss fight is beaten.

    Let's start with tweaking Marble Zone's boss, since that is the zone we're looking at right now. We're looking at Object 73.

    Go to loc_184F6. It is secondary routine 4 of the MZ boss fight, which is the routine that runs right after you've defeated the boss. There, you'll find this:

    Code:
    loc_184F6:                ; XREF: Obj73_ShipIndex
            subq.w    #1,$3C(a0)
            bmi.s    loc_18500
            bra.w    BossDefeated
    
    You can see that there's a timer (set in the previous routine) that prevents the game from continuing to loc_18500 until it has expired, and if it hasn't, it takes the game to the BossDefeated function instead (the function that makes the explosions, among other things). So... why is this information relevant?

    Well, that's because at loc_18500 lies this:

    Code:
    loc_18500:
            bset    #0,$22(a0)
            bclr    #7,$22(a0)
            clr.w    $10(a0)
            addq.b    #2,$25(a0)
            move.w    #-$26,$3C(a0)
            tst.b    ($FFFFF7A7).w        ; <--
            bne.s    locret_1852A        ; <--
            move.b    #1,($FFFFF7A7).w    ; <--
            clr.w    $12(a0)
    
    locret_1852A:
            rts    
    Yup, as you can see, the game sets $FFFFF7A7 right after the explosions are finished. This can, of course, be changed by moving the setting of $FFFFF7A7 to whatever portion of code rewards you with 1000 points, as that is the exact moment the boss is defeated. For MZ's boss fight, it's loc_18392.

    Code:
    loc_18392:                ; XREF: loc_1833E
            moveq    #100,d0
            bsr.w    AddPoints
            move.b    #4,$25(a0)        ; <--
            move.w    #$B4,$3C(a0)
            clr.w    $10(a0)
            rts    
    As you can see, this is the part of code that switches the MZ boss to the aforementioned routine 4. Add a line that moves 1 to byte $FFFFF7A7 somewhere before the rts.

    Save and build, and this is what you get:

    notpainful.gif

    And that is it!



    ...for Marble Zone.

    You see, we still have to implement this for other zones, which means repeating the changes. The pattern is the following:

    - Remove the existing lines that set byte $FFFFF7A7
    - Add a line that sets byte $FFFFF7A7 where score is awarded

    I'll tell you where you're supposed to edit the code, and some other things to prevent further ways to die.

    2.1) Green Hill Zone (Optional)

    It is already impossible to get hurt in Green Hill after defeating the boss fight. However, if you're making changes to the boss area, or the boss itself, that make it so Sonic can, in fact, be hurt even after defeating it, make the following edits:

    - Remove the existing lines in loc_17984
    - Set $FFFFF7A7 in loc_1784C

    2.2) Spring Yard Zone

    While it is also impossible to get hurt in Spring Yard after defeating the boss, it is possible to fall and die.

    I do, however, have something in mind for that.

    First, you want to:

    - Remove the existing lines in loc_1947E
    - Set $FFFFF7A7 in loc_19258

    However, that does not fix dying. You can fix this by placing solid ground right after defeating the boss, but that's boring. Here's an idea I came up with.

    Go to Boundary_Bottom, and add the following lines below the label:

    Code:
            tst.b   ($FFFFF7A7).w       ; GIO: has a boss fight been defeated?
            bne.s   @afterboss            ; GIO: if yes, branch
    
    Now, add this below the rts in Boundary_Bottom:

    Code:
    @afterboss:
            btst    #2,($FFFFD022).w        ; GIO: this is an excerpt of code from Sonic_ResetOnFloor
            beq.s    @skip
            bclr    #2,($FFFFD022).w
            move.b    #$13,($FFFFD016).w
            move.b    #9,($FFFFD017).w
            subq.w    #5,($FFFFD00C).w
     
        @skip:
            clr.b   ($FFFFD03C).w            ; GIO: clear the jumping flag
            addq.w    #8,($FFFFD00C).w        ; GIO: from this point on, this is an excerpt of code from the upwards facing spring's code
            move.w    #-$A00,($FFFFD012).w    ; move Sonic upwards
            bset    #1,($FFFFD022).w
            bclr    #3,($FFFFD022).w
            move.b    #$10,($FFFFD01C).w    ; use "bouncing" animation
            move.b    #2,($FFFFD024).w
            move.w    #$CC,d0
            jmp    (PlaySound_Special).l ;    play spring sound
        
    Yup. It's exactly what you think. I made the bottom boundary behave just like a yellow spring when the boss is defeated. It's funny, and it's efficient too, because you don't need to edit the layout in real time when the boss ends.

    Apply the changes, save, and build. If necessary, fix the out of range branches too.

    syzboss.gif

    2.3) Labyrinth Zone

    Labyrinth Zone works pretty distinctly compared to other bosses. So much so, that not once does $FFFFF7A7 get set throughout it, so when the boss truly ends is up to interpretation.

    For the purposes of this guide, I consider this boss fight to be over the moment the LZ music starts. Therefore, at loc_18112.

    Code:
    loc_18112:
            move.w    #$82,d0
            jsr    (PlaySound).l    ; play LZ music
            bset    #0,$22(a0)
            addq.b    #2,$25(a0)
    
    You can add a line that sets $FFFFF7A7 below the jump to PlaySound. That will protect you from damage.

    But! You're still not safe from drowning. Easy fix! Add this line below the recently added line:

    Code:
            move.b  #30,($FFFFFE14).w
    That resets Sonic's air counter. Now, we need to make sure it straight up can not go down. Go to Obj0A_ReduceAir. That's the function that handles air reduction.

    Add these two lines right below the label:

    Code:
            tst.b   ($FFFFF7A7).w
            bne.s   Obj0A_DontReduceAir
    
    And add the Obj0A_DontReduceAir label right above the rts.

    Now Sonic won't drown, no matter how long his stay underwater!

    2.4) Star Light Zone

    We're back to the way standard bosses work.

    - Remove the existing lines in loc_18B52
    - Set $FFFFF7A7 in loc_18A46

    2.5) Final Zone

    Final Zone is a strange one.

    In REV00 (which is what the 2005 disassembly is based on), Final Zone doesn't even award you points. Let's look at the GitHub disassembly for REV01:

    Code:
    loc_19FBC:
            if Revision=0
            else
                moveq    #100,d0
                bsr.w    AddPoints
            endc
            move.b    #6,$34(a0)
            move.w    #$25C0,obX(a0)
            move.w    #$53C,obY(a0)
            move.b    #$14,obHeight(a0)
            rts    
    This code causes 1000 points to be awarded after the searchlight in Final Zone blows up. Which is a fair point where to declare the boss fight finished. I can't really come up with an effective way to protect Sonic from getting crushed (hence the "near" part in the title), so if you have suggestions, feel free to write them down.

    Anyway, we can set $FFFFF7A7 to 1 in loc_19FBC, and Sonic will still be protected from all forms of damage, since the boss fight is finished. (And, while you're at it, you can also implement the 1000 points bonus)

    However, I do recommend placing an invisible wall that prevents Sonic from falling off the level at the end of the fight, even though we've already implemented a protection against bottomless pits. You'll need to add an instance of Obj71 using your level editor of choice for that.

    And that's it! You've made Sonic near impossible to harm after defeating a boss fight in Sonic 1! This should prevent unfair accidents from occuring to players.

    ADDENDUM: What about Time Overs?

    Yeah, I completely forgot about Time Overs when writing the guide. That, too, is an easy fix. Just add a line that clears byte $FFFFFE1E, the byte that tells the game whether the timer should be updated or not, at the same locations where you would add the lines that set $FFFFF7A7 to 1. This will stop the timer right after the boss fight.
     
    Last edited: Dec 23, 2022