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