don't click here

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

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

  1. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,252
    1,425
    93
    your mom
    Yes.
     
    • Like Like x 1
    • Informative Informative x 1
    • List
  2. BenoitRen

    BenoitRen

    Tech Member
    420
    186
    43
    Thanks for the detailed explanation, Devon. :)
    The ported C code's version doesn't just calculate the ID, though, which is why I was confused. I think it was ported incorrectly:
    Code (C):
    1. if (!((systemtimer.b.b4 + ((pActwk - actwk) / 68U)) & 3)) {
    This should be:
    Code (C):
    1. if (!((systemtimer.b.b4 + (pActwk - actwk)) & 3)) {
    EDIT: Or even:
    Code (C):
    1. if (!((systemtimer.b.b4 + (pActwk - actwk - 1)) & 3)) {
     
    Last edited: Mar 25, 2024
  3. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,252
    1,425
    93
    your mom
    The division is there to convert its offset from the start of the object pool to an index value, which is comparable to using the object update loop counter from register d7. Though, this is assuming the subtraction treats both values as plain raw addresses, so if that's NOT the case (i.e. the compiler doing the division automatically), then that's a bug, yes.

    Also, if you wanted it to be more accurate, you'd wanna subtract the object's slot address from the end of the object pool, divide that into an index value, then subtract 1, since register d7 in the original code counted down as it went from the start to the end of the object pool.
     
    Last edited: Mar 25, 2024
  4. BenoitRen

    BenoitRen

    Tech Member
    420
    186
    43
    The compiler indeed does the division automatically. This is the equivalent MIPS ASM of Sonic CD's if statement:
    Code (ASM):
    1.  
    2. lw           v1,32(sp)
    3. lui          v0,hi(actwk+260)
    4. addiu        v0,v0,lo(actwk+18032)
    5. subu         v1,v1,v0
    6. li           v0,68
    7. div          v1,v0
    8. bne          v0,$0,*+12
    9. nop        
    10. break        7
    11. mflo         v1
    12. li           v0,68
    13. divu         v1,v0
    14. bne          v0,$0,*+12
    15. nop        
    16. break        7
    17. mflo         v1
    18. lui          v0,hi(systemtimer+261)
    19. lb           v0,lo(systemtimer)(v0)
    20. dsll32       v0,v0,24
    21. dsra32       v0,v0,24
    22. addu         v0,v0,v1
    23. andi         v0,v0,0x3
    24. bne          v0,$0,*+148
    For accuracy, would it be enough to have my own version of Process_Sprites count backwards just like the original? Then I don't need to change the code of flyringmove.
     
  5. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,252
    1,425
    93
    your mom
    Yup, that's a bug alright.

    And just as long as you still go from the start of the object pool to the end still and have the counter passed onto the object functions themselves, then yeah.
     
  6. BenoitRen

    BenoitRen

    Tech Member
    420
    186
    43
    At the tail end of Sonic 3's code for the animal capsule, I found a reference to the act results code. As a result, that's the next chunk I've been investigating. A chunk that I don't have labels for, so I have to name stuff myself (eek!).

    In Sonic 1 and Sonic CD, the code for the end of the level is spread between the goal and clear function sets. goal calculates the bonus scores, while clear adds them in small chunks to the player's score.

    Come Sonic 2, goal doesn't seem to exist anymore. When searching for "clear", I find masinclear. masin is Sonic 2's term for the animal capsules.

    I don't know how it's done in Sonic 2, but in Sonic 3 the functionality of goal and clear seems to have been combined into one object. As a result, I have no good idea for what to name it.

    There's also another object that is responsible for the numbers, and takes control of the player characters.
     
  7. Chimes

    Chimes

    The One SSG-EG Maniac Member
    626
    483
    63
    masin = マシン (mashin)?
     
  8. Brainulator

    Brainulator

    Regular garden-variety member Member
    Yeah, I figured out that it's a Japanified way of saying "machine".
     
  9. Kilo

    Kilo

    That inbetween sprite from S&K's title screen Tech Member
    334
    339
    63
    Canada
    In Sonic 1, the capsule or signpost object, the results object, and the HUD update function (
    scoreset) called by the scene's main loop work together to handle the end of level sequence.

    It starts with the signpost/capsule setting a flag to stop the timer, which is generally also to indicate we are in the end of level sequence, it then locks the player's control and forces them to run to the right, all done within either the capsule or the signpost's code.

    Then they both call a function in disasms called GotThroughAct. Which sets the left side boundary to the same as the right side boundary, locking the screen. Then loads a results card object. And then calculates the time and ring bonuses. And finally plays the end of level music.

    After the results card initializes and moves into place, it sets the flag which tells scoreset to update the bonus counter number tiles, and works away at ticking down the time bonus, followed by the ring bonus while they get added to the player's current score. Once it's done with that, the title card will get the next level ID the game should go to from LevelOrder, if the next level for whatever reason should be GHZ1 (You can't go back to the first level, of course), then it'll make the game reset, likely for demo builds.
     
    • Informative Informative x 1
    • List
  10. Kilo

    Kilo

    That inbetween sprite from S&K's title screen Tech Member
    334
    339
    63
    Canada
    So it's pretty well known by now that sprite mappings were done by hand, but let's take a look at some sprites that make this clear.

    Both the press start button text and GHZ's spike helix have a clever method of saving a single byte for frames that are intended to be blank. What they do is point to the actual sprite data, rather than the start of the sprite list, where a 0 is placed to indicate there are 0 sprites for the entry.
    Code (Text):
    1.         dc.w TtlSprtMaps_PSB+1-TitleSprite_Maps        ; Points to the 0 AFTER the sprite count, effectively displaying 0 sprites, or a blank frame.
    2.         dc.w TtlSprtMaps_PSB-TitleSprite_Maps
    3.         dc.w TtlSprtMaps_Filler-TitleSprite_Maps
    4.         dc.w TtlSprtMaps_TM-TitleSprite_Maps
    5. TtlSprtMaps_PSB:
    6.         dc.b 6
    7.         dc.b 0,    $C, 0, $F0, 0
    8.         dc.b 0,    0, 0, $F3, $20
    9.         dc.b 0,    0, 0, $F3, $30
    10.         dc.b 0,    $C, 0, $F4, $38
    11.         dc.b 0,    8, 0, $F8, $60
    12.         dc.b 0,    8, 0, $FB, $78
    Code (Text):
    1.         dc.w HelixMaps_Frame1-SpikeHelix_Maps, HelixMaps_Frame2-SpikeHelix_Maps
    2.         dc.w HelixMaps_Frame3-SpikeHelix_Maps, HelixMaps_Frame4-SpikeHelix_Maps
    3.         dc.w HelixMaps_Frame5-SpikeHelix_Maps, HelixMaps_Frame6-SpikeHelix_Maps
    4.         dc.w HelixMaps_Frame7+2-SpikeHelix_Maps    ; Similarily, points to the 0
    5.         dc.w HelixMaps_Frame7-SpikeHelix_Maps
    6. HelixMaps_Frame1:    dc.b 1
    7.         dc.b $F0, 1, 0,    0, $FC
    8. HelixMaps_Frame2:    dc.b 1
    9.         dc.b $F5, 5, 0,    2, $F8
    10. HelixMaps_Frame3:    dc.b 1
    11.         dc.b $F8, 5, 0,    6, $F8
    12. HelixMaps_Frame4:    dc.b 1
    13.         dc.b $FB, 5, 0,    $A, $F8
    14. HelixMaps_Frame5:    dc.b 1
    15.         dc.b 0,    1, 0, $E, $FC
    16. HelixMaps_Frame6:    dc.b 1
    17.         dc.b 4,    0, 0, $10, $FD
    18. HelixMaps_Frame7:    dc.b 1
    19.         dc.b $F4, 0, 0,    $11, $FD
    20.         even
    This was something I came to realize while converting the mapping files in my project to binaries and had this happen when I got to the spike helix. Because as it turns out, Flex 2 can't properly handle strange situations like this, not that I expect it to, since this is something that could only have happened by hand.
    upload_2024-4-13_15-5-14.png
    The shield and invincibility stars also do something similar.
    None of this is news, but I thought it was cool to share evidence about something some may still have had doubts about.
     
    • Like Like x 2
    • Informative Informative x 2
    • List
  11. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,252
    1,425
    93
    your mom
    I would also say that the graphical glitches with Sonic in the v0.02 prototype of Sonic CD point to this as well.
     
  12. Brainulator

    Brainulator

    Regular garden-variety member Member
    Care to elaborate?
     
  13. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,252
    1,425
    93
    your mom
    In the v0.02 prototype, a number of Sonic's frames are glitched out, particularly the 0 degree walking sprites, the 45 degree angle running sprites, the "I'm outta here" sprites, and the spinning disc sprites, all due to problems in the mappings/DPLC data. You can see them in this sheet. To me, they can definitely be the result of human error, especially when combined with the existing evidence that the data was made by hand.
     
    • Informative Informative x 2
    • List
  14. Brainulator

    Brainulator

    Regular garden-variety member Member
    What I meant was that I wanted to see the mapping and DPLC data for myself.
     
  15. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,252
    1,425
    93
    your mom
    When I get the chance, I can take a look. I've been out all day for a concert out of state and I need time to rest first.
     
  16. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,252
    1,425
    93
    your mom
    So, upon inspection, I noticed that all the of glitches stem from bad DPLC data. Let's take a look, shall we?

    Case 1: "I'm outta here" last frame

    The sprite mappings and DPLC data is actually fine here. It's just that the maximum amount of tiles allowed to be loaded for a sprite is 0x17, which this sprite exceeds, particularly due to the fact that the tiles get loaded into work RAM first before being DMA'd into VRAM, in which there isn't a whole lot of space to work with there. Not really much we can do outside of refactoring how the tiles are loaded.

    [​IMG]

    Case 2: Spinning disc (front)


    Here, the first 2 DPLC entries erroneously load an extra tile than it should, which throws off the sprite mappings.

    [​IMG]

    Code (ASM):
    1. byte_22E08A:    dc.b 4
    2.                 dc.w $2179        ; Should be $1179
    3.                 dc.w $217B        ; Should be $117B
    4.                 dc.w $217D
    5.                 dc.w $8180

    Fix those values, and we get this:

    [​IMG]

    Case 3: Spinning disc (back)

    Similar issue as before. The first DPLC entry loads 3 more tiles than it should.

    [​IMG]

    Code (ASM):
    1. byte_22E09C:    dc.b 3
    2.                 dc.w $8189        ; Should be $5189
    3.                 dc.w $218F
    4.                 dc.w $8192

    Fix that value, and we get this:

    [​IMG]

    Case 4: Spinning disc (front left)

    This one isn't really visible, but there's a few errors with its DPLC entries.

    First off, the last entry loads an extra tile. Since it's the last entry though, this doesn't really affect anything.

    [​IMG]

    However, there's something curious about all of the entries. You see, this particular sprite is a horizontally flipped version of another sprite. What's interesting is that it seems they tried to apply the sprite X flip flag to the DPLC entries, because the graphics tile offset are all offset by 0x800, which is the same offset used for sprite tile IDs to make them flipped.

    Code (ASM):
    1. byte_22E094:    dc.b 3
    2.                 dc.w $B966        ; Should be $B166
    3.                 dc.w $5972        ; Should be $5172
    4.                 dc.w $1978        ; Should be $178

    Now, the offset of 0x800 here doesn't really make any visible impact, because it gets shifted out when it's converted into a byte offset. You see, when multiplying, it doesn't actually get expanded into a longword, it remains a regular word, which is why the bit is shifted out.

    Code (ASM):
    1.                 moveq   #0,d2                           ; Get first byte of entry
    2.                 move.b  (a2)+,d2
    3.                 move.w  d2,d0                           ; Get number of tiles
    4.                 lsr.b   #4,d0
    5.                 lsl.w   #8,d2                           ; Get tile offset in graphics data
    6.                 move.b  (a2)+,d2
    7.                 lsl.w   #5,d2                           ; Multiply by 0x20 (as a word)
    8.                 lea     Art_Sonic,a1                    ; Add to base graphics data address
    9.                 adda.l  d2,a1

    Case 5: Spinning disc (back left)

    [​IMG]

    The exact same issue as the previous one, except that it doesn't load extra tiles.

    Code (ASM):
    1. byte_22E0AA:    dc.b 2
    2.                 dc.w $F99B        ; Should be $F19B
    3.                 dc.w $39AB        ; Should be $31AB

    Case 6: Walking sprite

    With this one, the last DPLC entry actually loads 1 LESS tile that it should. The glitched tile is a leftover from the previous walking sprite displayed.

    [​IMG]

    Code (ASM):
    1. byte_22E12A:    dc.b 3
    2.                 dc.w $2090
    3.                 dc.w $B2FD
    4.                 dc.w $1309        ; Should be $2309
    Fix that value, and we get this:

    [​IMG]

    Case 7: Angled walking sprite

    Another case of extra tiles being loaded and throwing off the sprite mappings. This time 4 extra tiles are loaded in one of the DPLC entries.

    [​IMG]

    Code (ASM):
    1. byte_22E156:    dc.b 6
    2.                 dc.w $333A
    3.                 dc.w $133E
    4.                 dc.w $B340        ; Should be $7340
    5.                 dc.w $348
    6.                 dc.w $349
    7.                 dc.w $234A

    Fix that value and we get this:

    [​IMG]

    Case 8: Angled running sprite #1

    Another case where a DPLC entry loads 1 less tile that is should.

    [​IMG]

    Code (ASM):
    1. byte_22E27E:    dc.b 4
    2.                 dc.w $34EF
    3.                 dc.w $4F3
    4.                 dc.w $14F4        ; Should be $24F4
    5.                 dc.w $B4F7

    Fix that value, and we get this:

    [​IMG]

    Case 9: Angled running sprite #2

    Basically the exact same issue as the last one. Seems to have been copied and pasted and then modified.

    [​IMG]

    Code (ASM):
    1. byte_22E296:    dc.b 4
    2.                 dc.w $34EF
    3.                 dc.w $4F3
    4.                 dc.w $14F4        ; Should be $24F4
    5.                 dc.w $B50F

    Fix that value, and we get this:

    [​IMG]

    Bonus case: Shield sprite in the final game

    This one isn't really an error, but it's definitely a clear sign that the sprite mappings were done by hand. Basically, like some objects in Sonic 1, for the blank sprite, it uses a 0 that's found inside another sprite.

    Code (ASM):
    1. byte_23EEB6:    dc.b 4
    2.                 dc.b $E8, $A, 0, 0, $E8
    3.                 dc.b $E8,  $A, 0, 9, 0
    4. byte_23EEC1:    dc.b 0, $A, $10, 0, $E8        ; Blank sprite uses this "0" from this sprite for the piece count
    5.                 dc.b 0, $A, $10, 9, 0

    Conclusion

    So, those are some extra tidbits from Sonic CD that I believe help point to the fact that the sprites were done by hand, particularly since only a select set of frames have correct sprite mappings, but broken DPLC entries. There's just not enough consistency to believe that a tool did all this, either.
     
    Last edited: Apr 14, 2024
    • Like Like x 4
    • Informative Informative x 4
    • Agree Agree x 1
    • Useful Useful x 1
    • List
  17. Kilo

    Kilo

    That inbetween sprite from S&K's title screen Tech Member
    334
    339
    63
    Canada
    This is probably nothing, but I wonder if anyone's got an answer.

    The RNG routine will make a seed if the input value is 0. The seed it uses in particular is 0x2A6D365A. I've tried throwing it around in different formats, in decimal it's 711,800,410 which almost seems like a messed up phone number? Or a birthday? In ASCII it's *m6Z, which is... something.

    I could also be overthinking it, it is meant to be random after all. But this seems like the kind of thing that a developer would stick an easter egg into.
    Also worth keeping in mind is that this is most likely a library that multiple games used so it may not even be relevant to Sonic in particular.
     
  18. BenoitRen

    BenoitRen

    Tech Member
    420
    186
    43
    Starting Sonic 3, the seed is 0x2A6D365B (711800411). Yeah, they just added 1.
     
  19. Kilo

    Kilo

    That inbetween sprite from S&K's title screen Tech Member
    334
    339
    63
    Canada
    Actually, that was starting with Sonic & Knuckles. Sonic 3 alone still uses 0x2A6D365A.
     
    • Like Like x 1
    • Informative Informative x 1
    • List
  20. Devon

    Devon

    I'm a loser, baby, so why don't you kill me? Tech Member
    1,252
    1,425
    93
    your mom
    I don't think it has any meaning. Just a random set of digits. Also yeah, other Genesis games have the exact same function.