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