don't click here

Theory on Unused Sonic 3 Abilities (and Other Things)

Discussion in 'Engineering & Reverse Engineering' started by Alex Field, Jul 20, 2024.

  1. The Sonic 3 prototype is rather well known for having many abilities that didn't make it into the final; of course, the most infamous of these is the proto-"Drop Dash" you can perform in-game, but there are also two unused abilities that can only be accessed through hacking. Silver Sonic 1992 covered both of them in the video below, but I will summarize them both here as well:

    • The first of these abilities is a multidirectional air dash, unreferenced by any code but present at $B8B8. A code analysis by Silver Sonic 1992 found out that this was an early version of Hyper Sonic's air dash in Sonic 3 & Knuckles; it functions similarly to the final, but does not yet destroy all enemies on screen and creates camera problems due to flaws with how the camera movement was programmed into it. It also, strangely, has a check to ensure the camera moves for Tails (at this point, Tails is more similar to his Sonic 2 programming than Sonic 3), meaning that he likely too was meant to use it in some manner.
    • The second of these abilities should get called by the Sonic_JumpHeight code, but is stopped right before by a branch to a return command; disabling said branch and pressing A in the air causes Sonic to shoot out a ring in both directions without deducting from his ring count. This should in theory also destroy badniks, monitors, and even collect rings, however it (for some bizarre reason) makes reference to Sonic's collision code, meaning it often fails to detect anything. This wasn't repurposed for anything in the final.
    With how similar the Hyper Dash is in the final, I'll only be posting the ring throw code here.
    Code (Text):
    1. Sonic_ThrowRings:
    2.         btst    #2,Obj_Status(a0)
    3.         beq.w    Offset_0x00B8B6
    4.         move.b    (Control_Ports_Logical_Data+1).w,d0
    5.         andi.b    #$20,d0
    6.         beq.s    Offset_0x00B83A
    7.         move.w    Obj_Speed_X(a0),d2
    8.         bsr.w    SingleObjectLoad
    9.         bne.w    Offset_0x00B80C
    10.         bsr.w    Obj_ThrownRing
    11.         move.w    #$800,Obj_Speed_X(a1)
    12.         move.w    #0,Obj_Speed_Y(a1)
    13.         add.w    d2,Obj_Speed_X(a1)
    14.         bsr.w    SingleObjectLoad
    15.         bne.w    Offset_0x00B80C
    16.         bsr.w    Obj_ThrownRing
    17.         move.w    #-$800,Obj_Speed_X(a1)
    18.         move.w    #0,Obj_Speed_Y(a1)
    19.         add.w    d2,Obj_Speed_X(a1)
    20.  
    21. Offset_0x00B80C:
    22.         btst    #2,Obj_Status(a0)
    23.         beq.s    Offset_0x00B83A
    24.         bclr    #2,Obj_Status(a0)
    25.         move.b    Obj_Height_2(a0),d0
    26.         move.b    Obj_Height_3(a0),Obj_Height_2(a0)
    27.         move.b    Obj_Width_3(a0),Obj_Width_2(a0)
    28.         move.b    #0,Obj_Ani_Number(a0)
    29.         sub.b    Obj_Height_3(a0),d0
    30.         ext.w    d0
    31.         add.w    d0,Obj_Y(a0)
    32.  
    33. Offset_0x00B83A:
    34.         move.b    (Control_Ports_Logical_Data+1).w,d0
    35.         andi.b    #$10,d0
    36.         beq.s    Offset_0x00B8B6
    37.         move.w    Obj_Speed_X(a0),d2
    38.         bsr.w    SingleObjectLoad
    39.         bne.w    Offset_0x00B870
    40.         bsr.w    Obj_ThrownRing
    41.         move.w    #$800,Obj_Speed_X(a1)
    42.         btst    #0,Obj_Status(a0)
    43.         beq.s    Offset_0x00B866
    44.         neg.w    Obj_Speed_X(a1)
    45.  
    46. Offset_0x00B866:
    47.         move.w    #0,Obj_Speed_Y(a1)
    48.         add.w    d2,Obj_Speed_X(a1)
    49.  
    50. Offset_0x00B870:
    51.         btst    #2,Obj_Status(a0)
    52.         beq.s    Offset_0x00B89E
    53.         bclr    #2,Obj_Status(a0)
    54.         move.b    Obj_Height_2(a0),d0
    55.         move.b    Obj_Height_3(a0),Obj_Height_2(a0)
    56.         move.b    Obj_Width_3(a0),Obj_Width_2(a0)
    57.         move.b    #0,Obj_Ani_Number(a0)
    58.         sub.b    Obj_Height_3(a0),d0
    59.         ext.w    d0
    60.         add.w    d0,Obj_Y(a0)
    61.  
    62. Offset_0x00B89E:
    63.         move.w    #0,Obj_Speed_Y(a0)
    64.         move.w    #$200,d0
    65.         btst    #0,Obj_Status(a0)
    66.         bne.s    Offset_0x00B8B2
    67.         neg.w    d0
    68.  
    69. Offset_0x00B8B2:
    70.         add.w    d0,Obj_Speed_X(A0)
    71.  
    72. Offset_0x00B8B6:
    73.         rts
    74. ; End of subroutine Sonic_ThrowRings
    Code (Text):
    1. ; ===========================================================================
    2. ; ---------------------------------------------------------------------------
    3. ; Object - Thrown Ring
    4. ; ---------------------------------------------------------------------------
    5. ; Offset_0x00B994:
    6. Obj_ThrownRing:
    7.         move.l    #ThrownRing_LoadIndex,(a1)
    8.         move.w    Obj_X(a0),Obj_X(a1)
    9.         move.w    Obj_Y(a0),Obj_Y(a1)
    10.         move.l    #Rings_Mappings,Obj_Map(a1)
    11.         move.w    #$26BC,Obj_Art_VRAM(a1)
    12.         move.b    #$84,Obj_Flags(a1)
    13.         move.w    #$180,Obj_Priority(a1)
    14.         move.b    #8,Obj_Width(a1)
    15.         rts
    16. ;-------------------------------------------------------------------------------
    17. ; Offset_0x00B9C8:
    18. ThrownRing_LoadIndex:
    19.         moveq    #0,d0
    20.         move.b    Obj_Routine(a0),d0
    21.         move.w    ThrownRing_Index(pc,d0.w),d1
    22.         jmp    ThrownRing_Index(pc,d1.w)
    23. ; ===========================================================================
    24. ; Offset_0x00B9D6:
    25. ThrownRing_Index:
    26.         dc.w    ThrownRing_Init-ThrownRing_Index
    27.         dc.w    ThrownRing_Main-ThrownRing_Index
    28. ; ===========================================================================
    29. ; Offset_0x00B9DA:
    30. ThrownRing_Init:
    31.         addq.b    #2,Obj_Routine(a0)
    32.         move.b    #2,Obj_Ani_Number(a0)
    33.         move.b    #8,Obj_Height_2(a0)
    34.         move.b    #8,Obj_Width_2(a0)
    35.         bset    #1,Obj_Player_Status(a0)
    36. ;-------------------------------------------------------------------------------
    37. ; Offset_0x00B9F6:
    38. ThrownRing_Main:
    39.         move.l    a0,a2
    40.         jsr    (Touch_Response).l
    41.         cmpi.b    #2,Obj_Routine(a0)
    42.         beq.s    Offset_0x00BA10
    43.         nop
    44.         nop
    45.         nop
    46.         nop
    47.         nop
    48.  
    49. Offset_0x00BA10:
    50.         cmpi.b    #2,Obj_Ani_Number(a0)
    51.         beq.s    Offset_0x00BA22
    52.         nop
    53.         nop
    54.         nop
    55.         nop
    56.         nop
    57.  
    58. Offset_0x00BA22:
    59.         move.b    (Vertical_Interrupt_Count+3).w,d0
    60.         andi.w    #3,d0
    61.         bne.s    Offset_0x00BA36
    62.         addq.b    #1,Obj_Map_Id(a0)
    63.         andi.b    #3,Obj_Map_Id(a0)
    64.  
    65. Offset_0x00BA36:
    66.         bsr.w    SpeedToPos
    67.         tst.b   Obj_Flags(A0)
    68.         bpl.w    DeleteObject
    69.         bra.w    DisplaySprite
    Also take notice of the "nop" code in the thrown rings; "nop" stands for "no operation" and is typically used for code requiring precise CPU timing, however they are also often used by developers (especially in the Genesis Sonic games) to blank out any code that was there. As a result, the thrown rings likely had some secondary purpose that is now long gone.

    Now here's where the mystery comes into play...

    For a long time, concept art for Sonic 3 was extremely limited, that is until the release of Sonic Origins and tons of never before seen pieces. Among these were two scraps showing unused shield ideas for the game:
    [​IMG] [​IMG]
    Now a lot of these on their own are nothing special, and indeed many of them did get used in the final just condensed down. However for this, we are interested in two; Flame (#1) and Thunder (#2). Their descriptions, translated, reads "attacks" and "wipes out enemies, takes time to charge."

    My theory is that the unused abilities in the prototype (the air dash and the ring throw) were, in fact, programmed takes on these abilities; for the air dash, it is often theorized that Hyper Sonic was created as a result of the game split as an incentive to complete the Sonic & Knuckles Special Stages (although I have found no sources to indicate this was ever confirmed, please correct me if I'm wrong), but its presence here creates a hole in that theory. However, with the fact that the Thunder ability was seemingly repurposed for Hyper Sonic in S3K, it's possible that here, it was the original Lightning Shield's double-jump ability.

    As for the ring throw, it is a bit harder to explain, however it's possible this was an early Fire Shield ability. The evidence here is admittedly a bit looser than the above, but another thing about the above images is that "Bubble" (#3) and "Water" (#4) had no specified double-jump ability, instead they would've allowed you breathe underwater, and move faster underwater respectively; if we do process by elimination, that means that the ring throw ability could only really have been meant for the Fire Shield. As for why it's a ring instead of a fireball, it was a placeholder asset until a proper graphic could be created; Sonic 2 did something similar with the debug pathswapper objects also using the ring graphics.

    Now it's important to stress that none of this is confirmed (nor likely will be in the future given... Naka's unavailable), so do not take any of this theorizing as truth. The only reason I made these connections was using vague context clues, however at the end of the day, only Sega truly knows what these were meant for (if even that).
     
    • Like Like x 8
    • Informative Informative x 2
    • Useful Useful x 1
    • List
  2. There is one thing I think is worth noting though, and that’s base Mecha Sonic’s attacks.

    Specifically, while two of his attacks (his air dash and his bounce) match up with the Fire and Bubble Shields respectively, his third attack better matches up with the Drop Dash.

    Following the pattern set by the other attacks and their corresponding Shields, this would suggest that the Drop Dash may have originally been the move associated with the Lightning Shield.
     
  3. That is also true, and something I never noticed, although that does raise the question on why it's enabled by default for Sonic in the prototype while the other two abilities are intentionally disabled.

    It's not even like the dropdash is even that hard to disable; how it works is that in the jump code, it sets a number to -1, then triggers it once the "Sonic_ResetOnFloor" routine is thrown. Disabling either more-or-less disables the entire thing.
    (ignore the variable name; Esrael probably got confused while labelling the variables)
    upload_2024-7-22_22-18-38.png
     
  4. BenoitRen

    BenoitRen

    Tech Member
    772
    380
    63
    The ring throw and the mention of Mecha Sonic remind me of his 360 degree ring throw while he's super.
     
    • Like Like x 2
    • Agree Agree x 1
    • List
  5. Another thing is that the drop dash is available in Competition Mode, unlike all other abilities which lack any code for them specifically in the prototype. It seems more like the drop dash was intended to be a regular ability for Sonic in general rather than a shield ability, as no shields exist in Competition.
     
    • Informative Informative x 7
    • Like Like x 2
    • List
  6. (double posting)
    A sort of addendum I am writing for a few more theories that came to my mind while I'm documenting NyperYuhgard's disassembly:
    • Internally, the Ball Shooter is after the Beam Rocket in both ID, and the position of its code. Taking into account that the Beam Rocket has a different spawn position in the prototype (towards the middle of the stage), is it possible the Launch Base 2 was only having one boss before the split happened? This also would explain why Knuckles never fights it in S3&K, nor Big Arms in the Limited Edition prototypes (Big Arms is completely missing in 1103, not even having any art present).
    • Another observation is how disorganized the object list is towards the end. $B8 to to $C4 are all taken up by IceCap objects, only for it to devolve into the Tunnelbot, hidden monitors (which aren't even used yet), the capsules, Knuckles in the intro, the IceCap trampoline supports, Knuckles' switch (incl. the LBZ cutscene), and the final intro. This suggests that all these objects were added in a rush with no attempt made at organization.
     
    • Like Like x 1
    • Informative Informative x 1
    • List
  7. sayonararobocop

    sayonararobocop

    Member
    257
    107
    43
    I think the lack of Big Arms in 1103 is highly interesting - perhaps they decided to add a Final Boss in once it became clear that Sonic 3 would be split.

    Ball Shooter after Beam Rocket is curious, I always thought personally that Ball Shooter was a strangely easy and out of place boss fight. I wonder if perhaps it was conceived for Knuckles because his lower jump height would probably make it more challenging.
     
  8. Eh, I don't think so for Knuckles as he doesn't fight it at all in his route and there's no indication in either the prototype or the final that he was ever meant to fight it. I just think that Ball Shooter was conceived at the last second to extend Launch Base ever so slightly.
    [​IMG]
    I should also bring up this piece of concept art in Origins, which shows that the path to the Death Egg boss was intended to simply be Sonic running along a beam followed by a single boss, so it seems clear that Big Arms was added at the last second to give the game some conclusion, also explaining why they never implemented the "if Super Sonic, do not hurt" check.

    Might also start using this thread to post other coding oddities, such as an unused Sonic 2-like sound queue system. If you're unaware, Sonic 2 had a strange sound queue setup where each one got assigned a piece of ram in 68k memory, which when called is then run through an input system (Sonic 2 GitHub labels this as "sndDriverInput") to be used by the Z80... this was really just slow and unnecessary, and was replaced with a more optimized one for Sonic 3. However, it seems like the developers initially retained this as the code still exists.

    In Sonic 3 Alone, all the code below the return command was removed, however the return command was still referenced despite now being completely useless (complete with all the Z80 stops!). The dummy command was finally removed in Sonic & Knuckles, but that STILL left in the Z80 stops.
    upload_2024-7-29_22-1-24.png
     
    • Like Like x 1
    • Informative Informative x 1
    • List
  9. Blue Spikeball

    Blue Spikeball

    Member
    2,493
    1,051
    93
    Isn't that boss in the upper right corner of the image an early Big Arms?
     
  10. Clownacy

    Clownacy

    Tech Member
    1,093
    666
    93
    Is it really? I thought the idea was to minimise the number of times that the Z80 bus is requested by only doing it in the vertical interrupt handler. On top of minimising the amount of time spent waiting for the Z80 bus to be obtained, this also has the benefit of avoiding a race condition: it is possible for the vertical interrupt to occur right after the Z80 bus is requested, but before it is used for anything; since the interrupt handler releases the Z80 bus, this will cause writes to the Z80 RAM to fail, resulting in sounds or even songs to simply not play. Even worse, if the interrupt occurs after the bus is requested, but not after the loop that waits for the bus to be obtained, then the loop will repeat endlessly and the game will freeze. I have no idea why Sonic 3's developers scrapped Sonic 2's system.
     
    • Like Like x 1
    • Agree Agree x 1
    • List
  11. Devon

    Devon

    DROWN, DROWN, DROWN MYSELF! Tech Member
    1,396
    1,690
    93
    your mom
    I'd also like to point out, in relation to the sound queue, that Sonic CD also has it for its FM sound effects driver, which runs on the Z80.
     
    • Informative Informative x 1
    • List
  12. Ah, that makes sense.

    Also, take a wild guess who's the unlucky one trying to disassemble the 1103 Z80 driver? (I barely know Z80)
    upload_2024-7-30_18-59-7.png
     
  13. Brainulator

    Brainulator

    Regular garden-variety member Member
  14. The Z80 driver has been fully disassembled! (almost, data shifting still creates problems) Therefore, let me document other oddities with the driver I've found. Firstly, despite Volume Envelope $25 already existing in the data, it goes unreferenced by the table; $26 is missing entirely.
    upload_2024-8-2_14-2-10.png
    Many sound drivers including Sonic 1 and 2 had a 'priority' system which determines what sounds should not get interrupted (usually the continue sound); Sonic 3 & Knuckles removed this entirely, however the table for it still exists, although the rest of the code seems to have been gutted out already. I am unsure what the $E1852 data block is meant to be.
    upload_2024-8-2_14-3-36.png
    Sonic 1 also had a system called "special SFX" which was used exclusively by Green Hill's waterfalls to play it continually (and the prototype had a few more of these); the final Sonic 3 driver has some remnants from an attempted reimplementation of the system, before it was scrapped in favour of the continuous SFX system. As it turns out, the coordination flag for it does exist (replaced in the final by its continuous SFX flag), but is simply a return command at this point.
    upload_2024-8-2_14-6-28.png
    And finally, you're more than well aware of the horrible sound glitches this prototype has; as it turns out, it's because cfStopTrack doesn't perform the needed bankswitch to the sound bank, relying on the zPlaySound function itself to perform the switch, which it often fails to do.
    upload_2024-8-2_14-8-38.png
     
    • Informative Informative x 1
    • List
  15. Clownacy

    Clownacy

    Tech Member
    1,093
    666
    93
    The $E1852 data block doesn't have anything to do with that table: SoundDriverLoad only loads $1852 bytes of data, stopping short of Offset_0x0E1852. That table is really just referencing RAM address 0x1852, which is an empty space. It would appear that the developers had already reserved space for the universal voice bank, but not filled it in yet. I couldn't tell you what the data at Offset_0x0E1852 is, unfortunately. It appears to start with a single Z80 pointer, but that's all that I can figure out.

    I wouldn't call the driver's Special SFX support a 'partial reimplementation' of S1's system; vanilla SMPS has pretty much always had Special SFX support built in, the Sonic drivers just tend to remove it as part of their customisation.
     
    • Like Like x 2
    • Informative Informative x 2
    • List
  16. Fair point; I just called it "re-implementation" here since I am very, very unfamiliar with Z80 drivers. As for other things I have found, the developers were already in the middle of implementing PAL optimization (although, the final has a bug which still causes the wrong tempo):
    upload_2024-8-3_20-45-55.png
    That value that is called once in the final is called more here; I've called it "zSoundIndex" so far, but I will admit I'm not sure if that is a fully accurate descriptor:
    upload_2024-8-3_20-46-41.png upload_2024-8-3_20-47-3.png
    (also called in cfStopTrack)

    zGetPointerTable seemingly uses a RAM variable to get the sound driver pointers; I say seemingly because the bit-perfect decompilation uses RAM $1C02 for it, even though the pointers are located at $1200. Unless there's a Z80 quirk I'm missing:
    upload_2024-8-3_20-48-3.png
     

    Attached Files:

    Last edited: Aug 3, 2024
    • Informative Informative x 1
    • List
  17. Double-posting for two reasons. First is that I've created a small hack of the prototype to fix the bankswitch failure, meaning all the irritating sound glitches will no longer occur (missing music restores are caused by a missing feature).

    And secondly, I think I've figured out how zPointerTable works; both the 1103 and Final driver load a default set of 'variables', except that the final have them all set to $00, making it worthless. Here, however, $1C02 is set to $1200, which is the location of the sound driver pointers are. Since the Z80 needs them byte-swapped, it is listed as $0012 in the 68000 memory.
    upload_2024-8-10_19-48-24.png
    In the final, it instead uses an unused part of the Z80 memory to set a pointer to it... even though it can just directly call it.
     

    Attached Files:

    • Informative Informative x 2
    • List