parser: Support __getstate__/__setstate__ protocol.
[python-bulletml.git] / bulletml / parser.py
1 """BulletML parser.
2
3 This is based on the format described at
4 http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/bulletml_ref_e.html.
5
6 Unless you are adding support for new tags, the only class you should
7 care about in here is BulletML.
8 """
9
10 from __future__ import division
11
12 import math
13
14 from xml.etree.ElementTree import ElementTree
15
16 try:
17 from io import StringIO
18 except ImportError:
19 try:
20 from cStringIO import StringIO
21 except ImportError:
22 from StringIO import StringIO
23
24 from bulletml.errors import Error
25 from bulletml.expr import NumberDef, INumberDef
26
27 __all_ = ["ParseError", "BulletML"]
28
29 class ParseError(Error):
30 """Raised when an error occurs parsing the XML structure."""
31 pass
32
33 def realtag(element):
34 """Strip namespace poop off the front of a tag."""
35 try:
36 return element.tag.rsplit('}', 1)[1]
37 except ValueError:
38 return element.tag
39
40 class ParamList(object):
41 """List of parameter definitions."""
42
43 def __init__(self, params=()):
44 self.params = list(params)
45
46 @classmethod
47 def FromElement(cls, doc, element):
48 """Construct using an ElementTree-style element."""
49 return cls([NumberDef(subelem.text) for subelem in element
50 if realtag(subelem) == "param"])
51
52 def __getstate__(self):
53 if self.params:
54 return dict(params=[param.expr for param in self.params])
55 else:
56 return {}
57
58 def __setstate__(self, state):
59 self.__init__(NumberDef(param) for param in state.get("params", []))
60
61 def __call__(self, params, rank):
62 return [param(params, rank) for param in self.params]
63
64 def __repr__(self):
65 return "%s(%r)" % (type(self).__name__, self.params)
66
67 class Direction(object):
68 """Raw direction value."""
69
70 VALID_TYPES = ["relative", "absolute", "aim", "sequence"]
71
72 def __init__(self, type, value):
73 if type not in self.VALID_TYPES:
74 raise ValueError("invalid type %r" % type)
75 self.type = type
76 self.value = value
77
78 def __getstate__(self):
79 return dict(type=self.type, value=self.value.expr)
80
81 def __setstate__(self, state):
82 self.__init__(state["type"], NumberDef(state["value"]))
83
84 @classmethod
85 def FromElement(cls, doc, element, default="absolute"):
86 """Construct using an ElementTree-style element."""
87 return cls(element.get("type", default), NumberDef(element.text))
88
89 def __call__(self, params, rank):
90 return (math.radians(self.value(params, rank)), self.type)
91
92 def __repr__(self):
93 return "%s(%r, type=%r)" % (
94 type(self).__name__, self.value, self.type)
95
96 class ChangeDirection(object):
97 """Direction change over time."""
98
99 def __init__(self, term, direction):
100 self.term = term
101 self.direction = direction
102
103 def __getstate__(self):
104 return dict(frames=self.term.expr,
105 type=self.direction.type,
106 value=self.direction.value.expr)
107
108 def __setstate__(self, state):
109 self.__init__(INumberDef(state["frames"]),
110 Direction(state["type"], NumberDef(state["value"])))
111
112 @classmethod
113 def FromElement(cls, doc, element):
114 """Construct using an ElementTree-style element."""
115 for subelem in element.getchildren():
116 tag = realtag(subelem)
117 if tag == "direction":
118 direction = Direction.FromElement(doc, subelem)
119 elif tag == "term":
120 term = INumberDef(subelem.text)
121 try:
122 return cls(term, direction)
123 except UnboundLocalError as exc:
124 raise ParseError(str(exc))
125
126 def __call__(self, params, rank):
127 return self.term(params, rank), self.direction(params, rank)
128
129 def __repr__(self):
130 return "%s(term=%r, direction=%r)" % (
131 type(self).__name__, self.term, self.direction)
132
133 class Speed(object):
134 """Raw speed value."""
135
136 VALID_TYPES = ["relative", "absolute", "sequence"]
137
138 def __init__(self, type, value):
139 if type not in self.VALID_TYPES:
140 raise ValueError("invalid type %r" % type)
141 self.type = type
142 self.value = value
143
144 def __getstate__(self):
145 return dict(type=self.type, value=self.value.expr)
146
147 def __setstate__(self, state):
148 self.__init__(state["type"], NumberDef(state["value"]))
149
150 @classmethod
151 def FromElement(cls, doc, element):
152 """Construct using an ElementTree-style element."""
153 return cls(element.get("type", "absolute"), NumberDef(element.text))
154
155 def __call__(self, params, rank):
156 return (self.value(params, rank), self.type)
157
158 def __repr__(self):
159 return "%s(%r, type=%r)" % (type(self).__name__, self.value, self.type)
160
161 class ChangeSpeed(object):
162 """Speed change over time."""
163
164 def __init__(self, term, speed):
165 self.term = term
166 self.speed = speed
167
168 def __getstate__(self):
169 return dict(frames=self.term.expr,
170 type=self.speed.type,
171 value=self.speed.value.expr)
172
173 def __setstate__(self, state):
174 self.__init__(INumberDef(state["frames"]),
175 Speed(state["type"], NumberDef(state["value"])))
176
177 @classmethod
178 def FromElement(cls, doc, element):
179 """Construct using an ElementTree-style element."""
180 for subelem in element.getchildren():
181 tag = realtag(subelem)
182 if tag == "speed":
183 speed = Speed.FromElement(doc, subelem)
184 elif tag == "term":
185 term = INumberDef(subelem.text)
186 try:
187 return cls(term, speed)
188 except UnboundLocalError as exc:
189 raise ParseError(str(exc))
190
191 def __call__(self, params, rank):
192 return self.term(params, rank), self.speed(params, rank)
193
194 def __repr__(self):
195 return "%s(term=%r, speed=%r)" % (
196 type(self).__name__, self.term, self.speed)
197
198 class Wait(object):
199 """Wait for some frames."""
200
201 def __init__(self, frames):
202 self.frames = frames
203
204 def __getstate__(self):
205 return dict(frames=self.frames.expr)
206
207 def __setstate__(self, state):
208 self.__init__(INumberDef(state["frames"]))
209
210 @classmethod
211 def FromElement(cls, doc, element):
212 """Construct using an ElementTree-style element."""
213 return cls(INumberDef(element.text))
214
215 def __call__(self, params, rank):
216 return self.frames(params, rank)
217
218 def __repr__(self):
219 return "%s(%r)" % (type(self).__name__, self.frames)
220
221 class Tag(object):
222 """Set a bullet tag."""
223
224 def __init__(self, tag):
225 self.tag = tag
226
227 def __getstate__(self):
228 return dict(tag=self.tag)
229
230 def __setstate__(self, state):
231 self.__init__(state["tag"])
232
233 @classmethod
234 def FromElement(cls, doc, element):
235 """Construct using an ElementTree-style element."""
236 return cls(element.text)
237
238 class Untag(object):
239 """Unset a bullet tag."""
240
241 def __init__(self, tag):
242 self.tag = tag
243
244 def __getstate__(self):
245 return dict(tag=self.tag)
246
247 def __setstate__(self, state):
248 self.__init__(state["tag"])
249
250 @classmethod
251 def FromElement(cls, doc, element):
252 """Construct using an ElementTree-style element."""
253 return cls(element.text)
254
255 class Vanish(object):
256 """Make the owner disappear."""
257
258 def __init__(self):
259 pass
260
261 @classmethod
262 def FromElement(cls, doc, element):
263 """Construct using an ElementTree-style element."""
264 return cls()
265
266 def __repr__(self):
267 return "%s()" % (type(self).__name__)
268
269 class Repeat(object):
270 """Repeat an action definition."""
271
272 def __init__(self, times, action):
273 self.times = times
274 self.action = action
275
276 def __getstate__(self):
277 return dict(times=self.times.expr, action=self.action)
278
279 def __setstate__(self, state):
280 self.__init__(INumberDef(state["times"]), state["action"])
281
282 @classmethod
283 def FromElement(cls, doc, element):
284 """Construct using an ElementTree-style element."""
285 for subelem in element.getchildren():
286 tag = realtag(subelem)
287 if tag == "times":
288 times = INumberDef(subelem.text)
289 elif tag == "action":
290 action = ActionDef.FromElement(doc, subelem)
291 elif tag == "actionRef":
292 action = ActionRef.FromElement(doc, subelem)
293 try:
294 return cls(times, action)
295 except UnboundLocalError as exc:
296 raise ParseError(str(exc))
297
298 def __call__(self, params, rank):
299 return self.times(params, rank), self.action(params, rank)
300
301 def __repr__(self):
302 return "%s(%r, %r)" % (type(self).__name__, self.times, self.action)
303
304 class Accel(object):
305 """Accelerate over some time."""
306
307 horizontal = None
308 vertical = None
309
310 def __init__(self, term, horizontal=None, vertical=None):
311 self.term = term
312 self.horizontal = horizontal
313 self.vertical = vertical
314
315 def __getstate__(self):
316 state = dict(frames=self.term.expr)
317 if self.horizontal:
318 state["horizontal"] = self.horizontal
319 if self.vertical:
320 state["vertical"] = self.vertical
321 return state
322
323 def __setstate__(self, state):
324 self.__init__(INumberDef(state["term"]), state.get("horizontal"),
325 state.get("vertical"))
326
327 @classmethod
328 def FromElement(cls, doc, element):
329 """Construct using an ElementTree-style element."""
330 horizontal = None
331 vertical = None
332
333 for subelem in element.getchildren():
334 tag = realtag(subelem)
335 if tag == "term":
336 term = INumberDef(subelem.text)
337 elif tag == "horizontal":
338 horizontal = Speed.FromElement(doc, subelem)
339 elif tag == "vertical":
340 vertical = Speed.FromElement(doc, subelem)
341
342 try:
343 return cls(term, horizontal, vertical)
344 except AttributeError:
345 raise ParseError
346
347 def __call__(self, params, rank):
348 frames = self.term(params, rank)
349 horizontal = self.horizontal and self.horizontal(params, rank)
350 vertical = self.vertical and self.vertical(params, rank)
351 return frames, horizontal, vertical
352
353 def __repr__(self):
354 return "%s(%r, horizontal=%r, vertical=%r)" % (
355 type(self).__name__, self.term, self.horizontal, self.vertical)
356
357 class BulletDef(object):
358 """Bullet definition."""
359
360 direction = None
361 speed = None
362
363 def __init__(self, actions=[], direction=None, speed=None):
364 self.direction = direction
365 self.speed = speed
366 self.actions = list(actions)
367
368 def __getstate__(self):
369 state = dict()
370 if self.direction:
371 state["direction"] = self.direction
372 if self.speed:
373 state["speed"] = self.speed
374 if self.actions:
375 state["actions"] = self.actions
376 return state
377
378 def __setstate__(self, state):
379 self.__init__(**state)
380
381 @classmethod
382 def FromElement(cls, doc, element):
383 """Construct using an ElementTree-style element."""
384 actions = []
385 speed = None
386 direction = None
387 for subelem in element.getchildren():
388 tag = realtag(subelem)
389 if tag == "direction":
390 direction = Direction.FromElement(doc, subelem)
391 elif tag == "speed":
392 speed = Speed.FromElement(doc, subelem)
393 elif tag == "action":
394 actions.append(ActionDef.FromElement(doc, subelem))
395 elif tag == "actionRef":
396 actions.append(ActionRef.FromElement(doc, subelem))
397 dfn = cls(actions, direction, speed)
398 doc.bullets[element.get("label")] = dfn
399 return dfn
400
401 def __call__(self, params, rank):
402 actions = [action(params, rank) for action in self.actions]
403 return (
404 self.direction and self.direction(params, rank),
405 self.speed and self.speed(params, rank),
406 actions)
407
408 def __repr__(self):
409 return "%s(direction=%r, speed=%r, actions=%r)" % (
410 type(self).__name__, self.direction, self.speed, self.actions)
411
412 class BulletRef(object):
413 """Create a bullet by name with parameters."""
414
415 def __init__(self, bullet, params=None):
416 self.bullet = bullet
417 self.params = ParamList() if params is None else params
418
419 def __getstate__(self):
420 state = dict(bullet=self.bullet)
421 if self.params.params:
422 state["params"] = self.params.__getstate__()
423 return state
424
425 def __setstate__(self, state):
426 bullet = state["bullet"]
427 params = [NumberDef(param) for param in state.get("params", [])]
428 self.__init__(bullet, params)
429
430 @classmethod
431 def FromElement(cls, doc, element):
432 """Construct using an ElementTree-style element."""
433 bullet = cls(element.get("label"), ParamList.FromElement(doc, element))
434 doc._bullet_refs.append(bullet)
435 return bullet
436
437 def __call__(self, params, rank):
438 return self.bullet(self.params(params, rank), rank)
439
440 def __repr__(self):
441 return "%s(params=%r, bullet=%r)" % (
442 type(self).__name__, self.params, self.bullet)
443
444 class ActionDef(object):
445 """Action definition.
446
447 To support parsing new actions, add tags to
448 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
449 FromElement classmethod, which take the BulletML instance and
450 ElementTree element as arguments.
451 """
452
453 # This is self-referential, so it's filled in later.
454 CONSTRUCTORS = dict()
455
456 def __init__(self, actions):
457 self.actions = list(actions)
458
459 def __getstate__(self):
460 return dict(actions=self.actions)
461
462 def __setstate__(self, state):
463 self.__init__(state)
464
465 @classmethod
466 def FromElement(cls, doc, element):
467 """Construct using an ElementTree-style element."""
468 actions = []
469 for subelem in element.getchildren():
470 tag = realtag(subelem)
471 try:
472 ctr = cls.CONSTRUCTORS[tag]
473 except KeyError:
474 continue
475 else:
476 actions.append(ctr.FromElement(doc, subelem))
477 dfn = cls(actions)
478 doc.actions[element.get("label")] = dfn
479 return dfn
480
481 def __call__(self, params, rank):
482 return self.actions, params
483
484 def __repr__(self):
485 return "%s(%r)" % (type(self).__name__, self.actions)
486
487 class ActionRef(object):
488 """Run an action by name with parameters."""
489
490 def __init__(self, action, params=None):
491 self.action = action
492 self.params = params or ParamList()
493
494 def __getstate__(self):
495 state = dict(action=self.action)
496 if self.params.params:
497 state["params"] = self.params.__getstate__()
498 return state
499
500 def __setstate__(self, state):
501 action = state["action"]
502 params = [NumberDef(param) for param in state.get("params", [])]
503 self.__init__(action, params)
504
505 @classmethod
506 def FromElement(cls, doc, element):
507 """Construct using an ElementTree-style element."""
508 action = cls(element.get("label"), ParamList.FromElement(doc, element))
509 doc._action_refs.append(action)
510 return action
511
512 def __call__(self, params, rank):
513 return self.action(self.params(params, rank), rank)
514
515 def __repr__(self):
516 return "%s(params=%r, action=%r)" % (
517 type(self).__name__, self.params, self.action)
518
519 class Offset(object):
520 """Provide an offset to a bullet's initial position."""
521
522 VALID_TYPES = ["relative", "absolute"]
523
524 def __init__(self, type, x, y):
525 if type not in self.VALID_TYPES:
526 raise ValueError("invalid type %r" % type)
527 self.type = type
528 self.x = x
529 self.y = y
530
531 def __getstate__(self):
532 state = dict(type=self.type)
533 if self.x:
534 state["x"] = self.x.expr
535 if self.y:
536 state["y"] = self.y.expr
537 return state
538
539 def __setstate__(self, state):
540 self.__init__(state["type"], state.get("x"), state.get("y"))
541
542 @classmethod
543 def FromElement(cls, doc, element):
544 """Construct using an ElementTree-style element."""
545 type = element.get("type", "relative")
546 x = None
547 y = None
548 for subelem in element:
549 tag = realtag(subelem)
550 if tag == "x":
551 x = NumberDef(subelem.text)
552 elif tag == "y":
553 y = NumberDef(subelem.text)
554 return cls(type, x, y)
555
556 def __call__(self, params, rank):
557 return (self.x(params, rank) if self.x else 0,
558 self.y(params, rank) if self.y else 0)
559
560 class FireDef(object):
561 """Fire definition (creates a bullet)."""
562
563 def __init__(self, bullet, direction=None, speed=None, offset=None):
564 self.bullet = bullet
565 self.direction = direction
566 self.speed = speed
567 self.offset = offset
568
569 def __getstate__(self):
570 try:
571 params = self.bullet.params
572 except AttributeError:
573 state = dict(bullet=self.bullet)
574 else:
575 if params.params:
576 state = dict(bullet=self.bullet)
577 else:
578 state = dict(bullet=self.bullet.bullet)
579 if self.direction:
580 state["direction"] = self.direction
581 if self.speed:
582 state["speed"] = self.speed
583 if self.offset:
584 state["offset"] = self.offset
585 return state
586
587 @classmethod
588 def FromElement(cls, doc, element):
589 """Construct using an ElementTree-style element."""
590 direction = None
591 speed = None
592 offset = None
593
594 for subelem in element.getchildren():
595 tag = realtag(subelem)
596 if tag == "direction":
597 direction = Direction.FromElement(doc, subelem, "aim")
598 elif tag == "speed":
599 speed = Speed.FromElement(doc, subelem)
600 elif tag == "bullet":
601 bullet = BulletDef.FromElement(doc, subelem)
602 elif tag == "bulletRef":
603 bullet = BulletRef.FromElement(doc, subelem)
604 elif tag == "offset":
605 offset = Offset.FromElement(doc, subelem)
606 try:
607 fire = cls(bullet, direction, speed, offset)
608 except UnboundLocalError as exc:
609 raise ParseError(str(exc))
610 else:
611 doc.fires[element.get("label")] = fire
612 return fire
613
614 def __call__(self, params, rank):
615 direction, speed, actions = self.bullet(params, rank)
616 if self.direction:
617 direction = self.direction(params, rank)
618 if self.speed:
619 speed = self.speed(params, rank)
620 return direction, speed, actions, self.offset
621
622 def __repr__(self):
623 return "%s(direction=%r, speed=%r, bullet=%r)" % (
624 type(self).__name__, self.direction, self.speed, self.bullet)
625
626 class FireRef(object):
627 """Fire a bullet by name with parameters."""
628
629 def __init__(self, fire, params=None):
630 self.fire = fire
631 self.params = params or ParamList()
632
633 def __getstate__(self):
634 state = dict(fire=self.fire)
635 if self.params.params:
636 state["params"] = self.params.__getstate__()
637 return state
638
639 def __setstate__(self, state):
640 fire = state["fire"]
641 params = [NumberDef(param) for param in state.get("params", [])]
642 self.__init__(fire, params)
643
644 @classmethod
645 def FromElement(cls, doc, element):
646 """Construct using an ElementTree-style element."""
647 fired = cls(element.get("label"), ParamList.FromElement(doc, element))
648 doc._fire_refs.append(fired)
649 return fired
650
651 def __call__(self, params, rank):
652 return self.fire(self.params(params, rank), rank)
653
654 def __repr__(self):
655 return "%s(params=%r, fire=%r)" % (
656 type(self).__name__, self.params, self.fire)
657
658 class BulletML(object):
659 """BulletML document.
660
661 A BulletML document is a collection of bullets, actions, and
662 firings, as well as a base game type.
663
664 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
665 its parsing. It maps tag names to classes with a FromElement
666 classmethod, which take the BulletML instance and ElementTree
667 element as arguments.
668
669 """
670
671 CONSTRUCTORS = dict(
672 bullet=BulletDef,
673 action=ActionDef,
674 fire=FireDef,
675 )
676
677 def __init__(self, type="none", bullets=None, fires=None, actions=None):
678 self.type = type
679 self.bullets = {} if bullets is None else bullets
680 self.actions = {} if actions is None else actions
681 self.fires = {} if fires is None else fires
682
683 def __getstate__(self):
684 return dict(type=self.type, actions=self.actions)
685
686 def __setstate__(self, state):
687 self.__init__(state["type"], state.get("actions"))
688
689 @classmethod
690 def FromDocument(cls, source):
691 """Return a BulletML instance based on a string or file-like."""
692 if not hasattr(source, 'read'):
693 source = StringIO(source)
694
695 tree = ElementTree()
696 root = tree.parse(source)
697
698 self = cls(type=root.get("type", "none"))
699
700 self._bullet_refs = []
701 self._action_refs = []
702 self._fire_refs = []
703
704 for element in root.getchildren():
705 tag = realtag(element)
706 if tag in self.CONSTRUCTORS:
707 self.CONSTRUCTORS[tag].FromElement(self, element)
708
709 try:
710 for ref in self._bullet_refs:
711 ref.bullet = self.bullets[ref.bullet]
712 for ref in self._fire_refs:
713 ref.fire = self.fires[ref.fire]
714 for ref in self._action_refs:
715 ref.action = self.actions[ref.action]
716 except KeyError as exc:
717 raise ParseError("unknown reference %s" % exc)
718
719 del(self._bullet_refs)
720 del(self._action_refs)
721 del(self._fire_refs)
722
723 self.bullets.pop(None, None)
724 self.actions.pop(None, None)
725 self.fires.pop(None, None)
726
727 return self
728
729 @property
730 def top(self):
731 """Get a list of all top-level actions."""
732 return [dfn for name, dfn in self.actions.items()
733 if name and name.startswith("top")]
734
735 def __repr__(self):
736 return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
737 type(self).__name__, self.type, self.bullets, self.actions,
738 self.fires)
739
740 ActionDef.CONSTRUCTORS = dict(
741 repeat=Repeat,
742 fire=FireDef,
743 fireRef=FireRef,
744 changeSpeed=ChangeSpeed,
745 changeDirection=ChangeDirection,
746 accel=Accel,
747 wait=Wait,
748 vanish=Vanish,
749 tag=Tag,
750 untag=Untag,
751 action=ActionDef,
752 actionRef=ActionRef)