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