#!/usr/bin/env python # # Up-convert WML and maps between versions. # # By Eric S. Raymond April 2007. # # All conversion logic for lifting WML and maps from older versions of the # markup to newer ones should live here. This includes resource path changes # and renames, also map format conversions. # # The recommended procedure is this: # 1. Run it with --dryrun first to see what it will do. # 2. If the messages look good, run without --dryrun; the old content # will be left in backup files with a -bak extension. # 3. Test the conversion. # 4. Use either --clean to remove the -bak files or --revert to # undo the conversion. # # Note about the 1.3.1 -> 1.3.2 map conversion: terrain codes will only be # spotted and converted when preceded by space, comma, or equal sign. This # will handle maps (either in their own files or included as a WML attribute) # and the most common cases in WML (e.g. filters and unit declarations). import sys, os, re, getopt filemoves = { # Older includes all previous to 1.3.1. "older" : ( ("creepy.ogg", "underground.ogg"), ("eagle.wav", "gryphon-shriek-1.ogg"), ("lightning.wav", "lightning.ogg"), # Bug fix ), "1.3.1" : ( # Peasant images moved to a new directory ("human-loyalists/peasant.png", "human-peasants/peasant.png"), ("human-loyalists/peasant-attack.png", "human-peasants/peasant-attack.png"), ("human-loyalists/peasant-attack2.png", "human-peasants/peasant-attack2.png"), ("human-loyalists/peasant-ranged.png", "human-peasants/peasant-ranged.png"), ("human-loyalists/peasant-idle-1.png", "human-peasants/peasant-idle-1.png"), ("human-loyalists/peasant-idle-2.png", "human-peasants/peasant-idle-2.png"), ("human-loyalists/peasant-idle-3.png", "human-peasants/peasant-idle-3.png"), ("human-loyalists/peasant-idle-4.png", "human-peasants/peasant-idle-4.png"), ("human-loyalists/peasant-idle-5.png", "human-peasants/peasant-idle-5.png"), ("human-loyalists/peasant-idle-6.png", "human-peasants/peasant-idle-6.png"), ("human-loyalists/peasant-idle-7.png", "human-peasants/peasant-idle-7.png"), # All Great Mage attacks were renamed ("great-mage-attack-magic1.png", "great-mage-attack-magic-1.png"), ("great-mage-attack-magic2.png", "great-mage-attack-magic-2.png"), ("great-mage+female-attack-magic1.png", "great-mage+female-attack-magic-1.png"), ("great-mage+female-attack-magic2.png", "great-mage+female-attack-magic-2.png"), ("great-mage-attack-staff1.png", "great-mage-attack-staff-1.png"), ("great-mage-attack-staff2.png", "great-mage-attack-staff-2.png"), ("great-mage+female-attack-staff1.png", "great-mage+female-attack-staff-1.png"), ("great-mage+female-attack-staff2.png", "great-mage+female-attack-staff-2.png"), # All Arch Mage attacks were renamed ("arch-mage-attack-magic1.png", "arch-mage-attack-magic-1.png"), ("arch-mage-attack-magic2.png", "arch-mage-attack-magic-2.png"), ("arch-mage+female-attack-magic1.png", "arch-mage+female-attack-magic-1.png"), ("arch-mage+female-attack-magic2.png", "arch-mage+female-attack-magic-2.png"), ("arch-mage-attack-staff1.png", "arch-mage-attack-staff-1.png"), ("arch-mage-attack-staff2.png", "arch-mage-attack-staff-2.png"), ("arch-mage+female-attack-staff1.png", "arch-mage+female-attack-staff-1.png"), ("arch-mage+female-attack-staff2.png", "arch-mage+female-attack-staff-2.png"), # All Red Mage attacks were renamed ("red-mage-attack-magic1.png", "red-mage-attack-magic-1.png"), ("red-mage-attack-magic2.png", "red-mage-attack-magic-2.png"), ("red-mage+female-attack-magic1.png", "red-mage+female-attack-magic-1.png"), ("red-mage+female-attack-magic2.png", "red-mage+female-attack-magic-2.png"), ("red-mage-attack-staff1.png", "red-mage-attack-staff-1.png"), ("red-mage-attack-staff2.png", "red-mage-attack-staff-2.png"), ("red-mage+female-attack-staff1.png", "red-mage+female-attack-staff-1.png"), ("red-mage+female-attack-staff2.png", "red-mage+female-attack-staff-2.png"), # Timothy Pinkham supplied titles for two of his music files. # Zhaytee supplied a title for wesnoth-1.ogg # gameplay03.ogg, and and wesnoth-[25].ogg already had titles. ("gameplay01.ogg", "knolls.ogg"), ("gameplay02.ogg", "wanderer.ogg"), ("gameplay03.ogg", "battle.ogg"), ("wesnoth-1.ogg", "revelation.ogg"), ("wesnoth-2.ogg", "loyalists.ogg"), ("wesnoth-5.ogg", "northerners.ogg"), ), # An empty sentinel value at end is required. # Always have the current version here. "1.3.2" : (), } # 1.3.1 -> 1.3.2 terrain conversions terrain_conversions = { re.compile(r"(?<=[ ,=])Bww([|/\\])\b") : "Ww^Bw\\1", re.compile(r"(?<=[ ,=])Bwo([|/\\])\b") : "Wo^Bw\\1", re.compile(r"(?<=[ ,=])Bss([|/\\])\b") : "Ss^Bw\\1", re.compile(r"(?<=[ ,=])Dc\b") : "Dd^Dc", re.compile(r"(?<=[ ,=])Dr\b") : "Dd^Dr", re.compile(r"(?<=[ ,=])Do\b") : "Dd^Do", re.compile(r"(?<=[ ,=])Fa\b") : "Aa^Fpa", re.compile(r"(?<=[ ,=])Fet\b") : "Gg^Fet", re.compile(r"(?<=[ ,=])Ff\b") : "Gs^Fp", re.compile(r"(?<=[ ,=])Ft\b") : "Gs^Ft", re.compile(r"(?<=[ ,=])Rfvs\b") : "Re^Gvs", re.compile(r"(?<=[ ,=])Uf\b") : "Uu^Uf", re.compile(r"(?<=[ ,=])Uui\b") : "Uu^Ii", re.compile(r"(?<=[ ,=])Uhi\b") : "Uh^Ii", re.compile(r"(?<=[ ,=])Vda\b") : "Dd^Vda", re.compile(r"(?<=[ ,=])Vdt\b") : "Dd^Vdt", re.compile(r"(?<=[ ,=])Vea\b") : "Aa^Vea", re.compile(r"(?<=[ ,=])Veg\b") : "Gg^Ve", re.compile(r"(?<=[ ,=])Vha\b") : "Aa^Vha", re.compile(r"(?<=[ ,=])Vhg\b") : "Gg^Vh", re.compile(r"(?<=[ ,=])Vhh\b") : "Hh^Vhh", re.compile(r"(?<=[ ,=])Vhha\b") : "Ha^Vhha", re.compile(r"(?<=[ ,=])Vhm\b") : "Mm^Vhh", re.compile(r"(?<=[ ,=])Vht\b") : "Gs^Vht", re.compile(r"(?<=[ ,=])Vu\b") : "Uu^Vu", re.compile(r"(?<=[ ,=])Vud\b") : "Uu^Vud", re.compile(r"(?<=[ ,=])Vwm\b") : "Ww^Vm", re.compile(r"(?<=[ ,=])Vs\b") : "Ss^Vhs", re.compile(r"(?<=[ ,=])Vsm\b") : "Ss^Vm", re.compile(r"(?<=[ ,=])Xm\b") : "Mm^Xm" } class maptransform_error: "Error object to be thrown by maptransform." def __init__(self, infile, inline, imap, x, y, type): self.infile = infile self.inline = inline self.x = x self.y = y self.type = type def __str__(self): return '"%s", line %d: %s at (%d, %d)\n' % \ (self.input, self.inline, self.type, self.x, self.y) def translator(input, mapxform, textxform): "Apply mapxform to map lines and textxform to non-map lines." modified = False # This hairy regexp excludes map_data lines that contain {} file # references, also lines that are empty or hold just one keep # character (somewhat pathological, but not handling these will # make the regression tests break). mapdata = re.compile(r'map_data="[A-Za-z0-9\/|\\&_~?\[\]\']{2,}') mfile = [] map_only = not input.endswith(".cfg") for line in open(input): mfile.append(line); if mapdata.search(line): map_only = False cont = False outmap = [] newdata = [] lineno = baseline = 0 while mfile: line = mfile.pop(0) lineno += 1 if map_only or mapdata.search(line): baseline = 0 cont = True # Assumes map is more than 1 line long. if not map_only: line = line.split('"')[1] if line: outmap.append(line) while cont and mfile: line = mfile.pop(0) lineno += 1 if line and line[0] == '#': newdata.append(line) continue if '"' in line: cont = False line = line.split('"')[0] if line and not line.endswith("\n"): line += "\n" if line: outmap.append(line) if not map_only: line="map_data=\"\n"; newdata.append(line) for y in range(len(outmap)): newline = mapxform(input, baseline, outmap, y) newdata.append(newline) if newline != outmap[y]: modified = True # All lines of the map are processed, add the appropriate trailer if map_only: line="\n" else: line="\"\n" newdata.append(line) else: # Handle text (non-map) lines newline = textxform(input, lineno, line) newdata.append(newline) if newline != line: modified = True # Return None if the transformation functions made no changes. if modified: return "".join(newdata) else: return None def allcfgfiles(dir): "Get the names of all .cfg files under dir, ignoring .svn directories." datafiles = [] os.path.walk(dir, lambda arg, dir, names: datafiles.extend(map(lambda x: os.path.normpath(os.path.join(dir, x)), names)), None) datafiles = filter(lambda x: ".svn" not in x, datafiles) datafiles = filter(lambda x: x.endswith(".cfg") or ('maps' in x and os.path.isfile(x)), datafiles) return datafiles def help(): sys.stderr.write("""\ Usage: upconvert [options] Convert Battle of Wesnoth WML from older versions to newer ones. Options may be any of these: -h, --help Emit this help message and quit -d, --dryrun List changes but don't perform them. -o, --oldversion Specify version to begin with. -v, --verbose List files as they are examined. -c, --clean Clean up -bak files -r, --revert Revert the conversion from the -bak files """) def texttransform(input, lineno, line): "Resource-name transformation on text lines." transformed = line for step in fileconversions: for (old, new) in step: transformed = transformed.replace(old, new) if transformed != line: print "%s, line %d: %s -> %s" % \ (input, lineno+1, line.strip(), transformed.strip()) return transformed def maptransform(input, baseline, inmap, y): "Convert a map line from 1.3.1 multiletter format to 1.3.2 format." mapline = inmap[y] for (old, new) in terrain_conversions.items(): mapline = old.sub(new, mapline) return mapline if __name__ == '__main__': (options, arguments) = getopt.getopt(sys.argv[1:], "cdho:rv", [ "help", "oldversion=", "dryrun", "verbose", "clean", "revert", ]) oldversion = 'older' dryrun = False verbose = False clean = False revert = False for (switch, val) in options: if switch in ('-h', '--help'): help() sys.exit(0) elif switch in ('-o', '--oldversion'): oldversion = val elif switch in ('-v', '--verbose'): verbose = True elif switch in ('-d', '--dryrun'): dryrun = True elif switch in ('-c', '--clean'): clean = True elif switch in ('-r', '--revert'): revert = True if clean and revert: sys.stderr.write("upconvert: can't do clean and revert together.\n") sys.exit(1) # Compute the series of version upgrades to perform, and describe it. versions = filemoves.keys() versions.sort() versions = [versions[-1]] + versions[:-1] # Move 'older' to front if oldversion in versions: versions = versions[versions.index(oldversion):] else: print >>sys.stderr, "upconvert: unrecognized version." sys.exit(1) explain = "Performing upgrades for:" for i in range(len(versions)-1): explain += " %s -> %s," % (versions[i], versions[i+1]) sys.stdout.write(explain[:-1] + ".\n") fileconversions = map(lambda x: filemoves[x], versions[:-1]) # Perform resource file substitutions ofp = None for fn in allcfgfiles("."): if verbose: print fn backup = fn + "-bak" if clean or revert: # Do housekeeping if os.path.exists(backup): if clean: print "Removing %s" % backup if not dryrun: os.remove(backup) elif revert: print "Reverting %s" % backup if not dryrun: os.rename(backup, fn) else: # Do file conversions try: changed = translator(fn, maptransform, texttransform) if changed: print "%s modified." % fn if not dryrun: os.rename(fn, backup) ofp = open(fn, "w") ofp.write(changed) ofp.close() except maptransform_error, e: sys.stderr.write("upconvert: " + `e` + "\n") sys.exit(1) # upconvert ends here