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."""
367 def __init__(self
, actions
=(), direction
=None, speed
=None, tags
=()):
368 self
.direction
= direction
370 self
.actions
= list(actions
)
371 self
.tags
= set(tags
)
373 def __getstate__(self
):
376 state
.append(("direction", self
.direction
))
378 state
.append(("speed", self
.speed
))
380 state
.append(("actions", self
.actions
))
382 state
.append(("tags", list(self
.tags
)))
385 def __setstate__(self
, state
):
387 self
.__init
__(**state
)
390 def FromXML(cls
, doc
, element
):
391 """Construct using an ElementTree-style element."""
396 for subelem
in element
.getchildren():
397 tag
= realtag(subelem
)
398 if tag
== "direction":
399 direction
= Direction
.FromXML(doc
, subelem
)
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
))
407 tags
.add(subelem
.text
)
408 dfn
= cls(actions
, direction
, speed
, tags
)
409 doc
._bullets
[element
.get("label")] = dfn
412 def __call__(self
, params
, rank
):
413 actions
= [action(params
, rank
) for action
in self
.actions
]
415 self
.direction
and self
.direction(params
, rank
),
416 self
.speed
and self
.speed(params
, rank
),
421 return "%s(direction=%r, speed=%r, actions=%r)" % (
422 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
424 class BulletRef(object):
425 """Create a bullet by name with parameters."""
427 def __init__(self
, bullet
, params
=None):
429 self
.params
= ParamList() if params
is None else params
431 def __getstate__(self
):
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
))
439 def __setstate__(self
, state
):
441 bullet
= state
["bullet"]
442 params
= [NumberDef(param
) for param
in state
.get("params", [])]
443 self
.__init
__(bullet
, ParamList(params
))
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
)
452 def __call__(self
, params
, rank
):
453 return self
.bullet(self
.params(params
, rank
), rank
)
456 return "%s(params=%r, bullet=%r)" % (
457 type(self
).__name
__, self
.params
, self
.bullet
)
459 class ActionDef(object):
460 """Action definition.
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.
468 # This is self-referential, so it's filled in later.
469 CONSTRUCTORS
= dict()
471 def __init__(self
, actions
):
472 self
.actions
= list(actions
)
474 def __getstate__(self
):
475 return dict(actions
=self
.actions
)
477 def __setstate__(self
, state
):
479 self
.__init
__(state
["actions"])
482 def FromXML(cls
, doc
, element
):
483 """Construct using an ElementTree-style element."""
485 for subelem
in element
.getchildren():
486 tag
= realtag(subelem
)
488 ctr
= cls
.CONSTRUCTORS
[tag
]
492 actions
.append(ctr
.FromXML(doc
, subelem
))
494 doc
._actions
[element
.get("label")] = dfn
497 def __call__(self
, params
, rank
):
498 return self
.actions
, params
501 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
503 class ActionRef(object):
504 """Run an action by name with parameters."""
506 def __init__(self
, action
, params
=None):
508 self
.params
= params
or ParamList()
510 def __getstate__(self
):
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
))
518 def __setstate__(self
, state
):
520 action
= state
["action"]
521 params
= [NumberDef(param
) for param
in state
.get("params", [])]
522 self
.__init
__(action
, ParamList(params
))
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
)
531 def __call__(self
, params
, rank
):
532 return self
.action(self
.params(params
, rank
), rank
)
535 return "%s(params=%r, action=%r)" % (
536 type(self
).__name
__, self
.params
, self
.action
)
538 class Offset(object):
539 """Provide an offset to a bullet's initial position."""
541 VALID_TYPES
= ["relative", "absolute"]
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)
550 def __getstate__(self
):
551 state
= [('type', self
.type)]
553 state
.append(('x', self
.x
.expr
))
555 state
.append(('y', self
.y
.expr
))
558 def __setstate__(self
, state
):
560 self
.__init
__(state
["type"], state
.get("x"), state
.get("y"))
563 def FromXML(cls
, doc
, element
):
564 """Construct using an ElementTree-style element."""
565 type = element
.get("type", "relative")
568 for subelem
in element
:
569 tag
= realtag(subelem
)
571 x
= NumberDef(subelem
.text
)
573 y
= NumberDef(subelem
.text
)
574 return cls(type, x
, y
)
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)
580 class FireDef(object):
581 """Fire definition (creates a bullet)."""
584 self
, bullet
, direction
=None, speed
=None, offset
=None, tags
=()):
586 self
.direction
= direction
589 self
.tags
= set(tags
)
591 def __getstate__(self
):
594 state
.append(("direction", self
.direction
))
596 state
.append(("speed", self
.speed
))
598 state
.append(("offset", self
.offset
))
600 state
.append(("tags", list(self
.tags
)))
602 params
= self
.bullet
.params
603 except AttributeError:
604 state
.append(('bullet', self
.bullet
))
607 state
.append(('bullet', self
.bullet
))
609 # Strip out empty BulletRefs.
610 state
.append(('bullet', self
.bullet
.bullet
))
613 def __setstate__(self
, state
):
615 self
.__init
__(**state
)
618 def FromXML(cls
, doc
, element
):
619 """Construct using an ElementTree-style element."""
625 for subelem
in element
.getchildren():
626 tag
= realtag(subelem
)
627 if tag
== "direction":
628 direction
= Direction
.FromXML(doc
, subelem
, "aim")
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
)
638 tags
.add(subelem
.text
)
640 fire
= cls(bullet
, direction
, speed
, offset
, tags
)
641 except UnboundLocalError as exc
:
642 raise ParseError(str(exc
))
644 doc
._fires
[element
.get("label")] = fire
647 def __call__(self
, params
, rank
):
648 direction
, speed
, tags
, actions
= self
.bullet(params
, rank
)
650 direction
= self
.direction(params
, rank
)
652 speed
= self
.speed(params
, rank
)
653 tags
= tags
.union(self
.tags
)
654 return direction
, speed
, self
.offset
, tags
, actions
657 return "%s(direction=%r, speed=%r, bullet=%r)" % (
658 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
660 class FireRef(object):
661 """Fire a bullet by name with parameters."""
663 def __init__(self
, fire
, params
=None):
665 self
.params
= params
or ParamList()
667 def __getstate__(self
):
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
))
675 def __setstate__(self
, state
):
678 params
= [NumberDef(param
) for param
in state
.get("params", [])]
679 self
.__init
__(fire
, ParamList(params
))
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
)
688 def __call__(self
, params
, rank
):
689 return self
.fire(self
.params(params
, rank
), rank
)
692 return "%s(params=%r, fire=%r)" % (
693 type(self
).__name
__, self
.params
, self
.fire
)
695 class BulletML(object):
696 """BulletML document.
698 A BulletML document is a collection of top-level actions and the
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.
714 def __init__(self
, type="none", actions
=None):
715 self
.type = intern(type)
716 self
.actions
= [] if actions
is None else actions
718 def __getstate__(self
):
719 return [('type', self
.type), ('actions', self
.actions
)]
721 def __setstate__(self
, state
):
723 self
.__init
__(state
["type"], actions
=state
.get("actions"))
726 def FromXML(cls
, source
):
727 """Return a BulletML instance based on XML."""
728 if not hasattr(source
, 'read'):
729 source
= StringIO(source
)
732 root
= tree
.parse(source
)
734 doc
= cls(type=root
.get("type", "none"))
739 doc
._bullet
_refs
= []
740 doc
._action
_refs
= []
743 for element
in root
.getchildren():
744 tag
= realtag(element
)
745 if tag
in doc
.CONSTRUCTORS
:
746 doc
.CONSTRUCTORS
[tag
].FromXML(doc
, element
)
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
)
758 doc
.actions
= [act
for name
, act
in doc
._actions
.items()
759 if name
and name
.startswith("top")]
761 del(doc
._bullet
_refs
)
762 del(doc
._action
_refs
)
771 def FromYAML(cls
, source
):
772 """Create a BulletML instance based on YAML."""
774 # Late import to avoid a circular dependency.
776 import bulletml
.bulletyaml
779 raise ParseError("PyYAML is not available")
782 return yaml
.load(source
)
783 except Exception as exc
:
784 raise ParseError(str(exc
))
787 def FromDocument(cls
, source
):
788 """Create a BulletML instance based on a seekable file or string.
790 This attempts to autodetect if the stream is XML or YAML.
792 if not hasattr(source
, 'read'):
793 source
= StringIO(source
)
794 start
= source
.read(1)
797 return cls
.FromXML(source
)
798 elif start
== "!" or start
== "#":
799 return cls
.FromYAML(source
)
801 raise ParseError("unknown initial character %r" % start
)
804 return "%s(type=%r, actions=%r)" % (
805 type(self
).__name
__, self
.type, self
.actions
)
807 ActionDef
.CONSTRUCTORS
= dict(
811 changeSpeed
=ChangeSpeed
,
812 changeDirection
=ChangeDirection
,