Last active
July 1, 2016 02:22
-
-
Save TurBoss/53ac84f7fa7b5fc00ce8 to your computer and use it in GitHub Desktop.
Beherith's SMF map decompiler
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python | |
# PD license by Beherith | |
import sys | |
import struct | |
from PIL 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]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment