wesnoth/data/tools/upconvert

324 lines
13 KiB
Python
Executable File

#!/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