summaryrefslogblamecommitdiff
path: root/tmx_converter.py
blob: c6b4e525d656c595462ef96b55d4a24c596fe57a (plain) (tree)





























                                                                                

             

                               
                              

                                                             
                          



                                                             
            
  









                                               
                                                                                                                        


                    
                                    
                      
                        
















                                
                 












                           






                        


                   

                      








                                                         



                                                   


                                              
                                              




                                                                  

                                                               
     
                                                                  



                                             
                                                                            
                                 



                               


                                               
                          



                              

                               




























                                                                         

                                                               




                                                                                   


                                                                           


                                        

                                                                                    
                          
                                                        

                                                                                       

                                                              
                                       

                                                                             























                                                                     






                                         
































                                                                                

                                              




                                       








                                                                                           


                                        
                                                                                   

                                  
                                                                                                   




                                       
                                                               
                               
                                                                                                            

                      


                                             
 





                                                                        
                                                             



                                                                                     
                                                           

                                                                                                             

                          



























                                                                                                             

                                                                      
                                                   
















                                                                                           


                        










                                                                                                        

                   
                      




                                           
                                      




                                                                         


                                                                                             

                                                                                                                  

                                                                                              

                                                                           
                                                        







                                                                       
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

##    tmx_converter.py - Extract walkmap, warp, and spawn information from maps.
##
##    Copyright © 2012 Ben Longbons <b.r.longbons@gmail.com>
##
##    This file is part of The Mana World
##
##    This program is free software: you can redistribute it and/or modify
##    it under the terms of the GNU General Public License as published by
##    the Free Software Foundation, either version 2 of the License, or
##    (at your option) any later version.
##
##    This program is distributed in the hope that it will be useful,
##    but WITHOUT ANY WARRANTY; without even the implied warranty of
##    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##    GNU General Public License for more details.
##
##    You should have received a copy of the GNU General Public License
##    along with this program.  If not, see <http://www.gnu.org/licenses/>.


from __future__ import print_function

import sys
import os
import posixpath
import struct
import xml.sax
import base64
import zlib

dump_all = False # wall of text
check_mobs = True # mob_db.txt

# lower case versions of everything except 'spawn' and 'warp'
other_object_types = set([
    'particle_effect',
    'npc', # not interpreted by client
    'script', # for ManaServ
    'fixme', # flag for things that didn't have a type before
    'music',
])

# Somebody has put ManaServ fields in our data!
other_spawn_fields = (
    'spawn_rate',
)
other_warp_fields = (
)

TILESIZE = 32
SEPARATOR = '|'
MESSAGE = 'This file is generated automatically. All manually added changes will be removed when running the Converter.'
CLIENT_MAPS = 'maps'
SERVER_WLK = 'data'
SERVER_NPCS = 'npc'
TMWA_MAP_CONF = 'conf/tmwa-map.conf'
NPC_MOBS = '_mobs.txt'
NPC_NODES = '_nodes.txt'
NPC_WARPS = '_warps.txt'
NPC_IMPORTS = '_import.txt'
NPC_MASTER_IMPORTS = NPC_IMPORTS

class State(object):
    pass
State.INITIAL = State()
State.LAYER = State()
State.DATA = State()
State.FINAL = State()

class Object(object):
    __slots__ = (
        'name',
        #'map',
        'x', 'y',
        'w', 'h',
        'ignore',
    )
class Mob(Object):
    __slots__ = (
        'monster_id',
        'max_beings',
        'ea_spawn',
        'ea_death',
    ) + other_spawn_fields
    def __init__(self):
        self.max_beings = 1
        self.ea_spawn = 0
        self.ea_death = 0

class Node(Object):
    __slots__ = (
        'subtype'
    )
    def __init__(self):
        self.subtype = 0

class Warp(Object):
    __slots__ = (
        'dest_map',
        'dest_tile_x',
        'dest_tile_y',
    ) + other_warp_fields

class ContentHandler(xml.sax.ContentHandler):
    __slots__ = (
        'locator',  # keeps track of location in document
        'out',      # open file handle to .wlk
        'state',    # state of collision info
        'tilesets', # first gid of each tileset
        'buffer',   # characters within a section
        'encoding', # encoding of layer data
        'compression', # compression of layer data
        'width',    # width of the collision layer
        'height',   # height of the collision layer
        'base',     # base name of current map
        'npc_dir',  # world/map/npc/<base>
        'mobs',     # open file to _mobs.txt
        'nodes',     # open file to _nodes.txt
        'warps',    # open file to _warps.txt
        'imports',  # open file to _import.txt
        'name',     # name property of the current map
        'object',   # stores properties of the latest <object> tag
        'mob_ids',  # set of all mob types that spawn here
        'node_types', # set of all node types that appear here
        'node_objs', # set of all node objects that appear here
    )
    def __init__(self, out, npc_dir, mobs, warps, imports, nodes):
        xml.sax.ContentHandler.__init__(self)
        self.locator = None
        self.out = open(out, 'w')
        self.state = State.INITIAL
        self.tilesets = set([0]) # consider the null tile as its own tileset
        self.buffer = bytearray()
        self.encoding = None
        self.compression = None
        self.width = None
        self.height = None
        self.base = posixpath.basename(npc_dir)
        self.npc_dir = npc_dir
        self.mobs = mobs
        self.nodes = nodes
        self.warps = warps
        self.imports = imports
        self.object = None
        self.mob_ids = set()
        self.node_types = set()
        self.node_objs = set()

    def setDocumentLocator(self, loc):
        self.locator = loc

    # this method randomly cuts in the middle of a line; thus funky logic
    def characters(self, s):
        if not s.strip():
            return
        if self.state is State.DATA:
            self.buffer += s.encode('ascii')

    def startDocument(self):
        pass

    def startElement(self, name, attr):
        if dump_all:
            attrs = ' '.join('%s="%s"' % (k,v) for k,v in attr.items())
            if attrs:
                print('<%s %s>' % (name, attrs))
            else:
                print('<%s>' % name)

        if self.state is State.INITIAL:
            if name == u'property' and attr[u'name'].lower() == u'name':
                self.name = attr[u'value']
                self.mobs.write('// %s\n' % MESSAGE)
                self.mobs.write('// %s mobs\n\n' % self.name)
                self.warps.write('// %s\n' % MESSAGE)
                self.warps.write('// %s warps\n\n' % self.name)
                self.nodes.write('// %s\n' % MESSAGE)
                self.nodes.write('// %s nodes\n\n' % self.name)

            if name == u'tileset':
                self.tilesets.add(int(attr[u'firstgid']))

            if name == u'layer' and attr[u'name'].lower().startswith(u'collision'):
                self.width = int(attr[u'width'])
                self.height = int(attr[u'height'])
                self.out.write(struct.pack('<HH', self.width, self.height))
                self.state = State.LAYER
        elif self.state is State.LAYER:
            if name == u'data':
                if attr.get(u'encoding','') not in (u'', u'csv', u'base64', u'xml'):
                    print('Bad encoding:', attr.get(u'encoding',''))
                    return
                self.encoding = attr.get(u'encoding','')
                if attr.get(u'compression','') not in (u'', u'none', u'zlib', u'gzip'):
                    print('Bad compression:', attr.get(u'compression',''))
                    return
                self.compression = attr.get(u'compression','')
                self.state = State.DATA
        elif self.state is State.DATA:
            self.out.write(chr(int(attr.get(u'gid',0)) not in self.tilesets))
        elif self.state is State.FINAL:
            if name == u'object':
                obj_type = attr[u'type'].lower()
                x = int(attr[u'x']) / TILESIZE;
                y = int(attr[u'y']) / TILESIZE;
                w = int(attr.get(u'width', 0)) / TILESIZE;
                h = int(attr.get(u'height', 0)) / TILESIZE;
                # I'm not sure exactly what the w/h shrinking is for,
                # I just copied it out of the old converter.
                # I know that the x += w/2 is to get centers, though.
                if obj_type == 'spawn':
                    self.object = Mob()
                    if w > 1:
                        w -= 1
                    if h > 1:
                        h -= 1
                    x += w/2
                    y += h/2
                elif obj_type == 'warp':
                    self.object = Warp()
                    x += w/2
                    y += h/2
                    w -= 2
                    h -= 2
                elif obj_type == 'node':
                    self.object = Node()
                    w += x - 1
                    h += y - 1
                    if w == x and h == y:
                        w = 0
                        h = 0
                else:
                    if obj_type not in other_object_types:
                        print('Unknown object type:', obj_type, file=sys.stderr)
                    self.object = None
                    return
                obj = self.object
                obj.x = x
                obj.y = y
                obj.w = w
                obj.h = h
                obj.name = attr[u'name']
            elif name == u'property':
                obj = self.object
                if obj is None:
                    return
                key = attr[u'name'].lower()
                value = attr[u'value']
                # Not true due to defaulting
                #assert not hasattr(obj, key)
                try:
                    value = int(value)
                except ValueError:
                    pass
                setattr(obj, key, value)

    def add_warp_line(self, line):
        self.warps.write(line)

    def endElement(self, name):
        if dump_all:
            print('</%s>' % name)

        if name == u'object':
            if hasattr(self.object, 'ignore'):
                return;
            obj = self.object
            if isinstance(obj, Mob):
                mob_id = obj.monster_id
                if mob_id < 1000:
                    mob_id += 1002
                if check_mobs:
                    try:
                        name = mob_names[mob_id]
                    except KeyError:
                        print('Warning: unknown mob ID: %d (%s)' % (mob_id, obj.name))
                    else:
                        if name != obj.name:
                            print('Warning: wrong mob name: %s (!= %s)' % (obj.name, name))
                            obj.name = name
                self.mob_ids.add(mob_id)
                self.mobs.write(
                    SEPARATOR.join([
                        '%s,%d,%d,%d,%d' % (self.base, obj.x, obj.y, obj.w, obj.h),
                        'monster',
                        obj.name,
                        '%d,%d,%dms,%dms\n' % (mob_id, obj.max_beings, obj.ea_spawn, obj.ea_death),
                    ])
                )
            elif isinstance(obj, Warp):
                self.warps.write(
                    SEPARATOR.join([
                        '%s,%d,%d' % (self.base, obj.x, obj.y),
                        'warp',
                        '%d,%d,%s,%d,%d\n' % (obj.w, obj.h, obj.dest_map, obj.dest_tile_x, obj.dest_tile_y),
                    ])
                )
            elif isinstance(obj, Node):
                self.node_types.add(obj.name)
                self.node_objs.add(obj)

        if name == u'data':
            if self.state is State.DATA:
                if self.encoding == u'csv':
                    for x in self.buffer.split(','):
                        self.out.write(chr(int(x) not in self.tilesets))
                elif self.encoding == u'base64':
                    data = base64.b64decode(str(self.buffer))
                    if self.compression == u'zlib':
                        data = zlib.decompress(data)
                    elif self.compression == u'gzip':
                        data = zlib.decompressobj().decompress('x\x9c' + data[10:-8])
                    for i in range(self.width*self.height):
                        self.out.write(chr(int(struct.unpack('<I',data[i*4:i*4+4])[0]) not in self.tilesets))
                self.state = State.FINAL

    def endDocument(self):
        if len(self.node_types) > 0:
            self.nodes.write('%s,0,0,0|script|Node%s|32767\n{\n    end;\nOnInit:\n' % (self.base, self.base))
            for ntype in sorted(self.node_types):
                self.nodes.write('    setarray .m$, "_N-%s"' % (ntype))
                for nm in self.node_objs:
                    self.nodes.write(', "%s"' % (self.base))
                self.nodes.write(';\n    setarray .x1, "_N-%s"' % (ntype))
                for nx in self.node_objs:
                    self.nodes.write(', %d' % (nx.x))
                self.nodes.write(';\n    setarray .y1, "_N-%s"' % (ntype))
                for ny in self.node_objs:
                    self.nodes.write(', %d' % (ny.y))
                self.nodes.write(';\n    setarray .x2, "_N-%s"' % (ntype))
                for nw in self.node_objs:
                    self.nodes.write(', %d' % (nw.w))
                self.nodes.write(';\n    setarray .y2, "_N-%s"' % (ntype))
                for nh in self.node_objs:
                    self.nodes.write(', %d' % (nh.h))
                self.nodes.write(';\n    setarray .id, "_N-%s"' % (ntype))
                for nt in self.node_objs:
                    self.nodes.write(', %d' % (nt.subtype))
                self.nodes.write(';\n')
                # TODO don't make .id, .x2, .y2 when they are empty
                # TODO custom properties
                self.nodes.write('    donpcevent "_N-%s::OnMaybeStart";\n' % (ntype))
            self.nodes.write('    destroy;\n}\n')
        else:
            self.nodes.write('// (no nodes)\n')
        self.imports.write('// Map %s: %s\n' % (self.base, self.name))
        self.imports.write('// %s\n' % MESSAGE)
        self.imports.write('map: %s\n' % self.base)

        npcs = os.listdir(self.npc_dir)
        npcs.sort()
        for x in npcs:
            if x == NPC_IMPORTS:
                continue
            if x.startswith('.'):
                continue
            if x.endswith('.txt'):
                self.imports.write('npc: %s\n' % posixpath.join(SERVER_NPCS, self.base, x))
        pass

def main(argv):
    _, client_data, server_data = argv
    tmx_dir = posixpath.join(client_data, CLIENT_MAPS)
    wlk_dir = posixpath.join(server_data, SERVER_WLK)
    npc_dir = posixpath.join(server_data, SERVER_NPCS)
    if check_mobs:
        global mob_names
        mob_names = {}
        with open(posixpath.join(server_data, TMWA_MAP_CONF)) as mob_dbs:
            for mob_db_line in mob_dbs:
                if mob_db_line.startswith('mob_db:'):
                    with open(posixpath.join(server_data, mob_db_line.split(':')[1].strip())) as mob_db:
                        for line in mob_db:
                            if not line.strip():
                                continue
                            if line.startswith('//'):
                                continue
                            k, v, _ = line.split(',', 2)
                            mob_names[int(k)] = v.strip()

    npc_master = []
    map_basenames = []

    for arg in os.listdir(tmx_dir):
        base, ext = posixpath.splitext(arg)

        if ext == '.tmx':
            map_basenames.append(base)
            tmx = posixpath.join(tmx_dir, arg)
            wlk = posixpath.join(wlk_dir, base + '.wlk')
            this_map_npc_dir = posixpath.join(npc_dir, base)
            os.path.isdir(this_map_npc_dir) or os.mkdir(this_map_npc_dir)
            print('Converting %s to %s' % (tmx, wlk))
            with open(posixpath.join(this_map_npc_dir, NPC_MOBS), 'w') as mobs:
                with open(posixpath.join(this_map_npc_dir, NPC_WARPS), 'w') as warps:
                    with open(posixpath.join(this_map_npc_dir, NPC_IMPORTS), 'w') as imports:
                        with open(posixpath.join(this_map_npc_dir, NPC_NODES), 'w') as nodes:
                            xml.sax.parse(tmx, ContentHandler(wlk, this_map_npc_dir, mobs, warps, imports, nodes))
            npc_master.append('import: %s\n' % posixpath.join(SERVER_NPCS, base, NPC_IMPORTS))

    with open(posixpath.join(wlk_dir, 'resnametable.txt'), 'w') as resname:
        for base in sorted(map_basenames):
            resname.write('%s#%s.wlk#\n' % (base, base))
    with open(posixpath.join(npc_dir, NPC_MASTER_IMPORTS), 'w') as out:
        out.write('// %s\n\n' % MESSAGE)
        npc_master.sort()
        for line in npc_master:
            out.write(line)

if __name__ == '__main__':
    main(sys.argv)