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):
362 self
.direction
= direction
364 self
.actions
= list(actions
)
366 def __getstate__(self
):
369 state
.append(("direction", self
.direction
))
371 state
.append(("speed", self
.speed
))
373 state
.append(("actions", self
.actions
))
376 def __setstate__(self
, state
):
378 self
.__init
__(**state
)
381 def FromXML(cls
, doc
, element
):
382 """Construct using an ElementTree-style element."""
386 for subelem
in element
.getchildren():
387 tag
= realtag(subelem
)
388 if tag
== "direction":
389 direction
= Direction
.FromXML(doc
, subelem
)
391 speed
= Speed
.FromXML(doc
, subelem
)
392 elif tag
== "action":
393 actions
.append(ActionDef
.FromXML(doc
, subelem
))
394 elif tag
== "actionRef":
395 actions
.append(ActionRef
.FromXML(doc
, subelem
))
396 dfn
= cls(actions
, direction
, speed
)
397 doc
._bullets
[element
.get("label")] = dfn
400 def __call__(self
, params
, rank
):
401 actions
= [action(params
, rank
) for action
in self
.actions
]
403 self
.direction
and self
.direction(params
, rank
),
404 self
.speed
and self
.speed(params
, rank
),
408 return "%s(direction=%r, speed=%r, actions=%r)" % (
409 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
411 class BulletRef(object):
412 """Create a bullet by name with parameters."""
414 def __init__(self
, bullet
, params
=None):
416 self
.params
= ParamList() if params
is None else params
418 def __getstate__(self
):
420 if self
.params
.params
:
421 params
= [param
.expr
for param
in self
.params
.params
]
422 state
.append(("params", params
))
423 state
.append(('bullet', self
.bullet
))
426 def __setstate__(self
, state
):
428 bullet
= state
["bullet"]
429 params
= [NumberDef(param
) for param
in state
.get("params", [])]
430 self
.__init
__(bullet
, ParamList(params
))
433 def FromXML(cls
, doc
, element
):
434 """Construct using an ElementTree-style element."""
435 bullet
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
436 doc
._bullet
_refs
.append(bullet
)
439 def __call__(self
, params
, rank
):
440 return self
.bullet(self
.params(params
, rank
), rank
)
443 return "%s(params=%r, bullet=%r)" % (
444 type(self
).__name
__, self
.params
, self
.bullet
)
446 class ActionDef(object):
447 """Action definition.
449 To support parsing new actions, add tags to
450 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
451 FromXML classmethod, which take the BulletML instance and
452 ElementTree element as arguments.
455 # This is self-referential, so it's filled in later.
456 CONSTRUCTORS
= dict()
458 def __init__(self
, actions
):
459 self
.actions
= list(actions
)
461 def __getstate__(self
):
462 return dict(actions
=self
.actions
)
464 def __setstate__(self
, state
):
466 self
.__init
__(state
["actions"])
469 def FromXML(cls
, doc
, element
):
470 """Construct using an ElementTree-style element."""
472 for subelem
in element
.getchildren():
473 tag
= realtag(subelem
)
475 ctr
= cls
.CONSTRUCTORS
[tag
]
479 actions
.append(ctr
.FromXML(doc
, subelem
))
481 doc
._actions
[element
.get("label")] = dfn
484 def __call__(self
, params
, rank
):
485 return self
.actions
, params
488 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
490 class ActionRef(object):
491 """Run an action by name with parameters."""
493 def __init__(self
, action
, params
=None):
495 self
.params
= params
or ParamList()
497 def __getstate__(self
):
499 if self
.params
.params
:
500 params
= [param
.expr
for param
in self
.params
.params
]
501 state
.append(("params", params
))
502 state
.append(('action', self
.action
))
505 def __setstate__(self
, state
):
507 action
= state
["action"]
508 params
= [NumberDef(param
) for param
in state
.get("params", [])]
509 self
.__init
__(action
, ParamList(params
))
512 def FromXML(cls
, doc
, element
):
513 """Construct using an ElementTree-style element."""
514 action
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
515 doc
._action
_refs
.append(action
)
518 def __call__(self
, params
, rank
):
519 return self
.action(self
.params(params
, rank
), rank
)
522 return "%s(params=%r, action=%r)" % (
523 type(self
).__name
__, self
.params
, self
.action
)
525 class Offset(object):
526 """Provide an offset to a bullet's initial position."""
528 VALID_TYPES
= ["relative", "absolute"]
530 def __init__(self
, type, x
, y
):
531 if type not in self
.VALID_TYPES
:
532 raise ValueError("invalid type %r" % type)
537 def __getstate__(self
):
538 state
= [('type', self
.type)]
540 state
.append(('x', self
.x
.expr
))
542 state
.append(('y', self
.y
.expr
))
545 def __setstate__(self
, state
):
547 self
.__init
__(state
["type"], state
.get("x"), state
.get("y"))
550 def FromXML(cls
, doc
, element
):
551 """Construct using an ElementTree-style element."""
552 type = element
.get("type", "relative")
555 for subelem
in element
:
556 tag
= realtag(subelem
)
558 x
= NumberDef(subelem
.text
)
560 y
= NumberDef(subelem
.text
)
561 return cls(type, x
, y
)
563 def __call__(self
, params
, rank
):
564 return (self
.x(params
, rank
) if self
.x
else 0,
565 self
.y(params
, rank
) if self
.y
else 0)
567 class FireDef(object):
568 """Fire definition (creates a bullet)."""
570 def __init__(self
, bullet
, direction
=None, speed
=None, offset
=None):
572 self
.direction
= direction
576 def __getstate__(self
):
579 state
.append(("direction", self
.direction
))
581 state
.append(("speed", self
.speed
))
583 state
.append(("offset", self
.offset
))
585 params
= self
.bullet
.params
586 except AttributeError:
587 state
.append(('bullet', self
.bullet
))
590 state
.append(('bullet', self
.bullet
))
592 # Strip out empty BulletRefs.
593 state
.append(('bullet', self
.bullet
.bullet
))
596 def __setstate__(self
, state
):
598 self
.__init
__(**state
)
601 def FromXML(cls
, doc
, element
):
602 """Construct using an ElementTree-style element."""
607 for subelem
in element
.getchildren():
608 tag
= realtag(subelem
)
609 if tag
== "direction":
610 direction
= Direction
.FromXML(doc
, subelem
, "aim")
612 speed
= Speed
.FromXML(doc
, subelem
)
613 elif tag
== "bullet":
614 bullet
= BulletDef
.FromXML(doc
, subelem
)
615 elif tag
== "bulletRef":
616 bullet
= BulletRef
.FromXML(doc
, subelem
)
617 elif tag
== "offset":
618 offset
= Offset
.FromXML(doc
, subelem
)
620 fire
= cls(bullet
, direction
, speed
, offset
)
621 except UnboundLocalError as exc
:
622 raise ParseError(str(exc
))
624 doc
._fires
[element
.get("label")] = fire
627 def __call__(self
, params
, rank
):
628 direction
, speed
, actions
= self
.bullet(params
, rank
)
630 direction
= self
.direction(params
, rank
)
632 speed
= self
.speed(params
, rank
)
633 return direction
, speed
, actions
, self
.offset
636 return "%s(direction=%r, speed=%r, bullet=%r)" % (
637 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
639 class FireRef(object):
640 """Fire a bullet by name with parameters."""
642 def __init__(self
, fire
, params
=None):
644 self
.params
= params
or ParamList()
646 def __getstate__(self
):
648 if self
.params
.params
:
649 params
= [param
.expr
for param
in self
.params
.params
]
650 state
.append(("params", params
))
651 state
.append(('fire', self
.fire
))
654 def __setstate__(self
, state
):
657 params
= [NumberDef(param
) for param
in state
.get("params", [])]
658 self
.__init
__(fire
, ParamList(params
))
661 def FromXML(cls
, doc
, element
):
662 """Construct using an ElementTree-style element."""
663 fired
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
664 doc
._fire
_refs
.append(fired
)
667 def __call__(self
, params
, rank
):
668 return self
.fire(self
.params(params
, rank
), rank
)
671 return "%s(params=%r, fire=%r)" % (
672 type(self
).__name
__, self
.params
, self
.fire
)
674 class BulletML(object):
675 """BulletML document.
677 A BulletML document is a collection of top-level actions and the
680 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
681 its parsing. It maps tag names to classes with a FromXML
682 classmethod, which take the BulletML instance and ElementTree
683 element as arguments.
693 def __init__(self
, type="none", actions
=None):
695 self
.actions
= [] if actions
is None else actions
697 def __getstate__(self
):
698 return [('type', self
.type), ('actions', self
.actions
)]
700 def __setstate__(self
, state
):
702 self
.__init
__(state
["type"], actions
=state
.get("actions"))
705 def FromXML(cls
, source
):
706 """Return a BulletML instance based on XML."""
707 if not hasattr(source
, 'read'):
708 source
= StringIO(source
)
711 root
= tree
.parse(source
)
713 self
= cls(type=root
.get("type", "none"))
718 self
._bullet
_refs
= []
719 self
._action
_refs
= []
722 for element
in root
.getchildren():
723 tag
= realtag(element
)
724 if tag
in self
.CONSTRUCTORS
:
725 self
.CONSTRUCTORS
[tag
].FromXML(self
, element
)
728 for ref
in self
._bullet
_refs
:
729 ref
.bullet
= self
._bullets
[ref
.bullet
]
730 for ref
in self
._fire
_refs
:
731 ref
.fire
= self
._fires
[ref
.fire
]
732 for ref
in self
._action
_refs
:
733 ref
.action
= self
._actions
[ref
.action
]
734 except KeyError as exc
:
735 raise ParseError("unknown reference %s" % exc
)
737 self
.actions
= [act
for name
, act
in self
._actions
.items()
738 if name
and name
.startswith("top")]
740 del(self
._bullet
_refs
)
741 del(self
._action
_refs
)
750 def FromYAML(cls
, source
):
751 """Create a BulletML instance based on YAML."""
753 # Late import to avoid a circular dependency.
755 import bulletml
.bulletyaml
758 raise ParseError("PyYAML is not available")
761 return yaml
.load(source
)
762 except Exception, exc
:
763 raise ParseError(str(exc
))
766 def FromDocument(cls
, source
):
767 """Create a BulletML instance based on a seekable file or string.
769 This attempts to autodetect if the stream is XML or YAML.
771 if not hasattr(source
, 'read'):
772 source
= StringIO(source
)
773 start
= source
.read(1)
776 return cls
.FromXML(source
)
777 elif start
== "!" or start
== "#":
778 return cls
.FromYAML(source
)
780 raise ParseError("unknown initial character %r" % start
)
783 return "%s(type=%r, actions=%r)" % (
784 type(self
).__name
__, self
.type, self
.actions
)
786 ActionDef
.CONSTRUCTORS
= dict(
790 changeSpeed
=ChangeSpeed
,
791 changeDirection
=ChangeDirection
,