SMF map decompiler

SMF map decompiler

Discuss maps & map creation - from concept to execution to the ever elusive release.

Moderator: Moderators

Post Reply
User avatar
Beherith
Posts: 5145
Joined: 26 Oct 2007, 16:21

SMF map decompiler

Post by Beherith »

The following python2.x code completely decompiles a map, it outputs all features, raw heightmap, bmp heightmap, texture, typemap, metal map, grass map, min and max elevations, minimap.

Everything is decompile to be able to recompile an identical copy of the map by just feeding the files into mapconv.

Note the generous use of comments and clean coding style :lol:


Code: Select all

#!/usr/bin/python
# PD license by Beherith
import sys
import struct
import Image

print 'Welcome to the SMF decompiler by Beherith (mysterme@gmail.com). Place this script next to the .smf file and pass the .smf as a command line argument to this script to get it decompiled'

if len(sys.argv)>1:
	print 'Working on:',sys.argv[1]
else:
	exit(1)
SMFHeader_struct= struct.Struct('< 16s i i i i i i i f f i i i i i i i')
'''	char magic[16];      ///< "spring map file\0"
	int version;         ///< Must be 1 for now
	int mapid;           ///< Sort of a GUID of the file, just set to a random value when writing a map

	int mapx;            ///< Must be divisible by 128
	int mapy;            ///< Must be divisible by 128
	int squareSize;      ///< Distance between vertices. Must be 8
	int texelPerSquare;  ///< Number of texels per square, must be 8 for now
	int tilesize;        ///< Number of texels in a tile, must be 32 for now
	float minHeight;     ///< Height value that 0 in the heightmap corresponds to	
	float maxHeight;     ///< Height value that 0xffff in the heightmap corresponds to

	int heightmapPtr;    ///< File offset to elevation data (short int[(mapy+1)*(mapx+1)])
	int typeMapPtr;      ///< File offset to typedata (unsigned char[mapy/2 * mapx/2])
	int tilesPtr;        ///< File offset to tile data (see MapTileHeader)
	int minimapPtr;      ///< File offset to minimap (always 1024*1024 dxt1 compresed data plus 8 mipmap sublevels)
	int metalmapPtr;     ///< File offset to metalmap (unsigned char[mapx/2 * mapy/2])
	int featurePtr;      ///< File offset to feature data (see MapFeatureHeader)

	int numExtraHeaders; ///< Numbers of extra headers following main header
'''
ExtraHeader_struct= struct.Struct('< i i i')
'''	int size; ///< Size of extra header
	int type; ///< Type of extra header
	int extraoffset ; //MISSING FROM DOCS, only exists if type=1 (vegmap)'''
MapTileHeader_struct=struct.Struct('< i i')
'''	int numTileFiles; ///< Number of tile files to read in (usually 1)
	int numTiles;     ///< Total number of tiles'''
MapFeatureHeader_struct=struct.Struct('< i i')
'''	int numFeatureType;
	int numFeatures;'''
	
MapFeatureStruct_struct=struct.Struct('< i f f f f f')
'''int featureType;    ///< Index to one of the strings above
	float xpos;         ///< X coordinate of the feature
	float ypos;         ///< Y coordinate of the feature (height)
	float zpos;         ///< Z coordinate of the feature

	float rotation;     ///< Orientation of this feature (-32768..32767 for full circle)
	float relativeSize; ///< Not used at the moment keep 1'''
TileFileHeader_struct =struct.Struct('< 16s i i i i')
'''	char magic[16];      ///< "spring tilefile\0"
	int version;         ///< Must be 1 for now

	int numTiles;        ///< Total number of tiles in this file
	int tileSize;        ///< Must be 32 for now
	int compressionType; ///< Must be 1 (= dxt1) for now'''
	

_S3OHeader_struct = struct.Struct("< 12s i 5f 4i")
_S3OPiece_struct = struct.Struct("< 10i 3f")
_S3OVertex_struct = struct.Struct("< 3f 3f 2f")
_S3OChildOffset_struct = struct.Struct("< i")
_S3OIndex_struct = struct.Struct("< i")

SMALL_TILE_SIZE=680
MINIMAP_SIZE=699048
def pythonDecodeDXT1(data):# Python-only DXT1 decoder; this is slow!
	# input: one "row" of data (i.e. will produce 4*width pixels)
	blocks = len(data) / 8  # number of blocks in row
	out = ['', '', '', '']  # row accumulators

	for xb in xrange(blocks):
		# Decode next 8-byte block.        
		c0, c1, bits = struct.unpack('<HHI', data[xb*8:xb*8+8])
		# print c0,c1,bits
		# color 0, packed 5-6-5
		b0 = (c0 & 0x1f) << 3
		g0 = ((c0 >> 5) & 0x3f) << 2
		r0 = ((c0 >> 11) & 0x1f) << 3
		
		# color 1, packed 5-6-5
		b1 = (c1 & 0x1f) << 3
		g1 = ((c1 >> 5) & 0x3f) << 2
		r1 = ((c1 >> 11) & 0x1f) << 3

		# Decode this block into 4x4 pixels
		# Accumulate the results onto our 4 row accumulators
		for yo in xrange(4):
			for xo in xrange(4):
				# get next control op and generate a pixel
				
				control = bits & 3
				bits = bits >> 2
				if control == 0:
					out[yo] += chr(r0) + chr(g0) + chr(b0)
				elif control == 1:
					out[yo] += chr(r1) + chr(g1) + chr(b1)
				elif control == 2:                                
					if c0 > c1:
						out[yo] += chr((2 * r0 + r1 + 1) / 3) + chr((2 * g0 + g1 + 1) / 3) + chr((2 * b0 + b1 + 1) / 3)
					else:
						out[yo] += chr((r0 + r1) / 2) + chr((g0 + g1) / 2) + chr((b0 + b1) / 2)
				elif control == 3:
					if c0 > c1:
						out[yo] += chr((2 * r1 + r0 + 1) / 3) + chr((2 * g1 + g0 + 1) / 3) + chr((2 * b1 + b0 + 1) / 3)
					else:
						out[yo] += '\0\0\0'

	# All done.
	return out
	
def unpack_null_terminated_string(data, offset):
	result=''
	nextchar = 'X'
	while True:
		nextchar=struct.unpack_from('c',data,offset+len(result))[0]
		if nextchar=='\0':
			return result
		else:
			result+=nextchar
		if len(result)>10000:
			return result

class SMFMap:
	def __init__(self,filename):
		self.filename=filename
		self.basename=filename.rpartition('.')[0]
		self.smffile=open(filename,'rb').read()
		self.SMFHeader= SMFHeader_struct.unpack_from(self.smffile, 0)
		
		self.magic=self.SMFHeader[0]#;      ///< "spring map file\0"
		self.version=self.SMFHeader[1]#;         ///< Must be 1 for now
		self.mapid=self.SMFHeader[2]#;           ///< Sort of a GUID of the file, just set to a random value when writing a map

		self.mapx=self.SMFHeader[3]#;            ///< Must be divisible by 128
		self.mapy=self.SMFHeader[4]#;            ///< Must be divisible by 128
		self.squareSize=self.SMFHeader[5]#;      ///< Distance between vertices. Must be 8
		self.texelPerSquare=self.SMFHeader[6]#;  ///< Number of texels per square, must be 8 for now
		self.tilesize=self.SMFHeader[7]#;        ///< Number of texels in a tile, must be 32 for now
		self.minHeight=self.SMFHeader[8]#;     ///< Height value that 0 in the heightmap corresponds to	
		self.maxHeight=self.SMFHeader[9]#;     ///< Height value that 0xffff in the heightmap corresponds to

		self.heightmapPtr=self.SMFHeader[10]#;    ///< File offset to elevation data (short int[(mapy+1)*(mapx+1)])
		self.typeMapPtr=self.SMFHeader[11]#;      ///< File offset to typedata (unsigned char[mapy/2 * mapx/2])
		self.tilesPtr=self.SMFHeader[12]#;        ///< File offset to tile data (see MapTileHeader)
		self.minimapPtr=self.SMFHeader[13]#;      ///< File offset to minimap (always 1024*1024 dxt1 compresed data plus 8 mipmap sublevels)
		self.metalmapPtr=self.SMFHeader[14]#;     ///< File offset to metalmap (unsigned char[mapx/2 * mapy/2])
		self.featurePtr=self.SMFHeader[15]#;      ///< File offset to feature data (see MapFeatureHeader)

		self.numExtraHeaders=self.SMFHeader[16]#; ///< Numbers of extra headers following main header'''
		
		print 'Writing heightmap RAW (Remember, this is a %i by %i 16bit 1 channel IBM byte order raw!)'%((1+self.mapx),(1+self.mapy))
		self.heightmap=struct.unpack_from('< %iH'%((1+self.mapx)*(1+self.mapy)),self.smffile,self.heightmapPtr)
		heightmap_file=open(self.basename+'_height.raw','wb')
		for pixel in self.heightmap:
			heightmap_file.write(struct.pack('< H',pixel))
		heightmap_file.close()
		heightmap_img=Image.new('RGB',(1+self.mapx,1+self.mapy),'black')
		heightmap_img_pixels=heightmap_img.load()
		for x in range(heightmap_img.size[0]):
			for y in range(heightmap_img.size[1]):
				height=self.heightmap[(heightmap_img.size[0])*y+x]/256
				heightmap_img_pixels[x,y]=(height,height,height)
		heightmap_img.save(self.basename+'_height.bmp')
				
		print 'Writing MetalMap'
		self.metalmap= struct.unpack_from('< %iB'%((self.mapx/2)*(self.mapy/2)),self.smffile,self.metalmapPtr)
		metalmap_img=Image.new('RGB',(self.mapx/2,self.mapy/2),'black')
		metalmap_img_pixels=metalmap_img.load()
		for x in range(metalmap_img.size[0]):
			for y in range(metalmap_img.size[1]):
				metal=self.metalmap[(metalmap_img.size[0])*y+x]
				metalmap_img_pixels[x,y]=(metal,0,0)
		metalmap_img.save(self.basename+'_metal.bmp')
		
		print 'Writing typemap'
		self.typemap=  struct.unpack_from('< %iB'%((self.mapx/2)*(self.mapy/2)),self.smffile,self.typeMapPtr)
		typemap_img=Image.new('RGB',(self.mapx/2,self.mapy/2),'black')
		typemap_img_pixels=typemap_img.load()
		for x in range(typemap_img.size[0]):
			for y in range(typemap_img.size[1]):
				type=self.typemap[(typemap_img.size[0])*y+x]
				typemap_img_pixels[x,y]=(type,0,0)
		typemap_img.save(self.basename+'_type.bmp')
	
		print 'Writing minimap'
		miniddsheaderstr=([68, 68, 83, 32, 124, 0, 0, 0, 7, 16, 10, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 
		11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 4, 0, 0, 0, 68, 88, 84, 49, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 8, 16, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
		self.minimap=self.smffile[self.minimapPtr:self.minimapPtr+MINIMAP_SIZE]
		minimap_file=open(self.basename+'_mini.dds','wb')
		for c in miniddsheaderstr:
			minimap_file.write(struct.pack('< B',c))
		minimap_file.write(self.minimap)
		minimap_file.close()
		
		print 'Writing grassmap'
		vegmapoffset = SMFHeader_struct.size+ExtraHeader_struct.size+4
		for extraheader_index in range(self.numExtraHeaders):
			extraheader = ExtraHeader_struct.unpack_from(self.smffile,extraheader_index*ExtraHeader_struct.size+SMFHeader_struct.size)
			extraheader_size,extraheader_type,extraoffset =extraheader
			# print 'ExtraHeader',extraheader
			if extraheader_type==1: #grass
				# self.grassmap=struct.unpack_from('< %iB'%((self.mapx/4)*(self.mapy/4)),self.smffile,ExtraHeader_struct.size+SMFHeader_struct.size+extraheader_size)
				self.grassmap=struct.unpack_from('< %iB'%((self.mapx/4)*(self.mapy/4)),self.smffile,extraoffset)
				grassmap_img=Image.new('RGB',(self.mapx/4,self.mapy/4),'black')
				grassmap_img_pixels=grassmap_img.load()
				for x in range(grassmap_img.size[0]):
					for y in range(grassmap_img.size[1]):
						grass=self.grassmap[(grassmap_img.size[0])*y+x]
						if grass==1:
							grass = 255
						else:
							grass = 0
						grassmap_img_pixels[x,y]=(grass,grass,grass)
				grassmap_img.save(self.basename+'_grass.bmp')

		
		#MapFeatureHeader is followed by numFeatureType zero terminated strings indicating the names
		#of the features in the map. Then follow numFeatures MapFeatureStructs.
		self.mapfeaturesheader = MapFeatureHeader_struct.unpack_from(self.smffile,self.featurePtr)
		self.numFeatureType,self.numFeatures=self.mapfeaturesheader
		self.featurenames=[]
		featureoffset = self.featurePtr + MapFeatureHeader_struct.size
		while len(self.featurenames)<self.numFeatureType:
			featurename = unpack_null_terminated_string(self.smffile,featureoffset)
			self.featurenames.append(featurename)
			featureoffset+=len(featurename)+1 #cause of null terminator
			# print featurename
			'''nextchar= 'N'
			while nextchar != '\0':
				nextchar=struct.unpack_from('c',self.smffile,len(featurename)+self.featurePtr+MapFeatureHeader_struct.size
					+sum([len(fname)+1 for fname in self.featurenames]))[0]
				if nextchar =='\0':
					self.featurenames.append(featurename)
					featurename=''
				else:
					featurename+=nextchar'''
					
		print 'Features found in map definition',self.featurenames
		feature_offset=self.featurePtr+MapFeatureHeader_struct.size+sum([len(fname)+1 for fname in self.featurenames])
		self.features=[]
		for feature_index in range(self.numFeatures):
			feat= MapFeatureStruct_struct.unpack_from(self.smffile,feature_offset+MapFeatureStruct_struct.size*feature_index)
			# print feat
			self.features.append({'name':self.featurenames[feat[0]],'x':feat[1],'y':feat[2],'z':feat[3],'rotation':feat[4],'relativeSize':feat[5],})
			# print self.features[-1]
		print 'Writing feature placement file'
		feature_file=open(self.basename+'_featureplacement.lua','w')
		for feature in self.features:
			feature_file.write('{ name = \'%s\', x = %i, z = %i, rot = "%i" ,scale = %f },\n'%(feature['name'],feature['x'],feature['z'],feature['rotation'],feature['relativeSize']))
		feature_file.close()
		
		print 'loading tile files'
		self.maptileheader=MapTileHeader_struct.unpack_from(self.smffile,self.tilesPtr)
		self.numtilefiles,self.numtiles=self.maptileheader
		self.tilefiles=[]
		tileoffset=self.tilesPtr+MapTileHeader_struct.size
		for i in range(self.numtilefiles):
			numtilesinfile=struct.unpack_from('< i',self.smffile,tileoffset)[0]
			tileoffset+=4 #sizeof(int)
			tilefilename=unpack_null_terminated_string(self.smffile,tileoffset)
			tileoffset+=len(tilefilename)+1 #cause of null terminator
			self.tilefiles.append([tilefilename,numtilesinfile,open(tilefilename,'rb').read()])
			print tilefilename, 'has',numtilesinfile,'tiles'
		self.tileindices=struct.unpack_from('< %ii'%((self.mapx/4)*(self.mapy/4)),self.smffile,tileoffset)
			
		
		self.tiles=[]
		for tilefile in self.tilefiles:
			tileFileHeader = TileFileHeader_struct.unpack_from(tilefile[2],0)
			magic,version,numTiles,tileSize,compressionType=tileFileHeader
			#print tilefile[0],': magic,version,numTiles,tileSize,compressionType',magic,version,numTiles,tileSize,compressionType
			for i in range(numTiles):
				self.tiles.append(struct.unpack_from('< %is'%(SMALL_TILE_SIZE),tilefile[2], TileFileHeader_struct.size+i*SMALL_TILE_SIZE)[0])
			
			
		
		print 'Generating texture, this is very very slow (few minutes)'
		textureimage=Image.new('RGB',(self.mapx*8,self.mapy*8),'black')
		textureimagepixels=textureimage.load()
		for ty in range(self.mapy/4):
			# print 'row',ty
			for tx in range(self.mapx/4):
				currtile=self.tiles[self.tileindices[(self.mapx/4)*ty+tx]]
				# print 'Tile',(self.mapx/4)*ty+tx
				#one tile is 32x32, and pythonDecodeDXT1 will need one 'row' of data, assume this is 8*8 bytes
				for rows in xrange(8):
					# print "currtile",currtile
					dxdata=currtile[rows*64:(rows+1)*64]
					# print len(dxdata),dxdata
					dxtrows=pythonDecodeDXT1(dxdata) #decode in 8 block chunks
					for x in xrange(tx*32,(tx+1)*32):
						for y in xrange(ty*32+4*rows,ty*32+4+4*rows):
							# print rows, tx,ty,x,y
							# print dxtrows
							oy=(ty*32+4*rows)
							textureimagepixels[x,y]=(ord(dxtrows[y-oy][3*(x-tx*32) +0]),ord(dxtrows[y-oy][3*(x-tx*32) +1]),ord(dxtrows[y-oy][3*(x-tx*32) +2]))
		textureimage.save(self.basename+'_texture.bmp')
		print 'Done, one final bit of important info: the maps maxheight is %i, while the minheight is %i'%(self.maxHeight,self.minHeight)
mymap = SMFMap(sys.argv[1])

hokomoko
Spring Developer
Posts: 593
Joined: 02 Jun 2014, 00:46

Re: SMF map decompiler

Post by hokomoko »

nice!
Put it on github or somewhere?
User avatar
TurBoss
Jauria RTS Developer
Posts: 89
Joined: 27 Jan 2014, 01:04

Re: SMF map decompiler

Post by TurBoss »

Nice Project

I have created a gist for this
https://gist.github.com/TurBoss/53ac84f7fa7b5fc00ce8

replaced import Image with from PIL import Image
also replaced 3 spaces?¿ with 4

thx!
Last edited by TurBoss on 01 Jul 2016, 04:20, edited 1 time in total.
User avatar
Forboding Angel
Evolution RTS Developer
Posts: 14673
Joined: 17 Nov 2005, 02:43

Re: SMF map decompiler

Post by Forboding Angel »

Cool, but superfluous. http://www.bundysoft.com/L3DT/

Still cool though :-)
User avatar
Silentwings
Posts: 3720
Joined: 25 Oct 2008, 00:23

Re: SMF map decompiler

Post by Silentwings »

That's very handy.

Also, the free version of L3DT is limited to 2049x2049.
User avatar
Silentwings
Posts: 3720
Joined: 25 Oct 2008, 00:23

Re: SMF map decompiler

Post by Silentwings »

Tested and worked beautifully. Thanks!
Post Reply

Return to “Map Creation”