The layout for the first Sonic 3 stage can be found in the General/Special Stage/Layout/S3 1.bin file. We can use a tool such as S3SSEdit to add in a suitable formation of yellow spheres, which later will become our green spheres.
However, as soon will be apparent, it is more than pedagogic to open up the layout file in a hex editor and take a look at what's under the covers. See, if you set the row length to exactly 32 bytes...
Holy smokes! That's the first special stage, all right!
So as it happens, special stage layouts work much in the same as regular level layouts do, with each byte in the layout file describing the contents of each space in the level grid. From the screenshot above, we can see that red spheres correspond to a value of 01, blue spheres correspond to a value of 02, and the yellow spheres we added correspond to a value of 05. This will be important later, so just keep it at the back of your mind for now.
Looking closely though, at the very end of the file there are four extra words which do not correspond to any part of the stage's layout. Each of these words is actually copied into its own RAM variable right before the special stage starts, as part of the layout load code at sub_85B0:
move.w (a2)+,(Special_stage_angle).w move.w (a2)+,(Special_stage_X_pos).w move.w (a2)+,(Special_stage_Y_pos).w move.w (a2)+,(Special_stage_rings_left).wThe fourth word is the number of rings necessary to obtain a perfect bonus. Four groups of blue spheres × 16 spheres each = 64 = $40, so that looks about right. Note that the number of blue spheres necessary to clear the stage is not stored; we'll get back to this soon.
The second and third words set the player's starting position on the level grid. These are fixed-point numbers, with the high byte representing the integer part and the low byte representing the fractional part, so the values $200, $200 start us off at point (2,2) on the grid, with (0,0) being the northwest (top-left) corner.
The first word is interesting. It defines the player's starting direction along the level grid, and a key of valid values can be found in sonic3k.constants.asm:
Special_stage_angle = ramaddr( $FFFFE426 ) ; byte ; $00 = north, $40 = west, $80 = south, $C0 = east
A value of $80 represents a southward-facing direction (down), which lines up with what we see both in S3SSEdit and in-game... but wait a second, it says right there that Special_stage_angle is a byte!
This is not an oversight: if you go through the entire code, you'll find that every reference to the Special_stage_angle variable is a byte access, except for the initial write from the layout seen above. We have an entire byte of RAM that is loaded from the layout file and then left completely unused. Let's go ahead and label that byte:
Special_stage_angle = ramaddr( $FFFFE426 ) ; byte ; $00 = north, $40 = west, $80 = south, $C0 = east Special_stage_green_spheres = ramaddr( $FFFFE427 ) ; byte
We can now flag individual special stages as green sphere stages by setting byte $401 of their layout file to a non-zero value, and then check the Special_stage_green_spheres flag anywhere in the code and branch to custom logic when relevant. Let's set that byte in our layout, build the ROM and confirm that everything still works properly.
Alright, let's finally start coding stuff. Two things stick out to me: the first is that our green spheres aren't green (laughs). We can fix this by adding the following code at loc_82A6, directly after the call to sub_85B0:
bsr.w sub_85B0 tst.b (Special_stage_green_spheres).w beq.s loc_82BE lea (Target_palette_line_4+4).w,a1 move.l #$0C600A0,d0 move.l d0,(a1)+ move.l #$0600020,(a1)+ move.l d0,(a1)+ loc_82BE: lea ($FFFF5500).l,a1Let's make sure we understand what we're doing here. First, we load the address of the first yellow sphere color into register a1. The yellow colors start two entries into the fourth palette line, and each Mega Drive color is two bytes long, so that comes to four bytes after Target_palette_line_4.
Next, we load the colors $0C6, $0A0 into the d0 register and write them over the first two yellow colors, incrementing register a1 in the process. We then write the colors $060, $020 over the two middle yellows, increment a1 again, and close it out by writing the colors $0C6, $0A0 from d0 over the last two yellows as well.
The second thing that stuck out to me, which admittedly isn't obvious without a comparison screenshot, is that the total blue sphere count is now incorrect. Well I mean, it's correct right now since green spheres are still springs, but that's going to change once we start turning them into blue spheres on the fly.
So how is the total sphere count determined? The code responsible for this is at sub_9EA0:
sub_9EA0: lea (Plane_buffer).w,a3 moveq #0,d1 move.w #$3FF,d0 loc_9EAA: cmpi.b #2,(a3)+ bne.s loc_9EB2 addq.w #1,d1 loc_9EB2: dbf d0,loc_9EAA move.w d1,(Special_stage_spheres_left).w rtsQuite plainly, this function loops through each byte in the special stage layout, and increments register d1 every time it finds a 2, which as we saw before, corresponds to a blue sphere. We'll extend this by also incrementing d1 whenever we find a 5 (yellow sphere), but only if the stage has been flagged as a green sphere stage:
loc_9EAA: move.b (a3)+,d2 cmpi.b #2,d2 beq.s loc_9EB0 tst.b (Special_stage_green_spheres).w beq.s loc_9EB2 cmpi.b #5,d2 bne.s loc_9EB2 loc_9EB0: addq.w #1,d1Build the ROM, run it and verify that the total sphere count is back to 102:
Alright, we've got the look down, so next time we'll jump right into what happens when a blue sphere is touched, and see how we can adapt that behavior for green spheres. See you there!