Saturday, January 26, 2019

Green spheres in special stages, part 3

We have a lot to cover this time as well, so let's get to it. sub_972E is the function responsible for handling collisions between the player and all other special stage objects, and particularly by the time we get to loc_97AA, the d2 register will hold the contents of the layout cell closest to the player's position:
loc_97AA:
    cmpi.b  #2,d2
    bne.s   loc_97C8
    bsr.w   Find_SStageCollisionResponseSlot
    bne.s   loc_97BE
    move.b  #2,(a2)
    move.l  a1,4(a2)

loc_97BE:
    moveq   #$65,d0
    jsr     (Play_Sound_2).l
    rts
The check at the top ensures that the rest of this code only executes when the cell the player is on contains a 2, which as we saw before, corresponds to a blue sphere. This is the blue sphere collision code! So what does it do?

Not much, really. Besides playing the blue sphere sound, all this does is call the Find_SStageCollisionResponseSlot function, which if you happen to have read my previous series on responsive object collision, works quite a bit like the Add_SpriteToCollisionResponseList function, in that it allows the object to store information about the collision that just took place into a list to be processed in the future, this time by the Touch_SSSprites function.

In this case, since there are no SSTs associated with special stage objects, all information must be stored directly into the collision response list. Each entry is eight bytes long, and the code at sub_972E consumes five of them right off the bat: byte 0 is the object's routine, while bytes 4-7 store a RAM pointer to the object's location in the layout.

Touch_SSSprites uses the routine byte to index into the off_9DFC array. Note that since zero denotes an empty slot in the collision response list, this array is actually one-based, so the routine value set by loc_97AA above refers to the second function in the array, not the third.

Which is a good thing, because there are only two functions in the array!
off_9DFC:   dc.l Touch_SSSprites_Ring
            dc.l Touch_SSSprites_BlueSphere
Touch_SSSprites_BlueSphere is a bit more involved than the preceding code, so let's break it up into small parts:
    subq.b  #1,2(a0)
    bpl.s   locret_9E86
    move.b  #9,2(a0)
Right at the start, we run a timer in byte 2 of the collision response slot. This timer is decremented every frame, and it prevents the function from doing anything until the result of the decrement is negative. Since the timer is always zero the first time through this code, we skip the branch and reset the timer to 9 in the process.
    movea.l 4(a0),a1
    cmpi.b  #2,(a1)
    bne.s   loc_9E62
Next, we load the object's location into register a1, and check the contents of that layout cell. If the object at that cell is somehow not a blue sphere, then we jump to loc_9E62. This might not make much sense right now, but for the time being, we skip the branch and continue to the code below.
    bsr.w   sub_9E88
    move.b  #$A,(a1)
    bsr.s   sub_9EBC
    beq.s   locret_9E60
    move.b  #4,(a1)
    clr.l   (a0)
    clr.l   4(a0)

locret_9E60:
    rts
When called, sub_9E88 decrements the remaining sphere count once, and if the count has reached zero, disables the player's ability to jump. Meanwhile, sub_9EBC is responsible for clearing out large groups of blue spheres once they are enclosed by red spheres, and returns a non-zero value if such a closed pattern is found.

Let's go over that within the context of our code. When a closed pattern of red spheres is found, we skip the branch to locret_9E60 and set the object at the current layout cell to 4, which corresponds to a ring. The collision response slot is then cleared out, signaling that we are done processing this sphere.

So what happens when a closed pattern isn't found? Well, right before sub_9EBC is called, the contents of the current layout cell are set to $A, and when we take the branch to locret_9E60, that change sticks -- for the nine frames that we told the timer at the start of the function to wait around for.

But what does a value of $A represent? It's not a red sphere, since as we previously saw, those correspond to a value of 1. The answer lies in the MapPtr_A10A array, which defines the mappings pointer and base VDP pattern for all the objects that can be placed in the special stage layout:
MapPtr_A10A:
    dc.l Map_SStageSphere       ; 0
    dc.l $86800000              ;
    dc.l Map_SStageSphere       ; 1
    dc.l $86800000              ;
    dc.l Map_SStageSphere       ; 2
    dc.l $C6800000              ;
    ...
    dc.l Map_SStageSphere       ; $A
    dc.l $C6800000              ;
As it turns out, object $A is identical to object 2, blue sphere, except for the fact that it is not object 2. That means it is not caught by the collision code at loc_97AA, and therefore not re-added to the collision response list while we're still waiting out the timer on the first one. It also means that once the timer expires, we will take that branch to loc_9E62.

How come we didn't change the blue sphere to a red sphere right away, though? Let's keep reading.
loc_9E62:
    move.b  #0,2(a0)
    move.w  (Special_stage_X_pos).w,d0
    or.w    (Special_stage_Y_pos).w,d0
    andi.w  #$E0,d0
    beq.s   locret_9E86
Here, we take the player's current X and Y positions, and check the three highest bits of their fractional part. If none of those are set, then the function does nothing except clear the timer to ensure it doesn't roll over. This essentially means we're waiting until we're no longer at one of the crossroads in the layout.

You may have guessed it by now, but the reason we're doing this is so we don't run into the red sphere we're trying to place. Once we're in the clear, we can finally write a 1 to the layout and clear out the collision response slot.
    cmpi.b  #$A,(a1)
    bne.s   loc_9E80
    move.b  #1,(a1)

loc_9E80:
    clr.l   (a0)
    clr.l   4(a0)

locret_9E86:
    rts
Note the sanity check at the top: it ensures we don't accidentally place a red sphere over a layout cell which has since been converted to a ring by the closed pattern algorithm in sub_9EBC.

Okay, that was a lot of words, but not much to show for it. In the next and final part, we'll use everything we've learned to write a custom implementation of green spheres using the existing framework.

Tuesday, January 22, 2019

Green spheres in special stages, part 2

Okay, so the first thing we should do is set up a test stage. For the purpose of this series, I'll just be doing small edits to the first Sonic 3 stage. We could edit one of the Sonic & Knuckles stages, but since those are Kosinski-compressed, it would add the extra inconvenience of having to decompress the file before making our changes, and then recompress the file before building and testing the ROM.


The layout for the first Sonic 3 stage can be found in the General/Special Stage/Layout/S3 1.bin file. We can use a tool such as S3SSEdit to add in a suitable formation of yellow spheres, which later will become our green spheres.

However, as soon will be apparent, it is more than pedagogic to open up the layout file in a hex editor and take a look at what's under the covers. See, if you set the row length to exactly 32 bytes...


Holy smokes! That's the first special stage, all right!

So as it happens, special stage layouts work much in the same as regular level layouts do, with each byte in the layout file describing the contents of each space in the level grid. From the screenshot above, we can see that red spheres correspond to a value of 01, blue spheres correspond to a value of 02, and the yellow spheres we added correspond to a value of 05. This will be important later, so just keep it at the back of your mind for now.

Looking closely though, at the very end of the file there are four extra words which do not correspond to any part of the stage's layout. Each of these words is actually copied into its own RAM variable right before the special stage starts, as part of the layout load code at sub_85B0:
    move.w  (a2)+,(Special_stage_angle).w
    move.w  (a2)+,(Special_stage_X_pos).w
    move.w  (a2)+,(Special_stage_Y_pos).w
    move.w  (a2)+,(Special_stage_rings_left).w
The fourth word is the number of rings necessary to obtain a perfect bonus. Four groups of blue spheres × 16 spheres each = 64 = $40, so that looks about right. Note that the number of blue spheres necessary to clear the stage is not stored; we'll get back to this soon.

The second and third words set the player's starting position on the level grid. These are fixed-point numbers, with the high byte representing the integer part and the low byte representing the fractional part, so the values $200, $200 start us off at point (2,2) on the grid, with (0,0) being the northwest (top-left) corner.

The first word is interesting. It defines the player's starting direction along the level grid, and a key of valid values can be found in sonic3k.constants.asm:
Special_stage_angle =           ramaddr( $FFFFE426 ) ; byte ; $00 = north, $40 = west, $80 = south, $C0 = east

A value of $80 represents a southward-facing direction (down), which lines up with what we see both in S3SSEdit and in-game... but wait a second, it says right there that Special_stage_angle is a byte!

This is not an oversight: if you go through the entire code, you'll find that every reference to the Special_stage_angle variable is a byte access, except for the initial write from the layout seen above. We have an entire byte of RAM that is loaded from the layout file and then left completely unused. Let's go ahead and label that byte:
Special_stage_angle =           ramaddr( $FFFFE426 ) ; byte ; $00 = north, $40 = west, $80 = south, $C0 = east
Special_stage_green_spheres =   ramaddr( $FFFFE427 ) ; byte

We can now flag individual special stages as green sphere stages by setting byte $401 of their layout file to a non-zero value, and then check the Special_stage_green_spheres flag anywhere in the code and branch to custom logic when relevant. Let's set that byte in our layout, build the ROM and confirm that everything still works properly.


Alright, let's finally start coding stuff. Two things stick out to me: the first is that our green spheres aren't green (laughs). We can fix this by adding the following code at loc_82A6, directly after the call to sub_85B0:
    bsr.w   sub_85B0
    tst.b   (Special_stage_green_spheres).w
    beq.s   loc_82BE
    lea     (Target_palette_line_4+4).w,a1
    move.l  #$0C600A0,d0
    move.l  d0,(a1)+
    move.l  #$0600020,(a1)+
    move.l  d0,(a1)+

loc_82BE:
    lea     ($FFFF5500).l,a1
Let's make sure we understand what we're doing here. First, we load the address of the first yellow sphere color into register a1. The yellow colors start two entries into the fourth palette line, and each Mega Drive color is two bytes long, so that comes to four bytes after Target_palette_line_4.


Next, we load the colors $0C6, $0A0 into the d0 register and write them over the first two yellow colors, incrementing register a1 in the process. We then write the colors $060, $020 over the two middle yellows, increment a1 again, and close it out by writing the colors $0C6, $0A0 from d0 over the last two yellows as well.

The second thing that stuck out to me, which admittedly isn't obvious without a comparison screenshot, is that the total blue sphere count is now incorrect. Well I mean, it's correct right now since green spheres are still springs, but that's going to change once we start turning them into blue spheres on the fly.


So how is the total sphere count determined? The code responsible for this is at sub_9EA0:
sub_9EA0:
    lea     (Plane_buffer).w,a3
    moveq   #0,d1
    move.w  #$3FF,d0

loc_9EAA:
    cmpi.b  #2,(a3)+
    bne.s   loc_9EB2
    addq.w  #1,d1

loc_9EB2:
    dbf     d0,loc_9EAA
    move.w  d1,(Special_stage_spheres_left).w
    rts
Quite plainly, this function loops through each byte in the special stage layout, and increments register d1 every time it finds a 2, which as we saw before, corresponds to a blue sphere. We'll extend this by also incrementing d1 whenever we find a 5 (yellow sphere), but only if the stage has been flagged as a green sphere stage:
loc_9EAA:
    move.b  (a3)+,d2
    cmpi.b  #2,d2
    beq.s   loc_9EB0
    tst.b   (Special_stage_green_spheres).w
    beq.s   loc_9EB2
    cmpi.b  #5,d2
    bne.s   loc_9EB2

loc_9EB0:
    addq.w  #1,d1
Build the ROM, run it and verify that the total sphere count is back to 102:


Alright, we've got the look down, so next time we'll jump right into what happens when a blue sphere is touched, and see how we can adapt that behavior for green spheres. See you there!

Monday, January 21, 2019

Green spheres in special stages, part 1

Man, I am just crazy good at updating this blog, aren't I?

First, some housekeeping. Back in October, I finally finished the SonLVL object definitions; you can now find them both in the S&K disasm repo and alongside the main SonLVL download. I then used them to produce a set of full-sized level maps for both Sonic 3 and Sonic & Knuckles, which you can find at the Sonic Retro wiki.

I soon got back to working on my hack as had I longed to, and have been semi-regularly posting a dev log to YouTube. It's still not at a point where I'm ready to share all my plans for it, but you'll get the basic idea from the videos alone.


Anyway, a while ago, Sonic Retro user Travelsonic posted a thread asking for documentation on special stage object behavior. I suggested I could blog a bit about the process of adding green spheres to the special stage, and here we are. It only took me two months to get around to it!

So, what are green spheres, exactly? Green spheres are one of the two new sphere types Stealth implemented in his "Blue Spheres 2" concept demo, which eventually made its way into Sonic Mania as a bonus game:


Quite simply, green spheres function as an additional layer on top of blue spheres: just like a blue sphere becomes red when touched, a green sphere first becomes blue, and must then be touched again before it becomes red.

Pink spheres, on the other hand, are two-way teleporters that would be far more difficult to implement, not only due to the programming overhead involved, but also a much more pressing factor: the palette.


As I alluded to in an early post, all four sphere types use the same set of sprites with different color palettes, in order to cut down on VRAM usage. Specifically, the first half of each palette line corresponds to the red, white, blue and yellow spheres respectively, in which the white palette replaces two otherwise duplicate colors with shades of red, revealing a star design that is invisible in every other sphere type.


And therein lies the rub: this design implies that there can only be four different sphere types at play at any given time, which means that if we want to add a new type, it will have to replace one of the existing colors.

Famously, the goal of the special stage is to Get Blue Spheres, so obviously those cannot be replaced. And since blue spheres become red spheres when touched, red spheres cannot be replaced either. The conclusion is that in order to implement green spheres, we must sacrifice either white spheres or yellow spheres.

Over the course of this series, I will show you how to implement green spheres over yellow spheres, though you could easily go the other way with presumably little complications. I will also implement a method of marking individual stages as green sphere stages, which if present enables all the additional logic surrounding green spheres, and if omitted preserves their behavior as ordinary yellow spheres.

Continued in part 2!

Thursday, September 20, 2018

On the subject of bitwise operators in C#

This subject is a bit off-band for the blog, but I figured it could also double as a status update. The first draft of the object definitions is almost complete; only Death Egg Zone remains at the time of writing. After that, I'll probably go over the entire set and make everything a little bit more consistent, add a few more overlays here and there, etc. I'm currently aiming to get everything done early next month, so we'll see how that goes.

I've also made up my mind about what the focus of my hack will be, so I can't wait to jump on that as well. It's going to be a lot of work up front, but I'm hoping the payoff is worth it. Anyway, time for a rant.


As you may or may not be aware, SonLVL is programmed in C#. Due to this, the most powerful way of writing SonLVL object definitions is to just roll your own C# code against SonLVL's public API, which SonLVL then compiles on the fly by calling up the C# compiler at runtime.

This is good! C# is a great programming language, and one which I regularly work with in my day job, so being able to transfer my existing skill set certainly makes it easier on both sides.

Now, the greatest complexity in writing object definitions comes from wrangling subtypes. Apart from the X/Y flip flags, the subtype is the only way of instructing objects to serve up a different appearance or behavior. As such, more often than not, several different properties are packed into the individual bits of the subtype byte. And therein lies the rub: performing bitwise operations in C# is just sad.


Let's take, for example, the Automatic Tunnel object. These are the high speed chutes found in Launch Base Zone and Lava Reef Zone. They have three properties, which are encoded into the subtype as follows:

  • Bits 0-4 are the Path ID, which defines the set of waypoints the player will be sent through.
  • Bit 6 is the Launch flag; if set, the player will keep their momentum at the end of the tunnel.
  • Bit 7 is the Reverse flag; if set, the player will go through the waypoints in reverse order.
  • Bit 5 is unused.

Here's the above information in graphical form, because humans love graphics:
     0  0  0  0  0  0  0  0 

   Reverse     Launch     Path ID
Alright, so now let's say I want to have a property box where the user can change the path ID, without affecting the other flags. Sounds easy enough. Just blank out the path ID bits already in the subtype, truncate the user value to five bits, and join the two together. So let's write that.
    subtype = (subtype & 0xE0) | (value & 0x1F);
Hit compile and... compilation error. An expression of type int cannot be assigned to the variable subtype, which is of type byte. Oh right, the literals 0xE0 and 0x1F are of type int, so the AND operations are lifted to int: both subtype and value get promoted from byte to int and operator &(int a, int b) is called, which itself returns int. The two resulting ints are then ORed together, so the entire expression is of type int, which cannot be assigned to a variable of type byte.

There's actually no way to write a byte literal in C#; you are expected to cast the int literal to byte. The compiler will do the right thing and not insert a conversion operation, but work with byte from the start. So let's write that.
    subtype = (subtype & (byte)0xE0) | (value & (byte)0x1F);
Hit compile, same error. As it turns out...

Pain point #1: There are no bitwise operators defined on byte


It's not the literals, it's the operators! There actually isn't such a thing as byte operator &(byte a, byte b) in C#; they go down to int and that's it. So when we write subtype & (byte)0xE0, the compiler promotes both bytes to int and then calls int operator &(int a, int b), once again resulting in a subexpression of type int.

The same thing goes for the OR operator, so no matter how we slice it, the whole expression will always evaluate to int. So the correct solution is to cast that instead:
    subtype = (byte)((subtype & 0xE0) | (value & 0x1F));
It's already getting hard to read through all the parentheses, but it's only going to get worse.

Pain point #2: Bitwise operations do not return bool


Let's turn our attention to the flags. In the case of the Reverse flag, I want the user value to be a yes/no toggle, so value is a bool. Then, depending on whether the bool is true or not, we set the relevant bit to 1 or 0. Let's write that.
    subtype = (byte)((subtype & 0x7F) | (value ? 0x80 : 0x00));
Alright, relatively painless. But what about the reverse operation, where we look up the current subtype and figure out the current state of the Launch flag? This time we're assigning to value, which is of type bool. So we write
    value = subtype & 0x80;
which again results in a compilation error, this time stating that an expression of type int cannot be assigned to a variable of type bool.

This is because in C#, unlike C and C++ before it, bools are strongly typed. They can only hold the values true and false, which alleviates the situation where 1 and 2 both mean true, but compare differently to one another. But that means there's no quick way to write a bit test in C#; one must append either != 0 or == 0x80, the former a tautology, the latter a repetition.

Now, since the Reverse flag happens to be the most significant bit, we can sidestep the issue by instead writing:
    value = subtype >= 0x80;
But in the case of the Launch flag, imagine my surprise when I write
    value = subtype & 0x40 != 0;
and I get yet another compilation error: operator & cannot be applied to operands of type byte and bool.

Pain point #3: Bitwise operators are also logical operators


If the previous point was to get rid of legacy C bullcrap, then this one enshrines it. Early versions of C did not have the logical operators && and ||, so to combine two or more equality comparisons into a single conditional expression, you would use the bitwise operators & and |, like so:
    if (day == 25 & month == 12) printf("It's Christmas!\n");
In order for this kind of expression to evaluate correctly, bitwise operations were given lower precedence than equality comparisons, so that the program would first check that the day is 25, then that the month is December, before it combines the results and decides whether it's Christmas or not. When bitwise operators were added to the C# specification, their precedence was kept the same, presumably in order to avoid "gotcha" scenarios when porting over legacy C and C++ code.

So above, when we wrote
    value = subtype & 0x40 != 0;
what the compiler actually does is check 0x40 and 0 for equality, and then attempt to combine the result with the value of subtype, which is the complete opposite of what we were trying to accomplish!

The solution is, again, to add more parentheses to the expression:
    value = (subtype & 0x40) != 0;
But here's the kicker: since in C#, equality comparisons result in bool, not int, they had to introduce separate, eager logical operators &(bool a, bool b) and |(bool a, bool b) to go along with the to the existing short-circuiting logical operators &&(bool a, bool b) and ||(bool a, bool b). So they could have avoided this whole disaster by simply giving the eager logical operators a different notation from the bitwise operators! Grrr.

With all that parenthesizing, it's no surprise that we end up with code that looks a little something like this:
properties[2] = new PropertySpec("Launch", typeof(bool), "Extended",
    "If set, the player will launch off at the end of the path.", null,
    (obj) => (obj.SubType & 0x40) != 0,
    (obj, value) => obj.SubType = (byte)((obj.SubType & 0xBF) | ((bool)value ? 0x40 : 0)));

And that's just a little bit sad.

Update 27/02/2020: Eric Lippert expands on the last point over at his own blog. This post was mostly inspired by Eric's writings there and elsewhere on the the Internet, so being able to finally link back is incredibly delightful to me.

Monday, July 9, 2018

An overview of the new API features in SonLVL

Okay, here we go. As promised in my previous post, here's a rundown of all the features MainMemory has graciously added to SonLVL's API in order to support my ongoing object definition adventure.

Debug overlays: Like I hinted at in last week's post, objects now have the option of drawing a secondary sprite, which is rendered with high priority above all regular sprites and level blocks. The most basic use of this feature is to plot out the movement patterns of continuously moving objects, such as floating platforms.


Beyond that, I also made it so that objects which are configured to move to a predetermined position will plot out the location of their collision box at the end of their movement pattern.


Streamlined sprites: Under the hood, the sprite rendering code has been greatly improved, resulting in faster load times and smoother scrolling when compared to previous versions of SonLVL. Sprites are also refreshed more often now, allowing me to do crazy stuff such as have crushing objects automatically detect the floors and ceilings as you drag them around the level.


Depth and priority: SonLVL now considers VDP priority information when rendering the main level view: high priority level blocks will hide any overlapping sprites, unless they are also set to high priority. Objects can now also optionally report the sprite's SST priority value, known here as depth, for proper sorting between sprites.


Extra colors: Four additional color palettes are now available in each level. These are hidden in the editor, but can be used by sprites in order to render objects which normally overwrite one of the palette lines, such as bosses, as well as grant the Knuckles player start its proper coloring.


XML player starts: Player start markers can now be defined using a limited subset of the XML syntax for regular object defintions, allowing for custom poses in each level, as well as composite sprites where necessary.


Layout swap option: This is a big one. It allows level configurations to define a set of layout copy areas in its level layout, and then swap them into the main layout through a menu option. This is absolutely vital while editing LBZ1, but also quite useful in FBZ, and to a lesser extent AIZ1, LBZ2 and SOZ2.


Animated PLC support: Levels can now optionally load blocks of uncompressed art, which are then rendered in place of the placeholder blocks in the main level view, allowing for a level's animated tiles to be rendered as they appear in-game. This is a really big one because it's not useful for just Sonic 3; MainMemory has already gone ahead and added animated tiles to the Sonic 1 and 2 level configurations.


So that's where we stand. The whole thing is still a work in progress -- only the S3 levels are done at the moment. If you're feeling intrepid enough, you can always grab the current stuff from my personal Git repo; constructive feedback is highly appreciated if you have any.

Monday, July 2, 2018

What I've been up to this whole time

It's been a while, huh?

You may be wondering where the hell I've been. Unfortunately, progress on the hack is pretty much where I left it four months ago -- I've produced a couple of assets and had a couple new ideas, no doubt in part due to all the crazy Mania Plus stuff just around the corner -- but I seem to have a knack for getting into digressions which soon become much more work than anyone could have reasonably anticipated.

Case in point: right now, here's what your average Sonic 3 stage looks like when opened up in the SonLVL level editor:


Urgh. A bunch of floating question marks, the actual level layout covered by a brick wall, and to round it out, a couple of placeholder numerals hanging around near the corner.

Now, let's check out what the same section looks like when using my work-in-progress SonLVL configuration files:


OMG there's so much stuff to talk about in this screenshot.

What you're seeing here is the result of a semi-joint venture between the author of SonLVL, MainMemory and myself, with the goal of providing a complete set of SonLVL object definitions for Sonic 3 & Knuckles. Essentially, what that entails is reverse-engineering the original 68k code, and producing accurate representations of all the objects within the level editor.

This process can be further boiled down into three core points:
  1. Identify every object ID used in a given stage, and give each of them human-readable names;
  2. Document the effects of the subtype byte and the X/Y flip flags on the object's behavior and appearance, and provide a reasonable way for the user to view and modify these properties;
  3. Render a visual representation of the object, which should first and foremost be accurate to the object's in-game appearance, and if possible, illustrate the object's movement pattern as defined by its properties.

Let's focus on that last point. SonLVL already has a definition for Sonic 2's invisible block object, which highlights the object's actual size by drawing a yellow box around the rather unhelpful Tails block that appears in-game.


Upon porting this object to the Sonic 3 side of things, I quickly realized that a similar concept could be used to illustrate the alternating movement pattern of objects such as retracting spikes, thus making them stand out from their stationary brethren. Just draw the spikes at their "on" position, and the box at the "off" position!


There were two problems with this idea. Since the box was logically part of the object's sprite, you couldn't draw the spikes without the box, which ruined the map export feature. This wasn't an issue with the invisible block object, since its status as a "debug object" meant it didn't appear in the exported map anyway.

The other problem was that because the sprite was now twice as tall, so were the object's selection bounds!


Yuck!

No, if I was doing this, I was doing it right, which meant that somehow, I had to make SonLVL draw an optional "debug overlay" on top of an object's regular sprite. Luckily, I was already in talks with MainMemory at this point, and little did she know, I was about to make her work on SonLVL more than she had during the entire preceding year.

Next time, I'll go over all of the new features, and how I'm currently using them to make the Sonic 3 object definitions vastly superior to the ones available for previous titles.

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 23, 2018

The many tendrils of a Sonic 3 level, part 2.5

Before we proceed any further in our analysis, a brief digression. On the subject of the Animate_Tiles function, reader Silver Sonic 1992 commented:
Lava Reef zone has some type of dynamically reloading tiles. Is it just garbage data?
I completely missed this the first time around. Amidst all the pointers to null routines, the Offs_AniFunc table actually contains a pointer to a properly-defined animation routine for Lava Reef Zone 1:
                 dc.w AnimateTiles_NULL-Offs_AniFunc
                 dc.w AniPLC_ALZ-Offs_AniFunc
                 dc.w AnimateTiles_LRZ1-Offs_AniFunc
                 dc.w AniPLC_ALZ-Offs_AniFunc
                 dc.w AnimateTiles_NULL-Offs_AniFunc
                 dc.w AniPLC_ALZ-Offs_AniFunc
Notably, apart from some tweaks to the target VRAM offsets to make the routine work also for act 2, as well as the fact that ArtUnc_AniLRZ__BG2 seemingly grew to 1.5x its size sometime after Sonic 3's release, the bulk of the routine is quite similar to the code found in Sonic & Knuckles:
AnimateTiles_LRZ1:                              AnimateTiles_LRZ1:
                                                    move.w  #$6400,d4
                                                    move.w  #$6880,d6
                                                    bra.s   loc_282D0
                                                ; ---------------------------------------------

                                                AnimateTiles_LRZ2:
                                                    move.w  #$6400,d4
                                                    move.w  #$6880,d6

                                                loc_282D0:
    lea     (Anim_Counters).w,a3                    lea     (Anim_Counters).w,a3
    moveq   #0,d0                                   moveq   #0,d0
    move.w  ($FFFFEEE4).w,d0                        move.w  ($FFFFEEE4).w,d0
    sub.w   (Camera_X_pos_BG_copy).w,d0             sub.w   (Camera_X_pos_BG_copy).w,d0
                                                    subq.w  #1,d0
    divu.w  #$30,d0                                 divu.w  #$30,d0
    swap    d0                                      swap    d0
    cmp.b   1(a3),d0                                cmp.b   1(a3),d0
    beq.s   loc_27440                               beq.s   loc_2833C
    move.b  d0,1(a3)                                move.b  d0,1(a3)
    moveq   #0,d1                                   moveq   #0,d1
    move.w  d0,d2                                   move.w  d0,d2
    andi.w  #7,d0                                   andi.w  #7,d0
    lsl.w   #7,d0                                   lsl.w   #7,d0
    move.w  d0,d1                                   move.w  d0,d1
    lsl.w   #3,d0                                   lsl.w   #3,d0
    add.w   d0,d1                                   add.w   d0,d1
    move.l  d1,d5                                   move.l  d1,d5
    andi.w  #$38,d2                                 andi.w  #$38,d2
    move.w  d2,d0                                   move.w  d2,d0
    lsl.w   #3,d2                                   lsl.w   #3,d2
    add.w   d2,d1                                   add.w   d2,d1
    add.w   d2,d2                                   add.w   d2,d2
    add.w   d2,d1                                   add.w   d2,d1
    lsr.w   #1,d0                                   lsr.w   #1,d0
    lea     word_27446(pc,d0.w),a4                  lea     word_2834C(pc,d0.w),a4
    lea     (ArtUnc_AniALZ).l,a0                    lea     (ArtUnc_AniLRZ__BG).l,a0
    move.w  #$6020,d4
    add.l   a0,d1                                   add.l   a0,d1
    move.w  d4,d2                                   move.w  d4,d2
    move.w  (a4)+,d3                                move.w  (a4)+,d3
    add.w   d3,d4                                   add.w   d3,d4
    add.w   d3,d4                                   add.w   d3,d4
    jsr     (Add_To_DMA_Queue).l                    jsr     (Add_To_DMA_Queue).l
    move.l  d5,d1                                   move.l  d5,d1
    add.l   a0,d1                                   add.l   a0,d1
    move.w  d4,d2                                   move.w  d4,d2
    move.w  (a4)+,d3                                move.w  (a4)+,d3
    beq.s   loc_27440                               beq.s   loc_2833C
    jsr     (Add_To_DMA_Queue).l                    jsr     (Add_To_DMA_Queue).l

loc_27440:                                      loc_2833C:
                                                    cmpi.b  #$16,(Current_zone).w
                                                    beq.s   locret_2834A
    addq.w  #2,a3                                   addq.w  #2,a3
    bra.w   loc_2745E                               bra.w   loc_28364
; --------------------------------------------- ; ---------------------------------------------

                                                locret_2834A:
                                                    rts
                                                ; ---------------------------------------------
word_27446:                                     word_2834C:
    dc.w  $240,     0                               dc.w  $240,     0
    dc.w  $1E0,   $60                               dc.w  $1E0,   $60
    dc.w  $180,   $C0                               dc.w  $180,   $C0
    dc.w  $120,  $120                               dc.w  $120,  $120
    dc.w   $C0,  $180                               dc.w   $C0,  $180
    dc.w   $60,  $1E0                               dc.w   $60,  $1E0
; --------------------------------------------- ; ---------------------------------------------

loc_2745E:                                      loc_28364:
    moveq   #0,d0                                   moveq   #0,d0
    move.w  ($FFFFEEE2).w,d0                        move.w  ($FFFFEEE2).w,d0
    sub.w   (Camera_X_pos_BG_copy).w,d0             sub.w   (Camera_X_pos_BG_copy).w,d0
    andi.w  #$1F,d0                                 andi.w  #$1F,d0
    cmp.b   1(a3),d0                                cmp.b   1(a3),d0
    beq.s   locret_274BE                            beq.s   loc_283CC
    move.b  d0,1(a3)                                move.b  d0,1(a3)
    moveq   #0,d1                                   moveq   #0,d1
    move.w  d0,d2                                   move.w  d0,d2
    andi.w  #7,d0                                   andi.w  #7,d0
    lsl.w   #8,d0                                   lsl.w   #7,d0
                                                    move.w  d0,d1
                                                    add.w   d0,d0
                                                    add.w   d1,d0
    move.w  d0,d1                                   move.w  d0,d1
    move.l  d1,d5                                   move.l  d1,d5
    andi.w  #$18,d2                                 andi.w  #$18,d2
    move.w  d2,d0                                   move.w  d2,d0
    lsl.w   #3,d2                                   lsl.w   #2,d2
                                                    add.w   d2,d1
                                                    add.w   d2,d2
    add.w   d2,d1                                   add.w   d2,d1
    lsr.w   #1,d0                                   lsr.w   #1,d0
    lea     word_274C0(pc,d0.w),a4                  lea     word_283D2(pc,d0.w),a4
    lea     (ArtUnc_AniALZ).l,a0                    lea     (ArtUnc_AniLRZ__BG2).l,a0
    move.w  #$64A0,d4                               move.w  d6,d4
    add.l   a0,d1                                   add.l   a0,d1
    move.w  d4,d2                                   move.w  d4,d2
    move.w  (a4)+,d3                                move.w  (a4)+,d3
    add.w   d3,d4                                   add.w   d3,d4
    add.w   d3,d4                                   add.w   d3,d4
    jsr     (Add_To_DMA_Queue).l                    jsr     (Add_To_DMA_Queue).l
    move.l  d5,d1                                   move.l  d5,d1
    add.l   a0,d1                                   add.l   a0,d1
    move.w  d4,d2                                   move.w  d4,d2
    move.w  (a4)+,d3                                move.w  (a4)+,d3
    beq.s   locret_274BE                            beq.s   loc_283CC
    jsr     (Add_To_DMA_Queue).l                    jsr     (Add_To_DMA_Queue).l

locret_274BE:                                   loc_283CC:
                                                    addq.w  #2,a3
    rts                                             bra.w   loc_286E8
; --------------------------------------------- ; ---------------------------------------------
word_274C0:                                     word_283D2:
    dc.w   $80,     0                               dc.w   $C0,     0
    dc.w   $60,   $20                               dc.w   $90,   $30
    dc.w   $40,   $40                               dc.w   $60,   $60
    dc.w   $20,   $60                               dc.w   $30,   $90
; --------------------------------------------- ; ---------------------------------------------
Much like what happened with the level load block though, the uncompressed art used by this routine was completely wiped from the Sonic 3 ROM, leaving the code pointing at whatever data came next. In this case it's ArtUnc_AniALZ, which is uncompressed art normally used by Azure Lake's animation routine.

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?

Sunday, March 11, 2018

The many tendrils of a Sonic 3 level, part 1

Apologies for the radio silence during the past month; I've been absolutely swamped with work and haven't had time for either the blog or the hack.

On the subject of loading Sonic & Knuckles stages in Sonic 3, an anonymous commenter wondered whether the bogus entries in the S3A level load block are enough to make the game crash while loading those levels, to which I answered that yes, that should be quite enough.

However, are they the sole cause of errors in this scenario? Assuming we patch up the level load block to point at valid Kosinski data where appropriate, would those stages then boot up properly? Not necessarily.

First off, as we saw before, the level load block itself contains byte pointers which are used as an index to the PalPoint and Offs_PLC arrays. Obviously, these bytes must correspond to valid entries within those arrays, otherwise we'll once again be attempting to decompress garbage data, or trying to copy nonsensical color values to a random location in the Mega Drive's address space.

Beyond this though, there are several other points in the game where code is executed and data is loaded conditionally depending on which level is being played. These points are scattered throughout the ROM without any structure, which is partially why Sonic 3 has a reputation of being a harder game to hack than previous Sonic titles.

Let's start from the top. At $23CA, we have the AnPal_Load function, which is called once every frame in order to run all of the animated palettes within a stage. To accomplish this, it takes the value of the current zone and act and uses it as an index to the OffsAnPal array, which is itself a list of pointers to smaller routines that then handle all the palette animations specific to that stage:
OffsAnPal:      dc.w AnPal_AIZ1-OffsAnPal
                dc.w AnPal_AIZ2-OffsAnPal
                dc.w AnPal_HCZ1-OffsAnPal
                dc.w AnPal_HCZ2-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_CNZ-OffsAnPal
                dc.w AnPal_CNZ-OffsAnPal
                dc.w AnPal_FBZ-OffsAnPal
                dc.w AnPal_FBZ-OffsAnPal
                dc.w AnPal_ICZ-OffsAnPal
                dc.w AnPal_ICZ-OffsAnPal
                dc.w AnPal_LBZ1-OffsAnPal
                dc.w AnPal_LBZ2-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_LRZ1-OffsAnPal
                dc.w AnPal_LRZ2-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_BPZ-OffsAnPal
                dc.w AnPal_BPZ-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_CGZ-OffsAnPal
                dc.w AnPal_CGZ-OffsAnPal
                dc.w AnPal_EMZ-OffsAnPal
                dc.w AnPal_EMZ-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
                dc.w AnPal_None-OffsAnPal
Interestingly, Flying Battery Zone has its own routine just like in Sonic & Knuckles, except that here it does absolutely nothing. More interestingly, there are also routines defined for both acts of Lava Reef Zone, and although the routine for act 2 is blank, the routine for act 1 is fully functional, and even references some otherwise unused palette data!


The presence of this data, which is identical to the corresponding data in the Sonic & Knuckles ROM, ties squarely into the notion that Lava Reef Zone had already entered production by the time of Sonic 3's release.

Moving right along, at $4680 we have the LevelMusic_Playlist, which as we saw before, dictates which track is played at level load based on the current zone and act. Sonic 3's version of the playlist is identical to the one found in Sonic & Knuckles, except for the following points:

  • The Rolling Jump bonus stage reuses the track for the Gumball bonus stage.
  • Both the ending level slot and the four acts at the end of the level list make use of the special stage track.
  • Sky Sanctuary Zone and Death Egg Zone are bugged. Instead of using the same song for both acts, act 2 of Sky Sanctuary Zone uses the song meant for Death Egg Zone 1, and then both acts of Death Egg Zone use the song meant for Death Egg Zone 2.

At $1A1F4, we find the LevelSizes structure. All of the levels unused in Sonic 3 have the default size of $1000 pixels down and $6000 pixels across. Shortly after, at $1A8A8 we find the LevelResizeArray. Like in Sonic & Knuckles, every stage past Launch Base Zone 2 points at an empty dynamic resize routine.

Then at $26A92, we find the Animate_Tiles function, which is also called once every frame in order to process all the animated elements within a level. Much like the AnPal_Load function, it uses the current zone and act as an index to an array of pointers to smaller routines, which then handle the tile-based animations specific to that level. Interleaved with this array however, is another array containing pointers to the level's "animated PLC", which consists of the ROM address of the uncompressed art, the destination VRAM address, and the duration of each frame of animation.
Offs_AniFunc:   dc.w AnimateTiles_AIZ1-Offs_AniFunc
Offs_AniPLC:    dc.w AniPLC_AIZ1-Offs_AniFunc
                dc.w AnimateTiles_AIZ2-Offs_AniFunc
                dc.w AniPLC_AIZ2-Offs_AniFunc
                dc.w AnimateTiles_HCZ1-Offs_AniFunc
                dc.w AniPLC_HCZ1-Offs_AniFunc
                dc.w AnimateTiles_HCZ2-Offs_AniFunc
                dc.w AniPLC_HCZ2-Offs_AniFunc
                dc.w AnimateTiles_MGZ-Offs_AniFunc
                dc.w AniPLC_MGZ-Offs_AniFunc
                dc.w AnimateTiles_MGZ-Offs_AniFunc
                dc.w AniPLC_MGZ-Offs_AniFunc
                dc.w AnimateTiles_CNZ-Offs_AniFunc
                dc.w AniPLC_CNZ-Offs_AniFunc
                dc.w AnimateTiles_CNZ-Offs_AniFunc
                dc.w AniPLC_CNZ-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ICZ-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ICZ-Offs_AniFunc
                dc.w AnimateTiles_ICZ-Offs_AniFunc
                dc.w AniPLC_ICZ-Offs_AniFunc
                dc.w AnimateTiles_ICZ-Offs_AniFunc
                dc.w AniPLC_ICZ-Offs_AniFunc
                dc.w AnimateTiles_LBZ1-Offs_AniFunc
                dc.w AniPLC_LBZ1-Offs_AniFunc
                dc.w AnimateTiles_LBZ2-Offs_AniFunc
                dc.w AniPLC_LBZ2-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ALZ-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ALZ-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ALZ-Offs_AniFunc
                dc.w AnimateTiles_NULL-Offs_AniFunc
                dc.w AniPLC_ALZ-Offs_AniFunc
                ...
As with the OffsAnPal array, all of the empty slots in the Offs_AniFunc array point to blank, "null" routines. However, much like the level load block, empty slots in the Offs_AniPLC array are filled with pointers to whatever data follows the previous PLC entry. As a result, Flying Battery Zone points to the PLC for Icecap Zone, all of the Sonic & Knuckles levels point to the PLC for Azure Lake, and every level past Desert Palace (the last stage with animated PLCs) points at the end of the animated PLC block.

Okay, but unlike the data mismatch in the level load block, the Azure Lake PLC is obviously valid PLC data, so loading Sonic & Knuckles levels shouldn't cause the game to crash. And even if the PLC data is invalid, such as with the levels that point at the end of the animated PLC block, the fact that the animation routine is blank means the game doesn't do anything with the invalid PLC data.

So far we haven't found anything that might crash the game. In the next post, we'll look at one aspect which could, and in the process, stumble upon an obscure version difference.