wesnoth/data/tools/wmlflip
2009-03-30 09:14:06 +00:00

291 lines
9.6 KiB
Python
Executable File

#!/usr/bin/env python
"""
wmlflip -- coordinate transformation for .cfg macro calls.
Modify macro coordinate arguments in a .cfg file. Use this if you've
mirror-reversed a map and need to change coordinate-using macros.
Takes a cross-reference of all known macros and looks for formals that
are either X, Y, *_X, or _Y, so it's guaranteed to catch everything.
Note: will not transform coorinates given as bare attribute values in, say,
UnitWML. This should be fixed.
Options:
-m Argument of this switch should name the map file.
Required, because the coordinate transform needs to know the
map dimensions.
-x Coordinate transformation for a horizontally flipped map.
-v Enable debugging output.
-h Emit a help message and quit.
More transformations would be easy to write.
"""
import sys, os, time, getopt, cStringIO
from wesnoth.wmltools import *
class ParseArgs:
"Mine macro argument locations out of a .cfg file."
def __init__(self, fp, verbose=False):
self.fp = fp
self.verbose = verbose
self.parsed = []
self.namestack = []
self.pushback = None
self.lead = ""
self.parse_until([''])
def getchar(self):
if self.pushback:
c = self.pushback
self.pushback = None
return c
else:
return self.fp.read(1)
def ungetchar(self, c):
if verbose:
print "pushing back", c
self.pushback = c
def parse_until(self, enders):
"Parse until we reach specified terminator."
if self.verbose:
self.lead += "*"
print self.lead + " parse_until(%s) starts" % enders
while True:
c = self.getchar()
if self.verbose:
print self.lead + "I see", c
if c in enders:
if self.verbose:
print self.lead + "parse_until(%s) ends" % enders
self.lead = self.lead[:-1]
return c
elif c == '{':
self.parse_call()
def parse_call(self):
"We see a start of call."
if self.verbose:
self.lead += "*"
print self.lead + "parse_call()"
self.namestack.append(["", []])
# Fill in the name of the called macro
while True:
c = self.getchar()
if c.isalnum() or c == '_':
self.namestack[-1][0] += c
else:
break
if self.verbose:
print self.lead + "name", self.namestack[-1]
# Discard if no arguments
if c == '}':
self.namestack.pop()
if self.verbose:
print self.lead + "parse_call() ends"
self.lead = self.lead[:-1]
return
# If non-space, this is something like a filename include;
# skip until closing }
if not c.isspace():
while True:
c = self.getchar()
if c == '}':
if self.verbose:
print self.lead + "parse_call() ends"
self.lead = self.lead[:-1]
return
# It's a macro call with arguments;
# parse them, recording the character offsets
while self.parse_actual():
continue
# Discard trailing }
self.getchar()
# Record the scope we just parsed
self.parsed.append(self.namestack.pop())
if self.verbose:
print self.lead + "parse_call() ends"
self.lead = self.lead[:-1]
def parse_actual(self):
"Parse an actual argument."
# Skip leading whitespace
if self.verbose:
self.lead += "*"
print self.lead + "parse_actual() begins"
while True:
c = self.getchar()
if not c.isspace():
break
if c == '}':
if self.verbose:
print "** parse_actual() returns False"
self.lead = self.lead[:-1]
return False
# Looks like we have a real argument
argstart = self.fp.tell() - 1
# Skip leading translation mark, if any
if c == "_":
c = self.getchar()
# Get the argument itself
if c == '{':
self.parse_call()
argend = self.fp.tell()
elif c == '(':
self.parse_until([")"])
argend = self.fp.tell()
elif c == '"':
if verbose:
print self.lead + "starting string argument"
self.parse_until(['"'])
argend = self.fp.tell()
else:
ender = self.parse_until(['', ' ', '\t', '\r', '\n', '}'])
argend = self.fp.tell() - 1
self.ungetchar(ender)
self.namestack[-1][1].append((argstart, argend))
if self.verbose:
print self.lead + "parse_actual() returns True"
self.lead = self.lead[:-1]
return True
def relevant_macros():
"Compute indices of (X, Y) pairs in formals of all mainline macros."
# Cross-reference all files.
here = os.getcwd()
pop_to_top("wmlflip")
cref = CrossRef(scopelist())
os.chdir(here)
# Look at all definitions. Extract those with in "_?X" or "_?Y".
# Generate a dictionary mapping definition names to the indices
# the X Y formal arguments.
relevant = {}
for name in cref.xref:
for ref in cref.xref[name]:
have_x = have_y = None
for (i, arg) in enumerate(ref.args):
if arg == "X" or arg.endswith("_X"):
have_x = i
if arg == "Y" or arg.endswith("_Y"):
have_y = i
if have_x is not None and have_y is not None:
relevant[name] = (have_x, have_y)
return relevant
def transformables(filename, relevant, verbose):
"Return a list of transformable (X,Y) regions in the specified file."
# Grab the content
fp = open(filename, "r")
content = fp.read()
fp.close()
# Get argument offsets from it.
calls = ParseArgs(cStringIO.StringIO(content), verbose)
# Filter out irrelevant calls.
parsed = filter(lambda x: x[0] in relevant, calls.parsed)
# Derive list of coordinate pair locations.
pairs = []
for (name, arglocs) in parsed:
(have_x, have_y) = relevant[name]
pairs.append((arglocs[have_x], arglocs[have_y]))
# FIXME: extract spans associated with x,y attributes, too.
# Transform these back to front so later changes won't screw up earlier ones
pairs.reverse()
# Return the file content as a string and the transformable extents in it.
return (content, pairs)
def mapsize(filename):
"Return the size of a specified mappfile."
x = y = 0
for line in open(filename):
if "," in line:
y += 1
nx = line.count(",") + 1
assert(x == 0 or x == nx)
x = nx
return (x, y)
if __name__ == '__main__':
flip_x = flip_y = False
verbose = 0
mapfile = None
(options, arguments) = getopt.getopt(sys.argv[1:], "m:xyv")
for (switch, val) in options:
if switch in ('-h', '--help'):
sys.stderr.write(__doc__)
sys.exit(0)
elif switch in ('-m'):
mapfile = val
elif switch in ('-x'):
flip_x = True
elif switch in ('-y'):
print >>sys.stderr, "Vertical flip is not yet supported."
sys.exit(0)
elif switch == '-v':
verbose += 1
if verbose:
print "Debugging output enabled."
if mapfile:
(mx, my) = mapsize(mapfile)
print >>sys.stderr, "%s is %d wide by %d high" % (mapfile, mx, my)
if arguments and not flip_x:
print >>sys.stderr, "No coordinate transform is specified."
sys.exit(0)
if flip_x and not mapfile:
print >>sys.stderr, "X flip transformation needs to know the map size.."
sys.exit(0)
# Are we doing file transformations?
if arguments:
relevant = relevant_macros()
# For each file named on the command line...
for filename in arguments:
if verbose:
print >>sys.stderr, "Processing file", filename
(content, pairs) = transformables(filename, relevant, verbose > 1)
# Extract the existing coordimates as numbers
source = []
for ((xs, xe), (ys, ye)) in pairs:
x = int(content[xs:xe])
y = int(content[ys:ye])
source.append((x, y))
# Compute the target coordinate pairs
target = []
for (x, y) in source:
# Note: This is the *only* part of this code that is
# specific to a particular transform. The rest of the
# code doesn't care how the target pairs are derives from
# the source ones. We could do matrix algebra here, but
# beware the effects of hex-grid transformation.
if flip_x:
yn = y
xn = mx - x - 1
# This is generic again
target.append((xn, yn))
if verbose:
print "(%d, %d) -> (%d, %d)" % (x, y, xn, yn)
# Perform the actual transformation
for (((xs, xe), (ys, ye)), (xn, yn)) in zip(pairs, target):
content = content[:ys] +`yn` + content[ye:]
content = content[:xs] + `xn` + content[xe:]
fp = open(filename, "w")
fp.write(content)
fp.close()