Showing posts with label Hydrocity Zone. Show all posts
Showing posts with label Hydrocity Zone. Show all posts

Monday, January 29, 2018

Drowning during bosses

An anonymous reader asks:
I don't think you've talked about it yet so I'd like to ask a few things related to the Hydrocity Act 1 boss in S3A.
Sure, go ahead.
Why does it use the Act 2 boss BGM instead?
loc_47DBA:                                      loc_69F64:
    move.l  #Obj_HCZ_MinibossLoop,(a0)              move.l  #Obj_HCZ_MinibossLoop,(a0)
    moveq   #$19,d0                                 moveq   #$2E,d0
    jsr     (Play_Sound).l                          jsr     (Play_Sound).l
                                                    move.b  #$2E,($FFFFFF91).w

locret_47DC8:                                   locret_69F78:
    rts                                             rts
Because for some reason, in S3A the act 1 boss requests the act 2 boss music.
Why, if you let the air countdown start and then get out of the water, does the S&K Act 1 boss BGM start to play? It's seems to be the only time where track 18 (I think it's 18) is used. (Apparently this glitch also happens in Act 2 but I haven't tried)
Because, to put it mildly, the code that's responsible for resuming a level's music after having previously switched to the drowning theme is a brittle piece of crap:
Player_ResetAirTimer:
    cmpi.b  #$C,$2C(a1)
    bhi.s   loc_1744C
    cmpa.w  #Player_1,a1
    bne.s   loc_1744C
    move.w  (Level_music).w,d0
    btst    #1,$2B(a1)
    beq.s   loc_17430
    move.w  #$2C,d0

loc_17430:
    tst.b   (Super_Sonic_Knux_flag).w
    beq.w   loc_1743C
    move.w  #$2C,d0

loc_1743C:
    tst.b   (Boss_flag).w
    beq.s   loc_17446
    move.w  #$18,d0

loc_17446:
    jsr     (Play_Sound).l

loc_1744C:
    move.b  #$1E,$2C(a1)
    rts
Let's go over what this code does. First, the current level's music is loaded from the Level_music RAM variable, which as we saw before, is pulled from the master playlist, based on the current zone and act. This value is then stored in the d0 register, from which it will eventually be read by the Play_Sound function called at loc_17446.

Before it can reach loc_17446 though, the value must survive a gauntlet, which begins by checking bit 1 of the player's status_secondary bitfield, which is set when the player is invincible. If the player is currently invincible, the contents of the d0 register are replaced with the value $2C, which is the ID for the invincibility music track.

Next, if the Super_Sonic_Knux_flag is set, the value of the d0 register is once again replaced with $2C, the ID for the invincibility music track. Note that this check is redundant: the player is always invincible when in their Super form.

Finally, the Boss_flag is checked. If the player is currently fighting a boss, then the game should continue playing boss music, and therefore the d0 register is overwritten with the value $18, which as the commenter duly notes, is the theme used by act 1 bosses in Sonic & Knuckles. No effort is made to pick specific themes for each boss, despite the fact that there are only two bosses in the entire game where the player can trigger the countdown music.

This is annoying for several reasons. In Sonic 3, both bosses play the act 2 boss theme, so they could've just made the code set d0 to $19 and be done with it: at least it would always resume the right track! Meanwhile, as we can see from the code near the start of this post, Sonic & Knuckles actually saves the current boss track to RAM address $FF91, but nothing in the game ever makes use of this value.

Why complicate matters, though? Just check the current act and use that information to pick one song or the other.
It's possible to drown during the score tally but in S3&K this was apparently fixed. Why does it happen and how it was fixed?
The question presupposes a falsehood: the only way to drown during the level results is by running out of air just as the results object slides into view, which works equally well in both Sonic 3 and Sonic & Knuckles.


In practice, however, the level results object prevents this by restoring both player objects' air reserves (at offset $2C of their SSTs) right as the stage clear theme begins playing:
Obj_LevelResultsWait:
    tst.w   $2E(a0)
    beq.s   loc_2CF4C
    subq.w  #1,$2E(a0)
    cmpi.w  #$121,$2E(a0)
    bne.s   locret_2CF8E                        ; Play after eh, a second or so
    move.b  #$1E,($FFFFB02C).w                  ; Reset air for Hydrocity
    move.b  #$1E,($FFFFB076).w
    moveq   #$29,d0
    jmp     (Play_Sound).l                      ; Play level complete theme
The title card object then repeats the process, ensuring the player always begins the second act with a full set of lungs.
Thanks in advance.
Thank you for the questions!

Monday, January 15, 2018

No fun allowed

In S3A only, if you defeat the Blastoid enemies that are placed directly over the special stage rings in Hydrocity Zone 1, you can actually fall through a hole in the floor directly into the ring's secret room.


For whatever reason, in Sonic 3 & Knuckles, these holes were hastily patched up with invisible collision objects, and as a result, Knuckles can't glide-land or climb up on this ledge properly.


Then again, in S3A you can defeat Blastoids by just walking into them, likely because they don't use the regular enemy collision type. So uh, pick your poison, I guess.

Wednesday, January 3, 2018

His and hers act 2 capsules

One of S3A's little idiosyncrasies is that, unlike the ones in which it descends from the top of the screen, any levels that feature stationary act 2 end capsules have them placed directly into the object layout, instead of having the boss object spawn them at the end of the fight.

In S&K, this was changed so that stationary capsules are also spawned by the boss, which unfortunately led to this bug where the capsule in Hydrocity Zone 2 is not properly aligned with the screen lock:


Why the developers changed this is a mystery; perhaps they were trying to remove any possibility that the player might somehow miss the screen lock and proceed to the next stage without fighting the boss, as can be seen here.

Surprisingly, such a minor detail can actually give us insight on an unrelated aspect of the game's development. It turns out, the object layout for Flying Battery Zone 2 contains one such stationary capsule, at coordinates $3210, $660.
FBZ2_Sprites:   dc.w   $80,   $80,     0, $3130, $86D8, $B400, $3210, $8660, $C600
                dc.w $FFFF,     0,     0
The reason this is interesting is because the S3A placement of the capsule is a stone's throw away from its final resting place in Sonic & Knuckles. This suggests that by the time it was removed from S3A, the level layout for Flying Battery 2 was already quite similar to its final layout in Sonic & Knuckles.


Astute readers will notice the second object present in the layout placed slightly to the left of the capsule at coordinates $3130, $6D8. Foreshadowing: the sign of a quality blog.

Friday, December 22, 2017

That's cheating, but I'll let it slide

Reader Silver Sonic 1992 asks:
A bit unrelated, but is there a reason there is a duplicate of the Sonic's falling animation?
First, a little background. All three characters have three distinct animations that make use of their falling sprites. These animations take up IDs $19 through $1B. Here are the definitions for Sonic's animations:
byte_12C0A:     dc.b    9, $D7, $D8, $FF

byte_12C0E:     dc.b  $40, $8D, $FF

byte_12C11:     dc.b    9, $8C, $8D, $FF
The second one, $1A, is the animation which plays when falling back from taking damage, and consists of only a single frame. The third one, $1B, is used at the start of a few levels, and alternates between the damage frame and a second one to show Sonic flailing his arms as he falls into the stage.

Now the third animation, $19, is used while sliding on stuff like the icy slopes in Icecap Zone 1, and the sand streams in Sandopolis Zone. As it turns out, if the regular falling animation is used for these obstacles, Sonic's sprite actually floats a fair bit off the floor:


The developers' solution was to duplicate the mapping frames used by the falling animation, shift them diagonally down towards the slope, make a copy of the falling anim that points to those frames, and have slides use that anim instead:


Not all slides, however. If the water slides in Hydrocity Zone 2 used the proper sliding anim, you wouldn't be able to see much of your character due to the high walls of the waterway:


In this instance, the developers cheated and made the water slide call the falling anim instead, which means that Sonic is technically hovering in mid-air again.


And that's why there's a duplicate of Sonic's falling animation. Unless you were talking about mapping frames $D0 and $D1. I think those are just unused.

Monday, December 11, 2017

Sonic Eraser

Following up on the topic of Hydrocity Zone's water jets, there's something that's always bugged me. Just before you're flung up in the air by a vertical jet, it sends you through one of these: a translucent white tube that crosses over another path through the stage.


These things are level chunks, and they're usually flanked by collision change objects that set Sonic's priority flag. This is because the level blocks themselves have their priority flag set too, presumably so they overlap Sonic as he rides up the tube. Using debug mode to avoid the collision change object shows how this would look.


However, for some unfathomable reason, once the vertical water jet object spawns, it sets both Sonic and Tails' current animation to $1C:
    move.b  #$1C,($FFFFB020).w
    move.b  #$1C,($FFFFB06A).w
    move.l  #loc_30338,(a0)
So what does animation $1C do? It simply displays mapping frame 0 for 120 frames, or two seconds. Mapping frame 0 is a blank frame which is usually reserved for when the player is blinking from having taken damage.
byte_12C15:     dc.b  $77,   0, $FF
In other words, right before Sonic goes through the tube, the water jet makes him invisible for two seconds, completely destroying the point of making the tube translucent and giving it high priority.


I have no explanation for this; the event appears to play out fine if you remove the offending lines of code. Maybe there was a bug here that was hastily covered up, before being fixed properly?

Friday, December 8, 2017

Hydrocity Zone intro area: the water jet

Finally, we arrive at the fourth and final element of the Hydrocity Zone intro sequence: the large jet of water that propels you out of the flooded tunnel.


The water jet is an object, and it's actually placed directly within the level's object layout. There are two subtypes of the jet object, one vertical and the other horizontal, and both can be manually placed using debug mode.


Now, it doesn't look like much until you place it because it loads its own art when it spawns, overwriting the graphics for the Turbo Spiker enemy. And although the object deletes itself and reloads the enemy art the instant it goes off-screen, there's still a known edge case in which Super Sonic can accelerate fast enough to bring a couple of Turbo Spikers into view before their art has finished decompressing.


As you play around with debug mode, you may notice that sometimes, you can't seem to place any horizontal jets. This is because the init code for the horizontal jet contains a clause which deletes the object if player 1's Y coordinate is less than $500; apparently a hack to prevent the jet from spawning when you take the top path out of the intro area.

However, this creates an oversight. If like before we take the long way around to the back of the tunnel, but avoid going near the object until we're below the $500 mark, then the water jet will shoot out for no reason as we climb up the wall.


Note how when we do this, the rotating palette remains in its slow setting. This is because the horizontal jet counts on it having been sped up by the tunnel background object, so its only responsibility it to slow the palette back down.

Which leads us to our final oversight. Possibly to avoid shooting out for no reason, when the horizontal jet deletes itself, it doesn't set its flag in the object respawn table. As a result, the jet will shoot out once and then never reappear, even if we take the long way around back to the intro area and use the tunnel exit again.


Now, this is hardly an oversight in and of itself, since it's pretty obvious that you were never meant to return to the intro area. The oversight is that since the water jet doesn't respawn, there's nobody around to slow the rotating palette back down, which means all the waterfalls are once again animating at a ridiculous speed.


The only way out of this mess is to trigger one of the vertical water jets, which respawn every single time.

Thursday, December 7, 2017

Hydrocity Zone intro area: the tunnel

Moving past the door and the button, we reach the third element of the Hydrocity Zone intro sequence: the tunnel. Back when I talked about the background brick pattern, I mentioned how the palette trick was necessary to display the bricks both wet and dry, side-by-side.


Once water floods the tunnel, however, the dry colors peel off and reveal the wet colors underneath. How exactly is this effect accomplished? The answer is sprites! Lots and lots of sprites! The tunnel background is an object!


Remember how I said the door object is the mastermind, and implied that it's part of the stage's object layout? I lied. It's actually the tunnel background object which is placed in the layout, and it spawns the door as a helper object, with both objects monitoring the level trigger array.

The fact that it's an object brings us to our first oversight: the object is created with an absurdly low priority value, which causes it to be rendered in front of nearly all other sprites. This is probably so the trailing wave will cover objects placed in the tunnel, but has the unfortunate side effect of also obscuring the objects with the background brick pattern.


Next, let's talk about mapping frames. When the tunnel object spawns, it uses the third frame seen above, switching to the fourth frame when the level trigger array is first set, before looping between the first and second frames.


In order to reduce the total sprite count, the trailing wave is actually baked into the brick pattern, which tiles horizontally every 32 pixels. Then, to minimize VRAM usage, only two variations of the wave graphics are included, 16 pixels apart. As a result, the wave can only lurch forward in increments of 16 pixels, but by switching back to the first mapping frame and simultaneously shifting the object by 32 pixels, the wave can move forward indefinitely using those two frames.

In practice, the tunnel object only animates every other frame, so the wave's movement is limited to only 30 frames per second. However, thanks to the button object, the level's rotating palette is currently animating at a blistering 60 frames per second, which helps mask the jittery movement.


Oh, but it's not over yet. The tunnel object has a shadow baked into its brick pattern, but at one point, the actual tunnel extends upward, which changes the height at which the shadow should be displayed!

The solution is to shift the tunnel object upward once it reaches a certain position, and then pull it back horizontally a bit to account for the diagonal slant of the trailing wave. And if that sounds at all plausible, then get this: the brick pattern is designed in a way that doing so maintains the relative position of every brick.


Unfortunately, this achievement is marred by a stupid oversight. Notice how in the screenshots above, the trailing wave is in the same exact position, but the brick pattern has moved. When the tunnel object gets pulled back, it's pulled back too far, causing the wave to stay in the same spot for four consecutive frames, and throwing the animation out of phase with the movement.

Once this happens, the tunnel object's brick pattern becomes irreparably misaligned with the actual tunnel background.

Wednesday, December 6, 2017

Hydrocity Zone intro area: the door

Our attention now turns to the two objects placed in the intro pool: the door and the button. The door is the mastermind here; all the button does is set one of the flags in the level trigger array, which in turn is monitored by the door object.


Before we get into detail regarding the door, however, I need to introduce something else entirely. Water tunnels are a level event exclusive to Hydrocity Zone: when players step into their area of effect, water tunnels override their X and Y velocity with preset values, although the player is still able to influence their own movement across one of the axes.
HCZ1_WaterTunLocs: dc.w    $E
        ; Min X, Min Y, Max X, Max Y, X Vel, Y Vel, Player can influence which axis (Clear = Y)
    dc.w   $380,  $580,  $5A0,  $5C0,  $3F0, $FFE0,     0
    dc.w   $5A0,  $560,  $A80,  $5C0,  $3F0, $FFF0,     0
    dc.w  $1400,  $A80, $15A0,  $AC0,  $400,     0,     0
    dc.w  $15A0,  $A40, $1960,  $AC0,  $400, $FFC0,     0
    dc.w  $1990,  $780, $19E0,  $7F0,     0, $FC00,  $100
    dc.w  $1990,  $7F0, $19F0,  $878, $FEC0, $FC00,  $100
    dc.w  $1990,  $878, $19F0,  $8FD,  $140, $FC00,  $100
    dc.w  $1990,  $8FD, $19F0,  $978, $FEC0, $FC00,  $100
    dc.w  $1990,  $978, $19F0,  $A10,  $100, $FC00,  $100
    dc.w  $1960,  $A10, $19D0,  $A80,  $300, $FD80,  $100
    dc.w  $2B00,  $800, $2C20,  $840,  $400,     0,     0
    dc.w  $2C20,  $7C0, $2EE0,  $840,  $400, $FFC0,     0
    dc.w  $2EE0,  $790, $2F50,  $800,  $300, $FD00,  $100
    dc.w  $2F00,  $700, $2F70,  $790,  $100, $FC00,  $100
    dc.w  $2F30,  $680, $2F70,  $700,     0, $FC00,  $100
These tunnels can be disabled programmatically by setting the flags at RAM address $F7C7: bit 0 for player 1, bit 1 for player 2. I bring this up because there are two water tunnels in the intro pool, one of which directly overlaps the button:


When the door object spawns, it sets the $F7C7 flags to prevent the players from getting sucked into the sealed tunnel. As soon as the the level trigger array takes on a non-zero value though, the door snaps open, clearing the $F7C7 flags and re-enabling water tunnels in the process.
loc_2FE28:
    move.b  #3,($FFFFF7C7).w
    move.l  #loc_2FE34,(a0)

loc_2FE34:
    tst.b   (Level_trigger_array).w
    beq.s   loc_2FE58
    move.b  #3,$22(a0)
    move.b  #1,$24(a0)
    move.l  #loc_2FE5E,(a0)
    move.b  #0,($FFFFF7C7).w
    move.b  #1,($FFFFF650).w

loc_2FE58:
    jmp     (Sprite_OnScreen_Test).l
Another thing it does is set the general purpose "animated palette flag" over at RAM address $F650. Within the context of Hydrocity Zone 1, what the flag does is speed up the stage's rotating water palette from once every eight frames...


...to once every frame, for use in the tunnel flooding animation that starts immediately after the door is opened.


There's an oversight here, however. In a Sonic and Tails game, Tails can open the door while Sonic is outside the water tunnel's area of effect. This speeds up the rotating palette all the same, except Sonic is then free to back out of the pool and catch the intro waterfall animating at a ludicrous speed.


If that's not good enough for you, don't worry. There's another oversight at play here.


Remember how the door disables water tunnels when it spawns? It doesn't actually re-enable them until we step on the button, so if we scroll both objects into view and then leave the area without touching the button (by taking the top path) then water tunnels will remain disabled. We can then take the long way around and enter the tunnel from the back side, because there is no rushing current to stop us.


Now, since we took the top path, the current dynamic resize routine is still 0, and so the background bricks take on their yellow coloring. Unfortunately, you can't go far enough left to trigger the switch to resize routine 2: attempting to go past one of the breakable bars instantly re-enables the water tunnel.

Tuesday, December 5, 2017

Hydrocity Zone intro area: the pool

It's immediately apparent that the large pool of water at the start of Hydrocity Zone 1 looks different from all other water in the game. None of the foreground tiles are affected by the water palette, and neither are sprites, for that matter.


Actually, when you put it that way, only the brick pattern in the background takes on different colors below the waterline. The reason for this is made clear if we open up the stage in SonLVL, or just travel all the way to its loopback:


There's no water palette at all! The bricks below the waterline take on different colors because they use different colors.

Normally, the level blocks containing this brick pattern are drawn using the three shades of turquoise from the latter half of palette line 2. However, exclusively within the intro pool such level blocks are instead drawn with palette line 3, which grants the bricks a set of yellow colors otherwise used solely by a candle-lit ornament in the stage's background.


Then, once the camera position approaches the vicinity of the pool, the stage's dynamic resize routines kick in and load the underwater brick colors over the three shades of yellow, thereby completing the effect.


This explains why the bricks appear yellow in the loopback: although the pool blocks are within view, the actual camera position is far beyond the end of the level, outside the range monitored by the resize routines.

Okay, but what's the reason for all this trickery? Why not just a regular water palette like everywhere else in the game?


The reason is the dry tunnel off to the right. We need a way to draw the brick pattern both dry and submerged, side-by-side. The usual scanline tricks aren't going to help us.

All right, so swapping out the colors is indeed a suitable solution. If you take a close look at the underwater brick colors though, you'll notice something's wrong. The highlight color used in the intro pool (left, below) is brighter and more blue than the actual water palette used in the rest of the stage (right):


The reason for this becomes obvious once you look at the dynamic resize routine code: the color values were punched in by hand, rather than being read from the stage's palette files, and the programmer got one of the colors wrong:
loc_1C892:
    cmpi.w  #$360,(Camera_X_pos).w
    bhs.s   locret_1C8B6
    cmpi.w  #$3E0,(Camera_Y_pos).w
    blo.s   locret_1C8B6
    lea     (Normal_palette_line_4+$10).w,a1
    move.w  #$B80,(a1)+
    move.w  #$240,(a1)+
    move.w  #$220,(a1)+
    addq.b  #2,(Dynamic_resize_routine).w

locret_1C8B6:
    rts
Wait, what? B80? That's not even a valid Mega Drive color! In practice, what happens is the low bit of the blue channel gets ignored, and the result is the same as A80. But the actual color is supposed to be 680!

I can almost imagine how this error occurred. Someone wrote down the intended color values on a piece of paper: 680, 240, 220. Then the programmer came along and blindly typed in what he read from the piece of paper.


Admittedly the error isn't as obvious on real hardware, but there was no reason to hammer the values into the code like that. Furthermore, the code itself was later replicated on the Sonic & Knuckles cartridge, so they had another chance to fix their mistake. They missed it again! Aargh.

Okay, one last glitch with the resize routines. Hydrocity Zone 1 has three of them, and they're arranged logically like so:

0

2 → 4

The level starts from routine 0. When the camera dips far enough below, routine 0 loads the underwater brick colors as seen above, and changes the current routine to 2. Conversely, if the camera goes back up, routine 2 reloads the yellow background colors and resets the current routine back to 0.

There's another code path for it to take, though. If the camera remains low and instead leaves the vicinity of the pool to the right, routine 2 again reloads the yellow colors, but instead advances the current routine to 4.

Routine 4 is a stub function: it doesn't have any logic. As such, once it's reached, no more dynamic resize code will run for the remainder of the stage. Which means that as soon as you take the tunnel out of the pool, the underwater bricks become yellow, and then remain stuck that way.


We can confirm this by going the long way around, back to the high path out of the pool. At the location pictured above, a collision change object usually forces the player onto the top walkway, preventing them from returning to the pool.

However, as we've come to expect, shoddy placement of the object allows you to cross it to the right, then vault over it to the left to remain on the lower walkway. With no other obstacles in our way, we can finally dive back in the drink.


Yeah!