1"""
2Pekka Kana 2 map support for Tiled
32012, <samuli@tuomola.net>
4
5Notes:
6- should make PK2 classes mixins with Tiled ones
7"""
8from lib import cpystruct
9from os.path import dirname, exists
10from struct import pack,unpack,Struct
11from base64 import b64encode, b64decode
12import os, sys, re, string
13import tiled as T
14
15maps = []
16
17class PK2(T.Plugin):
18  @classmethod
19  def nameFilter(cls):
20    return "Pekka Kana 2 (*.map)"
21
22  @classmethod
23  def shortName(cls):
24    return "pk2"
25
26  @classmethod
27  def supportsFile(cls, f):
28    return open(f, 'rb').read(4) == b'1.3\0'
29
30  @classmethod
31  def read(cls, f):
32    lvl = PK2MAP()
33    with open(f, 'rb') as fh:
34      lvl.unpack(fh)
35      # spriteCount is +1
36      fh.seek(-len(lvl.spriteFiles[0]), 1)
37      lay1 = PK2MAPLAYER(fh)
38      lay2 = PK2MAPLAYER(fh)
39      lay3 = PK2MAPLAYER(fh)
40      print(lvl)
41
42    # -- tileset
43    img = T.qt.QImage()
44    imgfile = dirname(f)+'/../../gfx/tiles/'+str(lvl.tileFile)
45    img.load(imgfile, 'BMP')
46    t = T.Tiled.Tileset.create('Tiles', 32,32, 0, 0)
47    t.data().setTransparentColor(T.qt.QColor(img.color(255)))
48    t.data().loadFromImage(img, imgfile)
49
50    # find common bounding box for the layers
51    bb = ['','',10,10]
52    for l in [lay1,lay2,lay3]:
53      print(l)
54      bb[0] = min([bb[0], l.lx.num])
55      bb[1] = min([bb[1], l.ly.num])
56      bb[2] = max([bb[2], l.width()])
57      bb[3] = max([bb[3], l.height()])
58
59    print('bounds', bb)
60
61    m = T.Tiled.Map(T.Tiled.Map.Orthogonal, bb[2], bb[3], 32,32)
62    maps.append(m)
63
64    # -- background image
65    lai = T.Tiled.ImageLayer('Scenery', bb[2], bb[3])
66    img = T.qt.QImage()
67    imgfile = dirname(f)+'/../../gfx/scenery/'+str(lvl.fieldFile)
68    img.load(imgfile, 'BMP')
69    lai.loadFromImage(img, imgfile)
70
71    # -- layers
72    la1 = T.Tiled.TileLayer('Back', 0,0, bb[2], bb[3])
73    lay1.doTiles(t, la1, bb)
74
75    la2 = T.Tiled.TileLayer('Front', 0,0, bb[2], bb[3])
76    lay2.doTiles(t, la2, bb)
77
78    sprdir = dirname(f)+'/../../sprites/'
79    lay3.sprites = [0]
80    lay3.spriteGfx = {}
81
82    for s in lvl.spriteFiles:
83      # spriteCount is +1
84      if not exists(sprdir+str(s)): break
85
86      spr = PK2SPR(sprdir+str(s), m)
87
88      if not lay3.spriteGfx.has_key(str(spr.kuvatiedosto)):
89        sprfile = find_case_insensitive_filename(sprdir, str(spr.kuvatiedosto))
90        img = T.qt.QImage()
91        img.load(sprdir+sprfile, 'BMP')
92        print('loading', sprdir+sprfile)
93        sprts = T.Tiled.Tileset.create(sprfile, 32,32, 0, 0)
94        sprts.data().setTransparentColor(T.qt.QColor(img.color(255)))
95        sprts.data().loadFromImage(img, sprdir+sprfile)
96        lay3.spriteGfx[str(spr.kuvatiedosto)] = sprts
97
98      #sprgfx[(str(spr.kuvatiedosto]
99      lay3.sprites.append(spr)
100
101      #print spr
102
103    la3 = T.Tiled.ObjectGroup('Sprites', bb[2], bb[3])
104    lay3.doSprites(la3, bb)
105    m.addLayer(lai)
106    m.addTileset(t)
107    for sprts in lay3.spriteGfx.values():
108      m.addTileset(sprts)
109    m.addLayer(la1)
110    m.addLayer(la2)
111    m.addLayer(la3)
112
113    for f in lvl.__slots__:
114      val = repr(getattr(lvl, f))
115      m.setProperty(f, b64encode(val))
116
117    return m
118
119  @classmethod
120  def write(cls, m, fn):
121    out = PK2MAP()
122
123    for f in m.properties().keys():
124      if not f.startswith('__'):
125        setattr(out, f, b64decode(m.property(f)))
126
127    #setattr(out, "sprites", ['pla'])
128
129    with open(fn, 'wb') as fh:
130      print(out.pack(), file=fh)
131
132      for i in range(m.layerCount()):
133        tiles = []
134        l = 0
135        if isTileLayerAt(m, i):
136          l = tileLayerAt(m, i)
137          print(l)
138        elif isObjectGroupAt(m, i):
139          #l = objectGroupAt(m, i)
140          continue
141
142        for y in range(l.height()):
143          for x in range(l.width()):
144            if l.cellAt(x, y).tile != None:
145              tiles.append( l.cellAt(x, y).tile.id() )
146        print(0,0, l.width(), l.height(), file=fh)
147        print(bytearray(tiles), file=fh)
148
149    return True
150
151def find_case_insensitive_filename(path, fn):
152  for f in os.listdir(path):
153    if f.lower() == fn.lower():
154      return f
155
156class asciilongfile(cpystruct.CpyStruct("char filename[100]")):
157  @classmethod
158  def fromraw(cls, v):
159    return re.search('[\w\.]*', v, re.U).group(0)
160  def __repr__(self):
161    return str(self)
162  def __str__(self):
163    return self.filename
164
165class asciifile(cpystruct.CpyStruct("char filename[13]")):
166  @classmethod
167  def fromraw(cls, v):
168    return re.search('[\w\.]*', v, re.U).group(0)
169  def __repr__(self):
170    return str(self)
171  def __str__(self):
172    return self.filename
173
174class asciitxt(cpystruct.CpyStruct("char txt[40]")):
175  @classmethod
176  def fromraw(cls, v):
177    return re.search('[\w\. ]', v, re.U).group(0)
178
179class asciinum(cpystruct.CpyStruct("char num[8]")):
180  @classmethod
181  def fromraw(cls, v):
182    #v = ''.join(re.findall('[0-9]', v))
183    v = re.sub('[^0-9]','',v)
184    return 0 if not v.strip().isdigit() else int(v)
185
186class PK2MAPLAYER(cpystruct.CpyStruct("asciinum lx, ly, w, h;")):
187  MAXW = 256
188  MAXH = 224
189  MAXSZ = MAXW*MAXH
190
191  def width(self):
192    return self.w.num + 1
193  def height(self):
194    return self.h.num + 1
195
196  def __init__(self, dat):
197    # should make cpystruct support this usecase better
198    super(self.__class__, self).__init__(dat)
199
200    #print str(cpystruct.peek(dat, 128))
201
202    self.layer = bytearray(self.MAXSZ)
203    for i in range(len(self.layer)): self.layer[i] = 0xff
204
205    for y in range(self.ly.num, self.ly.num+self.height()):
206      for x in range(self.lx.num, self.lx.num+self.width()):
207        self.layer[x+y*self.MAXW] = dat.read(1)
208
209  def findBounds(self):
210    "find bounding box for coords that have tiles"
211    mx,my,mw,mh = None,None,10,10
212
213    for y in range(self.ly, self.ly+self.height()):
214      for x in range(self.lx, self.lx+self.width()):
215        if self.layer[x + y * self.MAXW] != 255:
216          if not my: my = y
217          if not mx or x < mx: mx = x
218          if x > mw: mw = x
219          if y > mh: mh = y
220
221    if not mx: mx = 0
222    if not my: my = 0
223    return mx, my, mw, mh
224
225  def doSprites(self, la, bb):
226    for y in range(self.height()):
227      for x in range(self.width()):
228        sprite = self.layer[self.lx.num + x + (self.ly.num + y) * self.MAXW]
229        if sprite != 255:
230          #if sprite > len(self.sprites):
231          #  print 'invalid spr',sprite
232          #  continue
233          rx = self.lx.num + x - bb[0]
234          ry = self.ly.num + y - bb[1] + 1
235          spr = self.sprites[sprite]
236          obj = T.Tiled.MapObject(str(spr.kuvatiedosto), '', T.qt.QPointF(rx, ry), T.qt.QSizeF(1, 1)) #spr.width, spr.height))
237          # 0 should point to the actual sprite but how?
238          obj.setCell(Tiled.Cell(self.spriteGfx[str(spr.kuvatiedosto)].data().tileAt(0)))
239          la.addObject(obj)
240
241  def doTiles(self, ts, la, bb):
242    for y in range(self.height()):
243      for x in range(self.width()):
244        tile = self.layer[self.lx.num + x + (self.ly.num + y) * self.MAXW]
245        if tile != 255:
246          rx = self.lx.num + x - bb[0]
247          ry = self.ly.num + y - bb[1]
248          if tile == 149: print('start @',rx,ry)
249          if tile == 150: print('end @',rx,ry)
250          ti = ts.data().tileAt(tile)
251          if ti != None and rx < bb[2] and ry < bb[3]:
252            # app should check that coords are within layer
253            #print rx,ry,self.ly,y
254            la.setCell(rx, ry, T.Tiled.Cell(ti))
255          else:
256            print('invalid',rx,ry)
257
258class PK2SPR_ANIM(cpystruct.CpyStruct("""
259  uchar   seq[10];
260  uchar   frames;
261  bool    loop;
262""")):
263  def __repr__(self):
264    return str(self)
265
266class PK2SPR(cpystruct.CpyStruct("""
267asciinum  tyyppi;
268asciilongfile  kuvatiedosto;
269asciilongfile  aanitiedostot[7];
270int     aanet[7];
271uchar   frameja;
272PK2SPR_ANIM animaatiot[20];
273uchar   animaatioita;
274uchar   frame_rate;
275int     kuva_x;
276int     kuva_y;
277int     kuva_frame_leveys;
278int     kuva_frame_korkeus;
279int     kuva_frame_vali;
280char    nimi[30];
281int     width, height;
282double  paino;
283bool    vihollinen;
284int     energia;
285int     vahinko;
286uchar   vahinko_tyyppi;
287uchar   suojaus;
288int     pisteet;
289int     AI[10];
290uchar   max_hyppy;
291double  max_nopeus;
292int     latausaika;
293uchar   vari;
294bool    este;
295int     tuhoutuminen;
296bool    avain;
297bool    tarisee;
298uchar   bonusten_lkm;
299int     hyokkays1_aika;
300int     hyokkays2_aika;
301int     pallarx_kerroin;
302char    muutos_sprite[100];
303char    bonus_sprite[100];
304char    ammus1_sprite[100];
305char    ammus2_sprite[100];
306
307bool    tiletarkistus;
308DWORD   aani_frq;
309bool    random_frq;
310
311bool    este_ylos;
312bool    este_alas;
313bool    este_oikealle;
314bool    este_vasemmalle;
315
316uchar   lapinakyvyys;
317bool    hehkuu;
318int     tulitauko;
319bool    liitokyky;
320bool    boss;
321bool    bonus_aina;
322bool    osaa_uida;
323""")):
324  def __init__(self, f, m):
325    with open(f, 'rb') as fh:
326      super(self.__class__, self).__init__(fh)
327
328class PK2MAP(cpystruct.CpyStruct("""
329  char ver[4];
330  BYTE nan;
331  asciifile tileFile, fieldFile, musicFile;
332  asciitxt mapName, author;
333  asciinum lvl, air, trig1, trig2, trig3;
334  asciinum playTime, v1, background, plrSpr;
335  asciinum lvlX, lvlY, icon;
336  asciinum spriteCount;
337  asciifile spriteFiles[spriteCount];
338""")): pass
339
340