"""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 io import StringIO except ImportError: 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 FromXML(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 def __getstate__(self): return [('type', self.type), ('value', self.value.expr)] def __setstate__(self, state): state = dict(state) self.__init__(state["type"], NumberDef(state["value"])) @classmethod def FromXML(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 def __getstate__(self): return [('frames', self.term.expr), ('type', self.direction.type), ('value', self.direction.value.expr)] def __setstate__(self, state): state = dict(state) self.__init__(INumberDef(state["frames"]), Direction(state["type"], NumberDef(state["value"]))) @classmethod def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" for subelem in element.getchildren(): tag = realtag(subelem) if tag == "direction": direction = Direction.FromXML(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 def __getstate__(self): return [('type', self.type), ('value', self.value.expr)] def __setstate__(self, state): state = dict(state) self.__init__(state["type"], NumberDef(state["value"])) @classmethod def FromXML(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 def __getstate__(self): return [('frames', self.term.expr), ('type', self.speed.type), ('value', self.speed.value.expr)] def __setstate__(self, state): state = dict(state) self.__init__(INumberDef(state["frames"]), Speed(state["type"], NumberDef(state["value"]))) @classmethod def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" for subelem in element.getchildren(): tag = realtag(subelem) if tag == "speed": speed = Speed.FromXML(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 def __getstate__(self): return dict(frames=self.frames.expr) def __setstate__(self, state): self.__init__(INumberDef(state["frames"])) @classmethod def FromXML(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 Tag(object): """Set a bullet tag.""" def __init__(self, tag): self.tag = tag def __getstate__(self): return dict(tag=self.tag) def __setstate__(self, state): self.__init__(state["tag"]) @classmethod def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" return cls(element.text) class Untag(object): """Unset a bullet tag.""" def __init__(self, tag): self.tag = tag def __getstate__(self): return dict(tag=self.tag) def __setstate__(self, state): self.__init__(state["tag"]) @classmethod def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" return cls(element.text) class Vanish(object): """Make the owner disappear.""" def __init__(self): pass @classmethod def FromXML(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 def __getstate__(self): return [('times', self.times.expr), ('action', self.action)] def __setstate__(self, state): state = dict(state) self.__init__(INumberDef(state["times"]), state["action"]) @classmethod def FromXML(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.FromXML(doc, subelem) elif tag == "actionRef": action = ActionRef.FromXML(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 def __getstate__(self): state = [('frames', self.term.expr)] if self.horizontal: state.append(('horizontal', self.horizontal)) if self.vertical: state.append(('vertical', self.vertical)) return state def __setstate__(self, state): state = dict(state) self.__init__(INumberDef(state["frames"]), state.get("horizontal"), state.get("vertical")) @classmethod def FromXML(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.FromXML(doc, subelem) elif tag == "vertical": vertical = Speed.FromXML(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) def __getstate__(self): state = [] if self.direction: state.append(("direction", self.direction)) if self.speed: state.append(("speed", self.speed)) if self.actions: state.append(("actions", self.actions)) return state def __setstate__(self, state): state = dict(state) self.__init__(**state) @classmethod def FromXML(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.FromXML(doc, subelem) elif tag == "speed": speed = Speed.FromXML(doc, subelem) elif tag == "action": actions.append(ActionDef.FromXML(doc, subelem)) elif tag == "actionRef": actions.append(ActionRef.FromXML(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 = ParamList() if params is None else params def __getstate__(self): state = [] if self.params.params: params = [param.expr for param in self.params.params] state.append(("params", params)) state.append(('bullet', self.bullet)) return state def __setstate__(self, state): state = dict(state) bullet = state["bullet"] params = [NumberDef(param) for param in state.get("params", [])] self.__init__(bullet, ParamList(params)) @classmethod def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" bullet = cls(element.get("label"), ParamList.FromXML(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 FromXML 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) def __getstate__(self): return dict(actions=self.actions) def __setstate__(self, state): state = dict(state) self.__init__(state["actions"]) @classmethod def FromXML(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.FromXML(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() def __getstate__(self): state = [] if self.params.params: params = [param.expr for param in self.params.params] state.append(("params", params)) state.append(('action', self.action)) return state def __setstate__(self, state): state = dict(state) action = state["action"] params = [NumberDef(param) for param in state.get("params", [])] self.__init__(action, ParamList(params)) @classmethod def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" action = cls(element.get("label"), ParamList.FromXML(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 def __getstate__(self): state = [('type', self.type)] if self.x: state.append(('x', self.x.expr)) if self.y: state.append(('y', self.y.expr)) return state def __setstate__(self, state): state = dict(state) self.__init__(state["type"], state.get("x"), state.get("y")) @classmethod def FromXML(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 def __getstate__(self): state = [] if self.direction: state.append(("direction", self.direction)) if self.speed: state.append(("speed", self.speed)) if self.offset: state.append(("offset", self.offset)) try: params = self.bullet.params except AttributeError: state.append(('bullet', self.bullet)) else: if params.params: state.append(('bullet', self.bullet)) else: # Strip out empty BulletRefs. state.append(('bullet', self.bullet.bullet)) return state def __setstate__(self, state): state = dict(state) self.__init__(**state) @classmethod def FromXML(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.FromXML(doc, subelem, "aim") elif tag == "speed": speed = Speed.FromXML(doc, subelem) elif tag == "bullet": bullet = BulletDef.FromXML(doc, subelem) elif tag == "bulletRef": bullet = BulletRef.FromXML(doc, subelem) elif tag == "offset": offset = Offset.FromXML(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() def __getstate__(self): state = [] if self.params.params: params = [param.expr for param in self.params.params] state.append(("params", params)) state.append(('fire', self.fire)) return state def __setstate__(self, state): state = dict(state) fire = state["fire"] params = [NumberDef(param) for param in state.get("params", [])] self.__init__(fire, ParamList(params)) @classmethod def FromXML(cls, doc, element): """Construct using an ElementTree-style element.""" fired = cls(element.get("label"), ParamList.FromXML(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 top-level actions and the base game type. You can add tags to the BulletML.CONSTRUCTORS dictionary to extend its parsing. It maps tag names to classes with a FromXML classmethod, which take the BulletML instance and ElementTree element as arguments. """ CONSTRUCTORS = dict( bullet=BulletDef, action=ActionDef, fire=FireDef, ) def __init__(self, type="none", actions=None): self.type = type self.actions = [] if actions is None else actions def __getstate__(self): return [('type', self.type), ('actions', self.actions)] def __setstate__(self, state): state = dict(state) self.__init__(state["type"], actions=state.get("actions")) @classmethod def FromXML(cls, source): """Return a BulletML instance based on XML.""" if not hasattr(source, 'read'): source = StringIO(source) tree = ElementTree() root = tree.parse(source) self = cls(type=root.get("type", "none")) self._bullets = {} self._actions = {} self._fires = {} 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].FromXML(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) self.actions = [act for name, act in self._actions.items() if name and name.startswith("top")] del(self._bullet_refs) del(self._action_refs) del(self._fire_refs) del(self._bullets) del(self._actions) del(self._fires) return self @classmethod def FromYAML(cls, source): """Create a BulletML instance based on YAML.""" # Late import to avoid a circular dependency. try: import bulletml.bulletyaml import yaml except ImportError: raise ParseError("PyYAML is not available") else: try: return yaml.load(source) except Exception, exc: raise ParseError(str(exc)) @classmethod def FromDocument(cls, source): """Create a BulletML instance based on a seekable file or string. This attempts to autodetect if the stream is XML or YAML. """ if not hasattr(source, 'read'): source = StringIO(source) start = source.read(1) source.seek(0) if start == "<": return cls.FromXML(source) elif start == "!" or start == "#": return cls.FromYAML(source) else: raise ParseError("unknown initial character %r" % start) def __repr__(self): return "%s(type=%r, actions=%r)" % ( type(self).__name__, self.type, self.actions) ActionDef.CONSTRUCTORS = dict( repeat=Repeat, fire=FireDef, fireRef=FireRef, changeSpeed=ChangeSpeed, changeDirection=ChangeDirection, accel=Accel, wait=Wait, vanish=Vanish, tag=Tag, untag=Untag, action=ActionDef, actionRef=ActionRef)