Showing posts with label Graphics. Show all posts
Showing posts with label Graphics. Show all posts

Tuesday, October 1, 2019

One Steve Shield Limit

Reader Emi asked:
There's something that always bothered me in sonic 3 with super sonic
Why SS can't use the insta shield? Or why you can't use the insta shield with normal invicibility.
I'm not sure if that's a bug but it feels odd since the rest still can use their special moves as super but sonic can't
The reason for this is twofold.

First off, the insta-shield is implemented as regular power-up -- it is actually a separate object which spawns into the player 1 shield shot when playing as Sonic, and Sonic is also awarded one whenever he loses any other shield. This is because unlike other characters' special moves, the insta-shield must animate independently from Sonic, and it loads its art to the same VRAM slot reserved for shields and invincibility stars.

This is probably why Sonic can't use any shield moves while invincible. Since shields and invincibility share a single VRAM slot, they can't be displayed at the same time (thank goodness), and it would be pretty bad design to let players use shield moves without knowing ahead of time which move Sonic would actually perform. As such, all shield moves are disabled while Sonic's shield is hidden, and this includes Super Sonic.

Of course, you could reasonably argue that since Sonic's shield is hidden, the insta-shield should just override all other shield moves for the duration of the Super form. After all, it's what Hyper Sonic's jump dash does. That would probably work, if not for another VRAM conflict: similar to the insta-shield, the large sparks that trail behind Super characters at high speeds load their art to the same VRAM slot as shields and invincibility stars.


Actually, this is probably why they fixed that Sonic 2 bug where invincibility and Super would stack! It wasn't a problem in Sonic 2 because shields and invincibility used separate VRAM slots, since players could potentially have both at the same time in a 2P game. Anyway, it was probably too much trouble to mediate VRAM usage between the insta-shield and the sparks, so Super Sonic specifically has no jump moves. That much is intentional:
    bclr    #Status_RollJump,status(a0)             bclr    #Status_RollJump,status(a0)
    tst.b   (Super_Sonic_Knux_flag).w               tst.b   (Super_Sonic_Knux_flag).w
    beq.s   Sonic_FireShield                        beq.s   Sonic_FireShield
                                                    bmi.w   Sonic_HyperDash
    move.b  #1,double_jump_flag(a0)                 move.b  #1,double_jump_flag(a0)
    rts                                             rts
; --------------------------------------------- ; ---------------------------------------------

Sonic_FireShield:                               Sonic_FireShield:
    btst    #Status_Invincible,$2B(a0)              btst    #Status_Invincible,$2B(a0)
    bne.w   locret_12A20                            bne.w   locret_11A14
    btst    #Status_FireShield,$2B(a0)              btst    #Status_FireShield,$2B(a0)
    beq.s   Sonic_LightningShield                   beq.s   Sonic_LightningShield
Now, what probably wasn't intentional is how Super Sonic can trigger the bubble shield's bounce effect upon touching the ground in S3A!


Note how in the above bit of code, Sonic's double_jump_flag is still set upon a successful double jump, even when the Super_Sonic_Knux_flag is on. If Sonic happens to have a bubble shield, that will cause the Player_TouchFloor routine to trigger a bubble bounce, despite the initial downward plunge bit never actually taking place.

S&K adds a check for Super_Sonic_Knux_flag to this code, as well as a clumsy character ID check since characters other than Sonic sometimes run through this code too I guess:
    tst.b   double_jump_flag(a0)                    tst.b   double_jump_flag(a0)
    beq.s   locret_130BC                            beq.s   locret_12230
                                                    tst.b   character_id(a0)
                                                    bne.s   loc_1222A
                                                    tst.b   (Super_Sonic_Knux_flag).w
                                                    bne.s   loc_1222A
    btst    #Status_BublShield,$2B(a0)              btst    #Status_BublShield,$2B(a0)
    beq.s   loc_130B6                               beq.s   loc_1222A
    bsr.s   BubbleShield_Bounce                     bsr.s   BubbleShield_Bounce

loc_130B6:                                      loc_1222A:
    move.b  #0,double_jump_flag(a0)                 move.b  #0,double_jump_flag(a0)
What baffles me is, why include that code path at all? If you just remove the Super_Sonic_Knux_flag check, then the invincibility check in Sonic_FireShield will also catch Super Sonic and bail without setting the double_jump_flag. Yes, the game will continue checking the A, B and C buttons once every frame until Sonic lands back on the floor, but guess what? The game already does that for invincibility!

Okay, so the insta-shield is just a regular old object in the shield slot. Does that mean that if we award it to characters other than Sonic, they too can benefit from its unique properties? Not really.


You see, all the interesting stuff is done by the Sonic object itself. So long as his double_jump_flag is set to 1, Sonic's attack radius is increased beyond its normal range. The insta-shield simply plays its animation whenever the flag jumps from 0 to 1, and then at the end of the animation, it increases the flag to 2, returning Sonic's attack radius to normal.

Incidentally, this is the source of another S3A bug: the double_jump_flag is only cleared when Sonic touches the floor, so if the insta-shield's animation ends after Sonic has already landed, the flag will remain stuck at 2 until Sonic touches the floor again, and any double jumps prior to that will be considered to have been "already spent", as it were.

S&K fixes this by checking whether the double_jump_flag has already been cleared before attempting to set it to 2.


In my upcoming ROM hack, which I *swear* isn't cancelled, you can have characters other than Tails as your support character. Shields can still be awarded to player 1, though, so what do we do about the insta-shield art conflict?

Well, the spindash dust object is already working overtime as skid dust, water splashes and the drowning timer, so now it also serves as an insta-shield for player 2. Yeah, it disappears when player 2 jumps in/out of water and fails to show up at all when they're drowning, but as reader Emi notes, blocking the insta-shield altogether would just feel... odd.

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.

Friday, March 16, 2018

The many tendrils of a Sonic 3 level, part 2: the animal exchange

Picking up from where we left off, at $2BD98 we come across the Obj_Animal object, which as we learned before, is the small animal that spawns both from a defeated enemy and from an act 2 end capsule. And right at the start of its code, we find this seemingly innocuous byte array:
byte_2BDDA: dc.b 5, 1
            dc.b 0, 3
            dc.b 5, 1
            dc.b 0, 5
            dc.b 6, 5
            dc.b 2, 3
            dc.b 6, 1
            dc.b 6, 5
            dc.b 6, 5
            dc.b 6, 5
            dc.b 6, 5
            dc.b 6, 5
            dc.b 6, 5
This is actually a very dangerous byte array! When the animal object is first initialized, it uses the value of the current zone, plus the value in register d0 (which is randomly set to either 0 or 1), to index the byte_2BDDA array, and put the resulting byte into offset $30 of its own SST:
loc_2BF4A:
    moveq   #0,d1
    move.b  (Current_zone).w,d1
    add.w   d1,d1
    add.w   d0,d1
    lea     byte_2BDDA,a1
    move.b  (a1,d1.w),d0
    move.b  d0,$30(a0)
Then at loc_2BFEA, after the animal first lands on the floor, it feeds this value into the routine counter at offset 5 of its own SST, in order to determine whether it should hop or fly away:
    move.b  $30(a0),d0
    add.b   d0,d0
    addq.b  #4,d0
    move.b  d0,5(a0)
It is therefore vital that the byte_2BDDA array contain two entries for each level in the game, as these determine which two animals may spawn in any given stage. With Sonic 3 in particular, every stage past Launch Base Zone reuses the same animal combination as Flying Battery Zone. In Sonic & Knuckles, only Doomsday Zone retains this property.
byte_2BDDA: dc.b 5, 1  ; AIZ                    byte_2C7BA: dc.b 5, 1  ; AIZ
            dc.b 0, 3  ; HCZ                                dc.b 0, 3  ; HCZ
            dc.b 5, 1  ; MGZ                                dc.b 5, 1  ; MGZ
            dc.b 0, 5  ; CNZ                                dc.b 0, 5  ; CNZ
            dc.b 6, 5  ; FBZ                                dc.b 6, 5  ; FBZ
            dc.b 2, 3  ; ICZ                                dc.b 2, 3  ; ICZ
            dc.b 6, 1  ; LBZ                                dc.b 5, 1  ; LBZ
            dc.b 6, 5  ; MHZ                                dc.b 6, 1  ; MHZ
            dc.b 6, 5  ; SOZ                                dc.b 0, 1  ; SOZ
            dc.b 6, 5  ; LRZ                                dc.b 5, 1  ; LRZ
            dc.b 6, 5  ; SSZ                                dc.b 0, 5  ; SSZ
            dc.b 6, 5  ; DEZ                                dc.b 6, 1  ; DEZ
            dc.b 6, 5  ; DDZ                                dc.b 6, 5  ; DDZ
                                                            dc.b 5, 1  ; Ending
                                                            dc.b 5, 1  ; ALZ
                                                            dc.b 5, 1  ; BPZ
                                                            dc.b 5, 1  ; DPZ
                                                            dc.b 5, 1  ; CGZ
                                                            dc.b 5, 1  ; EMZ
                                                            dc.b 5, 1  ; Gumball
                                                            dc.b 5, 1  ; Pachinko
                                                            dc.b 5, 1  ; Slots
                                                            dc.b 5, 1  ; LRZ Boss
                                                            dc.b 5, 1  ; DEZ Boss
Interestingly, in Sonic & Knuckles, the array was extended to cover every level slot, rather than stopping at Doomsday Zone. This was probably done for the sake of the Lava Reef Zone boss act, in which animals spawn from the capsule at the very end of the stage. Indeed, if we look at the numbers, all of the new entries in the array use the same animal combination as Lava Reef Zone.

The length of this array in Sonic 3 suggests that attempting to load any level slot beyond Doomsday Zone is inherently dangerous. But in reality, it only becomes a problem if an animal is spawned while playing the stage; it shouldn't affect whether the stage can load or not.

What does affect it are the animals' graphics, which are loaded by the PLCLoad_AnimalsAndExplosion function at $543F4 when the level starts. Much like the animal object, it uses the value of the current zone as an offset to an array, which contains pointers to the PLCs for each stage's combination of animals.

And therein lies the rub, because in Sonic 3, this array only goes up to Launch Base Zone:
off_54414:  dc.w PLC_54422-off_54414  ; AIZ     off_85FFE:  dc.w PLC_8602E-off_85FFE  ; AIZ
            dc.w PLC_54430-off_54414  ; HCZ                 dc.w PLC_8603C-off_85FFE  ; HCZ
            dc.w PLC_5443E-off_54414  ; MGZ                 dc.w PLC_8604A-off_85FFE  ; MGZ
            dc.w PLC_5444C-off_54414  ; CNZ                 dc.w PLC_86058-off_85FFE  ; CNZ
            dc.w PLC_5445A-off_54414  ; FBZ                 dc.w PLC_86066-off_85FFE  ; FBZ
            dc.w PLC_54468-off_54414  ; ICZ                 dc.w PLC_86074-off_85FFE  ; ICZ
            dc.w PLC_54476-off_54414  ; LBZ                 dc.w PLC_86082-off_85FFE  ; LBZ
                                                            dc.w PLC_86090-off_85FFE  ; MHZ
                                                            dc.w PLC_8609E-off_85FFE  ; SOZ
                                                            dc.w PLC_860AC-off_85FFE  ; LRZ
                                                            dc.w PLC_860BA-off_85FFE  ; SSZ
                                                            dc.w PLC_860C8-off_85FFE  ; DEZ
                                                            dc.w PLC_860D6-off_85FFE  ; DDZ
                                                            dc.w PLC_860E4-off_85FFE  ; Ending
                                                            dc.w PLC_860E4-off_85FFE  ; ALZ
                                                            dc.w PLC_860E4-off_85FFE  ; BPZ
                                                            dc.w PLC_860E4-off_85FFE  ; DPZ
                                                            dc.w PLC_860E4-off_85FFE  ; CGZ
                                                            dc.w PLC_860E4-off_85FFE  ; EMZ
                                                            dc.w PLC_860E4-off_85FFE  ; Gumball
                                                            dc.w PLC_860E4-off_85FFE  ; Pachinko
                                                            dc.w PLC_860E4-off_85FFE  ; Slots
                                                            dc.w PLC_860E4-off_85FFE  ; LRZ Boss
                                                            dc.w PLC_860E4-off_85FFE  ; DEZ Boss
PLC_54422:  dc.w 1                              PLC_8602E:  dc.w 1
            dc.l ArtNem_BlueFlicky                          dc.l ArtNem_BlueFlicky
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Chicken                             dc.l ArtNem_Chicken
            dc.w $B240                                      dc.w $B240
PLC_54430:  dc.w 1                              PLC_8603C:  dc.w 1
            dc.l ArtNem_Rabbit                              dc.l ArtNem_Rabbit
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Seal                                dc.l ArtNem_Seal
            dc.w $B240                                      dc.w $B240
PLC_5443E:  dc.w 1                              PLC_8604A:  dc.w 1
            dc.l ArtNem_BlueFlicky                          dc.l ArtNem_BlueFlicky
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Chicken                             dc.l ArtNem_Chicken
            dc.w $B240                                      dc.w $B240
PLC_5444C:  dc.w 1                              PLC_86058:  dc.w 1
            dc.l ArtNem_Rabbit                              dc.l ArtNem_Rabbit
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_BlueFlicky                          dc.l ArtNem_BlueFlicky
            dc.w $B240                                      dc.w $B240
PLC_5445A:  dc.w 1                              PLC_86066:  dc.w 1
            dc.l ArtNem_Squirrel                            dc.l ArtNem_Squirrel
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_BlueFlicky                          dc.l ArtNem_BlueFlicky
            dc.w $B240                                      dc.w $B240
PLC_54468:  dc.w 1                              PLC_86074:  dc.w 1
            dc.l ArtNem_Penguin                             dc.l ArtNem_Penguin
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Seal                                dc.l ArtNem_Seal
            dc.w $B240                                      dc.w $B240
PLC_54476:  dc.w 1                              PLC_86082:  dc.w 1
            dc.l ArtNem_Squirrel                            dc.l ArtNem_BlueFlicky
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Chicken                             dc.l ArtNem_Chicken
            dc.w $B240                                      dc.w $B240
                                                PLC_86090:  dc.w 1
                                                            dc.l ArtNem_Squirrel
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_8609E:  dc.w 1
                                                            dc.l ArtNem_Rabbit
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_860AC:  dc.w 1
                                                            dc.l ArtNem_BlueFlicky
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_860BA:  dc.w 1
                                                            dc.l ArtNem_Rabbit
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_860C8:  dc.w 1
                                                            dc.l ArtNem_Squirrel
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_860D6:  dc.w 1
                                                            dc.l ArtNem_Squirrel
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
                                                PLC_860E4:  dc.w 1
                                                            dc.l ArtNem_BlueFlicky
                                                            dc.w $B000
                                                            dc.l ArtNem_Chicken
                                                            dc.w $B240
So even if we were to fix up the level load block for the Sonic & Knuckles stages, the PLCLoad_AnimalsAndExplosion function would then take the value of the current zone, go past the end of the pointer array straight into the actual PLC definitions, and eventually attempt to load a nonsensical PLC, which would most definitely crash the game. Huzzah!
PLC_54476:  dc.w 1                              PLC_86082:  dc.w 1
            dc.l ArtNem_Squirrel                            dc.l ArtNem_BlueFlicky
            dc.w $B000                                      dc.w $B000
            dc.l ArtNem_Chicken                             dc.l ArtNem_Chicken
            dc.w $B240                                      dc.w $B240
It was only by looking at the animal data side-by-side like this that I stumbled upon one of the most obscure differences between Sonic 3 and Sonic & Knuckles that I know of. In Sonic 3, the animals which appear in Launch Base Zone are Ricky (the squirrel) and Cucky (the chicken). In Sonic & Knuckles however, Ricky was replaced by Flicky (the flicky).

This wasn't an accident; if you check the byte_2BDDA array at the top of this post, you'll see that the animal type was changed from 6 to 5 there, too.


Finally, on the subject of animal types, here's something that always bugged me. In Sonic 1, there were seven different animals. From left to right, they are: Pocky, Cucky, Pecky, Rocky, Picky, Flicky and Ricky.


Sonic 2 then added five more, whose names I can't find a source for: an eagle, a mouse, a monkey, a turtle and a bear.


When it came time to make Sonic 3, the developers decided to scale back to the original cast of animals. Indeed, if you look at the byte_2BDDA array, you'll find that they're numbered 0 through 6... but animal 4 is nowhere to be seen.


Picky appears to have disappeared; not even his graphics can be found within the ROM. Maybe he's still hiding in the Casino Night Zone?

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.

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.

Monday, January 22, 2018

Japan mode

All three main series Sonic games have a routine which looks up the region code of the Mega Drive it's running on, and saves that information to a RAM variable when the game first starts up.

Besides handling the timing differences between PAL and NTSC systems, this variable is also used at various points to determine whether the game is running on a Japanese system, and if so, remove the trademark symbols from all Sonic and SEGA logos, since those trademarks don't apply in Japan.


This variable also controls the name which Tails goes by. Sonic Team originally named the character Miles Prower, and he was only given the "Tails" moniker at the request of Sega of America's marketing department. Despite this, Sonic 2's programmers made it so the original name is still used when the game is played on a Japanese system, a trait that was later carried over into Sonic 3.


In S3A however, this feature seems to be unfinished. No trademark symbols are removed when playing the game on a Japanese console, and Tails' name is only changed in the level results screen, and not in his lives counter:


Looking at the S3A ROM reveals why: the graphics for the alternate counter seem to be missing entirely! As a result, to finish this feature for Sonic 3 & Knuckles, the developers had to include the Miles lives counter within the S&K ROM.

Friday, January 19, 2018

You're going to pay for this!

An anonymous commenter asked:
Since you have covered the Surfboard and the Rotating spheres, could you talk about the "Eggman flying away" sprites?
Sure. At ROM offset $1796EE, there's this block of uncompressed art which doesn't look like anything at first, but if you spread out the tiles horizontally, apply Sonic's palette and then tilt your head sideways, they start looking a bit familiar...


Okay, so these sprites definitely resemble Eggman riding the Egg Mobile, but why are they sideways and stretched out like that? That's because they're actually meant to be rendered using a software scaling routine, the same one used by the Marble Garden Zone 2 boss, as well as the Egg Robo enemies in Sonic & Knuckles.

It turns out that, much like the surfboard intro before it, there's actually an unused, fully-programmed object which uses these sprites, exclusively in the Sonic 3 ROM at offset $51B4C.

No other code references this object, so it's impossible to know for sure when it was meant to appear, but its placement in the ROM directly after the Launch Base Zone 2 bosses, but before the exploding Death Egg object seems to indicate that it was meant for the game's ending sequence.

By using the PAR code 05165A:04F2, we can force the object to spawn as the sky begins to brighten:


As can be seen in the above video, the fleeing Eggman object actually matches the vertical movement of the exploding Death Egg quite well. Perhaps at one point, the Death Egg was further to the left on the screen, and this scene showed Eggman heading towards it, setting the stage for the second part of the story.

Tuesday, January 9, 2018

Eosian Balls

An anonymous commenter asked:
Could you write about the rotating red sphere thing on AIZ1 (Sonic 3 alone debug)? Did that have any purpose other than destroy the game´s palette and framerates?
Directly after the surfboarding Sonic object, Angel Island Zone 1's debug reel once again treats us to an unusual sprite: a set of red spheres revolving around each other in three dimensions:


What on earth could be the purpose of this object? Here's what I conjectured when I was just a kid: it's only available in AIZ1's debug reel, it comes right after the surfboard, it's red and it messes with the HUD once it's placed. It must be the early equivalent of the cutscene Knuckles object! I was a dumb kid.

The truth makes considerably more sense: it is a test of the 3D sprite effect seen in the game's special stages. The fact that the object is part of AIZ1's debug reel seems to be entirely circumstantial; it was likely added to the first level of the game simply for ease of access. However, its origins in the special stage become evident when we look up the location of its graphics within the ROM:

NameAddress
ArtUnc_SStageTailstails0909E8
Map_SStageTailstails0910E8
PLC_SStageTailstails091106
ArtNem_SphereTest0911CE
ArtNem_SStageShadow091B2E
ArtNem_GetBlueSpheres091B70
ArtNem_GBSArrow091CF2

The object's overall behavior can actually be controlled by player 2. The controls, although a tad obtuse, are as follows:
  • Down/Left: moves the object up and down
  • Up/Right/B: toggles the rotation of each axis
  • A/C: shrinks/enlarges the object
  • Start: toggles between automatic and manual rotation

No doubt owing to its roots, the sphere test object stores its movement and rotation state in a region of RAM starting at address $E400, which is used by the special stage for the same purpose. In the main game however, the same area is used to maintain a buffer of the player's last few positions. As a result, exiting debug mode and moving around will very quickly make the object become all kinds of messed up.

Monday, January 8, 2018

The unused surfboard intro

In S3A only, if we go through the debug reel for Angel Island Zone 1, we eventually run across an unusual sprite, which when placed spawns the object seen below: a surfboarding Sonic that hovers around the player's position.


One thing which is immediately apparent about this object, is that much like the Flying Battery Zone monkey bar sprites before them, the surfboarding sprites are an almost perfect match for the standing sprite from Sonic 2:


When this object is spawned through debug mode, its subtype is always set to a non-zero value. And since it shares its VRAM address with the Sonic object, it corrupts Sonic's appearance as soon as it spawns.

However, when a subtype of 0 is supplied, the object actually turns Sonic invisible by setting his mapping frame to zero and disables his movement and animation routines by setting bits 0 and 1 of his object_control attribute:
Obj_SurfboardIntro:
    ...
    tst.b   $2C(a0)
    bne.s   loc_20B78
    lea     (Player_1).w,a1
    move.b  #0,$22(a1)
    move.b  #3,$2E(a1)

loc_20B78:
    ...
So what's going on? It turns out that underneath its surface, this object contains code for an alternate intro sequence to Angel Island Zone 1. The presence of Sonic 2 sprites suggests that this sequence was the stage's original intro, before the plane intro seen in the final game was implemented.

We can restore the surfboard intro by simply changing which intro object is spawned at the start of the level:
SpawnLevelMainSprites:                          SpawnLevelMainSprites:
    ...                                             ...
    cmpi.w  #0,(Current_zone_and_act).w             cmpi.w  #0,(Current_zone_and_act).w
    bne.s   loc_4DFE                                bne.s   loc_4DFE
    cmpi.w  #2,(Player_mode).w                      cmpi.w  #2,(Player_mode).w
    beq.s   locret_4DFC                             beq.s   locret_4DFC
    move.l  #Obj_PlaneIntro,($FFFFB172).w           move.l  #Obj_SurfboardIntro,($FFFFB172).w
    clr.b   (Level_started_flag).w                  clr.b   (Level_started_flag).w
    rts                                             rts
Since we're going to make a lot of modifications to the game this time around, I'll put any relevant PAR codes alongside their respective snippets. Here are the codes for the above change:
004DF0:0002
004DF2:0B0E
That alone restores most of the scrapped intro, but there are still a bunch of rough spots. For instance, although the big waves have somehow fixed themselves, Sonic still has a splash of garbage trailing behind him, and as he jumps off the surfboard, it too turns into a garbled mess. This is because the graphics for these objects have not been loaded.


As part of the setup for his intro sequence, when Sonic first starts Angel Island Zone 1, the game loads PLC $A instead of the regular AIZ1 PLC. Just by looking at it, though, we can easily spot the problem: when the intro was reworked, the graphics for the surfboard and its splash were removed from the PLC:
PLC_0A: dc.w 0
        dc.l ArtNem_AIZIntroSprites
        dc.w $7A20
Unfortunately, there's no longer any room to add those graphics back to the PLC without stepping over the regular AIZ1 PLC. Fortunately however, PLC 0 is completely unused, so we can repurpose it for our needs:
PLC_00: dc.w 3                                  PLC_00: dc.w 2
        dc.l ArtNem_SonicLifeIcon                       dc.l ArtNem_SurfboardSplash
        dc.w $FA80                                      dc.w $A520
        dc.l ArtNem_Ring                                dc.l ArtNem_AIZIntroSprites
        dc.w $D780                                      dc.w $7A20
        dc.l ArtNem_EnemyPts                            dc.l ArtNem_Surfboard
        dc.w $BC80                                      dc.w $B0A0
        dc.l ArtNem_Monitors                            dc.l ArtNem_Monitors
        dc.w $9880                                      dc.w $9880
Below are the PAR codes for these changes. My apologies.
05B084:0002
05B086:0014
05B088:86E0
05B08A:A520
05B08C:0014
05B08E:81A0
05B090:7A20
05B092:0014
05B094:8978
05B096:B0A0
All we have to do now is make the intro code load PLC 0 instead of PLC $A:
loc_4756:                                       loc_4756:
    bsr.w   LevelLoad_ActiveCharacter               bsr.w   LevelLoad_ActiveCharacter
    tst.b   (Last_star_post_hit).w                  tst.b   (Last_star_post_hit).w
    bne.w   loc_4782                                bne.w   loc_4782
    cmpi.w  #0,(Current_zone_and_act).w             cmpi.w  #0,(Current_zone_and_act).w
    bne.s   loc_4782                                bne.s   loc_4782
    cmpi.w  #2,(Player_mode).w                      cmpi.w  #2,(Player_mode).w
    beq.s   loc_4782                                beq.s   loc_4782
    moveq   #1,d0                                   moveq   #1,d0
    jsr     (Load_PLC_2).l                          jsr     (Load_PLC_2).l
    moveq   #$A,d0                                  moveq   #0,d0
    bsr.w   Load_PLC                                bsr.w   Load_PLC
    bra.s   loc_479A                                bra.s   loc_479A
Which translates to the following PAR code:
00477A:7000
Should look mighty slick now, but we still have an additional issue: since the title card never comes down, the level gets stuck in this weird state where there's no HUD, rings are invisible, and most other objects appear as a garbled mess:


To fix this, we can simply replace the surfboard intro object's final call to the Sprite_OnScreen_Test function with a call to the last routine of the cutscene Knuckles object, which fixes everything for free.
loc_20D38:                                      loc_20D38:
    jmp     (Sprite_OnScreen_Test).l                jmp     (loc_448B0).l
Doing so gives us our final two PAR codes:
020D3A:0004
020D3C:48B0
That should about cover it. Let's finally take a look at our fully-restored surfboard intro sequence:


I know, it's not perfect. Since the game doesn't wait for the Nemesis queue to be empty before starting the level, the big waves still appear garbled for a while. However, fixing this would require much deeper changes to the game's code, the likes of which are beyond the scope of simple PAR codes.

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

Thursday, December 21, 2017

The underside of a hero

Not even the Special Stage sprites are exempt from historical oversights. Aside from a really bad misalignment with the standing frame, each sprite features a highly-detailed head that uses a bunch of different colors, whereas the torso and the legs seem much rougher in comparison, using just two shades of blue, at times resembling basic 3D polygons.


It would appear that once again, some of Sonic's sprites were left half-unfinished. But that isn't the only oversight these sprites share with the monkey bar animation.


In both Sonic 1 and Sonic 2, the soles of Sonic and Tails' shoes were colored the same as the rest of the shoe: red. For Sonic 3, the developers changed it so all three characters have black soles, but this redesign was never reflected in the Special Stage sprites for any of them.


Given their close tie to Sonic 2, it should be no surprise that Sonic's monkey bar sprites also feature red soles. They're not alone, though: red soles are also present in some rotation sprites used in Hydrocity Zone, Mushroom Hill Zone and Lava Reef Zone, as well as the horizontal "corkscrew" animation, which is lifted directly from Sonic 2.

EDIT: This was pointed out by commenter greenknight9000, which I somehow missed the first time around. My bad!

Wednesday, December 20, 2017

The face of a hero, part 3

In mapping frames $80-$85, all three characters have a set of unique sprites that's only used while swinging across the monkey bars in Flying Battery Zone. Despite clearly sharing the same base as the standing rotation sprites however, at least Sonic's sprites appear to be noticeably less detailed, looking almost unfinished at times:


These sprites are included in standalone Sonic 3, which isn't too surprising when you consider how Flying Battery Zone was originally meant to appear earlier in the game. What's surprising is how they're identical to the sprites later found in Sonic & Knuckles. It seems like a rough version of the animation was first cobbled together, but then the stage itself got canned before the sprites were polished up, and eventually they just forgot about the whole thing.

Particularly interesting is the first frame, which doesn't share much of a likeness with the rest of the Sonic 3 sprites, but is more or less a perfect match for the standing sprite from Sonic 2.


This is probably holdover from the days where Sonic was still using adapted versions of his Sonic 2 sprites, suggesting that at one point they weren't just placeholders, but actually part of the game's design.

Tuesday, December 19, 2017

The face of a hero, part 2

So how come frame $56 looks different from the regular standing sprite? Well, one possibility is that the differences are unintentional: at some point the developers went back and touched up the standing sprite, and frame $56 didn't receive those tweaks by simple virtue of being a separate frame.

Another possibility is that the differences are intentional, arguably to serve as a better inbetween frame for the standing rotation anim. There's a hole in that theory though, which is that the same cross-eyed gaze can be found in a couple of other sprites, such as the hanging animation:


Now, you could argue that the second theory still applies: the developers may have simply based the hanging sprite on frame $56 instead of the correct standing frame. The first theory seems more plausible though, if only for the fact that it has happened on multiple other occasions.

However, as an anonymous commenter pointed out:
Anyhow, what I find interesting about that presumably-early standing sprite is that the shine on Sonic's shoe matches his ducking sprites, where it spontaneously moves.

To me, that is the most convincing argument.

Monday, December 18, 2017

The face of a hero, part 1

Sonic's sprites in Sonic 3 have always been a divisive subject. Some people like them, but most people hate them, and they will go out of their way to make fun of his "baseball mits" and "clown shoes" without prior provocation.

With the release of Sonic Mania, these lovely individuals have seemingly been vindicated, as Sonic's sprites in that title are based off the ones in Sonic 1 and 2. This important design decision then led to fan-made comparisons such as this, to which the True Elite immediately respond by pointing out how cross-eyed the Sonic 3 sprites look.


Except that if you bother playing the game, you realize this is what Sonic's standing sprite actually looks like:


So where did the other sprite come from? We've actually seen it before on this very blog. It's one of the rotation sprites used when standing on rotating objects, such as the barrels in Carnival Night Zone.


This sprite just happens to be slightly different from the regular standing sprite, namely with respect to Sonic's left shoe, his mouth, and his gaze, which does indeed look a bit cross-eyed.

Okay, but how did this relatively obscure sprite end up in the comparison?

Well, as I pointed out back then, the standing rotation sprites take up mapping frames $55 through $5B. Meanwhile, as we recently saw, the actual standing pose is all the way over at mapping frame $BA. Someone comes along to make a rudimentary sprite rip, finds frame $56 long before reaching frame $BA, so they push it to the top of their sprite sheet.

Much later, someone naΓ―vely takes the first sprite in this sheet, blows the pixels up to the size of Legos, and uses them to win an argument on the Internet.

So how come the other characters don't suffer from this kind of mix-up?

Well, as for Tails, frame $56 is significantly different from his standing sprite, which is nearly unchanged from the one in Sonic 2. This makes it unlikely that anyone would confuse the two.


As for Knuckles, he actually uses frame $56 in his main standing animation, in no short part due to his sheer number of unique sprites, which struggle to fit within the 252 mapping frame limit.