Add <tag> support to BulletDef and FireDef.
[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, tags=()):
362 self.direction = direction
363 self.speed = speed
364 self.actions = list(actions)
365 self.tags = set(tags)
366
367 def __getstate__(self):
368 state = []
369 if self.direction:
370 state.append(("direction", self.direction))
371 if self.speed:
372 state.append(("speed", self.speed))
373 if self.actions:
374 state.append(("actions", self.actions))
375 if self.tags:
376 state.append(("tags", list(self.tags)))
377 return state
378
379 def __setstate__(self, state):
380 state = dict(state)
381 self.__init__(**state)
382
383 @classmethod
384 def FromXML(cls, doc, element):
385 """Construct using an ElementTree-style element."""
386 actions = []
387 speed = None
388 direction = None
389 for subelem in element.getchildren():
390 tag = realtag(subelem)
391 if tag == "direction":
392 direction = Direction.FromXML(doc, subelem)
393 elif tag == "speed":
394 speed = Speed.FromXML(doc, subelem)
395 elif tag == "action":
396 actions.append(ActionDef.FromXML(doc, subelem))
397 elif tag == "actionRef":
398 actions.append(ActionRef.FromXML(doc, subelem))
399 elif tag == "tag":
400 self.tags.add(subelem.text)
401 dfn = cls(actions, direction, speed)
402 doc._bullets[element.get("label")] = dfn
403 return dfn
404
405 def __call__(self, params, rank):
406 actions = [action(params, rank) for action in self.actions]
407 return (
408 self.direction and self.direction(params, rank),
409 self.speed and self.speed(params, rank),
410 self.tags,
411 actions)
412
413 def __repr__(self):
414 return "%s(direction=%r, speed=%r, actions=%r)" % (
415 type(self).__name__, self.direction, self.speed, self.actions)
416
417 class BulletRef(object):
418 """Create a bullet by name with parameters."""
419
420 def __init__(self, bullet, params=None):
421 self.bullet = bullet
422 self.params = ParamList() if params is None else params
423
424 def __getstate__(self):
425 state = []
426 if self.params.params:
427 params = [param.expr for param in self.params.params]
428 state.append(("params", params))
429 state.append(('bullet', self.bullet))
430 return state
431
432 def __setstate__(self, state):
433 state = dict(state)
434 bullet = state["bullet"]
435 params = [NumberDef(param) for param in state.get("params", [])]
436 self.__init__(bullet, ParamList(params))
437
438 @classmethod
439 def FromXML(cls, doc, element):
440 """Construct using an ElementTree-style element."""
441 bullet = cls(element.get("label"), ParamList.FromXML(doc, element))
442 doc._bullet_refs.append(bullet)
443 return bullet
444
445 def __call__(self, params, rank):
446 return self.bullet(self.params(params, rank), rank)
447
448 def __repr__(self):
449 return "%s(params=%r, bullet=%r)" % (
450 type(self).__name__, self.params, self.bullet)
451
452 class ActionDef(object):
453 """Action definition.
454
455 To support parsing new actions, add tags to
456 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
457 FromXML classmethod, which take the BulletML instance and
458 ElementTree element as arguments.
459 """
460
461 # This is self-referential, so it's filled in later.
462 CONSTRUCTORS = dict()
463
464 def __init__(self, actions):
465 self.actions = list(actions)
466
467 def __getstate__(self):
468 return dict(actions=self.actions)
469
470 def __setstate__(self, state):
471 state = dict(state)
472 self.__init__(state["actions"])
473
474 @classmethod
475 def FromXML(cls, doc, element):
476 """Construct using an ElementTree-style element."""
477 actions = []
478 for subelem in element.getchildren():
479 tag = realtag(subelem)
480 try:
481 ctr = cls.CONSTRUCTORS[tag]
482 except KeyError:
483 continue
484 else:
485 actions.append(ctr.FromXML(doc, subelem))
486 dfn = cls(actions)
487 doc._actions[element.get("label")] = dfn
488 return dfn
489
490 def __call__(self, params, rank):
491 return self.actions, params
492
493 def __repr__(self):
494 return "%s(%r)" % (type(self).__name__, self.actions)
495
496 class ActionRef(object):
497 """Run an action by name with parameters."""
498
499 def __init__(self, action, params=None):
500 self.action = action
501 self.params = params or ParamList()
502
503 def __getstate__(self):
504 state = []
505 if self.params.params:
506 params = [param.expr for param in self.params.params]
507 state.append(("params", params))
508 state.append(('action', self.action))
509 return state
510
511 def __setstate__(self, state):
512 state = dict(state)
513 action = state["action"]
514 params = [NumberDef(param) for param in state.get("params", [])]
515 self.__init__(action, ParamList(params))
516
517 @classmethod
518 def FromXML(cls, doc, element):
519 """Construct using an ElementTree-style element."""
520 action = cls(element.get("label"), ParamList.FromXML(doc, element))
521 doc._action_refs.append(action)
522 return action
523
524 def __call__(self, params, rank):
525 return self.action(self.params(params, rank), rank)
526
527 def __repr__(self):
528 return "%s(params=%r, action=%r)" % (
529 type(self).__name__, self.params, self.action)
530
531 class Offset(object):
532 """Provide an offset to a bullet's initial position."""
533
534 VALID_TYPES = ["relative", "absolute"]
535
536 def __init__(self, type, x, y):
537 if type not in self.VALID_TYPES:
538 raise ValueError("invalid type %r" % type)
539 self.type = type
540 self.x = x
541 self.y = y
542
543 def __getstate__(self):
544 state = [('type', self.type)]
545 if self.x:
546 state.append(('x', self.x.expr))
547 if self.y:
548 state.append(('y', self.y.expr))
549 return state
550
551 def __setstate__(self, state):
552 state = dict(state)
553 self.__init__(state["type"], state.get("x"), state.get("y"))
554
555 @classmethod
556 def FromXML(cls, doc, element):
557 """Construct using an ElementTree-style element."""
558 type = element.get("type", "relative")
559 x = None
560 y = None
561 for subelem in element:
562 tag = realtag(subelem)
563 if tag == "x":
564 x = NumberDef(subelem.text)
565 elif tag == "y":
566 y = NumberDef(subelem.text)
567 return cls(type, x, y)
568
569 def __call__(self, params, rank):
570 return (self.x(params, rank) if self.x else 0,
571 self.y(params, rank) if self.y else 0)
572
573 class FireDef(object):
574 """Fire definition (creates a bullet)."""
575
576 def __init__(
577 self, bullet, direction=None, speed=None, offset=None, tags=()):
578 self.bullet = bullet
579 self.direction = direction
580 self.speed = speed
581 self.offset = offset
582 self.tags = set(tags)
583
584 def __getstate__(self):
585 state = []
586 if self.direction:
587 state.append(("direction", self.direction))
588 if self.speed:
589 state.append(("speed", self.speed))
590 if self.offset:
591 state.append(("offset", self.offset))
592 if self.tags:
593 state.append(("tags", list(self.tags)))
594 try:
595 params = self.bullet.params
596 except AttributeError:
597 state.append(('bullet', self.bullet))
598 else:
599 if params.params:
600 state.append(('bullet', self.bullet))
601 else:
602 # Strip out empty BulletRefs.
603 state.append(('bullet', self.bullet.bullet))
604 return state
605
606 def __setstate__(self, state):
607 state = dict(state)
608 self.__init__(**state)
609
610 @classmethod
611 def FromXML(cls, doc, element):
612 """Construct using an ElementTree-style element."""
613 direction = None
614 speed = None
615 offset = None
616
617 for subelem in element.getchildren():
618 tag = realtag(subelem)
619 if tag == "direction":
620 direction = Direction.FromXML(doc, subelem, "aim")
621 elif tag == "speed":
622 speed = Speed.FromXML(doc, subelem)
623 elif tag == "bullet":
624 bullet = BulletDef.FromXML(doc, subelem)
625 elif tag == "bulletRef":
626 bullet = BulletRef.FromXML(doc, subelem)
627 elif tag == "offset":
628 offset = Offset.FromXML(doc, subelem)
629 elif tag == "tag":
630 self.tags.add(subelem.text)
631 try:
632 fire = cls(bullet, direction, speed, offset)
633 except UnboundLocalError as exc:
634 raise ParseError(str(exc))
635 else:
636 doc._fires[element.get("label")] = fire
637 return fire
638
639 def __call__(self, params, rank):
640 direction, speed, tags, actions = self.bullet(params, rank)
641 if self.direction:
642 direction = self.direction(params, rank)
643 if self.speed:
644 speed = self.speed(params, rank)
645 tags = tags.union(self.tags)
646 return direction, speed, self.offset, tags, actions
647
648 def __repr__(self):
649 return "%s(direction=%r, speed=%r, bullet=%r)" % (
650 type(self).__name__, self.direction, self.speed, self.bullet)
651
652 class FireRef(object):
653 """Fire a bullet by name with parameters."""
654
655 def __init__(self, fire, params=None):
656 self.fire = fire
657 self.params = params or ParamList()
658
659 def __getstate__(self):
660 state = []
661 if self.params.params:
662 params = [param.expr for param in self.params.params]
663 state.append(("params", params))
664 state.append(('fire', self.fire))
665 return state
666
667 def __setstate__(self, state):
668 state = dict(state)
669 fire = state["fire"]
670 params = [NumberDef(param) for param in state.get("params", [])]
671 self.__init__(fire, ParamList(params))
672
673 @classmethod
674 def FromXML(cls, doc, element):
675 """Construct using an ElementTree-style element."""
676 fired = cls(element.get("label"), ParamList.FromXML(doc, element))
677 doc._fire_refs.append(fired)
678 return fired
679
680 def __call__(self, params, rank):
681 return self.fire(self.params(params, rank), rank)
682
683 def __repr__(self):
684 return "%s(params=%r, fire=%r)" % (
685 type(self).__name__, self.params, self.fire)
686
687 class BulletML(object):
688 """BulletML document.
689
690 A BulletML document is a collection of top-level actions and the
691 base game type.
692
693 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
694 its parsing. It maps tag names to classes with a FromXML
695 classmethod, which take the BulletML instance and ElementTree
696 element as arguments.
697
698 """
699
700 CONSTRUCTORS = dict(
701 bullet=BulletDef,
702 action=ActionDef,
703 fire=FireDef,
704 )
705
706 def __init__(self, type="none", actions=None):
707 self.type = type
708 self.actions = [] if actions is None else actions
709
710 def __getstate__(self):
711 return [('type', self.type), ('actions', self.actions)]
712
713 def __setstate__(self, state):
714 state = dict(state)
715 self.__init__(state["type"], actions=state.get("actions"))
716
717 @classmethod
718 def FromXML(cls, source):
719 """Return a BulletML instance based on XML."""
720 if not hasattr(source, 'read'):
721 source = StringIO(source)
722
723 tree = ElementTree()
724 root = tree.parse(source)
725
726 doc = cls(type=root.get("type", "none"))
727
728 doc._bullets = {}
729 doc._actions = {}
730 doc._fires = {}
731 doc._bullet_refs = []
732 doc._action_refs = []
733 doc._fire_refs = []
734
735 for element in root.getchildren():
736 tag = realtag(element)
737 if tag in doc.CONSTRUCTORS:
738 doc.CONSTRUCTORS[tag].FromXML(doc, element)
739
740 try:
741 for ref in doc._bullet_refs:
742 ref.bullet = doc._bullets[ref.bullet]
743 for ref in doc._fire_refs:
744 ref.fire = doc._fires[ref.fire]
745 for ref in doc._action_refs:
746 ref.action = doc._actions[ref.action]
747 except KeyError as exc:
748 raise ParseError("unknown reference %s" % exc)
749
750 doc.actions = [act for name, act in doc._actions.items()
751 if name and name.startswith("top")]
752
753 del(doc._bullet_refs)
754 del(doc._action_refs)
755 del(doc._fire_refs)
756 del(doc._bullets)
757 del(doc._actions)
758 del(doc._fires)
759
760 return doc
761
762 @classmethod
763 def FromYAML(cls, source):
764 """Create a BulletML instance based on YAML."""
765
766 # Late import to avoid a circular dependency.
767 try:
768 import bulletml.bulletyaml
769 import yaml
770 except ImportError:
771 raise ParseError("PyYAML is not available")
772 else:
773 try:
774 return yaml.load(source)
775 except Exception, exc:
776 raise ParseError(str(exc))
777
778 @classmethod
779 def FromDocument(cls, source):
780 """Create a BulletML instance based on a seekable file or string.
781
782 This attempts to autodetect if the stream is XML or YAML.
783 """
784 if not hasattr(source, 'read'):
785 source = StringIO(source)
786 start = source.read(1)
787 source.seek(0)
788 if start == "<":
789 return cls.FromXML(source)
790 elif start == "!" or start == "#":
791 return cls.FromYAML(source)
792 else:
793 raise ParseError("unknown initial character %r" % start)
794
795 def __repr__(self):
796 return "%s(type=%r, actions=%r)" % (
797 type(self).__name__, self.type, self.actions)
798
799 ActionDef.CONSTRUCTORS = dict(
800 repeat=Repeat,
801 fire=FireDef,
802 fireRef=FireRef,
803 changeSpeed=ChangeSpeed,
804 changeDirection=ChangeDirection,
805 accel=Accel,
806 wait=Wait,
807 vanish=Vanish,
808 tag=Tag,
809 untag=Untag,
810 action=ActionDef,
811 actionRef=ActionRef)