Crystal Caves Map Format
| Format type | Map/level |
|---|---|
| Map type | 2D tile-based |
| Layer count | 1 |
| Tile size (pixels) | 16×16 |
| Viewport (pixels) | 320×192 |
| Games |
The Crystal Caves Map Format describes a particular section of data within the main Crystal Caves executable file, where the game levels are stored. Each level is 40 tiles wide and a varying number of tiles high.
Location
The levels are stored inside the executable. To view them, the executable must be decompressed (UNP'd; UNLZEXE works fine for this.) After decompression, the levels for episodes 1 and 2 are stored at offset 0x8C30 (seg000:7332) or 0x8CE0 (depending on the decompression utility) and take up just under 17kB of space. The levels for episode 3 are stored at offset 0x8F24 in the UNLZEXE'd executable.
There are 19 levels, including the main map and two story levels (used at the start and end of the game.) Each level is stored in order, beginning with the introduction/story, finale/story, main map, then level 1, 2, 3, etc.
The number of rows for each map is hard-coded into the executable as machine instructions, not as an array of heights for each level. In addition, some maps can use data from other maps (in most cases the first or the last row) or use two data-rows to produce a single row on the map. Practically, this makes it very hard to increase a level's height and causes a lot of restrictions to modifying the maps. The levels (not counting Intro and Finale) are all sized 40 x 24 tiles and it is believed that each level uses 24 rows of data, with the following exceptions:
| Level | Rows of data |
|---|---|
| Intro | 5 |
| Finale | 6 |
| Main map | 25 |
| Level 7 | 23 |
| Level 8 | 23 |
| Level 14 | 23 |
Format
Each map is made up of a variable number of Pascal-style strings, one for each row. This string format contains no terminating 0x00, but rather stores the string length in the first byte. Thus each row of each level begins with the byte 0x28 (decimal 40, the map width.) Each subsequent byte in the string represents a single 16x16 tile.
Map codes
Unfortunately map codes cannot be converted into tileset indices automatically. This is because the maps were almost certainly designed by hand in a hex editor, without the use of a specific level editing program. Thus the codes were chosen to make some sense to a human, and the game uses some logic to convert the codes into tiles when levels are loaded.
This makes it quite challenging to write a level editor, as despite the map format containing one byte for each tile in the map, many bytes affect the surrounding tiles. For example the "[" character denotes a sign, and if it is followed by a "d" then the two cells will be occupied by a "danger" sign. But if the character following "[" is a "g", a 3×2 cell green box with a yellow border will be inserted instead, assuming the remaining area in the 3×2 space is filled with the letter "n" (which is widely used as a generic "show next tile" code.)
The tiles to use for platforms in a level (including the colour of steel girders and other elements) are set outside the level file in an unknown location. The level codes are relative references to this base value. This means a level can be easily changed from having blue platforms to red ones, brown ones, or blue rocks instead, without changing any level codes in the file.
Unfortunately it is not known where these values are actually set, so for the time being each level is stuck with whatever structural tileset it has in the original game.
BlitzMax Code
This is the code of a very simple map viewer for Crystal Caves. You need to convert the tile graphics from CC1.GFX to PNG with Wombat and UNLZEXE the executable to use this code.
'Crystal Caves Map Viewer
'by K1n9_Duk3
SuperStrict
Local TileImg:TImage = LoadAnimImage("cc1.gfx.png", 16, 16, 0, 1150)
Local in:TStream = ReadFile("CC1-UNLZ.EXE")
Local Maps:Byte[800, 40]
Local MaxY:Int
If in
SeekStream(in, $8ce0) 'CC1 & CC2
' SeekStream(in, $8f24) 'CC3
Local y:Int
Repeat
If ReadByte(in) <> 40 Then Exit
Local s$ = ""
For Local x:Int = 0 Until 40
Local b:Byte = ReadByte(in)
Maps[y, x] = b
Next
y :+ 1
MaxY = y
Forever
CloseFile(in)
Else
End
EndIf
AppTitle = "Crystal Caves Map Viewer"
Graphics 640, 480
SetClsColor(128, 128, 128)
Local CamY:Int
Local Textmode:Byte
Repeat
Cls
For Local y:Int = 0 Until 30
For Local x:Int = 0 Until 40
Local MapTile:Byte = Maps[y+CamY, x]
Local TileIndex:Int = -1
Select MapTile
'Crystals (indices for CC1):
Case $52 TileIndex = 600
Case $2B TileIndex = 601
Case $62 TileIndex = 602
Case $63 TileIndex = 603
'Walls (indices for magenta walls):
Case $72 TileIndex = 1100
Case $74 TileIndex = 1101
Case $79 TileIndex = 1102
Case $66 TileIndex = 1104
Case $67 TileIndex = 1105
Case $68 TileIndex = 1106
Case $34 TileIndex = 1108
Case $35 TileIndex = 1109
Case $36 TileIndex = 1110
'Metal beams (indices for blue beams):
Case $44 TileIndex = 953
Case $64 TileIndex = 954
Case $98 TileIndex = 953 'with hidden crystal
Case $99 TileIndex = 954 'with hidden crystal
Case $9A TileIndex = 955 'with hidden crystal
'Small platform (blue index):
Case $5F TileIndex = 950
'Other Stuff:
Case $21 TileIndex = 650
Case $22 TileIndex = 630
Case $23 TileIndex = 124
Case $24 TileIndex = 860
Case $25 TileIndex = 539
Case $26 TileIndex = 662
Case $28 TileIndex = 184
Case $29 TileIndex = 185
Case $2A TileIndex = 105
Case $2C TileIndex = 537
Case $2D TileIndex = 536
Case $2E TileIndex = 538
Case $2F TileIndex = 480
Case $30 TileIndex = 43
Case $38 TileIndex = 34
Case $39 TileIndex = 96
Case $3A TileIndex = 629
Case $3D TileIndex = 689
Case $3F TileIndex = 50
Case $41 TileIndex = 882
Case $42 TileIndex = 6
Case $45 TileIndex = 680
Case $46 TileIndex = 470
Case $47 TileIndex = 298
Case $48 TileIndex = 590
Case $49 TileIndex = 233
Case $4A TileIndex = 453
Case $4B TileIndex = 1050
Case $4C TileIndex = 1051
Case $4D TileIndex = 300
Case $53 TileIndex = 154
Case $56, $D7, $D6 TileIndex = 594
Case $57 TileIndex = 150
Case $58 TileIndex = 562
Case $59 TileIndex = 250
Case $5D TileIndex = 299
Case $5E TileIndex = 201
Case $61, $71, $82 TileIndex = 560
Case $69 TileIndex = 674
Case $6A TileIndex = 605
Case $6B TileIndex = 1052
Case $6C TileIndex = 1053
Case $6F TileIndex = 100
Case $70 TileIndex = 604
Case $73, $77, $84 TileIndex = 559
Case $76 TileIndex = 580
Case $78 TileIndex = 12
Case $7C TileIndex = 184
Case $7E TileIndex = 212
Case $85
TileIndex = 427
If Maps[y+CamY+1, x] <> $85 Then TileIndex = 431
Case $86
TileIndex = 422
If Maps[y+CamY+1, x] <> $86 Then TileIndex = 423
Case $87
TileIndex = 0
If Maps[y+CamY+1, x] <> $87 Then TileIndex = 4
Case $88
TileIndex = 1
If Maps[y+CamY+1, x] <> $88 Then TileIndex = 5
Case $89 TileIndex = 558
Case $8A TileIndex = 554
Case $8B TileIndex = 3
Case $8C TileIndex = 587
Case $90 TileIndex = 386
Case $91 TileIndex = 499
Case $92 TileIndex = 476
Case $93 TileIndex = 477
Case $94 TileIndex = 378
Case $95 TileIndex = 379
Case $A0 TileIndex = 416
Case $A1 TileIndex = 420
Case $A2 TileIndex = 418
Case $A3 TileIndex = 424
Case $A4 TileIndex = 426
Case $A5 TileIndex = 425
Case $A6 TileIndex = 414
Case $A7 TileIndex = 649
Case $A8 TileIndex = 643
Case $A9 TileIndex = 646
Case $AA TileIndex = 648
Case $AB TileIndex = 644
Case $AC TileIndex = 645
Case $B0 TileIndex = 2
Case $B1 TileIndex = 598
Case $B2 TileIndex = 586
Case $B3 TileIndex = 856
Case $BA TileIndex = 583
Case $BB TileIndex = 588
Case $BC TileIndex = 589
Case $BD TileIndex = 579
Case $BE TileIndex = 578
Case $BF TileIndex = 540
Case $C0 TileIndex = 544
Case $C1 TileIndex = 546
Case $C2 TileIndex = 547
Case $C3 TileIndex = 1057
Case $C4 TileIndex = 1061
Case $C6 TileIndex = 442
Case $CA TileIndex = 584
Case $CB TileIndex = 582
Case $C7 TileIndex = 443
Case $CD TileIndex = 590
Case $CE TileIndex = 1056
Case $CF TileIndex = 543
Case $D0 TileIndex = 557
Case $D1 TileIndex = 542
Case $D5 TileIndex = 639
Case $D8 TileIndex = 581
Case $D9 TileIndex = 545
Case $DA TileIndex = 541
Case $E7 TileIndex = 548
Case $E8 TileIndex = 394
Case $E9 TileIndex = 395
Case $EA TileIndex = 396
Case $EB TileIndex = 397
Case $EC TileIndex = 398
Case $ED TileIndex = 399
Case $F0 TileIndex = 852
Case $F3 TileIndex = 1044
Case $F4 TileIndex = 8
Case $F5 TileIndex = 10
Case $F6 TileIndex = 182
Case $F7 TileIndex = 183
Case $F9 TileIndex = 553
Case $FA TileIndex = 561
Case $FB TileIndex = 585
Case $FC TileIndex = 498
Case $FD TileIndex = 499
Case $FE TileIndex = 476
EndSelect
'Don't draw after a $5B value:
If Maps[y+CamY, x-1] = $5B Then TileIndex = -1
If TileImg And Not Textmode And MapTile = $C5
DrawImage(TileImg, x*16, y*16, 536)
DrawImage(TileImg, x*16, y*16, 539)
ElseIf TileImg And Not Textmode And TileIndex >= 0
DrawImage(TileImg, x*16, y*16, TileIndex)
ElseIf MapTile <> $20
DrawText(Hex(MapTile)[6..], x*16, y*16+3)
EndIf
Next
Next
Flip
If KeyDown(KEY_DOWN)
CamY :+ 1
If CamY > MaxY-30 Then CamY = MaxY-30
EndIf
If KeyDown(KEY_UP)
CamY :- 1
If CamY < 0 Then CamY = 0
EndIf
If KeyHit(KEY_TAB)
Textmode = Not Textmode
EndIf
Until KeyHit(KEY_ESCAPE) Or AppTerminate()
Credits
The location of the map data was discovered by Lemm. If you find this information helpful in a project you're working on, please give credit where credit is due. (A link back to this wiki would be nice too!)