don't click here

Sonic CD decompilation

Discussion in 'Engineering & Reverse Engineering' started by BenoitRen, Jul 17, 2023.

  1. Devon


    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    your mom
    I described what the object was in the quirks thread. It used to have code in the prototype.
  2. BenoitRen


    Tech Member
    Sorry, I was under the impression that the evidence was inconclusive, given that we don't even have an object ID.
  3. Devon


    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    your mom
    That object is defined in the object index at ID 0x2B in the Sega CD version, at least.
  4. BenoitRen


    Tech Member
    All the source code related to R4, Tidal Tempest, has been committed!

    Each zone has more files to decompile. R1 has 66 files. R3 has 76 files. R4 has a whopping 101 files! In the case of Tidal Tempest, it's not only because of more objects, but also because it has water.

    The zones usually share a GAME.C file which contains all of the initialisation code, and a PLAYSUB.C file that contains code for common zone-related objects like sign posts and flowers. R4, however, has its own copy of these to have custom code for water.

    R4 also has its own version of SCRCHK.C, but only for the first act's present.

    Now, as for my nemesis, scrolling code...

    R3's files for scrolling management were largely identical. It's mostly the scroll() function that differed each time. Thankfully, whoever did R4 did some optimisation.

    The scroll() function used to be a 200-line monster. Now, it's 70 lines for the first act's present, 50 lines for the rest, and most of the time it's identical. Some files were even merged; R42A and R42B, and R42C and R42D, share the same scrolling file. They could have gone even further, as the files are still largely identical, but for some reason they didn't.
    Wait, you mean the position in the index, not its "actno"? I took a look, and it's indeed in the index at position 43 (0x2B)!
  5. Devon


    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    your mom
    It is this object. Basically, 3 of these teleporters circle around, and when Sonic enters one, he gets teleported to a nearby teleporter. Again, it existed in the 510 prototype, but ultimately got removed, with its graphics being left behind.


    (With graphical fix)


    Here's a disassembly of the object from the prototype, done in the style of the source code/decompilation:
    Code (ASM):
    1. ; -------------------------------------------------------------------------
    2. ; Teleporter object
    3. ; -------------------------------------------------------------------------
    5. miracle:
    6.         moveq   #0,d0                           ; Run routine
    7.         move.b  r_no0(a0),d0
    8.         move.w  miracle_move_tbl(pc,d0.w),d0
    9.         jsr     miracle_move_tbl(pc,d0.w)
    11.         addq.w  #1,actfree+4(a0)                ; Get sine and cosine of angle
    12.         move.w  actfree+4(a0),d0
    13.         jsr     sinset
    14.         asr.w   #1,d1
    15.         asr.w   #1,d0
    16.         addq.w  #8,d1
    17.         addq.w  #8,d0
    18.         asr.w   #1,d1
    19.         asr.w   #1,d0
    20.         add.w   actfree(a0),d1                  ; Add origin point
    21.         add.w   actfree+2(a0),d0
    22.         move.w  d1,xposi(a0)                    ; Set position
    23.         move.w  d0,yposi(a0)
    25.         movea.w actfree+6(a0),a1                ; Has one of the teleporters despawned?
    26.         cmpi.b  #$2B,actno(a1)
    27.         beq.w   .check2                         ; If not, branch
    28.         jmp     frameout                        ; If so, delete ourselves
    30. .check2:
    31.         movea.w actfree+8(a0),a1                ; Has one of the teleporters despawned?
    32.         cmpi.b  #$2B,actno(a1)
    33.         beq.w   .draw                           ; If not, branch
    34.         jmp     frameout                        ; If so, delete ourselves
    36. .draw:
    37.         jsr     actionsub                       ; Draw sprite
    38.         move.w  actfree+$14(a0),d0              ; Check despawn
    39.         jmp     frameout_s00
    41. ; -------------------------------------------------------------------------
    43. miracle_move_tbl:
    44.         dc.w    miracle_init-miracle_move_tbl
    45.         dc.w    miracle_checkcoli-miracle_move_tbl
    46.         dc.w    miracle_reset-miracle_move_tbl
    47.         dc.w    miracle_release-miracle_move_tbl
    48.         dc.w    miracle_flash0-miracle_move_tbl
    49.         dc.w    miracle_flash1-miracle_move_tbl
    50.         dc.w    miracle_flash2-miracle_move_tbl
    51.         dc.w    miracle_settarget-miracle_move_tbl
    53. ; -------------------------------------------------------------------------
    54. ; Initialize
    55. ; -------------------------------------------------------------------------
    57. miracle_init:
    58.         jsr     actwkchk                        ; Allocate object slot
    59.         beq.w   .spawn3                         ; If it was successful, branch
    60.         lea     (a0),a1                         ; If it wasn't, just use ourselves
    62. .spawn3:
    63.         move.b  actno(a0),actno(a1)             ; Set object ID
    64.         lea     (a1),a2                         ; Save object slot pointer
    66.         jsr     actwkchk                        ; Allocate object slot
    67.         beq.w   .spawn2                         ; If it was successful, branch
    68.         lea     (a0),a1                         ; If it wasn't, just use ourselves
    70. .spawn2:
    71.         move.b  actno(a0),actno(a1)             ; Set object ID
    73.         move.w  #0,actfree+4(a0)                ; Set first teleporter's angle
    74.         move.w  a1,actfree+6(a0)                ; Set teleporter object links
    75.         move.w  a2,actfree+8(a0)
    77.         move.w  #$55,actfree+4(a1)              ; Set second teleporter's angle
    78.         move.w  a0,actfree+6(a1)                ; Set teleporter object links
    79.         move.w  a2,actfree+8(a1)
    81.         move.w  #$AA,actfree+4(a2)              ; Set third teleporter's angle
    82.         move.w  a0,actfree+6(a2)                ; Set teleporter object links
    83.         move.w  a1,actfree+8(a2)
    85.         lea     (a0),a6                         ; Set up first teleporter
    86.         bsr.w   miracle_setup
    87.         lea     (a1),a6                         ; Set up second teleporter
    88.         bsr.w   miracle_setup
    89.         lea     (a2),a6                         ; Set up third teleporter
    91. ; -------------------------------------------------------------------------
    92. ; Set up teleporter
    93. ; -------------------------------------------------------------------------
    95. miracle_setup:
    96.         ori.b   #4,actflg(a6)                   ; Set sprite flags
    97.         move.b  #3,sprpri(a6)                   ; Set priority
    98.         move.b  #24,sprhs(a6)                   ; Set sprite width
    99.         move.b  #16,sprvsize(a6)                ; Set sprite height
    100.         move.l  #miracle_pat,patbase(a6)        ; Set sprite data
    101.         move.b  #2,r_no0(a6)                    ; Advance routine
    103.         move.w  #$3AF,d0                        ; Base tile ID
    104.         cmpi.b  #2,stageno                      ; Are we in act 3? (BUG: should be "stageno+1")
    105.         bne.s   .set_tile                       ; If not, branch
    106.         move.w  #$41C,d0                        ; If so, use other base tile ID
    108. .set_tile:
    109.         move.w  d0,sproffset(a6)
    111.         move.w  xposi(a0),actfree+$14(a6)       ; Set despawn check position
    112.         move.w  xposi(a0),actfree(a6)           ; Set origin point X
    113.         move.w  yposi(a0),actfree+2(a6)         ; Set origin point Y
    114.         rts
    116. ; -------------------------------------------------------------------------
    117. ; Check collision with the player
    118. ; -------------------------------------------------------------------------
    120. miracle_checkcoli:
    121.         lea     actwk,a6                        ; Check collision with player 1
    122.         bsr.w   miracle_coli
    123.         tst.w   actfree+$A(a0)                  ; Has there been a collision?
    124.         bne.w   .collided                       ; If so, branch
    126.         lea     actwk+$40,a6                    ; Check collision with player 2
    127.         bsr.w   miracle_coli
    128.         tst.w   actfree+$A(a0)                  ; Has there been a collision?
    129.         bne.w   .collided                       ; If so, branch
    130.         rts
    132. .collided:
    133.         move.b  #8,r_no0(a0)                    ; Start flashing
    134.         move.w  #21,actfree+$C(a0)              ; Set flash timer
    136.         movea.w actfree+6(a0),a1                ; Disable collision in the second teleporter
    137.         move.b  #4,r_no0(a1)
    138.         move.w  #0,actfree+$C(a1)
    140.         movea.w actfree+8(a0),a1                ; Disable collision in the third teleporter
    141.         move.b  #4,r_no0(a1)
    142.         move.w  #0,actfree+$C(a1)
    143.         rts
    145. ; -------------------------------------------------------------------------
    147. miracle_coli:
    148.         tst.w   yspeed(a6)                      ; Is the player moving upwards?
    149.         bmi.w   .end                            ; If so, branch
    151.         move.w  yposi(a6),d0                    ; Check vertical collision
    152.         sub.w   yposi(a0),d0
    153.         subi.w  #-24,d0
    154.         subi.w  #16,d0
    155.         bcc.w   .end                            ; If there was no collision, branch
    157.         move.w  xposi(a6),d0                    ; Check horizontal collision
    158.         sub.w   xposi(a0),d0
    159.         subi.w  #-16,d0
    160.         subi.w  #32,d0
    161.         bcc.w   .end                            ; If there was no collision, branch
    163.         move.w  a6,actfree+$A(a0)               ; Set link to player object
    164.         bset    #0,actfree+2(a6)                ; Shift downwards to next odd pixel
    166. .end:
    167.         rts
    169. ; -------------------------------------------------------------------------
    170. ; Check reset
    171. ; -------------------------------------------------------------------------
    173. miracle_reset:
    174.         addi.w  #-1,actfree+$C(a0)              ; Decrement wait timer
    175.         bne.w   .end                            ; If it hasn't run out, branch
    177.         move.b  #2,r_no0(a0)                    ; Enable collision in first teleporter
    178.         movea.w actfree+6(a0),a1                ; Enable collision in second teleporter
    179.         move.b  #2,r_no0(a1)
    180.         movea.w actfree+8(a0),a1                ; Enable collision in third teleporter
    181.         move.b  #2,r_no0(a1)
    183. .end:
    184.         rts
    186. ; -------------------------------------------------------------------------
    187. ; Release the player
    188. ; -------------------------------------------------------------------------
    190. miracle_release:
    191.         addi.w  #-1,actfree+$C(a0)              ; Decrement wait timer
    192.         bne.w   .flash                          ; If it hasn't run out, branch
    194.         move.b  #4,r_no0(a0)                    ; Set reset time
    195.         move.w  #61,actfree+$C(a0)
    196.         move.b  #0,patno(a0)                    ; Stop flashing
    198.         movea.w actfree+$A(a0),a1               ; Get player object
    199.         move.w  #0,actfree+$A(a0)               ; Unlink it
    201.         bset    #2,cddat(a1)                    ; Set player's rolling flag
    202.         move.w  #0,xspeed(a1)                   ; Halt the player horizontally
    203.         move.w  #-$700,yspeed(a1)               ; Make the player jump up
    205.         move.w  xposi(a0),xposi(a1)             ; Move player to us
    206.         move.w  yposi(a0),d0
    207.         addi.w  #-$10,d0
    208.         move.w  d0,yposi(a1)
    210.         bclr    #0,actfree+2(a1)                ; Shift upwards to previous even pixel
    211.         rts
    213. .flash:
    214.         lea     miracle_pchg(pc),a1             ; Flash sprite
    215.         jmp     patchg
    217. ; -------------------------------------------------------------------------
    218. ; Flash #1
    219. ; -------------------------------------------------------------------------
    221. miracle_flash0:
    222.         addi.w  #-1,actfree+$C(a0)              ; Decrement wait timer
    223.         bne.w   .flash                          ; If it hasn't run out, branch
    225.         addq.b  #2,r_no0(a0)                    ; Advance routine
    226.         move.w  #21,actfree+$C(a0)              ; Set flash timer
    228. .flash:
    229.         lea     miracle_pchg(pc),a1             ; Flash sprite
    230.         jmp     patchg
    232. ; -------------------------------------------------------------------------
    233. ; Flash #2
    234. ; -------------------------------------------------------------------------
    236. miracle_flash1:
    237.         addi.w  #-1,actfree+$C(a0)              ; Decrement wait timer
    238.         bne.w   .flash                          ; If it hasn't run out, branch
    240.         addq.b  #2,r_no0(a0)                    ; Advance routine
    241.         move.w  #21,actfree+$C(a0)              ; Set flash timer
    243. .flash:
    244.         lea     miracle_pchg(pc),a1             ; Flash sprite
    245.         jmp     patchg
    247. ; -------------------------------------------------------------------------
    248. ; Flash #3
    249. ; -------------------------------------------------------------------------
    251. miracle_flash2:
    252.         addi.w  #-1,actfree+$C(a0)              ; Decrement wait timer
    253.         bne.w   .flash                          ; If it hasn't run out, branch
    255.         addq.b  #2,r_no0(a0)                    ; Advance routine
    256.         move.w  #21,actfree+$C(a0)              ; Set flash timer
    258. .flash:
    259.         lea     miracle_pchg(pc),a1             ; Flash sprite
    260.         jmp     patchg
    262. ; -------------------------------------------------------------------------
    263. ; Set teleportation targer
    264. ; -------------------------------------------------------------------------
    266. miracle_settarget:
    267.         addi.w  #-1,actfree+$C(a0)              ; Decrement wait timer
    268.         bne.w   .flash                          ; If it hasn't run out, branch
    270.         move.b  #4,r_no0(a0)                    ; Deactivate
    271.         move.w  #0,actfree+$C(a0)
    272.         move.b  #0,patno(a0)
    274.         movea.w actfree+6(a0),a1                ; Set target to the second teleporter
    275.         movea.w actfree+8(a0),a2                ; Get third teleporter
    276.         tst.b   ranum                           ; Should we randomly switch to the third teleporter?
    277.         bpl.w   .set_target                     ; If not, branch
    278.         exg     a1,a2                           ; Set target to the third teleporter
    280. .set_target:
    281.         move.w  actfree+$A(a0),actfree+$A(a1)   ; Set player object link in target
    282.         move.w  #0,actfree+$A(a0)               ; Unlink player object from us
    283.         move.b  #6,r_no0(a1)                    ; Flash and release the player from the target teleporter
    284.         move.w  #61,actfree+$C(a1)
    285.         rts
    287. .flash:
    288.         lea     miracle_pchg(pc),a1             ; Flash sprite
    289.         jmp     patchg
    291. ; -------------------------------------------------------------------------
    292. ; Animation data
    293. ; -------------------------------------------------------------------------
    295. miracle_pchg:
    296.         dc.w    miracle_pchg0-miracle_pchg
    298. miracle_pchg0:
    299.         dc.b    3
    300.         dc.b    0, 1, $FF
    301.         even
    303. ; -------------------------------------------------------------------------
    304. ; Sprite data
    305. ; -------------------------------------------------------------------------
    307. miracle_pat:
    308.         dc.w    miracle_00-miracle_pat
    309.         dc.w    miracle_01-miracle_pat
    311. miracle_00:
    312.         dc.b    2
    313.         dc.b    $F0, $B, 0, 0, $E8
    314.         dc.b    $F0, $B, 8, 0, 0
    316. miracle_01:
    317.         dc.b    4
    318.         dc.b    0, 1, 0, $C, $F8
    319.         dc.b    0, 1, 8, $C, 0
    320.         dc.b    $F0, $B, 0, 0, $E8
    321.         dc.b    $F0, $B, 8, 0, 0
    322.         even
    324. ; -------------------------------------------------------------------------

    Also, I would like to shed some light on the "sprite status table" in the wiki section.

    "cddat" is another set of flags that are pretty much object specific, unlike "actflg" which holds flags for more general object stuff. Objects do tend to use it to handle sprite flipping if it uses the animation system, as it is possible to also set flipping with each sprite defined in an animation, and Sonic himself uses it, because sprite flipping is utilized for displaying the correct angled running sprite on the ground. Outside of that, Sonic, for example, uses this to keep track of if he's in the air, if he's in a rolling state, if he's pushing, if he's standing on an object, and if he jumped while he was rolling on the ground (used to locked his controls in the air in the other Genesis games to get around an issue with the speed cap in the air that rolling speeds can get cancelled out by).

    "cdsts" is the index to its entry in the global object status table. If it's 0, then it has no entry. The object status table is mainly used to manage object respawning. Bit 7 is set when an object in the stage is spawned, which prevents it from being respawned until it gets cleared. An object like a monitor also utilizes it to keep track of whether it was destroyed or not. Enemies will clear bit 7 if they have gone far enough off screen to despawn, so that they can spawn again. When they get destroyed, bit 7 will remain set to keep them from respawning.

    "userflag" is basically the object's subtype ID/properties that are set per object in the stage layout (set by the "user" of the stage editor). In the other Genesis games, this is 1 byte, but in Sonic CD, it's 2 bytes. An example of this is having a flag that determines if an enemy should be broken/aged or not, or what kind of item a monitor should have.
    Last edited: Mar 6, 2024
    • Like Like x 3
    • Informative Informative x 1
    • List
  6. BenoitRen


    Tech Member
    I've whined mentioned before that the ASM produced by the PS2 compiler has a lot of small differences with the original ASM. Instead of being a configuration issue, it is possible that I have the wrong version of the compiler.

    The disassembled ELF files's debug info mentions that it was compiled with Metrowerks CodeWarrior version If I use the version found on to compile the decompiled code, that same version string is inserted into the result. However, if you launch it on the command line without any arguments and actually look at the compiler info, it says... 3.0.1:
    This suggests there's a discrepancy between the version inserted into the debug info and the actual version of the compiler used.

    I hope to find the actual compiler used in the near future.
    • Informative Informative x 2
    • List
  7. BenoitRen


    Tech Member
    Today's the project's first anniversary! The first commit to my local SVN repository was exactly one year ago.

    The following has been decompiled the past year:
    • core/shared code
    • Palmtree Panic
    • Collision Chaos
    • Tidal Tempest
    • Special Stage
    • AVIGOOD (ending movie code)
    • AVIOPEN (opening movie code)
    • Best Time
    • Title screen
    • D.A. Garden
    • Save Data
    • Sound Test
    • Stage Select
    • Time Attack
    • THANKS (post-credits scene)
    • Visual Mode
    The following is what remains to be decompiled:
    • Quartz Quadrant
    • Wacky Workbench
    • Stardust Speedway
    • Metallic Madness
    There's something missing, though: the control program that ties all of this together. That doesn't have debug info, as far as I know, so it can't easily be decompiled. I'll have to look deeper into this so I can have something playable to show.

    As for the compiler issue I mentioned last month: it turns out that several versions of CodeWarrior output as the version string in the ELF file. The most recent one to do so is CodeWarrior 3.0.4. A preview version of this one is available, and it seems to fix most ASM discrepancies. The main discrepancy still present is the ASM generated for the initialisation of local arrays, which I don't know yet how to solve. A bug-fixed version of the compiler shipped with 3.0.4 is part of the CodeWarrior 3.5 package, but that also has this discrepancy, so it's most likely due to the decompiled code.
  8. Devon


    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    your mom
    I just wanna say that I'm super happy about the progress made after I managed to dump the remaining missing symbols, on top of all the additional knowledge we gained about the original source code. Definitely would also be cool to see this ported to other platforms (especially modern Windows) as an alternative to the 2011 remake for a more "authentic" experience in regards the original Sega CD version... with some bug fixes of course.
    Last edited: Apr 8, 2024
  9. BenoitRen


    Tech Member
    I focused on my C port of Sonic & Knuckles Collection in March and April, hoping in the meantime that I'd find the version of the compiler that was used. I did try to find the control program used for the game, but haven't found it.

    When I got fired near the end of April I figured I'd use the extra time to decompile another zone. It's almost done, but, as always, the copy of the scrolling code for each act version takes time.

    Now, let's move on to the reason I'm writing this update.

    I mentioned that there seems to be a discrepancy between the version inserted into the debug info and the actual version of the compiler used.

    The version number given on the command line by the compiler seems to vary on the environment, and it's not clear where it gets it from. The only thing you can rely on is the build date.

    A fellow reverse engineer, who's also decompiling a game compiled with Metrowerks CodeWarriors, created a list of CodeWarrior releases to aid in finding the right version. As the debug info contained version in the comment section, I narrowed my search to compilers inserting that version string. According to the list, this meant a compiler as old as the one shipped with CodeWarrior R2.5, and not newer than the one shipped with CodeWarrior R3.04.

    Like I said before, the output from the version shipped with CodeWarrior R3.04 is closer to the original, but it's not there yet.

    Then, yesterday evening, it was revealed to me that, while the compilers that ship with CodeWarrior R3.5 and newer do emit 3.0.0 to the debug info's comment section, the linker overrides this with! In other words, one of the newer compilers could also have been the one used to compile the original ASM!

    I tested the newer compilers. Their output is almost identical to the original. The only remaining difference is that in 1% of the output they use a different register. Most of the time this means it uses $v0 or $v1 instead of $at.

    I went back to WARP.ELF, and managed to make my version's main (code) section 99% identical (if you disregard the memory addresses). Next, I got the data section identical save for a couple numeric identifiers. Memory addresses are still off because in memory the global data seems to start at another location. I suspect it requires tweaking the linker command file, which I know little about, so I'm putting that off.

    I'm already reviewing the core files used by R5, and will resume my R5 work after that. You can expect a release of R5's code soon!
  10. Devon


    A̸ ̴S̴ ̵C̵ ̷E̶ ̸N̸ ̴D̶ ̵E̶ ̸D̶ Tech Member
    your mom
    I took a look at the "missing symbols" page again, and I want to point out that tagPALETTEENTRY and tagPOINT come from the Win32 API.
    • Informative Informative x 5
    • Agree Agree x 1
    • List
  11. BenoitRen


    Tech Member
    All the source code related to R5, Quartz Quadrant, has been committed!

    I've found the cause of the remaining 1% of differences, which means that this is the first zone to compile to identical ASM! Except for data offsets, of course.

    This zone had less code than previous ones, which is why I was able to decompile it in a month despite also working on other things for a part of that. The size of the boss code, however, is second only to the first zone's, as that easy running challenge consists of more than a couple parts.
  12. BenoitRen


    Tech Member
    I've gone back through the code of the previous zones I've decompiled, and corrected those until they also matched the original ASM. This was mostly successful.

    I say "mostly", because there's one file of R4 where the decompiled C code generates one less instruction than the original. It's not an important instruction (it's actually superfluous), but ideally I'd also like to match it.

    Then there's some good news and bad news.

    The good news: I've found the control program that ties all of the ELF files together. It's S1.DAT in the root of the disc.

    The bad news: it has no symbols whatsoever, and I know nothing about PS2 programming that would enable me to reverse engineer it. It does have some debug messages, which enabled me to identify some functions, but that's pretty much it.
    • Like Like x 3
    • Informative Informative x 2
    • List
  13. BenoitRen


    Tech Member
    Everything under TITLE (title screen, Sound Test, D.A. Garden, etc.) has been checked again and corrected until it matched 99% of the original ASM. Not 100%, because the main source file of Time Attack doesn't match one superfluous instruction. Yes, it's the same case as for R4...

    Also checked WARP again, corrected it, and it matches 100%.

    Now that that's out of the way, I can get back to decompiling zones. Or rounds, as the game calls them.
  14. BenoitRen


    Tech Member
    All the source code related to R6, Wacky Workbench, has been committed!

    This only took two weeks due to two factors:
    • I could copy/paste most of the scrolling code from R5, and then between R6 versions
    • I made maximum use of my jobless free time to decompile like a madman. On my most productive day I processed 18 files.

    This zone uses its own version of the PLAYER object that seems to remove some sound-related code, and adds some functions. One of those functions I can't match 100% because one variable is put into a register instead of on the stack, and I can't figure out why.

    Two more zones to go.