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:
    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)

    moveq   #$65,d0
    jsr     (Play_Sound_2).l
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)

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:
    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.
    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)

    clr.l   (a0)
    clr.l   4(a0)

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.


  1. Me and Silver Sonic 1992 were discussing something on this video: as to possibly making the game load Knuckles 2P sprites in a 1P format with the 1P Knuckles palette to possibly allow for a more complete Knuckles experience in Sonic 3 Alone. If you could look into it and find a Game Genie or PAR code that makes this possible we would greatly appreciate it. Thank you.

    1. Here are the codes:

      004F6A:6000 2P player objects in 1P mode
      0118BC:6016 Skip harmful level wrapping code
      01403A:6016 Skip harmful level wrapping code (Tails)

      Press C at the level select to pick your character. Surprisingly, using these codes to play as Knuckles causes his route through AIZ to become playable.

    2. Quite interesting. "Micro Knuckles" is a great improvement over literally Sonic.

  2. This reminds me of two things:

    Why is the Special Stage box-to-ring detection system so wonky in the locked-on Sonic 3 Special Stages?
    What makes certain layouts, such as a few in Sonic 3 & Windows, crash the game? This doesn't happen in the original games, but it might be worth looking into for the sake of those who wish to create custom Special Stages. MainMemory ran into when he ported Mania's Bonus Stages over.