Monday, January 28, 2019

Green spheres in special stages, part 4

Alright, let's finally start writing our green sphere logic. Because we're implementing them over yellow spheres, there's actually something extra we need to take into account: yellow spheres are the only objects Tails will interact with in a Sonic and Tails game, so we have to skip over that code by adding a check for our green spheres flag at loc_92C4:
    cmpi.b  #5,$44(a0)
    bne.s   loc_9304
    tst.b   (Special_stage_green_spheres).w
    bne.s   loc_9304
    tst.b   (Special_stage_clear_routine).w
    bne.s   loc_9304
Next, we'll go to sub_972E and look for the yellow sphere handling code. It can be found at loc_97EE, so that's where we need to add the object to the collision response list, which will look very similar to the blue sphere handling code:
loc_97EE:
    cmpi.b  #5,d2
    bne.s   loc_9822
    tst.b   (Special_stage_green_spheres).w
    beq.s   loc_97F4
    bsr.w   Find_SStageCollisionResponseSlot
    bne.s   loc_97BE
    move.b  #3,(a2)
    move.l  a1,4(a2)
    bra.s   loc_97BE

loc_97F4:
    tst.b   (Special_stage_jump_lock).w
We write the routine and layout pointer to the collision response slot as usual, then jump to loc_97BE to play the blue sphere sound. This time however, we set the routine to 3, which we'll then define in the off_9DFC array as follows:
off_9DFC:   dc.l Touch_SSSprites_Ring
            dc.l Touch_SSSprites_BlueSphere
            dc.l Touch_SSSprites_GreenSphere
We also need to add a dummy green sphere definition to the MapPtr_A10A array, which will serve as our halfway state between touching the green sphere and placing a blue sphere in its stead. Turns out, there's another blue sphere clone in slot $C which is completely unused, so we can just tweak its VDP pattern to point at palette line 4 and use that:
    dc.l Map_SStageChaosEmerald ; $B
    dc.l $E5A70000              ;
    dc.l Map_SStageSphere       ; $C
    dc.l $E6800000              ;
    dc.l Map_SStageSuperEmerald ; $D
    dc.l $E5A70000              ;
Okay, we're now ready to write our implementation of Touch_SSSprites_GreenSphere, which is basically going to be a lighter version of Touch_SSSprites_BlueSphere. Here's what it needs to do:
  • The first time through, we change the object at the layout position to $C, and set up a timer of nine frames.
  • On subsequent loops, we run out the timer, and then wait until any of the top three bits in the fractional part of the player's position are set, signaling that we are no longer right on top of the sphere.
  • After we're done waiting, we write 2 (a blue sphere) to the layout and clear out the collision response slot.

So let's go ahead and turn that into code:
Touch_SSSprites_GreenSphere:
    movea.l 4(a0),a1
    cmpi.b  #$C,(a1)
    beq.s   .checkChangeColor
    move.b  #$C,(a1)
    move.b  #9,1(a0)
    rts

.checkChangeColor:
    subq.b  #1,1(a0)
    bpl.s   .return
    move.w  (Special_stage_X_pos).w,d0
    or.w    (Special_stage_Y_pos).w,d0
    andi.w  #$E0,d0
    beq.s   .return
    move.b  #2,(a1)
    clr.l   (a0)
    clr.l   4(a0)

.return:
   rts
That should about do it. If we build and test the ROM right now though, we'll soon realize there is a problem. Although our green spheres correctly only turn blue once we've walked past them, we end up collecting those immediately after, leaving behind a trail of red spheres as we go.


So what is the reason for the discrepancy between red and blue spheres? Why do we collect the blue sphere as we're walking away, but not activate the red sphere in the same circumstances? The answer can be found at sub_972E:
sub_972E:
    lea     (Plane_buffer).w,a1
    move.w  (Special_stage_X_pos).w,d0
    addi.w  #$80,d0
    lsr.w   #8,d0
    andi.w  #$1F,d0
    move.w  (Special_stage_Y_pos).w,d1
    addi.w  #$80,d1
    lsr.w   #8,d1
    andi.w  #$1F,d1
    lsl.w   #5,d1
    or.b    d0,d1
    lea     (a1,d1.w),a1
    move.b  (a1),d2
    beq.w   locret_98AE
Note how before we truncate the player's X and Y coordinates, we add onto them a fractional value of $80, or 0.5. This effectively rounds the player's position towards the closest integer, which means that if we're walking from position A to position B, then we will touch the object at position B as soon as we're closer to B then we are to A.

That's not really the relevant part, though. If we continue reading, we'll soon run into some very familiar code:
    cmpi.b  #1,d2
    bne.s   loc_97AA
    move.w  (Special_stage_X_pos).w,d0
    or.w    (Special_stage_Y_pos).w,d0
    andi.w  #$E0,d0
    bne.s   locret_97A8
When the object at the player's current position is 1, a red sphere, we are once again we are checking the top three bits in the fractional part of the player's position. However, this time we're not waiting until any of them are set. We're waiting until all of them are cleared! Red spheres only activate if we're right on top of them!

This explains the mismatched behavior between blue spheres and red spheres. For red spheres, it is sufficient to wait until we are no longer at the center of the intersection before placing them in the layout. Before placing a blue sphere however, we must ensure that the player has since moved to another layout cell.

To do this, we will first write the player's position to the collision response slot at loc_97EE:
    move.b  #3,(a2)
    move.w  d1,2(a2)
    move.l  a1,4(a2)
Now we can repeat the same calculation in Touch_SSSprites_GreenSphere, and then compare it against the stored value. As soon as these are different, it is safe to place the blue sphere in the layout. We preserve the original check so that the sphere still changes from green to blue with the same timing that a blue sphere does to red, except all we write to the layout at that point is $A, a dummy blue sphere:
Touch_SSSprites_GreenSphere:
    movea.l 4(a0),a1
    cmpi.b  #$C,(a1)
    beq.s   .checkChangeColor
    cmpi.b  #$A,(a1)
    beq.s   .checkPlayerMoved
    move.b  #$C,(a1)
    move.b  #9,1(a0)
    rts

.checkChangeColor:
    subq.b  #1,1(a0)
    bpl.s   .checkPlayerMoved
    move.w  (Special_stage_X_pos).w,d0
    or.w    (Special_stage_Y_pos).w,d0
    andi.w  #$E0,d0
    beq.s   .return
    move.b  #$A,(a1)

.checkPlayerMoved:
    move.w  (Special_stage_X_pos).w,d0
    addi.w  #$80,d0
    lsr.w   #8,d0
    andi.w  #$1F,d0
    move.w  (Special_stage_Y_pos).w,d1
    addi.w  #$80,d1
    lsr.w   #8,d1
    andi.w  #$1F,d1
    lsl.w   #5,d1
    or.b    d0,d1
    cmp.w   2(a0),d1
    beq.s   .return
    move.b  #2,(a1)
    clr.l   (a0)
    clr.l   4(a0)

.return:
   rts
Note how we change the first return branch to point at the new check, which forces the sphere to become collectible as soon as we move to another layout cell, even if the sphere was still running out its timer. This avoids a bug at very high speeds, where if we collect a green sphere and then immediately run into a bumper, we fail to pick up the resulting blue sphere as we backpedal through it.


And that's it. We've successfully added green spheres to the special stage, while yellow spheres continue to work in all the layouts we didn't touch. Many thanks to flamewing for approving my pull requests to the skdisasm repo over these past few weeks; this series would've been unpalatable without properly labeled variables and subroutines.

Saturday, January 26, 2019

Green spheres in special stages, part 3

We have a lot to cover this time as well, so let's get to it. sub_972E is the function responsible for handling collisions between the player and all other special stage objects, and particularly by the time we get to loc_97AA, the d2 register will hold the contents of the layout cell closest to the player's position:
loc_97AA:
    cmpi.b  #2,d2
    bne.s   loc_97C8
    bsr.w   Find_SStageCollisionResponseSlot
    bne.s   loc_97BE
    move.b  #2,(a2)
    move.l  a1,4(a2)

loc_97BE:
    moveq   #$65,d0
    jsr     (Play_Sound_2).l
    rts
The check at the top ensures that the rest of this code only executes when the cell the player is on contains a 2, which as we saw before, corresponds to a blue sphere. This is the blue sphere collision code! So what does it do?

Not much, really. Besides playing the blue sphere sound, all this does is call the Find_SStageCollisionResponseSlot function, which if you happen to have read my previous series on responsive object collision, works quite a bit like the Add_SpriteToCollisionResponseList function, in that it allows the object to store information about the collision that just took place into a list to be processed in the future, this time by the Touch_SSSprites function.

In this case, since there are no SSTs associated with special stage objects, all information must be stored directly into the collision response list. Each entry is eight bytes long, and the code at sub_972E consumes five of them right off the bat: byte 0 is the object's routine, while bytes 4-7 store a RAM pointer to the object's location in the layout.

Touch_SSSprites uses the routine byte to index into the off_9DFC array. Note that since zero denotes an empty slot in the collision response list, this array is actually one-based, so the routine value set by loc_97AA above refers to the second function in the array, not the third.

Which is a good thing, because there are only two functions in the array!
off_9DFC:   dc.l Touch_SSSprites_Ring
            dc.l Touch_SSSprites_BlueSphere
Touch_SSSprites_BlueSphere is a bit more involved than the preceding code, so let's break it up into small parts:
    subq.b  #1,2(a0)
    bpl.s   locret_9E86
    move.b  #9,2(a0)
Right at the start, we run a timer in byte 2 of the collision response slot. This timer is decremented every frame, and it prevents the function from doing anything until the result of the decrement is negative. Since the timer is always zero the first time through this code, we skip the branch and reset the timer to 9 in the process.
    movea.l 4(a0),a1
    cmpi.b  #2,(a1)
    bne.s   loc_9E62
Next, we load the object's location into register a1, and check the contents of that layout cell. If the object at that cell is somehow not a blue sphere, then we jump to loc_9E62. This might not make much sense right now, but for the time being, we skip the branch and continue to the code below.
    bsr.w   sub_9E88
    move.b  #$A,(a1)
    bsr.s   sub_9EBC
    beq.s   locret_9E60
    move.b  #4,(a1)
    clr.l   (a0)
    clr.l   4(a0)

locret_9E60:
    rts
When called, sub_9E88 decrements the remaining sphere count once, and if the count has reached zero, disables the player's ability to jump. Meanwhile, sub_9EBC is responsible for clearing out large groups of blue spheres once they are enclosed by red spheres, and returns a non-zero value if such a closed pattern is found.

Let's go over that within the context of our code. When a closed pattern of red spheres is found, we skip the branch to locret_9E60 and set the object at the current layout cell to 4, which corresponds to a ring. The collision response slot is then cleared out, signaling that we are done processing this sphere.

So what happens when a closed pattern isn't found? Well, right before sub_9EBC is called, the contents of the current layout cell are set to $A, and when we take the branch to locret_9E60, that change sticks -- for the nine frames that we told the timer at the start of the function to wait around for.

But what does a value of $A represent? It's not a red sphere, since as we previously saw, those correspond to a value of 1. The answer lies in the MapPtr_A10A array, which defines the mappings pointer and base VDP pattern for all the objects that can be placed in the special stage layout:
MapPtr_A10A:
    dc.l Map_SStageSphere       ; 0
    dc.l $86800000              ;
    dc.l Map_SStageSphere       ; 1
    dc.l $86800000              ;
    dc.l Map_SStageSphere       ; 2
    dc.l $C6800000              ;
    ...
    dc.l Map_SStageSphere       ; $A
    dc.l $C6800000              ;
As it turns out, object $A is identical to object 2, blue sphere, except for the fact that it is not object 2. That means it is not caught by the collision code at loc_97AA, and therefore not re-added to the collision response list while we're still waiting out the timer on the first one. It also means that once the timer expires, we will take that branch to loc_9E62.

How come we didn't change the blue sphere to a red sphere right away, though? Let's keep reading.
loc_9E62:
    move.b  #0,2(a0)
    move.w  (Special_stage_X_pos).w,d0
    or.w    (Special_stage_Y_pos).w,d0
    andi.w  #$E0,d0
    beq.s   locret_9E86
Here, we take the player's current X and Y positions, and check the three highest bits of their fractional part. If none of those are set, then the function does nothing except clear the timer to ensure it doesn't roll over. This essentially means we're waiting until we're no longer at one of the crossroads in the layout.

You may have guessed it by now, but the reason we're doing this is so we don't run into the red sphere we're trying to place. Once we're in the clear, we can finally write a 1 to the layout and clear out the collision response slot.
    cmpi.b  #$A,(a1)
    bne.s   loc_9E80
    move.b  #1,(a1)

loc_9E80:
    clr.l   (a0)
    clr.l   4(a0)

locret_9E86:
    rts
Note the sanity check at the top: it ensures we don't accidentally place a red sphere over a layout cell which has since been converted to a ring by the closed pattern algorithm in sub_9EBC.

Okay, that was a lot of words, but not much to show for it. In the next and final part, we'll use everything we've learned to write a custom implementation of green spheres using the existing framework.

Tuesday, January 22, 2019

Green spheres in special stages, part 2

Okay, so the first thing we should do is set up a test stage. For the purpose of this series, I'll just be doing small edits to the first Sonic 3 stage. We could edit one of the Sonic & Knuckles stages, but since those are Kosinski-compressed, it would add the extra inconvenience of having to decompress the file before making our changes, and then recompress the file before building and testing the ROM.


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).w
The 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,a1
Let'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
    rts
Quite 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,d1
Build 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!

Monday, January 21, 2019

Green spheres in special stages, part 1

Man, I am just crazy good at updating this blog, aren't I?

First, some housekeeping. Back in October, I finally finished the SonLVL object definitions; you can now find them both in the S&K disasm repo and alongside the main SonLVL download. I then used them to produce a set of full-sized level maps for both Sonic 3 and Sonic & Knuckles, which you can find at the Sonic Retro wiki.

I soon got back to working on my hack as had I longed to, and have been semi-regularly posting a dev log to YouTube. It's still not at a point where I'm ready to share all my plans for it, but you'll get the basic idea from the videos alone.


Anyway, a while ago, Sonic Retro user Travelsonic posted a thread asking for documentation on special stage object behavior. I suggested I could blog a bit about the process of adding green spheres to the special stage, and here we are. It only took me two months to get around to it!

So, what are green spheres, exactly? Green spheres are one of the two new sphere types Stealth implemented in his "Blue Spheres 2" concept demo, which eventually made its way into Sonic Mania as a bonus game:


Quite simply, green spheres function as an additional layer on top of blue spheres: just like a blue sphere becomes red when touched, a green sphere first becomes blue, and must then be touched again before it becomes red.

Pink spheres, on the other hand, are two-way teleporters that would be far more difficult to implement, not only due to the programming overhead involved, but also a much more pressing factor: the palette.


As I alluded to in an early post, all four sphere types use the same set of sprites with different color palettes, in order to cut down on VRAM usage. Specifically, the first half of each palette line corresponds to the red, white, blue and yellow spheres respectively, in which the white palette replaces two otherwise duplicate colors with shades of red, revealing a star design that is invisible in every other sphere type.


And therein lies the rub: this design implies that there can only be four different sphere types at play at any given time, which means that if we want to add a new type, it will have to replace one of the existing colors.

Famously, the goal of the special stage is to Get Blue Spheres, so obviously those cannot be replaced. And since blue spheres become red spheres when touched, red spheres cannot be replaced either. The conclusion is that in order to implement green spheres, we must sacrifice either white spheres or yellow spheres.

Over the course of this series, I will show you how to implement green spheres over yellow spheres, though you could easily go the other way with presumably little complications. I will also implement a method of marking individual stages as green sphere stages, which if present enables all the additional logic surrounding green spheres, and if omitted preserves their behavior as ordinary yellow spheres.

Continued in part 2!