Showing posts with label Code. Show all posts
Showing posts with label Code. Show all posts

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!

Sunday, April 1, 2018

The many tendrils of a Sonic 3 level, part 3: hiding in plain sight

Have you ever been on an egg hunt, and ended up walking past the damn things multiple times without actually seeing them?

Shortly after the code for the animal object, at $2CA7C we run across the code for the title card object. The first thing it does in its init routine is check if the current zone is one of the Competition levels, and if so set byte $44 of its own SST.
Obj_TitleCardInit:
        cmpi.b  #$E,(Current_zone).w
        bcs.s   loc_2CA96
        cmpi.b  #$12,(Current_zone).w
        bhi.s   loc_2CA96
        st      $44(a0)
        jmp     (Delete_Current_Sprite).l
When this flag is set, various aspects of the object's behavior are changed in order to display a unique set of title cards in Competition mode. However, these are ultimately never shown, because the object calls the Delete_Current_Sprite function immediately after setting the flag.
loc_2CA96:
        ...
        lea     TitleCard_LevelGfx,a1
        moveq   #0,d0
        move.b  (Apparent_zone).w,d0
        lsl.w   #2,d0
        movea.l (a1,d0.w),a1
        move.w  #$A9A0,d2
        jsr     (Queue_Kos_Module).l
Shortly after, we find the code which loads the title card graphics. It uses the value of the apparent zone as an index to the TitleCard_LevelGfx array, which is a list of pointers to KosM-compressed archives containing the letters that spell out each zone's name:
TitleCard_LevelGfx:     dc.l ArtKosM_AIZTitleCard
                        dc.l ArtKosM_HCZTitleCard
                        dc.l ArtKosM_MGZTitleCard
                        dc.l ArtKosM_CNZTitleCard
                        dc.l ArtKosM_FBZTitleCard
                        dc.l ArtKosM_ICZTitleCard
                        dc.l ArtKosM_LBZTitleCard
                        dc.l ArtKosM_AIZTitleCard       ; MHZ
                        dc.l ArtKosM_AIZTitleCard       ; SOZ
                        dc.l ArtKosM_AIZTitleCard       ; LRZ
                        dc.l ArtKosM_AIZTitleCard       ; SSZ
                        dc.l ArtKosM_AIZTitleCard       ; DEZ
                        dc.l ArtKosM_AIZTitleCard       ; DDZ
                        dc.l ArtKosM_AIZTitleCard       ; HPZ
                        dc.l ArtKosM_ALZTitleCard
                        dc.l ArtKosM_BPZTitleCard
                        dc.l ArtKosM_DPZTitleCard
                        dc.l ArtKosM_CGZTitleCard
                        dc.l ArtKosM_EMZTitleCard
                        dc.l ArtKosM_BonusTitleCard
                        dc.l ArtKosM_BonusTitleCard
                        dc.l ArtKosM_BonusTitleCard
Then, at $2CC62, the apparent zone is used again, this time to inform the mapping frame used by the "name" portion of the title card:
Obj_TitleCardName:
        move.b  (Apparent_zone).w,d0
        add.b   d0,$22(a0)
The sprite mappings used by the title card object can be found at $2D90C. These mappings have the same layout as their Sonic & Knuckles counterparts, except the S&K stages all point at a blank mapping frame, and there's a mapping frame for the Competition mode title cards which was removed in Sonic & Knuckles.
Map_TitleCard:  dc.w Map_TitleCard_Blank-Map_TitleCard
                dc.w Map_TitleCard_Banner-Map_TitleCard
                dc.w Map_TitleCard_Act-Map_TitleCard
                dc.w Map_TitleCard_Zone-Map_TitleCard
                dc.w Map_TitleCard_AIZ-Map_TitleCard
                dc.w Map_TitleCard_HCZ-Map_TitleCard
                dc.w Map_TitleCard_MGZ-Map_TitleCard
                dc.w Map_TitleCard_CNZ-Map_TitleCard
                dc.w Map_TitleCard_FBZ-Map_TitleCard
                dc.w Map_TitleCard_ICZ-Map_TitleCard
                dc.w Map_TitleCard_LBZ-Map_TitleCard
                dc.w Map_TitleCard_Blank-Map_TitleCard  ; MHZ
                dc.w Map_TitleCard_Blank-Map_TitleCard  ; SOZ
                dc.w Map_TitleCard_Blank-Map_TitleCard  ; LRZ
                dc.w Map_TitleCard_Blank-Map_TitleCard  ; SSZ
                dc.w Map_TitleCard_Blank-Map_TitleCard  ; DEZ
                dc.w Map_TitleCard_Blank-Map_TitleCard  ; DDZ
                dc.w Map_TitleCard_Blank-Map_TitleCard  ; HPZ
                dc.w Map_TitleCard_2PMode-Map_TitleCard
                dc.w Map_TitleCard_Bonus-Map_TitleCard
                dc.w Map_TitleCard_Stage-Map_TitleCard
Note that both TitleCard_LevelGfx and Map_TitleCard contain valid entries for Flying Battery Zone, which is why we can get as far as displaying its title card in Sonic 3.

That's not why we're here, though. At $2CC08, right before being dismissed, the title card is responsible for loading the current level's KosM PLCs. It does this by calling the rather appropriately named LoadEnemyArt function:
LoadEnemyArt:
        lea     off_2DF60,a6
        move.w  (Apparent_zone_and_act).w,d0
        ror.b   #1,d0
        lsr.w   #6,d0
        adda.w  (a6,d0.w),a6
        move.w  (a6)+,d6
        bmi.s   locret_2DF5E

loc_2DF50:
        movea.l (a6)+,a1
        move.w  (a6)+,d2
        jsr     (Queue_Kos_Module).l
        dbf     d6,loc_2DF50

locret_2DF5E:
        rts
Once again, it uses the value of the apparent zone (and act) as an index to a pointer array, this time containing pointers to the KosM PLCs for each act in the game.

The thing is, much like TitleCard_LevelGfx and Map_TitleCard before it, this pointer array also contains valid entries for Flying Battery Zone:
off_2DF60:      dc.w PLCKosM_AIZ-off_2DF60
                dc.w PLCKosM_AIZ-off_2DF60
                dc.w PLCKosM_HCZ1-off_2DF60
                dc.w PLCKosM_HCZ2-off_2DF60
                dc.w PLCKosM_MGZ1-off_2DF60
                dc.w PLCKosM_MGZ2-off_2DF60
                dc.w PLCKosM_CNZ-off_2DF60
                dc.w PLCKosM_CNZ-off_2DF60
                dc.w PLCKosM_FBZ-off_2DF60
                dc.w PLCKosM_FBZ-off_2DF60
                dc.w PLCKosM_ICZ-off_2DF60
                dc.w PLCKosM_ICZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
                dc.w PLCKosM_LBZ-off_2DF60
Remember when I said the graphics for Flying Battery's enemies went out the door with the rest of the level load block?
PLCKosM_FBZ:    dc.w 1
                dc.l ArtKosM_Blaster
                dc.w $A000
                dc.l ArtKosM_Technosqueek
                dc.w $A500
Uh... April Fools!

The PAR code 02DF46:7010 will force every level to load Flying Battery Zone's KosM PLC. You can use this alongside the codes 05B58C:7010 and 05B5C2:7010 from my previous post in order to place the FBZ enemies in any level using debug mode. FBZ's palette is long gone (I checked), but luckily Carnival Night Zone's is a suitable replacement.

Monday, February 12, 2018

How many did I say? Sorry, I meant half that.

As I begin development of the yet unnamed hack, my first order of business is to free up as much space on the ROM as possible. We saw before how the Mega Drive has a 32 megabit, or 4 MB ROM address space, and when Sonic 3 is locked on to Sonic & Knuckles, they take up the entirety of this space.

However, as I also mentioned, the public disassembly contains a special build flag, the Sonic3_Complete flag, which according to the disassembly's README file...
[...] will change the assembly process, incorporating all necessary Sonic 3 data split from the original rom to create a complete Sonic 3 and Knuckles rom without filler.
Indeed, setting this flag causes all Sonic 3 data that isn't referenced by Sonic & Knuckles to be trimmed off, producing a lean, 3.23 MB ROM. Can we get it even smaller, though?

As an initial approach, I tried repacking all of the compressed assets in the game using flamewing's optimized suite of compressors. These are able to output smaller versions of the same files, but which the game's Kosinski and Nemesis decompressors can unpack all the same. Doing so brings the ROM size down to 3.16 MB.

We can do better, though. As you may recall, the Sonic 3 cartridge contains a small subset of Knuckles' sprites that are used during his cutscene appearances. In Sonic 3 & Knuckles, these sprites are still used for the cutscenes which take place in Sonic 3 stages, so the Sonic3_Complete flag includes the cutscene art block in the ROM.


Much like the player object, cutscene Knuckles uses DPLCs to ensure that only one of its sprites needs to be loaded to VRAM at a time. This requires that all of his sprites be stored uncompressed in the ROM, which takes up a significant amount of space. All told, Knuckles takes up about 19.7 KB of the Sonic 3 cartridge.

However, since player sprites are also uncompressed, this presents an opportunity. If we take a close look at the above sprites, we can see that most of them are identical to sprites which can be found in the main Knuckles art block. In fact, only nine sprites along the bottom row are unique to the cutscene art block.


Once isolated, these sprites only take up about 4.3 KB. If we add them to the main Knuckles art block, and then simply edit cutscene Knuckles' DPLC scripts to point at the identical sprites in the main art block, we can lose the cutscene art block entirely and save about 15.4 KB.

The main art block is exactly 130,944 bytes. Adding the sprites above makes it 135,360 bytes. That's just over 132 KB.

Uh-oh.

To understand why this is a problem, let's look at the documentation for the DPLC format over at the Sonic Retro wiki:
Sonic 3 & Knuckles uses the same DPLC format as Sonic 2 for the main characters: the first word is the number of DPLC requests to make, and each successive word (up to the value of the first word) is split up so that the first nybble is the number of tiles to load minus one, and the last three nybbles are the offset (in tiles, i.e. multiples of $20 bytes) of the art to load from the beginning of the object's specified art offset in ROM.
To put it differently, each DPLC entry is a word in the format $NAAA, in which N is the number of tiles to copy to VRAM (minus one), and AAA is the the number of the tile to start copying from. So for instance, if we wanted to queue up tiles $3E2 through $3E7, that's six tiles starting from $3E2, so we would write down $53E2.

This format can only index up to $1000 tiles, and because each tile is $20 bytes long, that means the art block can't be larger than $1000 × $20 = $20000 bytes, or 128 KB.

So now we have to go into the art block and somehow save up 4 KB of tiles in order to add in the 4.3 KB of sprites from the cutscene block. Thankfully, there are a lot of poorly optimized sprites to aid us in our task.


For instance, in the teetering sprites above, there's a lot of empty space, and the shoe on the floor appears three times despite being identical in each frame. By nudging Knuckles around slightly, we can reduce the size of the sprite pieces and reuse the shoe for every frame, saving about a dozen tiles. Rinse and repeat for all 252 of Knuckles' frames.

You do the impossible and fit everything into 128 KB. That should work, right? Just build the ROM and-- oh, bloody hell.



Assuming you actually followed that link to the Sonic Retro wiki, you may have noticed that the DPLC format is different between Player Characters and Other Objects. Here's what it says in the latter case:
For other objects, the first word is the number of DPLC requests to make minus one, and each successive word (up to the value of the first word plus one) is split up so that the last nybble is the number of tiles to load minus one, and the first three nybbles are the offset (in tiles, i.e. multiples of $20 bytes) of the art to load from the beginning of the object's specified art offset in ROM.
Cutscene Knuckles isn't a player character, so by exclusion, he must be an "other object". So what, that just means the DPLC entry format is $AAAN rather than $NAAA, right?

Not so fast. In order to calculate the ROM offset for the DMA transfer, the DPLC code must take the starting tile number AAA and multiply it by $20, the size of each tile. Let's look at how the code in Knuckles_Load_PLC does it:
                andi.w  #$FFF,d1
                lsl.l   #5,d1
Pretty straightforward. The bottom 24 bits are isolated, and then promoted to a longword as the value is shifted left five times, multiplying it by $20. Now let's look at the general purpose code in Perform_DPLC:
                andi.w  #$FFF0,d1               ; Isolate all but lower 4 bits
                add.w   d1,d1
This time around, the tile number is in the top 24 bits, so it is already multiplied by $10 and only needs to be multiplied by two in order to calculate the offset. This is done by adding by adding the contents of the d1 register back onto itself, without promoting the value to a longword.

There are two consequences to this. One is that the d1 register does not have to be cleared before loading each new DPLC entry. The other is that tiles past the 64 KB boundary are inaccessible: since the topmost bit is always truncated, a request for tile $9B9 results in tile $1B9 being loaded instead.

At this point I just shrugged, moved all the cutscene sprites to the first half of the art block, and called it a day.

Friday, January 26, 2018

Too many tails

Reader muteKi asks:
Fun little thing I've been at least a little curious about (and got to thinking about it because of the recent posts on Big Arm / LBZ's end sequence) -- for some reason there's a regression with Tails's tails not getting removed at the end of the stage when the Death Egg falls, specifically in Tails alone mode in S3K.
So, to clarify: at the end of Launch Base Zone 2, the player automatically turns to face the Death Egg, which is falling in the background. Exclusively in Sonic 3 & Knuckles though, Tails' namesake appendages aren't hidden as his animation changes from looking up to the standing rotation, granting him an extra set of tails for the duration of the sequence.


In order to understand what's going on, let's first take a brief look at how Tails' tails operate. Each frame, the tails object looks at Tails' animation to determine which animation it should play.

For instance, when Tails is in his looking up animation (7), his tails will quickly flick up and down. Meanwhile, when he's in his walking animation (0), the object blanks itself out, because Tails' walking cycle already has the tails baked in.
; animation master script table for the tails
; chooses which animation script to run depending on what Tails is doing
Obj_Tails_Tail_AniSelection:
    dc.b    0,0     ; TailsAni_Walk,Run     -> Blank
    dc.b    3       ; TailsAni_Roll         -> Directional
    dc.b    3       ; TailsAni_Roll2        -> Directional
    dc.b    9       ; TailsAni_Push         -> Pushing
    dc.b    1       ; TailsAni_Wait         -> Swish
    dc.b    0       ; TailsAni_Balance      -> Blank
    dc.b    2       ; TailsAni_LookUp       -> Flick
    dc.b    1       ; TailsAni_Duck         -> Swish
    dc.b    7       ; TailsAni_Spindash     -> Spindash
    ...
Alright, now let's look at the changes made to the cutscene object between Sonic 3 (left) and Sonic & Knuckles (right):
loc_5117A:                                      loc_72C3C:
    move.l  #locret_511CC,(a0)                      move.l  #loc_72C68,(a0)
    clr.b   ($FFFFFA88).w                           clr.b   ($FFFFFA88).w
    clr.w   $1C(a1)                                 jsr     (Stop_Object).l
    clr.w   $18(a1)
    clr.w   $1A(a1)
    bclr    #0,4(a1)                                bclr    #0,4(a1)
    bclr    #0,$2A(a1)                              bclr    #0,$2A(a1)
    move.w  #$101,(Ctrl_1_logical).w                move.w  #$101,(Ctrl_1_logical).w
    st      (Ctrl_1_locked).w
    jsr     Create_New_Sprite
    bne.s   loc_511C4
    move.l  #loc_5182E,(a1)
    lea     (Player_1).w,a2
    move.w  $10(a2),$10(a1)
    move.w  $14(a2),$14(a1)

loc_511C4:
    lea     ChildObjDat_52010(pc),a2                lea     ChildObjDat_73806(pc),a2
    jmp     CreateChild6_Simple(pc)                 jmp     (CreateChild6_Simple).l
Okay, a lot of structural differences right off the bat. First, S3A stops the player by clearing their three velocity values in-line; S&K instead opts to call the Stop_Object function, which does the same exact thing.

The other big structural change is that the S3A object actually spawns another object to run the next part of the code at loc_5182E, while setting its own code pointer to a stub location. The S&K object cuts the middle man by setting its own code pointer directly to the next bit of code, at loc_72C68.

Beyond those points, the only difference so far is that S3A locks the player's controls by setting the Ctrl_1_locked flag.
loc_5182E:                                      loc_72C68:
    btst    #0,($FFFFFA88).w                        btst    #0,($FFFFFA88).w
    beq.w   locret_50DD2                            beq.s   locret_72C9C
    move.l  #loc_5185A,(a0)                         move.l  #loc_72C9E,(a0)
    move.l  #loc_51868,$34(a0)                      move.l  #loc_72CBE,$34(a0)
    clr.b   (Ctrl_1_locked).w
    lea     (Player_1).w,a1                         lea     (Player_1).w,a1
                                                    bsr.w   sub_72C8E
                                                    lea     (Player_2).w,a1
                                                    clr.b   $20(a0)

                                                sub_72C8E:
    move.b  #$53,$2E(a1)                            move.b  #$83,$2E(a1)
    move.b  #0,$20(a1)                              clr.b   $24(a1)
                                                    clr.b   $23(a1)

                                                locret_72C9C:
                                                    rts
Here we go. Apart from S3A now clearing the flag it had just previously set, and different flags being set on the player's object_control bitfield, the code was altered to account for player 2, which now joins player 1 during the Beam Rocket fight in a Sonic and Tails game.

Note how S3A sets the player's current animation (at offset $20 of their SST) to 0, walking, just as the standing rotation frames kick in. This has the effect of blanking out Tails' tails, according to the rules in the Obj_Tails_Tail_AniSelection lookup table. Note further how S&K tries to do the same thing just to player 2, but ends up pointing at register a0 rather than a1, clearing offset $20 of the cutscene object, rather than Tails' current animation.

Okay, so that line of code is wrong, but fixing it wouldn't solve our problem: when playing as Tails alone, the Tails object is in the player 1 slot, not the player 2 slot, and the code very specifically avoids clearing player 1's animation for some reason. (Why? Who knows.)

The weird part is that when you play as Sonic and Tails, the tails object IS blanked out properly.


The reason for this lies in the code which runs through the standing rotation frames:
loc_5185A:                                      loc_72C9E:
                                                   lea     (Player_1).w,a1
    lea     byte_52070(pc),a1                      lea     byte_7386A(pc),a2
    bsr.w   Animate_ExternalPlayerSprite           jsr     (Animate_ExternalPlayerSprite).l
    jmp     (Player_Load_PLC).l                    lea     (Player_2).w,a1
                                                   clr.b   $20(a1)
                                                   lea     byte_73874(pc),a2
                                                   jmp     (Animate_ExternalPlayerSprite).l
Clearly the developers also had no clue why Tails' tails weren't going away, so they just made it so Tails' animation gets cleared every frame from then on. Now that's class.

The correct solution is to fix the clr.b $20(a0) line and move it inside the sub_72C8E function so it affects both players.

Thursday, January 25, 2018

Why do the S monitors in S3A turn Tails into a block of mush?

Several weird things happen when Tails breaks open an S monitor in S3A. First off, he takes on this ungodly scrambled appearance, all while not receiving the customary 50 rings.


Then, although he does become invincible, his speed isn't increased, nor are rings ever drained from his count. He also doesn't glow at all, though by defeating some enemies, we can verify that Flickies still become yellow.


So what's going on here? Well, the short version is that Super Tails simply doesn't exist yet. To get into the nitty-gritty of it however, we need to take a look at the Monitor_Give_SuperSonic function in S3A.

As we previously saw, in S&K this function is broken up into three distinct code paths, and which path is taken depends on the player's current character. In S3A though, the same function is completely linear, which creates some interesting differences in its behavior when playing as Tails. S3A to the left, S&K to the right:
Monitor_Give_SuperSonic:                                Monitor_Give_SuperSonic:
    addq.w  #1,(a2)                                         addq.w  #1,(a2)
    addi.w  #50,(Ring_count).w                              addi.w  #50,(Ring_count).w
    move.b  #1,(Super_Hyper_palette_status).w               move.b  #1,(Super_Hyper_palette_status).w
    move.b  #$F,(Palette_timer).w                           move.b  #$F,(Palette_timer).w
    move.b  #1,(Super_Sonic_Knux_flag).w                    move.b  #1,(Super_Sonic_Knux_flag).w
    move.w  #60,(Super_Hyper_frame_count).w                 move.w  #60,(Super_Hyper_frame_count).w
    move.l  #Map_SuperSonic,(Player_1+mappings).w           ...
    move.b  #$81,(Player_1+object_control).w                move.b  #1,(Super_Tails_flag).w
    move.b  #$1F,(Player_1+anim).w                          move.b  #$29,(Player_1+anim).w
    move.l  #Obj_SuperSonic_Stars,(Super_stars).w
    move.w  #$A00,(Sonic_Knux_top_speed).w                  move.w  #$800,(Tails_top_speed).w
    move.w  #$30,(Sonic_Knux_acceleration).w                move.w  #$18,(Tails_acceleration).w
    move.w  #$100,(Sonic_Knux_deceleration).w               move.w  #$C0,(Tails_deceleration).w
                                                            move.l  #Obj_HyperTails_Birds,(Hyper_Sonic_stars).w
                                                            ...
                                                            move.b  #$81,(Player_1+object_control).w
    move.b  #0,(Player_1+invincibility_timer).w             move.b  #0,(Player_1+invincibility_timer).w
    bset    #Status_Invincible,status_secondary(a1)         bset    #Status_Invincible,status_secondary(a1)
    moveq   #$46,d0                                         moveq   #$9F,d0
    jsr     (Play_Sound_2).l                                jsr     (Play_Sound_2).l
    moveq   #$2C,d0                                         moveq   #$2C,d0
    jmp     (Play_Sound).l                                  jmp     (Play_Sound).l

First off, both versions of the function add 50 rings to the Ring_count RAM variable, no change there. In S3A however, Tails actually uses the Ring_count_P2 variable, a holdover from Sonic 2's 2P mode in which each character kept track of their own ring count. This accounts for Tails' unchanging ring count.

Next, S3A replaces player 1's mappings with the mappings for Super Sonic. In S&K, Sonic is the only character whose sprites change for his super form; S3A assumes that only Sonic will ever turn super, therefore the player should always receive the new mappings. This results in Tails' corrupted appearance.

Only S&K sets what would become the Super_Tails_flag, enabling Super Tails' rotating palette. However, both games set the Super_Sonic_Knux_flag, which enables Super Sonic's rotating palette and makes Flickies turn yellow.

S3A only writes Super Sonic's speed values to Sonic's speed variables, leaving Tails' values unaffected. And finally, as we've seen before, rather than play the regular transformation sound, S3A monitors play a whistling sound instead.


The sprite corruption can be avoided with debug mode. When the player enters debug mode, their mappings are saved to RAM address $FFCA, so they can be restored upon exiting the mode. By waiting out the transformation sequence in debug mode, Tails' old mappings are kept safe at $FFCA and will undo the damage done by the S monitor.

Tuesday, January 23, 2018

Tails' extra sprites

If you happened to follow my suggestion of loading a savestate of the S3&K level select in Sonic & Knuckles, you'll find that you can now circumvent the restrictions imposed by the S&K alone flag, and are able to play the game as Tails.

Note that to prevent the game from crashing, Japan mode must be enabled in the S3&K savestate. This is because the game tries to load Tails' lives counter from the Sonic 3 ROM, making the Nemesis decompressor choke on the garbage data. As we previously saw though, the Miles lives counter is part of the Sonic & Knuckles ROM, so that works out.


When you do so, you might notice that Tails appears to be completely invisible. Sonic & Knuckles normally gets its Tails art from the Sonic 3 ROM, and since it's missing, the DMA transfers from that address space simply result in a string of zeroes, which translate to transparent pixels.

Knowing this, you may be surprised to learn that certain gimmicks in the later stages cause Tails to temporarily become visible again, such as the 3D pathways in Lava Reef Zone:


The reason for this lies in the Tails_Load_PLC function. When Tails' mapping frame is greater than $D1, the standard ArtUnc_Tails uncompressed art block is swapped out for the ArtUnc_Tails_Extra art block:
Tails_Load_PLC:
    moveq   #0,d0
    move.b  $22(a0),d0

Tails_Load_PLC2:
    cmp.b   ($FFFFF7DE).w,d0
    beq.s   locret_15CCE
    move.b  d0,($FFFFF7DE).w
    lea     (PLC_Tails).l,a2
    add.w   d0,d0
    adda.w  (a2,d0.w),a2
    move.w  (a2)+,d5
    subq.w  #1,d5
    bmi.s   locret_15CCE
    move.w  #$D400,d4
    move.l  #ArtUnc_Tails,d6
    cmpi.w  #$1A2,d0
    blo.s   loc_15CA6
    move.l  #ArtUnc_Tails_Extra,d6
It turns out the ArtUnc_Tails_Extra art block is actually part of the Sonic & Knuckles ROM, which is why Tails suddenly becomes visible when his mapping frame is set to a value higher than $D1. Unsurprisingly, those mapping frames don't exist in Sonic 3: they were added specifically for Sonic & Knuckles.

The implication here is that any gimmicks making use of these frames hadn't yet been designed at the time of Sonic 3's release, which is why the frames themselves are missing. On the other hand, over at Flying Battery Zone...


...Tails remains invisible in his monkey bar frames, indicating that those are read from the Sonic 3 cartridge, even when the two games are locked-on. Just another sign that Flying Battery Zone was all but meant for the first half of Sonic 3.

Thursday, January 18, 2018

Keep your arms inside at all times

One version difference that a lot of people latch onto is Launch Base Zone's final boss, Big Arm, and the behavior of its grabbing attack when the player is Super. In Sonic & Knuckles, this just bounces the player off the ground with a spring sound, but in Sonic 3, it causes Super Sonic to drop all of his rings and thus revert back to normal Sonic.


The reason for this is boring: in Sonic 3, regardless of whether the player is Super or not, the HurtCharacter function is called, which uh, hurts the character. Meanwhile, Sonic & Knuckles added a check for invincibility (not Super!) that calls up a separate code path if the appropriate bit is set. This suggests the original behavior was just an oversight.
loc_510AE:                          loc_74664:
    move.w  d0,$18(a0)                  move.w  d0,$18(a0)
    move.w  #-$600,$1A(a0)              move.w  #-$600,$1A(a0)
    move.w  #$3F,$2E(a0)                move.w  #$3F,$2E(a0)
                                        btst    #Status_Invincible,(Player_1+status_secondary).w
                                        bne.s   loc_7468C
    movea.l a0,a2                       movea.l a0,a2
    lea     (Player_1).w,a0             lea     (Player_1).w,a0
    jsr     (HurtCharacter).l           jsr     (HurtCharacter).l
    movea.l a2,a0                       movea.l a2,a0
    rts                                 rts
                                    
                                    loc_7468C:
                                        lea     (Player_1).w,a1
                                        clr.b   $2E(a1)
                                        neg.w   d0
                                        move.w  d0,$18(a1)
                                        move.w  #-$400,$1A(a1)
                                        bset    #1,$2A(a1)
                                        bclr    #3,$2A(a1)
                                        clr.b   $40(a1)
                                        clr.b   $3D(a1)
                                        move.b  #2,$20(a1)
                                        move.b  #2,5(a1)
                                        moveq   #-$4F,d0
                                        jmp     (Play_Sound_2).l
There's another, more interesting oversight with the Big Arm boss, which wasn't fixed in Sonic & Knuckles. It requires a second player, however in Sonic 3, the Tails object is deleted during the Egg Mobile ride, and in Sonic & Knuckles, only Knuckles fights the boss!

We can force the bug to occur in Sonic 3 by using the PAR code 05A8CC:6010, which prevents Tails from despawning at the end of Launch Base 2. Get the boss down to 1 HP, then let it grab you while having Tails deal the final hit.


When this happens, Sonic gets stuck in mid-air, at least until the ending sequence starts. The effects are more heinous in Sonic & Knuckles because a regular stage clear sequence was added to the level, which can't start because Sonic is not touching the floor.

What's interesting about this oversight is that both Sonic 3 and Sonic & Knuckles have code in place to prevent it. If the byte at offset $30 of Big Arm's SST is clear, then it calls the Restore_PlayerControl function, which clears the player's object_control flag and resets their animation:
loc_51D78:                                  loc_7506E:
    tst.b   $30(a0)                             tst.b   $30(a0)
    bne.s   loc_51D82                           bne.s   loc_7507A
    jsr     Restore_PlayerControl(pc)           jsr     (Restore_PlayerControl).l
                                            
loc_51D82:                                  loc_7507A:
    jmp     (SaveGame).l                        lea     ChildObjDat_75186(pc),a2
                                                jmp     (CreateChild1_Normal).l
Problem is, the byte at offset $30 is set when the boss is holding the player, which means the Restore_PlayerControl function is only called in the exact wrong situation: when the boss isn't holding the player.


This can be seen in both Sonic 3 and Sonic & Knuckles: upon dealing the final hit to the boss, the player's animation is reset, which results in them falling to the ground in their standing frame.

Tuesday, January 16, 2018

They just didn't care

There are three separate RAM flags which the game uses to track the state of debug mode: the debug cheat flag, the debug mode flag and debug placement mode.

  • The debug cheat flag is set when the player correctly inputs the debug cheat code, and is only cleared by a hard reset.
  • The debug mode flag is set when the player holds down the A button during level load, but only after the debug cheat flag has been set. It is cleared at the SEGA screen.
  • Finally, debug placement mode is set when the player presses the B button during a level, but only after the debug mode flag has been set. It is cleared when the player presses the B button again.

Sonic 3 and Sonic & Knuckles disagree on a few things regarding these flags. In Sonic 3 for instance, the debug cheat flag is set at the same time the level select is unlocked, but it has a separate cheat code in Sonic & Knuckles.

In particular though, just as with past Sonic games, Sonic 3 activates the debug HUD as soon as the debug mode flag is on, whereas Sonic & Knuckles waits until debug placement mode is also on, something which I've used extensively in the past to easily differentiate between S3A and S&K screenshots.


Of course, once the player exits object placement mode, the game must now restore the regular HUD. It can't just draw the score counter over the debug display, because any empty digits in the score counter would leave behind part of the debug display; it needs to wipe the whole thing clean first and draw the counters back in afterwards.

To that effect, the object placement exit code calls the HUD_DrawInitial function, which is used to initialize the HUD at level load. This is why the timer temporarily reverts to 0:00 until the next second rolls over.
    moveq   #0,d0
    move.w  d0,(Debug_placement_mode).w
    move    #$2700,sr
    jsr     (HUD_DrawInitial).l
    move.b  #1,(Update_HUD_score).w
    move.b  #-$80,(Update_HUD_ring_count).w
    move    #$2300,sr
    lea     (Player_1).w,a1
    move.l  ($FFFFFFCA).w,$C(a1)
    move.w  ($FFFFFFCE).w,$A(a1)
Unfortunately, nobody bothered to test this change in Competition mode. The regular HUD is never meant to be shown here, and so when HUD_DrawInitial is called, something goes wrong, and the player object is never properly restored, permanently removing them from the game.


This oversight definitely makes my Top 5 Stupidest Bugs in Sonic 3 list, since it makes debug mode totally useless in the Competition stages. Not only can you never interact with the items you place, but you can't even use debug mode's most basic functionality, free movement, because you can't place yourself back at the new position. Sheesh.

Friday, January 12, 2018

Can't escape!

Reader Silver Sonic 1992 asks:
Also, do you know what the "Special Stage 1" is for in the level select? Is it remnant of the super emerald special stages?
Doesn't look like it, but it's interesting nonetheless.

The special stage entries in the level select actually refer to an invalid zone ID, zone $40. When this zone is selected, a special handler prevents the invalid zone from getting loaded, and instead modifies the value of the Game_mode RAM variable, setting it to $2C for act 1, and $34 for act 2:
LevelSelect_PressStart:
    move.w  ($FFFFFF82).w,d0
    add.w   d0,d0
    move.w  LS_Level_Order(pc,d0.w),d0
    bmi.w   LevelSelect_Return
    cmpi.w  #$5555,d0                   ; This is used for the S&K zones
    beq.w   LevelSelect_Main
    cmpi.w  #$4001,d0                   ; Is Special Stage 2 selected?
    beq.w   LevelSelect_SpecialStage    ; If so, branch
    cmpi.w  #$4000,d0                   ; Is Special Stage 1 selected?
    bne.w   LevelSelect_StartZone       ; If not, branch
    move.b  #$2C,(Game_mode).w
    rts
; ---------------------------------------------------------------------------

LevelSelect_SpecialStage:
    move.b  #$34,(Game_mode).w
    rts
Once this variable is changed, the game instantly switches away from the current mode, $28 (level select) to whichever one got picked above. In Sonic & Knuckles, mode $2C refers to the title screen for the Blue Sphere bonus game, but in S3A, both it and $34 point at the special stage game mode.
GameModes:
    dc.l Sega_Screen                ;   0
    dc.l Title_Screen               ;   4
    dc.l Level                      ;   8
    dc.l Level                      ;  $C
    dc.l JumpToSegaScreen           ; $10
    dc.l ContinueScreen             ; $14
    dc.l JumpToSegaScreen           ; $18
    dc.l LevelSelect_S2Options      ; $1C
    dc.l S3Credits                  ; $20
    dc.l LevelSelect_S2Options      ; $24
    dc.l LevelSelect_S2Options      ; $28
    dc.l SpecialStage               ; $2C
    dc.l SpecialStage               ; $30
    dc.l SpecialStage               ; $34
    dc.l Competition_Menu           ; $38
    dc.l Competition_PlayerSelect   ; $3C
    dc.l Competition_LevelSelect    ; $40
    dc.l Competition_Results        ; $44
    dc.l SpecialStage_Results       ; $48
    dc.l SaveScreen                 ; $4C
    dc.l TimeAttack_Records         ; $50
At the end of the special stage mode's main loop, the Game_mode RAM variable is checked. If it's anything other than $34, then instead of jumping back to the start of the loop, execution progresses into a second loop, in which the screen progressively fades to white. The equivalent code can be found at loc_851A in the S&K disassembly.
    cmpi.b  #$34,(Game_mode).w
    beq.s   loc_77D2
    tst.w   (Demo_mode_flag).w
    beq.s   loc_7828
    move.b  #0,(Game_mode).w

loc_7828:
    move.w  #$3C,(Demo_timer).w
    move.w  #$3F,(Palette_fade_info).w
    clr.w   ($FFFFF794).w

loc_7838:
    move.b  #$1C,(V_int_routine).w
    bsr.w   Wait_VSync
    jsr     (Process_Sprites).l
    bsr.w   Animate_SSRings
    bsr.w   sub_8C1A
    jsr     (Render_Sprites).l
    jsr     Draw_SSSprites(pc)
    bsr.w   sub_8B9A
    bsr.w   sub_89E2
    bsr.w   Process_Nem_Queue_Init
    jsr     (Process_Kos_Module_Queue).l
    subq.w  #1,($FFFFF794).w
    bpl.s   loc_787C
    move.w  #2,($FFFFF794).w
    bsr.w   Pal_ToWhite

loc_787C:
    tst.w   (Demo_timer).w
    bne.s   loc_7838
Now, if the debug cheat is enabled, then pressing the A button while the game is paused makes the game jump directly to the level select. This is achieved by setting the Game_mode variable to $28, the value for the level select mode:
Pause_Loop:
    move.b  #$10,(V_int_routine).w
    bsr.w   Wait_VSync
    tst.b   (Slow_motion_flag).w
    beq.s   Pause_NoSlowMo
    btst    #6,(Ctrl_1_pressed).w
    beq.s   Pause_ChkFrameAdvance   ; branch if A isn't pressed
    move.b  #$28,(Game_mode).w
    nop
    bra.s   Pause_ResumeMusic
And that's what that check in the special stage mode was for. When the Game_mode variable changes from its regular value of $34, the game knows that it has to exit the special stage and go someplace else (for instance, the level select) so it begins fading to white, and once the fade is over, it jumps to the new game mode.

Except that, when we choose the "special stage 1" option, the Game_mode variable starts off with the value $2C. This calls up the special stage mode all the same, except the check fails immediately, triggering a fade out. Once the fade is over, the game jumps to the "new" game mode, $2C... which calls up the special stage mode all over again.

The cherry on top of the cake is this code at the end of that second loop:
loc_787C:
    tst.w   (Demo_timer).w
    bne.s   loc_7838
    addq.b  #1,($Current_special_stage).w
    cmpi.b  #7,(Current_special_stage).w
    bcs.s   locret_7894
    move.b  #0,(Current_special_stage).w

locret_7894:
    rts
This code ensures that the current stage value is properly incremented when the player exits the stage using the debug shortcut. That way, next time a special stage is accessed, the game will advance to the next stage in line, just like if the previous one had completed normally.

Inadvertently, this causes the "special stage 1" bug to cycle through each stage in an infinite loop. In Sonic & Knuckles, not only was the bug fixed, but the debug shortcut now leads to the SEGA screen, so the above code was removed.

Thursday, December 28, 2017

Roll height bugs, part 2

Objects that take control away from the player generally fail to handle the case in which Sonic is spinning. For instance, with the tube elevators I talked about last time, when you spindash directly into one, Sonic's feet will dig into the bottom of the capsule because his hitbox size and Y position aren't reset to their usual values.


This sort of oversight is normally harmless because Sonic only sinks into the object by about five pixels, a short enough distance that he ends up getting pushed out in the same direction he came from.

Play around with fire however, and eventually you'll get burned. When the player rides on one of Marble Garden Zone's spinning tops, it executes this interesting snippet of code:
loc_35048:
    move.b  $44(a1),d0
    addi.b  #$18,d0
    move.b  d0,$1E(a1)
    ...
Offsets $44 and $45 of the player's SST hold the default values of their y_radius and x_radius attributes. They are set once when the player object is first initialized and henceforth only read from, to restore the player's hitbox after rolling.

The spinning top object isn't messing with these values, which is good. But it is taking the default Y radius, adding 24 to it, and writing the new value to the player's y_radius attribute, which effectively changes the size of Sonic's hitbox from the usual 20×40 pixels to 20×88 pixels. This way lies madness.


Why does the object do this? To trick the solid object code into thinking the bottom of Sonic's hitbox is all the way at the bottom of the spinning top. You see, the game is written to process two kinds of solid collisions: object-to-level collision, and player-to-object collision. It is not equipped to process solidity between any two arbitrary objects.

However, by hijacking the player's own hitbox, the spinning top object can simulate floor collision between itself and the tops of other objects, which is all it really needs, without having to mess around with any of the underlying code.

Except the underlying code just so happens to contain this fragment (relevant bits in bold):
Sonic_TouchFloor:
    move.b  $1E(a0),d0
    move.b  $44(a0),$1E(a0)
    move.b  $45(a0),$1F(a0)
    btst    #2,$2A(a0)
    beq.s   loc_121D8
    bclr    #2,$2A(a0)
    move.b  #0,$20(a0)
    sub.b   $44(a0),d0
    ext.w   d0
    tst.b   (Reverse_gravity_flag).w
    beq.s   loc_121C4
    neg.w   d0

loc_121C4:
    move.w  d0,-(sp)
    move.b  $26(a0),d0
    addi.b  #$40,d0
    bpl.s   loc_121D2
    neg.w   (sp)

loc_121D2:
    move.w  (sp)+,d0
    add.w   d0,$14(a0)

loc_121D8:
    ...
When bit 2 of the player's status bitfield (aka their rolling flag) is set, the above code must shift their Y position up a few pixels in order to account for the change in their hitbox size. It calculates the number of pixels by subtracting the default Y radius from the current collision height, which is usually 30 pixels while rolling.

The current collision height is 88 pixels.


Here's the rundown: if Sonic starts riding the spinning top while rolling, then landing on a solid object will make the floor collision code attempt to shift him up by five pixels. However, since his current Y radius is an insane value, this ends up shifting him down by 24 pixels, which is enough to clear the sinking mud object and send him right through the floor.


It doesn't stop there. Because Sonic never dismounted the spinning top properly, his hitbox is still abnormally tall, which causes him to touch the floor much sooner than he normally would. Fortunately, the floor collision code restores Sonic's hitbox size whenever he lands, so there's no long-term damage.

Tuesday, December 26, 2017

You had it in you all along

In Launch Base Zone 1, you may have occasionally experienced a bug where you ride one of the tube elevator capsule thingies, and when you arrive at the other end, the door is already shut and you're immediately spat out of the lift.


So what's going on? First, let's get the basics of the tube elevator down. Even when you take another route through the level, or run far away and then come back, you'll always find a closed elevator sitting at the back end of the tube.


This suggests that the closed elevators are a permanent fixture of the stage, a fact confirmed by looking at the contents of the level's object layout: at the end of every tube is a second elevator object with bit 6 of its subtype set. A look at the object's code confirms that this causes it to spawn directly in the closed state:
Obj_LBZTubeElevator:
    move.l  #Map_LBZTubeElevator,$C(a0)
    move.w  #$2455,$A(a0)
    move.b  #$18,7(a0)
    move.b  #$30,6(a0)
    ori.b   #4,4(a0)
    move.w  #$80,8(a0)
    move.w  $10(a0),$44(a0)
    move.w  $14(a0),$46(a0)
    btst    #6,$2C(a0)
    beq.s   loc_29CEC
    move.b  #0,$22(a0)                  ; If BIT 6 of subtype set, the elevator remains closed
    move.b  #0,$26(a0)
    move.l  #Obj_LBZTubeElevatorClosed,(a0)
    bra.s   Obj_LBZTubeElevatorClosed

loc_29CEC:
    ...
Now we have a problem, though. When we do take the elevator, we don't want that second capsule waiting at the other end of the tube. This is accounted for by the closed elevator's code, which starts off with a rather complicated check:
Obj_LBZTubeElevatorClosed:
    lea     (Player_1).w,a1
    btst    #3,$2A(a1)
    beq.s   loc_29D8E
    tst.b   $2E(a1)
    beq.s   loc_29D8E
    movea.w $42(a1),a2
    cmpi.l  #Obj_LBZTubeElevatorActive,(a2)
    bne.s   loc_29D8E
    move.w  #$7FF0,$10(a0)

loc_29D8E:
    ...
Alright, so three things are being determined here. First, the object retrieves player 1's SST and checks whether bit 3 of its status bitfield is set: this is player 1's standing on object flag. If so, it then checks whether the player's object_control bitfield has any of its bits set: this is a sign that the player's movement is being controlled by another object.

If both those checks pass, then the elevator pulls out the big guns. It retrieves the word value stored at offset $42 of the player's SST, which we saw before contains the RAM address of the last object the player stood on. Since the player is currently standing on an object, this is the RAM address of the object the player is standing on right now.

It's time for the final check: the elevator looks at the code pointer of the object the player is currently standing on. If that object happens to be another elevator that isn't closed, then the first elevator assumes the second one is about to drop off the player at the back end of the tube, so it moves itself all the way to the end of the level, where it'll be collected by the object deletion routine.

So, does it work? Most of the time, yes. The elevator object bobs up and down inside the tube, which usually lifts Sonic off the floor, setting up the appropriate values in his SST.


However, when the elevator is at its lowest point, it is actually far enough away that Sonic will not snap onto its surface, which results in the tube ride starting while the player is still standing on solid ground.


When this happens, the player's object_control bitfield is set, but their status bitfield isn't, making the above checks fail. This causes the second elevator to spawn normally, and it is this elevator which squeezes Sonic out of the tube. (If you look closely, you'll be able to see both elevators bobbing up and down out of sync.)

Monday, December 25, 2017

Christmas corrections

Today is Christmas Day, a holiday which is typically celebrated by showering the people you love the most with copious amounts of gifts. Keeping with tradition, then, I thought this would be the perfect opportunity to celebrate three gifts that my readers have graciously offered me through the comments section of this blog.

(Stuttering Craig voice) This is Sonic 3 Unlocked's 2017 Top 3 Christmas Corrections!



Number Three!

As part of my short series on Lock-on Technology, I pointed out a difference with Knuckles' climbing animation between Knuckles in Sonic 2 and Sonic & Knuckles: exclusively in the latter, whenever Knuckles stands still on a wall, he reverts back to the first frame of the climbing animation.


I chalked this up to a feature introduced in the S&K version of the Knuckles object, but later, an anonymous commenter performed their own analysis of the source code, which I present below. Turns out, it's not actually a feature, it's a bug:
I think the second behaviour quirk you mentioned in this post is the result of a bug. There's some code in S3K that isn't in KiS2, at loc_16E10 in the current S3K Git disasm. The equivalent label in KiS2's Git disasm is loc_315B04.

What I think this new code does is handle floor collision, because Knuckles still seems to move briefly after the player stops pressing the D-Pad. The issue is, this new code overwrites d1 with the distance Knuckles is from the floor. d1 is checked immediately afterwards, has Knuckles's frame ID added to it, and is then used to calculate which frame Knuckles should display.

d1 will always be a positive number, usually a large one depending on how far Knuckles is from the ground. This means, when Knuckles's frame ID is added to it, it goes well beyond the ceiling value of $BC, causing the game to reset it to $B7, making Knuckles display the first frame of his animation. Chances are the number could overflow, too, causing him to display his last frame instead.

Safe to say, editing the code to properly back up d1 causes it to behave like KiS2 instead.
Let's take a look at the code mentioned. Knuckles in Sonic 2 to the left, Sonic & Knuckles to the right. Changes in bold:
loc_315B04:                                     loc_16E10:
                                                    move.b  (Ctrl_1_logical).w,d0
                                                    andi.b  #3,d0
                                                    bne.s   loc_16E34
                                                    move.b  $46(a0),d5
                                                    move.w  $14(a0),d2
                                                    addi.w  #9,d2
                                                    move.w  $10(a0),d3
                                                    bsr.w   sub_F828
                                                    tst.w   d1
                                                    bmi.w   loc_16D6E

                                                loc_16E34:
    tst.w   d1                                      tst.w   d1
    beq.s   loc_315B30                              beq.s   loc_16E60
    subq.b  #1,$1F(a0)                              subq.b  #1,$25(a0)
    bpl.s   loc_315B30                              bpl.s   loc_16E60
    move.b  #3,$1F(a0)                              move.b  #3,$25(a0)
    add.b   $1A(a0),d1                              add.b   $22(a0),d1
    cmp.b   #$B7,d1                                 cmpi.b  #$B7,d1
    bcc.s   loc_315B22                              bhs.s   loc_16E52
    move.b  #$BC,d1                                 move.b  #$BC,d1

loc_315B22:                                     loc_16E52:
    cmp.b   #$BC,d1                                 cmpi.b  #$BC,d1
    bls.s   loc_315B2C                              bls.s   loc_16E5C
    move.b  #$B7,d1                                 move.b  #$B7,d1

loc_315B2C:                                     loc_16E5C:
    move.b  d1,$1A(a0)                              move.b  d1,$22(a0)
In Sonic 2, when loc_315B04 is reached, the d1 register is set to 1, -1, or 0 depending on whether Knuckles is moving up, moving down, or standing still. Assuming neither branch to loc_315B30 is taken, Knuckles' current mapping frame is added to the value in d1, and then two bound checks are made before writing the resulting value back into Knuckles' mapping frame: if the value is less than $B7, d1 is set to $BC, and if it's greater than $BC, d1 is set to $B7.

The gist of it is: while Knuckles is climbing up a wall, his mapping frame gets progressively incremented, but when he's climbing down, it gets decremented instead. And if the mapping frame ever steps outside of the $B7-$BC range, it gets wrapped around to the other end of the range, in order to loop the animation.

In Sonic & Knuckles though, a call to sub_F828 was introduced, causing the FindFloor function to be called whenever the player is holding neither up nor down on the directional pad. The FindFloor function calculates an object's distance to the floor directly below it, and stores the result in register... d1.

The inevitable result follows: when the player lets go of the directional pad, sub_F828 is called and the value in d1 gets overwritten with the distance between the center of the Knuckles object and the floor. Knuckles' current mapping frame is then added to this value, which always produces a value greater than $BC. This triggers the bounds check, resetting Knuckles' mapping frame back to $B7, the first frame of the climbing animation.

In other words, the anonymous commenter's analysis is 100% correct. Good work!



Number Two!

On the subject of triggering slope glitch in Ice Cap Zone by having Tails break an ice block while Sonic is standing on it, Brainulator9 asked whether Tails could get slope glitch by instead breaking the block as Sonic. In Sonic 3 & Knuckles, this is impossible because player 2's status bits always get set, regardless of who breaks the blocks, and regardless of whether the Tails object is even present in the player 2 slot.


However, as Brainulator9 pointed out, the same isn't true of standalone Sonic 3, in which Sonic can indeed break Tails' gravity. Below is the relevant code: Sonic 3 to the left, Sonic & Knuckles to the right, once again changes in bold.
loc_58B3C:                                      loc_8B384:
    move.b  ($FFFFB020).w,$3A(a0)                   move.b  ($FFFFB020).w,$3A(a0)
    move.b  ($FFFFB06A).w,$3B(a0)                   move.b  ($FFFFB06A).w,$3B(a0)
    moveq   #$23,d1                                 moveq   #$23,d1
    moveq   #$10,d2                                 moveq   #$10,d2
    moveq   #$10,d3                                 moveq   #$10,d3
    move.w  $10(a0),d4                              move.w  $10(a0),d4
    jsr     (SolidObjectFull).l                     jsr     (SolidObjectFull).l
    bsr.w   sub_58B62                               bsr.w   sub_8B3AA
    jmp     (Sprite_OnScreen_Test).l                jmp     (Sprite_OnScreen_Test).l

sub_58B62:                                      sub_8B3AA:
    move.b  $2A(a0),d0                              move.b  $2A(a0),d0
    btst    #3,d0                                   btst    #3,d0
    beq.s   loc_58B78                               beq.s   loc_8B3C0
    lea     (Player_1).w,a1                         lea     (Player_1).w,a1
    cmpi.b  #2,$3A(a0)                              cmpi.b  #2,$3A(a0)
    beq.s   loc_58B8A                               beq.s   loc_8B3D2

loc_58B78:                                      loc_8B3C0:
    btst    #4,d0                                   btst    #4,d0
    beq.s   locret_58BD0                            beq.s   locret_8B430
    lea     (Player_1).w,a2                         lea     (Player_2).w,a1
    cmpi.b  #2,$3B(a0)                              cmpi.b  #2,$3B(a0)
    bne.s   locret_58BD0                            bne.s   locret_8B430

loc_58B8A:                                      loc_8B3D2:
    bset    #2,$2A(a1)                              bset    #2,$2A(a1)
    move.b  #$E,$1E(a1)                             move.b  #$E,$1E(a1)
    move.b  #7,$1F(a1)                              move.b  #7,$1F(a1)
    move.b  #2,$20(a1)                              move.b  #2,$20(a1)
    move.w  #-$300,$1A(a1)                          move.w  #-$300,$1A(a1)
    bset    #1,$2A(a1)                              bset    #1,$2A(a1)
    bclr    #3,$2A(a1)                              bclr    #3,$2A(a1)
    move.b  #2,5(a1)                                move.b  #2,5(a1)
                                                    btst    #4,$2A(a0)
                                                    beq.s   loc_8B41A
                                                    lea     (Player_2).w,a1
                                                    bset    #1,$2A(a1)
                                                    bclr    #3,$2A(a1)

                                                loc_8B41A:
    lea     ChildObjDat_58C20(pc),a2                lea     ChildObjDat_8B480(pc),a2
    jsr     CreateChild1_Normal(pc)                 jsr     CreateChild1_Normal(pc)
    moveq   #$6E,d0                                 moveq   #$6E,d0
    jsr     (Play_Sound_2).l                        jsr     (Play_Sound_2).l
    jsr     (Go_Delete_Sprite).l                    jsr     (Go_Delete_Sprite).l

locret_58BD0:                                   locret_8B430:
    rts                                             rts
Both versions of the code call the SolidObjectFull function, and then check bits 3 and 4 of the status bitfield along with the animation of the corresponding player, which is previously backed up to offsets $3A and $3B, in order to determine whether the player landed on the object whilst in their rolling animation.

Note how thoroughly botched the checks for player 2 are in Sonic 3, though: player 1's RAM address is loaded instead of player 2's, and it gets loaded to register a2 rather than register a1. The only reason this code works at all is because the SolidObjectFull function itself sets a1 to player 2's RAM address during the course of its execution, and then exits without overwriting the contents of the register with something else:
SolidObjectFull:
    lea     (Player_1).w,a1
    moveq   #3,d6
    movem.l d1-d4,-(sp)
    bsr.s   sub_1BA2A
    movem.l (sp)+,d1-d4
    lea     (Player_2).w,a1
    tst.b   4(a1)
    bpl.w   locret_1BA6A
    addq.b  #1,d6

sub_1BA2A:
    ...
That isn't the problem in and of itself, however: the problem is that the code at loc_58B8A only runs for a single player, which leaves the other player hanging if they happened to also be standing on the ice block at the time. Rather than fix this properly, Sonic 3 & Knuckles simply forces player 2 to fall off the block either way, resulting in the strange, lopsided behavior where player 1 can get slope glitch but not player 2.



Number One!

Finally, regarding the Japanese characters in the slot machine bonus stage, another anonymous commenter points out that if you read them vertically, top to bottom, then left to right, they make up the first sixteen letters of the Iroha.


Now, what is the Iroha? It is an ancient Japanese poem, which has the unique characteristic of using every single kana character exactly once. (The title refers to the first three characters used in the poem.)

γ‚€γƒ­γƒγƒ‹γƒ›γƒ˜γƒˆ iro ha nihoheto
チγƒͺγƒŒγƒ«γƒ²   chirinuru wo
ワカヨタレソ  wa ka yo tare so
γƒ„γƒγƒŠγƒ©γƒ    tsune naramu
γ‚¦γƒ°γƒŽγ‚ͺγ‚―γƒ€γƒž uwi no okuyama
ケフコエテ   kefu koete
γ‚’γ‚΅γ‚­γƒ¦γƒ‘γƒŸγ‚· asaki yume mishi
ヱヒヒセス   wehi mo sesu

Since each character only appears once, the Iroha serves as an alternative to the usual gojΕ«on ordering, but both work equally well as placeholder graphics for a level's animated PLCs.



That's all I've got. Thank you all so much for the valuable feedback; I hope every single one of you has a terrific holiday season, and don't forget: