321 lines
11 KiB
Python
Executable file
321 lines
11 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
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, io, re
|
|
from wesnoth.wmltools3 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
|
|
with open(filename, "r") as fp:
|
|
content = fp.read()
|
|
|
|
# Get argument offsets from it.
|
|
calls = ParseArgs(io.StringIO(content), verbose)
|
|
|
|
# Filter out irrelevant calls.
|
|
parsed = [x for x in calls.parsed if x[0] in relevant]
|
|
|
|
# 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(key=lambda element: element[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
|
|
with open(filename) as f:
|
|
for line in f:
|
|
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:txyvh")
|
|
|
|
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("Vertical flip is not yet supported.", file=sys.stderr)
|
|
sys.exit(0)
|
|
elif switch == '-v':
|
|
verbose += 1
|
|
if verbose:
|
|
print("Debugging output enabled.")
|
|
|
|
if mapfile:
|
|
(mx, my) = mapsize(mapfile)
|
|
print("%s is %d wide by %d high" % (mapfile, mx, my), file=sys.stderr)
|
|
|
|
if arguments and not flip_x and not translate:
|
|
print("No coordinate transform is specified.", file=sys.stderr)
|
|
sys.exit(0)
|
|
|
|
if flip_x and not mapfile:
|
|
print("X flip transformation needs to know the map size..", file=sys.stderr)
|
|
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("Processing file", filename, file=sys.stderr)
|
|
|
|
(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:]
|
|
|
|
with open(filename, "w") as fp:
|
|
fp.write(content)
|