246 lines
8.1 KiB
Python
246 lines
8.1 KiB
Python
|
""" Provides a serial parser and support functions for PIE models
|
||
|
|
||
|
All parser events will be passed back as a list starting with the event
|
||
|
type followed by the line number in the input file. Type-specific data
|
||
|
starts at position 3 in the list.
|
||
|
|
||
|
All directives will have the directive name (case preserved) and the
|
||
|
integer field following it in the list.
|
||
|
|
||
|
TEXTURE and NOTEXTURE directives will also have a filename string followed
|
||
|
by two integers (texture dimensions) appended to the list.
|
||
|
|
||
|
Anything not determined to be a directive is "data" whose components will
|
||
|
be left as strings unless passed through data_mutator, with the exception
|
||
|
of BSP data, which will remain unprocessed unless passed through
|
||
|
bsp_mutator.
|
||
|
|
||
|
Events of type "error" will always have an instance of a PIEParseError
|
||
|
derived class and possibly the type of expected token appended to the list.
|
||
|
|
||
|
Instances of PIESyntaxError indicate that no data from that line can be
|
||
|
trusted. Conversely, instances of PIEStructuralError indicate that the
|
||
|
minimum expected data was present (excess data may be appended to
|
||
|
the yielded list), but, depending on the context, data found on other
|
||
|
lines may not be trustworthy, such as in the hypothetical case of a
|
||
|
'LEVEL 5' directive appearing before a 'LEVEL 2' directive.
|
||
|
|
||
|
When a PIEStructuralError is passed through the generator, the proceeding
|
||
|
token will be the normal parsed contents of the same line, however a
|
||
|
PIEStructuralError instance will never be followed by data from the same
|
||
|
line to which the error applies.
|
||
|
|
||
|
"""
|
||
|
|
||
|
__version__ = "1.0"
|
||
|
__author__ = "Kevin Gillette"
|
||
|
|
||
|
# --------------------------------------------------------------------------
|
||
|
# pie v1.0 by Kevin Gillette (kage)
|
||
|
# --------------------------------------------------------------------------
|
||
|
# ***** BEGIN GPL LICENSE BLOCK *****
|
||
|
#
|
||
|
# This program is free software; you can redistribute it and/or
|
||
|
# modify it under the terms of the GNU General Public License
|
||
|
# as published by the Free Software Foundation; either version 2
|
||
|
# of the License, or (at your option) any later version.
|
||
|
#
|
||
|
# This program is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU General Public License
|
||
|
# along with this program; if not, write to the Free Software Foundation,
|
||
|
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||
|
#
|
||
|
# ***** END GPL LICENCE BLOCK *****
|
||
|
# --------------------------------------------------------------------------
|
||
|
|
||
|
TOKEN_TYPE = 0
|
||
|
LINENO = 1
|
||
|
LINE = 2
|
||
|
FIRST = 3 # use for retrieving data from polygons, points, connectors, etc.
|
||
|
DIRECTIVE_NAME = 3
|
||
|
DIRECTIVE_VALUE = 4
|
||
|
TEXTURE_FILENAME = 5
|
||
|
TEXTURE_WIDTH, TEXTURE_HEIGHT = 6, 7
|
||
|
ERROR = 3
|
||
|
ERROR_ASSUMED_TYPE = 4 # if it weren't an error, what would it be processed as?
|
||
|
|
||
|
class PIEParseError(Exception):
|
||
|
""" Base class for all PIE-related parsing errors """
|
||
|
|
||
|
class PIESyntaxError(PIEParseError):
|
||
|
""" Raised for line-fatal parsing errors """
|
||
|
|
||
|
class PIEStructuralError(PIEParseError):
|
||
|
""" Raised for non-fatal parsing errors related to the PIE specification """
|
||
|
|
||
|
def _handle_texture(s):
|
||
|
""" Breaks a 'TEXTURE' directive into its component parts. """
|
||
|
|
||
|
type, s = s.split(None, 1)
|
||
|
s, x, y = s.rsplit(None, 2)
|
||
|
try:
|
||
|
return [int(type), s, int(x), int(y)]
|
||
|
except ValueError:
|
||
|
raise PIESyntaxError("expected integer")
|
||
|
|
||
|
_directive_handlers = {
|
||
|
'TEXTURE': _handle_texture,
|
||
|
'NOTEXTURE': _handle_texture
|
||
|
}
|
||
|
|
||
|
def parse(pie):
|
||
|
""" Parses non-binary PIE files and yields lists of tokens via a generator
|
||
|
|
||
|
"pie" may be a file-like object, or a filename string.
|
||
|
|
||
|
Uses Python's universal EOL support. Makes the assumption that tokens
|
||
|
may not span multiple lines, which differs from Warzone 2100's internal
|
||
|
parser. Directives such as "PIE" or "CONNECTORS" have the proceding
|
||
|
tokens converted into python integers, with the except of "TEXTURE" and
|
||
|
"NOTEXTURE", which contain extra data within the declaration.
|
||
|
|
||
|
"""
|
||
|
|
||
|
if isinstance(pie, basestring):
|
||
|
pie = open(pie, "rU")
|
||
|
elif pie.closed:
|
||
|
raise TypeError("parse() takes either an open filehandle or a filename")
|
||
|
|
||
|
lineno = 0
|
||
|
for line in pie:
|
||
|
lineno += 1
|
||
|
vals = [None, lineno, line]
|
||
|
rest = line.split(None, 1)
|
||
|
if not rest: continue # blank line
|
||
|
first = rest[0]
|
||
|
if first.isalpha() and first[0].isupper():
|
||
|
if len(rest) == 1:
|
||
|
vals[0] = "error"
|
||
|
vals[3:] = [PIESyntaxError("expected more data"), "directive"]
|
||
|
yield vals
|
||
|
continue
|
||
|
vals[0] = "directive"
|
||
|
vals.append(first)
|
||
|
if first in _directive_handlers:
|
||
|
try:
|
||
|
vals.extend(_directive_handlers[first](rest[1]))
|
||
|
except PIEParseError, instance:
|
||
|
vals[3:] = [instance, "directive"]
|
||
|
vals[0] = "error"
|
||
|
else:
|
||
|
rest = rest[1].split()
|
||
|
try:
|
||
|
vals.append(int(rest[0]))
|
||
|
except ValueError:
|
||
|
vals[3:] = [PIESyntaxError("integer expected"), "directive"]
|
||
|
vals[0] = "error"
|
||
|
if len(rest) > 1:
|
||
|
yield ["error", lineno, line, PIEStructuralError(
|
||
|
"unexpected additional data")]
|
||
|
vals.extend(rest[1:])
|
||
|
else:
|
||
|
vals[0] = "data"
|
||
|
vals.append(first)
|
||
|
vals.extend(rest[1].split())
|
||
|
yield vals
|
||
|
|
||
|
def data_mutator(gen):
|
||
|
""" Modifies "data" tokens.
|
||
|
|
||
|
Converts strings to ints or floats depending on the most recent
|
||
|
directive. Validates "data" tokens where convenient.
|
||
|
|
||
|
Data following a POINTS directive will always have its components
|
||
|
converted to floats.
|
||
|
|
||
|
Data following a POLYGONS directive will have the first two fields
|
||
|
converted to integers, followed by a x number of ints and the rest
|
||
|
as floats where x is the value of the second field.
|
||
|
|
||
|
"""
|
||
|
|
||
|
mode = 0
|
||
|
for i in gen:
|
||
|
ilen = len(i)
|
||
|
if "directive" == i[0]:
|
||
|
directive = i[3]
|
||
|
if "POINTS" == directive: mode, name = 1, "point"
|
||
|
elif "CONNECTORS" == directive: mode, name = 1, "connector"
|
||
|
elif "POLYGONS" == directive: mode = 2
|
||
|
else: mode = 0
|
||
|
elif "data" != i[0] or 0 == mode: pass
|
||
|
elif 1 == mode: # points
|
||
|
if ilen == 6:
|
||
|
i[0] = name
|
||
|
try:
|
||
|
i[3:] = map(float, i[3:])
|
||
|
except ValueError:
|
||
|
i[0] = "error"
|
||
|
i[3:] = [PIESyntaxError("expected a floating-point number"), name]
|
||
|
else:
|
||
|
i[0] = "error"
|
||
|
i[3:] = [PIESyntaxError("not a valid " + name), name]
|
||
|
elif 2 == mode: # polygons
|
||
|
valid = False
|
||
|
if ilen > 7:
|
||
|
try:
|
||
|
type, points = int(i[3], 16), int(i[4])
|
||
|
if ilen > 4 + points and (ilen - 5 - points) % 2 == 0:
|
||
|
i[0], i[3], i[4], pos = "polygon", type, points, 5 + points
|
||
|
i[5:pos] = point_list = map(int, i[5:pos])
|
||
|
i[pos:] = map(float, i[pos:])
|
||
|
for pos in xrange(points - 1):
|
||
|
if point_list.count(point_list[pos]) > 1:
|
||
|
yield ["error", i[1], i[2], PIEStructuralError(
|
||
|
"duplicate vertices on same polygon")]
|
||
|
break
|
||
|
valid = True
|
||
|
except ValueError:
|
||
|
i[0] = "error"
|
||
|
i[3:] = [PIESyntaxError("expected a number"), "polygon"]
|
||
|
valid = True
|
||
|
if not valid:
|
||
|
i[0] = "error"
|
||
|
i[3:] = [PIESyntaxError("not a valid polygon"), "polygon"]
|
||
|
yield i
|
||
|
|
||
|
def bsp_mutator(gen):
|
||
|
mode = 0
|
||
|
for i in gen:
|
||
|
if "directive" == i[0]:
|
||
|
if "BSP" == i[3]: mode = 1
|
||
|
else: mode = 0
|
||
|
elif "data" != i[0]: pass
|
||
|
elif 1 == mode:
|
||
|
try:
|
||
|
i[3:] = map(int, i[3:])
|
||
|
i[0] = "bsp-data"
|
||
|
except ValueError:
|
||
|
i[3:] = [PIESyntaxError("expected an integer"), "bsp-data"]
|
||
|
i[0] = "error"
|
||
|
yield i
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
import sys
|
||
|
args = sys.argv
|
||
|
mutate = True
|
||
|
if "--no-mutate" in args:
|
||
|
del args[args.index("--no-mutate")]
|
||
|
mutate = False
|
||
|
if len(args) < 2:
|
||
|
sys.exit("when run directly, a filename argument is required")
|
||
|
filename = args[1]
|
||
|
gen = parse(filename)
|
||
|
if mutate: gen = bsp_mutator(data_mutator(gen))
|
||
|
for i in gen:
|
||
|
print i,
|
||
|
if i[0] == "error":
|
||
|
print i[3],
|
||
|
print
|
||
|
|
||
|
# Setup VIM: ex: et ts=2
|