Showing posts with label Lock-on. Show all posts
Showing posts with label Lock-on. Show all posts

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, 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:

Wednesday, October 18, 2017

The S&K alone flag

23 years ago on this very day, Sonic & Knuckles was released worldwide on the Sega Genesis. Happy anniversary!


To mark the occasion, I figured it might be relevant to discuss what happens when Sonic & Knuckles boots up without a second game locked on to the cartridge.

First, how does it detect a cartridge in the lock-on slot? The game's startup routine checks for the presence of a lock-on cartridge's ROM header by comparing the longword at address $200100 with the string "SEGA". If this check fails, then the lock-on slot does not contain a valid cartridge, and control jumps to SonicAndKnucklesStartup, which handles the boot sequence both for Sonic 3 & Knuckles as well as for standalone Sonic & Knuckles:
    moveq   #-1,d1
    move.l  (SegaHeadersText).l,d0          ; test to see if SEGA is at the locked on ROM's $100
    cmp.l   (LockonHeader).l,d0
    bne.w   SonicAndKnucklesStartup
Over at SonicAndKnucklesStartup, the value in the d1 register is written to RAM address $FFAE otherwise known as the SK_alone_flag. This flag is clear when the Sonic 3 cartridge is locked on, and set when the lock-on slot is empty:
SonicAndKnucklesStartup:
    bsr.s   Test_Checksum
    move.w  d1,(SK_alone_flag).w
    ...
So, what does the flag do? Well, perhaps rather obviously, the first thing the flag is responsible for is whether to display the Sonic & Knuckles title screen, or the Sonic 3 & Knuckles one. It also defines the game title used in stage title cards, and which logo appears at the end of the credits sequence.


Recall how I mentioned that Sonic 3 & Knuckles is just a special mode of Sonic & Knuckles that is allowed to reference content from the Sonic 3 cartridge? That is the mode in which the SK_alone_flag is clear. Conversely, when the flag is set, the game cannot be granted access the Sonic 3 cartridge, because it isn't there!

As a result, setting the flag causes a whole bunch of stuff to get skipped:
  • All save functionality is disabled. The battery backup is on the Sonic 3 cartridge, so there would be no point in running the code.
  • Only Sonic & Knuckles stages appear in the demo reel, and in his demos, Sonic isn't accompanied by Tails.
  • Tails also does not appear in Sonic's continue screen.
  • Star posts will only take you to the slot machine and rolling jump bonus stages.
  • The starting positions for Mushroom Hill Zone 1 are adjusted to skip over the first special stage ring.
  • There's only a single set of special stages, since the other one is on the Sonic 3 cartridge. As such, special stage rings will never take you to Hidden Palace, and thus the Super Emeralds are inaccessible.

Interestingly, the scenes added to the ending sequence when you clear the game with all Super Emeralds do not check the flag at all. This includes the scene where Knuckles sees Sonic and Tails off, which is set in Angel Island Zone!


Beyond that, there is a surprising amount of work to prevent you from accessing Sonic 3 content using the level select:
  • If either the Tails alone or the Sonic and Tails player option is chosen, the player mode is set to Sonic alone.
  • If a Sonic 3 level is selected, the current level is forced to Mushroom Hill Zone 1.
  • Even if the first level you select is a special stage, the saved current level is also forced to Mushroom Hill 1, to prevent you from getting sent to Angel Island Zone afterwards.
  • Finally, there's no debug cheat, which prevents you from using an obscure feature that allows the player to select which special stage to play by setting the Sound Test option to the intended stage number, and then holding down the A button during the fade-in.

However, my favorite feature definitely has to be Knuckles' intro sequence, which is a perfect tie-in with Sonic and Tails' post-credits scene when the game is cleared with all Chaos Emeralds. It explains the motivation behind Knuckles' story and makes it abundantly clear that it takes place after Sonic's.


So, next time you feel like playing Sonic 3 & Knuckles, consider playing Sonic 3 and Sonic & Knuckles separately, and keep an eye out for some of these features.

That's it for my series on Lock-on Technology! I have no clue what I'm gonna write about tomorrow.

Tuesday, October 17, 2017

Why no Knuckles in Sonic 1?

When Sonic 1 is locked on to the Sonic & Knuckles cartridge, rather than producing "Knuckles in Sonic 1" as one might expect, we are instead shown the following screen:


Of course, this is actually a splash screen for the "Blue Sphere" bonus game, but more to the point, it suggests there is NO WAY to combine the two games into one, as was previously done with Sonic 2. Is there a technical reason for this?

Takashi Iizuka, senior game designer for Sonic 3 and Sonic & Knuckles, says it's because Sonic 1's level design wasn't conductive to using Knuckles' abilities:
We realized that the construction of the world was really made for [Sonic] and when you started putting other characters into that world, it didn't really work [...] we didn't want people to have a bad experience, so we put them into the special stages.

For Sonic 2, the maps were made so that when you fly around as Super Sonic, you can fly around pretty easily [...] The maps are pretty tall in construction, so because of that we were able to fit Knuckles in and play around with him.
He's not wrong, but I still find that a flimsy excuse. After all, the levels in Sonic 2 are at times not very conductive either, but the developers found a way to work around those issues regardless. Yes, the presence of Super Sonic was a major catalyst in making Knuckles in Sonic 2 possible, but not in the manner described.

I mentioned before how in Sonic 2, the blue colors in the player palette aren't safe for use by anything other than Sonic, because when Sonic turns into Super Sonic, anything using those colors will also start glowing bright yellow.

Super Sonic didn't exist in Sonic 1, though, and as it turns out, quite a few different objects make use of the blue colors, including bumpers, buttons, item boxes and almost every kind of enemy. When Knuckles is introduced to the game, his palette corrupts the appearance of all these objects.


In particular, both Spring Yard Zone and the special stage contain level blocks which are rendered using more than one palette while sharing the same art. The visual integrity of these elements is completely destroyed once the uniform blue gradient is replaced by three shades of red and one shade of green.


So apart from the palette, are there any other issues? Not really. Back in September 2005, Stealth, aka Simon Thomley of Sonic Mania fame, released his Knuckles in Sonic 1 hack, which was considered the "Holy Grail of ROM Hacking" at the time and one of the first ROM hacks to be made through use of a split disassembly.

In this hack, Stealth avoids the palette issues entirely by coercing Knuckles' sprites into the Sonic 1 palette, rather than the other way around. The result is that while Knuckles looks a bit off, the rest of the game remains safely unaltered.

It doesn't even crash when you glide into a conveyor belt!


As a final note, since Stealth ported the Knuckles object from Knuckles in Sonic 2, the same behavioral quirks from the Sonic 2 object are present in this hack. These quirks would later carry over to the Sonic 1 and Sonic 2 mobile remakes, as Stealth would base the Retro Engine implementation on his tested, well-documented code from ten years prior.

Monday, October 16, 2017

Knuckles in Sonic 2: special stage sprites

Just as a quick aside: much like his running animation in Sonic 3, Knuckles' Sonic 2 special stage sprites were created by recoloring Sonic's sprites, then pasting a Knuckles head on top. More specifically, the head from his Sonic 3 special stage sprites, which is why the perspective looks a bit off.


The finalized sprite notoriously gives Knuckles tan arms, but don't be fooled into thinking this is noteworthy. Everybody has tan arms in the Sonic 2 special stage!


Similarly, Knuckles' appearance in the Sonic 2 title screen is based off of his 3D render from the Sonic & Knuckles title:


Note the incorrect transparency on the left fist in both games. Interestingly, the Sonic 2 title screen has more frames of animation than Sonic & Knuckles. I wonder if they were made specifically for Knuckles in Sonic 2, or are they evidence of a longer, scrapped Sonic & Knuckles title animation?

Friday, October 13, 2017

Knuckles in Sonic 2: player palette

Even after reordering Knuckles' colors to match Sonic 2's player palette, some of Sonic 2's graphics come out looking a bit weird, mainly due to the introduction of green in the middle of a previously uniform gradient. For instance, the barrier graphics end up looking disfigured, like so:


The solution is to include a replacement art file within the Knuckles in Sonic 2 patch ROM. Interestingly, the developers chose to make the replacement barrier grey; possibly a red barrier looked too dark.


The patch ROM also includes replacement art for the invincibility stars, as well as item box icons for either power-up:


So why is the green color in the middle of the gradient? My first guess was so that it lines up with the color used for the title card backgrounds, but it turns out that's wrong: without any modification, title cards would use a different color:


Instead, what I found was that the game uses animal graphics from the Sonic 2 cartridge, and the green color seems to have been purposely lined up so it causes minimal disturbance to the Flicky sprite:


Flickies aren't the only sprites without replacement art. As I showed off last time, the water sprites in Aquatic Ruin Zone are rendered using Sonic's blues in Sonic 2, translating into a red and green mess once Knuckles' palette is applied:


However, this was already a problem in Sonic 2, because the blue colors in the player palette aren't safe to use. When Sonic turns into Super Sonic, anything using those colors will also start glowing bright yellow:


Finally, let's look at the lives icon. This one is interesting because everything was remapped to the player palette, even the text which normally uses the ring colors on the enemy palette:


Note how the text also retains the white shine characteristic of Sonic 3's HUD. This leads me to believe that the palette conversions were all done by a programmer, without using graphics editing tools. Note additionally how Knuckles' head in Sonic 2 has a rougher appearance; yet another glimpse into the Knuckles of days past.

Thursday, October 12, 2017

Knuckles in Sonic 2: player sprites

As we've seen before, when playing as Knuckles, a different palette is loaded into the player palette slot. The only real difference between it and the regular Sonic palette is that Sonic's three blues are replaced with Knuckles' three reds:


Now, in order to take advantage of DMA, all player sprites must be stored uncompressed, which takes up a lot of ROM space. In fact, Knuckles' sprites alone use up about 1 of the 16 megabits in the Sonic & Knuckles ROM! That's half the total capacity of the Knuckles in Sonic 2 patch ROM!

Obviously, we would like to reuse Knuckles' sprites from the Sonic & Knuckles ROM in Sonic 2. However, therein lies a problem: although most of them are either similar or exactly the same, the order of the colors in Sonic 2's player palette is completely different from the one in Sonic & Knuckles:


Unfortunately, we can't just apply the Sonic & Knuckles palette directly because a lot of graphics depend on the Sonic 2 colors. For example, all the enemies in Emerald Hill Zone are rendered exclusively using the player palette:


So what's the solution?

Instead of DMAing Knuckles' sprites directly from ROM to VRAM, we first run them through a conversion routine which reads each tile from ROM, swaps all the color indices around to fit the Sonic 2 palette, and writes them to RAM. Finally, the fully-converted sprite then is DMA'd from RAM to VRAM.

The code for this can be found in the LoadKnucklesDynPLC routine of the Knuckles in Sonic 2 disassembly. It uses a lookup table to map each of the 256 byte values that may show up in the source data to their corresponding value once the Sonic 2 palette is applied:
ArtConvTable:   dc.b $00,$06,$05,$03,$02,$04,$0C,$0D,$0E,$0F,$0A,$0B,$07,$08,$09,$01
                dc.b $60,$66,$65,$63,$62,$64,$6C,$6D,$6E,$6F,$6A,$6B,$67,$68,$69,$61
                dc.b $50,$56,$55,$53,$52,$54,$5C,$5D,$5E,$5F,$5A,$5B,$57,$58,$59,$51
                dc.b $30,$36,$35,$33,$32,$34,$3C,$3D,$3E,$3F,$3A,$3B,$37,$38,$39,$31
                dc.b $20,$26,$25,$23,$22,$24,$2C,$2D,$2E,$2F,$2A,$2B,$27,$28,$29,$21
                dc.b $40,$46,$45,$43,$42,$44,$4C,$4D,$4E,$4F,$4A,$4B,$47,$48,$49,$41
                dc.b $C0,$C6,$C5,$C3,$C2,$C4,$CC,$CD,$CE,$CF,$CA,$CB,$C7,$C8,$C9,$C1
                dc.b $D0,$D6,$D5,$D3,$D2,$D4,$DC,$DD,$DE,$DF,$DA,$DB,$D7,$D8,$D9,$D1
                dc.b $E0,$E6,$E5,$E3,$E2,$E4,$EC,$ED,$EE,$EF,$EA,$EB,$E7,$E8,$E9,$E1
                dc.b $F0,$F6,$F5,$F3,$F2,$F4,$FC,$FD,$FE,$FF,$FA,$FB,$F7,$F8,$F9,$F1
                dc.b $A0,$A6,$A5,$A3,$A2,$A4,$AC,$AD,$AE,$AF,$AA,$AB,$A7,$A8,$A9,$A1
                dc.b $B0,$B6,$B5,$B3,$B2,$B4,$BC,$BD,$BE,$BF,$BA,$BB,$B7,$B8,$B9,$B1
                dc.b $70,$76,$75,$73,$72,$74,$7C,$7D,$7E,$7F,$7A,$7B,$77,$78,$79,$71
                dc.b $80,$86,$85,$83,$82,$84,$8C,$8D,$8E,$8F,$8A,$8B,$87,$88,$89,$81
                dc.b $90,$96,$95,$93,$92,$94,$9C,$9D,$9E,$9F,$9A,$9B,$97,$98,$99,$91
                dc.b $10,$16,$15,$13,$12,$14,$1C,$1D,$1E,$1F,$1A,$1B,$17,$18,$19,$11
You might recall how back when the earth was still flat, I mentioned tiles are made up of 4-bit color indices. This means each byte actually represents two separate pixels. As such, the actual pixel conversion function can be seen on the left nybble when going down the table, and the right nybble when going across.

All that's left is to apply the same procedure as in Sonic & Knuckles: take the regular player palette and replace Sonic's four blues with Knuckles' three reds, as well as the green of his socks:


Next time, we'll finish up our look at Knuckles in Sonic 2 by looking at the consequences of modifying those four colors, and the graphical replacements that came as a result.