1#!/usr/bin/env python 2 3""" Provides a serial parser and support functions for PIE models 4 5All parser events will be passed back as a list starting with the event 6type followed by the line number in the input file. Type-specific data 7starts at position 3 in the list. 8 9All directives will have the directive name (case preserved) and the 10integer field following it in the list. 11 12TEXTURE and NOTEXTURE directives will also have a filename string followed 13by two integers (texture dimensions) appended to the list. 14 15Anything not determined to be a directive is "data" whose components will 16be left as strings unless passed through data_mutator, with the exception 17of BSP data, which will remain unprocessed unless passed through 18bsp_mutator. 19 20Events of type "error" will always have an instance of a PIEParseError 21derived class and possibly the type of expected token appended to the list. 22 23Instances of PIESyntaxError indicate that no data from that line can be 24trusted. Conversely, instances of PIEStructuralError indicate that the 25minimum expected data was present (excess data may be appended to 26the yielded list), but, depending on the context, data found on other 27lines may not be trustworthy, such as in the hypothetical case of a 28'LEVEL 5' directive appearing before a 'LEVEL 2' directive. 29 30When a PIEStructuralError is passed through the generator, the proceeding 31token will be the normal parsed contents of the same line, however a 32PIEStructuralError instance will never be followed by data from the same 33line to which the error applies. 34 35""" 36 37__version__ = "1.0" 38__author__ = "Kevin Gillette" 39 40# -------------------------------------------------------------------------- 41# pie v1.0 by Kevin Gillette (kage) 42# -------------------------------------------------------------------------- 43# ***** BEGIN GPL LICENSE BLOCK ***** 44# 45# This program is free software; you can redistribute it and/or 46# modify it under the terms of the GNU General Public License 47# as published by the Free Software Foundation; either version 2 48# of the License, or (at your option) any later version. 49# 50# This program is distributed in the hope that it will be useful, 51# but WITHOUT ANY WARRANTY; without even the implied warranty of 52# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 53# GNU General Public License for more details. 54# 55# You should have received a copy of the GNU General Public License 56# along with this program; if not, write to the Free Software Foundation, 57# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 58# 59# ***** END GPL LICENCE BLOCK ***** 60# -------------------------------------------------------------------------- 61 62TOKEN_TYPE = 0 63LINENO = 1 64LINE = 2 65FIRST = 3 # use for retrieving data from polygons, points, connectors, etc. 66DIRECTIVE_NAME = 3 67DIRECTIVE_VALUE = 4 68TEXTURE_FILENAME = 5 69TEXTURE_WIDTH, TEXTURE_HEIGHT = 6, 7 70ERROR = 3 71ERROR_ASSUMED_TYPE = 4 # if it weren't an error, what would it be processed as? 72 73class PIEParseError(Exception): 74 """ Base class for all PIE-related parsing errors """ 75 76class PIESyntaxError(PIEParseError): 77 """ Raised for line-fatal parsing errors """ 78 79class PIEStructuralError(PIEParseError): 80 """ Raised for non-fatal parsing errors related to the PIE specification """ 81 82def _handle_texture(s): 83 """ Breaks a 'TEXTURE' directive into its component parts. """ 84 85 type, s = s.split(None, 1) 86 s, x, y = s.rsplit(None, 2) 87 try: 88 return [int(type), s, int(x), int(y)] 89 except ValueError: 90 raise PIESyntaxError("expected integer") 91 92def _handle_type(s): 93 try: 94 return [int(s, 16)] 95 except ValueError: 96 raise PIESyntaxError("expected a hexadecimal integer") 97 98_directive_handlers = { 99 'TEXTURE': _handle_texture, 100 'NOTEXTURE': _handle_texture, 101 'TYPE': _handle_type 102} 103 104def parse(pie): 105 """ Parses non-binary PIE files and yields lists of tokens via a generator 106 107 "pie" may be a file-like object, or a filename string. 108 109 Uses Python's universal EOL support. Makes the assumption that tokens 110 may not span multiple lines, which differs from Warzone 2100's internal 111 parser. Directives such as "PIE" or "CONNECTORS" have the proceeding 112 tokens converted into python integers, with the except of "TEXTURE" and 113 "NOTEXTURE", which contain extra data within the declaration. 114 115 """ 116 117 if isinstance(pie, basestring): 118 pie = open(pie, "rU") 119 elif pie.closed: 120 raise TypeError("parse() takes either an open filehandle or a filename") 121 122 lineno = 0 123 for line in pie: 124 lineno += 1 125 vals = [None, lineno, line] 126 rest = line.split(None, 1) 127 if not rest: continue # blank line 128 first = rest[0] 129 if first.isalpha() and first[0].isupper(): 130 if len(rest) == 1: 131 vals[0] = "error" 132 vals[3:] = [PIESyntaxError("expected more data"), "directive"] 133 yield vals 134 continue 135 vals[0] = "directive" 136 vals.append(first) 137 if first in _directive_handlers: 138 try: 139 vals.extend(_directive_handlers[first](rest[1])) 140 except PIEParseError, instance: 141 vals[3:] = [instance, "directive"] 142 vals[0] = "error" 143 else: 144 rest = rest[1].split() 145 try: 146 vals.append(int(rest[0])) 147 except ValueError: 148 vals[3:] = [PIESyntaxError("integer expected"), "directive"] 149 vals[0] = "error" 150 if len(rest) > 1: 151 yield ["error", lineno, line, PIEStructuralError( 152 "unexpected additional data")] 153 vals.extend(rest[1:]) 154 else: 155 vals[0] = "data" 156 vals.append(first) 157 try: 158 vals.extend(rest[1].split()) 159 except IndexError: 160 yield ["error", lineno, line, PIESyntaxError("malformed line")] 161 yield vals 162 163def data_mutator(gen): 164 """ Modifies "data" tokens. 165 166 Converts strings to ints or floats depending on the most recent 167 directive. Validates "data" tokens where convenient. 168 169 Data following a POINTS directive will always have its components 170 converted to floats. 171 172 Data following a POLYGONS directive will have the first two fields 173 converted to integers, followed by a x number of ints and the rest 174 as floats where x is the value of the second field. 175 176 """ 177 178 mode = 0 179 for i in gen: 180 ilen = len(i) 181 if "directive" == i[0]: 182 directive = i[3] 183 if "POINTS" == directive: mode, name = 1, "point" 184 elif "CONNECTORS" == directive: mode, name = 1, "connector" 185 elif "POLYGONS" == directive: mode = 2 186 else: mode = 0 187 elif "data" != i[0] or 0 == mode: pass 188 elif 1 == mode: # points 189 if ilen == 6: 190 i[0] = name 191 try: 192 i[3:] = map(float, i[3:]) 193 except ValueError: 194 i[0] = "error" 195 i[3:] = [PIESyntaxError("expected a floating-point number"), name] 196 else: 197 i[0] = "error" 198 i[3:] = [PIESyntaxError("not a valid " + name), name] 199 elif 2 == mode: # polygons 200 valid = False 201 if ilen > 7: 202 try: 203 type, points = int(i[3], 16), int(i[4]) 204 if ilen > 4 + points and (ilen - 5 - points) % 2 == 0: 205 i[0], i[3], i[4], pos = "polygon", type, points, 5 + points 206 i[5:pos] = point_list = map(int, i[5:pos]) 207 i[pos:] = map(float, i[pos:]) 208 for pos in xrange(points - 1): 209 if point_list.count(point_list[pos]) > 1: 210 yield ["error", i[1], i[2], PIEStructuralError( 211 "duplicate vertices on same polygon")] 212 break 213 valid = True 214 except ValueError: 215 i[0] = "error" 216 i[3:] = [PIESyntaxError("expected a number"), "polygon"] 217 valid = True 218 if not valid: 219 i[0] = "error" 220 i[3:] = [PIESyntaxError("not a valid polygon"), "polygon"] 221 yield i 222 223def bsp_mutator(gen): 224 mode = 0 225 for i in gen: 226 if "directive" == i[0]: 227 if "BSP" == i[3]: mode = 1 228 else: mode = 0 229 elif "data" != i[0]: pass 230 elif 1 == mode: 231 try: 232 i[3:] = map(int, i[3:]) 233 i[0] = "bsp-data" 234 except ValueError: 235 i[3:] = [PIESyntaxError("expected an integer"), "bsp-data"] 236 i[0] = "error" 237 yield i 238 239if __name__ == "__main__": 240 import sys 241 args = sys.argv 242 mutate = True 243 if "--no-mutate" in args: 244 del args[args.index("--no-mutate")] 245 mutate = False 246 if len(args) < 2: 247 sys.exit("when run directly, a filename argument is required") 248 filename = args[1] 249 gen = parse(filename) 250 if mutate: gen = bsp_mutator(data_mutator(gen)) 251 for i in gen: 252 print i, 253 if i[0] == "error": 254 print i[3], 255 print 256 257# Setup VIM: ex: et ts=2 258