Major refactor. Since Python is duck-typed the parser/impl split in the Java BulletML...
[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 actions, the only class you
7 should care about in here is BulletML.
8 """
9
10 from __future__ import division
11
12 from math import sin, cos, radians, pi as PI
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 PI_2 = PI * 2
37
38 class ParseError(Error):
39 """Raised when an error occurs parsing the XML structure."""
40 pass
41
42 def realtag(element):
43 """Strip namespace poop off the front of a tag."""
44 try:
45 return element.tag.rsplit('}', 1)[1]
46 except ValueError:
47 return element.tag
48
49 class ParamList(object):
50 """List of parameter definitions."""
51
52 def __init__(self, params=()):
53 self.params = list(params)
54
55 @classmethod
56 def FromXML(cls, doc, element):
57 """Construct using an ElementTree-style element."""
58 return cls([NumberDef(subelem.text) for subelem in element
59 if realtag(subelem) == "param"])
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 = intern(type)
76 self.value = value
77
78 def __getstate__(self):
79 return [('type', self.type), ('value', self.value.expr)]
80
81 def __setstate__(self, state):
82 state = dict(state)
83 self.__init__(state["type"], NumberDef(state["value"]))
84
85 @classmethod
86 def FromXML(cls, doc, element, default="absolute"):
87 """Construct using an ElementTree-style element."""
88 return cls(element.get("type", default), NumberDef(element.text))
89
90 def __call__(self, params, rank):
91 return (radians(self.value(params, rank)), self.type)
92
93 def __repr__(self):
94 return "%s(%r, type=%r)" % (
95 type(self).__name__, self.value, self.type)
96
97 class ChangeDirection(object):
98 """Direction change over time."""
99
100 def __init__(self, term, direction):
101 self.term = term
102 self.direction = direction
103
104 def __getstate__(self):
105 return [('frames', self.term.expr),
106 ('type', self.direction.type),
107 ('value', self.direction.value.expr)]
108
109 def __setstate__(self, state):
110 state = dict(state)
111 self.__init__(INumberDef(state["frames"]),
112 Direction(state["type"], NumberDef(state["value"])))
113
114 @classmethod
115 def FromXML(cls, doc, element):
116 """Construct using an ElementTree-style element."""
117 for subelem in element.getchildren():
118 tag = realtag(subelem)
119 if tag == "direction":
120 direction = Direction.FromXML(doc, subelem)
121 elif tag == "term":
122 term = INumberDef(subelem.text)
123 try:
124 return cls(term, direction)
125 except UnboundLocalError as exc:
126 raise ParseError(str(exc))
127
128 def __call__(self, owner, action, params, rank, created):
129 frames = self.term(params, rank)
130 direction, type = self.direction(params, rank)
131 action.direction_frames = frames
132 action.aiming = False
133 if type == "sequence":
134 action.direction = direction
135 else:
136 if type == "absolute":
137 direction -= owner.direction
138 elif type != "relative": # aim or default
139 action.aiming = True
140 direction += owner.aim - owner.direction
141
142 # Normalize to [-pi, pi).
143 direction = (direction + PI) % PI_2 - PI
144 if frames <= 0:
145 owner.direction += direction
146 else:
147 action.direction = direction / frames
148
149 def __repr__(self):
150 return "%s(term=%r, direction=%r)" % (
151 type(self).__name__, self.term, self.direction)
152
153 class Speed(object):
154 """Raw speed value."""
155
156 VALID_TYPES = ["relative", "absolute", "sequence"]
157
158 def __init__(self, type, value):
159 if type not in self.VALID_TYPES:
160 raise ValueError("invalid type %r" % type)
161 self.type = intern(type)
162 self.value = value
163
164 def __getstate__(self):
165 return [('type', self.type), ('value', self.value.expr)]
166
167 def __setstate__(self, state):
168 state = dict(state)
169 self.__init__(state["type"], NumberDef(state["value"]))
170
171 @classmethod
172 def FromXML(cls, doc, element):
173 """Construct using an ElementTree-style element."""
174 return cls(element.get("type", "absolute"), NumberDef(element.text))
175
176 def __call__(self, params, rank):
177 return (self.value(params, rank), self.type)
178
179 def __repr__(self):
180 return "%s(%r, type=%r)" % (type(self).__name__, self.value, self.type)
181
182 class ChangeSpeed(object):
183 """Speed change over time."""
184
185 def __init__(self, term, speed):
186 self.term = term
187 self.speed = speed
188
189 def __getstate__(self):
190 return [('frames', self.term.expr),
191 ('type', self.speed.type),
192 ('value', self.speed.value.expr)]
193
194 def __setstate__(self, state):
195 state = dict(state)
196 self.__init__(INumberDef(state["frames"]),
197 Speed(state["type"], NumberDef(state["value"])))
198
199 @classmethod
200 def FromXML(cls, doc, element):
201 """Construct using an ElementTree-style element."""
202 for subelem in element.getchildren():
203 tag = realtag(subelem)
204 if tag == "speed":
205 speed = Speed.FromXML(doc, subelem)
206 elif tag == "term":
207 term = INumberDef(subelem.text)
208 try:
209 return cls(term, speed)
210 except UnboundLocalError as exc:
211 raise ParseError(str(exc))
212
213 def __call__(self, owner, action, params, rank, created):
214 frames = self.term(params, rank)
215 speed, type = self.speed(params, rank)
216 action.speed_frames = frames
217 if frames <= 0:
218 if type == "absolute":
219 owner.speed = speed
220 elif type == "relative":
221 owner.speed += speed
222 elif type == "sequence":
223 action.speed = speed
224 elif type == "relative":
225 action.speed = speed / frames
226 else:
227 action.speed = (speed - owner.speed) / frames
228
229 def __repr__(self):
230 return "%s(term=%r, speed=%r)" % (
231 type(self).__name__, self.term, self.speed)
232
233 class Wait(object):
234 """Wait for some frames."""
235
236 def __init__(self, frames):
237 self.frames = frames
238
239 def __getstate__(self):
240 return dict(frames=self.frames.expr)
241
242 def __setstate__(self, state):
243 self.__init__(INumberDef(state["frames"]))
244
245 @classmethod
246 def FromXML(cls, doc, element):
247 """Construct using an ElementTree-style element."""
248 return cls(INumberDef(element.text))
249
250 def __call__(self, owner, action, params, rank, created):
251 action.wait_frames = self.frames(params, rank)
252 return True
253
254 def __repr__(self):
255 return "%s(%r)" % (type(self).__name__, self.frames)
256
257 class Tag(object):
258 """Set a bullet tag."""
259
260 def __init__(self, tag):
261 self.tag = tag
262
263 def __getstate__(self):
264 return dict(tag=self.tag)
265
266 def __setstate__(self, state):
267 self.__init__(state["tag"])
268
269 @classmethod
270 def FromXML(cls, doc, element):
271 """Construct using an ElementTree-style element."""
272 return cls(element.text)
273
274 def __call__(self, owner, action, params, rank, created):
275 owner.tags.add(self.tag)
276
277 class Untag(object):
278 """Unset a bullet tag."""
279
280 def __init__(self, tag):
281 self.tag = tag
282
283 def __getstate__(self):
284 return dict(tag=self.tag)
285
286 def __setstate__(self, state):
287 self.__init__(state["tag"])
288
289 @classmethod
290 def FromXML(cls, doc, element):
291 """Construct using an ElementTree-style element."""
292 return cls(element.text)
293
294 def __call__(self, owner, action, params, rank, created):
295 try:
296 owner.tags.remove(self.tag)
297 except KeyError:
298 pass
299
300 class Appearance(object):
301 """Set a bullet appearance."""
302
303 def __init__(self, appearance):
304 self.appearance = appearance
305
306 def __getstate__(self):
307 return dict(appearance=self.appearance)
308
309 def __setstate__(self, state):
310 self.__init__(state["appearance"])
311
312 @classmethod
313 def FromXML(cls, doc, element):
314 """Construct using an ElementTree-style element."""
315 return cls(element.text)
316
317 def __call__(self, owner, action, params, rank, created):
318 owner.apearance = self.appearance
319
320 class Vanish(object):
321 """Make the owner disappear."""
322
323 def __init__(self):
324 pass
325
326 @classmethod
327 def FromXML(cls, doc, element):
328 """Construct using an ElementTree-style element."""
329 return cls()
330
331 def __repr__(self):
332 return "%s()" % (type(self).__name__)
333
334 def __call__(self, owner, action, params, rank, created):
335 owner.vanish()
336 return True
337
338 class Repeat(object):
339 """Repeat an action definition."""
340
341 def __init__(self, times, action):
342 self.times = times
343 self.action = action
344
345 def __getstate__(self):
346 return [('times', self.times.expr), ('action', self.action)]
347
348 def __setstate__(self, state):
349 state = dict(state)
350 self.__init__(INumberDef(state["times"]), state["action"])
351
352 @classmethod
353 def FromXML(cls, doc, element):
354 """Construct using an ElementTree-style element."""
355 for subelem in element.getchildren():
356 tag = realtag(subelem)
357 if tag == "times":
358 times = INumberDef(subelem.text)
359 elif tag == "action":
360 action = ActionDef.FromXML(doc, subelem)
361 elif tag == "actionRef":
362 action = ActionRef.FromXML(doc, subelem)
363 try:
364 return cls(times, action)
365 except UnboundLocalError as exc:
366 raise ParseError(str(exc))
367
368 def __call__(self, owner, action, params, rank, created):
369 repeat = self.times(params, rank)
370 actions, params = self.action(params, rank)
371 child = action.__class__(
372 owner, action, actions, params, rank, repeat)
373 owner.replace(action, child)
374 child.step(owner, created)
375 return True
376
377 def __repr__(self):
378 return "%s(%r, %r)" % (type(self).__name__, self.times, self.action)
379
380 class Accel(object):
381 """Accelerate over some time."""
382
383 horizontal = None
384 vertical = None
385
386 def __init__(self, term, horizontal=None, vertical=None):
387 self.term = term
388 self.horizontal = horizontal
389 self.vertical = vertical
390
391 def __getstate__(self):
392 state = [('frames', self.term.expr)]
393 if self.horizontal:
394 state.append(('horizontal', self.horizontal))
395 if self.vertical:
396 state.append(('vertical', self.vertical))
397 return state
398
399 def __setstate__(self, state):
400 state = dict(state)
401 self.__init__(INumberDef(state["frames"]), state.get("horizontal"),
402 state.get("vertical"))
403
404 @classmethod
405 def FromXML(cls, doc, element):
406 """Construct using an ElementTree-style element."""
407 horizontal = None
408 vertical = None
409
410 for subelem in element.getchildren():
411 tag = realtag(subelem)
412 if tag == "term":
413 term = INumberDef(subelem.text)
414 elif tag == "horizontal":
415 horizontal = Speed.FromXML(doc, subelem)
416 elif tag == "vertical":
417 vertical = Speed.FromXML(doc, subelem)
418
419 try:
420 return cls(term, horizontal, vertical)
421 except AttributeError:
422 raise ParseError
423
424 def __call__(self, owner, action, params, rank, created):
425 frames = self.term(params, rank)
426 horizontal = self.horizontal and self.horizontal(params, rank)
427 vertical = self.vertical and self.vertical(params, rank)
428 action.accel_frames = frames
429 if horizontal:
430 mx, type = horizontal
431 if frames <= 0:
432 if type == "absolute":
433 owner.mx = mx
434 elif type == "relative":
435 owner.mx += mx
436 elif type == "sequence":
437 action.mx = mx
438 elif type == "absolute":
439 action.mx = (mx - owner.mx) / frames
440 elif type == "relative":
441 action.mx = mx / frames
442 if vertical:
443 my, type = vertical
444 if frames <= 0:
445 if type == "absolute":
446 owner.my = my
447 elif type == "relative":
448 owner.my += my
449 elif type == "sequence":
450 action.my = my
451 elif type == "absolute":
452 action.my = (my - owner.my) / frames
453 elif type == "relative":
454 action.my = my / frames
455
456 def __repr__(self):
457 return "%s(%r, horizontal=%r, vertical=%r)" % (
458 type(self).__name__, self.term, self.horizontal, self.vertical)
459
460 class BulletDef(object):
461 """Bullet definition."""
462
463 def __init__(self, actions=(), direction=None, speed=None, tags=(),
464 appearance=None):
465 self.direction = direction
466 self.speed = speed
467 self.actions = list(actions)
468 self.tags = set(tags)
469 self.appearance = appearance
470
471 def __getstate__(self):
472 state = []
473 if self.direction:
474 state.append(("direction", self.direction))
475 if self.speed:
476 state.append(("speed", self.speed))
477 if self.actions:
478 state.append(("actions", self.actions))
479 if self.tags:
480 state.append(("tags", list(self.tags)))
481 if self.appearance:
482 state.append(("appearance", self.appearance))
483 return state
484
485 def __setstate__(self, state):
486 state = dict(state)
487 self.__init__(**state)
488
489 @classmethod
490 def FromXML(cls, doc, element):
491 """Construct using an ElementTree-style element."""
492 actions = []
493 speed = None
494 direction = None
495 tags = set()
496 for subelem in element.getchildren():
497 tag = realtag(subelem)
498 if tag == "direction":
499 direction = Direction.FromXML(doc, subelem)
500 elif tag == "speed":
501 speed = Speed.FromXML(doc, subelem)
502 elif tag == "action":
503 actions.append(ActionDef.FromXML(doc, subelem))
504 elif tag == "actionRef":
505 actions.append(ActionRef.FromXML(doc, subelem))
506 elif tag == "tag":
507 tags.add(subelem.text)
508 dfn = cls(actions, direction, speed, tags)
509 doc._bullets[element.get("label")] = dfn
510 return dfn
511
512 def __call__(self, params, rank):
513 actions = [action(params, rank) for action in self.actions]
514 return (
515 self.direction and self.direction(params, rank),
516 self.speed and self.speed(params, rank),
517 self.tags,
518 self.appearance,
519 actions)
520
521 def __repr__(self):
522 return "%s(direction=%r, speed=%r, actions=%r)" % (
523 type(self).__name__, self.direction, self.speed, self.actions)
524
525 class BulletRef(object):
526 """Create a bullet by name with parameters."""
527
528 def __init__(self, bullet, params=None):
529 self.bullet = bullet
530 self.params = ParamList() if params is None else params
531
532 def __getstate__(self):
533 state = []
534 if self.params.params:
535 params = [param.expr for param in self.params.params]
536 state.append(("params", params))
537 state.append(('bullet', self.bullet))
538 return state
539
540 def __setstate__(self, state):
541 state = dict(state)
542 bullet = state["bullet"]
543 params = [NumberDef(param) for param in state.get("params", [])]
544 self.__init__(bullet, ParamList(params))
545
546 @classmethod
547 def FromXML(cls, doc, element):
548 """Construct using an ElementTree-style element."""
549 bullet = cls(element.get("label"), ParamList.FromXML(doc, element))
550 doc._bullet_refs.append(bullet)
551 return bullet
552
553 def __call__(self, params, rank):
554 return self.bullet(self.params(params, rank), rank)
555
556 def __repr__(self):
557 return "%s(params=%r, bullet=%r)" % (
558 type(self).__name__, self.params, self.bullet)
559
560 class ActionDef(object):
561 """Action definition.
562
563 To support parsing new actions, add tags to
564 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
565 FromXML classmethod, which take the BulletML instance and
566 ElementTree element as arguments.
567 """
568
569 # This is self-referential, so it's filled in later.
570 CONSTRUCTORS = dict()
571
572 def __init__(self, actions):
573 self.actions = list(actions)
574
575 def __getstate__(self):
576 return dict(actions=self.actions)
577
578 def __setstate__(self, state):
579 state = dict(state)
580 self.__init__(state["actions"])
581
582 @classmethod
583 def FromXML(cls, doc, element):
584 """Construct using an ElementTree-style element."""
585 actions = []
586 for subelem in element.getchildren():
587 tag = realtag(subelem)
588 try:
589 ctr = cls.CONSTRUCTORS[tag]
590 except KeyError:
591 continue
592 else:
593 actions.append(ctr.FromXML(doc, subelem))
594 dfn = cls(actions)
595 doc._actions[element.get("label")] = dfn
596 return dfn
597
598 def __call__(self, params, rank):
599 return self.actions, params
600
601 def __repr__(self):
602 return "%s(%r)" % (type(self).__name__, self.actions)
603
604 class ActionRef(object):
605 """Run an action by name with parameters."""
606
607 def __init__(self, action, params=None):
608 self.action = action
609 self.params = params or ParamList()
610
611 def __getstate__(self):
612 state = []
613 if self.params.params:
614 params = [param.expr for param in self.params.params]
615 state.append(("params", params))
616 state.append(('action', self.action))
617 return state
618
619 def __setstate__(self, state):
620 state = dict(state)
621 action = state["action"]
622 params = [NumberDef(param) for param in state.get("params", [])]
623 self.__init__(action, ParamList(params))
624
625 @classmethod
626 def FromXML(cls, doc, element):
627 """Construct using an ElementTree-style element."""
628 action = cls(element.get("label"), ParamList.FromXML(doc, element))
629 doc._action_refs.append(action)
630 return action
631
632 def __call__(self, params, rank):
633 return self.action(self.params(params, rank), rank)
634
635 def __repr__(self):
636 return "%s(params=%r, action=%r)" % (
637 type(self).__name__, self.params, self.action)
638
639 class Offset(object):
640 """Provide an offset to a bullet's initial position."""
641
642 VALID_TYPES = ["relative", "absolute"]
643
644 def __init__(self, type, x, y):
645 if type not in self.VALID_TYPES:
646 raise ValueError("invalid type %r" % type)
647 self.type = intern(type)
648 self.x = x
649 self.y = y
650
651 def __getstate__(self):
652 state = [('type', self.type)]
653 if self.x:
654 state.append(('x', self.x.expr))
655 if self.y:
656 state.append(('y', self.y.expr))
657 return state
658
659 def __setstate__(self, state):
660 state = dict(state)
661 x = NumberDef(state["x"]) if "x" in state else None
662 y = NumberDef(state["y"]) if "y" in state else None
663 self.__init__(state["type"], x, y)
664
665 @classmethod
666 def FromXML(cls, doc, element):
667 """Construct using an ElementTree-style element."""
668 type = element.get("type", "relative")
669 x = None
670 y = None
671 for subelem in element:
672 tag = realtag(subelem)
673 if tag == "x":
674 x = NumberDef(subelem.text)
675 elif tag == "y":
676 y = NumberDef(subelem.text)
677 return cls(type, x, y)
678
679 def __call__(self, params, rank):
680 return (self.x(params, rank) if self.x else 0,
681 self.y(params, rank) if self.y else 0)
682
683 class FireDef(object):
684 """Fire definition (creates a bullet)."""
685
686 def __init__(self, bullet, direction=None, speed=None, offset=None,
687 tags=(), appearance=None):
688 self.bullet = bullet
689 self.direction = direction
690 self.speed = speed
691 self.offset = offset
692 self.tags = set(tags)
693 self.appearance = appearance
694
695 def __getstate__(self):
696 state = []
697 if self.direction:
698 state.append(("direction", self.direction))
699 if self.speed:
700 state.append(("speed", self.speed))
701 if self.offset:
702 state.append(("offset", self.offset))
703 if self.tags:
704 state.append(("tags", list(self.tags)))
705 if self.appearance:
706 state.append(("appearance", self.appearance))
707 try:
708 params = self.bullet.params
709 except AttributeError:
710 state.append(('bullet', self.bullet))
711 else:
712 if params.params:
713 state.append(('bullet', self.bullet))
714 else:
715 # Strip out empty BulletRefs.
716 state.append(('bullet', self.bullet.bullet))
717 return state
718
719 def __setstate__(self, state):
720 state = dict(state)
721 self.__init__(**state)
722
723 @classmethod
724 def FromXML(cls, doc, element):
725 """Construct using an ElementTree-style element."""
726 direction = None
727 speed = None
728 offset = None
729 tags = set()
730 appearance = None
731
732 for subelem in element.getchildren():
733 tag = realtag(subelem)
734 if tag == "direction":
735 direction = Direction.FromXML(doc, subelem, "aim")
736 elif tag == "speed":
737 speed = Speed.FromXML(doc, subelem)
738 elif tag == "bullet":
739 bullet = BulletDef.FromXML(doc, subelem)
740 elif tag == "bulletRef":
741 bullet = BulletRef.FromXML(doc, subelem)
742 elif tag == "offset":
743 offset = Offset.FromXML(doc, subelem)
744 elif tag == "tag":
745 tags.add(subelem.text)
746 elif tag == "appearance":
747 appearance = subelem.text
748 try:
749 fire = cls(bullet, direction, speed, offset, tags, appearance)
750 except UnboundLocalError as exc:
751 raise ParseError(str(exc))
752 else:
753 doc._fires[element.get("label")] = fire
754 return fire
755
756 def __call__(self, owner, action, params, rank, created):
757 direction, speed, tags, appearance, actions = self.bullet(params, rank)
758 if self.direction:
759 direction = self.direction(params, rank)
760 if self.speed:
761 speed = self.speed(params, rank)
762 tags = tags.union(self.tags)
763 if self.appearance:
764 appearance = self.appearance
765
766 if direction:
767 direction, type = direction
768 if type == "aim" or type is None:
769 direction += owner.aim
770 elif type == "sequence":
771 direction += action.previous_fire_direction
772 elif type == "relative":
773 direction += owner.direction
774 else:
775 direction = owner.aim
776 action.previous_fire_direction = direction
777
778 if speed:
779 speed, type = speed
780 if type == "sequence":
781 speed += action.previous_fire_speed
782 elif type == "relative":
783 # The reference Noiz implementation uses
784 # prvFireSpeed here, but the standard is
785 # pretty clear -- "In case of the type is
786 # "relative", ... the speed is relative to the
787 # speed of this bullet."
788 speed += owner.speed
789 else:
790 speed = 1
791 action.previous_fire_speed = speed
792
793 x, y = owner.x, owner.y
794 if self.offset:
795 off_x, off_y = self.offset(params, rank)
796 if self.offset.type == "relative":
797 s = sin(direction)
798 c = cos(direction)
799 x += c * off_x + s * off_y
800 y += s * off_x - c * off_y
801 else:
802 x += off_x
803 y += off_y
804
805 if appearance is None:
806 appearance = owner.appearance
807 bullet = owner.__class__(
808 x=x, y=y, direction=direction, speed=speed,
809 target=owner.target, actions=actions, rank=rank,
810 appearance=appearance, tags=tags, Action=action.__class__)
811 created.append(bullet)
812
813 def __repr__(self):
814 return "%s(direction=%r, speed=%r, bullet=%r)" % (
815 type(self).__name__, self.direction, self.speed, self.bullet)
816
817 class FireRef(object):
818 """Fire a bullet by name with parameters."""
819
820 def __init__(self, fire, params=None):
821 self.fire = fire
822 self.params = params or ParamList()
823
824 def __getstate__(self):
825 state = []
826 if self.params.params:
827 params = [param.expr for param in self.params.params]
828 state.append(("params", params))
829 state.append(('fire', self.fire))
830 return state
831
832 def __setstate__(self, state):
833 state = dict(state)
834 fire = state["fire"]
835 params = [NumberDef(param) for param in state.get("params", [])]
836 self.__init__(fire, ParamList(params))
837
838 @classmethod
839 def FromXML(cls, doc, element):
840 """Construct using an ElementTree-style element."""
841 fired = cls(element.get("label"), ParamList.FromXML(doc, element))
842 doc._fire_refs.append(fired)
843 return fired
844
845 def __call__(self, owner, action, params, rank, created):
846 params = self.params(params, rank)
847 return self.fire(owner, action, params, rank, created)
848
849 def __repr__(self):
850 return "%s(params=%r, fire=%r)" % (
851 type(self).__name__, self.params, self.fire)
852
853 class BulletML(object):
854 """BulletML document.
855
856 A BulletML document is a collection of top-level actions and the
857 base game type.
858
859 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
860 its parsing. It maps tag names to classes with a FromXML
861 classmethod, which take the BulletML instance and ElementTree
862 element as arguments.
863
864 """
865
866 CONSTRUCTORS = dict(
867 bullet=BulletDef,
868 action=ActionDef,
869 fire=FireDef,
870 )
871
872 def __init__(self, type="none", actions=None):
873 self.type = intern(type)
874 self.actions = [] if actions is None else actions
875
876 def __getstate__(self):
877 return [('type', self.type), ('actions', self.actions)]
878
879 def __setstate__(self, state):
880 state = dict(state)
881 self.__init__(state["type"], actions=state.get("actions"))
882
883 @classmethod
884 def FromXML(cls, source):
885 """Return a BulletML instance based on XML."""
886 if not hasattr(source, 'read'):
887 source = StringIO(source)
888
889 tree = ElementTree()
890 root = tree.parse(source)
891
892 doc = cls(type=root.get("type", "none"))
893
894 doc._bullets = {}
895 doc._actions = {}
896 doc._fires = {}
897 doc._bullet_refs = []
898 doc._action_refs = []
899 doc._fire_refs = []
900
901 for element in root.getchildren():
902 tag = realtag(element)
903 if tag in doc.CONSTRUCTORS:
904 doc.CONSTRUCTORS[tag].FromXML(doc, element)
905
906 try:
907 for ref in doc._bullet_refs:
908 ref.bullet = doc._bullets[ref.bullet]
909 for ref in doc._fire_refs:
910 ref.fire = doc._fires[ref.fire]
911 for ref in doc._action_refs:
912 ref.action = doc._actions[ref.action]
913 except KeyError as exc:
914 raise ParseError("unknown reference %s" % exc)
915
916 doc.actions = [act for name, act in doc._actions.items()
917 if name and name.startswith("top")]
918
919 del(doc._bullet_refs)
920 del(doc._action_refs)
921 del(doc._fire_refs)
922 del(doc._bullets)
923 del(doc._actions)
924 del(doc._fires)
925
926 return doc
927
928 @classmethod
929 def FromYAML(cls, source):
930 """Create a BulletML instance based on YAML."""
931
932 # Late import to avoid a circular dependency.
933 try:
934 import bulletml.bulletyaml
935 import yaml
936 except ImportError:
937 raise ParseError("PyYAML is not available")
938 else:
939 try:
940 return yaml.load(source)
941 except Exception as exc:
942 raise ParseError(str(exc))
943
944 @classmethod
945 def FromDocument(cls, source):
946 """Create a BulletML instance based on a seekable file or string.
947
948 This attempts to autodetect if the stream is XML or YAML.
949 """
950 if not hasattr(source, 'read'):
951 source = StringIO(source)
952 start = source.read(1)
953 source.seek(0)
954 if start == "<":
955 return cls.FromXML(source)
956 elif start == "!" or start == "#":
957 return cls.FromYAML(source)
958 else:
959 raise ParseError("unknown initial character %r" % start)
960
961 def __repr__(self):
962 return "%s(type=%r, actions=%r)" % (
963 type(self).__name__, self.type, self.actions)
964
965 ActionDef.CONSTRUCTORS = dict(
966 repeat=Repeat,
967 fire=FireDef,
968 fireRef=FireRef,
969 changeSpeed=ChangeSpeed,
970 changeDirection=ChangeDirection,
971 accel=Accel,
972 wait=Wait,
973 vanish=Vanish,
974 tag=Tag,
975 appearance=Appearance,
976 untag=Untag,
977 action=ActionDef,
978 actionRef=ActionRef)