Showing posts with label Bonus Stage. Show all posts
Showing posts with label Bonus Stage. Show all posts

Wednesday, January 17, 2018

They just didn't care, part 2

Sonic 3 somehow managed to revive a bug from the prototype versions of Sonic 2: holding down a jump button while a level is loading causes the button press to be buffered until the player gains control of the character, making them jump as soon as the level starts.


As reader Brainulator9 points out, this works even when the player object spawns in mid-air, allowing you to do things you were never supposed to, such as turning into Super Sonic within a bonus stage.

Luckily, this was fixed in Sonic & Knuckles for all three player objects. However, there's one instance in which the game doesn't spawn the regular player objects. ...Yep, you know it.


Don't you just love Competition mode?

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:

Friday, December 1, 2017

Press F to Pay Respects

Unlike the Gumball Machine stage, the Rolling Jump bonus stage doesn't use random number generation to determine the subtype of spawned gumballs. Instead, it uses the item ball object's vertical position to pick a pool of gumball types, and then the actual type is selected from that pool based on the two lowest bits of the stage's overall frame counter.

This means that each item ball in the stage will only ever produce a predefined set of four gumball types, but the player can influence which of those actually spawns by delaying the frame in which they touch the object.
byte_1E4484:    dc.b    1,   3,   1,   3    ; 0-$FF
                dc.b    8,   3,   8,   5    ; $100-$1FF (unused)
                dc.b    1,   3,   6,   4    ; $200-$2FF
                dc.b    1,   7,   6,   5    ; $300-$3FF
                dc.b    8,   6,   4,   3    ; $400-$4FF
                dc.b    4,   3,   4,   5    ; $500-$5FF
                dc.b    8,   4,   5,   3    ; $600-$6FF
                dc.b    7,   3,   8,   3    ; $700-$7FF
                dc.b    6,   5,   6,   7    ; $800-$8FF
                dc.b    4,   3,   7,   5    ; $900-$9FF
                dc.b    6,   4,   6,   4    ; $A00-$AFF
                dc.b    7,   3,   3,   5    ; $B00-$BFF
                dc.b    4,   3,   4,   6    ; $C00-$CFF (unused)
                dc.b    3,   4,   3,   7    ; $D00-$DFF
                dc.b    4,   3,   4,   3    ; $E00-$EFF (unused)
                dc.b    4,   3,   4,   3    ; $F00-$FFF (unused)
Note that due to the stage's layout, several of these vertical ranges do not actually contain any item balls, rendering the associated type pool unused. Also note that relative to the Gumball Machine stage, all the subtypes are shifted forward by one, so for example, the two item balls at the very top of the stage have a 50% chance of spawning a 1-Up Ball or a Ring Ball, and since subtype 2 is missing entirely, no item ball will ever spawn a Replace Ball.


So what happens if you force the subtype of the gumball object to zero? Well, it turns out the Rolling Jump bonus stage has an unused gumball type of its own: the F Ball.


What does the F stand for? We can only speculate. Maybe it stood for Fast Ball, and it would have increased the speed of the rising trap at the bottom of the stage, as a sort of antithesis to the helpful Replace Ball from the Gumball Machine stage. Both of them are green, after all.
loc_4A3AC:
    moveq   #$7B,d0
    jsr     (Play_Sound_2).l
    rts
In reality, though, both the F Ball and the Replace Ball use the same item collection routine, which does nothing except play the bumper sound effect.

Thursday, November 30, 2017

VDP sprite masking

Particularly astute readers will notice there's something off about my previous post. If the Super gumball is a sprite, and it's being displayed behind the level blocks...


...then that means the level blocks must have their priority bit set. If that's the case, though, then the only way to display other sprites over them is to set their priority bit as well, which causes them to be displayed over all the level blocks:


So how do we get around this? It's time to break out the big guns.

Remember how the VDP implements the sprite attribute table as a linked list of sprite data? There's an obscure feature where if, in the process of rendering a row of pixels, the VDP encounters a sprite whose X coordinate is exactly zero, it will not render any sprites that appear further down the list.


The trick is to push the player sprites and the machine's handle into the table first, then push a garbage sprite with an X coordinate of zero, and finally push the gumball sprites which make up the contents of the machine. Coordinate zero is far left of the visible region of the screen, so the garbage sprite is never actually drawn, but it masks the gumball sprites because they appear further down the list.


The same trick is used in Sonic 2's title screen animation. When Sonic and Tails rise from inside the wing emblem, their sprites are actually fully constructed from the start and are simply moved up in order to gradually reveal them. A couple of garbage sprites at X coordinate zero prevent them from peeking out from the underside of the emblem.


Sonic 1's title screen behaves very similarly, but without actually using the sprite masking trick. Instead, it just places an absurd amount of garbage sprites immediately off screen, causing the sprite limit to kick in early, which produces a nigh indistinguishable effect.

Wednesday, November 29, 2017

Featuring S from the Gumball series

After reading yesterday's post, you might be under the impression that normally, the graphics for the Super gumball are completely unused. Well, you're half right.


If we get rid of the high priority sprites lining the top half of the gumball machine, we can get a better look at the generic placeholder gumballs which make up the foreground plane:


Oh, it seems we have a low priority sprite hiding behind the foreground layer. You would be forgiven for thinking that it's just a second Thunder Barrier, but we know better by now.


Hey, at least they managed to find a use for one of the sprites!

Tuesday, November 28, 2017

Super Gum

If you examine the gumball object's setup code at sub_612A8, you may notice how the object's mapping frame is at no point determined by its subtype value. Instead, the code copies the ROM address of one of several animation scripts to offset $30 of its SST, which the object later processes by calling the Animate_Raw function:
word_61466: dc.w $7F08
            dc.w $8FC
word_6146A: dc.w $7F09
            dc.w $9FC
word_6146E: dc.w $7F0A
            dc.w $AFC
word_61472: dc.w $7F0B
            dc.w $BFC
word_61476: dc.w $7F0C
            dc.w $CFC
word_6147A: dc.w $7F0D
            dc.w $DFC
word_6147E: dc.w $7F0E
            dc.w $EFC
word_61482: dc.w $7F0F
            dc.w $FFC
word_61486: dc.w $110
            dc.w $11FC
Hmm, it doesn't look like an animation script? Blame the formatting in the disassembly. It should start looking a bit more familiar if we break it into bytes:
word_61466: dc.b  $7F,   8,   8, $FC
word_6146A: dc.b  $7F,   9,   9, $FC
word_6146E: dc.b  $7F,  $A,  $A, $FC
word_61472: dc.b  $7F,  $B,  $B, $FC
word_61476: dc.b  $7F,  $C,  $C, $FC
word_6147A: dc.b  $7F,  $D,  $D, $FC
word_6147E: dc.b  $7F,  $E,  $E, $FC
word_61482: dc.b  $7F,  $F,  $F, $FC
word_61486: dc.b    1, $10, $11, $FC
Ah, that's more like it. Unsurprisingly, the animations have nothing to them: the same mapping frame is displayed twice, and the animation starts over. It seems pointless for the gumball object to use animations at all, doesn't it? That is, until you realize there are nine scripts, and that the last one alternates between two distinct mapping frames:


Given the letter printed on its surface, and its flashing coloration reminiscent of Super Sonic, it think it's fair to say we're looking at an unused ninth gumball, which would change the player into their super form. Indeed, if the gumball object's subtype is manually set to 8, it will use a unique item collection routine along with this animation.


Unfortunately, the item collection routine doesn't do much: all it does is set bit 7 of the Saved_status_secondary RAM address, which has nothing to do with super transformations and whose bits are all cleared on level load except for the three elemental shield bits, anyway.
loc_61264:
    bset    #7,(Saved_status_secondary).w
    rts
My guess is that at some point, bit 7 would spawn the player directly in their super form. It would be pretty dumb for the gumball's effect to wear off as soon as you left the bonus stage!

Monday, November 27, 2017

Aww, no grapes? What a rip-off.

In the Gumball Machine bonus stage, each time the player rotates the handle, a new gumball object is spawned, with a random subtype. This is achieved by calling the Random_Number function, which produces pseudo-random longword values along a uniform distribution.


The code at sub_612A8 whittles this value down to its four lower bits, generating a random number between 0 and 15, which is then used as an index into a table containing the actual subtype values, which in turn range from 0 to 7:
byte_612E0:
    dc.b 0  ; 1-Up Ball
    dc.b 3  ; Clear
    dc.b 1  ; Replace Ball
    dc.b 4  ; Bumper
    dc.b 2  ; Ring Ball
    dc.b 4  ; Bumper
    dc.b 5  ; Flame Barrier
    dc.b 4  ; Bumper
    dc.b 6  ; Aqua Barrier
    dc.b 3  ; Clear
    dc.b 7  ; Thunder Barrier
    dc.b 4  ; Bumper
    dc.b 5  ; Flame Barrier
    dc.b 6  ; Aqua Barrier
    dc.b 7  ; Thunder Barrier
    dc.b 2  ; Ring Ball
This effectively converts the uniform distribution of the randomly-generated number into a weighted probability function: for instance, the Thunder Barrier appears twice in the table, so it's twice as likely as a Replace Ball, which only appears once. Meanwhile, it's only half as likely as a bumper, which appears four times in the table.


There's a trick, though. Whenever the resulting gumball is a 1-Up Ball, a RAM flag is set which causes any subsequent 1-Ups to be replaced with the fourth entry from the table, producing a bumper:
sub_612A8:
    jsr     (Random_Number).l
    andi.w  #$F,d0
    bne.s   loc_612C2
    movea.w $46(a0),a1
    bset    #7,$38(a1)
    beq.s   loc_612C2
    moveq   #3,d0

loc_612C2:
    move.b  byte_612E0(pc,d0.w),d0
    move.b  d0,$2C(a0)
    ...
As a result, only a single 1-Up Ball will ever spawn per bonus stage, and once it does, the random distribution becomes even more skewed toward bumpers. Here's all of this information compiled into a table, because people like tables:

Gumball TypeLikelyhood (before 1-up)Likelyhood (after 1-up)
1-Up Ball1/160/16
Replace Ball1/161/16
Ring Ball2/162/16
Clear2/162/16
Bumper4/165/16
Flame Barrier2/162/16
Aqua Barrier2/162/16
Thunder Barrier2/162/16

Thursday, May 18, 2017

Direct Memory Access

DMA is a feature that allows the VDP to very quickly copy a large amount of data to an arbitrary location in VRAM. It is the method by which the sprite and background tables are written to VRAM every frame, but it can just as well be used to dynamically load tiles.


One of the main uses of DMA in Sonic games is to load the main character graphics as they're needed. Since only one of Sonic's sprites is ever visible at a time, new tiles can be loaded over the old ones, saving VRAM. Special Stage rings temporarily overwrite other graphics when they appear; using DMA minimizes the number of affected tiles.

DMA is also used to animate the background planes. By periodically writing new tiles over specific regions of the main level art, static elements such as the plants in Angel Island Zone and the mesh cylinders in Flying Battery Zone can be made to move. The same principle is used to achieve the extra background layer trick we previously saw.


One drawback of using DMA is that the source tile data must be uncompressed, which takes up a lot of storage space. As an example, Knuckles' sprites alone take up a whopping 1/16 of the 2 megabyte Sonic & Knuckles cartridge. In fact, uncompressed art takes up almost 20% of the combined S3&K ROM!

The rolling jump Bonus Stage is pretty smart about this. The graphics for its complex animated background are actually compressed within the ROM, but the game first decompresses them to 68k RAM, from where they can then be DMA'd to the appropriate location in VRAM.