3 This is based on the format described at
4 http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/bulletml_ref_e.html.
6 Unless you are adding support for new tags, the only class you should
7 care about in here is BulletML.
10 from __future__
import division
14 from xml
.etree
.ElementTree
import ElementTree
16 # Python 3 moved this for no really good reason.
18 from sys
import intern
23 from io
import StringIO
26 from cStringIO
import StringIO
28 from StringIO
import StringIO
30 from bulletml
.errors
import Error
31 from bulletml
.expr
import NumberDef
, INumberDef
34 __all__
= ["ParseError", "BulletML"]
36 class ParseError(Error
):
37 """Raised when an error occurs parsing the XML structure."""
41 """Strip namespace poop off the front of a tag."""
43 return element
.tag
.rsplit('}', 1)[1]
47 class ParamList(object):
48 """List of parameter definitions."""
50 def __init__(self
, params
=()):
51 self
.params
= list(params
)
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"])
59 def __call__(self
, params
, rank
):
60 return [param(params
, rank
) for param
in self
.params
]
63 return "%s(%r)" % (type(self
).__name
__, self
.params
)
65 class Direction(object):
66 """Raw direction value."""
68 VALID_TYPES
= ["relative", "absolute", "aim", "sequence"]
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)
76 def __getstate__(self
):
77 return [('type', self
.type), ('value', self
.value
.expr
)]
79 def __setstate__(self
, state
):
81 self
.__init
__(state
["type"], NumberDef(state
["value"]))
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
))
88 def __call__(self
, params
, rank
):
89 return (math
.radians(self
.value(params
, rank
)), self
.type)
92 return "%s(%r, type=%r)" % (
93 type(self
).__name
__, self
.value
, self
.type)
95 class ChangeDirection(object):
96 """Direction change over time."""
98 def __init__(self
, term
, direction
):
100 self
.direction
= direction
102 def __getstate__(self
):
103 return [('frames', self
.term
.expr
),
104 ('type', self
.direction
.type),
105 ('value', self
.direction
.value
.expr
)]
107 def __setstate__(self
, state
):
109 self
.__init
__(INumberDef(state
["frames"]),
110 Direction(state
["type"], NumberDef(state
["value"])))
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
)
120 term
= INumberDef(subelem
.text
)
122 return cls(term
, direction
)
123 except UnboundLocalError as exc
:
124 raise ParseError(str(exc
))
126 def __call__(self
, params
, rank
):
127 return self
.term(params
, rank
), self
.direction(params
, rank
)
130 return "%s(term=%r, direction=%r)" % (
131 type(self
).__name
__, self
.term
, self
.direction
)
134 """Raw speed value."""
136 VALID_TYPES
= ["relative", "absolute", "sequence"]
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)
144 def __getstate__(self
):
145 return [('type', self
.type), ('value', self
.value
.expr
)]
147 def __setstate__(self
, state
):
149 self
.__init
__(state
["type"], NumberDef(state
["value"]))
152 def FromXML(cls
, doc
, element
):
153 """Construct using an ElementTree-style element."""
154 return cls(element
.get("type", "absolute"), NumberDef(element
.text
))
156 def __call__(self
, params
, rank
):
157 return (self
.value(params
, rank
), self
.type)
160 return "%s(%r, type=%r)" % (type(self
).__name
__, self
.value
, self
.type)
162 class ChangeSpeed(object):
163 """Speed change over time."""
165 def __init__(self
, term
, speed
):
169 def __getstate__(self
):
170 return [('frames', self
.term
.expr
),
171 ('type', self
.speed
.type),
172 ('value', self
.speed
.value
.expr
)]
174 def __setstate__(self
, state
):
176 self
.__init
__(INumberDef(state
["frames"]),
177 Speed(state
["type"], NumberDef(state
["value"])))
180 def FromXML(cls
, doc
, element
):
181 """Construct using an ElementTree-style element."""
182 for subelem
in element
.getchildren():
183 tag
= realtag(subelem
)
185 speed
= Speed
.FromXML(doc
, subelem
)
187 term
= INumberDef(subelem
.text
)
189 return cls(term
, speed
)
190 except UnboundLocalError as exc
:
191 raise ParseError(str(exc
))
193 def __call__(self
, params
, rank
):
194 return self
.term(params
, rank
), self
.speed(params
, rank
)
197 return "%s(term=%r, speed=%r)" % (
198 type(self
).__name
__, self
.term
, self
.speed
)
201 """Wait for some frames."""
203 def __init__(self
, frames
):
206 def __getstate__(self
):
207 return dict(frames
=self
.frames
.expr
)
209 def __setstate__(self
, state
):
210 self
.__init
__(INumberDef(state
["frames"]))
213 def FromXML(cls
, doc
, element
):
214 """Construct using an ElementTree-style element."""
215 return cls(INumberDef(element
.text
))
217 def __call__(self
, params
, rank
):
218 return self
.frames(params
, rank
)
221 return "%s(%r)" % (type(self
).__name
__, self
.frames
)
224 """Set a bullet tag."""
226 def __init__(self
, tag
):
229 def __getstate__(self
):
230 return dict(tag
=self
.tag
)
232 def __setstate__(self
, state
):
233 self
.__init
__(state
["tag"])
236 def FromXML(cls
, doc
, element
):
237 """Construct using an ElementTree-style element."""
238 return cls(element
.text
)
241 """Unset a bullet tag."""
243 def __init__(self
, tag
):
246 def __getstate__(self
):
247 return dict(tag
=self
.tag
)
249 def __setstate__(self
, state
):
250 self
.__init
__(state
["tag"])
253 def FromXML(cls
, doc
, element
):
254 """Construct using an ElementTree-style element."""
255 return cls(element
.text
)
257 class Vanish(object):
258 """Make the owner disappear."""
264 def FromXML(cls
, doc
, element
):
265 """Construct using an ElementTree-style element."""
269 return "%s()" % (type(self
).__name
__)
271 class Repeat(object):
272 """Repeat an action definition."""
274 def __init__(self
, times
, action
):
278 def __getstate__(self
):
279 return [('times', self
.times
.expr
), ('action', self
.action
)]
281 def __setstate__(self
, state
):
283 self
.__init
__(INumberDef(state
["times"]), state
["action"])
286 def FromXML(cls
, doc
, element
):
287 """Construct using an ElementTree-style element."""
288 for subelem
in element
.getchildren():
289 tag
= realtag(subelem
)
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
)
297 return cls(times
, action
)
298 except UnboundLocalError as exc
:
299 raise ParseError(str(exc
))
301 def __call__(self
, params
, rank
):
302 return self
.times(params
, rank
), self
.action(params
, rank
)
305 return "%s(%r, %r)" % (type(self
).__name
__, self
.times
, self
.action
)
308 """Accelerate over some time."""
313 def __init__(self
, term
, horizontal
=None, vertical
=None):
315 self
.horizontal
= horizontal
316 self
.vertical
= vertical
318 def __getstate__(self
):
319 state
= [('frames', self
.term
.expr
)]
321 state
.append(('horizontal', self
.horizontal
))
323 state
.append(('vertical', self
.vertical
))
326 def __setstate__(self
, state
):
328 self
.__init
__(INumberDef(state
["frames"]), state
.get("horizontal"),
329 state
.get("vertical"))
332 def FromXML(cls
, doc
, element
):
333 """Construct using an ElementTree-style element."""
337 for subelem
in element
.getchildren():
338 tag
= realtag(subelem
)
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
)
347 return cls(term
, horizontal
, vertical
)
348 except AttributeError:
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
358 return "%s(%r, horizontal=%r, vertical=%r)" % (
359 type(self
).__name
__, self
.term
, self
.horizontal
, self
.vertical
)
361 class BulletDef(object):
362 """Bullet definition."""
364 def __init__(self
, actions
=(), direction
=None, speed
=None, tags
=()):
365 self
.direction
= direction
367 self
.actions
= list(actions
)
368 self
.tags
= set(tags
)
370 def __getstate__(self
):
373 state
.append(("direction", self
.direction
))
375 state
.append(("speed", self
.speed
))
377 state
.append(("actions", self
.actions
))
379 state
.append(("tags", list(self
.tags
)))
382 def __setstate__(self
, state
):
384 self
.__init
__(**state
)
387 def FromXML(cls
, doc
, element
):
388 """Construct using an ElementTree-style element."""
393 for subelem
in element
.getchildren():
394 tag
= realtag(subelem
)
395 if tag
== "direction":
396 direction
= Direction
.FromXML(doc
, subelem
)
398 speed
= Speed
.FromXML(doc
, subelem
)
399 elif tag
== "action":
400 actions
.append(ActionDef
.FromXML(doc
, subelem
))
401 elif tag
== "actionRef":
402 actions
.append(ActionRef
.FromXML(doc
, subelem
))
404 tags
.add(subelem
.text
)
405 dfn
= cls(actions
, direction
, speed
, tags
)
406 doc
._bullets
[element
.get("label")] = dfn
409 def __call__(self
, params
, rank
):
410 actions
= [action(params
, rank
) for action
in self
.actions
]
412 self
.direction
and self
.direction(params
, rank
),
413 self
.speed
and self
.speed(params
, rank
),
418 return "%s(direction=%r, speed=%r, actions=%r)" % (
419 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
421 class BulletRef(object):
422 """Create a bullet by name with parameters."""
424 def __init__(self
, bullet
, params
=None):
426 self
.params
= ParamList() if params
is None else params
428 def __getstate__(self
):
430 if self
.params
.params
:
431 params
= [param
.expr
for param
in self
.params
.params
]
432 state
.append(("params", params
))
433 state
.append(('bullet', self
.bullet
))
436 def __setstate__(self
, state
):
438 bullet
= state
["bullet"]
439 params
= [NumberDef(param
) for param
in state
.get("params", [])]
440 self
.__init
__(bullet
, ParamList(params
))
443 def FromXML(cls
, doc
, element
):
444 """Construct using an ElementTree-style element."""
445 bullet
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
446 doc
._bullet
_refs
.append(bullet
)
449 def __call__(self
, params
, rank
):
450 return self
.bullet(self
.params(params
, rank
), rank
)
453 return "%s(params=%r, bullet=%r)" % (
454 type(self
).__name
__, self
.params
, self
.bullet
)
456 class ActionDef(object):
457 """Action definition.
459 To support parsing new actions, add tags to
460 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
461 FromXML classmethod, which take the BulletML instance and
462 ElementTree element as arguments.
465 # This is self-referential, so it's filled in later.
466 CONSTRUCTORS
= dict()
468 def __init__(self
, actions
):
469 self
.actions
= list(actions
)
471 def __getstate__(self
):
472 return dict(actions
=self
.actions
)
474 def __setstate__(self
, state
):
476 self
.__init
__(state
["actions"])
479 def FromXML(cls
, doc
, element
):
480 """Construct using an ElementTree-style element."""
482 for subelem
in element
.getchildren():
483 tag
= realtag(subelem
)
485 ctr
= cls
.CONSTRUCTORS
[tag
]
489 actions
.append(ctr
.FromXML(doc
, subelem
))
491 doc
._actions
[element
.get("label")] = dfn
494 def __call__(self
, params
, rank
):
495 return self
.actions
, params
498 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
500 class ActionRef(object):
501 """Run an action by name with parameters."""
503 def __init__(self
, action
, params
=None):
505 self
.params
= params
or ParamList()
507 def __getstate__(self
):
509 if self
.params
.params
:
510 params
= [param
.expr
for param
in self
.params
.params
]
511 state
.append(("params", params
))
512 state
.append(('action', self
.action
))
515 def __setstate__(self
, state
):
517 action
= state
["action"]
518 params
= [NumberDef(param
) for param
in state
.get("params", [])]
519 self
.__init
__(action
, ParamList(params
))
522 def FromXML(cls
, doc
, element
):
523 """Construct using an ElementTree-style element."""
524 action
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
525 doc
._action
_refs
.append(action
)
528 def __call__(self
, params
, rank
):
529 return self
.action(self
.params(params
, rank
), rank
)
532 return "%s(params=%r, action=%r)" % (
533 type(self
).__name
__, self
.params
, self
.action
)
535 class Offset(object):
536 """Provide an offset to a bullet's initial position."""
538 VALID_TYPES
= ["relative", "absolute"]
540 def __init__(self
, type, x
, y
):
541 if type not in self
.VALID_TYPES
:
542 raise ValueError("invalid type %r" % type)
543 self
.type = intern(type)
547 def __getstate__(self
):
548 state
= [('type', self
.type)]
550 state
.append(('x', self
.x
.expr
))
552 state
.append(('y', self
.y
.expr
))
555 def __setstate__(self
, state
):
557 x
= NumberDef(state
["x"]) if "x" in state
else None
558 y
= NumberDef(state
["y"]) if "y" in state
else None
559 self
.__init
__(state
["type"], x
, y
)
562 def FromXML(cls
, doc
, element
):
563 """Construct using an ElementTree-style element."""
564 type = element
.get("type", "relative")
567 for subelem
in element
:
568 tag
= realtag(subelem
)
570 x
= NumberDef(subelem
.text
)
572 y
= NumberDef(subelem
.text
)
573 return cls(type, x
, y
)
575 def __call__(self
, params
, rank
):
576 return (self
.x(params
, rank
) if self
.x
else 0,
577 self
.y(params
, rank
) if self
.y
else 0)
579 class FireDef(object):
580 """Fire definition (creates a bullet)."""
583 self
, bullet
, direction
=None, speed
=None, offset
=None, tags
=()):
585 self
.direction
= direction
588 self
.tags
= set(tags
)
590 def __getstate__(self
):
593 state
.append(("direction", self
.direction
))
595 state
.append(("speed", self
.speed
))
597 state
.append(("offset", self
.offset
))
599 state
.append(("tags", list(self
.tags
)))
601 params
= self
.bullet
.params
602 except AttributeError:
603 state
.append(('bullet', self
.bullet
))
606 state
.append(('bullet', self
.bullet
))
608 # Strip out empty BulletRefs.
609 state
.append(('bullet', self
.bullet
.bullet
))
612 def __setstate__(self
, state
):
614 self
.__init
__(**state
)
617 def FromXML(cls
, doc
, element
):
618 """Construct using an ElementTree-style element."""
624 for subelem
in element
.getchildren():
625 tag
= realtag(subelem
)
626 if tag
== "direction":
627 direction
= Direction
.FromXML(doc
, subelem
, "aim")
629 speed
= Speed
.FromXML(doc
, subelem
)
630 elif tag
== "bullet":
631 bullet
= BulletDef
.FromXML(doc
, subelem
)
632 elif tag
== "bulletRef":
633 bullet
= BulletRef
.FromXML(doc
, subelem
)
634 elif tag
== "offset":
635 offset
= Offset
.FromXML(doc
, subelem
)
637 tags
.add(subelem
.text
)
639 fire
= cls(bullet
, direction
, speed
, offset
, tags
)
640 except UnboundLocalError as exc
:
641 raise ParseError(str(exc
))
643 doc
._fires
[element
.get("label")] = fire
646 def __call__(self
, params
, rank
):
647 direction
, speed
, tags
, actions
= self
.bullet(params
, rank
)
649 direction
= self
.direction(params
, rank
)
651 speed
= self
.speed(params
, rank
)
652 tags
= tags
.union(self
.tags
)
653 return direction
, speed
, self
.offset
, tags
, actions
656 return "%s(direction=%r, speed=%r, bullet=%r)" % (
657 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
659 class FireRef(object):
660 """Fire a bullet by name with parameters."""
662 def __init__(self
, fire
, params
=None):
664 self
.params
= params
or ParamList()
666 def __getstate__(self
):
668 if self
.params
.params
:
669 params
= [param
.expr
for param
in self
.params
.params
]
670 state
.append(("params", params
))
671 state
.append(('fire', self
.fire
))
674 def __setstate__(self
, state
):
677 params
= [NumberDef(param
) for param
in state
.get("params", [])]
678 self
.__init
__(fire
, ParamList(params
))
681 def FromXML(cls
, doc
, element
):
682 """Construct using an ElementTree-style element."""
683 fired
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
684 doc
._fire
_refs
.append(fired
)
687 def __call__(self
, params
, rank
):
688 return self
.fire(self
.params(params
, rank
), rank
)
691 return "%s(params=%r, fire=%r)" % (
692 type(self
).__name
__, self
.params
, self
.fire
)
694 class BulletML(object):
695 """BulletML document.
697 A BulletML document is a collection of top-level actions and the
700 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
701 its parsing. It maps tag names to classes with a FromXML
702 classmethod, which take the BulletML instance and ElementTree
703 element as arguments.
713 def __init__(self
, type="none", actions
=None):
714 self
.type = intern(type)
715 self
.actions
= [] if actions
is None else actions
717 def __getstate__(self
):
718 return [('type', self
.type), ('actions', self
.actions
)]
720 def __setstate__(self
, state
):
722 self
.__init
__(state
["type"], actions
=state
.get("actions"))
725 def FromXML(cls
, source
):
726 """Return a BulletML instance based on XML."""
727 if not hasattr(source
, 'read'):
728 source
= StringIO(source
)
731 root
= tree
.parse(source
)
733 doc
= cls(type=root
.get("type", "none"))
738 doc
._bullet
_refs
= []
739 doc
._action
_refs
= []
742 for element
in root
.getchildren():
743 tag
= realtag(element
)
744 if tag
in doc
.CONSTRUCTORS
:
745 doc
.CONSTRUCTORS
[tag
].FromXML(doc
, element
)
748 for ref
in doc
._bullet
_refs
:
749 ref
.bullet
= doc
._bullets
[ref
.bullet
]
750 for ref
in doc
._fire
_refs
:
751 ref
.fire
= doc
._fires
[ref
.fire
]
752 for ref
in doc
._action
_refs
:
753 ref
.action
= doc
._actions
[ref
.action
]
754 except KeyError as exc
:
755 raise ParseError("unknown reference %s" % exc
)
757 doc
.actions
= [act
for name
, act
in doc
._actions
.items()
758 if name
and name
.startswith("top")]
760 del(doc
._bullet
_refs
)
761 del(doc
._action
_refs
)
770 def FromYAML(cls
, source
):
771 """Create a BulletML instance based on YAML."""
773 # Late import to avoid a circular dependency.
775 import bulletml
.bulletyaml
778 raise ParseError("PyYAML is not available")
781 return yaml
.load(source
)
782 except Exception as exc
:
783 raise ParseError(str(exc
))
786 def FromDocument(cls
, source
):
787 """Create a BulletML instance based on a seekable file or string.
789 This attempts to autodetect if the stream is XML or YAML.
791 if not hasattr(source
, 'read'):
792 source
= StringIO(source
)
793 start
= source
.read(1)
796 return cls
.FromXML(source
)
797 elif start
== "!" or start
== "#":
798 return cls
.FromYAML(source
)
800 raise ParseError("unknown initial character %r" % start
)
803 return "%s(type=%r, actions=%r)" % (
804 type(self
).__name
__, self
.type, self
.actions
)
806 ActionDef
.CONSTRUCTORS
= dict(
810 changeSpeed
=ChangeSpeed
,
811 changeDirection
=ChangeDirection
,