X-Git-Url: https://git.yukkurigames.com/?p=python-bulletml.git;a=blobdiff_plain;f=bulletml%2Fparser.py;h=c8bc990b105bfab652326be334a7db4b0be6a820;hp=0af66943893632fedcd3f5dac68a7defc216cb75;hb=bf5ff1d116840e1330c80894132919ec515596e4;hpb=9104092535823794c1b00f3c0fb16f5181992a00 diff --git a/bulletml/parser.py b/bulletml/parser.py index 0af6694..c8bc990 100644 --- a/bulletml/parser.py +++ b/bulletml/parser.py @@ -1,20 +1,40 @@ """BulletML parser. -http://www.asahi-net.or.jp/~cs8k-cyu/bulletml/index_e.html +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 actions, the only class you +should care about in here is BulletML. """ from __future__ import division +from math import sin, cos, radians, pi as PI + from xml.etree.ElementTree import ElementTree +# Python 3 moved this for no really good reason. try: - from cStringIO import StringIO + from sys import intern except ImportError: - from StringIO import StringIO + pass + +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"] + +PI_2 = PI * 2 + class ParseError(Error): """Raised when an error occurs parsing the XML structure.""" pass @@ -28,18 +48,18 @@ def realtag(element): class ParamList(object): """List of parameter definitions.""" - def __init__(self, element): - self.params = [] - if element: - for subelem in element: - if realtag(subelem) == "param": - self.params.append(NumberDef(subelem.text)) + + 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): - new_params = [param(params, rank) for param in self.params] - while len(new_params) < len(params): - new_params.append(params[len(new_params)]) - return new_params + return [param(params, rank) for param in self.params] def __repr__(self): return "%s(%r)" % (type(self).__name__, self.params) @@ -47,12 +67,28 @@ class ParamList(object): class Direction(object): """Raw direction value.""" - def __init__(self, doc, element, type="absolute"): - self.type = element.get("type", type) - self.value = NumberDef(element.text) + 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 = intern(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 (self.value(params, rank), self.type) + return (radians(self.value(params, rank)), self.type) def __repr__(self): return "%s(%r, type=%r)" % ( @@ -61,20 +97,54 @@ class Direction(object): class ChangeDirection(object): """Direction change over time.""" - def __init__(self, doc, element): + 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": - self.direction = Direction(doc, subelem) + direction = Direction.FromXML(doc, subelem) elif tag == "term": - self.term = INumberDef(subelem.text) + term = INumberDef(subelem.text) try: - self.term, self.direction - except AttributeError: - raise ParseError + 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 __call__(self, owner, action, params, rank, created): + frames = self.term(params, rank) + direction, type = self.direction(params, rank) + action.direction_frames = frames + action.aiming = False + if type == "sequence": + action.direction = direction + else: + if type == "absolute": + direction -= owner.direction + elif type != "relative": # aim or default + action.aiming = True + direction += owner.aim - owner.direction + + # Normalize to [-pi, pi). + direction = (direction + PI) % PI_2 - PI + if frames <= 0: + owner.direction += direction + else: + action.direction = direction / frames def __repr__(self): return "%s(term=%r, direction=%r)" % ( @@ -83,9 +153,25 @@ class ChangeDirection(object): class Speed(object): """Raw speed value.""" - def __init__(self, doc, element, type="absolute"): - self.type = element.get("type", type) - self.value = NumberDef(element.text) + 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 = intern(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) @@ -96,20 +182,49 @@ class Speed(object): class ChangeSpeed(object): """Speed change over time.""" - def __init__(self, doc, element): + 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": - self.speed = Speed(doc, subelem) + speed = Speed.FromXML(doc, subelem) elif tag == "term": - self.term = INumberDef(subelem.text) + term = INumberDef(subelem.text) try: - self.term, self.speed - except AttributeError: - raise ParseError + 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 __call__(self, owner, action, params, rank, created): + frames = self.term(params, rank) + speed, type = self.speed(params, rank) + action.speed_frames = frames + if frames <= 0: + if type == "absolute": + owner.speed = speed + elif type == "relative": + owner.speed += speed + elif type == "sequence": + action.speed = speed + elif type == "relative": + action.speed = speed / frames + else: + action.speed = (speed - owner.speed) / frames def __repr__(self): return "%s(term=%r, speed=%r)" % ( @@ -117,75 +232,277 @@ class ChangeSpeed(object): class Wait(object): """Wait for some frames.""" - def __init__(self, doc, element): - self.frames = INumberDef(element.text) - def __call__(self, params, rank): - return self.frames(params, rank) + 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, owner, action, params, rank, created): + action.wait_frames = self.frames(params, rank) + return True 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) + + def __call__(self, owner, action, params, rank, created): + owner.tags.add(self.tag) + +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) + + def __call__(self, owner, action, params, rank, created): + try: + owner.tags.remove(self.tag) + except KeyError: + pass + +class Appearance(object): + """Set a bullet appearance.""" + + def __init__(self, appearance): + self.appearance = appearance + + def __getstate__(self): + return dict(appearance=self.appearance) + + def __setstate__(self, state): + self.__init__(state["appearance"]) + + @classmethod + def FromXML(cls, doc, element): + """Construct using an ElementTree-style element.""" + return cls(element.text) + + def __call__(self, owner, action, params, rank, created): + owner.apearance = self.appearance + class Vanish(object): """Make the owner disappear.""" - def __init__(self, doc, element): + + 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__) + def __call__(self, owner, action, params, rank, created): + owner.vanish() + return True + class Repeat(object): """Repeat an action definition.""" - def __init__(self, doc, element): + 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": - self.times = INumberDef(subelem.text) + times = INumberDef(subelem.text) elif tag == "action": - self.action = ActionDef(doc, subelem) + action = ActionDef.FromXML(doc, subelem) elif tag == "actionRef": - self.action = ActionRef(doc, subelem) - + action = ActionRef.FromXML(doc, subelem) try: - self.times, self.action - except AttributeError: - raise ParseError - - def __call__(self, params, rank): - return self.times(params, rank), self.action(params, rank) + return cls(times, action) + except UnboundLocalError as exc: + raise ParseError(str(exc)) + + def __call__(self, owner, action, params, rank, created): + repeat = self.times(params, rank) + actions, params = self.action(params, rank) + child = action.Child(actions, params, rank, repeat) + owner.replace(action, child) + child.step(owner, created) + return True def __repr__(self): return "%s(%r, %r)" % (type(self).__name__, self.times, self.action) +class If(object): + """Conditional actions.""" + + def __init__(self, cond, then, else_=None): + self.cond = cond + self.then = then + self.else_ = else_ + + def __getstate__(self): + if self.else_: + return [('cond', self.cond.expr), + ('then', self.then), + ('else', self.else_)] + else: + return [('cond', self.cond.expr), ('then', self.then)] + + def __setstate__(self, state): + state = dict(state) + state["else_"] = state.pop("else", None) + state["cond"] = INumberDef(state["cond"]) + self.__init__(**state) + + @classmethod + def FromXML(cls, doc, element): + """Construct using an ElementTree-style element.""" + else_ = None + for subelem in element.getchildren(): + tag = realtag(subelem) + if tag == "cond": + cond = INumberDef(subelem.text) + elif tag == "then": + then = ActionDef.FromXML(doc, subelem) + elif tag == "else": + else_ = ActionDef.FromXML(doc, subelem) + try: + return cls(cond, then, else_) + except UnboundLocalError as exc: + raise ParseError(str(exc)) + + def __call__(self, owner, action, params, rank, created): + if self.cond(params, rank): + branch = self.then + else: + branch = self.else_ + + if branch: + actions, params = branch(params, rank) + child = action.Child(actions, params, rank) + owner.replace(action, child) + child.step(owner, created) + return True + class Accel(object): """Accelerate over some time.""" horizontal = None vertical = None - def __init__(self, doc, element): + 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": - self.term = INumberDef(subelem.text) + term = INumberDef(subelem.text) elif tag == "horizontal": - self.horizontal = Speed(doc, subelem) + horizontal = Speed.FromXML(doc, subelem) elif tag == "vertical": - self.vertical = Speed(doc, subelem) + vertical = Speed.FromXML(doc, subelem) try: - self.term + return cls(term, horizontal, vertical) except AttributeError: raise ParseError - def __call__(self, params, rank): + def __call__(self, owner, action, params, rank, created): frames = self.term(params, rank) - horizontal = self.horizontal and ( - self.horizontal(params, rank), self.horizontal.type) - vertical = self.vertical and ( - self.vertical(params, rank), self.vertical.type) - return frames, horizontal, vertical + horizontal = self.horizontal and self.horizontal(params, rank) + vertical = self.vertical and self.vertical(params, rank) + action.accel_frames = frames + if horizontal: + mx, type = horizontal + if frames <= 0: + if type == "absolute": + owner.mx = mx + elif type == "relative": + owner.mx += mx + elif type == "sequence": + action.mx = mx + elif type == "absolute": + action.mx = (mx - owner.mx) / frames + elif type == "relative": + action.mx = mx / frames + if vertical: + my, type = vertical + if frames <= 0: + if type == "absolute": + owner.my = my + elif type == "relative": + owner.my += my + elif type == "sequence": + action.my = my + elif type == "absolute": + action.my = (my - owner.my) / frames + elif type == "relative": + action.my = my / frames def __repr__(self): return "%s(%r, horizontal=%r, vertical=%r)" % ( @@ -194,30 +511,62 @@ class Accel(object): class BulletDef(object): """Bullet definition.""" - direction = None - speed = None + def __init__(self, actions=(), direction=None, speed=None, tags=(), + appearance=None): + self.direction = direction + self.speed = speed + self.actions = list(actions) + self.tags = set(tags) + self.appearance = appearance - def __init__(self, doc, element): - self.actions = [] - doc.bullets[element.get("label")] = self + 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)) + if self.tags: + state.append(("tags", list(self.tags))) + if self.appearance: + state.append(("appearance", self.appearance)) + 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 + tags = set() for subelem in element.getchildren(): tag = realtag(subelem) if tag == "direction": - self.direction = Direction(doc, subelem) + direction = Direction.FromXML(doc, subelem) elif tag == "speed": - self.speed = Speed(doc, subelem) + speed = Speed.FromXML(doc, subelem) elif tag == "action": - self.actions.append(ActionDef(doc, element)) + actions.append(ActionDef.FromXML(doc, subelem)) elif tag == "actionRef": - self.actions.append(ActionRef(doc, element)) + actions.append(ActionRef.FromXML(doc, subelem)) + elif tag == "tag": + tags.add(subelem.text) + dfn = cls(actions, direction, speed, tags) + doc._bullets[element.get("label")] = dfn + return dfn def __call__(self, params, rank): - actions = [] - for action in self.actions: - actions.append(action(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), + self.tags, + self.appearance, actions) def __repr__(self): @@ -227,10 +576,30 @@ class BulletDef(object): class BulletRef(object): """Create a bullet by name with parameters.""" - def __init__(self, doc, element): - self.bullet = element.get("label") - self.params = ParamList(element) - doc._bullet_refs.append(self) + 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) @@ -240,29 +609,42 @@ class BulletRef(object): type(self).__name__, self.params, self.bullet) class ActionDef(object): - """Action definition.""" + """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"]) - def __init__(self, doc, element): - doc.actions[element.get("label")] = self - self.actions = [] + @classmethod + def FromXML(cls, doc, element): + """Construct using an ElementTree-style element.""" + actions = [] for subelem in element.getchildren(): tag = realtag(subelem) try: - ctr = dict( - repeat=Repeat, - fire=FireDef, - fireRef=FireRef, - changeSpeed=ChangeSpeed, - changeDirection=ChangeDirection, - accel=Accel, - wait=Wait, - vanish=Vanish, - action=ActionDef, - actionRef=ActionRef)[tag] + ctr = cls.CONSTRUCTORS[tag] except KeyError: continue else: - self.actions.append(ctr(doc, subelem)) + 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 @@ -273,10 +655,30 @@ class ActionDef(object): class ActionRef(object): """Run an action by name with parameters.""" - def __init__(self, doc, element): - self.action = element.get("label") - self.params = ParamList(element) - doc._action_refs.append(self) + 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) @@ -285,36 +687,179 @@ class ActionRef(object): 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 = intern(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) + x = NumberDef(state["x"]) if "x" in state else None + y = NumberDef(state["y"]) if "y" in state else None + self.__init__(state["type"], x, 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).""" - direction = None - speed = None + def __init__(self, bullet, direction=None, speed=None, offset=None, + tags=(), appearance=None): + self.bullet = bullet + self.direction = direction + self.speed = speed + self.offset = offset + self.tags = set(tags) + self.appearance = appearance + + 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)) + if self.tags: + state.append(("tags", list(self.tags))) + if self.appearance: + state.append(("appearance", self.appearance)) + 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 + tags = set() + appearance = None - def __init__(self, doc, element): - doc.fires[element.get("label")] = self for subelem in element.getchildren(): tag = realtag(subelem) if tag == "direction": - self.direction = Direction(doc, subelem) + direction = Direction.FromXML(doc, subelem, "aim") elif tag == "speed": - self.speed = Speed(doc, subelem) + speed = Speed.FromXML(doc, subelem) elif tag == "bullet": - self.bullet = BulletDef(doc, subelem) + bullet = BulletDef.FromXML(doc, subelem) elif tag == "bulletRef": - self.bullet = BulletRef(doc, subelem) + bullet = BulletRef.FromXML(doc, subelem) + elif tag == "offset": + offset = Offset.FromXML(doc, subelem) + elif tag == "tag": + tags.add(subelem.text) + elif tag == "appearance": + appearance = subelem.text try: - self.bullet - except AttributeError: - raise ParseError - - def __call__(self, params, rank): - direction, speed, actions = self.bullet(params, rank) + fire = cls(bullet, direction, speed, offset, tags, appearance) + except UnboundLocalError as exc: + raise ParseError(str(exc)) + else: + doc._fires[element.get("label")] = fire + return fire + + def __call__(self, owner, action, params, rank, created): + direction, speed, tags, appearance, 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 + tags = tags.union(self.tags) + if self.appearance: + appearance = self.appearance + + if direction: + direction, type = direction + if type == "aim" or type is None: + direction += owner.aim + elif type == "sequence": + direction += action.previous_fire_direction + elif type == "relative": + direction += owner.direction + else: + direction = owner.aim + action.previous_fire_direction = direction + + if speed: + speed, type = speed + if type == "sequence": + speed += action.previous_fire_speed + elif type == "relative": + # The reference Noiz implementation uses + # prvFireSpeed here, but the standard is + # pretty clear -- "In case of the type is + # "relative", ... the speed is relative to the + # speed of this bullet." + speed += owner.speed + else: + speed = 1 + action.previous_fire_speed = speed + + x, y = owner.x, owner.y + if self.offset: + off_x, off_y = self.offset(params, rank) + if self.offset.type == "relative": + s = sin(direction) + c = cos(direction) + x += c * off_x + s * off_y + y += s * off_x - c * off_y + else: + x += off_x + y += off_y + + if appearance is None: + appearance = owner.appearance + bullet = owner.__class__( + x=x, y=y, direction=direction, speed=speed, + target=owner.target, actions=actions, rank=rank, + appearance=appearance, tags=tags, Action=action.__class__) + created.append(bullet) def __repr__(self): return "%s(direction=%r, speed=%r, bullet=%r)" % ( @@ -323,14 +868,34 @@ class FireDef(object): class FireRef(object): """Fire a bullet by name with parameters.""" - def __init__(self, doc, element): - self.fire = element.get("label") - self.params = ParamList(element) - doc._fire_refs.append(self) - - def __call__(self, params, rank): - """Generate a Bullet from the FireDef and params.""" - return self.fire(self.params(params, rank), rank) + 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, owner, action, params, rank, created): + params = self.params(params, rank) + return self.fire(owner, action, params, rank, created) def __repr__(self): return "%s(params=%r, fire=%r)" % ( @@ -339,8 +904,14 @@ class FireRef(object): class BulletML(object): """BulletML document. - A BulletML document is a collection of bullets, actions, and - firings, as well as a base game type. + 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( @@ -349,49 +920,111 @@ class BulletML(object): fire=FireDef, ) - def __init__(self, source): - self.bullets = {} - self.actions = {} - self.fires = {} + def __init__(self, type="none", actions=None): + self.type = intern(type) + self.actions = [] if actions is None else actions + + def __getstate__(self): + return [('type', self.type), ('actions', self.actions)] - self._bullet_refs = [] - self._action_refs = [] - self._fire_refs = [] + def __setstate__(self, state): + state = dict(state) + self.__init__(state["type"], actions=state.get("actions")) - if isinstance(source, (str, unicode)): + @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.type = root.get("type", "none") + doc = cls(type=root.get("type", "none")) + + doc._bullets = {} + doc._actions = {} + doc._fires = {} + doc._bullet_refs = [] + doc._action_refs = [] + doc._fire_refs = [] for element in root.getchildren(): tag = realtag(element) - if tag in self.CONSTRUCTORS: - self.CONSTRUCTORS[tag](self, element) + if tag in doc.CONSTRUCTORS: + doc.CONSTRUCTORS[tag].FromXML(doc, 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] + for ref in doc._bullet_refs: + ref.bullet = doc._bullets[ref.bullet] + for ref in doc._fire_refs: + ref.fire = doc._fires[ref.fire] + for ref in doc._action_refs: + ref.action = doc._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) + doc.actions = [act for name, act in doc._actions.items() + if name and name.startswith("top")] - @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")] + del(doc._bullet_refs) + del(doc._action_refs) + del(doc._fire_refs) + del(doc._bullets) + del(doc._actions) + del(doc._fires) + + return doc + + @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 as 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, bullets=%r, actions=%r, fires=%r)" % ( - type(self).__name__, self.type, self.bullets, self.actions, - self.fires) + 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, + appearance=Appearance, + untag=Untag, + action=ActionDef, + actionRef=ActionRef) +ActionDef.CONSTRUCTORS["if"] = If