"""BulletML parser. This is based on the format described at http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/bulletml_ref_e.html. Unless you are adding support for new tags, the only class you should care about in here is BulletML. """ from __future__ import division import math from xml.etree.ElementTree import ElementTree try: from cStringIO import StringIO except ImportError: from StringIO import StringIO from bulletml.errors import Error from bulletml.expr import NumberDef, INumberDef __all_ = ["ParseError", "BulletML"] class ParseError(Error): """Raised when an error occurs parsing the XML structure.""" pass def realtag(element): """Strip namespace poop off the front of a tag.""" try: return element.tag.rsplit('}', 1)[1] except ValueError: return element.tag class ParamList(object): """List of parameter definitions.""" def __init__(self, params=()): self.params = list(params) @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" return cls([NumberDef(subelem.text) for subelem in element if realtag(subelem) == "param"]) def __call__(self, params, rank): return [param(params, rank) for param in self.params] def __repr__(self): return "%s(%r)" % (type(self).__name__, self.params) class Direction(object): """Raw direction value.""" VALID_TYPES = ["relative", "absolute", "aim", "sequence"] def __init__(self, type, value): if type not in self.VALID_TYPES: raise ValueError("invalid type %r" % type) self.type = type self.value = value @classmethod def FromElement(cls, doc, element, default="absolute"): """Construct using an ElementTree-style element.""" return cls(element.get("type", default), NumberDef(element.text)) def __call__(self, params, rank): return (math.radians(self.value(params, rank)), self.type) def __repr__(self): return "%s(%r, type=%r)" % ( type(self).__name__, self.value, self.type) class ChangeDirection(object): """Direction change over time.""" def __init__(self, term, direction): self.term = term self.direction = direction @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" for subelem in element.getchildren(): tag = realtag(subelem) if tag == "direction": direction = Direction.FromElement(doc, subelem) elif tag == "term": term = INumberDef(subelem.text) try: return cls(term, direction) except UnboundLocalError as exc: raise ParseError(str(exc)) def __call__(self, params, rank): return self.term(params, rank), self.direction(params, rank) def __repr__(self): return "%s(term=%r, direction=%r)" % ( type(self).__name__, self.term, self.direction) class Speed(object): """Raw speed value.""" VALID_TYPES = ["relative", "absolute", "sequence"] def __init__(self, type, value): if type not in self.VALID_TYPES: raise ValueError("invalid type %r" % type) self.type = type self.value = value @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" return cls(element.get("type", "absolute"), NumberDef(element.text)) def __call__(self, params, rank): return (self.value(params, rank), self.type) def __repr__(self): return "%s(%r, type=%r)" % (type(self).__name__, self.value, self.type) class ChangeSpeed(object): """Speed change over time.""" def __init__(self, term, speed): self.term = term self.speed = speed @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" for subelem in element.getchildren(): tag = realtag(subelem) if tag == "speed": speed = Speed.FromElement(doc, subelem) elif tag == "term": term = INumberDef(subelem.text) try: return cls(term, speed) except UnboundLocalError as exc: raise ParseError(str(exc)) def __call__(self, params, rank): return self.term(params, rank), self.speed(params, rank) def __repr__(self): return "%s(term=%r, speed=%r)" % ( type(self).__name__, self.term, self.speed) class Wait(object): """Wait for some frames.""" def __init__(self, frames): self.frames = frames @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" return cls(INumberDef(element.text)) def __call__(self, params, rank): return self.frames(params, rank) def __repr__(self): return "%s(%r)" % (type(self).__name__, self.frames) class Vanish(object): """Make the owner disappear.""" def __init__(self): pass @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" return cls() def __repr__(self): return "%s()" % (type(self).__name__) class Repeat(object): """Repeat an action definition.""" def __init__(self, times, action): self.times = times self.action = action @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" for subelem in element.getchildren(): tag = realtag(subelem) if tag == "times": times = INumberDef(subelem.text) elif tag == "action": action = ActionDef.FromElement(doc, subelem) elif tag == "actionRef": action = ActionRef.FromElement(doc, subelem) try: return cls(times, action) except UnboundLocalError as exc: raise ParseError(str(exc)) def __call__(self, params, rank): return self.times(params, rank), self.action(params, rank) def __repr__(self): return "%s(%r, %r)" % (type(self).__name__, self.times, self.action) class Accel(object): """Accelerate over some time.""" horizontal = None vertical = None def __init__(self, term, horizontal=None, vertical=None): self.term = term self.horizontal = horizontal self.vertical = vertical @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" horizontal = None vertical = None for subelem in element.getchildren(): tag = realtag(subelem) if tag == "term": term = INumberDef(subelem.text) elif tag == "horizontal": horizontal = Speed.FromElement(doc, subelem) elif tag == "vertical": vertical = Speed.FromElement(doc, subelem) try: return cls(term, horizontal, vertical) except AttributeError: raise ParseError def __call__(self, params, rank): frames = self.term(params, rank) horizontal = self.horizontal and self.horizontal(params, rank) vertical = self.vertical and self.vertical(params, rank) return frames, horizontal, vertical def __repr__(self): return "%s(%r, horizontal=%r, vertical=%r)" % ( type(self).__name__, self.term, self.horizontal, self.vertical) class BulletDef(object): """Bullet definition.""" direction = None speed = None def __init__(self, actions=[], direction=None, speed=None): self.direction = direction self.speed = speed self.actions = list(actions) @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" actions = [] speed = None direction = None for subelem in element.getchildren(): tag = realtag(subelem) if tag == "direction": direction = Direction.FromElement(doc, subelem) elif tag == "speed": speed = Speed.FromElement(doc, subelem) elif tag == "action": actions.append(ActionDef.FromElement(doc, subelem)) elif tag == "actionRef": actions.append(ActionRef.FromElement(doc, subelem)) dfn = cls(actions, direction, speed) doc.bullets[element.get("label")] = dfn return dfn def __call__(self, params, rank): actions = [action(params, rank) for action in self.actions] return ( self.direction and self.direction(params, rank), self.speed and self.speed(params, rank), actions) def __repr__(self): return "%s(direction=%r, speed=%r, actions=%r)" % ( type(self).__name__, self.direction, self.speed, self.actions) class BulletRef(object): """Create a bullet by name with parameters.""" def __init__(self, bullet, params=None): self.bullet = bullet self.params = params or ParamList() @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" bullet = cls(element.get("label"), ParamList.FromElement(doc, element)) doc._bullet_refs.append(bullet) return bullet def __call__(self, params, rank): return self.bullet(self.params(params, rank), rank) def __repr__(self): return "%s(params=%r, bullet=%r)" % ( type(self).__name__, self.params, self.bullet) class ActionDef(object): """Action definition. To support parsing new actions, add tags to ActionDef.CONSTRUCTORS. It maps tag names to classes with a FromElement classmethod, which take the BulletML instance and ElementTree element as arguments. """ # This is self-referential, so it's filled in later. CONSTRUCTORS = dict() def __init__(self, actions): self.actions = list(actions) @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" actions = [] for subelem in element.getchildren(): tag = realtag(subelem) try: ctr = cls.CONSTRUCTORS[tag] except KeyError: continue else: actions.append(ctr.FromElement(doc, subelem)) dfn = cls(actions) doc.actions[element.get("label")] = dfn return dfn def __call__(self, params, rank): return self.actions, params def __repr__(self): return "%s(%r)" % (type(self).__name__, self.actions) class ActionRef(object): """Run an action by name with parameters.""" def __init__(self, action, params=None): self.action = action self.params = params or ParamList() @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" action = cls(element.get("label"), ParamList.FromElement(doc, element)) doc._action_refs.append(action) return action def __call__(self, params, rank): return self.action(self.params(params, rank), rank) def __repr__(self): return "%s(params=%r, action=%r)" % ( type(self).__name__, self.params, self.action) class Offset(object): """Provide an offset to a bullet's initial position.""" VALID_TYPES = ["relative", "absolute"] def __init__(self, type, x, y): if type not in self.VALID_TYPES: raise ValueError("invalid type %r" % type) self.type = type self.x = x self.y = y @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" type = element.get("type", "relative") x = None y = None for subelem in element: tag = realtag(subelem) if tag == "x": x = NumberDef(subelem.text) elif tag == "y": y = NumberDef(subelem.text) return cls(type, x, y) def __call__(self, params, rank): return (self.x(params, rank) if self.x else 0, self.y(params, rank) if self.y else 0) class FireDef(object): """Fire definition (creates a bullet).""" def __init__(self, bullet, direction=None, speed=None, offset=None): self.bullet = bullet self.direction = direction self.speed = speed self.offset = offset @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" direction = None speed = None offset = None for subelem in element.getchildren(): tag = realtag(subelem) if tag == "direction": direction = Direction.FromElement(doc, subelem, "aim") elif tag == "speed": speed = Speed.FromElement(doc, subelem) elif tag == "bullet": bullet = BulletDef.FromElement(doc, subelem) elif tag == "bulletRef": bullet = BulletRef.FromElement(doc, subelem) elif tag == "offset": offset = Offset.FromElement(doc, subelem) try: fire = cls(bullet, direction, speed, offset) except UnboundLocalError as exc: raise ParseError(str(exc)) else: doc.fires[element.get("label")] = fire return fire def __call__(self, params, rank): direction, speed, actions = self.bullet(params, rank) if self.direction: direction = self.direction(params, rank) if self.speed: speed = self.speed(params, rank) return direction, speed, actions, self.offset def __repr__(self): return "%s(direction=%r, speed=%r, bullet=%r)" % ( type(self).__name__, self.direction, self.speed, self.bullet) class FireRef(object): """Fire a bullet by name with parameters.""" def __init__(self, fire, params=None): self.fire = fire self.params = params or ParamList() @classmethod def FromElement(cls, doc, element): """Construct using an ElementTree-style element.""" fired = cls(element.get("label"), ParamList.FromElement(doc, element)) doc._fire_refs.append(fired) return fired def __call__(self, params, rank): return self.fire(self.params(params, rank), rank) def __repr__(self): return "%s(params=%r, fire=%r)" % ( type(self).__name__, self.params, self.fire) class BulletML(object): """BulletML document. A BulletML document is a collection of bullets, actions, and firings, as well as a base game type. You can add tags to the BulletML.CONSTRUCTORS dictionary to extend its parsing. It maps tag names to classes with a FromElement classmethod, which take the BulletML instance and ElementTree element as arguments. """ CONSTRUCTORS = dict( bullet=BulletDef, action=ActionDef, fire=FireDef, ) def __init__(self, type="none", bullets=None, fires=None, actions=None): self.type = type self.bullets = {} if bullets is None else bullets self.actions = {} if actions is None else actions self.fires = {} if fires is None else fires @classmethod def FromDocument(cls, source): """Return a BulletML instance based on a string or file-like.""" if isinstance(source, (str, unicode)): source = StringIO(source) tree = ElementTree() root = tree.parse(source) self = cls(type=root.get("type", "none")) self._bullet_refs = [] self._action_refs = [] self._fire_refs = [] for element in root.getchildren(): tag = realtag(element) if tag in self.CONSTRUCTORS: self.CONSTRUCTORS[tag].FromElement(self, element) try: for ref in self._bullet_refs: ref.bullet = self.bullets[ref.bullet] for ref in self._fire_refs: ref.fire = self.fires[ref.fire] for ref in self._action_refs: ref.action = self.actions[ref.action] except KeyError as exc: raise ParseError("unknown reference %s" % exc) del(self._bullet_refs) del(self._action_refs) del(self._fire_refs) self.bullets.pop(None, None) self.actions.pop(None, None) self.fires.pop(None, None) return self @property def top(self): """Get a list of all top-level actions.""" return [dfn for name, dfn in self.actions.iteritems() if name and name.startswith("top")] def __repr__(self): return "%s(type=%r, bullets=%r, actions=%r, fires=%r)" % ( type(self).__name__, self.type, self.bullets, self.actions, self.fires) ActionDef.CONSTRUCTORS = dict( repeat=Repeat, fire=FireDef, fireRef=FireRef, changeSpeed=ChangeSpeed, changeDirection=ChangeDirection, accel=Accel, wait=Wait, vanish=Vanish, action=ActionDef, actionRef=ActionRef)