# Copyright 2004-2019 Tom Rothamel # # 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. from __future__ import print_function import renpy.display import renpy.pyanalysis import random def compiling(loc): file, number = loc # @ReservedAssignment renpy.game.exception_info = "Compiling ATL code at %s:%d" % (file, number) def executing(loc): file, number = loc # @ReservedAssignment 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.func_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 @atl_warper def instant(t): return 1.0 position = renpy.object.Sentinel("position") def any_object(x): return x def bool_or_none(x): if x is None: return x return bool(x) def float_or_none(x): if x is None: return x return float(x) # 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, "rotate_pad" : bool, "transform_anchor" : bool, "xzoom" : float, "yzoom" : float, "zoom" : float, "nearest" : bool_or_none, "alpha" : float, "additive" : float, "around" : (position, position), "alignaround" : (float, float), "angle" : float, "radius" : float, "crop" : (float, float, float, float), "crop_relative" : bool, "size" : (int, int), "maxsize" : (int, int), "corner1" : (float, float), "corner2" : (float, float), "subpixel" : bool, "delay" : float, "xoffset" : float, "yoffset" : float, "offset" : (int, int), "xcenter" : position, "ycenter" : position, "debug" : any_object, "events" : bool, "xpan" : float_or_none, "ypan" : float_or_none, "xtile" : int, "ytile" : int, } 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: if v is None: return None else: return type(b)(v) else: return ty(v) def interpolate(t, a, b, type): # @ReservedAssignment """ Linearly interpolate the arguments. """ # Recurse into tuples. if isinstance(b, tuple): if a is None: a = [ None ] * len(b) 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, basestring)): 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 spline[0] is None: return spline[-1] 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) # A list of atl transforms that may need to be compile. compile_queue = [ ] def compile_all(): """ Called after the init phase is finished and transforms are compiled, to compile all transforms. """ global compile_queue for i in compile_queue: if i.atl.constant == GLOBAL_CONST: i.compile() compile_queue = [ ] # 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): # @ReservedAssignment expr = renpy.python.escape_unicode(expr) return eval(expr, renpy.store.__dict__, self.context) # @UndefinedVariable def __eq__(self, other): if not isinstance(other, Context): return False return self.context == other.context def __ne__(self, other): return not (self == other) # 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) parent_transform = None atl_st_offset = 0 # The block, as first compiled for prediction. predict_block = None nosave = [ 'parent_transform' ] def __init__(self, atl, context, parameters): # The constructor will be called by atltransform. 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 same thing, but only if the code was compiled into a block # for prediction purposes only. self.predict_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 # The child, without any transformations. self.raw_child = None # The parent transform that was called to create this transform. self.parent_transform = None # The offset between st and when this ATL block first executed. self.atl_st_offset = 0 if renpy.game.context().init_phase: compile_queue.append(self) def _handles_event(self, event): if (self.block is not None) and (self.block._handles_event(event)): return True if self.child is None: return False return self.child._handles_event(event) def get_block(self): """ Returns the compiled block to use. """ if self.block: return self.block elif self.predict_block and renpy.display.predict.predicting: return self.predict_block else: return 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. """ super(ATLTransformBase, self).take_execution_state(t) self.atl_st_offset = None if self is t: return elif not isinstance(t, ATLTransformBase): return elif t.atl is not self.atl: return # Important to do it this way, so we use __eq__. The exception handling # optimistically assumes that uncomparable objects are the same. try: if not (t.context == self.context): return except: pass 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 self.st = t.st self.at = t.at self.st_offset = t.st_offset self.at_offset = t.at_offset self.atl_st_offset = t.atl_st_offset if self.child is renpy.display.motion.null: if t.child and t.child._duplicatable: self.child = t.child._duplicate(None) else: self.child = t.child self.raw_child = t.raw_child def __call__(self, *args, **kwargs): _args = kwargs.pop("_args", None) 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 = None 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.iteritems(): 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) if child is None: child = self.child # 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, _args=_args, ) rv.parent_transform = self rv.take_state(self) return rv def compile(self): # @ReservedAssignment """ Compiles the ATL code into a block. As necessary, updates the properties. """ constant = (self.atl.constant == GLOBAL_CONST) if not constant: for p in self.parameters.positional: if p not in self.context.context: raise Exception("Cannot compile ATL Transform at %s:%d, as it's missing positional parameter %s." % ( self.atl.loc[0], self.atl.loc[1], self.parameters.positional[0], )) if constant and self.parent_transform: if self.parent_transform.block: self.block = self.parent_transform.block self.properties = self.parent_transform.properties self.parent_transform = None return self.block old_exception_info = renpy.game.exception_info block = self.atl.compile(self.context) if all( isinstance(statement, Interpolation) and statement.duration == 0 for statement in block.statements ): self.properties = [] for interp in block.statements: self.properties.extend(interp.properties) if not constant and renpy.display.predict.predicting: self.predict_block = block else: self.block = block self.predict_block = None renpy.game.exception_info = old_exception_info if constant and self.parent_transform: self.parent_transform.block = self.block self.parent_transform.properties = self.properties self.parent_transform = None return block def execute(self, trans, st, at): if self.done: return None block = self.get_block() if block is None: block = self.compile() events = [ ] # Hide request. if trans.hide_request: self.transform_event = "hide" if trans.replaced_request: self.transform_event = "replaced" # Notice transform events. if renpy.config.atl_multiple_events: if self.transform_event != self.last_transform_event: events.append(self.transform_event) self.last_transform_event = self.transform_event # Propagate transform_events from children. if (self.child is not None) and self.child.transform_event != self.last_child_transform_event: self.last_child_transform_event = self.child.transform_event if self.child.transform_event is not None: self.transform_event = self.child.transform_event # Notice transform events, again. if self.transform_event != self.last_transform_event: events.append(self.transform_event) self.last_transform_event = self.transform_event if self.transform_event in renpy.config.repeat_transform_events: self.transform_event = None self.last_transform_event = None old_exception_info = renpy.game.exception_info if (self.atl_st_offset is None) or (st - self.atl_st_offset) < 0: self.atl_st_offset = st if self.atl.animation: timebase = at else: timebase = st - self.atl_st_offset action, arg, pause = block.execute(trans, timebase, self.atl_state, events) renpy.game.exception_info = old_exception_info if action == "continue" and not renpy.display.predict.predicting: self.atl_state = arg else: self.done = True return pause def predict_one(self): self.atl.predict(self.context) def visit(self): block = self.get_block() if block is None: block = self.compile() return self.children + block.visit() # This is used in mark_constant to analyze expressions for constness. is_constant_expr = renpy.pyanalysis.Analysis().is_constant_expr GLOBAL_CONST = renpy.pyanalysis.GLOBAL_CONST # The base class for raw ATL statements. class RawStatement(object): constant = None 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): # @ReservedAssignment raise Exception("Compile not implemented.") # Predicts the images used by this statement. def predict(self, ctx): return def mark_constant(self): """ Sets self.constant to true if all expressions used in this statement and its children are constant. """ self.constant = 0 # 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, events): raise Exception("Not implemented.") # Return a list of displayable children. def visit(self): return [ ] # Does this respond to an event? def _handles_event(self, event): return False # 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): # @ReservedAssignment compiling(self.loc) statements = [ i.compile(ctx) for i in self.statements ] return Block(self.loc, statements) def predict(self, ctx): for i in self.statements: i.predict(ctx) def mark_constant(self): constant = GLOBAL_CONST for i in self.statements: i.mark_constant() constant = min(constant, i.constant) self.constant = constant # 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 _handles_event(self, event): for i in self.statements: if i._handles_event(event): return True return False def execute(self, trans, st, state, events): 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, events) # 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 if duration <= 0: raise Exception("ATL appears to be in an infinite loop.") # Figure how many durations can occur between the # start of the loop and now. new_repeats = int((target - loop_start) / duration) 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): # @ReservedAssignment 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, [ ]) child = renpy.easy.displayable(child) if isinstance(child, ATLTransformBase): child.compile() return child.get_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 "instant" 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 mark_constant(self): constant = GLOBAL_CONST constant = min(constant, is_constant_expr(self.warp_function)) constant = min(constant, is_constant_expr(self.duration)) constant = min(constant, is_constant_expr(self.circles)) for _name, expr in self.properties: constant = min(constant, is_constant_expr(expr)) for _name, exprs in self.splines: for expr in exprs: constant = min(constant, is_constant_expr(expr)) for expr, withexpr in self.expressions: constant = min(constant, is_constant_expr(expr)) constant = min(constant, is_constant_expr(withexpr)) self.constant = constant def predict(self, ctx): for i, _j in self.expressions: try: i = ctx.eval(i) except: continue if isinstance(i, ATLTransformBase): i.atl.predict(ctx) return try: renpy.easy.predict(i) except: continue # 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): # @ReservedAssignment compiling(self.loc) child = ctx.eval(self.expression) return Child(self.loc, child, None) def mark_constant(self): self.constant = is_constant_expr(self.expression) # This allows us to have multiple ATL transforms as children. class RawChild(RawStatement): def __init__(self, loc, child): super(RawChild, self).__init__(loc) self.children = [ child ] def compile(self, ctx): # @ReservedAssignment children = [ ] for i in self.children: children.append(renpy.display.motion.ATLTransform(i, context=ctx.context)) box = renpy.display.layout.MultiBox(layout='fixed') for i in children: box.add(i) return Child(self.loc, box, None) def mark_constant(self): constant = GLOBAL_CONST for i in self.children: i.mark_constant() constant = min(constant, i.constant) self.constant = constant # 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 = child self.transition = transition def execute(self, trans, st, state, events): executing(self.loc) old_child = trans.raw_child child = self.child if child._duplicatable: child = self.child._duplicate(trans._args) child._unique() if (old_child is not None) and (old_child is not renpy.display.motion.null) and (self.transition is not None): child = self.transition(old_widget=old_child, new_widget=child) child._unique() else: child = child trans.set_child(child, duplicate=False) 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, events): executing(self.loc) warper = warpers.get(self.warper, self.warper) if (self.warper != "instant") and (state is None) and ( (trans.atl_state is not None) or (trans.st == 0) ): first = True else: first = False if self.duration: complete = min(1.0, st / self.duration) else: complete = 1.0 if complete < 0.0: complete = 0.0 elif complete > 1.0: 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) has_angle = False for k, v in self.properties: setattr(newts, k, v) if k == "angle": newts.last_angle = v has_angle = True # Now, the things we change linearly are in the difference # between the new and old states. linear = trans.state.diff(newts) revolution = None splines = [ ] revdir = self.revolution circles = self.circles if (revdir or (has_angle and renpy.config.automatic_polar_motion)) and (newts.xaround is not None): # Remove various irrelevant motions. for i in [ 'xpos', 'ypos', 'xanchor', 'yanchor', 'xaround', 'yaround', 'xanchoraround', 'yanchoraround', ]: linear.pop(i, None) if revdir 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 revdir == "clockwise": if endangle < startangle: startangle -= 360 startangle -= circles * 360 elif revdir == "counterclockwise": if endangle > startangle: startangle += 360 startangle += circles * 360 # Store the revolution. revolution = (startangle, endangle, startradius, endradius) else: last_angle = trans.state.last_angle or trans.state.angle revolution = (last_angle, newts.last_angle, trans.state.radius, newts.radius) # Figure out the splines. for name, values in self.splines: splines.append((name, [ getattr(trans.state, name) ] + values)) state = (linear, revolution, splines) # Ensure that we set things, even if they don't actually # change from the old state. for k, v in self.properties: if k not in linear: setattr(trans.state, k, v) else: linear, revolution, splines = state # Linearly interpolate between the things in linear. for k, (old, new) in linear.iteritems(): 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 angle = interpolate(complete, startangle, endangle, float) trans.state.last_angle = angle trans.state.angle = angle 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 ((not first) or (not renpy.config.atl_one_frame)) and (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, max(0, 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): # @ReservedAssignment compiling(self.loc) repeats = self.repeats if repeats is not None: repeats = ctx.eval(repeats) return Repeat(self.loc, repeats) def mark_constant(self): self.constant = is_constant_expr(self.repeats) class Repeat(Statement): def __init__(self, loc, repeats): super(Repeat, self).__init__(loc) self.repeats = repeats def execute(self, trans, st, state, events): 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): # @ReservedAssignment return Parallel(self.loc, [i.compile(ctx) for i in self.blocks]) def predict(self, ctx): for i in self.blocks: i.predict(ctx) def mark_constant(self): constant = GLOBAL_CONST for i in self.blocks: i.mark_constant() constant = min(constant, i.constant) self.constant = constant class Parallel(Statement): def __init__(self, loc, blocks): super(Parallel, self).__init__(loc) self.blocks = blocks def _handles_event(self, event): for i in self.blocks: if i._handles_event(event): return True return False def execute(self, trans, st, state, events): 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, events) 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): # @ReservedAssignment compiling(self.loc) return Choice(self.loc, [ (ctx.eval(chance), block.compile(ctx)) for chance, block in self.choices]) def predict(self, ctx): for _i, j in self.choices: j.predict(ctx) def mark_constant(self): constant = GLOBAL_CONST for _chance, block in self.choices: block.mark_constant() constant = min(constant, block.constant) self.constant = constant class Choice(Statement): def __init__(self, loc, choices): super(Choice, self).__init__(loc) self.choices = choices def _handles_event(self, event): for i in self.choices: if i[1]._handles_event(event): return True return False def execute(self, trans, st, state, events): 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, events) 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): # @ReservedAssignment compiling(self.loc) return Time(self.loc, ctx.eval(self.time)) def mark_constant(self): self.constant = is_constant_expr(self.time) class Time(Statement): def __init__(self, loc, time): super(Time, self).__init__(loc) self.time = time def execute(self, trans, st, state, events): return "continue", None, None # The On statement. class RawOn(RawStatement): def __init__(self, loc, names, block): super(RawOn, self).__init__(loc) self.handlers = { } for i in names: self.handlers[i] = block def compile(self, ctx): # @ReservedAssignment compiling(self.loc) handlers = { } for k, v in self.handlers.iteritems(): handlers[k] = v.compile(ctx) return On(self.loc, handlers) def predict(self, ctx): for i in self.handlers.itervalues(): i.predict(ctx) def mark_constant(self): constant = GLOBAL_CONST for block in self.handlers.itervalues(): block.mark_constant() constant = min(constant, block.constant) self.constant = constant class On(Statement): def __init__(self, loc, handlers): super(On, self).__init__(loc) self.handlers = handlers def _handles_event(self, event): if event in self.handlers: return True else: return False def execute(self, trans, st, state, events): 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. for event in events: if event in self.handlers: # Do not allow people to abort the hide or replaced event. lock_event = (name == "hide" and trans.hide_request) or (name == "replaced" and trans.replaced_request) if not lock_event: 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, events) # If we get a continue, save our state. if action == "continue": # If it comes from a hide block, indicate that. if name == "hide" or name == "replaced": trans.hide_response = False trans.replaced_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" or name == "replaced": 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.itervalues() 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): # @ReservedAssignment return Event(self.loc, self.name) def mark_constant(self): self.constant = GLOBAL_CONST class Event(Statement): def __init__(self, loc, name): super(Event, self).__init__(loc) self.name = name def execute(self, trans, st, state, events): 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): # @ReservedAssignment compiling(self.loc) return Function(self.loc, ctx.eval(self.expr)) def mark_constant(self): self.constant = is_constant_expr(self.expr) class Function(Statement): def __init__(self, loc, function): super(Function, self).__init__(loc) self.function = function def _handles_event(self, event): return True def execute(self, trans, st, state, events): 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'): names = [ l.require(l.word) ] while l.match(','): name = l.word() if name is None: break names.append(name) l.require(':') l.expect_eol() l.expect_block('on') block = parse_atl(l.subblock_lexer()) statements.append(RawOn(loc, names, 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) continue # 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)