#!/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, or cropped one and need to translate map coordinates. 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 all relevant macro arguments. Also catches WML-fragment arguments of the form (x,y=mmm,nnn), and x= y= pairs on adjacent lines as in UnitWML declrations (but will fail if the x= line has a comment). Two cases this will *not* handle: Interleaved coordinate list literals like x=1,2,3,4,5, and ranges like x=4-6. 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. -t Translate - shift coordinates by specified xd,yd offsets -v Enable debugging output. -h Emit a help message and quit. If your offsets are negative (and thus led with '-') insert -- after the options to pacify the argument parser. More transformations would be easy to write. """ import sys, os, time, getopt, cStringIO, re 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) # Extract coordinate pair locations from macro arguments. pairs = [] for (name, arglocs) in parsed: (have_x, have_y) = relevant[name] pairs.append((arglocs[have_x], arglocs[have_y])) # Extract spans associated with x,y attributes. for m in re.finditer("x,y=([0-9]+),([0-9]+)", content): pairs.append(((m.start(1), m.end(1)), (m.start(2), m.end(2)))) # Extract spans associated with x= y= on adjacent lines. for m in re.finditer(r"x=([0-9]+)\s+y=([0-9]+)", content): pairs.append(((m.start(1), m.end(1)), (m.start(2), m.end(2)))) # More pair extractions can go here. # Sort by start of the x coordinate, then reverse the list, # so later changes won't screw up earlier ones. # Presumes that coordinate pairs are never interleaved. pairs.sort(lambda p, q: cmp(p[0][0], q[0][0])) 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 mapfile." 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 translate = False (options, arguments) = getopt.getopt(sys.argv[1:], "m:txyv") 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 ('-t'): translate = True 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 and not translate: 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) if translate: dx = int(arguments.pop(0)) dy = int(arguments.pop(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 coordinates 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 derived # from the source ones. We could do arbitrary matrix # algebra here, but beware the effects of hex-grid # transformation. if flip_x: yn = y xn = mx - x - 1 if translate: yn = y + dy xn = x + dx # 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] + repr(yn) + content[ye:] content = content[:xs] + repr(xn) + content[xe:] fp = open(filename, "w") fp.write(content) fp.close()