don't click here

Sonic CD Quirks/Deconstruction

Discussion in 'Engineering & Reverse Engineering' started by Devon, Jul 11, 2022.

  1. Devon

    Devon

    DROWN, DROWN, DROWN MYSELF! Tech Member
    1,388
    1,676
    93
    your mom
    Hey, did you know that checkpoints don't exist or even function at all in the past or future time zones? In fact, the game even has code that forces Sonic to spawn at the beginning of the stage if you're not in the present when he dies.

    [​IMG]
     
    • Informative Informative x 9
    • Like Like x 2
    • List
  2. DigitalDuck

    DigitalDuck

    Arriving four years late. Member
    5,403
    487
    63
    Lincs, UK
    TurBoa, S1RL
    I genuinely never noticed this and it didn't seem right to me, but I went and checked and it's absolutely true - the present time zones have checkpoints pretty liberally placed all over, but they're completely absent in every other time zone. Incredible that I never noticed it before.
     
  3. Blue Spikeball

    Blue Spikeball

    Member
    2,480
    1,038
    93
    Same. It blows my mind that I never noticed this.

    Why on earth would they do that?
     
  4. nineko

    nineko

    I am the Holy Cat Tech Member
    6,350
    509
    93
    italy
    Since checkpoints have an internal order (most noticeable in Emerald Hill Zone), I guess they just didn't know how to handle multiple checkpoints at potentially different locations in different timezones, and they didn't feel like using two more bytes of RAM. But it's only a guess.
     
  5. Devon

    Devon

    DROWN, DROWN, DROWN MYSELF! Tech Member
    1,388
    1,676
    93
    your mom
    Here's a bonus fun fact. In Palmtree Panic act 1, in the present, the checkpoint object is set to ID 0x13, but in the other time zones, they are set to ID 0x2B. The checkpoints in the present for that stage wouldn't have even shown up for the other time zones because of this!
     
  6. The Joebro64

    The Joebro64

    SAY HELLO TO MY CHOCOLATE BLEND Member
    3,190
    2,855
    93
    I always noticed you'd restart at the beginning if you died or restarted in the past, but it'd never occurred to me that was why. Interesting!
     
  7. BenoitRen

    BenoitRen

    Tech Member
    771
    379
    63
    I think I found the relevant code for the C port in PLAYER.C, function play00erase. A "checkpoint" is apparently known as a "marker" there:
    Code (Text):
    1. if (markerno == 0 && time_flag == 1) play_start &= 253;
    2.  
    3.  
    4. if (pl_suu != 0) {
    5.   if (time_flag == 1) {
    6.     if (markerno != 0) plflag = 1;
    7.   } else {
    8.     plflag = 0;
    9.   }
    10. }
    11. sub_sync(14);
     
    • Like Like x 1
    • Informative Informative x 1
    • List
  8. Brainulator

    Brainulator

    Regular garden-variety member Member
    I knew about the restarting thing for a while now... maybe I've just played the 2011 version quite a bit.

    If I had to guess, I suspect that it would be a problem if, after time-traveling, respawning at a checkpoint's coordinates would lead to you being stuck inside a wall or something.
     
  9. BenoitRen

    BenoitRen

    Tech Member
    771
    379
    63
    There's an inconsistency in the code for the first boss in the C version. I wonder if it's also in the Mega CD original.

    In file BOSS_1.C, function egg1leg1_11, lines 2961 and 2962, it retrieves the ID of a sub-object:
    Code (Text):
    1. subact = pActwk->actfree[6];
    2. subact = actwk[subact].actfree[6];
    What's wrong with it? Usually these IDs are retrieved as a short, not as a char, like this:
    Code (Text):
    1. subact = ((short*)pActwk)[26];
    The maximum ID is 128, which fits in one byte, so at first glance this isn't an issue. However, on a big-endian system (like the GameCube), this would retrieve the high byte, which would be zero.
     
    • Like Like x 1
    • Informative Informative x 1
    • List
  10. Devon

    Devon

    DROWN, DROWN, DROWN MYSELF! Tech Member
    1,388
    1,676
    93
    your mom
    That looks like a bug they made while translating the code. The original game treats it like the latter.
    Code (Text):
    1. ROM:0020D0EE egg1leg1_11:
    2. ROM:0020D0EE         btst    #0,actfree+2(a0)
    3. ROM:0020D0F4         bne.s   loc_20D12E
    4. ROM:0020D0F6         movea.w actfree+6(a0),a1
    5. ROM:0020D0FA         movea.w actfree+6(a1),a1

    (Here, the values are direct 16-bit addresses in work RAM rather than offsets in the "actwk" pool, since they get sign extended, but it's basically the same logic)
     
    Last edited: Dec 3, 2023
    • Informative Informative x 1
    • List
  11. BenoitRen

    BenoitRen

    Tech Member
    771
    379
    63
    I haven't seen this documented anywhere, so here goes: Sonic CD's Time Attack menu replaces any high score name that consists of "AAA" by "YOU".
     
    • Informative Informative x 5
    • Like Like x 1
    • List
  12. Blastfrog

    Blastfrog

    See ya starside. Member
    Well, that's not fair to anyone who genuinely has those initials! What of poor old Allen Adam Abbot?
     
  13. saxman

    saxman

    Oldbie Tech Member
    Much like "SEX" in Super Hang-On. Discrimination!
     
    • Like Like x 1
    • Agree Agree x 1
    • List
  14. OrionNavattan

    OrionNavattan

    Tech Member
    174
    171
    43
    Oregon
    Adapting the Sub CPU's command manager in the SP Extension for use in my Mode 1 error handler's test ROM revealed a small but destructive oversight within it:
    Code (ASM):
    1. .WaitCommand:
    2.    move.w   GACOMCMD0.w,d0           ; Get command ID from Main CPU
    3.    beq.s   .WaitCommand
    4.    cmp.w   GACOMCMD0.w,d0
    5.    bne.s   .WaitCommand
    6.    cmpi.w   #(.SPCmdsEnd-.SPCmds)/2+1,d0   ; Note: that "+1" shouldn't be there
    7.    bcc.w   SPCmdFinish           ; If it's an invalid ID, crash the sub CPU!
    8.  
    9.    move.w   d0,d1               ; Execute command
    10.    add.w   d0,d0
    11.    move.w   .SPCmds(pc,d0.w),d0
    12.    jsr   .SPCmds(pc,d0.w)

    The intent of the cmpi.w and bcc.w is to allow the system to gracefully handle the possibility of the main CPU passing an invalid command ID by acknowledging the main CPU using the invalid ID. However, they bungled it completely since SPCmdFinish is meant to be one level higher on the call stack (specifically, branched to by command handlers that have been called via the jsr). The sub CPU's stack pointer is at its initial value ($10000) while in the command wait loop, so as it stands, passing an invalid command from the main CPU leads to a stack underflow, with the first four bytes of whatever sub CPU code file was previously run loaded into the PC as a return address. Granted, this could have been a means for the developers to detect invalid commands, but the v0.02 proto's version of this routine doesn't perform any validation of commands, and I can't find any disassembles of any other protos offhand to see what they did, so I only assume this was unintentional. (The +1 in the cmpi.w is also a mistake, it was meant to produce the ID of the first invalid command, but is not necessary here due to the presence of a 'dc.w 0' dummy entry at the start of the command table.)

    Anyhow, to fix this bug, we need to rework the routine so that SPCmdFinish is called rather than branched to for invalid commands. The following is how I fixed it in my test ROM (which also changes the validation check slightly and eliminates the need for a dummy entry at the start of the command table):
    Code (ASM):
    1. MainLoop:
    2.        move.w   (mcd_maincom_0).w,d0   ; get command ID from main CPU
    3.        beq.s   MainLoop               ; wait if not set
    4.        cmp.w   (mcd_maincom_0).w,d0   ; why do this (a safeguard against spurious writes)?
    5.        bne.s   MainLoop
    6.        cmpi.w   #sizeof_SubCPUCmd_Index/2,d0   ; is it a valid command?
    7.        bhi.s   .invalid               ; branch if not
    8.  
    9.      ; move.w d0,d1   ; required in the original code, since the id is used by the LoadLevel command
    10.        add.w   d0,d0
    11.        move.w   SubCPUCmd_Index-2(pc,d0.w),d0   ; minus 2 since IDs start at 1; eliminates the need for a 'dc.w 0' dummy entry
    12.        jsr   SubCPUCmd_Index(pc,d0.w)       ; run the command
    13.        bra.s   MainLoop
    14. ; ===========================================================================
    15.  
    16. .invalid:
    17.        bsr.s   CmdFinish   ; call the command finish routine instead of branching
    18.        bra.s   MainLoop
     
    Last edited: Jan 22, 2024
  15. Devon

    Devon

    DROWN, DROWN, DROWN MYSELF! Tech Member
    1,388
    1,676
    93
    your mom
    That "+1" was a mistake, because "(.SPCmdsEnd-.SPCmds)/2" ends refers to whatever data *after* the table, because ".SPCmdsEnd" is placed after the last entry. Even then, because the original command table does include a dummy entry for command ID 0, so there's no need for that offset to begin with.

    You also need that "move.w d0,d1", because the level loading function relies on it.
     
    Last edited: Jan 22, 2024
  16. OrionNavattan

    OrionNavattan

    Tech Member
    174
    171
    43
    Oregon
    You're right, I made a math mistake there (this is what happens when you're tired). It's been fixed.
    I removed the move.w d0,d1 since it wasn't needed for what I was doing, but I've added a clarification.
     
  17. BenoitRen

    BenoitRen

    Tech Member
    771
    379
    63
    In Collision Chaos/R3, there is an object called "Miracle", and the only thing it does is disappear (calls frameout). Anyone have any idea what it is?
     
  18. MarkeyJester

    MarkeyJester

    Original, No substitute Resident Jester
    2,235
    491
    63
    Japan
    Might be a good idea to share the object code so those of us (who are unable to actively acquire the source at this current moment to investigate) can help d=
     
  19. Devon

    Devon

    DROWN, DROWN, DROWN MYSELF! Tech Member
    1,388
    1,676
    93
    your mom
    I *think* it might be this object that's found in the 510 prototype. Its code was completely dummied out in the final. Does it happen to be ID 0x2B?
     
    Last edited: Jan 27, 2024
  20. BenoitRen

    BenoitRen

    Tech Member
    771
    379
    63
    There isn't anything more to share, unfortunately. This is literally all there is in the file:
    Code (Text):
    1. void miracle(sprite_status* actionwk) {
    2.   frameout(actionwk);
    3. }