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