mirror of
https://github.com/wesnoth/wesnoth
synced 2025-05-21 15:56:40 +00:00

It's mostly about making the scripts run if python defaults to python3. Has been tested for each script.
323 lines
11 KiB
Python
Executable File
323 lines
11 KiB
Python
Executable File
#!/usr/bin/env python2
|
|
"""
|
|
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()
|