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."""
389 for subelem
in element
.getchildren():
390 tag
= realtag(subelem
)
391 if tag
== "direction":
392 direction
= Direction
.FromXML(doc
, subelem
)
394 speed
= Speed
.FromXML(doc
, subelem
)
395 elif tag
== "action":
396 actions
.append(ActionDef
.FromXML(doc
, subelem
))
397 elif tag
== "actionRef":
398 actions
.append(ActionRef
.FromXML(doc
, subelem
))
400 self
.tags
.add(subelem
.text
)
401 dfn
= cls(actions
, direction
, speed
)
402 doc
._bullets
[element
.get("label")] = dfn
405 def __call__(self
, params
, rank
):
406 actions
= [action(params
, rank
) for action
in self
.actions
]
408 self
.direction
and self
.direction(params
, rank
),
409 self
.speed
and self
.speed(params
, rank
),
414 return "%s(direction=%r, speed=%r, actions=%r)" % (
415 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
417 class BulletRef(object):
418 """Create a bullet by name with parameters."""
420 def __init__(self
, bullet
, params
=None):
422 self
.params
= ParamList() if params
is None else params
424 def __getstate__(self
):
426 if self
.params
.params
:
427 params
= [param
.expr
for param
in self
.params
.params
]
428 state
.append(("params", params
))
429 state
.append(('bullet', self
.bullet
))
432 def __setstate__(self
, state
):
434 bullet
= state
["bullet"]
435 params
= [NumberDef(param
) for param
in state
.get("params", [])]
436 self
.__init
__(bullet
, ParamList(params
))
439 def FromXML(cls
, doc
, element
):
440 """Construct using an ElementTree-style element."""
441 bullet
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
442 doc
._bullet
_refs
.append(bullet
)
445 def __call__(self
, params
, rank
):
446 return self
.bullet(self
.params(params
, rank
), rank
)
449 return "%s(params=%r, bullet=%r)" % (
450 type(self
).__name
__, self
.params
, self
.bullet
)
452 class ActionDef(object):
453 """Action definition.
455 To support parsing new actions, add tags to
456 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
457 FromXML classmethod, which take the BulletML instance and
458 ElementTree element as arguments.
461 # This is self-referential, so it's filled in later.
462 CONSTRUCTORS
= dict()
464 def __init__(self
, actions
):
465 self
.actions
= list(actions
)
467 def __getstate__(self
):
468 return dict(actions
=self
.actions
)
470 def __setstate__(self
, state
):
472 self
.__init
__(state
["actions"])
475 def FromXML(cls
, doc
, element
):
476 """Construct using an ElementTree-style element."""
478 for subelem
in element
.getchildren():
479 tag
= realtag(subelem
)
481 ctr
= cls
.CONSTRUCTORS
[tag
]
485 actions
.append(ctr
.FromXML(doc
, subelem
))
487 doc
._actions
[element
.get("label")] = dfn
490 def __call__(self
, params
, rank
):
491 return self
.actions
, params
494 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
496 class ActionRef(object):
497 """Run an action by name with parameters."""
499 def __init__(self
, action
, params
=None):
501 self
.params
= params
or ParamList()
503 def __getstate__(self
):
505 if self
.params
.params
:
506 params
= [param
.expr
for param
in self
.params
.params
]
507 state
.append(("params", params
))
508 state
.append(('action', self
.action
))
511 def __setstate__(self
, state
):
513 action
= state
["action"]
514 params
= [NumberDef(param
) for param
in state
.get("params", [])]
515 self
.__init
__(action
, ParamList(params
))
518 def FromXML(cls
, doc
, element
):
519 """Construct using an ElementTree-style element."""
520 action
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
521 doc
._action
_refs
.append(action
)
524 def __call__(self
, params
, rank
):
525 return self
.action(self
.params(params
, rank
), rank
)
528 return "%s(params=%r, action=%r)" % (
529 type(self
).__name
__, self
.params
, self
.action
)
531 class Offset(object):
532 """Provide an offset to a bullet's initial position."""
534 VALID_TYPES
= ["relative", "absolute"]
536 def __init__(self
, type, x
, y
):
537 if type not in self
.VALID_TYPES
:
538 raise ValueError("invalid type %r" % type)
543 def __getstate__(self
):
544 state
= [('type', self
.type)]
546 state
.append(('x', self
.x
.expr
))
548 state
.append(('y', self
.y
.expr
))
551 def __setstate__(self
, state
):
553 self
.__init
__(state
["type"], state
.get("x"), state
.get("y"))
556 def FromXML(cls
, doc
, element
):
557 """Construct using an ElementTree-style element."""
558 type = element
.get("type", "relative")
561 for subelem
in element
:
562 tag
= realtag(subelem
)
564 x
= NumberDef(subelem
.text
)
566 y
= NumberDef(subelem
.text
)
567 return cls(type, x
, y
)
569 def __call__(self
, params
, rank
):
570 return (self
.x(params
, rank
) if self
.x
else 0,
571 self
.y(params
, rank
) if self
.y
else 0)
573 class FireDef(object):
574 """Fire definition (creates a bullet)."""
577 self
, bullet
, direction
=None, speed
=None, offset
=None, tags
=()):
579 self
.direction
= direction
582 self
.tags
= set(tags
)
584 def __getstate__(self
):
587 state
.append(("direction", self
.direction
))
589 state
.append(("speed", self
.speed
))
591 state
.append(("offset", self
.offset
))
593 state
.append(("tags", list(self
.tags
)))
595 params
= self
.bullet
.params
596 except AttributeError:
597 state
.append(('bullet', self
.bullet
))
600 state
.append(('bullet', self
.bullet
))
602 # Strip out empty BulletRefs.
603 state
.append(('bullet', self
.bullet
.bullet
))
606 def __setstate__(self
, state
):
608 self
.__init
__(**state
)
611 def FromXML(cls
, doc
, element
):
612 """Construct using an ElementTree-style element."""
617 for subelem
in element
.getchildren():
618 tag
= realtag(subelem
)
619 if tag
== "direction":
620 direction
= Direction
.FromXML(doc
, subelem
, "aim")
622 speed
= Speed
.FromXML(doc
, subelem
)
623 elif tag
== "bullet":
624 bullet
= BulletDef
.FromXML(doc
, subelem
)
625 elif tag
== "bulletRef":
626 bullet
= BulletRef
.FromXML(doc
, subelem
)
627 elif tag
== "offset":
628 offset
= Offset
.FromXML(doc
, subelem
)
630 self
.tags
.add(subelem
.text
)
632 fire
= cls(bullet
, direction
, speed
, offset
)
633 except UnboundLocalError as exc
:
634 raise ParseError(str(exc
))
636 doc
._fires
[element
.get("label")] = fire
639 def __call__(self
, params
, rank
):
640 direction
, speed
, tags
, actions
= self
.bullet(params
, rank
)
642 direction
= self
.direction(params
, rank
)
644 speed
= self
.speed(params
, rank
)
645 tags
= tags
.union(self
.tags
)
646 return direction
, speed
, self
.offset
, tags
, actions
649 return "%s(direction=%r, speed=%r, bullet=%r)" % (
650 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
652 class FireRef(object):
653 """Fire a bullet by name with parameters."""
655 def __init__(self
, fire
, params
=None):
657 self
.params
= params
or ParamList()
659 def __getstate__(self
):
661 if self
.params
.params
:
662 params
= [param
.expr
for param
in self
.params
.params
]
663 state
.append(("params", params
))
664 state
.append(('fire', self
.fire
))
667 def __setstate__(self
, state
):
670 params
= [NumberDef(param
) for param
in state
.get("params", [])]
671 self
.__init
__(fire
, ParamList(params
))
674 def FromXML(cls
, doc
, element
):
675 """Construct using an ElementTree-style element."""
676 fired
= cls(element
.get("label"), ParamList
.FromXML(doc
, element
))
677 doc
._fire
_refs
.append(fired
)
680 def __call__(self
, params
, rank
):
681 return self
.fire(self
.params(params
, rank
), rank
)
684 return "%s(params=%r, fire=%r)" % (
685 type(self
).__name
__, self
.params
, self
.fire
)
687 class BulletML(object):
688 """BulletML document.
690 A BulletML document is a collection of top-level actions and the
693 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
694 its parsing. It maps tag names to classes with a FromXML
695 classmethod, which take the BulletML instance and ElementTree
696 element as arguments.
706 def __init__(self
, type="none", actions
=None):
708 self
.actions
= [] if actions
is None else actions
710 def __getstate__(self
):
711 return [('type', self
.type), ('actions', self
.actions
)]
713 def __setstate__(self
, state
):
715 self
.__init
__(state
["type"], actions
=state
.get("actions"))
718 def FromXML(cls
, source
):
719 """Return a BulletML instance based on XML."""
720 if not hasattr(source
, 'read'):
721 source
= StringIO(source
)
724 root
= tree
.parse(source
)
726 doc
= cls(type=root
.get("type", "none"))
731 doc
._bullet
_refs
= []
732 doc
._action
_refs
= []
735 for element
in root
.getchildren():
736 tag
= realtag(element
)
737 if tag
in doc
.CONSTRUCTORS
:
738 doc
.CONSTRUCTORS
[tag
].FromXML(doc
, element
)
741 for ref
in doc
._bullet
_refs
:
742 ref
.bullet
= doc
._bullets
[ref
.bullet
]
743 for ref
in doc
._fire
_refs
:
744 ref
.fire
= doc
._fires
[ref
.fire
]
745 for ref
in doc
._action
_refs
:
746 ref
.action
= doc
._actions
[ref
.action
]
747 except KeyError as exc
:
748 raise ParseError("unknown reference %s" % exc
)
750 doc
.actions
= [act
for name
, act
in doc
._actions
.items()
751 if name
and name
.startswith("top")]
753 del(doc
._bullet
_refs
)
754 del(doc
._action
_refs
)
763 def FromYAML(cls
, source
):
764 """Create a BulletML instance based on YAML."""
766 # Late import to avoid a circular dependency.
768 import bulletml
.bulletyaml
771 raise ParseError("PyYAML is not available")
774 return yaml
.load(source
)
775 except Exception, exc
:
776 raise ParseError(str(exc
))
779 def FromDocument(cls
, source
):
780 """Create a BulletML instance based on a seekable file or string.
782 This attempts to autodetect if the stream is XML or YAML.
784 if not hasattr(source
, 'read'):
785 source
= StringIO(source
)
786 start
= source
.read(1)
789 return cls
.FromXML(source
)
790 elif start
== "!" or start
== "#":
791 return cls
.FromYAML(source
)
793 raise ParseError("unknown initial character %r" % start
)
796 return "%s(type=%r, actions=%r)" % (
797 type(self
).__name
__, self
.type, self
.actions
)
799 ActionDef
.CONSTRUCTORS
= dict(
803 changeSpeed
=ChangeSpeed
,
804 changeDirection
=ChangeDirection
,