1""" 2Mappy support for Tiled 32012-2013, <samuli@tuomola.net> 4""" 5from base64 import b64encode, b64decode 6from collections import OrderedDict 7try: 8 from StringIO import StringIO 9except ImportError: 10 from io import StringIO 11 12import os, sys, struct 13import pickle 14 15import tiled as T 16from lib.mappy_types import BLKSTR, MPHD, fmpchunk 17 18 19class FMPPicklerMixin: 20 21 @classmethod 22 def unpackchunks(cls, f): 23 24 chunks = OrderedDict() 25 with open(f, 'rb') as fh: 26 frm = fmpchunk() 27 frm.unpack(fh) 28 frm.data = fh.read(4) 29 filelen = frm.len - 16 30 chunks[frm.id] = frm 31 print(frm) 32 33 while fh.tell() < filelen: 34 fc = fmpchunk() 35 if not fc.unpack(fh): break 36 print(fh.tell(), fc) 37 fc.data = fh.read(fc.len) 38 chunks[fc.id] = fc 39 40 return chunks 41 42 @classmethod 43 def packchunks(cls, fn, chunks): 44 45 with open(fn, 'wb') as fh: 46 for k,v in chunks.items(): 47 print(k, len(v) + v.len) # chunk header + data 48 fh.write(v.pack()) 49 fh.write(v.data) 50 51 @classmethod 52 def picklechunks(cls, chunks): 53 src = StringIO() 54 pi = pickle.Pickler(src, 2) 55 pi.dump(chunks) 56 print("packlen", src.len) 57 return b64encode(src.getvalue()) 58 59 @classmethod 60 def unpicklechunks(cls, data): 61 src = StringIO(b64decode(data)) 62 print("unpacklen",src.len) 63 return pickle.Unpickler(src).load() 64 65 66class Mappy(T.Plugin, FMPPicklerMixin): 67 68 @classmethod 69 def nameFilter(cls): 70 return "Mappy (*.fmp)" 71 72 @classmethod 73 def shortName(cls): 74 return "mappy" 75 76 @classmethod 77 def supportsFile(cls, f): 78 return open(f,'rb').read(4) == b'FORM' 79 80 @classmethod 81 def read(cls, f): 82 83 print('Loading map at',f) 84 chunks = cls.unpackchunks(f) 85 hd = MPHD() 86 # perhaps cpystruct should only read as many bytes as it can handle? 87 hd.unpack(chunks['MPHD'].data[:len(hd)]) 88 89 m = T.Tiled.Map(T.Tiled.Map.Orthogonal, hd.mapwidth, hd.mapheight, 90 hd.blockwidth, hd.blockheight) 91 if hd.type == 2: 92 print('Isometric maps not supported at the moment') 93 return m 94 95 #TODO: m.setProperty('chunks', cls.picklechunks(chunks)) 96 97 tset = T.Tiled.Tileset.create('Tiles', hd.blockwidth, hd.blockheight, 0, 0) 98 cmap = list(FMPColormap.unpack(chunks['CMAP'].data)) 99 tset.data().loadFromImage(FMPTileGfx.unpack(hd, chunks['BGFX'].data, cmap), "") 100 101 blks = FMPBlocks(chunks['BKDT'].data, hd).blocks 102 103 for c in ['LYR'+str(i) for i in range(7,0,-1)]+['BODY']: 104 if c not in chunks: continue 105 print('populating',c) 106 lay = T.Tiled.TileLayer(c,0,0,hd.mapwidth, hd.mapheight) 107 lvl = list(FMPLayer.unpack(hd, chunks[c].data)) 108 FMPLayer.populate(lay, blks, tset.data(), hd, lvl) 109 m.addLayer(lay) 110 111 m.addTileset(tset) 112 113 return m 114 115 116 @classmethod 117 def write(cls, m, fn): 118 119 if m.orientation() != T.Tiled.Map.Orthogonal: 120 print('Isometric maps not supported at the moment') 121 return False 122 if not m.property('chunks'): 123 raise Exception("Export depends on unparsed binary blobs from original " 124 +"fmp to be stored in the map property 'chunks'") 125 126 print('Writing map at',fn) 127 128 chunks = cls.unpicklechunks(m.property('chunks')) 129 hd = MPHD() 130 hd.unpack(chunks['MPHD'].data[:len(hd)]) 131 blks = FMPBlocks(chunks['BKDT'].data, hd).blocks 132 133 for i in range(m.layerCount()): 134 135 if not isTileLayerAt(m, i): continue 136 l = tileLayerAt(m, i) 137 138 chunks[l.name()].data = FMPLayer.pack(hd, blks, l, i) 139 140 cls.packchunks(fn, chunks) 141 142 return True 143 144 145class FMPBlocks: 146 147 @classmethod 148 def unpack(cls, chunk, hd): 149 mod = hd.blockwidth * hd.blockheight * ((hd.blockdepth+1)/8) 150 for pos in range(0, hd.numblockstr*hd.blockstrsize, hd.blockstrsize): 151 b = BLKSTR() 152 b.unpack(chunk[pos:pos+hd.blockstrsize]) 153 if hd.type == 0: 154 for i in range(4): 155 b.olay[i] /= mod 156 yield b 157 158 def __init__(self, chunk, hd): 159 self.blocks = list(FMPBlocks.unpack(chunk, hd)) 160 161 162class FMPTileGfx: 163 164 @classmethod 165 def unpack(cls, hd, dat, cmap): 166 n = 0 167 w,h = hd.blockwidth*10, int(hd.blockheight*hd.numblockgfx/10+hd.blockheight) 168 fmt = T.qt.QImage.Format_Indexed8 if hd.blockdepth==8 else T.qt.QImage.Format_ARGB32 169 img = T.qt.QImage(w, h, fmt) 170 img.setColorTable(cmap) 171 172 for i in range(hd.numblockgfx): 173 col,row = int(i%10), int(i/10) 174 tx,ty = col*hd.blockwidth, row*hd.blockheight 175 176 for y in range(hd.blockheight): 177 for x in range(hd.blockwidth): 178 c = struct.unpack('B', dat[n:n+1])[0] 179 if hd.blockdepth==8: 180 img.setPixel(tx+x, ty+y, c) 181 else: 182 img.setPixel(tx+x, ty+y, cmap[c]) 183 n+=1 184 185 return img 186 187 188class FMPLayer: 189 190 @classmethod 191 def unpack(cls, hd, dat): 192 for i in range(0,hd.mapheight*hd.mapwidth*2,2): 193 d = struct.unpack('<H', dat[i:i+2])[0] 194 #d<0 is anim? 195 if hd.type == 0: d /= hd.blockstrsize 196 197 yield d 198 199 @classmethod 200 def pack(cls, hd, blocks, layer, laynum): 201 dat = StringIO() 202 print(hd) 203 for y in range(layer.height()): 204 for x in range(layer.width()): 205 cell = layer.cellAt(x, y) 206 tid = 0 if cell.isEmpty() else cell.tile.id() 207 n = blocks[tid].olay[laynum] 208 if hd.type == 0: 209 n *= hd.blockstrsize 210 dat.write( struct.pack('<H', n) ) 211 212 return dat.getvalue() 213 214 @classmethod 215 def populate(cls, layer, blocks, tileset, hd, ldata): 216 217 for fg in range(4): 218 i = 0 219 220 for y in range(hd.mapheight): 221 for x in range(hd.mapwidth): 222 v = int(ldata[i]) 223 224 if v >= len(blocks): 225 pass#print 'unknown block at',i 226 else: 227 n = int(blocks[v].olay[fg]) 228 if n != 0: 229 ti = tileset.tileAt(n) 230 if ti is None: 231 print('invalid tile',n,'at',x,y) 232 else: 233 layer.setCell(x, y, T.Tiled.Cell(ti)) 234 i += 1 235 236 237class FMPColormap: 238 239 @classmethod 240 def unpack(self, cmap): 241 """cmap -- rgb bytearray""" 242 print('got',len(cmap)) 243 for i in range(0,len(cmap),3): 244 r,g,b = struct.unpack('3B', cmap[i:i+3]) 245 yield T.qt.QColor(r,g,b).rgb() 246 247 @classmethod 248 def pack(self, cmap): 249 """cmap -- T.qt.QImage.colorTable""" 250 yield fmpchunk(id='CMAP', len=len(list(cmap))).pack() 251 for c in cmap: 252 yield struct.pack('3B', (c.qRed,c.qGreen,c.qBlue)) 253 254