Monday, July 31, 2017

*Blocks your path*

Back in my first post about breakable walls, reader Telnaior was asking about Knuckles-only walls. There's actually no consistent way to mark a wall Knuckles-only; each level handles it differently in the object's init code, though it's always based on the object's subtype. Some levels, such as Mushroom Hill Zone, make it so every wall is Knuckles-only.
loc_21818:
    move.w  ($FFFFB018).w,$30(a0)
    move.w  ($FFFFB062).w,$32(a0)
    moveq   #0,d1
    move.b  7(a0),d1
    addi.w  #$B,d1
    ...
When a breakable wall is marked Knuckles-only, control is deferred to loc_21818, at first identical to loc_21568, which we've studied before. The interesting bits start at loc_21862, where we check whether the wall should break.
loc_21862:
    lea      (Player_1).w,a1
    move.w  $30(a0),d1
    move.w  d6,d0
    andi.w  #1,d0
    beq.s   loc_218B0
    cmpi.b  #2,$38(a1)
    bne.s   loc_218B0
    bclr    #5,$2A(a0)
    bsr.s   sub_218CE
    btst    #6,$2A(a0)
    beq.s   loc_2185C
    lea     (Player_2).w,a1
    cmpi.b  #2,$38(a1)
    bne.s   loc_2185C
    ...
Notice that everything but the Knuckles character ID check is gone, and that an identical check is now also performed on player 2's SST. Perhaps at one point, Knuckles-only walls were considered for Competition mode.

When the wall does break, an interesting piece of code runs over at loc_218EE. It does another character ID check for Knuckles (paranoid much?), and then checks if the double jump flag at $2F(a1) is set to 1.
loc_218EE:
    move.w  $18(a1),$1C(a1)
    bclr    #5,$2A(a1)
    cmpi.b  #2,$38(a1)
    bne.s   loc_21928
    cmpi.b  #1,$2F(a1)
    bne.s   loc_21928
    move.b  #2,$2F(a1)
    move.b  #$21,$20(a1)
    bclr    #0,$2A(a1)
    tst.w   $18(a1)
    bpl.s   loc_21928
    bset    #0,$2A(a1)

loc_21928:
    ...
If both of those check out, then it means the wall was broken by a gliding Knuckles, and the object forces Knuckles out of the gliding state by setting the double jump flag to 2 and Knuckles' animation to $21, which is the falling-from-gliding animation. This is interesting because regular breakable walls don't have this bit of code, so Knuckles will happily glide right through a bunch of them.


It also turns out the rocky walls from Angel Island Zone and Lava Reef Zone are actually a different object, which does contain this code, so the fact that it's missing from the standard breakable wall object is possibly an oversight.

Friday, July 28, 2017

The onus in "bonus" is on "us"

This has absolutely nothing to do with breakable walls, I was just reminded of it when I was flipping between the Sonic 1 and Sonic 3 pictures in my previous post.


This is the sprite mapping layout for the HUD in Sonic 1. If you look at the S in "RINGS", you can see how each letter is aligned to the right, leaving a single pixel-wide gap on the left for spacing purposes.


To make the font italic in Sonic 2, the bottom half of each letter got dragged one pixel to the left. As a result, the S now spans the entire width of an 8x8 tile, but it looks okay because there's still a pixel of spacing overall.


When they removed the italics for Sonic 3 though, rather than starting from the Sonic 1 graphics, it seems they instead "straightened out" the Sonic 2 font: observe the bottom right corner of the S. As a side effect, all the text is now aligned to the left. Why does this matter? Because it screwed up the spacing on the "BONUS" mappings.


The "BONU" bit of the text happens to span the entirety of the 32 pixel-wide sprite, and counts on the leading space in the S to make the spacing work out. The S, as it turns out, is reused from the HUD graphics. When they aligned all the text to the left, they broke this spacing, and they never moved the "BONU" mapping over to compensate.

Got that? Okay, now go watch this considerably more interesting video.

Thursday, July 27, 2017

Back whence you came, or possibly not

When you crash through the rocky walls in Angel Island Zone and Lava Reef Zone, they break off into nicely separated rock sprites which then fly off to whichever direction you punched them.


This makes a lot of sense, which is why I was quite surprised when I saw the Knuckles wall in Hydrocity Zone 2 do this:


I thought, surely, this must be a bug. The pieces are flying right back at you! That's when I realized that the simple 2x4 brick walls have had this behavior all the way back since Sonic 1.


It seems only the walls that use this classic mapping layout inherit the backwards-flying behavior. The thick columns in Marble Garden Zone also break off into square pieces, but they fly away from you. The ice walls in Icecap Zone break up into clean rock sprites like Angel Island and Lava Reef, but the pieces just fall off to either side.

There's no moral to this story, I'm just venting.

BONUS CONTENT because I took a pretty good screenshot: the breakable wall object is still present within Sonic 2's code, and has been refactored to work with the dual player system. However, no code to handle player 2 was actually added, so Tails can't break it.

The object is completely unused in the final game, but it can be seen in the Nick Arcade prototype's Green Hill Zone.


In the Sonic 2 mobile remake, they brought the object back for Hidden Palace Zone. Tails can now break it, of course.

Wednesday, July 26, 2017

One thing's for sure… you do exist

If you've picked anything up from my blogging style is that I like to first explain a topic in relative detail, and then follow it up with a practical example that illustrates what I'm talking about. Case in point, Icecap Zone 1:


When you break through one of these large walls as Knuckles, more often than not you'll come to a complete stop, and Knuckles will switch over to his pushing animation. I say "more often than not", because if your position happens to line up so that you're right next to the wall but not actually touching it, the wall will break before it kills your speed.

All of this happens because the breaking wall object in this stage does none of the things I mentioned before. This was fine in standalone Sonic 3, since the only thing that could break those walls was a sliding ice block.

Actually, here's a fun exercise: if you use debug mode to place one of those sliding blocks and then push it towards the walls guarding Knuckles' routes, you'll find that they'll break just fine. There's actually only one kind of breakable wall in this stage, and they prevent you from breaking into Knuckles' routes by simply not placing any ice blocks nearby.

Tuesday, July 25, 2017

I was never here

The breakable wall object, like most other solid objects in the game, checks for collision with Sonic and pals by calling the SolidObjectFull function. This function takes in a bunch of parameters through the d1-d4 registers, as well as the solid object in the a0 register and a player object in the a1 register.
loc_21568:
    move.w  ($FFFFB018).w,$30(a0)
    move.w  ($FFFFB062).w,$32(a0)
    moveq   #0,d1
    move.b  7(a0),d1
    addi.w  #$B,d1
    moveq   #0,d2
    move.b  6(a0),d2
    move.w  d2,d3
    addq.w  #1,d3
    move.w  $10(a0),d4
    jsr     (SolidObjectFull).l
    tst.b   $2C(a0)
    bpl.s   loc_215A4
    ...
After executing this function however, if either player was touching the object, then they get pushed outside the object's bounding box, and their speed is killed. This is obviously bad in the case where the player breaks down the wall, which is why prior to calling the function, the breakable wall object records the horizontal velocity for both players.

If you look at the first two lines of loc_21568 above, the RAM values at $B018 and $B062 are stored in offsets $30 and $32 of the breakable wall's SST. $B000 is the RAM address of player 1's SST, normally identified by the Player_1 label. An object's X velocity is kept at offset $18, so $B000 + $18 = player 1's X velocity at $B018. The same principle applies to player 2, whose SST is directly after player 1's at $B04A and is normally identified by the Player_2 label.
loc_215A4:
    swap    d6
    andi.w  #3,d6
    bne.s   loc_215B2

loc_215AC:
    jmp     (Sprite_OnScreen_Test).l
; ---------------------------------------------------------------------------

loc_215B2:
    lea     (Player_1).w,a1
    move.w  $30(a0),d1
    move.w  d6,d0
    andi.w  #1,d0
    beq.s   loc_2162A
    tst.b   (Super_Sonic_Knux_flag).w
    bne.s   loc_215F4
    ...
A bit later, the stored velocity is recalled into register d1, which I handwaved in my previous post. None of the following code actually runs unless one of two bits in register d6 are set, which get set by the SolidObjectFull function depending if the object was touched by player 1, player 2, or both.

Note that just checking bit 5 of $2A isn't enough: not only is it not set when the player is in mid-air, but it remains set as long the player is still touching the object, so if you stood with your back to a wall and spindashed away, it would break.
sub_2165A:
    move.w  d1,$18(a1)
    addq.w  #4,$10(a1)
    movea.l $34(a0),a4
    move.w  $10(a0),d0
    cmp.w   $10(a1),d0
    blo.s   loc_2167A
    subi.w  #8,$10(a1)
    movea.l $38(a0),a4
            
loc_2167A:  
    move.w  $18(a1),$1C(a1)
    bclr    #5,$2A(a1)
    move.l  #loc_21692,(a0)
    ...
Finally, if the wall did break, d1 is written back into the player's horizontal velocity at $18(a1), almost as if the wall had never been there in the first place. A slight tug is also given to the player's horizontal position at $10(a1), moving them four pixels towards the center of the wall. Bit 5 of $2A(a1) is also cleared in order to prevent the player from switching over to the pushing animation.

Monday, July 24, 2017

Special wall-breaking powers

The bit of code responsible for deciding whether or not a player should be able to destroy a breakable wall has quite a few peculiarities which make its behavior less than obvious, so I thought we could have a look at it and learn a couple of things along the way, along with help from our friend, the Sonic Retro wiki.

Here's the code:
    tst.b   (Super_Sonic_Knux_flag).w
    bne.s   loc_215F4
    cmpi.b  #2,$38(a1)
    beq.s   loc_215F4
    btst    #4,$2B(a1)
    bne.s   loc_215E0
    btst    #5,$2A(a0)
    beq.s   loc_2162A

loc_215E0:
    cmpi.b  #2,$20(a1)
    bne.s   loc_2162A
    move.w  d1,d0
    bpl.s   loc_215EE
    neg.w   d0

loc_215EE:
    cmpi.w  #$480,d0
    blo.s   loc_2162A

loc_215F4:
    bclr    #5,$2A(a0)
    bsr.s   sub_2165A
    ...
Alright, let's start from the top.
    tst.b   (Super_Sonic_Knux_flag).w
    bne.s   loc_215F4
Pretty straightforward. If you're Super Sonic or Super Knuckles (or the Hyper version of either character), then you can break the wall. Note that this leaves out Super Tails, as if he wasn't screwed over enough already.
    cmpi.b  #2,$38(a1)
    beq.s   loc_215F4
$38 is the character ID: Sonic is 0, Tails is 1, Knuckles is 2. So if you're playing as Knuckles, then you can break the wall. This effectively means the previous check is only relevant when playing as Sonic.
    btst    #4,$2B(a1)
    bne.s   loc_215E0
$2B is the secondary status bitfield, exclusive to the player objects. Sonic Retro helpfully chimes in:

BitHexDescription
4$10Fire shield flag.

So if you have a flame barrier, we skip over the next check. Which happens to be...
    btst    #5,$2A(a0)
    beq.s   loc_2162A
$2A is the primary status bitfield, universal to all objects. Note that we're checking the flags of the breakable wall in a0, not the player object loaded in a1. Retro says:

BitHexDescription
5$20Set if Sonic is pushing on this object.

This is a bit inaccurate. Bit 5 is only set if player 1 is pushing on the object while standing on something. Combined with the previous check, this means if you have a flame barrier, then you can break the wall even if in mid-air.
    cmpi.b  #2,$20(a1)
    bne.s   loc_2162A
$20 is the current animation ID, for objects that use the global animation system. In the case of the player objects, #2 corresponds to the spinning animation, used while either rolling or jumping. So if you aren't in the spinning animation, then you can't break the wall.
    move.w  d1,d0
    bpl.s   loc_215EE
    neg.w   d0

loc_215EE:
    cmpi.w  #$480,d0
    blo.s   loc_2162A
Finally, the player's horizontal velocity, which was previously stored in d1, is copied over to d0 and transformed into its absolute value. That value is then compared to $480, and if you're moving at a horizontal speed of less than 4.5 pixels, then you can't break the wall. This also applies to the flame barrier check, which is what allows Sonic to break through walls using his fireball dash move.

Notice anything fishy, though? Nowhere do we check whether the player has performed a double jump move or not, so if you have a flame barrier, just jumping at the wall with the requisite speed will work. In fact, there's no check for Sonic either, which means Tails can break down walls in the exact same way. This surely annoys players who try to avoid the first special ring in Angel Island 2 by jumping -- only to go through the wall due to their speed coming out of the tube.

Friday, July 21, 2017

All it takes is one bad apple

A couple of years ago I came across a strange bug involving the small animal object. I had no idea how it worked when I first posted the video, but later I figured out that if you quickly scroll a collapsing ledge off screen right as it's starting to break apart, the next enemy you destroy has a good chance of spawning an animal frozen in mid-air.


Here's the relevant code from the crumbling platform object:
loc_205A6:
    move.b  $2A(a0),d0
    andi.b  #$18,d0
    beq.s   loc_205B6
    move.b  #1,$3A(a0)

loc_205B6:
    moveq   #0,d1
    move.b  7(a0),d1
    movea.l $3C(a0),a2
    move.w  $10(a0),d4
    jsr     (SolidObjectTopSloped2).l
    bra.w   Sprite_OnScreen_Test
Okay, so. At loc_205A6, we're testing the status of two bits in the object's status bitfield at $2A(a0). These bits are set depending on whether player 1 and/or player 2 are standing on the object. If either player is on the platform, $3A(a0) is set to 1, which signals the platform to be destroyed and eventually results in the object switching over to this code:
    subq.b  #1,$38(a0)
    bne.s   locret_2061E
    move.l  #loc_20620,(a0)
    lea     (Player_1).w,a1
    moveq   #3,d6
    bsr.s   sub_205FC
    lea     (Player_2).w,a1
    moveq   #4,d6

sub_205FC:
    btst    d6,$2A(a0)
    beq.s   locret_2061E
    bclr    d6,$2A(a0)
    bclr    #3,$2A(a1)
    bclr    #5,$2A(a1)
    bset    #1,$2A(a1)
    move.b  #1,$21(a1)

locret_2061E:
    rts
Once per frame, $38(a0) is decremented from a starting value of 7. When it reaches 0, the object calls the sub_205FC function twice, once for each player. This function checks whether a given player is currently on the platform (by testing the status bit specified in d6) and if so, forces them off. Since the function's body begins right after the code which calls it, the second bsr.s instruction is elided and the code falls directly into the function.

Note that in the case where $38(a0) isn't zero, we haven't actually run the code that makes the platform solid, as we do at loc_205B6. We can't do it at the end of this block due to the aforementioned fallthrough, but we have to do it before locret_2061E, and we can't do it between the decrement and the branch, since it would mess with the latter's result. So they ended up doing it in the absolute worst possible place:
loc_205A6:
    move.b  $2A(a0),d0
    andi.b  #$18,d0
    beq.s   sub_205B6
    move.b  #1,$3A(a0)

sub_205B6:
    moveq   #0,d1
    move.b  7(a0),d1
    movea.l $3C(a0),a2
    move.w  $10(a0),d4
    jsr     (SolidObjectTopSloped2).l
    bra.w   Sprite_OnScreen_Test
; ---------------------------------------------------------------------------
    ...

loc_205DE:
    bsr.w   sub_205B6
    subq.b  #1,$38(a0)
    bne.s   locret_2061E
    ...
Oh dear. There are now 8 frames in which if the platform is scrolled away, sub_205B6 will call Sprite_OnScreen_Test, which will then call Delete_Current_Sprite, which will set every byte on the object's SST to 0. And then, like some sort of shambling corpse, the object will execute the rest of loc_205DE, decrementing $38(a0) once before exiting.

We now have a free slot in which byte $38 is not zero, ready to screw over whatever object happens to spawn there.
    tst.b   $38(a0)
    bne.s   loc_2C9CA
    jsr     (Create_New_Sprite).l
    bne.s   loc_2C9C4
    move.l  #Obj_EnemyScore,(a1)
    move.w  $10(a0),$10(a1)
    move.w  $14(a0),$14(a1)
    move.w  $3E(a0),d0
    lsr.w   #1,d0
    move.b  d0,$22(a1)

loc_2C9C4:
    jmp     (Draw_Sprite).l
; ---------------------------------------------------------------------------

loc_2C9CA:
    move.b  #$1C,5(a0)
    clr.w   $18(a0)
    jmp     (Draw_Sprite).l
Through spectacular coincidence, the init code for the animal object checks the value of $38(a0), and if it isn't zero, the routine byte is set to $1C, and as can be seen in the video above, no score popup object is created.

But what is routine $1C? And under which circumstances is byte $38 set prior to the object's own initialization?
loc_2CB02:
    tst.b   4(a0)
    bpl.w   loc_2C9DA
    subq.w  #1,$36(a0)
    bne.w   loc_2CB1E
    move.b  #2,5(a0)
    move.w  #$80,8(a0)

loc_2CB1E:
    jmp     (Draw_Sprite).l
Turns out, routine $1C is actually leftover code from Sonic 2. In Sonic 2, animals inside end capsules are spawned with $38(a0) set to 1, and $36(a0) set in increments of 8. This causes them to all enter loc_2CB02 and begin decrementing $36(a0), and as they finish, they return to their normal routines and hop away one by one.

Unfortunately, our animal spawned with $36(a0) already set to 0, which after decrementing becomes $FFFF, or 65,535. In other words, it will wait 65,535 frames before hopping away. That's over 18 minutes. There's no saving him.

So please make sure you don't do anything after calling Delete_Current_Sprite. Think of the animals.

Thursday, July 20, 2017

Hide and seek

You might have noticed that, if you bump your head on the ceiling and fail to collect it the first time around, this Special Stage ring halfway through Angel Island Zone 1 likes to disappear until you land back on the floor proper.


While they're on screen, Special Stage rings DMA their graphics over the area normally taken up by enemy explosions. The explosion graphics aren't reloaded until the ring is deleted, which is probably why unlike almost every other object, the ring deletes itself when you go too far away from it vertically as well as horizontally.

I say almost, because this behavior isn't unknown. Throwaway object such as enemy projectiles, or crumbling platform debris all exhibit it, using some variation on the Sprite_CheckDeleteXY function shown below. Special Stage rings are particularly interesting though, because they're meant to respawn once you scroll them back into range.
Sprite_CheckDeleteXY:
    move.w  $10(a0),d0
    andi.w  #-$80,d0
    sub.w   (Camera_X_pos_coarse_back).w,d0
    cmpi.w  #$280,d0
    bhi.w   Go_Delete_Sprite
    move.w  $14(a0),d0
    sub.w   (Camera_Y_pos).w,d0
    addi.w  #$80,d0
    cmpi.w  #$200,d0
    bhi.w   Go_Delete_Sprite
    jmp     (Draw_Sprite).l
Once per frame, the object manager calculates the Camera_X_pos_coarse_back RAM variable to define the range of objects which is currently active. It's basically the live camera position rounded down to the nearest 128 pixel boundary, as seen below. Each time the camera moves 128 pixels horizontally, the range slides over and new objects are loaded.
loc_1B7F2:
    move.w  (Camera_Y_pos).w,d1
    subi.w  #$80,d1
    andi.w  #$FF80,d1
    move.w  d1,(Camera_Y_pos_coarse_back).w
    move.w  (Camera_X_pos).w,d1
    subi.w  #$80,d1
    andi.w  #$FF80,d1
    move.w  d1,(Camera_X_pos_coarse_back).w
Already we can see a discrepancy, though. The object manager maintains coarse values for both the camera's X and Y positions, but the Sprite_CheckDeleteXY function above uses the live value for the Y position. The result is that unless it's vertically aligned with a 128 pixel boundary, there's a mismatch between the range at which the object deletes itself, and the range at which it is respawned by the object manager.

Which is exactly what afflicts our buddy up there.

       Object deleted at this camera position       Object respawned at this camera position

Wednesday, July 19, 2017

Bop it! Twist it! Pull it! Shake it!

When you encounter the Marble Garden Zone 1 miniboss near the start of the level, if you quickly run away as soon as it begins burrowing into the ceiling, you'll get caught in a permanent earthquake until something else causes the screen to stop shaking.


The reason for this should be clear, given what we've learned. The miniboss object has two distinct code paths through which it deletes itself: one for when it's scrolled off-screen and needs to be respawned, and another for when it finishes burrowing into the ceiling and is gone forever.

Unfortunately, both code paths are active at the same time, and the earthquake flag is only cleared when the latter path completes, so if you scroll the object off-screen after the quake starts but before it stops digging, it deletes itself without clearing the flag, which results in endless shaking.

You can confirm this hypothesis by going back to the spot where you encountered the miniboss: the object will respawn because it deleted itself through the on-screen test. If you let it finish digging, then it will no longer appear.

Tuesday, July 18, 2017

Door Into Bummer

Not far into act 1 of Lava Reef Zone, we come across this simple obstacle: a wall-mounted cannon over a closed door, shooting an endless barrage of teeny tiny pellets. Once the thing is put out of its misery, the door slides open, revealing the path to the rest of the stage.


When the cannon object is punched in the face, it does a couple of important things. First, it sets one of the flags in the level trigger array, based on the four lowest bits of its subtype. The trigger array is a set of general-purpose, temporary flags which get cleared whenever a level loads. In particular, the flag set by the cannon is under the watchful eye of the nearby door object. As soon as the flag is set, the door wakes up and does its thing.
loc_42E84:
    ...
    move.b  $2C(a0),d0
    andi.w  #$F,d0
    lea     (Level_trigger_array).w,a3
    lea     (a3,d0.w),a3
    moveq   #0,d3
    ...

sub_42EC0:
    ...
    bset    d3,(a3)
    move.l  #Obj_Explosion,(a0)
    move.b  #2,5(a0)
    ...
The second thing the cannon does is overwrite its own code pointer with the address for the enemy explosion object. It also sets the routine byte to 2 in order to skip over the initial bit which spawns a small animal along with a score popup. The key takeaway though, is that once the explosion object does its thing, it calls Delete_Current_Sprite, which deletes the object and ensures it doesn't spawn again lest the player wander off and then come back.

So far everything sounds good. But there's a wrinkle to this plan. Remember how I said the level trigger array is cleared whenever a level loads? Well, there's a star post right before this area. If you destroy the cannon, then hit the star post with enough rings to enter a bonus stage, and then come back...


Congratulations, you have screwed yourself over.

When you enter a bonus stage, the object respawn table for the current stage is not cleared. This is what prevents you from going back and tagging all the rings and enemies again. However, the level trigger array is cleared, putting you in a situation where the door is very much closed, but the only object that can do anything about it has been permanently removed from the map. The only way out of this mess is to lose a life or reload your save file.

Monday, July 17, 2017

Maybe you'll stop feeling so bad

When an object is no longer useful, it can delete itself from object RAM by calling the Delete_Current_Sprite function. This function zeroes out all 74 bytes of the object's SST, preparing the slot for the next object that'll take its place.
Delete_Current_Sprite:
    movea.l a0,a1
    moveq   #$11,d0
    moveq   #0,d1

loc_1ABBC:
    move.l  d1,(a1)+
    dbf     d0,loc_1ABBC
    move.w  d1,(a1)+
    rts
Note that the object processor only considers the code pointer when deciding whether a slot is empty or not; wiping the rest of the SST is the kind of convenience one might expect from a modern, memory-managed programming language, rather than assembly code written in the early 90s.

Objects which have scrolled far away are discarded through a similar process. Unlike in a managed environment, there is no garbage-collecting entity which erases objects when they are no longer active. Instead, each object is responsible for freeing its own memory by calling a function like Sprite_OnScreen_Test below:
Sprite_OnScreen_Test:
    move.w  $10(a0),d0
    andi.w  #$FF80,d0
    sub.w   (Camera_X_pos_coarse_back).w,d0
    cmpi.w  #$280,d0
    bhi.w   loc_1B5A0
    bra.w   Draw_Sprite
; ---------------------------------------------------------------------------

loc_1B5A0:
    move.w  respawn_addr(a0),d0
    beq.s   loc_1B5AC
    movea.w d0,a2
    bclr    #7,(a2)

loc_1B5AC:
    bra.w   Delete_Current_Sprite
Much like the code which loads objects from a level's object layout, what this function does is check whether the object is still within the same vertical slice of the level as the player (or more specifically, the camera), and if not, then it jumps to Delete_Current_Sprite, which in turn clears the object's SST.

An extra detail about this function is that it loads the value of respawn_addr(a0), offset $48 in the object's SST, into an address register and clears bit 7 of that address before deleting the object. What's that all about?

Separate from object RAM, the game maintains an "object respawn table" which consists of one byte for each object in the level's object layout. Every time an object is loaded from the layout, the object manager sets bit 7 of its entry in the respawn table, and then writes the entry's address to offset $48 in the object's SST.

It is the object's responsibility to once again clear bit 7 before deleting itself. If the object manager loads an object from the layout and bit 7 of its respawn entry is already set, then it will be considered to have been permanently deleted and will not spawn again. This is why Sprite_OnScreen_Test clears the bit and Delete_Current_Sprite does not: the former temporarily removes the object when it's too far away, whereas the latter is called at the end of an object's life, such as when an enemy is destroyed.

Finally, the remaining bits in a respawn table entry can be used however the object sees fit. For instance, monitors use bit 0 as a makeshift respawn flag, while keeping bit 7 clear at all times. This causes them to always respawn, and then their init code makes them switch over to a broken appearance if bit 0 is set.

Friday, July 14, 2017

S monitor blues

Brainulator9 commented on my previous post regarding S monitors:
Does this also work with Tails and Knuckles? In fact, how come Knuckles doesn't shockwave when using an S monitor.
It can also happens with Tails because the monitor loads the super birds object, and like the hyper stars object, the first thing it does is load the super bird art which, you guessed it, is in KosM format.
Obj_HyperTails_Birds:
    lea     (ArtKosM_SuperTailsBirds).l,a1
    move.w  #$D000,d2
    jsr     (Queue_Kos_Module).l
    ...
Knuckles doesn't make shockwaves when you turn Hyper from an S box because he's not really Hyper, he's just Super. Try jumping in water: he'll still blow bubbles and drown if you leave him there long enough. But how can that be, he has the afterimages and everything!

Turns out the S monitor has its own super transformation code, cobbled up from the code that runs when you transform normally. It is also hastily edited from the original Sonic 3 monitor code, which only turned you Super. And to show how slapped together the code is, here it is in its unabridged form. Relevant bits highlighted by me.
Monitor_Give_SuperSonic:
    addq.w  #1,(a2)
    addi.w  #50,(Ring_count).w
    move.b  #1,(Super_Hyper_palette_status).w
    move.b  #$F,(Palette_timer).w
    move.b  #1,(Super_Sonic_Knux_flag).w
    move.w  #60,(Super_Hyper_frame_count).w
    move.w  #$800,(Sonic_Knux_top_speed).w
    move.w  #$18,(Sonic_Knux_acceleration).w
    move.w  #$C0,(Sonic_Knux_deceleration).w
    move.b  #$1F,(Player_1+anim).w
    cmpi.w  #2,(Player_mode).w
    bne.s   loc_1DACE
    move.b  #0,(Super_Sonic_Knux_flag).w
    move.b  #1,(Super_Tails_flag).w
    move.b  #$29,(Player_1+anim).w
    move.w  #$800,(Tails_top_speed).w
    move.w  #$18,(Tails_acceleration).w
    move.w  #$C0,(Tails_deceleration).w
    move.l  #Obj_HyperTails_Birds,(Hyper_Sonic_stars).w
    bra.s   loc_1DB0A
; ---------------------------------------------------------------------------

loc_1DACE:
    bhs.s   loc_1DB02
    move.l  #Map_SuperSonic,(Player_1+mappings).w
    move.b  #-1,(Super_Sonic_Knux_flag).w
    move.w  #$A00,(Sonic_Knux_top_speed).w
    move.w  #$30,(Sonic_Knux_acceleration).w
    move.w  #$100,(Sonic_Knux_deceleration).w
    move.l  #Obj_HyperSonic_Stars,(Hyper_Sonic_stars).w
    move.l  #Obj_HyperSonicKnux_Trail,(Super_stars).w
    bra.s   loc_1DB0A
; ---------------------------------------------------------------------------

loc_1DB02:
    move.l  #Obj_HyperSonicKnux_Trail,(Super_stars).w

loc_1DB0A:
    move.b  #$81,(Player_1+object_control).w
    move.b  #0,(Player_1+invincibility_timer).w
    bset    #Status_Invincible,status_secondary(a1)
    moveq   #$9F,d0
    jsr     (Play_Sound_2).l
    moveq   #$2C,d0
    jmp     (Play_Sound).l
First, notice the Super Sonic flag is set to 1, signaling a super transformation, which is leftover code from Sonic 3. After it sets up Knuckles' speed values for some reason, notice Tails' code sets the Super Sonic flag back to 0, and then sets the actual Super Tails flag to 1. It also loads up the super birds object, and leaves Knuckles' values lying around for the non-existent second player.

Sonic's code then sets the Super Sonic flag to -1 signaling a hyper transformation, and proceeds to overwrite Knuckles' speed values with Sonic's. It also replaces the super birds with the hyper stars object, and then loads up the afterimage object to a separate slot.

Knuckles' code only sets up the afterimage object, inheriting everything else from the code at the very top. This means he does eventually receive the correct speed values, but notice the Super Sonic flag was never changed from 1, which causes him to turn Super as in Sonic 3, while still keeping his afterimages.

In the developers' defense, it's not immediately apparent that anything is wrong. After all, the question was why Hyper Knuckles doesn't make shockwaves, not why Super Knuckles has afterimages. It would be far easier to notice a yellow Hyper Sonic, or Super Tails without any birds.

Finally, the S monitor is just a cheat. It's okay for it to be buggy and cheaply made. Copy-pasting code is easy and fast, whereas wiring it to the game's core routines would've been needlessly risky and time-consuming.

And hey, they did fix the whistling sound.

Thursday, July 13, 2017

Tree Top Trickery

The boss fight in Mushroom Hill Zone 2 is home to a yet another pseudo-3D effect, where the player and the boss race through a series of three-dimensional archs, composed of two pillars, one in the background and one in the foreground, connected by a thin plank which scrolls across the screen with semi-realistic perspective.


Much like the ice sheets in Icecap Zone 2's background, the plank is drawn with a skewed perspective in advance, but ironically you can never see the undistorted graphic due to the foreground pillar overlapping it.


Here's how everything comes into place: first, the normal level backdrop is drawn on the low background planes. Then the background pillar is rendered through a bunch of low priority sprites stacked vertically on top of each other.


In order for it to be deformed, the plank must be rendered through a background plane, and in order for it to overlap the sprite-based background pillar, it must be drawn on the high plane along with the foreground layer.

But wait! We've run out of background planes to draw the overlapping foreground pillar with! That bit is covered up by a couple of high priority sprites, much like the rounded corners at the bottom, because it turns out the grass and the pillar actually use two different palettes.


Rendering the plank on the same background plane as the pillar has its drawbacks. Given the angle, you would expect the characters to appear behind the pillar but in front of the plank, so while the rest of the boss has low priority, the top part is actually made out of high priority sprites.

There are also collision change objects looming over the entire field, setting the player's priority whenever they venture into the top portion of the screen. Even that doesn't cover every scenario, though.

Wednesday, July 12, 2017

The 2.5D waterline

Icecap Zone 2 also has a pseudo-3D effect going on in its background waterline. There's no vertical scrolling this time around, but on the other hand, the ice sheets floating on the water's surface display some realistic perspective as they scroll horizontally across the screen.


Once again, the parallax effect is achieved by scrolling each row of pixels faster than the one preceding it, but this time there's no DMA trickery involved. Turns out, the background itself was drawn skewed in advance, so by the time one of the four sets of ice sheets scrolls offscreen, the remaining three have deformed exactly the right amount in order for the animation to loop seamlessly.

Tuesday, July 11, 2017

The pseudo-3D waterline

Reader Spinner asked:
Could you please explain at some point how the water effect in the backgrounds of Hidrocity 1 and Launch Base 2 is accomplished? You know, that pseudo-3d effect reminiscent of mode 7.
It's a combination of two effects. Opening up Hydrocity Zone 1 in SonLVL shows us two strips of placeholder patterns along the background, hinting this is yet another DMA-based animation. This one animates when the camera moves vertically rather than horizontally, though.


Within the graphics for Hydrocity Zone 1 are the following four patterns. These are DMA'd over the above placeholders depending on the camera's position.


When the camera is above the waterline, the top placeholder is filled entirely with the first pattern, while the bottom one is initially filled with the second pattern. As the camera descends, the second pattern is reapplied, but the game begins progressively skipping over rows of pixels as the view approaches the water.

As a result of skipping over rows, the height taken up by the second pattern starts decreasing. When the bottom end of the pattern is reached, the game fills any remaining space with rows from the fourth pattern.


Once the camera crosses over the waterline, the same process occurs, except in reverse. Now the bottom half is filled entirely with the fourth pattern, and as the camera continues to descend, the top half is replaced by rows of pixels from the third pattern. Any remaining space is filled with rows from the first pattern.


The final touch, which really brings it together, is a carefully aligned parallax scroll. Areas of the background filled with the first and fourth patterns all scroll horizontally at the same rate, but the rows of pixels between them scroll faster the closer they are to the waterline, giving it a meaty 3D effect.