Crystal Caves Map Format

From ModdingWiki
Jump to: navigation, search
Crystal Caves Map Format
Crystal Caves Map Format.png
Format typeMap/level
Map type2D tile-based
Layer count1
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!)