Beispiel #1
0
def transcribe_dmm(args):
    if not os.path.isfile(args.subject):
        print('File {0} does not exist.'.format(args.subject))
        sys.exit(1)
    if not os.path.isfile(args.project):
        print('DM Environment File {0} does not exist.'.format(args.project))
        sys.exit(1)

    dmm = Map(forgiving_atom_lookups=True)
    basename, ext = os.path.splitext(args.subject)
    outfile = '{0}.trans{1}'.format(basename, ext)
    dmm.Load(args.subject, format='dmm')
    dmm.Save(outfile, format='dmm', serialize_cleanly=True)
Beispiel #2
0
def split_dmm(args):
    if not os.path.isfile(args.map):
        print('File {0} does not exist.'.format(args.mine))
        sys.exit(1)
    if not os.path.isfile(args.project):
        print('DM Environment File {0} does not exist.'.format(args.project))
        sys.exit(1)

    dmm = Map(forgiving_atom_lookups=True)
    dmm.Load(args.map, format='dmm')

    #fmt = DMMFormat(dmm)

    nz = len(dmm.zLevels)
    for z in range(nz):
        basename, ext = os.path.splitext(args.map)
        outfile = '{0}-{1}{2}'.format(basename, z + 1, ext)
        print('>>> Splitting z={}/{} to {}'.format(z + 1, nz, outfile))
        output = Map(forgiving_atom_lookups=True)
        currentZLevel = dmm.zLevels[z]
        newZLevel = output.CreateZLevel(currentZLevel.height,
                                        currentZLevel.width)
        #newZLevel.initial_load=True
        for y in range(currentZLevel.width):
            for x in range(currentZLevel.height):
                newTile = newZLevel.GetTile(x, y)
                for atom in currentZLevel.GetTile(x, y).GetAtoms():
                    newTile.AppendAtom(atom.copy(toNewMap=True))
        #newZLevel.initial_load=False
        output.Save(outfile, format='dmm')
Beispiel #3
0
def compare_dmm(args):
    if not os.path.isfile(args.theirs):
        print('File {0} does not exist.'.format(args.theirs))
        sys.exit(1)
    if not os.path.isfile(args.mine):
        print('File {0} does not exist.'.format(args.mine))
        sys.exit(1)
    if not os.path.isfile(args.project):
        print('DM Environment File {0} does not exist.'.format(args.project))
        sys.exit(1)
        
    theirs_dmm = Map(forgiving_atom_lookups=True)
    theirs_dmm.readMap(args.theirs)
    
    mine_dmm = Map(forgiving_atom_lookups=True)
    mine_dmm.readMap(args.mine)
    
    if theirs_dmm.width != mine_dmm.width:
        print('Width is not equal: {} != {}.'.format(theirs_dmm.width, mine_dmm.width))
        sys.exit(1)
    if theirs_dmm.height != mine_dmm.height:
        print('Height is not equal: {} != {}.'.format(theirs_dmm.height, mine_dmm.height))
        sys.exit(1)
    
    ttitle, _ = os.path.splitext(os.path.basename(args.theirs))
    mtitle, _ = os.path.splitext(os.path.basename(args.mine))
    
    output = '{} - {}.dmmpatch'.format(ttitle, mtitle)
    if args.output:
        output = args.output
    with open(output, 'w') as f:
        stats = {
            'diffs':0,
            'tilediffs':0
        }
        print('Comparing maps...')
        for z in range(len(theirs_dmm.zLevels)):
            for y in range(theirs_dmm.height):
                for x in range(theirs_dmm.width):
                    CHANGES = {}
                    
                    tTile = theirs_dmm.GetTileAt(x, y, z)
                    mTile = mine_dmm.GetTileAt(x, y, z)
                    
                    theirs = tTile.GetAtoms()
                    mine = mTile.GetAtoms()
                    
                    for atom in theirs + mine:
                        key = atom.MapSerialize()
                        change = None
                        if atom not in mine:
                            change = '-'
                        if atom not in theirs:
                            change = '+'
                        if change is not None:
                            CHANGES[key] = change
                    
                    if len(CHANGES) > 0:
                        f.write('<{},{},{}>\n'.format(x, y, z))
                        stats['tilediffs'] += 1
                        for key, change in CHANGES.items():
                            f.write(' {} {}\n'.format(change, key))
                            stats['diffs'] += 1
        print('Compared maps: {} differences in {} tiles.'.format(stats['diffs'],stats['tilediffs']))
Beispiel #4
0
def analyze_dmm(args):
    tmpl_head = '''
<html>
    <head>
        <title>BYONDTools Map Analysis :: {TITLE}</title>
    </head>
    <body>
        <h1>{TITLE}</h1>
        <ul>
            <li><a href="{ROOT}/instances/index.html">Instances</a></li>
            <li><a href="{ROOT}/index.html">Tiles</a></li>
        </ul>'''
    tmpl_footer = '''
    </body>
</html>
    '''
    
    presentable_attributes=[
        'path',
        'id',
        'filename',
        'line'
    ]

    def MakePage(**kwargs):
        rewt = '.'
        depth = kwargs.get('depth', 0)
        if depth > 0:
            rewt = '/'.join(['..'] * depth)
        title = kwargs.get('title', 'LOL NO TITLE')
        body = kwargs.get('body', '')
        return (tmpl_head + body + tmpl_footer).replace('{TITLE}', title).replace('{ROOT}', rewt)

    if not os.path.isfile(args.project):
        print('DM Environment file {0} does not exist.'.format(args.theirs))
        sys.exit(1)

    if not os.path.isfile(args.map):
        print('Map {0} does not exist.'.format(args.theirs))
        sys.exit(1)

    tree = ObjectTree()
    tree.ProcessFilesFromDME(args.project)
    dmm = Map(tree)
    dmm.readMap(args.map)
    
    basedir = os.path.join(os.path.dirname(args.project), 'analysis', os.path.basename(args.map))
    
    if not os.path.isdir(basedir):
        os.makedirs(os.path.join(basedir,'instances'))
        os.makedirs(os.path.join(basedir,'tiles'))
    
    # Dump instances
    instance_info = {}
    for atom in dmm.instances:
        if atom.path not in instance_info:
            instance_info[atom.path] = []
        instance_info[atom.path] += [atom.id]
        
        with open(os.path.join(basedir, 'instances', str(atom.id) + '.html'), 'w') as f:
            body = '<h2>Atom Data:</h2><table class="prettytable"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody>'
            for attr in presentable_attributes: 
                body += '<tr><th>{0}</th><td>{1}</td></tr>'.format(attr, getattr(atom, attr, None))
            body += '</tbody></table>'
            
            body += '<h2>Map-Specified Properties:</h2><table class="prettytable"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody>'
            for attr_name in atom.mapSpecified:
                body += '<tr><th>{0}</th><td>{1}</td></tr>'.format(attr_name, atom.getProperty(attr_name, None))
            body += '</tbody></table>'
            
            body += '<h2>All Properties:</h2><table class="prettytable"><thead><tr><th>Name</th><th>Value</th><th>File/Line</th></tr></thead><tbody>'
            for attr_name in sorted(atom.properties.keys()):
                attr = atom.properties[attr_name]
                body += '<tr><th>{0}</th><td>{1}</td><td>{2}:{3}</td></tr>'.format(attr_name, attr.value, attr.filename, attr.line)
            body += '</tbody></table>'
            f.write(MakePage(title='Instance #{0}'.format(atom.id), depth=1, body=body))
    with open(os.path.join(basedir, 'instances', 'index.html'), 'w') as idx:
        body = '<ul>'
        for atype, instances in instance_info.items():
            body += '<li><b>{0}</b><ul>'.format(atype)
            for iid in instances:
                body += '<li><a href="{{ROOT}}/instances/{0}.html">#{0}</a></li>'.format(iid)
            body += '</ul></li>'
        body += "</ul>"
        idx.write(MakePage(title='Instance Index'.format(atom.id), depth=1, body=body))
        
    # Tiles
    with open(os.path.join(basedir, 'index.html'), 'w') as f:
        body = '<table class="prettytable"><thead><tr><th>Icon</th><th>ID</th><th>Instances</th></tr></thead><tbody>'
        for tile in dmm.tileTypes:
            body += '<tr><td><img src="tiles/{0}.png" height="96" width="96" /></td><th>{0}</th><td><ul>'.format(tile.ID)
            for atom in tile.SortAtoms():
                body += '<li><a href="{{ROOT}}/instances/{0}.html">#{0}</a> - {1}</li>'.format(atom.id,atom.path)
            body += '</ul></td></tr>'
            img = tile.RenderToMapTile(0, os.path.dirname(sys.argv[1]), MapRenderFlags.RENDER_STARS)
            if img is None: continue
            pass_2 = tile.RenderToMapTile(1, os.path.dirname(sys.argv[1]), 0)
            if pass_2 is not None:
                img.paste(pass_2,(0,0,96,96),pass_2)
            img.save(os.path.join(basedir, 'tiles', '{0}.png'.format(tile.ID)), 'PNG')
        body += '</tbody></table>'
        f.write(MakePage(title='Tile Index'.format(atom.id), depth=0, body=body))
Beispiel #5
0
def analyze_dmm(args):
    tmpl_head = '''
<html>
    <head>
        <title>BYONDTools Map Analysis :: {TITLE}</title>
    </head>
    <body>
        <h1>{TITLE}</h1>
        <ul>
            <li><a href="{ROOT}/instances/index.html">Instances</a></li>
            <li><a href="{ROOT}/index.html">Tiles</a></li>
        </ul>'''
    tmpl_footer = '''
    </body>
</html>
    '''

    presentable_attributes = ['path', 'id', 'filename', 'line']

    def MakePage(**kwargs):
        rewt = '.'
        depth = kwargs.get('depth', 0)
        if depth > 0:
            rewt = '/'.join(['..'] * depth)
        title = kwargs.get('title', 'LOL NO TITLE')
        body = kwargs.get('body', '')
        return (tmpl_head + body + tmpl_footer).replace('{TITLE}',
                                                        title).replace(
                                                            '{ROOT}', rewt)

    if not os.path.isfile(args.project):
        print('DM Environment file {0} does not exist.'.format(args.theirs))
        sys.exit(1)

    if not os.path.isfile(args.map):
        print('Map {0} does not exist.'.format(args.theirs))
        sys.exit(1)

    tree = ObjectTree()
    tree.ProcessFilesFromDME(args.project)
    dmm = Map(tree)
    dmm.readMap(args.map)

    basedir = os.path.join(os.path.dirname(args.project), 'analysis',
                           os.path.basename(args.map))

    if not os.path.isdir(basedir):
        os.makedirs(os.path.join(basedir, 'instances'))
        os.makedirs(os.path.join(basedir, 'tiles'))

    # Dump instances
    instance_info = {}
    for atom in dmm.instances:
        if atom.path not in instance_info:
            instance_info[atom.path] = []
        instance_info[atom.path] += [atom.ID]

        with open(os.path.join(basedir, 'instances',
                               str(atom.ID) + '.html'), 'w') as f:
            body = '<h2>Atom Data:</h2><table class="prettytable"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody>'
            for attr in presentable_attributes:
                body += '<tr><th>{0}</th><td>{1}</td></tr>'.format(
                    attr, getattr(atom, attr, None))
            body += '</tbody></table>'

            body += '<h2>Map-Specified Properties:</h2><table class="prettytable"><thead><tr><th>Name</th><th>Value</th></tr></thead><tbody>'
            for attr_name in atom.mapSpecified:
                body += '<tr><th>{0}</th><td>{1}</td></tr>'.format(
                    attr_name, atom.getProperty(attr_name, None))
            body += '</tbody></table>'

            body += '<h2>All Properties:</h2><table class="prettytable"><thead><tr><th>Name</th><th>Value</th><th>File/Line</th></tr></thead><tbody>'
            for attr_name in sorted(atom.properties.keys()):
                attr = atom.properties[attr_name]
                body += '<tr><th>{0}</th><td>{1}</td><td>{2}:{3}</td></tr>'.format(
                    attr_name, attr.value, attr.filename, attr.line)
            body += '</tbody></table>'
            f.write(
                MakePage(title='Instance #{0}'.format(atom.ID),
                         depth=1,
                         body=body))
    with open(os.path.join(basedir, 'instances', 'index.html'), 'w') as idx:
        body = '<ul>'
        for atype, instances in instance_info.items():
            body += '<li><b>{0}</b><ul>'.format(atype)
            for iid in instances:
                body += '<li><a href="{{ROOT}}/instances/{0}.html">#{0}</a></li>'.format(
                    iid)
            body += '</ul></li>'
        body += "</ul>"
        idx.write(
            MakePage(title='Instance Index'.format(atom.ID),
                     depth=1,
                     body=body))

    # Tiles
    with open(os.path.join(basedir, 'index.html'), 'w') as f:
        body = '<table class="prettytable"><thead><tr><th>Icon</th><th>ID</th><th>Instances</th></tr></thead><tbody>'
        for tile in dmm.tileTypes:
            body += '<tr><td><img src="tiles/{0}.png" height="96" width="96" /></td><th>{0}</th><td><ul>'.format(
                tile.ID)
            for atom in tile.SortAtoms():
                body += '<li><a href="{{ROOT}}/instances/{0}.html">#{0}</a> - {1}</li>'.format(
                    atom.ID, atom.path)
            body += '</ul></td></tr>'
            img = tile.RenderToMapTile(0, os.path.dirname(sys.argv[1]),
                                       MapRenderFlags.RENDER_STARS)
            if img is None: continue
            pass_2 = tile.RenderToMapTile(1, os.path.dirname(sys.argv[1]), 0)
            if pass_2 is not None:
                img.paste(pass_2, (0, 0, 96, 96), pass_2)
            img.save(os.path.join(basedir, 'tiles', '{0}.png'.format(tile.ID)),
                     'PNG')
        body += '</tbody></table>'
        f.write(
            MakePage(title='Tile Index'.format(atom.ID), depth=0, body=body))
Beispiel #6
0
def compare_dmm(args):
    if not os.path.isfile(args.theirs):
        print('File {0} does not exist.'.format(args.theirs))
        sys.exit(1)
    if not os.path.isfile(args.mine):
        print('File {0} does not exist.'.format(args.mine))
        sys.exit(1)
    if not os.path.isfile(args.project):
        print('DM Environment File {0} does not exist.'.format(args.project))
        sys.exit(1)

    theirs_dmm = Map(forgiving_atom_lookups=True)
    theirs_dmm.Load(args.theirs, format='dmm')

    mine_dmm = Map(forgiving_atom_lookups=True)
    mine_dmm.Load(args.mine, format='dmm')

    ttitle, _ = os.path.splitext(os.path.basename(args.theirs))
    mtitle, _ = os.path.splitext(os.path.basename(args.mine))

    format = DMMFormat(theirs_dmm)

    output = '{} - {}.dmmpatch'.format(ttitle, mtitle)

    if args.output:
        output = args.output
    with open(output, 'w') as f:
        stats = {'diffs': 0, 'tilediffs': 0, 'tiles': 0, 'atoms': 0}
        print('Comparing maps...')
        for z in range(len(theirs_dmm.zLevels)):
            t_zlev = theirs_dmm.zLevels[z]
            m_zlev = mine_dmm.zLevels[z]
            if t_zlev.height != m_zlev.height or t_zlev.width != m_zlev.width:
                print(
                    '!!! ZLEVEL {} HEIGHT/WIDTH MISMATCH: ({},{}) != ({},{})'.
                    format(z, t_zlev.height, t_zlev.width, m_zlev.height,
                           m_zlev.width))
                continue
            print(" Scanning z-level {} ({}x{})...".format(
                z, t_zlev.height, t_zlev.width))
            for y in range(t_zlev.height):
                for x in range(t_zlev.width):
                    CHANGES = {}

                    tTile = theirs_dmm.GetTileAt(x, y, z)
                    # if tTile is None:
                    #    print('!!! THEIRS <{},{},{}>: Tile object is None!'.format(x, y, z))
                    #    #return
                    mTile = mine_dmm.GetTileAt(x, y, z)
                    # if tTile is None:
                    #    print('!!! MINE <{},{},{}>: Tile object is None!'.format(x, y, z))
                    #    #return

                    theirs = {}
                    mine = {}
                    all_keys = set()

                    if tTile:
                        for A in tTile.GetAtoms():
                            key = format.SerializeAtom(A)
                            all_keys.add(key)
                            if key not in theirs:
                                theirs[key] = [A, 0]
                            theirs[key][1] += 1
                    if mTile:
                        for A in mTile.GetAtoms():
                            key = format.SerializeAtom(A)
                            all_keys.add(key)
                            if key not in mine:
                                mine[key] = [A, 0]
                            mine[key][1] += 1

                    removals = set()
                    additions = set()
                    kept = {}

                    for key in all_keys:
                        change = None
                        minecount = 0
                        if key in mine:
                            minecount = mine[key][1]
                        theircount = 0
                        if key in theirs:
                            theircount = theirs[key][1]
                        delta = minecount - theircount
                        if delta < 0:
                            change = '-'
                            removals.add(key)
                        elif delta > 0:
                            change = '+'
                            additions.add(key)
                        if minecount > 0 and delta <= 0:
                            kept[key] = minecount
                        if change is not None:
                            CHANGES[key] = [
                                change,
                                abs(delta), minecount, theircount
                            ]
                        stats['tiles'] += 1

                    def writeChanges(f, source, CHANGES):
                        for key in source:
                            change, amount, mc, tc = CHANGES[key]
                            # change, amount, _, _ = changedat
                            # f.write(' # {} vs {}\n'.format(mc, tc))
                            abs_amt = abs(amount)
                            if abs_amt > 1:
                                f.write(' {}{} {}\n'.format(
                                    change, abs_amt if mc > 0 else '*', key))
                            else:
                                f.write(' {} {}\n'.format(change, key))
                            stats['diffs'] += abs_amt

                    if len(CHANGES) > 0:
                        f.write('<{},{},{}>\n'.format(x, y, z))
                        stats['tilediffs'] += 1
                        f.write(' @CHECK {before} {after} {tiledat}\n'.format(
                            before=tTile.GetHash(),
                            after=mTile.GetHash(),
                            tiledat=format.SerializeTile(mTile)))
                        if len(kept) == 0:
                            f.write(' -ALL\n')
                        else:
                            writeChanges(f, removals, CHANGES)
                        """
                        for key,count in kept.items():
                            if count > 1:
                                f.write(' ={} {}\n'.format(count, key))
                            else:
                                f.write(' = {}\n'.format(key))
                        """
                        writeChanges(f, additions, CHANGES)
        print('Compared maps: {} differences in {} tiles.'.format(
            stats['diffs'], stats['tilediffs']))
        print('Total: {} atoms, {} tiles.'.format(stats['diffs'],
                                                  stats['tilediffs']))
Beispiel #7
0
def patch_dmm(args):
    print(repr(args.patches))
    for i in range(len(args.patches[0])):
        patch = args.patches[0][i]
        if not os.path.isfile(patch):
            print('File {0} does not exist.'.format(patch))
            sys.exit(1)
    if not os.path.isfile(args.map):
        print('File {0} does not exist.'.format(args.mine))
        sys.exit(1)
    if not os.path.isfile(args.project):
        print('DM Environment File {0} does not exist.'.format(args.project))
        sys.exit(1)

    dmm = Map(forgiving_atom_lookups=True)
    dmm.Load(args.map, format='dmm')

    fmt = DMMFormat(dmm)

    context = None
    added = 0
    removed = 0

    currentpatch = ''

    def changeMarker(ln):
        a = Atom('/obj/effect/byondtools/changed')
        a.setProperty('tag',
                      '{}:{}'.format(currentpatch, ln),
                      flags=PropertyFlags.MAP_SPECIFIED)
        return a

    def printReport(context, added, removed):
        if context is not None:
            x, y, z = context
            print(' Z={} +{} -{}'.format(z, added, removed))

    REG_INSTRUCTION = re.compile(
        r'^(?P<change>[\+\-])(?P<amount>[0-9\*]+)?\s+(?P<atom>/.*)')

    for i in range(len(args.patches[0])):
        patch = args.patches[0][i]
        print('* Applying {}...'.format(patch))
        currentpatch = patch
        with open(patch) as f:
            ln = 0
            lz = -1
            skip_block = False
            for line in f:
                ln += 1
                line = line.strip()
                if line == '': continue
                if line.startswith('#'): continue
                if line.startswith('<') and line.endswith('>'):
                    strcoords = line.strip('<>').split(',')
                    newcoords = []
                    for coord in strcoords:
                        newcoords += [int(coord)]
                    if context is not None and lz != context[2]:
                        printReport(context, added, removed)
                        lz = context[2]
                        added = removed = 0
                    context = newcoords
                    skip_block = False
                    continue
                if line.startswith('@') and not skip_block:
                    # @CHECK before-hash after-hash atoms-block

                    x, y, z = context

                    if line.startswith('@CHECK'):
                        _, beforehash, afterhash, serdata = line.split(' ', 3)

                        if args.clobber:
                            #print('WARNING: <{},{},{}> has changed and will be overwritten. (--clobber)'.format(x, y, z))
                            t = fmt.consumeTileChunk(serdata, cache=False)
                            t.AppendAtom(changeMarker(ln))
                            dmm.SetTileAt(x, y, z, t)
                            skip_block = True
                            continue
                        curhash = dmm.GetTileAt(x, y, z).GetHash()
                        if afterhash == curhash:
                            print(
                                'Skipping <{},{},{}> (already what we expected)'
                                .format(x, y, z))
                            skip_block = True
                            continue
                        #else: print('PRE {} != {}: OK'.format(curhash,afterhash))
                        if beforehash != curhash:
                            print(
                                'WARNING: <{},{},{}> has changed.  Operations on this tile may not be accurate!'
                                .format(x, y, z))
                            continue
                        #else: print('OLD {} == {}: OK'.format(curhash,beforehash))

                if not skip_block and (line.startswith('+')
                                       or line.startswith('-')):
                    if line == '-ALL':
                        dmm.SetTileAt(x, y, z, dmm.CreateTile())
                        continue
                    m = REG_INSTRUCTION.match(line)
                    if m is None:
                        print('{}:{}: MALFORMED INSTRUCTION: {}'.format(
                            args.patch, ln, line))
                        sys.exit(1)
                    amount = m.group('amount')
                    if amount == '*':
                        amount = 9999
                    else:
                        amount = int(m.group('amount') or 1)
                    change = m.group('change')

                    atom = fmt.consumeAtom(m.group('atom'), ln)
                    atom.filename = args.patch
                    atom.line = ln
                    if atom is None:
                        print(
                            '{}:{}: WARNING: Unable to parse instance specified by chunk {}'
                            .format(args.patch, ln, m.group('atom')))
                        continue

                    x, y, z = context
                    if x == 0 and y == 0 and z == 0:
                        print('WE AT <0,0,0> SOMEFIN WRONG')

                    if z >= len(dmm.zLevels): continue

                    tile = dmm.GetTileAt(x, y, z)
                    if change == '-':
                        for _ in range(amount):
                            if tile.CountAtom(atom) > 0:
                                tile.RemoveAtom(atom, hash=False)
                                removed += 1
                            else:
                                break
                    elif change == '+':
                        for _ in range(amount):
                            tile.AppendAtom(atom, hash=False)
                        added += amount
                    tile.UpdateHash()
                    # dmm.SetTileAt(x, y, z, tile)
                    # print('{} - {}'.format((x,y,z),tile))
            printReport(context, added, removed)

    print('Saving...')
    dmm.Save(args.output if args.output else args.map)
Beispiel #8
0
def compare_dmm(args):
    if not os.path.isfile(args.theirs):
        print('File {0} does not exist.'.format(args.theirs))
        sys.exit(1)
    if not os.path.isfile(args.mine):
        print('File {0} does not exist.'.format(args.mine))
        sys.exit(1)
    if not os.path.isfile(args.project):
        print('DM Environment File {0} does not exist.'.format(args.project))
        sys.exit(1)

    theirs_dmm = Map(forgiving_atom_lookups=True)
    theirs_dmm.readMap(args.theirs)

    mine_dmm = Map(forgiving_atom_lookups=True)
    mine_dmm.readMap(args.mine)

    if theirs_dmm.width != mine_dmm.width:
        print('Width is not equal: {} != {}.'.format(theirs_dmm.width,
                                                     mine_dmm.width))
        sys.exit(1)
    if theirs_dmm.height != mine_dmm.height:
        print('Height is not equal: {} != {}.'.format(theirs_dmm.height,
                                                      mine_dmm.height))
        sys.exit(1)

    ttitle, _ = os.path.splitext(os.path.basename(args.theirs))
    mtitle, _ = os.path.splitext(os.path.basename(args.mine))

    output = '{} - {}.dmmpatch'.format(ttitle, mtitle)
    if args.output:
        output = args.output
    with open(output, 'w') as f:
        stats = {'diffs': 0, 'tilediffs': 0}
        print('Comparing maps...')
        for z in range(len(theirs_dmm.zLevels)):
            for y in range(theirs_dmm.height):
                for x in range(theirs_dmm.width):
                    CHANGES = {}

                    tTile = theirs_dmm.GetTileAt(x, y, z)
                    mTile = mine_dmm.GetTileAt(x, y, z)

                    theirs = tTile.GetAtoms()
                    mine = mTile.GetAtoms()

                    for atom in theirs + mine:
                        key = atom.MapSerialize()
                        change = None
                        if atom not in mine:
                            change = '-'
                        if atom not in theirs:
                            change = '+'
                        if change is not None:
                            CHANGES[key] = change

                    if len(CHANGES) > 0:
                        f.write('<{},{},{}>\n'.format(x, y, z))
                        stats['tilediffs'] += 1
                        for key, change in CHANGES.items():
                            f.write(' {} {}\n'.format(change, key))
                            stats['diffs'] += 1
        print('Compared maps: {} differences in {} tiles.'.format(
            stats['diffs'], stats['tilediffs']))