Super Mario Kart
My collection of SMK data. Teal represents RAM addresses and green represents headerless ROM addresses.
I believe the same compression is used for just about everything. Tiles, tilemaps, palettes and the like. RLE and LZ functions are included in it.
When a control byte is loaded, the top 3 bits are examined. Out of 8 variables potential, 7 of these carry regular compression functions with a similar format. This format consists of the top 3 bits holding the function code and the low 5 bits serving as a byte count. The counter simply defines how many bytes are to be loaded. The byte count makes no exceptions for functions that work with 2 bytes at a time. It'll immediately stop writing bytes when the count reaches zero. A control byte of FF signals the end of the compressed data.
One thing to note is that the counter always gets incremented before it's used. Here's a list of the function codes. The data comes directly after the control byte unless additional parameters are used which change the reading address.
0: Reads an amount of bytes from ROM in sequence stores them in the same sequence.
1: Reads one byte from ROM and continuously stores the same byte in sequence.
2: Reads two bytes from ROM and continously stores the same two bytes in sequence.
3: Reads one byte from ROM and continously stores it, but the byte to write is incremented after every write.
The below work with additional paramters which change the reading address.
FFFCCCCC AAAAAAAA AAAAAAAA
4: Reads two bytes consisting of a pointer to a previously written address. Bytes are sequentially read from the supplied reading address and stored in sequence to the target address.
5: Identical to 4, although every byte read is inverted (EOR #$FF) before it is stored. This command doesn't see much use.
This one works by changing the read address, but only 1 paramater byte is included.
6: Reads one byte, which is then subtracted from the current writing address to create a pointer to a previously written address. Bytes are read in sequence from this address and are stored in sequence.
7: This special command causes the function to be re interpreted.
FFF, the function code, operates just like explained above. This simply extends the byte counter to span 10 bits. Data or parameters follow these 2 bytes as normal. One quirk to note is that if the function value is 7, a variation of command 6 is performed. It is similar to the relationship to 4 and 5. Every byte read is inverted before it is stored.
These simply dictate where in track coordinates you're placed at the beginning. These have no effect on the finish line positions. Race and battle modes are handled differently. Here's some relevant addresses.
7E:1018: X coord P1.
7E:101C: Y coord P1.
7E:1118: X coord P2.
7E:111C: Y coord P2.
These have maximum values of 3FFh (relative to top left), so it's essentially 1024x1024 placement. Your position ingame aswell as your miniature kart in the bottom half of the screen is calculated from these. Battle mode is handled a bit differently. The table format is simple. One set is shared among 3 courses and 1 for the other.
Bytes 1-2: X coord P2
Bytes 3-4: Y coord P2
Bytes 5-6: X coord P1
Bytes 7-8: Y coord P1
18BFF: Battle Course 1, 2 and 4
18C03: Battle Course 3
Grandprix relies solely on one value to place the entire grid and Match Race uses the same value to position both karts. Only battle mode has a set of values for P1 and P2.
Bytes 1-2: X coord
Bytes 3-4: Y coord
The tracks are stored in a base tilemap (what you see in TrackDes) which sometimes have items overlaid onto them. Coins, Q blocks, zippers and a few jumpers are a few examples. Stuff that you don't see in Time Trial but are present in the other modes. Firstly, the base map is loaded and if it isn't Time Trial you're playing, some tiles are overwritten with the ones specified in a table.
7F:0000 - 7F:3FFF: This is where the mode 7 tilemap is stored. Pretty sure it's used as a lookup for any events that happen when a tile is touched / run over. It's put into VRAM at load time, but not used to update the surface ingame. There are seperate handlers for that.
7E:1800 - 7E:181F: 32 byte wide buffer for the tilemap grid to be overlaid onto the track.
The main table used for overlays consists of 3 entries per overlay grid.
Byte 1: Lower 6 bits are used as an index into a pointer table, which points to a set of tiles making up the tiles in the grid. Highest 2 bits are an index into a size table. The pointers are 16 bits wide and the working bank is 84h.
Bytes 2, 3: 16 bit index into the mode 7 tilemap RAM region mentioned above. This can also contain FFFF which will end the overlay routine.
4F23D - 4F2BD: Tilemap pointer table for overlay tiles. Indexed by lower 6 bits of byte 1 above. 2 bytes per entry. Working bank is also 84h. When the pointer is loaded, 32 bytes are loaded into the buffer above from the index provided from this table.
The grid of tiles is of a size specified by the highest 2 bits of Byte 1. The maximum valid size is 5x6 / 6x5 (maximum 32 bytes).
4F384 - 4F38B: Size table. 4 entries of 2 bytes each. First byte of each entry is X size, second is Y.
The game uses sizes of 2x2 (Q Blocks, slicks), 3x1, 1x3 (Jump bars, zippers), 5x5 (Coins). The grid of tiles has 4 tilemap bytes used for 2x2 blocks, 25 for 5x5 etc. The grid of tiles is exactly alike to the tileset indexes of the mode 7 tilemap. They're stored row by row. When the code finishes writing a row, it automatically wraps to the next row below the first entry of the previous row. When the tilemap pointer is retrieved, it always loads 32 bytes into the RAM buffer above, although it will be discarded if the size means it won't be used. If a tilemap entry of FFh is loaded, it will skip writing the byte and simply move onto the next. This is how coins use a 5x5 grid and have 'transparent' areas where the original track tilemap is seen.
After a full grid of overlaid tiles is written it moves along to the next 3 bytes in the main table and continues to do so until it encounters FFFF in bytes 2 and 3.
The object data refers to the following.
Basically, sprites that are applied to the track. They all use similar routines and have similar data formats. A maximum of 4 track objects can be on screen at any given time. These clusters of track objects appear / vanish at fixed areas of the track. That is, you can move thwomps from one area to another, and you can see them from a distance when your kart is in their old position. Approach their new positions though, and they'll likely disappear to be replaced by objects closer to their new position.
The format for the objects for ALL objects with exception of Ghost Valley pillars is as follows. There are 2 byte values for the coordinate of each object. The low 7 bits control the X position while the next higher up 7 bits control the Y position. The highest 2 bits should always be clear, since they'll exceed the maximum the calculations expect. The game expands them to 10 bits, so that it is interpreted at 1024x1024, the size of the track.
Ghost Valley uses 8 bit values for X and Y, and thus uses the whole of the 2 bytes to place the pillars. The game partially updates it's clusters, so that the first loaded are replaced with the newest, while in some cases still leaving the middle ones untouched. Every other track I've seen thus far updates the 4 objects all at once. It's also worth noting that Ghost Valley uses a different routine not just for the extra precision, but also that it updates objects every frame even if it's just going to result in the same layout.
One odd twist is that the game adds a constant of 4 to the X and Y coordinates on all tracks except Donut Plains, which adds a constant of 10. These are added after they are shifted to 10 bit values. A likely reason for this is that the ROM values would end up positioning the object on the top left corner of the tile (for 7bit (128x128) precision). The addition of 4 to X and Y would plant the object in the center of the tile (since they're 8x8). The Donut Plains addition of 10 to these values is most likely due to the Moles and their appropriate holes, although the pipes on DP1 are affected just the same way.
Note that while the ranges are 32 bytes each (except Ghost Valley), some are left unused. These are typically set to 00 in the ROM. A routine runs that checks the first cluster bytes for zero, and if it does contain zero, it wraps back to cluster 1.
Here's the offsets for the cups below (headerless). The sections are numbered as to what cluster they belong to. Since Ghost Valley updates partial clusters, I've inserted returns wherever a pillar(s) replaces the first of the appropriate cluster. A new cluster column indicates all 4 objects being reloaded. There are out of order offsets listed there, but that's just how they appear through 1 normal lap of racing.