#!/usr/bin/env python #-*- coding:utf-8 -*- import sys import os import subprocess import re import tempfile class MapDiff(object): @staticmethod def check_programs(): """ Checks the require programs are available """ def which(program): import os def is_exe(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) fpath, fname = os.path.split(program) if fpath: if is_exe(program): return program else: for path in os.environ["PATH"].split(os.pathsep): exe_file = os.path.join(path, program) if is_exe(exe_file): return exe_file return None platform_programs = MapDiff.PROGRAMS.get(sys.platform, MapDiff.PROGRAMS.get('default')) for program in platform_programs.values(): if not which(program): raise Exception('The required "%s" program is missing from your PATH.' % program) MAP_RE = re.compile(r'^\d{3}-\d{1}(\.tmx)?$') PROGRAMS = { 'default': { 'tmxrasterizer': 'tmxrasterizer', 'im_convert': 'convert', 'im_display': 'display', 'git': 'git', }, 'win32': { 'tmxrasterizer': 'tmxrasterizer.exe', 'im_convert': 'convert.exe', 'im_display': 'display.exe', 'git': 'git.exe', }, } def __init__(self): self.platform_programs = MapDiff.PROGRAMS.get(sys.platform, MapDiff.PROGRAMS.get('default')) def _diffmaps(self, tmx1, tmx2, tmxdiffpath): tmxraster1 = self._rastermap(tmx1) tmxraster2 = self._rastermap(tmx2) tmxf, tmxdiff = tempfile.mkstemp(suffix='.png') subprocess.check_call([ self.platform_programs.get('im_convert'), tmxraster1, tmxraster2, '-compose', 'Difference', '-auto-level', '-composite', tmxraster2, '-compose', 'Screen', '-composite', tmxdiffpath ]) os.unlink(tmxdiff) os.unlink(tmxraster1) os.unlink(tmxraster2) sys.stdout.write((u'Map diff written to %s\n' % tmxdiffpath).encode('utf-8')) subprocess.check_call([self.platform_programs.get('im_display'), tmxdiffpath]) def _rastermap(self, tmx): tmxf, tmxraster = tempfile.mkstemp(suffix='.png') subprocess.check_call([ self.platform_programs.get('tmxrasterizer'), '--scale', '1.0', tmx, tmxraster ]) if os.stat(tmxraster).st_size == 0: raise Exception('A problem was encountered when rendering a map') return tmxraster class MapGitRevDiff(MapDiff): def __init__(self, map_name): super(MapGitRevDiff, self).__init__() self.map_name = map_name def diff(self): if not MapDiff.MAP_RE.match(self.map_name): sys.stderr.write(u'Invalid map name: %s.\n' % self.map_name) return 1 if not self.map_name.endswith(u'.tmx'): self.map_name = self.map_name+u'.tmx' self.tmx_path = os.path.join(u'..', u'maps', self.map_name) self.map_number = os.path.splitext(os.path.basename(self.map_name))[0] p = subprocess.Popen([self.platform_programs.get('git'), '--no-pager', 'log', '-n', '2', '--oneline', '--follow', self.tmx_path], stdout=subprocess.PIPE) log = p.communicate()[0].splitlines() if not len(log) == 2: raise Exception('This map has only one version') c1 = log[0].split(' ')[0] c2 = log[1].split(' ')[0] # We have the 2 revs to compare. Let's extract the related tmx file p1 = self._mktmx_from_rev(c1) p2 = self._mktmx_from_rev(c2) try: difftmxpath = '%s_%s-%s.png' % (self.map_number, c1, c2) self._diffmaps(p1, p2, difftmxpath) finally: os.unlink(p1) os.unlink(p2) def _mktmx_from_rev(self, rev): p = subprocess.Popen([self.platform_programs.get('git'), '--no-pager', 'show', '%s:%s' % (rev, self.tmx_path)], stdout=subprocess.PIPE) contents = p.communicate()[0] revtmx = '%s-%s.tmx' % (self.map_number, rev) f = open(revtmx, 'w') f.write(contents) f.close() return revtmx class MapFileDiff(MapDiff): def __init__(self, map1, map2): super(MapFileDiff, self).__init__() self.map1 = map1 self.map2 = map2 def diff(self): b1 = os.path.splitext(os.path.basename(self.map1))[0] b2 = os.path.splitext(os.path.basename(self.map2))[0] difftmxpath = '%s__%s.png' % (b1, b2) self._diffmaps(self.map1, self.map2, difftmxpath) def usage(): sys.stderr.write(u'''Usage: %s MAP_NAME %s CHANGED_TMX REFERENCE_TMX Example: $ ./map-diff.py 007-1 will highlight the changes between the current 007-1 map and its previous version $ ./map-diff.py changes-made-by-someone-007-1.tmx ../maps-007-1.tmx will highlight the changes between the two tmx maps. Note that these 2 tmx to compare have to satisfy their dependancies, e.g tilesets. Hence they should be in a sibling directory of the client-data/maps folder. \n''' % (sys.argv[0], sys.argv[0])) def main(): if not len(sys.argv) > 1: usage() return 127 if not os.path.basename(os.path.dirname(os.getcwdu())) == u'client-data': sys.stderr.write(u'This script must be run from client-data/tools.\n') return 1 try: MapDiff.check_programs() except Exception as e: sys.stderr.write(u'%s\n' % e) return 126 if len(sys.argv) == 2: map_name = sys.argv[1] mapdiff = MapGitRevDiff(map_name) try: mapdiff.diff() except Exception as e: sys.stderr.write(u'\x1b[31m\x1b[1mError while generating the diff for map %s: %s\x1b[0m\n' % (map_name, e)) return 1 else: return 0 else: map1 = sys.argv[1] map2 = sys.argv[2] mapdiff = MapFileDiff(map1, map2) try: mapdiff.diff() except Exception as e: sys.stderr.write(u'\x1b[31m\x1b[1mError while generating the diff for %s and %s: %s\x1b[0m\n' % (map1, map2, e)) return 1 else: return 0 if __name__ == '__main__': sys.exit(main())