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
27 __all_
= ["ParseError", "BulletML"]
29 class ParseError(Error
):
30 """Raised when an error occurs parsing the XML structure."""
34 """Strip namespace poop off the front of a tag."""
36 return element
.tag
.rsplit('}', 1)[1]
40 class ParamList(object):
41 """List of parameter definitions."""
43 def __init__(self
, params
=()):
44 self
.params
= list(params
)
47 def FromElement(cls
, doc
, element
):
48 """Construct using an ElementTree-style element."""
49 return cls([NumberDef(subelem
.text
) for subelem
in element
50 if realtag(subelem
) == "param"])
52 def __getstate__(self
):
54 return dict(params
=[param
.expr
for param
in self
.params
])
58 def __setstate__(self
, state
):
59 self
.__init
__(NumberDef(param
) for param
in state
.get("params", []))
61 def __call__(self
, params
, rank
):
62 return [param(params
, rank
) for param
in self
.params
]
65 return "%s(%r)" % (type(self
).__name
__, self
.params
)
67 class Direction(object):
68 """Raw direction value."""
70 VALID_TYPES
= ["relative", "absolute", "aim", "sequence"]
72 def __init__(self
, type, value
):
73 if type not in self
.VALID_TYPES
:
74 raise ValueError("invalid type %r" % type)
78 def __getstate__(self
):
79 return dict(type=self
.type, value
=self
.value
.expr
)
81 def __setstate__(self
, state
):
82 self
.__init
__(state
["type"], NumberDef(state
["value"]))
85 def FromElement(cls
, doc
, element
, default
="absolute"):
86 """Construct using an ElementTree-style element."""
87 return cls(element
.get("type", default
), NumberDef(element
.text
))
89 def __call__(self
, params
, rank
):
90 return (math
.radians(self
.value(params
, rank
)), self
.type)
93 return "%s(%r, type=%r)" % (
94 type(self
).__name
__, self
.value
, self
.type)
96 class ChangeDirection(object):
97 """Direction change over time."""
99 def __init__(self
, term
, direction
):
101 self
.direction
= direction
103 def __getstate__(self
):
104 return dict(frames
=self
.term
.expr
,
105 type=self
.direction
.type,
106 value
=self
.direction
.value
.expr
)
108 def __setstate__(self
, state
):
109 self
.__init
__(INumberDef(state
["frames"]),
110 Direction(state
["type"], NumberDef(state
["value"])))
113 def FromElement(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
.FromElement(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)
144 def __getstate__(self
):
145 return dict(type=self
.type, value
=self
.value
.expr
)
147 def __setstate__(self
, state
):
148 self
.__init
__(state
["type"], NumberDef(state
["value"]))
151 def FromElement(cls
, doc
, element
):
152 """Construct using an ElementTree-style element."""
153 return cls(element
.get("type", "absolute"), NumberDef(element
.text
))
155 def __call__(self
, params
, rank
):
156 return (self
.value(params
, rank
), self
.type)
159 return "%s(%r, type=%r)" % (type(self
).__name
__, self
.value
, self
.type)
161 class ChangeSpeed(object):
162 """Speed change over time."""
164 def __init__(self
, term
, speed
):
168 def __getstate__(self
):
169 return dict(frames
=self
.term
.expr
,
170 type=self
.speed
.type,
171 value
=self
.speed
.value
.expr
)
173 def __setstate__(self
, state
):
174 self
.__init
__(INumberDef(state
["frames"]),
175 Speed(state
["type"], NumberDef(state
["value"])))
178 def FromElement(cls
, doc
, element
):
179 """Construct using an ElementTree-style element."""
180 for subelem
in element
.getchildren():
181 tag
= realtag(subelem
)
183 speed
= Speed
.FromElement(doc
, subelem
)
185 term
= INumberDef(subelem
.text
)
187 return cls(term
, speed
)
188 except UnboundLocalError as exc
:
189 raise ParseError(str(exc
))
191 def __call__(self
, params
, rank
):
192 return self
.term(params
, rank
), self
.speed(params
, rank
)
195 return "%s(term=%r, speed=%r)" % (
196 type(self
).__name
__, self
.term
, self
.speed
)
199 """Wait for some frames."""
201 def __init__(self
, frames
):
204 def __getstate__(self
):
205 return dict(frames
=self
.frames
.expr
)
207 def __setstate__(self
, state
):
208 self
.__init
__(INumberDef(state
["frames"]))
211 def FromElement(cls
, doc
, element
):
212 """Construct using an ElementTree-style element."""
213 return cls(INumberDef(element
.text
))
215 def __call__(self
, params
, rank
):
216 return self
.frames(params
, rank
)
219 return "%s(%r)" % (type(self
).__name
__, self
.frames
)
222 """Set a bullet tag."""
224 def __init__(self
, tag
):
227 def __getstate__(self
):
228 return dict(tag
=self
.tag
)
230 def __setstate__(self
, state
):
231 self
.__init
__(state
["tag"])
234 def FromElement(cls
, doc
, element
):
235 """Construct using an ElementTree-style element."""
236 return cls(element
.text
)
239 """Unset a bullet tag."""
241 def __init__(self
, tag
):
244 def __getstate__(self
):
245 return dict(tag
=self
.tag
)
247 def __setstate__(self
, state
):
248 self
.__init
__(state
["tag"])
251 def FromElement(cls
, doc
, element
):
252 """Construct using an ElementTree-style element."""
253 return cls(element
.text
)
255 class Vanish(object):
256 """Make the owner disappear."""
262 def FromElement(cls
, doc
, element
):
263 """Construct using an ElementTree-style element."""
267 return "%s()" % (type(self
).__name
__)
269 class Repeat(object):
270 """Repeat an action definition."""
272 def __init__(self
, times
, action
):
276 def __getstate__(self
):
277 return dict(times
=self
.times
.expr
, action
=self
.action
)
279 def __setstate__(self
, state
):
280 self
.__init
__(INumberDef(state
["times"]), state
["action"])
283 def FromElement(cls
, doc
, element
):
284 """Construct using an ElementTree-style element."""
285 for subelem
in element
.getchildren():
286 tag
= realtag(subelem
)
288 times
= INumberDef(subelem
.text
)
289 elif tag
== "action":
290 action
= ActionDef
.FromElement(doc
, subelem
)
291 elif tag
== "actionRef":
292 action
= ActionRef
.FromElement(doc
, subelem
)
294 return cls(times
, action
)
295 except UnboundLocalError as exc
:
296 raise ParseError(str(exc
))
298 def __call__(self
, params
, rank
):
299 return self
.times(params
, rank
), self
.action(params
, rank
)
302 return "%s(%r, %r)" % (type(self
).__name
__, self
.times
, self
.action
)
305 """Accelerate over some time."""
310 def __init__(self
, term
, horizontal
=None, vertical
=None):
312 self
.horizontal
= horizontal
313 self
.vertical
= vertical
315 def __getstate__(self
):
316 state
= dict(frames
=self
.term
.expr
)
318 state
["horizontal"] = self
.horizontal
320 state
["vertical"] = self
.vertical
323 def __setstate__(self
, state
):
324 self
.__init
__(INumberDef(state
["term"]), state
.get("horizontal"),
325 state
.get("vertical"))
328 def FromElement(cls
, doc
, element
):
329 """Construct using an ElementTree-style element."""
333 for subelem
in element
.getchildren():
334 tag
= realtag(subelem
)
336 term
= INumberDef(subelem
.text
)
337 elif tag
== "horizontal":
338 horizontal
= Speed
.FromElement(doc
, subelem
)
339 elif tag
== "vertical":
340 vertical
= Speed
.FromElement(doc
, subelem
)
343 return cls(term
, horizontal
, vertical
)
344 except AttributeError:
347 def __call__(self
, params
, rank
):
348 frames
= self
.term(params
, rank
)
349 horizontal
= self
.horizontal
and self
.horizontal(params
, rank
)
350 vertical
= self
.vertical
and self
.vertical(params
, rank
)
351 return frames
, horizontal
, vertical
354 return "%s(%r, horizontal=%r, vertical=%r)" % (
355 type(self
).__name
__, self
.term
, self
.horizontal
, self
.vertical
)
357 class BulletDef(object):
358 """Bullet definition."""
363 def __init__(self
, actions
=[], direction
=None, speed
=None):
364 self
.direction
= direction
366 self
.actions
= list(actions
)
368 def __getstate__(self
):
371 state
["direction"] = self
.direction
373 state
["speed"] = self
.speed
375 state
["actions"] = self
.actions
378 def __setstate__(self
, state
):
379 self
.__init
__(**state
)
382 def FromElement(cls
, doc
, element
):
383 """Construct using an ElementTree-style element."""
387 for subelem
in element
.getchildren():
388 tag
= realtag(subelem
)
389 if tag
== "direction":
390 direction
= Direction
.FromElement(doc
, subelem
)
392 speed
= Speed
.FromElement(doc
, subelem
)
393 elif tag
== "action":
394 actions
.append(ActionDef
.FromElement(doc
, subelem
))
395 elif tag
== "actionRef":
396 actions
.append(ActionRef
.FromElement(doc
, subelem
))
397 dfn
= cls(actions
, direction
, speed
)
398 doc
.bullets
[element
.get("label")] = dfn
401 def __call__(self
, params
, rank
):
402 actions
= [action(params
, rank
) for action
in self
.actions
]
404 self
.direction
and self
.direction(params
, rank
),
405 self
.speed
and self
.speed(params
, rank
),
409 return "%s(direction=%r, speed=%r, actions=%r)" % (
410 type(self
).__name
__, self
.direction
, self
.speed
, self
.actions
)
412 class BulletRef(object):
413 """Create a bullet by name with parameters."""
415 def __init__(self
, bullet
, params
=None):
417 self
.params
= ParamList() if params
is None else params
419 def __getstate__(self
):
420 state
= dict(bullet
=self
.bullet
)
421 if self
.params
.params
:
422 state
["params"] = self
.params
.__getstate
__()
425 def __setstate__(self
, state
):
426 bullet
= state
["bullet"]
427 params
= [NumberDef(param
) for param
in state
.get("params", [])]
428 self
.__init
__(bullet
, params
)
431 def FromElement(cls
, doc
, element
):
432 """Construct using an ElementTree-style element."""
433 bullet
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
434 doc
._bullet
_refs
.append(bullet
)
437 def __call__(self
, params
, rank
):
438 return self
.bullet(self
.params(params
, rank
), rank
)
441 return "%s(params=%r, bullet=%r)" % (
442 type(self
).__name
__, self
.params
, self
.bullet
)
444 class ActionDef(object):
445 """Action definition.
447 To support parsing new actions, add tags to
448 ActionDef.CONSTRUCTORS. It maps tag names to classes with a
449 FromElement classmethod, which take the BulletML instance and
450 ElementTree element as arguments.
453 # This is self-referential, so it's filled in later.
454 CONSTRUCTORS
= dict()
456 def __init__(self
, actions
):
457 self
.actions
= list(actions
)
459 def __getstate__(self
):
460 return dict(actions
=self
.actions
)
462 def __setstate__(self
, state
):
466 def FromElement(cls
, doc
, element
):
467 """Construct using an ElementTree-style element."""
469 for subelem
in element
.getchildren():
470 tag
= realtag(subelem
)
472 ctr
= cls
.CONSTRUCTORS
[tag
]
476 actions
.append(ctr
.FromElement(doc
, subelem
))
478 doc
.actions
[element
.get("label")] = dfn
481 def __call__(self
, params
, rank
):
482 return self
.actions
, params
485 return "%s(%r)" % (type(self
).__name
__, self
.actions
)
487 class ActionRef(object):
488 """Run an action by name with parameters."""
490 def __init__(self
, action
, params
=None):
492 self
.params
= params
or ParamList()
494 def __getstate__(self
):
495 state
= dict(action
=self
.action
)
496 if self
.params
.params
:
497 state
["params"] = self
.params
.__getstate
__()
500 def __setstate__(self
, state
):
501 action
= state
["action"]
502 params
= [NumberDef(param
) for param
in state
.get("params", [])]
503 self
.__init
__(action
, params
)
506 def FromElement(cls
, doc
, element
):
507 """Construct using an ElementTree-style element."""
508 action
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
509 doc
._action
_refs
.append(action
)
512 def __call__(self
, params
, rank
):
513 return self
.action(self
.params(params
, rank
), rank
)
516 return "%s(params=%r, action=%r)" % (
517 type(self
).__name
__, self
.params
, self
.action
)
519 class Offset(object):
520 """Provide an offset to a bullet's initial position."""
522 VALID_TYPES
= ["relative", "absolute"]
524 def __init__(self
, type, x
, y
):
525 if type not in self
.VALID_TYPES
:
526 raise ValueError("invalid type %r" % type)
531 def __getstate__(self
):
532 state
= dict(type=self
.type)
534 state
["x"] = self
.x
.expr
536 state
["y"] = self
.y
.expr
539 def __setstate__(self
, state
):
540 self
.__init
__(state
["type"], state
.get("x"), state
.get("y"))
543 def FromElement(cls
, doc
, element
):
544 """Construct using an ElementTree-style element."""
545 type = element
.get("type", "relative")
548 for subelem
in element
:
549 tag
= realtag(subelem
)
551 x
= NumberDef(subelem
.text
)
553 y
= NumberDef(subelem
.text
)
554 return cls(type, x
, y
)
556 def __call__(self
, params
, rank
):
557 return (self
.x(params
, rank
) if self
.x
else 0,
558 self
.y(params
, rank
) if self
.y
else 0)
560 class FireDef(object):
561 """Fire definition (creates a bullet)."""
563 def __init__(self
, bullet
, direction
=None, speed
=None, offset
=None):
565 self
.direction
= direction
569 def __getstate__(self
):
571 params
= self
.bullet
.params
572 except AttributeError:
573 state
= dict(bullet
=self
.bullet
)
576 state
= dict(bullet
=self
.bullet
)
578 state
= dict(bullet
=self
.bullet
.bullet
)
580 state
["direction"] = self
.direction
582 state
["speed"] = self
.speed
584 state
["offset"] = self
.offset
588 def FromElement(cls
, doc
, element
):
589 """Construct using an ElementTree-style element."""
594 for subelem
in element
.getchildren():
595 tag
= realtag(subelem
)
596 if tag
== "direction":
597 direction
= Direction
.FromElement(doc
, subelem
, "aim")
599 speed
= Speed
.FromElement(doc
, subelem
)
600 elif tag
== "bullet":
601 bullet
= BulletDef
.FromElement(doc
, subelem
)
602 elif tag
== "bulletRef":
603 bullet
= BulletRef
.FromElement(doc
, subelem
)
604 elif tag
== "offset":
605 offset
= Offset
.FromElement(doc
, subelem
)
607 fire
= cls(bullet
, direction
, speed
, offset
)
608 except UnboundLocalError as exc
:
609 raise ParseError(str(exc
))
611 doc
.fires
[element
.get("label")] = fire
614 def __call__(self
, params
, rank
):
615 direction
, speed
, actions
= self
.bullet(params
, rank
)
617 direction
= self
.direction(params
, rank
)
619 speed
= self
.speed(params
, rank
)
620 return direction
, speed
, actions
, self
.offset
623 return "%s(direction=%r, speed=%r, bullet=%r)" % (
624 type(self
).__name
__, self
.direction
, self
.speed
, self
.bullet
)
626 class FireRef(object):
627 """Fire a bullet by name with parameters."""
629 def __init__(self
, fire
, params
=None):
631 self
.params
= params
or ParamList()
633 def __getstate__(self
):
634 state
= dict(fire
=self
.fire
)
635 if self
.params
.params
:
636 state
["params"] = self
.params
.__getstate
__()
639 def __setstate__(self
, state
):
641 params
= [NumberDef(param
) for param
in state
.get("params", [])]
642 self
.__init
__(fire
, params
)
645 def FromElement(cls
, doc
, element
):
646 """Construct using an ElementTree-style element."""
647 fired
= cls(element
.get("label"), ParamList
.FromElement(doc
, element
))
648 doc
._fire
_refs
.append(fired
)
651 def __call__(self
, params
, rank
):
652 return self
.fire(self
.params(params
, rank
), rank
)
655 return "%s(params=%r, fire=%r)" % (
656 type(self
).__name
__, self
.params
, self
.fire
)
658 class BulletML(object):
659 """BulletML document.
661 A BulletML document is a collection of bullets, actions, and
662 firings, as well as a base game type.
664 You can add tags to the BulletML.CONSTRUCTORS dictionary to extend
665 its parsing. It maps tag names to classes with a FromElement
666 classmethod, which take the BulletML instance and ElementTree
667 element as arguments.
677 def __init__(self
, type="none", bullets
=None, fires
=None, actions
=None):
679 self
.bullets
= {} if bullets
is None else bullets
680 self
.actions
= {} if actions
is None else actions
681 self
.fires
= {} if fires
is None else fires
683 def __getstate__(self
):
684 return dict(type=self
.type, actions
=self
.actions
)
686 def __setstate__(self
, state
):
687 self
.__init
__(state
["type"], state
.get("actions"))
690 def FromDocument(cls
, source
):
691 """Return a BulletML instance based on a string or file-like."""
692 if not hasattr(source
, 'read'):
693 source
= StringIO(source
)
696 root
= tree
.parse(source
)
698 self
= cls(type=root
.get("type", "none"))
700 self
._bullet
_refs
= []
701 self
._action
_refs
= []
704 for element
in root
.getchildren():
705 tag
= realtag(element
)
706 if tag
in self
.CONSTRUCTORS
:
707 self
.CONSTRUCTORS
[tag
].FromElement(self
, element
)
710 for ref
in self
._bullet
_refs
:
711 ref
.bullet
= self
.bullets
[ref
.bullet
]
712 for ref
in self
._fire
_refs
:
713 ref
.fire
= self
.fires
[ref
.fire
]
714 for ref
in self
._action
_refs
:
715 ref
.action
= self
.actions
[ref
.action
]
716 except KeyError as exc
:
717 raise ParseError("unknown reference %s" % exc
)
719 del(self
._bullet
_refs
)
720 del(self
._action
_refs
)
723 self
.bullets
.pop(None, None)
724 self
.actions
.pop(None, None)
725 self
.fires
.pop(None, None)
731 """Get a list of all top-level actions."""
732 return [dfn
for name
, dfn
in self
.actions
.items()
733 if name
and name
.startswith("top")]
736 return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % (
737 type(self
).__name
__, self
.type, self
.bullets
, self
.actions
,
740 ActionDef
.CONSTRUCTORS
= dict(
744 changeSpeed
=ChangeSpeed
,
745 changeDirection
=ChangeDirection
,