# Copyright 2004-2010 PyTom # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import renpy import random def compiling(loc): file, number = loc renpy.game.exception_info = "Compiling ATL code at %s:%d" % (file, number) def executing(loc): file, number = loc renpy.game.exception_info = "Executing ATL code at %s:%d" % (file, number) # A map from the name of a time warp function to the function itself. warpers = { } def atl_warper(f): name = f.__name__ warpers[name] = f return f # The pause warper is used internally when no other warper is # specified. @atl_warper def pause(t): if t < 1.0: return 0.0 else: return 1.0 position = object() # A dictionary giving property names and the corresponding default # values. PROPERTIES = { "pos" : (position, position), "xpos" : position, "ypos" : position, "anchor" : (position, position), "xanchor" : position, "yanchor" : position, "xaround" : position, "yaround" : position, "xanchoraround" : float, "yanchoraround" : float, "align" : (float, float), "xalign" : float, "yalign" : float, "rotate" : float, "xzoom" : float, "yzoom" : float, "zoom" : float, "alpha" : float, "around" : (position, position), "alignaround" : (float, float), "angle" : float, "radius" : float, "crop" : (float, float, float, float), "size" : (int, int), "corner1" : (float, float), "corner2" : (float, float), "subpixel" : bool, "delay" : float, } def correct_type(v, b, ty): """ Corrects the type of v to match ty. b is used to inform the match. """ if ty is position: return type(b)(v) else: return ty(v) def interpolate(t, a, b, type): """ Linearly interpolate the arguments. """ if t >= 1.0: return b # Recurse into tuples. if isinstance(b, tuple): return tuple(interpolate(t, i, j, ty) for i, j, ty in zip(a, b, type)) # Deal with booleans, nones, etc. elif b is None or isinstance(b, bool): if t >= 1.0: return b else: return a # Interpolate everything else. else: if a is None: a = 0 return correct_type(a + t * (b - a), b, type) # Interpolate the value of a spline. This code is based on Aenakume's code, # from 00splines.rpy. def interpolate_spline(t, spline): if isinstance(spline[-1], tuple): return tuple(interpolate_spline(t, i) for i in zip(*spline)) if len(spline) == 2: t_p = 1.0 - t rv = t_p * spline[0] + t * spline[-1] elif len(spline) == 3: t_pp = (1.0 - t)**2 t_p = 2 * t * (1.0 - t) t2 = t**2 rv = t_pp * spline[0] + t_p * spline[1] + t2 * spline[2] elif len(spline) == 4: t_ppp = (1.0 - t)**3 t_pp = 3 * t * (1.0 - t)**2 t_p = 3 * t**2 * (1.0 - t) t3 = t**3 rv = t_ppp * spline[0] + t_pp * spline[1] + t_p * spline[2] + t3 * spline[3] else: raise Exception("ATL can't interpolate splines of length %d." % len(spline)) return correct_type(rv, spline[-1], position) # This is the context used when compiling an ATL statement. It stores the # scopes that are used to evaluate the various expressions in the statement, # and has a method to do the evaluation and return a result. class Context(object): def __init__(self, context): self.context = context def eval(self, expr): return eval(expr, renpy.store.__dict__, self.context) # This is intended to be subclassed by ATLTransform. It takes care of # managing ATL execution, which allows ATLTransform itself to not care # much about the contents of this file. class ATLTransformBase(renpy.object.Object): # Compatibility with older saves. parameters = renpy.ast.ParameterInfo([ ], [ ], None, None) def __init__(self, atl, context, parameters): super(ATLTransformBase, self).__init__() if parameters is None: parameters = ATLTransformBase.parameters # The parameters that we take. self.parameters = parameters # The raw code that makes up this ATL statement. self.atl = atl # The context in which execution occurs. self.context = Context(context) # The code after it has been compiled into a block. self.block = None # The properties of the block, if it contains only an # Interpolation. self.properties = None # The state of the statement we are executing. As this can be # shared between more than one object (in the case of a hide), # the data must not be altered. self.atl_state = None # Are we done? self.done = False # The transform event we are going to process. self.transform_event = None # The transform event we last processed. self.last_transform_event = None # The child transform event we last processed. self.last_child_transform_event = None def take_execution_state(self, t): """ Updates self to begin executing from the same point as t. This requires that t.atl is self.atl. """ self.done = t.done self.block = t.block self.atl_state = t.atl_state self.transform_event = t.transform_event self.last_transform_event = t.last_transform_event self.last_child_transform_event = t.last_child_transform_event def __call__(self, *args, **kwargs): context = self.context.context.copy() for k, v in self.parameters.parameters: if v is not None: context[k] = renpy.python.py_eval(v) positional = list(self.parameters.positional) args = list(args) child = self.child if not positional and args: child = args.pop(0) # Handle positional arguments. while positional and args: name = positional.pop(0) value = args.pop(0) if name in kwargs: raise Exception('Parameter %r is used as both a positional and keyword argument to a transition.' % name) context[name] = value if args: raise Exception("Too many arguments passed to ATL transform.") # Handle keyword arguments. for k, v in kwargs.items(): if k in positional: positional.remove(k) context[k] = v elif k in context: context[k] = v elif k == 'child': child = v else: raise Exception('Parameter %r is not known by ATL Transform.' % k) # Create a new ATL Transform. parameters = renpy.ast.ParameterInfo({}, positional, None, None) rv = renpy.display.motion.ATLTransform( atl=self.atl, child=child, style=self.style_arg, context=context, parameters=parameters) rv.take_state(self) return rv def compile(self): """ Compiles the ATL code into a block. As necessary, updates the properties. """ if self.parameters.positional and self.parameters.positional[0][1] is None: raise Exception("Cannot compile ATL Transform, as it's missing positional parameter %s." % self.parameters.positional[0]) old_exception_info = renpy.game.exception_info self.block = self.atl.compile(self.context) if len(self.block.statements) == 1 \ and isinstance(self.block.statements[0], Interpolation): interp = self.block.statements[0] if interp.duration == 0 and interp.properties: self.properties = interp.properties[:] renpy.game.exception_info = old_exception_info def execute(self, trans, st, at): if self.done: return None if not self.block: self.compile() # Propagate transform_events from children. if self.child: if self.child.transform_event != self.last_child_transform_event: self.last_child_transform_event = self.child.transform_event self.transform_event = self.child.transform_event # Hide request. if trans.hide_request: self.transform_event = "hide" # Notice transform events. if self.transform_event != self.last_transform_event: event = self.transform_event self.last_transform_event = self.transform_event else: event = None old_exception_info = renpy.game.exception_info if self.atl.animation: timebase = at else: timebase = st action, arg, pause = self.block.execute(trans, timebase, self.atl_state, event) renpy.game.exception_info = old_exception_info # print "Executing", self, self.state, self.xpos, self.ypos if action == "continue": self.atl_state = arg else: self.done = True return pause def predict(self, callback): self.atl.predict(self.context, callback) def visit(self): if not self.block: self.compile() return self.children + self.block.visit() # The base class for raw ATL statements. class RawStatement(renpy.object.Object): def __init__(self, loc): super(RawStatement, self).__init__() self.loc = loc # Compiles this RawStatement into a Statement, by using ctx to # evaluate expressions as necessary. def compile(self, ctx): raise Exception("Compile not implemented.") # Predicts the images used by this statement. def predict(self, ctx, callback): return # The base class for compiled ATL Statements. class Statement(renpy.object.Object): def __init__(self, loc): super(Statement, self).__init__() self.loc = loc # trans is the transform we're working on. # st is the time since this statement started executing. # state is the state stored by this statement, or None if # we've just started executing this statement. # event is an event we're triggering. # # "continue", state, pause - Causes this statement to execute # again, with the given state passed in the second time around. # # # "next", timeleft, pause - Causes the next statement to execute, # with timeleft being the amount of time left after this statement # finished. # # "event", (name, timeleft), pause - Causes an event to be reported, # and control to head up to the event handler. # # "repeat", (count, timeleft), pause - Causes the repeat behavior # to occur. # # As the Repeat statement can only appear in a block, only Block # needs to deal with the repeat behavior. # # Pause is the amount of time until execute should be called again, # or None if there's no need to call execute ever again. def execute(self, trans, st, state, event): raise Exception("Not implemented.") # Return a list of displayable children. def visit(self): return [ ] # This represents a Raw ATL block. class RawBlock(RawStatement): # Should we use the animation timebase or the showing timebase? animation = False def __init__(self, loc, statements, animation): super(RawBlock, self).__init__(loc) # A list of RawStatements in this block. self.statements = statements self.animation = animation def compile(self, ctx): compiling(self.loc) statements = [ i.compile(ctx) for i in self.statements ] return Block(self.loc, statements) def predict(self, ctx, callback): for i in self.statements: i.predict(ctx, callback) # A compiled ATL block. class Block(Statement): def __init__(self, loc, statements): super(Block, self).__init__(loc) # A list of statements in the block. self.statements = statements # The start times of various statements. self.times = [ ] for i, s in enumerate(statements): if isinstance(s, Time): self.times.append((s.time, i + 1)) self.times.sort() def execute(self, trans, st, state, event): executing(self.loc) # Unpack the state. if state is not None: index, start, loop_start, repeats, times, child_state = state else: index, start, loop_start, repeats, times, child_state = 0, 0, 0, 0, self.times[:], None # What we might be returning. action = "continue" arg = None pause = None while action == "continue": # Target is the time we're willing to execute to. # Max_pause is how long we'll wait before executing again. # If we have times queued up, then use them to inform target # and time. if times: time, tindex = times[0] target = min(time, st) max_pause = time - target # Otherwise, take the defaults. else: target = st max_pause = 15 while True: # If we've hit the last statement, it's the end of # this block. if index >= len(self.statements): return "next", target - start, None # Find the statement and try to run it. stmt = self.statements[index] action, arg, pause = stmt.execute(trans, target - start, child_state, event) # On continue, persist our state. if action == "continue": if pause is None: pause = max_pause action, arg, pause = "continue", (index, start, loop_start, repeats, times, arg), min(max_pause, pause) break elif action == "event": return action, arg, pause # On next, advance to the next statement in the block. elif action == "next": index += 1 start = target - arg child_state = None # On repeat, either terminate the block, or go to # the first statement. elif action == "repeat": count, arg = arg loop_end = target - arg duration = loop_end - loop_start # Figure how many durations can occur between the # start of the loop and now. new_repeats = int((target - loop_start) / duration) if duration <= 0: raise Exception("ATL appears to be in an infinite loop.") if count is not None: if repeats + new_repeats >= count: new_repeats = count - repeats loop_start += new_repeats * duration return "next", target - loop_start, None repeats += new_repeats loop_start = loop_start + new_repeats * duration start = loop_start index = 0 child_state = None if times: time, tindex = times[0] if time <= target: times.pop(0) index = tindex start = time child_state = None continue return action, arg, pause def visit(self): return [ j for i in self.statements for j in i.visit() ] # This can become one of four things: # # - A pause. # - An interpolation (which optionally can also reference other # blocks, as long as they're not time-dependent, and have the same # arity as the interpolation). # - A call to another block. # - A command to change the image, perhaps with a transition. # # We won't decide which it is until runtime, as we need the # values of the variables here. class RawMultipurpose(RawStatement): warp_function = None def __init__(self, loc): super(RawMultipurpose, self).__init__(loc) self.warper = None self.duration = None self.properties = [ ] self.expressions = [ ] self.splines = [ ] self.revolution = None self.circles = "0" def add_warper(self, name, duration, warp_function): self.warper = name self.duration = duration self.warp_function = warp_function def add_property(self, name, exprs): self.properties.append((name, exprs)) def add_expression(self, expr, with_clause): self.expressions.append((expr, with_clause)) def add_revolution(self, revolution): self.revolution = revolution def add_circles(self, circles): self.circles = circles def add_spline(self, name, exprs): self.splines.append((name, exprs)) def compile(self, ctx): compiling(self.loc) # Figure out what kind of statement we have. If there's no # interpolator, and no properties, than we have either a # call, or a child statement. if (self.warper is None and self.warp_function is None and not self.properties and not self.splines and len(self.expressions) == 1): expr, withexpr = self.expressions[0] child = ctx.eval(expr) if withexpr: transition = ctx.eval(withexpr) else: transition = None if isinstance(child, (int, float)): return Interpolation(self.loc, "pause", child, [ ], None, 0, [ ]) if isinstance(child, ATLTransformBase): child.compile() return child.block else: return Child(self.loc, child, transition) compiling(self.loc) # Otherwise, we probably have an interpolation statement. if self.warp_function: warper = ctx.eval(self.warp_function) else: warper = self.warper or "pause" if warper not in warpers: raise Exception("ATL Warper %s is unknown at runtime." % warper) properties = [ ] for name, expr in self.properties: if name not in PROPERTIES: raise Exception("ATL Property %s is unknown at runtime." % property) value = ctx.eval(expr) properties.append((name, value)) splines = [ ] for name, exprs in self.splines: if name not in PROPERTIES: raise Exception("ATL Property %s is unknown at runtime." % property) values = [ ctx.eval(i) for i in exprs ] splines.append((name, values)) for expr, with_ in self.expressions: try: value = ctx.eval(expr) except: raise Exception("Could not evaluate expression %r when compiling ATL." % expr) if not isinstance(value, ATLTransformBase): raise Exception("Expression %r is not an ATL transform, and so cannot be included in an ATL interpolation." % expr) value.compile() if value.properties is None: raise Exception("ATL transform %r is too complicated to be included in interpolation." % expr) properties.extend(value.properties) duration = ctx.eval(self.duration) circles = ctx.eval(self.circles) return Interpolation(self.loc, warper, duration, properties, self.revolution, circles, splines) def predict(self, ctx, callback): for i, j in self.expressions: try: i = ctx.eval(i) except: continue if isinstance(i, ATLTransformBase): i.atl.predict(ctx, callback) return try: i = renpy.easy.displayable(i) except: continue if isinstance(i, renpy.display.core.Displayable): i.predict(callback) # This lets us have an ATL transform as our child. class RawContainsExpr(RawStatement): def __init__(self, loc, expr): super(RawContainsExpr, self).__init__(loc) self.expression = expr def compile(self, ctx): compiling(self.loc) child = ctx.eval(self.expression) return Child(self.loc, child, None) # This allows us to have multiple children, inside a Fixed. class RawChild(RawStatement): def __init__(self, loc, child): super(RawChild, self).__init__(loc) self.children = [ child ] def compile(self, ctx): box = renpy.display.layout.MultiBox(layout='fixed') for i in self.children: box.add(renpy.display.motion.ATLTransform(i, context=ctx.context)) return Child(self.loc, box, None) # This changes the child of this statement, optionally with a transition. class Child(Statement): def __init__(self, loc, child, transition): super(Child, self).__init__(loc) self.child = renpy.easy.displayable(child) self.transition = transition def execute(self, trans, st, state, event): executing(self.loc) old_child = trans.raw_child if old_child is not None and self.transition is not None: child = self.transition(old_widget=old_child, new_widget=self.child) else: child = self.child trans.set_child(child) trans.raw_child = self.child return "next", st, None def visit(self): return [ self.child ] # This causes interpolation to occur. class Interpolation(Statement): def __init__(self, loc, warper, duration, properties, revolution, circles, splines): super(Interpolation, self).__init__(loc) self.warper = warper self.duration = duration self.properties = properties self.splines = splines # The direction we revolve in: cw, ccw, or None. self.revolution = revolution # The number of complete circles we make. self.circles = circles def execute(self, trans, st, state, event): executing(self.loc) warper = warpers.get(self.warper, self.warper) if self.duration: complete = min(1.0, st / self.duration) else: complete = 1.0 complete = warper(complete) if state is None: # Create a new transform state, and apply the property # changes to it. newts = renpy.display.motion.TransformState() newts.take_state(trans.state) for k, v in self.properties: setattr(newts, k, v) # Now, the things we change linearly are in the difference # between the new and old states. linear = trans.state.diff(newts) revolution = None splines = [ ] # Clockwise revolution. if self.revolution is not None: # Remove various irrelevant motions. for i in [ 'xpos', 'ypos', 'xanchor', 'yanchor', 'xaround', 'yaround', 'xanchoraround', 'yanchoraround', ]: linear.pop(i, None) if newts.xaround is not None: # Ensure we rotate around the new point. trans.state.xaround = newts.xaround trans.state.yaround = newts.yaround trans.state.xanchoraround = newts.xanchoraround trans.state.yanchoraround = newts.yanchoraround # Get the start and end angles and radii. startangle = trans.state.angle endangle = newts.angle startradius = trans.state.radius endradius = newts.radius # Make sure the revolution is in the appropriate direction, # and contains an appropriate number of circles. if self.revolution == "clockwise": if endangle < startangle: startangle -= 360 startangle -= self.circles * 360 elif self.revolution == "counterclockwise": if endangle > startangle: startangle += 360 startangle += self.circles * 360 # Store the revolution. revolution = (startangle, endangle, startradius, endradius) # Figure out the splines. for name, values in self.splines: splines.append((name, [ getattr(trans.state, name) ] + values)) state = (linear, revolution, splines) else: linear, revolution, splines = state # Linearly interpolate between the things in linear. for k, (old, new) in linear.items(): value = interpolate(complete, old, new, PROPERTIES[k]) setattr(trans.state, k, value) # Handle the revolution. if revolution is not None: startangle, endangle, startradius, endradius = revolution trans.state.angle = interpolate(complete, startangle, endangle, float) trans.state.radius = interpolate(complete, startradius, endradius, float) # Handle any splines we might have. for name, values in splines: value = interpolate_spline(complete, values) setattr(trans.state, name, value) if st >= self.duration: return "next", st - self.duration, None else: if not self.properties and not self.revolution and not self.splines: return "continue", state, self.duration - st else: return "continue", state, 0 # Implementation of the repeat statement. class RawRepeat(RawStatement): def __init__(self, loc, repeats): super(RawRepeat, self).__init__(loc) self.repeats = repeats def compile(self, ctx): compiling(self.loc) repeats = self.repeats if repeats is not None: repeats = ctx.eval(repeats) return Repeat(self.loc, repeats) class Repeat(Statement): def __init__(self, loc, repeats): super(Repeat, self).__init__(loc) self.repeats = repeats def execute(self, trans, st, state, event): return "repeat", (self.repeats, st), 0 # Parallel statement. class RawParallel(RawStatement): def __init__(self, loc, block): super(RawParallel, self).__init__(loc) self.blocks = [ block ] def compile(self, ctx): return Parallel(self.loc, [i.compile(ctx) for i in self.blocks]) def predict(self, ctx, callback): for i in self.blocks: i.predict(ctx, callback) class Parallel(Statement): def __init__(self, loc, blocks): super(Parallel, self).__init__(loc) self.blocks = blocks def execute(self, trans, st, state, event): executing(self.loc) if state is None: state = [ (i, None) for i in self.blocks ] # The amount of time left after finishing this block. left = [ ] # The duration of the pause. pauses = [ ] # The new state structure. newstate = [ ] for i, istate in state: action, arg, pause = i.execute(trans, st, istate, event) if pause is not None: pauses.append(pause) if action == "continue": newstate.append((i, arg)) elif action == "next": left.append(arg) elif action == "event": return action, arg, pause if newstate: return "continue", newstate, min(pauses) else: return "next", min(left), None def visit(self): return [ j for i in self.blocks for j in i.visit() ] # The choice statement. class RawChoice(RawStatement): def __init__(self, loc, chance, block): super(RawChoice, self).__init__(loc) self.choices = [ (chance, block) ] def compile(self, ctx): compiling(self.loc) return Choice(self.loc, [ (ctx.eval(chance), block.compile(ctx)) for chance, block in self.choices]) def predict(self, ctx, callback): for i, j in self.choices: j.predict(ctx, callback) class Choice(Statement): def __init__(self, loc, choices): super(Choice, self).__init__(loc) self.choices = choices def execute(self, trans, st, state, event): executing(self.loc) if state is None: total = 0 for chance, choice in self.choices: total += chance n = random.uniform(0, total) for chance, choice in self.choices: if n < chance: break n -= chance cstate = None else: choice, cstate = state action, arg, pause = choice.execute(trans, st, cstate, event) if action == "continue": return "continue", (choice, arg), pause else: return action, arg, None def visit(self): return [ j for i in self.choices for j in i[1].visit() ] # The Time statement. class RawTime(RawStatement): def __init__(self, loc, time): super(RawTime, self).__init__(loc) self.time = time def compile(self, ctx): compiling(self.loc) return Time(self.loc, ctx.eval(self.time)) class Time(Statement): def __init__(self, loc, time): super(Time, self).__init__(loc) self.time = time def execute(self, trans, st, state, event): return "continue", None, None # The On statement. class RawOn(RawStatement): def __init__(self, loc, name, block): super(RawOn, self).__init__(loc) self.handlers = { name : block } def compile(self, ctx): compiling(self.loc) handlers = { } for k, v in self.handlers.items(): handlers[k] = v.compile(ctx) return On(self.loc, handlers) def predict(self, ctx, callback): for i in self.handlers.values(): i.predict(ctx, callback) class On(Statement): def __init__(self, loc, handlers): super(On, self).__init__(loc) self.handlers = handlers def execute(self, trans, st, state, event): executing(self.loc) # If it's our first time through, start in the start state. if state is None: name, start, cstate = ("start", st, None) else: name, start, cstate = state # If we have an external event, and we have a handler for it, # handle it. if event in self.handlers: # Do not allow people to abort the hide handler with another # event. if name != "hide": name = event start = st cstate = None while True: # If we don't have a handler, return until we change event. if name not in self.handlers: return "continue", (name, start, cstate), None action, arg, pause = self.handlers[name].execute(trans, st - start, cstate, event) # If we get a continue, save our state. if action == "continue": # If it comes from a hide block, indicate that. if name == "hide": trans.hide_response = False return "continue", (name, start, arg), pause # If we get a next, then try going to the default # event, unless we're already in default, in which case we # go to None. elif action == "next": if name == "default" or name == "hide": name = None else: name = "default" start = st - arg cstate = None continue # If we get an event, then either handle it if we can, or # pass it up the stack if we can't. elif action == "event": name, arg = arg if name in self.handlers: start = max(st - arg, st - 30) cstate = None continue return "event", (name, arg), None def visit(self): return [ j for i in self.handlers.values() for j in i.visit() ] # Event statement. class RawEvent(RawStatement): def __init__(self, loc, name): super(RawEvent, self).__init__(loc) self.name = name def compile(self, ctx): return Event(self.loc, self.name) class Event(Statement): def __init__(self, loc, name): super(Event, self).__init__(loc) self.name = name def execute(self, trans, st, state, event): return "event", (self.name, st), None class RawFunction(RawStatement): def __init__(self, loc, expr): super(RawFunction, self).__init__(loc) self.expr = expr def compile(self, ctx): compiling(self.loc) return Function(self.loc, ctx.eval(self.expr)) class Function(Statement): def __init__(self, loc, function): super(Function, self).__init__(loc) self.function = function def execute(self, trans, st, state, event): fr = self.function(trans, st, trans.at) if fr is not None: return "continue", None, fr else: return "next", 0, None # This parses an ATL block. def parse_atl(l): l.advance() block_loc = l.get_location() statements = [ ] animation = False while not l.eob: loc = l.get_location() if l.keyword('repeat'): repeats = l.simple_expression() statements.append(RawRepeat(loc, repeats)) elif l.keyword('block'): l.require(':') l.expect_eol() l.expect_block('block') block = parse_atl(l.subblock_lexer()) statements.append(block) elif l.keyword('contains'): expr = l.simple_expression() if expr: l.expect_noblock('contains expression') statements.append(RawContainsExpr(loc, expr)) else: l.require(':') l.expect_eol() l.expect_block('contains') block = parse_atl(l.subblock_lexer()) statements.append(RawChild(loc, block)) elif l.keyword('parallel'): l.require(':') l.expect_eol() l.expect_block('parallel') block = parse_atl(l.subblock_lexer()) statements.append(RawParallel(loc, block)) elif l.keyword('choice'): chance = l.simple_expression() if not chance: chance = "1.0" l.require(':') l.expect_eol() l.expect_block('choice') block = parse_atl(l.subblock_lexer()) statements.append(RawChoice(loc, chance, block)) elif l.keyword('on'): name = l.require(l.word) l.require(':') l.expect_eol() l.expect_block('on') block = parse_atl(l.subblock_lexer()) statements.append(RawOn(loc, name, block)) elif l.keyword('time'): time = l.require(l.simple_expression) l.expect_noblock('time') statements.append(RawTime(loc, time)) elif l.keyword('function'): expr = l.require(l.simple_expression) l.expect_noblock('function') statements.append(RawFunction(loc, expr)) elif l.keyword('event'): name = l.require(l.word) l.expect_noblock('event') statements.append(RawEvent(loc, name)) elif l.keyword('pass'): l.expect_noblock('pass') statements.append(None) elif l.keyword('animation'): l.expect_noblock('animation') animation = True else: # If we can't assign it it a statement more specifically, # we try to parse it into a RawMultipurpose. That will # then be turned into another statement, as appropriate. # The RawMultipurpose we add things to. rm = renpy.atl.RawMultipurpose(loc) # Is the last clause an expression? last_expression = False # Is this clause an expression? this_expression = False # First, look for a warper. cp = l.checkpoint() warper = l.name() if warper in warpers: duration = l.require(l.simple_expression) warp_function = None elif warper == "warp": warper = None warp_function = l.require(l.simple_expression) duration = l.require(l.simple_expression) else: l.revert(cp) warper = None warp_function = None duration = "0" rm.add_warper(warper, duration, warp_function) # Now, look for properties and simple_expressions. while True: # Update expression status. last_expression = this_expression this_expression = False if l.keyword('pass'): continue # Parse revolution keywords. if l.keyword('clockwise'): rm.add_revolution('clockwise') continue if l.keyword('counterclockwise'): rm.add_revolution('counterclockwise') continue if l.keyword('circles'): expr = l.require(l.simple_expression) rm.add_circles(expr) # Try to parse a property. cp = l.checkpoint() prop = l.name() if prop in PROPERTIES: expr = l.require(l.simple_expression) # We either have a property or a spline. It's the # presence of knots that determine which one it is. knots = [ ] while l.keyword('knot'): knots.append(l.require(l.simple_expression)) if knots: knots.append(expr) rm.add_spline(prop, knots) else: rm.add_property(prop, expr) continue # Otherwise, try to parse it as a simple expressoon, # with an optional with clause. l.revert(cp) expr = l.simple_expression() if not expr: break if last_expression: l.error('ATL statement contains two expressions in a row; is one of them a misspelled property? If not, separate them with pass.') this_expression = True if l.keyword("with"): with_expr = l.require(l.simple_expression) else: with_expr = None rm.add_expression(expr, with_expr) l.expect_noblock('ATL') statements.append(rm) if l.eol(): l.advance() continue l.require(",", "comma or end of line") # Merge together statements that need to be merged together. merged = [ ] old = None for new in statements: if isinstance(old, RawParallel) and isinstance(new, RawParallel): old.blocks.extend(new.blocks) continue elif isinstance(old, RawChoice) and isinstance(new, RawChoice): old.choices.extend(new.choices) continue elif isinstance(old, RawChild) and isinstance(new, RawChild): old.children.extend(new.children) continue elif isinstance(old, RawOn) and isinstance(new, RawOn): old.handlers.update(new.handlers) continue # None is a pause statement, which gets skipped, but also # prevents things from combining. elif new is None: old = new continue merged.append(new) old = new return RawBlock(block_loc, merged, animation)