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
17 from io
import StringIO
20 from cStringIO
import StringIO
22 from StringIO
import StringIO
24 from bulletml
.errors
import Error
25 from bulletml
.expr
import NumberDef
, INumberDef
28 __all__
= ["ParseError", "BulletML"]
30 class ParseError(Error
):
31 """Raised when an error occurs parsing the XML structure."""
35 """Strip namespace poop off the front of a tag."""
37 return element
.tag
.rsplit('}', 1)[1]
41 class ParamList(object):
42 """List of parameter definitions."""
44 def __init__(self
, params
=()):
45 self
.params
= list(params
)
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"])
53 def __call__(self
, params
, rank
):
54 return [param(params
, rank
) for param
in self
.params
]
57 return "%s(%r)" % (type(self
).__name
__, self
.params
)
59 class Direction(object):
60 """Raw direction value."""
62 VALID_TYPES
= ["relative", "absolute", "aim", "sequence"]
64 def __init__(self
, type, value
):
65 if type not in self
.VALID_TYPES
:
66 raise ValueError("invalid type %r" % type)
70 def __getstate__(self
):
71 return [('type', self
.type), ('value', self
.value
.expr
)]
73 def __setstate__(self
, state
):
75 self
.__init
__(state
["type"], NumberDef(state
["value"]))
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
))
82 def __call__(self
, params
, rank
):
83 return (math
.radians(self
.value(params
, rank
)), self
.type)
86 return "%s(%r, type=%r)" % (
87 type(self
).__name
__, self
.value
, self
.type)
89 class ChangeDirection(object):
90 """Direction change over time."""
92 def __init__(self
, term
, direction
):
94 self
.direction
= direction
96 def __getstate__(self
):
97 return [('frames', self
.term
.expr
),
98 ('type', self
.direction
.type),
99 ('value', self
.direction
.value
.expr
)]
101 def __setstate__(self
, state
):
103 self
.__init
__(INumberDef(state
["frames"]),
104 Direction(state
["type"], NumberDef(state
["value"])))
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
)
114 term
= INumberDef(subelem
.text
)
116 return cls(term
, direction
)
117 except UnboundLocalError as exc
:
118 raise ParseError(str(exc
))
120 def __call__(self
, params
, rank
):
121 return self
.term(params
, rank
), self
.direction(params
, rank
)
124 return "%s(term=%r, direction=%r)" % (
125 type(self
).__name
__, self
.term
, self
.direction
)
128 """Raw speed value."""
130 VALID_TYPES
= ["relative", "absolute", "sequence"]
132 def __init__(self
, type, value
):
133 if type not in self
.VALID_TYPES
:
134 raise ValueError("invalid type %r" % type)
138 def __getstate__(self
):
139 return [('type', self
.type), ('value', self
.value
.expr
)]
141 def __setstate__(self
, state
):
143 self
.__init
__(state
["type"], NumberDef(state
["value"]))
146 def FromXML(cls
, doc
, element
):
147 """Construct using an ElementTree-style element."""
148 return cls(element
.get("type", "absolute"), NumberDef(element
.text
))
150 def __call__(self
, params
, rank
):
151 return (self
.value(params
, rank
), self
.type)
154 return "%s(%r, type=%r)" % (type(self
).__name
__, self
.value
, self
.type)
156 class ChangeSpeed(object):
157 """Speed change over time."""
159 def __init__(self
, term
, speed
):
163 def __getstate__(self
):
164 return [('frames', self
.term
.expr
),
165 ('type', self
.speed
.type),
166 ('value', self
.speed
.value
.expr
)]
168 def __setstate__(self
, state
):
170 self
.__init
__(INumberDef(state
["frames"]),
171 Speed(state
["type"], NumberDef(state
["value"])))
174 def FromXML(cls
, doc
, element
):
175 """Construct using an ElementTree-style element."""
176 for subelem
in element
.getchildren():
177 tag
= realtag(subelem
)
179 speed
= Speed
.FromXML(doc
, subelem
)
181 term
= INumberDef(subelem
.text
)
183 return cls(term
, speed
)
184 except UnboundLocalError as exc
:
185 raise ParseError(str(exc
))
187 def __call__(self
, params
, rank
):
188 return self
.term(params
, rank
), self
.speed(params
, rank
)
191 return "%s(term=%r, speed=%r)" % (
192 type(self
).__name
__, self
.term
, self
.speed
)
195 """Wait for some frames."""
197 def __init__(self
, frames
):
200 def __getstate__(self
):
201 return dict(frames
=self
.frames
.expr
)
203 def __setstate__(self
, state
):
204 self
.__init
__(INumberDef(state
["frames"]))
207 def FromXML(cls
, doc
, element
):
208 """Construct using an ElementTree-style element."""
209 return cls(INumberDef(element
.text
))
211 def __call__(self
, params
, rank
):
212 return self
.frames(params
, rank
)
215 return "%s(%r)" % (type(self
).__name
__, self
.frames
)
218 """Set a bullet tag."""
220 def __init__(self
, tag
):
223 def __getstate__(self
):
224 return dict(tag
=self
.tag
)
226 def __setstate__(self
, state
):
227 self
.__init
__(state
["tag"])
230 def FromXML(cls
, doc
, element
):
231 """Construct using an ElementTree-style element."""
232 return cls(element
.text
)
235 """Unset a bullet tag."""
237 def __init__(self
, tag
):
240 def __getstate__(self
):
241 return dict(tag
=self
.tag
)
243 def __setstate__(self
, state
):
244 self
.__init
__(state
["tag"])
247 def FromXML(cls
, doc
, element
):
248 """Construct using an ElementTree-style element."""
249 return cls(element
.text
)
251 class Vanish(object):
252 """Make the owner disappear."""
258 def FromXML(cls
, doc
, element
):
259 """Construct using an ElementTree-style element."""
263 return "%s()" % (type(self
).__name
__)
265 class Repeat(object):
266 """Repeat an action definition."""
268 def __init__(self
, times
, action
):
272 def __getstate__(self
):
273 return [('times', self
.times
.expr
), ('action', self
.action
)]
275 def __setstate__(self
, state
):
277 self
.__init
__(INumberDef(state
["times"]), state
["action"])
280 def FromXML(cls
, doc
, element
):
281 """Construct using an ElementTree-style element."""
282 for subelem
in element
.getchildren():
283 tag
= realtag(subelem
)
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
)
291 return cls(times
, action
)
292 except UnboundLocalError as exc
:
293 raise ParseError(str(exc
))
295 def __call__(self
, params
, rank
):
296 return self
.times(params
, rank
), self
.action(params
, rank
)
299 return "%s(%r, %r)" % (type(self
).__name
__, self
.times
, self
.action
)
302 """Accelerate over some time."""
307 def __init__(self
, term
, horizontal
=None, vertical
=None):
309 self
.horizontal
= horizontal
310 self
.vertical
= vertical
312 def __getstate__(self
):
313 state
= [('frames', self
.term
.expr
)]
315 state
.append(('horizontal', self
.horizontal
))
317 state
.append(('vertical', self
.vertical
))
320 def __setstate__(self
, state
):
322 self
.__init
__(INumberDef(state
["frames"]), state
.get("horizontal"),
323 state
.get("vertical"))
326 def FromXML(cls
, doc
, element
):
327 """Construct using an ElementTree-style element."""
331 for subelem
in element
.getchildren():
332 tag
= realtag(subelem
)
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
)
341 return cls(term
, horizontal
, vertical
)
342 except AttributeError:
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
352 return "%s(%r, horizontal=%r, vertical=%r)" % (
353 type(self
).__name
__, self
.term
, self
.horizontal
, self
.vertical
)
355 class BulletDef(object):
356 """Bullet definition."""
361 def __init__(self
, actions
=(), direction
=None, speed
=None, tags
=()):
362 self
.direction
= direction
364 self
.actions
= list(actions
)
365 self
.tags
= set(tags
)
367 def __getstate__(self
):
370 state
.append(("direction", self
.direction
))
372 state
.append(("speed", self
.speed
))
374 state
.append(("actions", self
.actions
))
376 state
.append(("tags", list(self
.tags
)))
379 def __setstate__(self
, state
):
381 self
.__init
__(**state
)
384 def FromXML(cls
, doc
, element
):
385 """Construct using an ElementTree-style element."""
390 for subelem
in element
.getchildren():
391 tag
= realtag(subelem
)
392 if tag
== "direction":
393 direction
= Direction
.FromXML(doc
, subelem
)
395 speed
= Speed
.FromXML(doc
, subelem
)
396 elif tag
== "action":
397 actions
.append(ActionDef
.FromXML(doc
, subelem
))
398 elif tag
== "actionRef":
399 actions
.append(ActionRef
.FromXML(doc
, subelem
))
401 tags
.add(subelem
.text
)
402 dfn
= cls(actions
, direction
, speed
, tags
)
403 doc
._bullets
[element
.get("label")] = dfn
406 def __call__(self
, params
, rank
):
407 actions
= [action(params
, rank
) for action
in self
.actions
]
409 self
.direction
and self
.direction(params
, rank
),
410 self
.speed
and self
.speed(params
, rank
),
415 return "%s(direction=%r, speed=%r, actions=%r)" % (
416 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
418 class BulletRef(object):
419 """Create a bullet by name with parameters."""
421 def __init__(self
, bullet
, params
=None):
423 self
.params
= ParamList() if params
is None else params
425 def __getstate__(self
):
427 if self
.params
.params
:
428 params
= [param
.expr
for param
in self
.params
.params
]
429 state
.append(("params", params
))
430 state
.append(('bullet', self
.bullet
))
433 def __setstate__(self
, state
):
435 bullet
= state
["bullet"]
436 params
= [NumberDef(param
) for param
in state
.get("params", [])]
437 self
.__init
__(bullet
, ParamList(params
))
440 def FromXML(cls
, doc
, element
):
441 """Construct using an ElementTree-style element."""
442 bullet
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
443 doc
._bullet
_refs
.append(bullet
)
446 def __call__(self
, params
, rank
):
447 return self
.bullet(self
.params(params
, rank
), rank
)
450 return "%s(params=%r, bullet=%r)" % (
451 type(self
).__name
__, self
.params
, self
.bullet
)
453 class ActionDef(object):
454 """Action definition.
456 To support parsing new actions, add tags to
457 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
458 FromXML classmethod, which take the BulletML instance and
459 ElementTree element as arguments.
462 # This is self-referential, so it's filled in later.
463 CONSTRUCTORS
= dict()
465 def __init__(self
, actions
):
466 self
.actions
= list(actions
)
468 def __getstate__(self
):
469 return dict(actions
=self
.actions
)
471 def __setstate__(self
, state
):
473 self
.__init
__(state
["actions"])
476 def FromXML(cls
, doc
, element
):
477 """Construct using an ElementTree-style element."""
479 for subelem
in element
.getchildren():
480 tag
= realtag(subelem
)
482 ctr
= cls
.CONSTRUCTORS
[tag
]
486 actions
.append(ctr
.FromXML(doc
, subelem
))
488 doc
._actions
[element
.get("label")] = dfn
491 def __call__(self
, params
, rank
):
492 return self
.actions
, params
495 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
497 class ActionRef(object):
498 """Run an action by name with parameters."""
500 def __init__(self
, action
, params
=None):
502 self
.params
= params
or ParamList()
504 def __getstate__(self
):
506 if self
.params
.params
:
507 params
= [param
.expr
for param
in self
.params
.params
]
508 state
.append(("params", params
))
509 state
.append(('action', self
.action
))
512 def __setstate__(self
, state
):
514 action
= state
["action"]
515 params
= [NumberDef(param
) for param
in state
.get("params", [])]
516 self
.__init
__(action
, ParamList(params
))
519 def FromXML(cls
, doc
, element
):
520 """Construct using an ElementTree-style element."""
521 action
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
522 doc
._action
_refs
.append(action
)
525 def __call__(self
, params
, rank
):
526 return self
.action(self
.params(params
, rank
), rank
)
529 return "%s(params=%r, action=%r)" % (
530 type(self
).__name
__, self
.params
, self
.action
)
532 class Offset(object):
533 """Provide an offset to a bullet's initial position."""
535 VALID_TYPES
= ["relative", "absolute"]
537 def __init__(self
, type, x
, y
):
538 if type not in self
.VALID_TYPES
:
539 raise ValueError("invalid type %r" % type)
544 def __getstate__(self
):
545 state
= [('type', self
.type)]
547 state
.append(('x', self
.x
.expr
))
549 state
.append(('y', self
.y
.expr
))
552 def __setstate__(self
, state
):
554 self
.__init
__(state
["type"], state
.get("x"), state
.get("y"))
557 def FromXML(cls
, doc
, element
):
558 """Construct using an ElementTree-style element."""
559 type = element
.get("type", "relative")
562 for subelem
in element
:
563 tag
= realtag(subelem
)
565 x
= NumberDef(subelem
.text
)
567 y
= NumberDef(subelem
.text
)
568 return cls(type, x
, y
)
570 def __call__(self
, params
, rank
):
571 return (self
.x(params
, rank
) if self
.x
else 0,
572 self
.y(params
, rank
) if self
.y
else 0)
574 class FireDef(object):
575 """Fire definition (creates a bullet)."""
578 self
, bullet
, direction
=None, speed
=None, offset
=None, tags
=()):
580 self
.direction
= direction
583 self
.tags
= set(tags
)
585 def __getstate__(self
):
588 state
.append(("direction", self
.direction
))
590 state
.append(("speed", self
.speed
))
592 state
.append(("offset", self
.offset
))
594 state
.append(("tags", list(self
.tags
)))
596 params
= self
.bullet
.params
597 except AttributeError:
598 state
.append(('bullet', self
.bullet
))
601 state
.append(('bullet', self
.bullet
))
603 # Strip out empty BulletRefs.
604 state
.append(('bullet', self
.bullet
.bullet
))
607 def __setstate__(self
, state
):
609 self
.__init
__(**state
)
612 def FromXML(cls
, doc
, element
):
613 """Construct using an ElementTree-style element."""
619 for subelem
in element
.getchildren():
620 tag
= realtag(subelem
)
621 if tag
== "direction":
622 direction
= Direction
.FromXML(doc
, subelem
, "aim")
624 speed
= Speed
.FromXML(doc
, subelem
)
625 elif tag
== "bullet":
626 bullet
= BulletDef
.FromXML(doc
, subelem
)
627 elif tag
== "bulletRef":
628 bullet
= BulletRef
.FromXML(doc
, subelem
)
629 elif tag
== "offset":
630 offset
= Offset
.FromXML(doc
, subelem
)
632 tags
.add(subelem
.text
)
634 fire
= cls(bullet
, direction
, speed
, offset
, tags
)
635 except UnboundLocalError as exc
:
636 raise ParseError(str(exc
))
638 doc
._fires
[element
.get("label")] = fire
641 def __call__(self
, params
, rank
):
642 direction
, speed
, tags
, actions
= self
.bullet(params
, rank
)
644 direction
= self
.direction(params
, rank
)
646 speed
= self
.speed(params
, rank
)
647 tags
= tags
.union(self
.tags
)
648 return direction
, speed
, self
.offset
, tags
, actions
651 return "%s(direction=%r, speed=%r, bullet=%r)" % (
652 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
654 class FireRef(object):
655 """Fire a bullet by name with parameters."""
657 def __init__(self
, fire
, params
=None):
659 self
.params
= params
or ParamList()
661 def __getstate__(self
):
663 if self
.params
.params
:
664 params
= [param
.expr
for param
in self
.params
.params
]
665 state
.append(("params", params
))
666 state
.append(('fire', self
.fire
))
669 def __setstate__(self
, state
):
672 params
= [NumberDef(param
) for param
in state
.get("params", [])]
673 self
.__init
__(fire
, ParamList(params
))
676 def FromXML(cls
, doc
, element
):
677 """Construct using an ElementTree-style element."""
678 fired
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
679 doc
._fire
_refs
.append(fired
)
682 def __call__(self
, params
, rank
):
683 return self
.fire(self
.params(params
, rank
), rank
)
686 return "%s(params=%r, fire=%r)" % (
687 type(self
).__name
__, self
.params
, self
.fire
)
689 class BulletML(object):
690 """BulletML document.
692 A BulletML document is a collection of top-level actions and the
695 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
696 its parsing. It maps tag names to classes with a FromXML
697 classmethod, which take the BulletML instance and ElementTree
698 element as arguments.
708 def __init__(self
, type="none", actions
=None):
710 self
.actions
= [] if actions
is None else actions
712 def __getstate__(self
):
713 return [('type', self
.type), ('actions', self
.actions
)]
715 def __setstate__(self
, state
):
717 self
.__init
__(state
["type"], actions
=state
.get("actions"))
720 def FromXML(cls
, source
):
721 """Return a BulletML instance based on XML."""
722 if not hasattr(source
, 'read'):
723 source
= StringIO(source
)
726 root
= tree
.parse(source
)
728 doc
= cls(type=root
.get("type", "none"))
733 doc
._bullet
_refs
= []
734 doc
._action
_refs
= []
737 for element
in root
.getchildren():
738 tag
= realtag(element
)
739 if tag
in doc
.CONSTRUCTORS
:
740 doc
.CONSTRUCTORS
[tag
].FromXML(doc
, element
)
743 for ref
in doc
._bullet
_refs
:
744 ref
.bullet
= doc
._bullets
[ref
.bullet
]
745 for ref
in doc
._fire
_refs
:
746 ref
.fire
= doc
._fires
[ref
.fire
]
747 for ref
in doc
._action
_refs
:
748 ref
.action
= doc
._actions
[ref
.action
]
749 except KeyError as exc
:
750 raise ParseError("unknown reference %s" % exc
)
752 doc
.actions
= [act
for name
, act
in doc
._actions
.items()
753 if name
and name
.startswith("top")]
755 del(doc
._bullet
_refs
)
756 del(doc
._action
_refs
)
765 def FromYAML(cls
, source
):
766 """Create a BulletML instance based on YAML."""
768 # Late import to avoid a circular dependency.
770 import bulletml
.bulletyaml
773 raise ParseError("PyYAML is not available")
776 return yaml
.load(source
)
777 except Exception as exc
:
778 raise ParseError(str(exc
))
781 def FromDocument(cls
, source
):
782 """Create a BulletML instance based on a seekable file or string.
784 This attempts to autodetect if the stream is XML or YAML.
786 if not hasattr(source
, 'read'):
787 source
= StringIO(source
)
788 start
= source
.read(1)
791 return cls
.FromXML(source
)
792 elif start
== "!" or start
== "#":
793 return cls
.FromYAML(source
)
795 raise ParseError("unknown initial character %r" % start
)
798 return "%s(type=%r, actions=%r)" % (
799 type(self
).__name
__, self
.type, self
.actions
)
801 ActionDef
.CONSTRUCTORS
= dict(
805 changeSpeed
=ChangeSpeed
,
806 changeDirection
=ChangeDirection
,