Crystal Caves Map Format
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.
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|
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.
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.
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()
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!)