Turn the S Monitor into a Special Stage Key (Sonic 1)

Discussion in 'Tutorials' started by Speems, Nov 2, 2022.

  1. Speems

    Speems Well-Known Member Member

    Joined:
    Mar 14, 2017
    Messages:
    83
    Location:
    Rochester Hills, MI
    For those unaware, Sonic 1 has three unused monitor types. These are Eggman, S, and Goggles. While the Eggman and Goggles monitors can easily be reworked (the former is the most common), the S monitor seems to be roped in with either enabling both invincibility and speed shoes or quick super transformation with 50 rings on the fly. But sometimes people want to rework something into a different purpose, and this tutorial aims that way for the S monitor. We are going to change the functionality for the S monitor to act as a key to spawn the Special Stage portal at the signpost. This guide aims towards the GitHub disassembly, but it can be workshopped for the Hivebrain 2005 disassembly.

    Step 1 - S Monitor Reworking

    Open the "2E Monitor Content Power-Up" asm file in the folder _incObj and scroll down to Pow_ChkS.
    Code:
    Pow_ChkS:
            cmpi.b    #7,d0        ; does monitor contain 'S'?
            bne.s    Pow_ChkEnd
    
    After the bne.s command, insert the following lines of code (make sure nop is gone):
    Code:
            move.b    #1,(f_keycheck).w ; key is earned
            move.w    #sfx_Cash,d0
            jsr    (PlaySound_Special).l    ; play register sound
    If you're using the sfx macro, use this in place of the last two lines of the previous code:
    Code:
            sfx        sfx_Cash,1,0,0    ; play ring sound
    This inserts functionality to the monitor, much like the ones the game currently uses. The sfx code reflects a macro for sound effect playback. The Hivebrain equivalent is Obj2E_ChkS.

    Step 2 - Giant Ring Check Change

    We need to get rid of the 50 ring check to instead reflect the S monitor's new purpose. Open the "4B Giant Ring" asm in the same folder and scroll down to GRing_Main and you should see this code:
    Code:
    GRing_Main:    ; Routine 0
            move.l    #Map_GRing,obMap(a0)
            move.w    #$2400,obGfx(a0)
            ori.b    #4,obRender(a0)
            move.b    #$40,obActWid(a0)
            tst.b    obRender(a0)
            bpl.s    GRing_Animate
            cmpi.b    #6,(v_emeralds).w ; do you have 6 emeralds?
            beq.w    GRing_Delete    ; if yes, branch
            cmpi.w    #50,(v_rings).w    ; do you have at least 50 rings?
            bcc.s    GRing_Okay    ; if yes, branch
            rts    
    At the last two lines before the rts, swap em out for these following lines:
    Code:
            tst.b    (f_keycheck).w    ; did you get the key?
            bne.s    GRing_Okay    ; if yes, branch (only if above 0)
    We have replaced a check for 50 rings with a tst.b command checking for the key address' value. This change is necessary since it is instead checking for the necessary byte rather than a word. If it's at 1, then the GRing_Okay routine is ready to place the portal at the end of the level. And if it's at 0, then the routine doesn't occur. The Hivebrain equivalent is Obj4B_Main.

    Step 3 - Additional Check Tweak

    At this point, the Giant Ring object will load at the signpost when the S monitor is broken in the level. But what if you go to the next level and you'll see the portal appear again despite not breaking this monitor again? Same applies if you died after breaking this monitor in a level, and a similar problem occurs upon respawning at the start of a level or at a lamppost. Go to the sonic.asm at the root of the disassembly, scroll to Level_ClrObjRam and insert this line within the empty space:
    Code:
        clr.b    (f_keycheck).w    ; key is redeemed
    The result should resemble the following:
    Code:
        Level_ClrObjRam:
            move.l    d0,(a1)+
            dbf    d1,Level_ClrObjRam ; clear object RAM
            clr.b    (f_keycheck).w    ; key is earned
            lea    ($FFFFF628).w,a1
            moveq    #0,d0
            move.w    #$15,d1
    This makes sure the address is reset when the next level loads after a Special Stage or is reset from respawning. This is especially helpful considering this occurs in both scenarios.

    This label is the exact same in the Hivebrain 05 disassembly.

    Step 4 - Variable Equation (GitHub Specific)

    At the root of the disassembly, open the "Variables" asm and add this line at the very end of the file:
    Code:
    f_keycheck:    equ    $FFFFXXXX    ; key flag (replace XXXX with correct address)
    Considering the GitHub disassembly's way of handling ram address and their equations, this feels like a necessity when adding new functions to previously unused ram addresses, although the original Hivebrain disassembly can just use the ram address we're using in place of f_keycheck. Make sure to replace XXXX with the address you're planning to use.

    Conclusion

    At the end of all this, you can plop one S monitor in each act with the suggestion of exploration to gain access to Special Stages (especially since if you put more than one, it'd only work for the first one obtained). The tiles for the S icon in the monitor art can also be changed to reflect the new purpose, like a key or a Chaos Emerald for example. Make sure the ram address you're using is not used by any other process. You can check what's safe to use at this page, although caution may be advised since some information may not stack up with what is (un)used: https://info.sonicretro.org/SCHG:Sonic_the_Hedgehog_(16-bit)/RAM_Editing

    This was inspired by this guide that workshops functionality to the Goggles monitor, which is created by Selbi, theocas, and Inferno Gear: https://info.sonicretro.org/SCHG_How-to:Set_up_the_Goggle_Monitor_to_work_with_it

    UPDATE 11/03/2022 - Changed some steps to reflect optimization and improved code from @Giovanni and proper explanations of what the code is doing.
     
    Last edited: Nov 3, 2022
    Hame and JGamer2151 like this.
  2. Giovanni

    Giovanni It's Joe-vanni, not Geo-vanni. Member

    Joined:
    Apr 16, 2015
    Messages:
    311
    Location:
    Italy
    This message was posted in response to a previous version of the guide. The original text is as follows in the spoiler.

    This guide features multiple errors. Not only is this an unoptimized implementation, but it also brings its own set of issues to the table. I'll go over the various steps and explain what's going on that is wrong and/or could be improved.

    Step 1

    Step 1 is fine, functionality wise, just make sure you replace the nop with the code provided in the guide, as you won't be needing the former. Efficiency wise, the code can be improved by replacing the "jsr" with a "jmp". This will save up on stack usage and processing power.

    Github users! If you want to use macros, for the macro equivalent, replace this:

    Code:
            move.w    #sfx_Cash,d0
            jmp    (PlaySound_Special).l    ; play register sound
    with this:

    Code:
            sfx        sfx_Cash,1,0,0    ; play ring sound    
    The digit right after sfx_Cash being 1 means that there will be a "jmp" to PlaySound_Special, rather than a "jsr" to PlaySound_Special.

    The other 2 digits won't be of use to you, but I still recommend that you give the macro a read to understand how it works.

    Step 2

    Step 2 is also fine, functionality wise. Optimization wise, it's really not.

    First, using a cmpi to check for a value that will either be 0 or 1 is unnecessary. Whenever you're met with such occurences, you want to favor using tst instructions. A tst instruction is an instruction that works in a similar way as your average cmp instruction. However, it's made specifically for checking for zero.

    Functionally, these two instructions...

    Code:
        cmpi.b    #0,(v_example).w
        tst.b   (v_example).w
    ...do the exact same thing. However, for optimization purposes, you want to use the tst instruction. You might be wondering why this is relevant, since we're checking for 1, here. I'll explain in due time.

    Secondly, the branch. The guide does not mention, at any point, replacing the branch. Not replacing "bcc" in the original code is functionally fine. However, the "bcc" instruction is used for checking if a value is "higher or the same". Checking for a value that is "higher or the same as 1" when the value can be 0 or 1 is plain overkill.

    Assuming you are using the original guide's code, you could replace the "bcc.s" with a "beq.s". That would translate, in this case, to "branch if equal to 1", and you'd be fine. But we can do better. Remember the "tst" instruction? We can use it to check for zero, and then use a "bne.s".

    All that said, you can replace this ring check...

    Code:
            cmpi.w    #50,(v_rings).w    ; do you have at least 50 rings?
            bcc.s    GRing_Okay    ; if yes, branch
    ...with this:

    Code:
            tst.b    (f_keycheck).w     ; did you collect the Special Stage key? (is f_keycheck 0?)
            bne.s    GRing_Okay            ; if yes, branch (branch if f_keycheck is NOT 0)
    Now, let's look at this statement:

    Changing the check from being for a word to being for a byte is good. It is only one byte that the game needs to account for, unlike the two bytes the game normally checks for when checking for the ring count. But why does the game throw an address error when you walk into the signpost area?

    Step 4 (?)

    Yes, I skipped over Step 3, I'll get back to it in due time.

    However, let's look at the assigned RAM address for f_keycheck:

    Code:
    f_keycheck:    equ $FFFFF793    ; key flag
    Notice something... odd with this address? If you did, I apologize for this pun.

    Anyway, $FFFFF793 is an odd address. The 68k requires even addresses to be used for operations with words (2-byte sequences) and long-words (4-byte sequences). Operations with bytes can occur with even or odd addresses indifferently. But odd addresses can not be used for word and long-word operations. Hence, why it crashes when you enter the signpost area otherwise.

    Just a note! According to the Github disassembly...

    Code:
    v_btnpushtime2:    equ $FFFFF792    ; button push duration - in demo (2 bytes)
    ...hence, $FFFFF793 is already used for demos, unlike what is stated on the Sonic Retro RAM editing page for Sonic 1. While you likely won't incur in glitches, feel free to use a different address to avoid demo playback errors, should the player hit the S monitor (unless your hack doesn't have demos at all, in which case, go forth).

    Step 3

    This step is plain wrong.

    First, the instruction provided doesn't even set anything. The proper instruction you'd want to use is a "move", or a "clr", even. However, even with the cmpi.b, the whole implementation appears to work correctly. Why? That's because the entire step is useless, should you use a specific address for f_keycheck. Anything in the $F700-$F7FF group is fine, as it gets cleared during the level loading sequence, as shown by Level_ClrRam, more precisely, in these lines of code:

    Code:
            lea    (v_screenposx).w,a1
            moveq    #0,d0
            move.w    #$3F,d1
    
        Level_ClrVars2:
            move.l    d0,(a1)+
            dbf    d1,Level_ClrVars2 ; clear misc variables
    This is a loop that clears out the entire $F700-$F7FF group. There are more like it, so you have a high chance of picking a variable that resets itself when the level restarts. And if for whatever reason you're so unlucky that you can't pick a RAM address that resets itself, that's a very easy to solve problem! Just add an instruction that clears out whatever you want to use for f_keycheck.

    Step 1 (???)

    We're back to step 1, since I want to propose an additional improvement.

    Since we're checking for 0 or not 0, why set f_ringcheck to 1 specifically? We can use a not.b instruction. It will replace $0 with $FF, since we're switching all bits (%00000000) to their opposite (%11111111), but it doesn't really matter to us, since we're checking for 0 or not 0. It will eventually reset itself whenever the level is reloaded, given the above instructions.

    Anyway, go back to Pow_ChkS, and replace this...

    Code:
            move.b  #1,(f_keycheck).w ; key is earned
    
    ...with this:

    Code:
            not.b   (f_keycheck).w ; key is earned
    
    That is all.

    EDIT: That is, assuming you're restricting yourself to one monitor per level. If you aren't, replace the "not.b" with "st".

    not.b would reset f_keycheck, were you to hit an even number of monitors in the level, causing the player to unfairly lose access to the Special Stage. st will simply set the value to $FF, and keep it to that, no matter how many monitors you hit.
     
    Last edited: Nov 3, 2022
    DeltaWooloo likes this.
  3. Speems

    Speems Well-Known Member Member

    Joined:
    Mar 14, 2017
    Messages:
    83
    Location:
    Rochester Hills, MI
    Aight, I've updated my guide to reflect the new code, thanks for the feedback! :)
    The initial key check thingy was somewhat accidental when reviewing the goggle guide and applying it for the key btw
     
  4. Devon

    Devon Down you're going... down you're going... Member

    Joined:
    Aug 26, 2013
    Messages:
    1,372
    Location:
    your mom
    The "music" and "sfx" macros have been deprecated in the GitHub disassembly as of March 22, 2022.

    (Sorry in advance for being pedantic) Almost. "CMP" subtracts the source value from the destination value and sets flags according to the result, including carry and overflow. "TST" checks the source value and sets the zero and negative flags, while clearing the carry and overflow flags. But yea, using "tst.b dN" is better than "cmpi.b #0,dN", since it saves 2 bytes (doesn't have to store the "0"), and saves 4 cycles (doesn't have to read the "0" so that it can compare it with dN), and both would functionally be the same if you are only checking the zero flag and/or the negative flag.

    You can also use the "ST" (Set on True) instruction, which will set the destination to $FF (byte only). In general, I recommend you check out the entire "Scc" set of instructions, as they are conditional and could be used for some additional optimizations. "Set on True" in this instance means that it will always set the destination to $FF (since the condition for this instruction is true, whereas other conditions would set it to $00 if the condition is false).

    It really doesn't matter what you do, it'll work fine either way.
     
    Last edited: Nov 4, 2022
    ProjectFM and Giovanni like this.
  5. Giovanni

    Giovanni It's Joe-vanni, not Geo-vanni. Member

    Joined:
    Apr 16, 2015
    Messages:
    311
    Location:
    Italy
    Thank you for the corrections! I can appreciate all types of additional information, although I did mention the st instruction at the bottom of my original post, as I was already informed by someone else about the Scc set of instructions existing. I will eventually familiarize with them.
     
    Last edited: Nov 4, 2022