Rather than being made directly out of tiles, Sonic levels are built out of larger. precomposed tile patterns which are progressively assembled in order to generate the full layout. Specifically, combining four tiles makes a 16x16 "block", and combining 64 blocks makes a 128x128 "chunk". Chunks are then placed along a grid, forming the level's layout.
The array which holds tile, block and chunk data for each level is called the "level load block". Here's what it looks like in the current disassembly:
; 1st PLC palette 2nd 8x8 data 2nd 16x16 data 2nd 128x128 data
; 2nd PLC 1st 8x8 data 1st 16x16 data 1st 128x128 data
LevelLoadBlock:
levartptrs $B, $B, $A, AIZ1_8x8_Primary_KosM, AIZ1_8x8_Secondary_KosM, AIZ1_16x16_Primary_Kos, AIZ1_16x16_Secondary_Kos, AIZ1_128x128_Kos, AIZ1_128x128_Kos ; ANGEL ISLAND ZONE ACT 1
levartptrs $C, $C, $B, AIZ2_8x8_Primary_KosM, AIZ2_8x8_Secondary_KosM, AIZ2_16x16_Primary_Kos, AIZ2_16x16_Secondary_Kos, AIZ2_128x128_Kos, AIZ2_128x128_Kos ; ANGEL ISLAND ZONE ACT 2
levartptrs $E, $F, $C, HCZ_8x8_Primary_KosM, HCZ1_8x8_Secondary_KosM, HCZ_16x16_Primary_Kos, HCZ1_16x16_Secondary_Kos, HCZ_128x128_Primary_Kos, HCZ1_128x128_Secondary_Kos ; HYDROCITY ZONE ACT 1
levartptrs $10, $11, $D, HCZ_8x8_Primary_KosM, HCZ2_8x8_Secondary_KosM, HCZ_16x16_Primary_Kos, HCZ2_16x16_Secondary_Kos, HCZ_128x128_Primary_Kos, HCZ2_128x128_Secondary_Kos ; HYDROCITY ZONE ACT 2
...
Yikes, it's a bit packed there, isn't it.
levartptrs is a macro, and here's the definition:
; macro for declaring a "main level load block" (MLLB)
levartptrs macro plc1,plc2,palette,art1,art2,map16x161,map16x162,map128x1281,map128x1282
dc.l (plc1<<24)|art1
dc.l (plc2<<24)|art2
dc.l (palette<<24)|map16x161
dc.l (palette<<24)|map16x162
dc.l map128x1281
dc.l map128x1282
endm
Okay, so each entry is made up of six longwords, each a pointer to some kind of level data. From the top, two pointers to 8x8 tile data, then two pointers to 16x16 block mappings, and finally two pointers to 128x128 chunk mappings.
Additionally, since the Mega Drive's ROM address space only goes up to 0x400000, the top byte in a longword pointer is never set, so we can sneak in some extra pointers: two PLC pointers and a palette pointer (which appears twice but only the first one is used).
Here's what it looks like unpacked:
...
dc.l HCZ_8x8_Primary_KosM+$E000000
dc.l HCZ1_8x8_Secondary_KosM+$F000000
dc.l HCZ_16x16_Primary_Kos+$C000000
dc.l HCZ1_16x16_Secondary_Kos+$C000000
dc.l HCZ_128x128_Primary_Kos
dc.l HCZ1_128x128_Secondary_Kos
...
The reasoning for two of each level data comes from Sonic 3's act system. The second act of each Zone tends to shoot off and do its own thing, often requiring a new set of tiles, blocks and chunks. Almost inevitably though, some elements end up being present in both acts; they're the same Zone, after all.
Rather than duplicating the common bits in both act 1 and act 2's data, a more space-efficient solution is to split them into their own "primary" data, and then each act adds on its own "secondary" data as needed. A similar trick was used in Sonic 2's Hill Top Zone because geez, that stage
did everything first.
The three byte pointers are indexes to arrays elsewhere in the ROM. The palette byte points to an array called, well:
PalPoint:
...
dc.l Pal_HCZ1
dc.w $FC20
dc.w $17
...
Each entry is made up of one longword and two words. The longword is a ROM pointer to the actual color data, the first word is a RAM pointer to the destination address, and the last word is the number of longwords to copy minus 1. Recall that
Mega Drive colors are 16 bits each: each longword holds two colors, making $18 longwords hold $30 colors, which completely fills out three palette lines.
The PLC bytes point to the PLC offset array, which is itself a pointer array that points to individual PLCs:
Offs_PLC:
...
dc.w PLC_0E-Offs_PLC ; HCZ 1 PLC 1
dc.w PLC_0F-Offs_PLC ; HCZ 1 PLC 2
...
Here's what a PLC looks like:
PLC_0E: plrlistheader
plreq $45C, ArtNem_Bubbles
plreq $3CA, ArtNem_HCZMisc
plreq $426, ArtNem_HCZButton
plreq $37A, ArtNem_HCZWaterRush
plreq $42E, ArtNem_HCZWaveSplash
plreq $43E, ArtNem_HCZSpikeBall
PLC_0E_End
PLC_0F: plrlistheader
plreq $44C, ArtNem_HCZDragonfly
PLC_0F_End
Oh great, more macros. Can I just take a minute to ask, does anybody actually like these? I'm not even going to bother with the macro definition; it's gibberish for all I can tell. You'll just have to believe me when I say it unpacks to this:
PLC_0E:
dc.w 5
dc.l ArtNem_Bubbles
dc.w $8B80
dc.l ArtNem_HCZMisc
dc.w $7940
dc.l ArtNem_HCZButton
dc.w $84C0
dc.l ArtNem_HCZWaterRush
dc.w $6F40
dc.l ArtNem_HCZWaveSplash
dc.w $85C0
dc.l ArtNem_HCZSpikeBall
dc.w $87C0
PLC_0F:
dc.w 0
dc.l ArtNem_HCZDragonfly
dc.w $8980
PLCs, or Pattern Load Cues, are essentially pairs of pointers: the first pointer, a longword, is a pointer to graphic data in ROM; the second pointer, a word, is the destination address in VRAM. The first word in each PLC is the number of entries minus 1.
Sonic 3 loads most of its sprite graphics through PLCs. They allow graphics to be arranged and reused between levels, allowing elements to be shared between both acts of a Zone, or used globally such as spikes and springs.
Next time, we'll look at one way PLCs can go very, very wrong.