sebbu

tmx_converter.py

Feb 7th, 2013
144
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.42 KB | None | 0 0
  1. #!/usr/bin/env python
  2. # -*- encoding: utf-8 -*-
  3.  
  4. ##    tmx_converter.py - Extract walkmap, warp, and spawn information from maps.
  5. ##
  6. ##    Copyright © 2012 Ben Longbons <b.r.longbons@gmail.com>
  7. ##
  8. ##    This file is part of The Mana World
  9. ##
  10. ##    This program is free software: you can redistribute it and/or modify
  11. ##    it under the terms of the GNU General Public License as published by
  12. ##    the Free Software Foundation, either version 2 of the License, or
  13. ##    (at your option) any later version.
  14. ##
  15. ##    This program is distributed in the hope that it will be useful,
  16. ##    but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. ##    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  18. ##    GNU General Public License for more details.
  19. ##
  20. ##    You should have received a copy of the GNU General Public License
  21. ##    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  22.  
  23.  
  24. from __future__ import print_function
  25.  
  26. import sys
  27. import os
  28. import posixpath
  29. import struct
  30. import xml.sax
  31. import base64
  32. import zlib
  33.  
  34. dump_all = False # wall of text
  35.  
  36. # lower case versions of everything except 'spawn' and 'warp'
  37. other_object_types = set([
  38.     'particle_effect',
  39.     'npc', # not interpreted by client
  40.     'script', # for ManaServ
  41.     'fixme', # flag for things that didn't have a type before
  42. ])
  43.  
  44. # Somebody has put ManaServ fields in our data!
  45. other_spawn_fields = (
  46.     'spawn_rate',
  47. )
  48. other_warp_fields = (
  49. )
  50.  
  51. TILESIZE = 32
  52. SEPARATOR = '\t'
  53. MESSAGE = 'This file is generated automatically. All manually changes will be removed when running the Converter.'
  54. CLIENT_MAPS = 'maps'
  55. SERVER_WLK = 'data'
  56. SERVER_NPCS = 'npc'
  57. NPC_MOBS = '_mobs.txt'
  58. NPC_WARPS = '_warps.txt'
  59. NPC_IMPORTS = '_import.txt'
  60. NPC_MASTER_IMPORTS = NPC_IMPORTS
  61.  
  62. class State(object):
  63.     pass
  64. State.INITIAL = State()
  65. State.LAYER = State()
  66. State.DATA = State()
  67. State.FINAL = State()
  68.  
  69. class Object(object):
  70.     __slots__ = (
  71.         'name',
  72.         #'map',
  73.         'x', 'y',
  74.         'w', 'h',
  75.     )
  76. class Mob(Object):
  77.     __slots__ = (
  78.         'monster_id',
  79.         'max_beings',
  80.         'ea_spawn',
  81.         'ea_death',
  82.     ) + other_spawn_fields
  83.     def __init__(self):
  84.         self.max_beings = 1
  85.         self.ea_spawn = 0
  86.         self.ea_death = 0
  87.  
  88. class Warp(Object):
  89.     __slots__ = (
  90.         'dest_map',
  91.         'dest_x',
  92.         'dest_y',
  93.     ) + other_warp_fields
  94.  
  95. class ContentHandler(xml.sax.ContentHandler):
  96.     __slots__ = (
  97.         'locator',  # keeps track of location in document
  98.         'out',      # open file handle to .wlk
  99.         'state',    # state of collision info
  100.         'tilesets', # first gid of each tileset
  101.         'buffer',   # characters within a section
  102.         'encoding', # encoding of layer data
  103.         'compression', # compression of layer data
  104.         'width',    # width of the collision layer
  105.         'height',   # height of the collision layer
  106.         'base',     # base name of current map
  107.         'npc_dir',  # world/map/npc/<base>
  108.         'mobs',     # open file to _mobs.txt
  109.         'warps',    # open file to _warps.txt
  110.         'imports',  # open file to _import.txt
  111.         'name',     # name property of the current map
  112.         'object',   # stores properties of the latest <object> tag
  113.         'mob_ids',  # set of all mob types that spawn here
  114.         'collision_fgid', # first gid in collision tileset
  115.         'collision_lgid', # last gid in collision tileset
  116.     )
  117.     def __init__(self, out, npc_dir, mobs, warps, imports):
  118.         xml.sax.ContentHandler.__init__(self)
  119.         self.locator = None
  120.         self.out = open(out, 'w')
  121.         self.state = State.INITIAL
  122.         self.tilesets = set([0]) # consider the null tile as its own tileset
  123.         self.buffer = bytearray()
  124.         self.encoding = None
  125.         self.compression = None
  126.         self.width = None
  127.         self.height = None
  128.         self.base = posixpath.basename(npc_dir)
  129.         self.npc_dir = npc_dir
  130.         self.mobs = mobs
  131.         self.warps = warps
  132.         self.imports = imports
  133.         self.object = None
  134.         self.mob_ids = set()
  135.         self.collision_fgid = 0
  136.         self.collision_lgid = 0
  137.  
  138.     def setDocumentLocator(self, loc):
  139.         self.locator = loc
  140.  
  141.     # this method randomly cuts in the middle of a line; thus funky logic
  142.     def characters(self, s):
  143.         if not s.strip():
  144.             return
  145.         if self.state is State.DATA:
  146.             self.buffer += s.encode('ascii')
  147.  
  148.     def startDocument(self):
  149.         pass
  150.  
  151.     def startElement(self, name, attr):
  152.         if dump_all:
  153.             attrs = ' '.join('%s="%s"' % (k,v) for k,v in attr.items())
  154.             if attrs:
  155.                 print('<%s %s>' % (name, attrs))
  156.             else:
  157.                 print('<%s>' % name)
  158.  
  159.         if self.state is State.INITIAL:
  160.             if name == u'property' and attr[u'name'].lower() == u'name':
  161.                 self.name = attr[u'value']
  162.                 self.mobs.write('// %s\n' % MESSAGE)
  163.                 self.mobs.write('// %s mobs\n\n' % self.name)
  164.                 self.warps.write('// %s\n' % MESSAGE)
  165.                 self.warps.write('// %s warps\n\n' % self.name)
  166.  
  167.             if name == u'tileset':
  168.                 self.tilesets.add(int(attr[u'firstgid']))
  169.                 if attr.get(u'name','').lower().startswith(u'collision'):
  170.                     self.collision_fgid = int(attr[u'firstgid'])
  171.  
  172.             if name == u'layer' and attr[u'name'].lower().startswith(u'collision'):
  173.                 self.width = int(attr[u'width'])
  174.                 self.height = int(attr[u'height'])
  175.                 self.out.write(struct.pack('<HH', self.width, self.height))
  176.                 self.state = State.LAYER
  177.         elif self.state is State.LAYER:
  178.             if name == u'layer' and self.collision_lgid == 0:
  179.                 self.collision_lgid = int(attr[u'firstgid'])-1
  180.             if name == u'data':
  181.                 if attr.get(u'encoding','') not in (u'', u'csv', u'base64', u'xml'):
  182.                     print('Bad encoding:', attr.get(u'encoding',''))
  183.                     return
  184.                 self.encoding = attr.get(u'encoding','')
  185.                 if attr.get(u'compression','') not in (u'', u'none', u'zlib', u'gzip'):
  186.                     print('Bad compression:', attr.get(u'compression',''))
  187.                     return
  188.                 self.compression = attr.get(u'compression','')
  189.                 self.state = State.DATA
  190.         elif self.state is State.DATA:
  191.             if name == u'tile':
  192.                 gid = int(attr.get(u'gid'))
  193.                 if (self.collision_lgid <> 0 and gid >= self.collision_lgid) or ( gid <> 0 and gid < self.collision_fgid):
  194.                     print('bad gid in layer (xml)', self.collision_fgid, gid, self.collision_lgid)
  195.                     return
  196.                 if gid <> 0:
  197.                     self.out.write(chr(int(attr.get(u'gid',0)) - self.collision_fgid))
  198.                 else:
  199.                     self.out.write(chr(0))
  200.         elif self.state is State.FINAL:
  201.             if name == u'object':
  202.                 if attr.get(u'type') == None:
  203.                     return
  204.                 obj_type = attr[u'type'].lower()
  205.                 x = int(attr[u'x']) / TILESIZE;
  206.                 y = int(attr[u'y']) / TILESIZE;
  207.                 w = int(attr.get(u'width', 0)) / TILESIZE;
  208.                 h = int(attr.get(u'height', 0)) / TILESIZE;
  209.                 # I'm not sure exactly what the w/h shrinking is for,
  210.                 # I just copied it out of the old converter.
  211.                 # I know that the x += w/2 is to get centers, though.
  212.                 if obj_type == 'spawn':
  213.                     self.object = Mob()
  214.                     if w > 1:
  215.                         w -= 1
  216.                     if h > 1:
  217.                         h -= 1
  218.                     x += w/2
  219.                     y += h/2
  220.                 elif obj_type == 'warp':
  221.                     self.object = Warp()
  222.                     x += w/2
  223.                     y += h/2
  224.                     w -= 2
  225.                     h -= 2
  226.                 else:
  227.                     if obj_type not in other_object_types:
  228.                         print('Unknown object type:', obj_type, file=sys.stderr)
  229.                     self.object = None
  230.                     return
  231.                 obj = self.object
  232.                 obj.x = x
  233.                 obj.y = y
  234.                 obj.w = w
  235.                 obj.h = h
  236.                 obj.name = attr[u'name']
  237.             elif name == u'property':
  238.                 obj = self.object
  239.                 if obj is None:
  240.                     return
  241.                 key = attr[u'name'].lower()
  242.                 value = attr[u'value']
  243.                 # Not true due to defaulting
  244.                 #assert not hasattr(obj, key)
  245.                 try:
  246.                     value = int(value)
  247.                 except ValueError:
  248.                     pass
  249.                 setattr(obj, key, value)
  250.  
  251.     def add_warp_line(self, line):
  252.         self.warps.write(line)
  253.  
  254.     def endElement(self, name):
  255.         if dump_all:
  256.             print('</%s>' % name)
  257.  
  258.         if name == u'object':
  259.             obj = self.object
  260.             if isinstance(obj, Mob):
  261.                 if not hasattr(obj, u'max_beings') or not hasattr(obj, u'ea_spawn') or not hasattr(obj, u'ea_death'):
  262.                     return
  263.                 mob_id = obj.monster_id
  264.                 if mob_id < 1000:
  265.                     mob_id += 1002
  266.                 self.mob_ids.add(mob_id)
  267.                 self.mobs.write(
  268.                     SEPARATOR.join([
  269.                         '%s.gat,%d,%d,%d,%d' % (self.base, obj.x, obj.y, obj.w, obj.h),
  270.                         'monster',
  271.                         obj.name,
  272.                         #'%d,%d,%d,%d,Mob%s::On%d\n' % (mob_id, obj.max_beings, obj.ea_spawn, obj.ea_death, self.base, mob_id),
  273.                         '%d,%d,%d,%d\n' % (mob_id, obj.max_beings, obj.ea_spawn, obj.ea_death),
  274.                     ])
  275.                 )
  276.             elif isinstance(obj, Warp):
  277.                 if not hasattr(obj, u'dest_map') or not hasattr(obj, u'dest_x') or not hasattr(obj, u'dest_y'):
  278.                     return
  279.                 self.warps.write(
  280.                     SEPARATOR.join([
  281.                         '%s.gat,%d,%d' % (self.base, obj.x, obj.y),
  282.                         'warp',
  283.                         obj.name,
  284.                         '%d,%d,%s.gat,%d,%d\n' % (obj.w, obj.h, obj.dest_map, obj.dest_x / 32, obj.dest_y / 32),
  285.                     ])
  286.                 )
  287.  
  288.         if name == u'data':
  289.             if self.state is State.DATA:
  290.                 if self.encoding == u'csv':
  291.                     for x in self.buffer.split(','):
  292.                         if (self.collision_lgid <> 0 and x >= self.collision_lgid) or ( x <> 0 and x < self.collision_fgid):
  293.                             print('bad gid in layer (csv)', self.collision_fgid, x, self.collision_lgid)
  294.                             return
  295.                         if x <> 0:
  296.                             self.out.write(chr(int(x) - self.collision_fgid))
  297.                         else:
  298.                             self.out.write(chr(0))
  299.                 elif self.encoding == u'base64':
  300.                     data = base64.b64decode(str(self.buffer))
  301.                     if self.compression == u'zlib':
  302.                         data = zlib.decompress(data)
  303.                     elif self.compression == u'gzip':
  304.                         data = zlib.decompressobj().decompress('x\x9c' + data[10:-8])
  305.                     for i in range(self.width*self.height):
  306.                         gid = int(struct.unpack('<I',data[i*4:i*4+4])[0])
  307.                         if (self.collision_lgid <> 0 and gid >= self.collision_lgid) or ( gid <> 0 and gid < self.collision_fgid):
  308.                             print('bad gid in layer (base64)', self.collision_fgid, gid, self.collision_lgid)
  309.                             return
  310.                         if gid <> 0:
  311.                             self.out.write(chr(gid - self.collision_fgid))
  312.                         else:
  313.                             self.out.write(chr(0))
  314.                 self.state = State.FINAL
  315.  
  316.     def endDocument(self):
  317.         #self.mobs.write(
  318.         #    SEPARATOR.join([
  319.         #        '\n\n%s.gat,0,0,0' % (self.base),
  320.         #        'script',
  321.         #        'Mob%s' % (self.base),
  322.         #        '-1,{\n',
  323.         #    ])
  324.         #)
  325.         #for mob_id in sorted(self.mob_ids):
  326.         #    self.mobs.write('On%d:\n    set @mobID, %d;\n    callfunc "MobPoints";\n    end;\n\n' % (mob_id, mob_id))
  327.         #self.mobs.write('    end;\n}\n')
  328.         self.imports.write('// Map %s: %s\n' % (self.base, self.name))
  329.         self.imports.write('// %s\n' % MESSAGE)
  330.         self.imports.write('map: %s.gat\n' % self.base)
  331.  
  332.         npcs = os.listdir(self.npc_dir)
  333.         npcs.sort()
  334.         for x in npcs:
  335.             if x == NPC_IMPORTS:
  336.                 continue
  337.             if x.startswith('.'):
  338.                 continue
  339.             if x.endswith('.txt'):
  340.                 self.imports.write('npc: %s\n' % posixpath.join(SERVER_NPCS, self.base, x))
  341.         pass
  342.  
  343. def main(argv):
  344.     _, client_data, server_data = argv
  345.     tmx_dir = posixpath.join(client_data, CLIENT_MAPS)
  346.     wlk_dir = posixpath.join(server_data, SERVER_WLK)
  347.     npc_dir = posixpath.join(server_data, SERVER_NPCS)
  348.  
  349.     npc_master = []
  350.  
  351.     for arg in os.listdir(tmx_dir):
  352.         base, ext = posixpath.splitext(arg)
  353.  
  354.         if ext == '.tmx':
  355.             tmx = posixpath.join(tmx_dir, arg)
  356.             wlk = posixpath.join(wlk_dir, base + '.wlk')
  357.             this_map_npc_dir = posixpath.join(npc_dir, base)
  358.             os.path.isdir(this_map_npc_dir) or os.mkdir(this_map_npc_dir)
  359.             print('Converting %s to %s' % (tmx, wlk))
  360.             with open(posixpath.join(this_map_npc_dir, NPC_MOBS), 'w') as mobs:
  361.                 with open(posixpath.join(this_map_npc_dir, NPC_WARPS), 'w') as warps:
  362.                     with open(posixpath.join(this_map_npc_dir, NPC_IMPORTS), 'w') as imports:
  363.                         xml.sax.parse(tmx, ContentHandler(wlk, this_map_npc_dir, mobs, warps, imports))
  364.             npc_master.append('import: %s\n' % posixpath.join(SERVER_NPCS, base, NPC_IMPORTS))
  365.  
  366.     with open(posixpath.join(npc_dir, NPC_MASTER_IMPORTS), 'w') as out:
  367.         out.write('// %s\n\n' % MESSAGE)
  368.         npc_master.sort()
  369.         for line in npc_master:
  370.             out.write(line)
  371.  
  372. if __name__ == '__main__':
  373.     main(sys.argv)
Add Comment
Please, Sign In to add comment