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