Everything That I Know About Sonic the Hedgehog's Source Code

Discussion in 'General Sonic Discussion' started by Clownacy, Mar 30, 2022.

  1. BenoitRen

    BenoitRen

    Tech Member
    455
    199
    43
    Now you know what kept me going for almost a year to decompile Sonic CD. :)
     
    • Like Like x 1
    • Agree Agree x 1
    • List
  2. Kilo

    Kilo

    That inbetween sprite from S&K's title screen Tech Member
    386
    390
    63
    Canada
    By the way, I can't remember for the life of me where I heard it. But I recall a source telling me about 5 or 6 years ago that Sonic 1's final build was made on April 30th 1991. We know it was at least April from the ROM header, but I don't know why the date April 30th specifically always stuck with me. Is anyone able to verify this or debunk it?
     
  3. Chimes

    Chimes

    The One SSG-EG Maniac Member
    635
    487
    63
    April 1991 is something I've heard from time to time, but I've never heard it being the 30th...
     
  4. BenoitRen

    BenoitRen

    Tech Member
    455
    199
    43
    According to the wiki, both the original Sonic the Hedgehog and Sonic 3 (& Knuckles) have a global variable called "Level reload flag" and "Level restart flag", respectively. I've been looking at Sonic CD's global variables for the official name.

    From my research, I've narrowed it down to these candidates: play_start, plflag.

    plflag seems unlikely, as it's also saved when moving past a checkpoint, and such a flag isn't mentioned as being saved there. The only possibility is that it's the screen resize routine counter, but the name doesn't seem appropriate. Value-wise, I guess it could fit, as its possible values are 0, 1, and 2.

    play_start is used as a bitfield. If bit 1 is set, divdevset() is called. If bit 0 is set, all kinds of values are reset to what they should be like when starting a new game (like lives being set to 3). Bit 1 is cleared if Sonic dies while no lampposts are activated and the time period is the present.

    Now I'm not so sure anymore.
     
    • Informative Informative x 1
    • List
  5. Kilo

    Kilo

    That inbetween sprite from S&K's title screen Tech Member
    386
    390
    63
    Canada
    The flag you're looking for is set in play00erase. It can only be 1 to indicate the level needs to be reset. And looking at play00erase in CD there's a flag set called gameflag. I believe this is what you're looking for.
    upload_2024-3-21_10-2-16.png
    upload_2024-3-21_10-2-27.png
    upload_2024-3-21_10-2-50.png
     
    • Informative Informative x 2
    • List
  6. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,262
    1,433
    93
    your mom
    It's neither.

    "plflag" indicates the "spawn mode" when starting a level. If it's 0, Sonic spawns from the start of the stage and resets the ring count and timer. It it's 1, Sonic spawns from the last checkpoint hit. If it's 2, Sonic spawns from the position set when time traveling.

    "play_start" keeps track of 2 flags, yes. Bit 0 is indeed set to indicate if a new game has started. Bit 1 keeps track of whether the title card should be loaded or not (which it doesn't when a checkpoint is hit or when Sonic is not in the Preset, yes).

    The ACTUAL value you are looking for is "gameflag". When you set it to 1, it restarts the level. What does make it slightly differ from the other games is that setting it to 2 goes to the next level, since Sonic CD needs to load a new file to go to the next level, and thus must handle it differently.

    EDIT: dammit Kilo u ninja'd me
     
    • Like Like x 2
    • Informative Informative x 2
    • List
  7. Kilo

    Kilo

    That inbetween sprite from S&K's title screen Tech Member
    386
    390
    63
    Canada
    So let's talk about macros why don't we. We know that sprite mappings were done by hand, as they use named labels and were included in an object's file bounds (For example, SCORE.ASM has the HUD's sprite mappings sandwiched between the HUD object and the functions to update the number tiles), but I think it's pretty fair to assume that it didn't look like this.
    Code (Text):
    1. scoresp0:
    2.         dc.b    10
    3.         dc.b    $80,$0d,$80,$00,$00    ;1:
    4.         dc.b    $80,$0d,$80,$18,$20    ;2:
    5.         dc.b    $80,$0d,$80,$20,$40    ;3:
    6.         dc.b    $90,$0d,$80,$10,$00    ;4:
    7.         dc.b    $90,$0d,$80,$28,$28    ;5:
    8.         dc.b    $a0,$0d,$80,$08,$00    ;6:
    9.         dc.b    $a0,$01,$80,$00,$20    ;7:
    10.         dc.b    $a0,$09,$80,$30,$30    ;8:
    11.         dc.b    $40,$05,$81,$0a,$00    ;9:
    12.         dc.b    $40,$0d,$81,$0e,$10    ;10:
    13.         dc.b    0
    It would have probably been more like what we have today with MapMacros.
    There's even partial evidence of it, as the we know debug tables use a macro called dcblw to simplify entries in comparison to the actual binary data.
    dcblw's name doesn't seem to have any significance, I think it's meant to represent a dc that does a byte, then a long, then a word... Which it doesn't for the debug tables, it does a long then 2 words. So it was probably modified off some other macro.

    Now while making the macro isn't the hard part, coming up with a name is. This is one that we'd have to get the source code from a Mega Drive game which may never happen, but in the mean time I'd like to propose one. This macro would've been used globally so it's name has to be meaningful unlike dcblw as that was private to EDIT.ASM. I'm going to personally propose dcpat or dcsp/dcspr. Would love to hear opinions though.
     
  8. BenoitRen

    BenoitRen

    Tech Member
    455
    199
    43
    Thanks, both of you! :)

    I've just added these three global variables to the wiki. Curiously, I can't find where in RAM plflag and play_start are stored. Maybe I'll come across them later.
     
  9. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,262
    1,433
    93
    your mom
    Neither of those exist in Sonic 3 & Knuckles, they are exclusive to Sonic CD.
     
    • Informative Informative x 4
    • Like Like x 2
    • List
  10. Brainulator

    Brainulator

    Regular garden-variety member Member
    When doing my own source code recreations that have never been shown off anywhere, I've used "spritepat" just to use something. As for the debug macro, I would not be surprised if the name is reflective of an earlier version of said macro. Take my suggestion with a grain of salt, of course.
     
  11. Kilo

    Kilo

    That inbetween sprite from S&K's title screen Tech Member
    386
    390
    63
    Canada
    imo, spritepat feels to long, I think dc definitely would've been part of it. spritepat also follows more of the naming convention of the mapping's label like playpat, ringpat, etc so it could be confused for an object's mapping address rather than a macro.
     
  12. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,262
    1,433
    93
    your mom
    The first 3 parameters of the macro do fit the name (object ID (byte), sprite data address (long), tile ID/properties (word)). The other 2 parameters are tacked on the end.
    Code (ASM):
    1. dcblw    macro    \1,\2,\3,\4,\5
    2.     dc.l    (\1)*$1000000+(\2)
    3.     dc.w    (\4)+(\5)*$100
    4.     dc.w    (\3)
    5.     endm
    Code (ASM):
    1.     dcblw    ring_act,ringpat,$26bc,0,$00

    So, perhaps the original macro only had the first 3 parameters at first, with the subtype and frame ID parameters being added later in development? It's even supported by the fact that in the debug mode code, the frame ID is set after the sprite data address/object ID combo value (object ID is buffered into the top byte, since addresses are only 24-bit, so they can do that without issue, saving a little bit of space and code in the process) and the tile ID/properties. The original code also has a commented out line that sets the subtype after that as well (it was changed so that the subtype is applied to the object as it is spawned, rather than buffering it in the player object's variable space like it does with the ID in the upper byte of the sprite data address).
    Code (ASM):
    1.      moveq     #0,d0
    2.      move.b    editno,d0
    3.      lsl.w     #3,d0
    4.      move.l    0(a2,d0.w),patbase(a0)
    5.      move.w    6(a2,d0.w),sproffset(a0)
    6.      move.b    5(a2,d0.w),patno(a0)
    7. ;    move.b    4(a2,d0.w),userflag(a0)

    ("*" changed to ";" for code box highlighting purposes)

    Object spawn code:
    Code (ASM):
    1.     move.w    xposi(a0),xposi(a1)
    2.     move.w    yposi(a0),yposi(a1)
    3.     move.b    patbase(a0),actno(a1)
    4.     move.b    actflg(a0),actflg(a1)
    5.     move.b    actflg(a0),cddat(a1)
    6.     andi.b    #$7f,cddat(a1)
    7.     moveq     #0,d0
    8.     move.b    editno,d0
    9.     lsl.w     #3,d0
    10.     move.b    4(a2,d0.w),userflag(a1)

    (FYI, the sprite data address, tile ID/properties, and sprite frame values don't get set for the spawned object, because the object itself handles that. They are just there for displaying which object is selected in debug mode.)

    The EDIT.ASM code is definitely worth a look for some extra tidbits as well.
     
    Last edited: Mar 22, 2024
    • Informative Informative x 2
    • Agree Agree x 1
    • List
  13. BenoitRen

    BenoitRen

    Tech Member
    455
    199
    43
    I've encountered an almost identical piece of code in Sonic & Knuckles Collection twice, now. Do you know if this exists in Sonic CD? I haven't been able to find it by searching for 640 (sum of 128+320+192). I'd like to borrow its function name. :)

    Never mind, I found it while double-checking before posting. It's frameoutchk!
    Code (C):
    1. void frameoutchk(sprite_status* pActwk) {
    2.   short xposi;
    3.   short scra_h;
    4.   unsigned char index;
    5.  
    6.   xposi = pActwk->xposi.w.h;
    7.   xposi &= -128;
    8.   scra_h = scra_h_posit.w.h;
    9.   scra_h -= 128;
    10.   scra_h &= -128;
    11.   xposi -= scra_h;
    12.   if (xposi < 641)
    13.    {
    14.     actionsub(pActwk);
    15.   }
    16.   else
    17.   {
    18.     index = pActwk->cdsts;
    19.     if (index) flagwork[index] &= 127;
    20.     frameout(pActwk);
    21.   }
    22. }
    EDIT: This is the S&KC version:
    Code (C):
    1. void frameoutchk(sprite_status* p_actwk) {
    2.   unsigned short screen_xposi = util__to_unsigned(LONG_HIGH_TO_SHORT(p_actwk->xposi)) & 0xFF80;
    3.   screen_xposi -= Screen_x_no;
    4.   if (screen_xposi <= 640) {
    5.     actionsub(p_actwk);
    6.   }
    7.   else {
    8.     if (SPRITE_STATUS_USHORT(p_actwk, 72) != 0) {
    9.       CLEAR_BIT_7(DAT_008549d4[SPRITE_STATUS_USHORT(p_actwk, 72)]);
    10.     }
    11.     frameout(p_actwk);
    12.   }
    13. }
    EDIT2: Ohh, this means that the "respawn table" is flagwork!
     
    Last edited: Mar 22, 2024
  14. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,262
    1,433
    93
    your mom
    Yeah, in Sonic 1, while there was that function, a good chunk of objects inlined that code (one reason is to properly handle despawning child objects, if they exist).
     
    • Informative Informative x 1
    • List
  15. BenoitRen

    BenoitRen

    Tech Member
    455
    199
    43
    The reason I've encountered it twice in S&KC is because, next to frameoutchk, there's an almost identical piece of code that, if the object is on screen, also adds it to the collision response list. That version has the name Sprite_CheckDeleteTouch3 in the disassembly.
     
  16. Brainulator

    Brainulator

    Regular garden-variety member Member
    Not to mention, in Sonic 1, frameoutchk was part of the Motobug's code. There are also minor differences in some cases, most of which involve jumping directly to frameout instead of running the respawn list check. One thing I noticed is that Roller's code uses a bgt instead of a bhi, the difference being that bgt is a signed comparison while bhi is unsigned. If I had to guess, this is because the Roller moves at high speeds and spawns to Sonic's left, offscreen.
     
  17. BenoitRen

    BenoitRen

    Tech Member
    455
    199
    43
    I'm porting all of the code related to rings, and I've been making good progress. However, now I'm stumped because of RAM address shenanigans.

    Luckily, I have Sonic CD's version to fall back on. This is the start of function flyringmove:
    Code (C):
    1. void flyringmove(sprite_status* pActwk) {
    2.   short d1;
    3.  
    4.  
    5.  
    6.   speedset2(pActwk);
    7.   if ((pActwk->yspeed.w += 24) >= 0) {
    8.     if (!((systemtimer.b.b4 + ((pActwk - actwk) / 68U)) & 3)) {
    I double-checked, and the calculation is meant to look like that. But why? Why is it calculating the index of pActwk in the actwk array, and then dividing that further by 68 (the size of the object)? Then it's ANDing the result by 3 (7 in S&KC), and adding it to a value that "Counts the number of times V-int has run.".

    If I look at the disassembly, the equivalent is adding the value of register d7 to systemtimer. Looking up d7, this is the most likely candidate:
    Code (ASM):
    1. moveq    #((Level_object_RAM+object_size)-(Dynamic_object_RAM+object_size))/object_size-1,d7
    ((0xB000 + 0x4A) - (0xB0DE + 0x4A)) / 0x4A - 1

    Isn't that calculation doing the same as pointer arithmetic? Makes me think the Sonic CD version is broken.
     
  18. Devon

    Devon

    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    1,262
    1,433
    93
    your mom
    The reason for that code is for performance reasons. Running up to 32 rings that bounce and check for floor collision is rather time consuming on the CPU, so what they opted to do to improve performance is spread out the timing of when the rings perform floor collision detection.

    In Sonic 1 and Sonic CD, the ANDing of 3 effectively makes it so that only 1/4th of the bouncing ring objects performs floor collision detection in a single frame. In Sonic 2 and 3K, this was changed to 1/8th instead via ANDing 7. The usage of the ring's current object slot ID and frame/V-BLANK counter is how the game cycles through which of the rings to perform floor collision detection with.

    In the original 68000 code, it uses register d7, which is used as the loop counter in "action" for iterating through each object slot, to determine which object slot "ID" the ring is using to perform. In the ported C code, that value isn't passed into the actual object functions, so instead, they opted to manually calculate this ID with pointer arithmetic to get more or less the same result.

    This isn't the line you are looking for, however. That is used for when Sonic is dead and the stage objects get paused. For normal gameplay, it uses this instead to update every object in Process_Sprites ("action"), which calculates the total number of object slots in RAM to update (minus 1 for use with the DBF instruction):
    Code (ASM):
    1. moveq    #(Object_RAM_end-Object_RAM)/object_size-1,d7

    As it goes through each object slot and performs an update, d7 gradually decreases until it reaches -1, which is how it can be used as a cheap way to determine which object slot an object is in. And again, the usage of the frame/V-BLANK counter is what effectively performs the actual cycling of rings that do floor collision detection.
     
    Last edited: Mar 25, 2024
    • Informative Informative x 2
    • Like Like x 1
    • List
  19. MainMemory

    MainMemory

    Kate the Wolf Tech Member
    4,744
    338
    63
    SonLVL
    This reminds me that it'd be easy to add a patch to the S&KC mod loader to process all the ring collision every frame, since PCs are more than capable of it.
     
  20. jbr

    jbr

    Member
    73
    29
    18
    That's really interesting. Does that mean there's a risk some rings will clip through the floor or walls if they're unlucky with their collision check timing?