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

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

1. ### BenoitRen

Tech Member
404
180
43
Okay, then let's look at where it's used, because it bothers me. From the Sonic 3 & Knuckles disassembly:
Code (Text):
1. jsr   (Random_Number).l
2. andi.w   #\$FF,d0
3. subi.w   #\$7F,d0
By ANDing with 0xFF, it ensures that the value is between 0 and 255. Then it subtracts 127 from it. There's a good chance that the number returned from Random_Number is 126 or lower. At that point it either becomes negative or underflows?

As it's being used to add to the X position, which we know is signed, I thought it must be signed. However, when we look at the rest:
Code (Text):
1. moveq   #0,d1
2. move.b   (_unkF7C3).w,d1
4. move.w   word_8F582(pc,d1.w),d2
5. cmpi.w   #\$120,d0
6. bhs.s   loc_8F564
It tests if the random number we got is higher than 0x120 (288). If we always get a number between 0 and 255, that's not possible! Unless... the number underflows to a high value.

The SK&C version seems to hint at this as well, as the comparison it does at that point is unsigned.

2. ### Devon

I'm a loser, baby, so why don't you kill me? Tech Member
1,246
1,416
93
In this specific event, yes, it is limiting what was returned to the range 0 to 0xFF, and then effectively making it signed by shifting it into the range of -0x7F to 0x80. Again, the it's up to the programmer to decide what to do with the random garbage that the function returns, and in this case, they were using it to turn it into a random value between -0x7F and 0x80.

You can still very much treat a signed value as unsigned for some tricks. Take this example code from Sonic 1:
Code (ASM):
1.         move.w    obX(a0),d0              ; get object position
2.         andi.w    #\$FF80,d0               ; round down to nearest \$80
3.         move.w    (v_screenposx).w,d1     ; get screen position
4.         subi.w    #128,d1
5.         andi.w    #\$FF80,d1
6.         sub.w     d1,d0                   ; approx distance between object and screen
7.         cmpi.w    #128+320+192,d0
8.         bhi.s     .Despawn

Here, it takes an object's X position and rounds it down to a multiple of 128. Then it takes the current screen position, subtracts 128, and rounds it down, and then gets subtracted from the object's modified X position value. Basically, it checks if the object has moved far enough away from the screen to despawn. But, it uses an unsigned branch after the comparison with a weird value.

If the object moves far enough to the right, then the distance away from the camera minus 128 pixels is greater than 128+320+192. The "128+" is there to offset the 128 pixels subtracted from the camera's position. The "320+" is there to offset the width of the screen, so that means the object despawns if they have more or less moved 192 pixels past the right side of the screen (yes, I know it's not accounting for the masks, but this is more or less the logic).

So, how does it check if the object has moved far enough away to the left? This is also where that subtraction of 128 comes in handy. It can easily be deduced that the object will despawn if they move 128 pixels past the left side of the screen (once again, ignoring the masks, but still). If the object is past the screen's position minus 128, then the subtraction results in a negative number. Here's where the magic comes in. If you treat the value as unsigned, then it turns into a massive number, which definitely is far beyond 128+320+192, so a despawn occurs. This trick helps to reduce the boundary checks from 2 checks to a singular one, and thus being more optimal.

As for the code from Sonic 3 & Knuckles... well, if the number is negative, then yeah, when treated as unsigned, it ends up being very well above 0x120, and thus will branch on that condition. Judging from the full code:
Code (ASM):
1.         jsr     (Random_Number).l
2.         andi.w  #\$FF,d0
3.         subi.w  #\$7F,d0
5.         moveq   #0,d1
6.         move.b  (_unkF7C3).w,d1
8.         move.w  word_8F582(pc,d1.w),d2
9.         cmpi.w  #\$120,d0
10.         bhs.s   loc_8F564
11.         bset    #0,render_flags(a0)
12.         neg.w   d2
13.
14. loc_8F564:
15.         move.w  d2,x_vel(a0)
16.         andi.w  #\$C,d1
17.         move.l  off_8F576(pc,d1.w),\$30(a0)
18.         bra.w   loc_8F58E
19. ; ---------------------------------------------------------------------------
20. off_8F576:
21.         dc.l    byte_8F682
22.         dc.l    byte_8F695
23.         dc.l    byte_8F6AA
24. word_8F582:
25.         dc.w    -\$100
26.         dc.w    -\$100
27.         dc.w    -\$100
28.         dc.w    -\$180
29.         dc.w    -\$180
30.         dc.w    -\$200

The logic pretty much ends up being that if d0 ends up being negative (which unsigned, is a lot greater than 0x120), then the X speed value chosen from word_8F582 remains unchanged, left as a negative value. Otherwise, if the X speed value is positive (which is less than 0x120 no matter what), then the value is negated to become positive, and the object's sprite is flipped.

Judging from all that, it can be deduced that the X position that is started off with is a center point, with a random value chosen to offset that from either the left or the right of that point. Then, a speed value is chosen and set up so that the object moves away from that point, no matter which side it's on.

I have no idea why they decided to do a weird unsigned check with 0x120. Maybe it's some kind of leftover from an earlier version that just so happened to work out the same as if they just decided to check if d0 was negative or not.

If you want an example of the output of the random function being treated as unsigned, here's this code from the Marble Zone boss in Sonic 1:
Code (ASM):
1. BossMarble_MakeLava:
2.         subq.b  #1,objoff_34(a0)                ; Decrement jumping fireball spawn timer
3.         bcc.s   loc_1845C                       ; If it hasn't run out, branch
4.
5.         jsr     (FindFreeObj).l                 ; Allocate object slot
6.         bne.s   loc_1844A                       ; If it failed, branch
7.         move.b  #id_LavaBall,obID(a1)           ; Spawn fireball that jumps up
8.         move.w  #boss_mz_y+\$D8,obY(a1)          ; Set its Y position to the lava pit
9.
10.         jsr     (RandomNumber).l                ; Get random value
11.         andi.l  #\$FFFF,d0                       ; Limit it between 0 and \$FFFF
12.         divu.w  #\$50,d0                         ; Divide by \$50
13.         swap    d0                              ; Get remainder (d0 becomes a value between 0 and \$4F)
15.         move.w  d0,obX(a1)                      ; Set jumping fireball at that X position
16.
17.         lsr.b   #7,d1                           ; Dunno what this is for (leftover from earlier code, probably)
18.         move.w  #\$00FF,obSubtype(a1)            ; Set subtype to "jumping" and use lower sprite priority
19.
20. loc_1844A:
21.         jsr     (RandomNumber).l                ; Get random value
22.         andi.b  #\$1F,d0                         ; Limit it between 0 and \$1F
23.         addi.b  #\$40,d0                         ; Shift range to \$40 and \$5F
24.         move.b  d0,objoff_34(a0)                ; Set timer for next jumping fireball spawn
25.
26. loc_1845C:

Basically, when a jumping fireball is spawned in the lava pit, its X position is set to the pit's X location + a random number between 0 and \$4F, which is done by performing a modulus operation on the value returned by the function via an unsigned division (the mask to \$FFFF is to prevent the division result from being too big, in which it would get discarded if that was the case). Then, the timer for spawning the next fireball is set to a random number between \$1F and \$5F, by ANDing the random result by \$1F and adding \$40.

Again, the function just returns a random jumble of bits without regard for signage (by that logic, the function should be treated as unsigned), leaving the programmer to decide what logic to perform with them.

Last edited: Mar 16, 2024
3. ### Brainulator

Regular garden-variety member Member

4. ### Devon

I'm a loser, baby, so why don't you kill me? Tech Member
1,246
1,416
93
I think they were just trying to understand the general logic with the random number generator, which appears in more than S3&K.

5. ### BenoitRen

Tech Member
404
180
43
I admit that it bordered on the topic of my port, but it was essentially about the original source code, so I figured it was on-topic.

6. ### Kilo

That inbetween sprite from S&K's title screen Member
298
292
63
So this object caught my eye from Sonic CD.

It's a very simple object, it displays the shield sprite and just deletes itself offscreen.
I wonder if this would've been an object present in Sonic 1's source code? It's use is similar to objects 2-7 in the Sonic 1 prototype, but those objects really only test frame timers or do nothing at all. Best candidate of an ID it could've been is 6. Entirely speculation though.

7. ### Brainulator

Regular garden-variety member Member
Alright. In that case, I agree with @Devon that the original code leaves it up to individual uses as to whether to treat it as signed or unsigned, because the 68000 does not differentiate between the two in memory. By all indication, it's just a random number generator that Sega gave to their developers to play with (the same code appears in multiple games besides the Sonic games).

8. ### Devon

I'm a loser, baby, so why don't you kill me? Tech Member
1,246
1,416
93
This applies to any architecture, really. Data is just a bunch of 1s and 0s, and how they are interpreted is up to the programmer ultimately. You can choose to interpret a 32-bit value with all 1s set as -1, or 0xFFFFFFFF, or whatever it ends up translating to for other systems when signed. In the end, it's still the same set of bits, just different ways it's interpreted. Hell, with floating point values in C, you can get the raw IEEE data by interpretating a pointer to it as an integer pointer, and read the bits from that pointer as is.

9. ### BenoitRen

Tech Member
404
180
43
There's a reason atan (or atan_sonic, as it's called in Sonic CD) isn't in the list of known functions on the wiki yet: I don't get it.

I know it calculates an angle using a tangent, the latter being retrieved using a look-up table. What throws me off is that you need two data points to calculate a tangent, but the function is only given one. What is the other data point?

10. ### Kilo

That inbetween sprite from S&K's title screen Member
298
292
63
Time for some speculation, just some thinkery, if you will.

In my initial version of my objects list I had made this mistake:

Now where did I source this from? Well my initial objects list was entirely taken from Sonic 1 J2ME, the port that I've taken to researching. I should've cross referenced my names from S2NA and CD, and that was a mistake I corrected later on. Here's the files I referenced:

After referencing S2NA and CD to update the object list, I had discarded the name windou from my mind. I mean of course, they use Sonic Advance graphics, this couldn't have been from Sonic 1's source...
Weeks go by. And then this was posted. I knew about this function when the prototype had released, but now seeing it in action made it fresh in my mind, and so it sat on the wrinkles of my brain for a couple days.
Then today on the bus ride home from work it hit me.

Could windou be the function name for this arcade-style HUD? It uses the window layer, after all. And the naming of these J2ME graphics do match the nomenclature of Sonic 1's source code despite not using original assets...
Again, it's just speculation, but is worth a thought.

11. ### MainMemory

Kate the Wolf Tech Member
4,742
338
63
SonLVL
What do you mean? As far as I can see, the function does take two inputs: d1 is the X distance and d2 is the Y distance. Overall it seems to be a pretty standard implementation of the common atan2 function.

12. ### Devon

I'm a loser, baby, so why don't you kill me? Tech Member
1,246
1,416
93
Yeah, basically, it calculates the angle formed from (0, 0) to the defined (x, y) point. For example, if x is 0, and y is 1, then it will return 90 degrees (0x40, according to how angles are set up in the Genesis Sonic games).

• Like x 1
• Agree x 1
• List
13. ### BenoitRen

Tech Member
404
180
43
Sorry, "data point" wasn't the correct term. I meant "coordinate point". The function receives X and Y, which is one point, (X, Y). Yet I read you need two points.

So the second point I was looking for is (0, 0).

Thanks, I've added the info to the wiki!

14. ### DigitalDuck

Arriving four years late. Member
5,349
438
63
Lincs, UK
TurBoa, S1RL
You don't need two points, a typical atan2 function takes one two-dimensional vector, or a pair of numbers acting as such. You can use two coordinate points to calculate the vector, but as that's a simple subtraction it's not usually part of the function.

If I want to know the angle from (11, 23) to (58, 13) I get the vector by doing (58, 13) - (11, 23) = (47, -10) and then get the angle with atan2(-10, 47). y is usually given before x because it's based on the standard atan function which returns an angle given the gradient (i.e. y / x), but it also takes into account the signs of x and y to determine the direction and therefore give an angle in the full circle range rather than just half a circle.

15. ### BenoitRen

Tech Member
404
180
43
@Kilo I just found out that a starpost is called "save" in Sonic & Knuckles Collection. You were right!

Looks like the Sonic CD developers did change the name in most places. I say most, because one function does refer to the object as "savewk".

16. ### Kilo

That inbetween sprite from S&K's title screen Member
298
292
63
That's why we cross reference, my friend. We've had the S2NA debug table and J2ME graphics tellin' us this the whole time. It's an easy mistake to make when you spend a lot of time researching a particular game that tells you otherwise, made the same mistake myself calling the HUD windou because J2ME did.

17. ### BenoitRen

Tech Member
404
180
43
I'm looking for the variable that holds the "screen resize routine counter". Supposedly it's saved when you hit a lamppost/starpost, but I don't see it in Sonic CD's code.

So I looked up which variables have "scr" in the name, and I'm wondering if "scrar_no" could be it. What do you think?

18. ### Kilo

That inbetween sprite from S&K's title screen Member
298
292
63
Looking at CD, I was perhaps thinking scr_cnt? I kind of doubt it since it's used in levermove and jumpchk? There's seemingly no equivalent code in Sonic 1 itself, though. I only figure such as it's name makes sense. However scrar_no is just as likely.

19. ### BenoitRen

Tech Member
404
180
43
According to the wiki, the original game does save a copy of the "Resize Level Routine Counter". However, it doesn't list the location of the source value.

20. ### Kilo

That inbetween sprite from S&K's title screen Member
298
292
63