10b789ae83a801bc0dc538afc183f8194240f00c
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 FromElement(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 dict(type=self
.type, value
=self
.value
.expr
)
73 def __setstate__(self
, state
):
74 self
.__init
__(state
["type"], NumberDef(state
["value"]))
77 def FromElement(cls
, doc
, element
, default
="absolute"):
78 """Construct using an ElementTree-style element."""
79 return cls(element
.get("type", default
), NumberDef(element
.text
))
81 def __call__(self
, params
, rank
):
82 return (math
.radians(self
.value(params
, rank
)), self
.type)
85 return "%s(%r, type=%r)" % (
86 type(self
).__name
__, self
.value
, self
.type)
88 class ChangeDirection(object):
89 """Direction change over time."""
91 def __init__(self
, term
, direction
):
93 self
.direction
= direction
95 def __getstate__(self
):
96 return dict(frames
=self
.term
.expr
,
97 type=self
.direction
.type,
98 value
=self
.direction
.value
.expr
)
100 def __setstate__(self
, state
):
101 self
.__init
__(INumberDef(state
["frames"]),
102 Direction(state
["type"], NumberDef(state
["value"])))
105 def FromElement(cls
, doc
, element
):
106 """Construct using an ElementTree-style element."""
107 for subelem
in element
.getchildren():
108 tag
= realtag(subelem
)
109 if tag
== "direction":
110 direction
= Direction
.FromElement(doc
, subelem
)
112 term
= INumberDef(subelem
.text
)
114 return cls(term
, direction
)
115 except UnboundLocalError as exc
:
116 raise ParseError(str(exc
))
118 def __call__(self
, params
, rank
):
119 return self
.term(params
, rank
), self
.direction(params
, rank
)
122 return "%s(term=%r, direction=%r)" % (
123 type(self
).__name
__, self
.term
, self
.direction
)
126 """Raw speed value."""
128 VALID_TYPES
= ["relative", "absolute", "sequence"]
130 def __init__(self
, type, value
):
131 if type not in self
.VALID_TYPES
:
132 raise ValueError("invalid type %r" % type)
136 def __getstate__(self
):
137 return dict(type=self
.type, value
=self
.value
.expr
)
139 def __setstate__(self
, state
):
140 self
.__init
__(state
["type"], NumberDef(state
["value"]))
143 def FromElement(cls
, doc
, element
):
144 """Construct using an ElementTree-style element."""
145 return cls(element
.get("type", "absolute"), NumberDef(element
.text
))
147 def __call__(self
, params
, rank
):
148 return (self
.value(params
, rank
), self
.type)
151 return "%s(%r, type=%r)" % (type(self
).__name
__, self
.value
, self
.type)
153 class ChangeSpeed(object):
154 """Speed change over time."""
156 def __init__(self
, term
, speed
):
160 def __getstate__(self
):
161 return dict(frames
=self
.term
.expr
,
162 type=self
.speed
.type,
163 value
=self
.speed
.value
.expr
)
165 def __setstate__(self
, state
):
166 self
.__init
__(INumberDef(state
["frames"]),
167 Speed(state
["type"], NumberDef(state
["value"])))
170 def FromElement(cls
, doc
, element
):
171 """Construct using an ElementTree-style element."""
172 for subelem
in element
.getchildren():
173 tag
= realtag(subelem
)
175 speed
= Speed
.FromElement(doc
, subelem
)
177 term
= INumberDef(subelem
.text
)
179 return cls(term
, speed
)
180 except UnboundLocalError as exc
:
181 raise ParseError(str(exc
))
183 def __call__(self
, params
, rank
):
184 return self
.term(params
, rank
), self
.speed(params
, rank
)
187 return "%s(term=%r, speed=%r)" % (
188 type(self
).__name
__, self
.term
, self
.speed
)
191 """Wait for some frames."""
193 def __init__(self
, frames
):
196 def __getstate__(self
):
197 return dict(frames
=self
.frames
.expr
)
199 def __setstate__(self
, state
):
200 self
.__init
__(INumberDef(state
["frames"]))
203 def FromElement(cls
, doc
, element
):
204 """Construct using an ElementTree-style element."""
205 return cls(INumberDef(element
.text
))
207 def __call__(self
, params
, rank
):
208 return self
.frames(params
, rank
)
211 return "%s(%r)" % (type(self
).__name
__, self
.frames
)
214 """Set a bullet tag."""
216 def __init__(self
, tag
):
219 def __getstate__(self
):
220 return dict(tag
=self
.tag
)
222 def __setstate__(self
, state
):
223 self
.__init
__(state
["tag"])
226 def FromElement(cls
, doc
, element
):
227 """Construct using an ElementTree-style element."""
228 return cls(element
.text
)
231 """Unset a bullet tag."""
233 def __init__(self
, tag
):
236 def __getstate__(self
):
237 return dict(tag
=self
.tag
)
239 def __setstate__(self
, state
):
240 self
.__init
__(state
["tag"])
243 def FromElement(cls
, doc
, element
):
244 """Construct using an ElementTree-style element."""
245 return cls(element
.text
)
247 class Vanish(object):
248 """Make the owner disappear."""
254 def FromElement(cls
, doc
, element
):
255 """Construct using an ElementTree-style element."""
259 return "%s()" % (type(self
).__name
__)
261 class Repeat(object):
262 """Repeat an action definition."""
264 def __init__(self
, times
, action
):
268 def __getstate__(self
):
269 return dict(times
=self
.times
.expr
, action
=self
.action
)
271 def __setstate__(self
, state
):
272 self
.__init
__(INumberDef(state
["times"]), state
["action"])
275 def FromElement(cls
, doc
, element
):
276 """Construct using an ElementTree-style element."""
277 for subelem
in element
.getchildren():
278 tag
= realtag(subelem
)
280 times
= INumberDef(subelem
.text
)
281 elif tag
== "action":
282 action
= ActionDef
.FromElement(doc
, subelem
)
283 elif tag
== "actionRef":
284 action
= ActionRef
.FromElement(doc
, subelem
)
286 return cls(times
, action
)
287 except UnboundLocalError as exc
:
288 raise ParseError(str(exc
))
290 def __call__(self
, params
, rank
):
291 return self
.times(params
, rank
), self
.action(params
, rank
)
294 return "%s(%r, %r)" % (type(self
).__name
__, self
.times
, self
.action
)
297 """Accelerate over some time."""
302 def __init__(self
, term
, horizontal
=None, vertical
=None):
304 self
.horizontal
= horizontal
305 self
.vertical
= vertical
307 def __getstate__(self
):
308 state
= dict(frames
=self
.term
.expr
)
310 state
["horizontal"] = self
.horizontal
312 state
["vertical"] = self
.vertical
315 def __setstate__(self
, state
):
316 self
.__init
__(INumberDef(state
["frames"]), state
.get("horizontal"),
317 state
.get("vertical"))
320 def FromElement(cls
, doc
, element
):
321 """Construct using an ElementTree-style element."""
325 for subelem
in element
.getchildren():
326 tag
= realtag(subelem
)
328 term
= INumberDef(subelem
.text
)
329 elif tag
== "horizontal":
330 horizontal
= Speed
.FromElement(doc
, subelem
)
331 elif tag
== "vertical":
332 vertical
= Speed
.FromElement(doc
, subelem
)
335 return cls(term
, horizontal
, vertical
)
336 except AttributeError:
339 def __call__(self
, params
, rank
):
340 frames
= self
.term(params
, rank
)
341 horizontal
= self
.horizontal
and self
.horizontal(params
, rank
)
342 vertical
= self
.vertical
and self
.vertical(params
, rank
)
343 return frames
, horizontal
, vertical
346 return "%s(%r, horizontal=%r, vertical=%r)" % (
347 type(self
).__name
__, self
.term
, self
.horizontal
, self
.vertical
)
349 class BulletDef(object):
350 """Bullet definition."""
355 def __init__(self
, actions
=[], direction
=None, speed
=None):
356 self
.direction
= direction
358 self
.actions
= list(actions
)
360 def __getstate__(self
):
363 state
["direction"] = self
.direction
365 state
["speed"] = self
.speed
367 state
["actions"] = self
.actions
370 def __setstate__(self
, state
):
371 self
.__init
__(**state
)
374 def FromElement(cls
, doc
, element
):
375 """Construct using an ElementTree-style element."""
379 for subelem
in element
.getchildren():
380 tag
= realtag(subelem
)
381 if tag
== "direction":
382 direction
= Direction
.FromElement(doc
, subelem
)
384 speed
= Speed
.FromElement(doc
, subelem
)
385 elif tag
== "action":
386 actions
.append(ActionDef
.FromElement(doc
, subelem
))
387 elif tag
== "actionRef":
388 actions
.append(ActionRef
.FromElement(doc
, subelem
))
389 dfn
= cls(actions
, direction
, speed
)
390 doc
.bullets
[element
.get("label")] = dfn
393 def __call__(self
, params
, rank
):
394 actions
= [action(params
, rank
) for action
in self
.actions
]
396 self
.direction
and self
.direction(params
, rank
),
397 self
.speed
and self
.speed(params
, rank
),
401 return "%s(direction=%r, speed=%r, actions=%r)" % (
402 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
404 class BulletRef(object):
405 """Create a bullet by name with parameters."""
407 def __init__(self
, bullet
, params
=None):
409 self
.params
= ParamList() if params
is None else params
411 def __getstate__(self
):
412 state
= dict(bullet
=self
.bullet
)
413 if self
.params
.params
:
414 state
["params"] = [param
.expr
for param
in self
.params
.params
]
417 def __setstate__(self
, state
):
418 bullet
= state
["bullet"]
419 params
= [NumberDef(param
) for param
in state
.get("params", [])]
420 self
.__init
__(bullet
, ParamList(params
))
423 def FromElement(cls
, doc
, element
):
424 """Construct using an ElementTree-style element."""
425 bullet
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
426 doc
._bullet
_refs
.append(bullet
)
429 def __call__(self
, params
, rank
):
430 return self
.bullet(self
.params(params
, rank
), rank
)
433 return "%s(params=%r, bullet=%r)" % (
434 type(self
).__name
__, self
.params
, self
.bullet
)
436 class ActionDef(object):
437 """Action definition.
439 To support parsing new actions, add tags to
440 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
441 FromElement classmethod, which take the BulletML instance and
442 ElementTree element as arguments.
445 # This is self-referential, so it's filled in later.
446 CONSTRUCTORS
= dict()
448 def __init__(self
, actions
):
449 self
.actions
= list(actions
)
451 def __getstate__(self
):
452 return dict(actions
=self
.actions
)
454 def __setstate__(self
, state
):
455 self
.__init
__(state
["actions"])
458 def FromElement(cls
, doc
, element
):
459 """Construct using an ElementTree-style element."""
461 for subelem
in element
.getchildren():
462 tag
= realtag(subelem
)
464 ctr
= cls
.CONSTRUCTORS
[tag
]
468 actions
.append(ctr
.FromElement(doc
, subelem
))
470 doc
.actions
[element
.get("label")] = dfn
473 def __call__(self
, params
, rank
):
474 return self
.actions
, params
477 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
479 class ActionRef(object):
480 """Run an action by name with parameters."""
482 def __init__(self
, action
, params
=None):
484 self
.params
= params
or ParamList()
486 def __getstate__(self
):
487 state
= dict(action
=self
.action
)
488 if self
.params
.params
:
489 state
["params"] = [param
.expr
for param
in self
.params
.params
]
492 def __setstate__(self
, state
):
493 action
= state
["action"]
494 params
= [NumberDef(param
) for param
in state
.get("params", [])]
495 self
.__init
__(action
, ParamList(params
))
498 def FromElement(cls
, doc
, element
):
499 """Construct using an ElementTree-style element."""
500 action
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
501 doc
._action
_refs
.append(action
)
504 def __call__(self
, params
, rank
):
505 return self
.action(self
.params(params
, rank
), rank
)
508 return "%s(params=%r, action=%r)" % (
509 type(self
).__name
__, self
.params
, self
.action
)
511 class Offset(object):
512 """Provide an offset to a bullet's initial position."""
514 VALID_TYPES
= ["relative", "absolute"]
516 def __init__(self
, type, x
, y
):
517 if type not in self
.VALID_TYPES
:
518 raise ValueError("invalid type %r" % type)
523 def __getstate__(self
):
524 state
= dict(type=self
.type)
526 state
["x"] = self
.x
.expr
528 state
["y"] = self
.y
.expr
531 def __setstate__(self
, state
):
532 self
.__init
__(state
["type"], state
.get("x"), state
.get("y"))
535 def FromElement(cls
, doc
, element
):
536 """Construct using an ElementTree-style element."""
537 type = element
.get("type", "relative")
540 for subelem
in element
:
541 tag
= realtag(subelem
)
543 x
= NumberDef(subelem
.text
)
545 y
= NumberDef(subelem
.text
)
546 return cls(type, x
, y
)
548 def __call__(self
, params
, rank
):
549 return (self
.x(params
, rank
) if self
.x
else 0,
550 self
.y(params
, rank
) if self
.y
else 0)
552 class FireDef(object):
553 """Fire definition (creates a bullet)."""
555 def __init__(self
, bullet
, direction
=None, speed
=None, offset
=None):
557 self
.direction
= direction
561 def __getstate__(self
):
563 params
= self
.bullet
.params
564 except AttributeError:
565 state
= dict(bullet
=self
.bullet
)
568 state
= dict(bullet
=self
.bullet
)
570 state
= dict(bullet
=self
.bullet
.bullet
)
572 state
["direction"] = self
.direction
574 state
["speed"] = self
.speed
576 state
["offset"] = self
.offset
579 def __setstate__(self
, state
):
580 self
.__init
__(**state
)
583 def FromElement(cls
, doc
, element
):
584 """Construct using an ElementTree-style element."""
589 for subelem
in element
.getchildren():
590 tag
= realtag(subelem
)
591 if tag
== "direction":
592 direction
= Direction
.FromElement(doc
, subelem
, "aim")
594 speed
= Speed
.FromElement(doc
, subelem
)
595 elif tag
== "bullet":
596 bullet
= BulletDef
.FromElement(doc
, subelem
)
597 elif tag
== "bulletRef":
598 bullet
= BulletRef
.FromElement(doc
, subelem
)
599 elif tag
== "offset":
600 offset
= Offset
.FromElement(doc
, subelem
)
602 fire
= cls(bullet
, direction
, speed
, offset
)
603 except UnboundLocalError as exc
:
604 raise ParseError(str(exc
))
606 doc
.fires
[element
.get("label")] = fire
609 def __call__(self
, params
, rank
):
610 direction
, speed
, actions
= self
.bullet(params
, rank
)
612 direction
= self
.direction(params
, rank
)
614 speed
= self
.speed(params
, rank
)
615 return direction
, speed
, actions
, self
.offset
618 return "%s(direction=%r, speed=%r, bullet=%r)" % (
619 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
621 class FireRef(object):
622 """Fire a bullet by name with parameters."""
624 def __init__(self
, fire
, params
=None):
626 self
.params
= params
or ParamList()
628 def __getstate__(self
):
629 state
= dict(fire
=self
.fire
)
630 if self
.params
.params
:
631 state
["params"] = [param
.expr
for param
in self
.params
.params
]
634 def __setstate__(self
, state
):
636 params
= [NumberDef(param
) for param
in state
.get("params", [])]
637 self
.__init
__(fire
, ParamList(params
))
640 def FromElement(cls
, doc
, element
):
641 """Construct using an ElementTree-style element."""
642 fired
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
643 doc
._fire
_refs
.append(fired
)
646 def __call__(self
, params
, rank
):
647 return self
.fire(self
.params(params
, rank
), rank
)
650 return "%s(params=%r, fire=%r)" % (
651 type(self
).__name
__, self
.params
, self
.fire
)
653 class BulletML(object):
654 """BulletML document.
656 A BulletML document is a collection of bullets, actions, and
657 firings, as well as a base game type.
659 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
660 its parsing. It maps tag names to classes with a FromElement
661 classmethod, which take the BulletML instance and ElementTree
662 element as arguments.
672 def __init__(self
, type="none", bullets
=None, fires
=None, actions
=None):
674 self
.bullets
= {} if bullets
is None else bullets
675 self
.actions
= {} if actions
is None else actions
676 self
.fires
= {} if fires
is None else fires
678 def __getstate__(self
):
679 return dict(type=self
.type, actions
=self
.actions
)
681 def __setstate__(self
, state
):
682 self
.__init
__(state
["type"], actions
=state
.get("actions"))
685 def FromXML(cls
, source
):
686 """Return a BulletML instance based on XML."""
687 if not hasattr(source
, 'read'):
688 source
= StringIO(source
)
691 root
= tree
.parse(source
)
693 self
= cls(type=root
.get("type", "none"))
695 self
._bullet
_refs
= []
696 self
._action
_refs
= []
699 for element
in root
.getchildren():
700 tag
= realtag(element
)
701 if tag
in self
.CONSTRUCTORS
:
702 self
.CONSTRUCTORS
[tag
].FromElement(self
, element
)
705 for ref
in self
._bullet
_refs
:
706 ref
.bullet
= self
.bullets
[ref
.bullet
]
707 for ref
in self
._fire
_refs
:
708 ref
.fire
= self
.fires
[ref
.fire
]
709 for ref
in self
._action
_refs
:
710 ref
.action
= self
.actions
[ref
.action
]
711 except KeyError as exc
:
712 raise ParseError("unknown reference %s" % exc
)
714 del(self
._bullet
_refs
)
715 del(self
._action
_refs
)
718 self
.bullets
.pop(None, None)
719 self
.actions
.pop(None, None)
720 self
.fires
.pop(None, None)
725 def FromYAML(cls
, source
):
726 """Create a BulletML instance based on YAML."""
728 # Late import to avoid a circular dependency.
730 import bulletml
.bulletyaml
733 raise ParseError("PyYAML is not available")
736 return yaml
.load(source
)
737 except Exception, exc
:
738 raise ParseError(str(exc
))
741 def FromDocument(cls
, source
):
742 """Create a BulletML instance based on a seekable file or string.
744 This attempts to autodetect if the stream is XML or YAML.
746 if not hasattr(source
, 'read'):
747 source
= StringIO(source
)
748 start
= source
.read(1)
751 return cls
.FromXML(source
)
753 return cls
.FromYAML(source
)
755 raise ParseError("unknown initial character %r" % start
)
759 """Get a list of all top-level actions."""
760 return [dfn
for name
, dfn
in self
.actions
.items()
761 if name
and name
.startswith("top")]
764 return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
765 type(self
).__name
__, self
.type, self
.bullets
, self
.actions
,
768 ActionDef
.CONSTRUCTORS
= dict(
772 changeSpeed
=ChangeSpeed
,
773 changeDirection
=ChangeDirection
,