class SL2Decompiler(DecompilerBase): """ An object which handles the decompilation of renpy screen language 2 screens to a given stream """ # This dictionary is a mapping of Class: unbound_method, which is used to determine # what method to call for which slast class dispatch = Dispatcher() def print_node(self, ast): self.advance_to_line(ast.location[1]) self.dispatch.get(type(ast), type(self).print_unknown)(self, ast) @dispatch(sl2.slast.SLScreen) def print_screen(self, ast): # Print the screen statement and create the block self.indent() self.write("screen %s" % ast.name) # If we have parameters, print them. if ast.parameters: self.write(reconstruct_paraminfo(ast.parameters)) # Print any keywords if ast.tag: self.write(" tag %s" % ast.tag) # If we're decompiling screencode, print it. Else, insert a pass statement self.print_keywords_and_children(ast.keyword, ast.children, ast.location[1]) @dispatch(sl2.slast.SLIf) def print_if(self, ast): # if and showif share a lot of the same infrastructure self._print_if(ast, "if") @dispatch(sl2.slast.SLShowIf) def print_showif(self, ast): # so for if and showif we just call an underlying function with an extra argument self._print_if(ast, "showif") def _print_if(self, ast, keyword): # the first condition is named if or showif, the rest elif keyword = First(keyword, "elif") for condition, block in ast.entries: self.advance_to_line(block.location[1]) self.indent() # if condition is None, this is the else clause if condition is None: self.write("else:") else: self.write("%s %s:" % (keyword(), condition)) # Every condition has a block of type slast.SLBlock if block.keyword or block.children: self.print_block(block) else: with self.increase_indent(): self.indent() self.write("pass") @dispatch(sl2.slast.SLBlock) def print_block(self, ast): # A block contains possible keyword arguments and a list of child nodes # this is the reason if doesn't keep a list of children but special Blocks self.print_keywords_and_children(ast.keyword, ast.children, None) @dispatch(sl2.slast.SLFor) def print_for(self, ast): # Since tuple unpickling is hard, renpy just gives up and inserts a # $ a,b,c = _sl2_i after the for statement if any tuple unpacking was # attempted in the for statement. Detect this and ignore this slast.SLPython entry if ast.variable == "_sl2_i": variable = ast.children[0].code.source[:-9] children = ast.children[1:] else: variable = ast.variable.strip() + " " children = ast.children self.indent() self.write("for %sin %s:" % (variable, ast.expression)) # Interestingly, for doesn't contain a block, but just a list of child nodes self.print_nodes(children, 1) @dispatch(sl2.slast.SLPython) def print_python(self, ast): self.indent() # Extract the source code from the slast.SLPython object. If it starts with a # newline, print it as a python block, else, print it as a $ statement code = ast.code.source if code[0] == "\n": code = code[1:] self.write("python:") with self.increase_indent(): self.write_lines(split_logical_lines(code)) else: self.write("$ %s" % code) @dispatch(sl2.slast.SLPass) def print_pass(self, ast): # A pass statement self.indent() self.write("pass") @dispatch(sl2.slast.SLUse) def print_use(self, ast): # A use statement requires reconstructing the arguments it wants to pass self.indent() self.write("use %s%s" % (ast.target, reconstruct_arginfo(ast.args))) if hasattr(ast, 'id') and ast.id is not None: self.write(" id %s" % ast.id) if hasattr(ast, 'block') and ast.block: self.write(":") self.print_block(ast.block) @dispatch(sl2.slast.SLTransclude) def print_transclude(self, ast): self.indent() self.write("transclude") @dispatch(sl2.slast.SLDefault) def print_default(self, ast): # A default statement self.indent() self.write("default %s = %s" % (ast.variable, ast.expression)) @dispatch(sl2.slast.SLDisplayable) def print_displayable(self, ast, has_block=False): # slast.SLDisplayable represents a variety of statements. We can figure out # what statement it represents by analyzing the called displayable and style # attributes. key = (ast.displayable, ast.style) nameAndChildren = self.displayable_names.get(key) if nameAndChildren is None: self.write_failure("Unknown SL2 displayable: %s" % str(key)) else: (name, children) = nameAndChildren self.indent() self.write(name) if ast.positional: self.write(" " + " ".join(ast.positional)) # The AST contains no indication of whether or not "has" blocks # were used. We'll use one any time it's possible (except for # directly nesting them, or if they wouldn't contain any children), # since it results in cleaner code. if (not has_block and children == 1 and len(ast.children) == 1 and isinstance(ast.children[0], sl2.slast.SLDisplayable) and ast.children[0].children and (not ast.keyword or ast.children[0].location[1] > ast.keyword[-1][1].linenumber)): self.print_keywords_and_children(ast.keyword, [], ast.location[1], needs_colon=True) self.advance_to_line(ast.children[0].location[1]) with self.increase_indent(): self.indent() self.write("has ") self.skip_indent_until_write = True self.print_displayable(ast.children[0], True) else: self.print_keywords_and_children(ast.keyword, ast.children, ast.location[1], has_block=has_block) displayable_names = { (behavior.OnEvent, None): ("on", 0), (behavior.OnEvent, 0): ("on", 0), (behavior.MouseArea, 0): ("mousearea", 0), (ui._add, None): ("add", 0), (sld.sl2add, None): ("add", 0), (ui._hotbar, "hotbar"): ("hotbar", 0), (sld.sl2vbar, None): ("vbar", 0), (sld.sl2bar, None): ("bar", 0), (ui._label, "label"): ("label", 0), (ui._textbutton, 0): ("textbutton", 0), (ui._imagebutton, "image_button"): ("imagebutton", 0), (im.image, "default"): ("image", 0), (behavior.Input, "input"): ("input", 0), (behavior.Timer, "default"): ("timer", 0), (ui._key, None): ("key", 0), (text.Text, "text"): ("text", 0), (layout.Null, "default"): ("null", 0), (dragdrop.Drag, None): ("drag", 1), (dragdrop.Drag, "drag"): ("drag", 1), (motion.Transform, "transform"): ("transform", 1), (ui._hotspot, "hotspot"): ("hotspot", 1), (sld.sl2viewport, "viewport"): ("viewport", 1), (behavior.Button, "button"): ("button", 1), (layout.Window, "frame"): ("frame", 1), (layout.Window, "window"): ("window", 1), (dragdrop.DragGroup, None): ("draggroup", 'many'), (ui._imagemap, "imagemap"): ("imagemap", 'many'), (layout.Side, "side"): ("side", 'many'), (layout.Grid, "grid"): ("grid", 'many'), (layout.MultiBox, "fixed"): ("fixed", 'many'), (layout.MultiBox, "vbox"): ("vbox", 'many'), (layout.MultiBox, "hbox"): ("hbox", 'many') } def print_keywords_and_children(self, keywords, children, lineno, needs_colon=False, has_block=False): # This function prints the keyword arguments and child nodes # Used in a displayable screen statement # If lineno is None, we're already inside of a block. # Otherwise, we're on the line that could start a block. keywords_by_line = [] current_line = (lineno, []) for key, value in keywords: if current_line[0] is None or value.linenumber > current_line[0]: keywords_by_line.append(current_line) current_line = (value.linenumber, []) current_line[1].extend((key, value)) keywords_by_line.append(current_line) last_keyword_line = keywords_by_line[-1][0] children_with_keywords = [] children_after_keywords = [] for i in children: if i.location[1] > last_keyword_line: children_after_keywords.append(i) else: children_with_keywords.append((i.location[1], i)) # the keywords in keywords_by_line[0] go on the line that starts the # block, not in it block_contents = sorted(keywords_by_line[1:] + children_with_keywords, key=itemgetter(0)) if keywords_by_line[0][1]: # this never happens if lineno was None self.write(" %s" % ' '.join(keywords_by_line[0][1])) if block_contents or (not has_block and children_after_keywords): if lineno is not None: self.write(":") with self.increase_indent(): for i in block_contents: if isinstance(i[1], list): self.advance_to_line(i[0]) self.indent() self.write(' '.join(i[1])) else: self.print_node(i[1]) elif needs_colon: self.write(":") self.print_nodes(children_after_keywords, 0 if has_block else 1)
class Decompiler(DecompilerBase): """ An object which hanldes the decompilation of renpy asts to a given stream """ # This dictionary is a mapping of Class: unbount_method, which is used to determine # what method to call for which ast class dispatch = Dispatcher() def __init__(self, out_file=None, decompile_python=False, indentation = ' ', printlock=None, translator=None): super(Decompiler, self).__init__(out_file, indentation, printlock) self.decompile_python = decompile_python self.translator = translator self.paired_with = False self.say_inside_menu = None self.label_inside_menu = None self.label_inside_call_location = None self.in_init = False self.missing_init = False self.init_offset = 0 self.is_356c6e34_or_later = False self.most_lines_behind = 0 self.last_lines_behind = 0 self.tag_outside_block = False def advance_to_line(self, linenumber): self.last_lines_behind = max(self.linenumber + (0 if self.skip_indent_until_write else 1) - linenumber, 0) self.most_lines_behind = max(self.last_lines_behind, self.most_lines_behind) super(Decompiler, self).advance_to_line(linenumber) def save_state(self): return (super(Decompiler, self).save_state(), self.paired_with, self.say_inside_menu, self.label_inside_menu, self.label_inside_call_location, self.in_init, self.missing_init, self.most_lines_behind, self.last_lines_behind) def commit_state(self, state): super(Decompiler, self).commit_state(state[0]) def rollback_state(self, state): self.paired_with = state[1] self.say_inside_menu = state[2] self.label_inside_menu = state[3] self.label_inside_call_location = state[4] self.in_init = state[5] self.missing_init = state[6] self.most_lines_behind = state[7] self.last_lines_behind = state[8] super(Decompiler, self).rollback_state(state[0]) def dump(self, ast, indent_level=0, init_offset=False, tag_outside_block=False): if (isinstance(ast, (tuple, list)) and len(ast) > 1 and isinstance(ast[-1], renpy.ast.Return) and (not hasattr(ast[-1], 'expression') or ast[-1].expression is None) and ast[-1].linenumber == ast[-2].linenumber): # A very crude version check, but currently the best we can do. # Note that this commit first appears in the 6.99 release. self.is_356c6e34_or_later = True self.tag_outside_block = tag_outside_block if self.translator: self.translator.translate_dialogue(ast) if init_offset and isinstance(ast, (tuple, list)): self.set_best_init_offset(ast) # skip_indent_until_write avoids an initial blank line super(Decompiler, self).dump(ast, indent_level, skip_indent_until_write=True) # if there's anything we wanted to write out but didn't yet, do it now for m in self.blank_line_queue: m(None) self.write("\n# Decompiled by unrpyc: https://github.com/CensoredUsername/unrpyc\n") assert not self.missing_init, "A required init, init label, or translate block was missing" def print_node(self, ast): # We special-case line advancement for some types in their print # methods, so don't advance lines for them here. if hasattr(ast, 'linenumber') and not isinstance(ast, (renpy.ast.TranslateString, renpy.ast.With, renpy.ast.Label, renpy.ast.Pass, renpy.ast.Return)): self.advance_to_line(ast.linenumber) # It doesn't matter what line "block:" is on. The loc of a RawBlock # refers to the first statement inside the block, which we advance # to from print_atl. elif hasattr(ast, 'loc') and not isinstance(ast, renpy.atl.RawBlock): self.advance_to_line(ast.loc[1]) self.dispatch.get(type(ast), type(self).print_unknown)(self, ast) # ATL printing functions def print_atl(self, ast): with self.increase_indent(): self.advance_to_line(ast.loc[1]) if ast.statements: self.print_nodes(ast.statements) # If a statement ends with a colon but has no block after it, loc will # get set to ('', 0). That isn't supposed to be valid syntax, but it's # the only thing that can generate that. elif ast.loc != ('', 0): self.indent() self.write("pass") @dispatch(renpy.atl.RawMultipurpose) def print_atl_rawmulti(self, ast): warp_words = WordConcatenator(False) # warpers if ast.warp_function: warp_words.append("warp", ast.warp_function, ast.duration) elif ast.warper: warp_words.append(ast.warper, ast.duration) elif ast.duration != "0": warp_words.append("pause", ast.duration) warp = warp_words.join() words = WordConcatenator(warp and warp[-1] != ' ', True) # revolution if ast.revolution: words.append(ast.revolution) # circles if ast.circles != "0": words.append("circles %s" % ast.circles) # splines spline_words = WordConcatenator(False) for name, expressions in ast.splines: spline_words.append(name, expressions[-1]) for expression in expressions[:-1]: spline_words.append("knot", expression) words.append(spline_words.join()) # properties property_words = WordConcatenator(False) for key, value in ast.properties: property_words.append(key, value) words.append(property_words.join()) # with expression_words = WordConcatenator(False) # TODO There's a lot of cases where pass isn't needed, since we could # reorder stuff so there's never 2 expressions in a row. (And it's never # necessary for the last one, but we don't know what the last one is # since it could get reordered.) needs_pass = len(ast.expressions) > 1 for (expression, with_expression) in ast.expressions: expression_words.append(expression) if with_expression: expression_words.append("with", with_expression) if needs_pass: expression_words.append("pass") words.append(expression_words.join()) to_write = warp + words.join() if to_write: self.indent() self.write(to_write) else: # A trailing comma results in an empty RawMultipurpose being # generated on the same line as the last real one. self.write(",") @dispatch(renpy.atl.RawBlock) def print_atl_rawblock(self, ast): self.indent() self.write("block:") self.print_atl(ast) @dispatch(renpy.atl.RawChild) def print_atl_rawchild(self, ast): for child in ast.children: self.indent() self.write("contains:") self.print_atl(child) @dispatch(renpy.atl.RawChoice) def print_atl_rawchoice(self, ast): for chance, block in ast.choices: self.indent() self.write("choice") if chance != "1.0": self.write(" %s" % chance) self.write(":") self.print_atl(block) if (self.index + 1 < len(self.block) and isinstance(self.block[self.index + 1], renpy.atl.RawChoice)): self.indent() self.write("pass") @dispatch(renpy.atl.RawContainsExpr) def print_atl_rawcontainsexpr(self, ast): self.indent() self.write("contains %s" % ast.expression) @dispatch(renpy.atl.RawEvent) def print_atl_rawevent(self, ast): self.indent() self.write("event %s" % ast.name) @dispatch(renpy.atl.RawFunction) def print_atl_rawfunction(self, ast): self.indent() self.write("function %s" % ast.expr) @dispatch(renpy.atl.RawOn) def print_atl_rawon(self, ast): for name, block in sorted(ast.handlers.items(), key=lambda i: i[1].loc[1]): self.indent() self.write("on %s:" % name) self.print_atl(block) @dispatch(renpy.atl.RawParallel) def print_atl_rawparallel(self, ast): for block in ast.blocks: self.indent() self.write("parallel:") self.print_atl(block) if (self.index + 1 < len(self.block) and isinstance(self.block[self.index + 1], renpy.atl.RawParallel)): self.indent() self.write("pass") @dispatch(renpy.atl.RawRepeat) def print_atl_rawrepeat(self, ast): self.indent() self.write("repeat") if ast.repeats: self.write(" %s" % ast.repeats) # not sure if this is even a string @dispatch(renpy.atl.RawTime) def print_atl_rawtime(self, ast): self.indent() self.write("time %s" % ast.time) # Displayable related functions def print_imspec(self, imspec): if imspec[1] is not None: begin = "expression %s" % imspec[1] else: begin = " ".join(imspec[0]) words = WordConcatenator(begin and begin[-1] != ' ', True) if imspec[2] is not None: words.append("as %s" % imspec[2]) if len(imspec[6]) > 0: words.append("behind %s" % ', '.join(imspec[6])) if isinstance(imspec[4], unicode): words.append("onlayer %s" % imspec[4]) if imspec[5] is not None: words.append("zorder %s" % imspec[5]) if len(imspec[3]) > 0: words.append("at %s" % ', '.join(imspec[3])) self.write(begin + words.join()) return words.needs_space @dispatch(renpy.ast.Image) def print_image(self, ast): self.require_init() self.indent() self.write("image %s" % ' '.join(ast.imgname)) if ast.code is not None: self.write(" = %s" % ast.code.source) else: if hasattr(ast, "atl") and ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.Transform) def print_transform(self, ast): self.require_init() self.indent() # If we have an implicit init block with a non-default priority, we need to store the priority here. priority = "" if isinstance(self.parent, renpy.ast.Init): init = self.parent if init.priority != self.init_offset and len(init.block) == 1 and not self.should_come_before(init, ast): priority = " %d" % (init.priority - self.init_offset) self.write("transform%s %s" % (priority, ast.varname)) if ast.parameters is not None: self.write(reconstruct_paraminfo(ast.parameters)) if hasattr(ast, "atl") and ast.atl is not None: self.write(":") self.print_atl(ast.atl) # Directing related functions @dispatch(renpy.ast.Show) def print_show(self, ast): self.indent() self.write("show ") needs_space = self.print_imspec(ast.imspec) if self.paired_with: if needs_space: self.write(" ") self.write("with %s" % self.paired_with) self.paired_with = True if hasattr(ast, "atl") and ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.ShowLayer) def print_showlayer(self, ast): self.indent() self.write("show layer %s" % ast.layer) if ast.at_list: self.write(" at %s" % ', '.join(ast.at_list)) if hasattr(ast, "atl") and ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.Scene) def print_scene(self, ast): self.indent() self.write("scene") if ast.imspec is None: if isinstance(ast.layer, unicode): self.write(" onlayer %s" % ast.layer) needs_space = True else: self.write(" ") needs_space = self.print_imspec(ast.imspec) if self.paired_with: if needs_space: self.write(" ") self.write("with %s" % self.paired_with) self.paired_with = True if hasattr(ast, "atl") and ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.Hide) def print_hide(self, ast): self.indent() self.write("hide ") needs_space = self.print_imspec(ast.imspec) if self.paired_with: if needs_space: self.write(" ") self.write("with %s" % self.paired_with) self.paired_with = True @dispatch(renpy.ast.With) def print_with(self, ast): # the 'paired' attribute indicates that this with # and with node afterwards are part of a postfix # with statement. detect this and process it properly if hasattr(ast, "paired") and ast.paired is not None: # Sanity check. check if there's a matching with statement two nodes further if not(isinstance(self.block[self.index + 2], renpy.ast.With) and self.block[self.index + 2].expr == ast.paired): raise Exception("Unmatched paired with {0} != {1}".format( repr(self.paired_with), repr(ast.expr))) self.paired_with = ast.paired elif self.paired_with: # Check if it was consumed by a show/scene statement if self.paired_with is not True: self.write(" with %s" % ast.expr) self.paired_with = False else: self.advance_to_line(ast.linenumber) self.indent() self.write("with %s" % ast.expr) self.paired_with = False # Flow control @dispatch(renpy.ast.Label) def print_label(self, ast): # If a Call block preceded us, it printed us as "from" if (self.index and isinstance(self.block[self.index - 1], renpy.ast.Call)): return remaining_blocks = len(self.block) - self.index if remaining_blocks > 1: next_ast = self.block[self.index + 1] # See if we're the label for a call location, rather than a standalone label. if (not ast.block and (not hasattr(ast, 'parameters') or ast.parameters is None) and hasattr(next_ast, 'linenumber') and next_ast.linenumber == ast.linenumber and isinstance(next_ast, store.locations.CallLocation)): self.label_inside_call_location = ast return # See if we're the label for a menu, rather than a standalone label. if (not ast.block and (not hasattr(ast, 'parameters') or ast.parameters is None) and hasattr(next_ast, 'linenumber') and next_ast.linenumber == ast.linenumber and (isinstance(next_ast, renpy.ast.Menu) or (remaining_blocks > 2 and isinstance(next_ast, renpy.ast.Say) and self.say_belongs_to_menu(next_ast, self.block[self.index + 2])))): self.label_inside_menu = ast return self.advance_to_line(ast.linenumber) self.indent() # It's possible that we're an "init label", not a regular label. There's no way to know # if we are until we parse our children, so temporarily redirect all of our output until # that's done, so that we can squeeze in an "init " if we are. out_file = self.out_file self.out_file = StringIO() missing_init = self.missing_init self.missing_init = False try: self.write("label %s%s%s:" % ( ast.name, reconstruct_paraminfo(ast.parameters) if hasattr(ast, 'parameters') else '', " hide" if hasattr(ast, 'hide') and ast.hide else "")) self.print_nodes(ast.block, 1) finally: if self.missing_init: out_file.write("init ") self.missing_init = missing_init out_file.write(self.out_file.getvalue()) self.out_file = out_file @dispatch(renpy.ast.Jump) def print_jump(self, ast): self.indent() self.write("jump %s%s" % ("expression " if ast.expression else "", ast.target)) @dispatch(store.locations.CallLocation) def print_call_location(self, ast): self.indent() self.write("call location %s" % ast.location) if self.label_inside_call_location is not None: self.write(" %s" % self.label_inside_call_location.name) self.label_inside_call_location = None self.write(":") with self.increase_indent(): for zone, props, condition, show, block in ast.zones: self.indent() words = WordConcatenator(False) if isinstance(zone, tuple): zone, image_name, zorder, behind = zone words.append(zone) words.append(*image_name) if zorder is not None: words.append("zorder %s" % zorder) if behind: words.append("behind %s" % ','.join(behind)) else: words.append(zone) if props is not None: words.append("pass %s" % reconstruct_arginfo(props)) if isinstance(show, renpy.ast.PyExpr): words.append("showif %s" % show) if isinstance(condition, renpy.ast.PyExpr): words.append("if %s" % condition) self.write("%s:" % words.join()) self.print_nodes(block, 1) @dispatch(renpy.ast.Call) def print_call(self, ast): self.indent() words = WordConcatenator(False) words.append("call") if ast.expression: words.append("expression") words.append(ast.label) if hasattr(ast, 'arguments') and ast.arguments is not None: if ast.expression: words.append("pass") words.append(reconstruct_arginfo(ast.arguments)) # We don't have to check if there's enough elements here, # since a Label or a Pass is always emitted after a Call. next_block = self.block[self.index + 1] if isinstance(next_block, renpy.ast.Label): words.append("from %s" % next_block.name) self.write(words.join()) @dispatch(renpy.ast.Return) def print_return(self, ast): if ((not hasattr(ast, 'expression') or ast.expression is None) and self.parent is None and self.index + 1 == len(self.block) and self.index and ast.linenumber == self.block[self.index - 1].linenumber): # As of Ren'Py commit 356c6e34, a return statement is added to # the end of each rpyc file. Don't include this in the source. return self.advance_to_line(ast.linenumber) self.indent() self.write("return") if hasattr(ast, 'expression') and ast.expression is not None: self.write(" %s" % ast.expression) @dispatch(renpy.ast.If) def print_if(self, ast): statement = First("if %s:", "elif %s:") for i, (condition, block) in enumerate(ast.entries): # The non-Unicode string "True" is the condition for else:. if (i + 1) == len(ast.entries) and not isinstance(condition, unicode): self.indent() self.write("else:") else: if(hasattr(condition, 'linenumber')): self.advance_to_line(condition.linenumber) self.indent() self.write(statement() % condition) self.print_nodes(block, 1) @dispatch(renpy.ast.While) def print_while(self, ast): self.indent() self.write("while %s:" % ast.condition) self.print_nodes(ast.block, 1) @dispatch(renpy.ast.Pass) def print_pass(self, ast): if (self.index and isinstance(self.block[self.index - 1], renpy.ast.Call)): return if (self.index > 1 and isinstance(self.block[self.index - 2], renpy.ast.Call) and isinstance(self.block[self.index - 1], renpy.ast.Label) and self.block[self.index - 2].linenumber == ast.linenumber): return self.advance_to_line(ast.linenumber) self.indent() self.write("pass") def should_come_before(self, first, second): return first.linenumber < second.linenumber def require_init(self): if not self.in_init: self.missing_init = True def set_best_init_offset(self, nodes): votes = {} for ast in nodes: if not isinstance(ast, renpy.ast.Init): continue offset = ast.priority # Keep this block in sync with print_init if len(ast.block) == 1 and not self.should_come_before(ast, ast.block[0]): if isinstance(ast.block[0], renpy.ast.Screen): offset -= -500 elif isinstance(ast.block[0], renpy.ast.Testcase): offset -= 500 elif isinstance(ast.block[0], renpy.ast.Image): offset -= 500 if self.is_356c6e34_or_later else 990 elif isinstance(ast.block[0], renpy.ast.UserStatement): if ast.block[0].line.startswith("image minion "): offset -= 500 elif ast.block[0].line.startswith("ability ") or ast.block[0].line.startswith("init memories ") or ast.block[0].line.startswith("person store ") or ast.block[0].line.startswith("layered_image "): offset -= 50 elif ast.block[0].line.startswith("init market "): offset -= 150 elif ast.block[0].line.startswith("define location "): offset -= -500 elif ast.block[0].line.startswith("inv_item ") or ast.block[0].line.startswith("achievement ") or ast.block[0].line.startswith("define person "): offset -= 100 votes[offset] = votes.get(offset, 0) + 1 if votes: winner = max(votes, key=votes.get) # It's only worth setting an init offset if it would save # more than one priority specification versus not setting one. if votes.get(0, 0) + 1 < votes[winner]: self.set_init_offset(winner) def set_init_offset(self, offset): def do_set_init_offset(linenumber): # if we got to the end of the file and haven't emitted this yet, # don't bother, since it only applies to stuff below it. if linenumber is None or linenumber - self.linenumber <= 1 or self.indent_level: return True if offset != self.init_offset: self.indent() self.write("init offset = %s" % offset) self.init_offset = offset return False self.do_when_blank_line(do_set_init_offset) @dispatch(renpy.ast.Init) def print_init(self, ast): in_init = self.in_init self.in_init = True try: # A bunch of statements can have implicit init blocks # Define has a default priority of 0, screen of -500 and image of 990 # Keep this block in sync with set_best_init_offset # TODO merge this and require_init into another decorator or something if len(ast.block) == 1 and ( isinstance(ast.block[0], (renpy.ast.Define, renpy.ast.Default, renpy.ast.Transform)) or (ast.priority == -500 + self.init_offset and isinstance(ast.block[0], renpy.ast.Screen)) or (ast.priority == self.init_offset and isinstance(ast.block[0], renpy.ast.Style)) or (ast.priority == 500 + self.init_offset and isinstance(ast.block[0], renpy.ast.Testcase)) or (ast.priority == 0 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("layeredimage ")) or (ast.priority == 500 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("image minion ")) or (ast.priority == 50 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("ability ")) or (ast.priority == 50 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("init memories ")) or (ast.priority == 150 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("init market ")) or (ast.priority == -500 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("define location ")) or (ast.priority == 0 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("location_event ")) or (ast.priority == 100 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("inv_item ")) or (ast.priority == 0 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("goals_block ")) or (ast.priority == 100 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("achievement ")) or (ast.priority == 50 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("person store ")) or (ast.priority == 100 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("define person ")) or (ast.priority == 50 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("layered_image ")) or # Images had their default init priority changed in commit 679f9e31 (Ren'Py 6.99.10). # We don't have any way of detecting this commit, though. The closest one we can # detect is 356c6e34 (Ren'Py 6.99). For any versions in between these, we'll emit # an unnecessary "init 990 " before image statements, but this doesn't affect the AST, # and any other solution would result in incorrect code being generated in some cases. (ast.priority == (500 if self.is_356c6e34_or_later else 990) + self.init_offset and isinstance(ast.block[0], renpy.ast.Image))) and not ( self.should_come_before(ast, ast.block[0])): # If they fulfill this criteria we just print the contained statement self.print_nodes(ast.block) # translatestring statements are split apart and put in an init block. elif (len(ast.block) > 0 and ast.priority == self.init_offset and all(isinstance(i, renpy.ast.TranslateString) for i in ast.block) and all(i.language == ast.block[0].language for i in ast.block[1:])): self.print_nodes(ast.block) else: self.indent() self.write("init") if ast.priority != self.init_offset: self.write(" %d" % (ast.priority - self.init_offset)) if len(ast.block) == 1 and not self.should_come_before(ast, ast.block[0]): self.write(" ") self.skip_indent_until_write = True self.print_nodes(ast.block) else: self.write(":") self.print_nodes(ast.block, 1) finally: self.in_init = in_init def print_say_inside_menu(self): self.print_say(self.say_inside_menu, inmenu=True) self.say_inside_menu = None def print_menu_item(self, label, condition, block, arguments): self.indent() self.write('"%s"' % string_escape(label)) if arguments is not None: self.write(reconstruct_arginfo(arguments)) if block is not None: if isinstance(condition, unicode): self.write(" if %s" % condition) self.write(":") self.print_nodes(block, 1) @dispatch(renpy.ast.Menu) def print_menu(self, ast): self.indent() self.write("menu") if self.label_inside_menu is not None: self.write(" %s" % self.label_inside_menu.name) self.label_inside_menu = None if hasattr(ast, "arguments") and ast.arguments is not None: self.write(reconstruct_arginfo(ast.arguments)) self.write(":") with self.increase_indent(): if ast.with_ is not None: self.indent() self.write("with %s" % ast.with_) if ast.set is not None: self.indent() self.write("set %s" % ast.set) if hasattr(ast, "item_arguments"): item_arguments = ast.item_arguments else: item_arguments = [None] * len(ast.items) for (label, condition, block), arguments in zip(ast.items, item_arguments): if self.translator: label = self.translator.strings.get(label, label) state = None # if the condition is a unicode subclass with a "linenumber" attribute it was script. # If it isn't ren'py used to insert a "True" string. This string used to be of type str # but nowadays it's of time unicode, just not of type PyExpr if isinstance(condition, unicode) and hasattr(condition, "linenumber"): if self.say_inside_menu is not None and condition.linenumber > self.linenumber + 1: # The easy case: we know the line number that the menu item is on, because the condition tells us # So we put the say statement here if there's room for it, or don't if there's not self.print_say_inside_menu() self.advance_to_line(condition.linenumber) elif self.say_inside_menu is not None: # The hard case: we don't know the line number that the menu item is on # So try to put it in, but be prepared to back it out if that puts us behind on the line number state = self.save_state() self.most_lines_behind = self.last_lines_behind self.print_say_inside_menu() self.print_menu_item(label, condition, block, arguments) if state is not None: if self.most_lines_behind > state[7]: # state[7] is the saved value of self.last_lines_behind # We tried to print the say statement that's inside the menu, but it didn't fit here # Undo it and print this item again without it. We'll fit it in later self.rollback_state(state) self.print_menu_item(label, condition, block, arguments) else: self.most_lines_behind = max(state[6], self.most_lines_behind) # state[6] is the saved value of self.most_lines_behind self.commit_state(state) if self.say_inside_menu is not None: # There was no room for this before any of the menu options, so it will just have to go after them all self.print_say_inside_menu() # Programming related functions @dispatch(renpy.ast.Python) def print_python(self, ast, early=False): self.indent() code = ast.code.source if code[0] == '\n': code = code[1:] self.write("python") if early: self.write(" early") if ast.hide: self.write(" hide") if hasattr(ast, "store") and ast.store != "store": self.write(" in ") # Strip prepended "store." self.write(ast.store[6:]) self.write(":") with self.increase_indent(): self.write_lines(split_logical_lines(code)) else: self.write("$ %s" % code) @dispatch(renpy.ast.EarlyPython) def print_earlypython(self, ast): self.print_python(ast, early=True) @dispatch(renpy.ast.Define) @dispatch(renpy.ast.Default) def print_define(self, ast): self.require_init() self.indent() if isinstance(ast, renpy.ast.Default): name = "default" else: name = "define" # If we have an implicit init block with a non-default priority, we need to store the priority here. priority = "" if isinstance(self.parent, renpy.ast.Init): init = self.parent if init.priority != self.init_offset and len(init.block) == 1 and not self.should_come_before(init, ast): priority = " %d" % (init.priority - self.init_offset) index = "" if hasattr(ast, "index") and ast.index is not None: index = "[%s]" % ast.index.source if not hasattr(ast, "store") or ast.store == "store": self.write("%s%s %s%s = %s" % (name, priority, ast.varname, index, ast.code.source)) else: self.write("%s%s %s.%s%s = %s" % (name, priority, ast.store[6:], ast.varname, index, ast.code.source)) # Specials # Returns whether a Say statement immediately preceding a Menu statement # actually belongs inside of the Menu statement. def say_belongs_to_menu(self, say, menu): return (not say.interact and say.who is not None and say.with_ is None and (not hasattr(say, "attributes") or say.attributes is None) and isinstance(menu, renpy.ast.Menu) and menu.items[0][2] is not None and not self.should_come_before(say, menu)) @dispatch(renpy.ast.Say) def print_say(self, ast, inmenu=False): if (not inmenu and self.index + 1 < len(self.block) and self.say_belongs_to_menu(ast, self.block[self.index + 1])): self.say_inside_menu = ast return self.indent() self.write(say_get_code(ast, inmenu)) @dispatch(renpy.ast.UserStatement) def print_userstatement(self, ast): self.indent() self.write(ast.line) if hasattr(ast, "block") and ast.block: with self.increase_indent(): self.print_lex(ast.block) def print_lex(self, lex): for file, linenumber, content, block in lex: self.advance_to_line(linenumber) self.indent() self.write(content) if block: with self.increase_indent(): self.print_lex(block) @dispatch(renpy.ast.Style) def print_style(self, ast): self.require_init() keywords = {ast.linenumber: WordConcatenator(False, True)} # These don't store a line number, so just put them on the first line if ast.parent is not None: keywords[ast.linenumber].append("is %s" % ast.parent) if ast.clear: keywords[ast.linenumber].append("clear") if ast.take is not None: keywords[ast.linenumber].append("take %s" % ast.take) for delname in ast.delattr: keywords[ast.linenumber].append("del %s" % delname) # These do store a line number if ast.variant is not None: if ast.variant.linenumber not in keywords: keywords[ast.variant.linenumber] = WordConcatenator(False) keywords[ast.variant.linenumber].append("variant %s" % ast.variant) for key, value in ast.properties.iteritems(): if value.linenumber not in keywords: keywords[value.linenumber] = WordConcatenator(False) keywords[value.linenumber].append("%s %s" % (key, value)) keywords = sorted([(k, v.join()) for k, v in keywords.items()], key=itemgetter(0)) self.indent() self.write("style %s" % ast.style_name) if keywords[0][1]: self.write(" %s" % keywords[0][1]) if len(keywords) > 1: self.write(":") with self.increase_indent(): for i in keywords[1:]: self.advance_to_line(i[0]) self.indent() self.write(i[1]) # Translation functions @dispatch(renpy.ast.Translate) def print_translate(self, ast): self.indent() self.write("translate %s %s:" % (ast.language or "None", ast.identifier)) self.print_nodes(ast.block, 1) @dispatch(renpy.ast.EndTranslate) def print_endtranslate(self, ast): # an implicitly added node which does nothing... pass @dispatch(renpy.ast.TranslateString) def print_translatestring(self, ast): self.require_init() # Was the last node a translatestrings node? if not(self.index and isinstance(self.block[self.index - 1], renpy.ast.TranslateString) and self.block[self.index - 1].language == ast.language): self.indent() self.write("translate %s strings:" % ast.language or "None") # TranslateString's linenumber refers to the line with "old", not to the # line with "translate %s strings:" with self.increase_indent(): self.advance_to_line(ast.linenumber) self.indent() self.write('old "%s"' % string_escape(ast.old)) if hasattr(ast, 'newloc'): self.advance_to_line(ast.newloc[1]) self.indent() self.write('new "%s"' % string_escape(ast.new)) @dispatch(renpy.ast.TranslateBlock) @dispatch(renpy.ast.TranslateEarlyBlock) def print_translateblock(self, ast): self.indent() self.write("translate %s " % (ast.language or "None")) self.skip_indent_until_write = True in_init = self.in_init if len(ast.block) == 1 and isinstance(ast.block[0], (renpy.ast.Python, renpy.ast.Style)): # Ren'Py counts the TranslateBlock from "translate python" and "translate style" as an Init. self.in_init = True try: self.print_nodes(ast.block) finally: self.in_init = in_init # Screens @dispatch(renpy.ast.Screen) def print_screen(self, ast): self.require_init() screen = ast.screen if isinstance(screen, renpy.screenlang.ScreenLangScreen): self.linenumber = screendecompiler.pprint(self.out_file, screen, self.indent_level, self.linenumber, self.decompile_python, self.skip_indent_until_write, self.printlock) self.skip_indent_until_write = False elif isinstance(screen, renpy.sl2.slast.SLScreen): def print_atl_callback(linenumber, indent_level, atl): old_linenumber = self.linenumber self.linenumber = linenumber with self.increase_indent(indent_level - self.indent_level): self.print_atl(atl) new_linenumber = self.linenumber self.linenumber = old_linenumber return new_linenumber self.linenumber = sl2decompiler.pprint(self.out_file, screen, print_atl_callback, self.indent_level, self.linenumber, self.skip_indent_until_write, self.printlock, self.tag_outside_block) self.skip_indent_until_write = False else: self.print_unknown(screen) # Testcases @dispatch(renpy.ast.Testcase) def print_testcase(self, ast): self.require_init() self.indent() self.write('testcase %s:' % ast.label) self.linenumber = testcasedecompiler.pprint(self.out_file, ast.test.block, self.indent_level + 1, self.linenumber, self.skip_indent_until_write, self.printlock) self.skip_indent_until_write = False
class SL2Decompiler(DecompilerBase): """ An object which handles the decompilation of renpy screen language 2 screens to a given stream """ def __init__(self, print_atl_callback, out_file=None, indentation = ' ', printlock=None, tag_outside_block=False): super(SL2Decompiler, self).__init__(out_file, indentation, printlock) self.print_atl_callback = print_atl_callback self.tag_outside_block = tag_outside_block # This dictionary is a mapping of Class: unbound_method, which is used to determine # what method to call for which slast class dispatch = Dispatcher() def print_node(self, ast): self.advance_to_line(ast.location[1]) self.dispatch.get(type(ast), type(self).print_unknown)(self, ast) @dispatch(sl2.slast.SLScreen) def print_screen(self, ast): # Print the screen statement and create the block self.indent() self.write("screen %s" % ast.name) # If we have parameters, print them. if ast.parameters: self.write(reconstruct_paraminfo(ast.parameters)) # If we're decompiling screencode, print it. Else, insert a pass statement self.print_keywords_and_children(ast.keyword, ast.children, ast.location[1], tag=ast.tag, atl_transform=getattr(ast, 'atl_transform', None)) @dispatch(sl2.slast.SLIf) def print_if(self, ast): # if and showif share a lot of the same infrastructure self._print_if(ast, "if") @dispatch(sl2.slast.SLShowIf) def print_showif(self, ast): # so for if and showif we just call an underlying function with an extra argument self._print_if(ast, "showif") def _print_if(self, ast, keyword): # the first condition is named if or showif, the rest elif keyword = First(keyword, "elif") for condition, block in ast.entries: self.advance_to_line(block.location[1]) self.indent() # if condition is None, this is the else clause if condition is None: self.write("else:") else: self.write("%s %s:" % (keyword(), condition)) # Every condition has a block of type slast.SLBlock if block.keyword or block.children or getattr(block, 'atl_transform', None): self.print_block(block) else: with self.increase_indent(): self.indent() self.write("pass") @dispatch(sl2.slast.SLBlock) def print_block(self, ast): # A block contains possible keyword arguments and a list of child nodes # this is the reason if doesn't keep a list of children but special Blocks self.print_keywords_and_children(ast.keyword, ast.children, None, atl_transform=getattr(ast, 'atl_transform', None)) @dispatch(sl2.slast.SLFor) def print_for(self, ast): # Since tuple unpickling is hard, renpy just gives up and inserts a # $ a,b,c = _sl2_i after the for statement if any tuple unpacking was # attempted in the for statement. Detect this and ignore this slast.SLPython entry if ast.variable == "_sl2_i": variable = ast.children[0].code.source[:-9] children = ast.children[1:] else: variable = ast.variable.strip() + " " children = ast.children self.indent() if hasattr(ast, "index_expression") and ast.index_expression is not None: self.write("for %sindex %s in %s:" % (variable, ast.index_expression, ast.expression)) else: self.write("for %sin %s:" % (variable, ast.expression)) # Interestingly, for doesn't contain a block, but just a list of child nodes self.print_nodes(children, 1) @dispatch(sl2.slast.SLPython) def print_python(self, ast): self.indent() # Extract the source code from the slast.SLPython object. If it starts with a # newline, print it as a python block, else, print it as a $ statement code = ast.code.source if code.startswith("\n"): code = code[1:] self.write("python:") with self.increase_indent(): self.write_lines(split_logical_lines(code)) else: self.write("$ %s" % code) @dispatch(sl2.slast.SLPass) def print_pass(self, ast): # A pass statement self.indent() self.write("pass") @dispatch(sl2.slast.SLUse) def print_use(self, ast): # A use statement requires reconstructing the arguments it wants to pass self.indent() self.write("use ") args = reconstruct_arginfo(ast.args) if isinstance(ast.target, PyExpr): self.write("expression %s" % ast.target) if args: self.write(" pass ") else: self.write("%s" % ast.target) self.write("%s" % reconstruct_arginfo(ast.args)) if hasattr(ast, 'id') and ast.id is not None: self.write(" id %s" % ast.id) if hasattr(ast, 'block') and ast.block: self.write(":") self.print_block(ast.block) @dispatch(sl2.slast.SLTransclude) def print_transclude(self, ast): self.indent() self.write("transclude") @dispatch(sl2.slast.SLDefault) def print_default(self, ast): # A default statement self.indent() self.write("default %s = %s" % (ast.variable, ast.expression)) @dispatch(sl2.slast.SLDisplayable) def print_displayable(self, ast, has_block=False): # slast.SLDisplayable represents a variety of statements. We can figure out # what statement it represents by analyzing the called displayable and style # attributes. key = (ast.displayable, ast.style) nameAndChildren = self.displayable_names.get(key) if nameAndChildren is None: # This is either a displayable we don't know about, or a user-defined displayable # workaround: assume the name of the displayable matches the given style # this is rather often the case. However, as it may be wrong we have to # print a debug message nameAndChildren = (ast.style, 'many') self.print_debug( """Warning: Encountered a user-defined displayable of type '{}'. Unfortunately, the name of user-defined displayables is not recorded in the compiled file. For now the style name '{}' will be substituted. To check if this is correct, find the corresponding renpy.register_sl_displayable call.""".format( ast.displayable, ast.style ) ) (name, children) = nameAndChildren self.indent() self.write(name) if ast.positional: self.write(" " + " ".join(ast.positional)) if hasattr(ast, 'variable'): variable = ast.variable else: variable = None atl_transform = getattr(ast, 'atl_transform', None) # The AST contains no indication of whether or not "has" blocks # were used. We'll use one any time it's possible (except for # directly nesting them, or if they wouldn't contain any children), # since it results in cleaner code. if (not has_block and children == 1 and len(ast.children) == 1 and isinstance(ast.children[0], sl2.slast.SLDisplayable) and ast.children[0].children and (not ast.keyword or ast.children[0].location[1] > ast.keyword[-1][1].linenumber) and (atl_transform is None or ast.children[0].location[1] > atl_transform.loc[1])): self.print_keywords_and_children(ast.keyword, [], ast.location[1], needs_colon=True, variable=variable, atl_transform=atl_transform) self.advance_to_line(ast.children[0].location[1]) with self.increase_indent(): self.indent() self.write("has ") self.skip_indent_until_write = True self.print_displayable(ast.children[0], True) else: self.print_keywords_and_children(ast.keyword, ast.children, ast.location[1], has_block=has_block, variable=variable, atl_transform=atl_transform) displayable_names = { (behavior.OnEvent, None): ("on", 0), (behavior.OnEvent, 0): ("on", 0), (behavior.MouseArea, 0): ("mousearea", 0), (behavior.MouseArea, None): ("mousearea", 0), (ui._add, None): ("add", 0), (sld.sl2add, None): ("add", 0), (ui._hotbar, "hotbar"): ("hotbar", 0), (sld.sl2vbar, None): ("vbar", 0), (sld.sl2bar, None): ("bar", 0), (ui._label, "label"): ("label", 0), (ui._textbutton, 0): ("textbutton", 0), (ui._textbutton, "button"): ("textbutton", 0), (ui._imagebutton, "image_button"): ("imagebutton", 0), (im.image, "default"): ("image", 0), (behavior.Input, "input"): ("input", 0), (behavior.Timer, "default"): ("timer", 0), (ui._key, None): ("key", 0), (text.Text, "text"): ("text", 0), (layout.Null, "default"): ("null", 0), (dragdrop.Drag, None): ("drag", 1), (dragdrop.Drag, "drag"): ("drag", 1), (motion.Transform, "transform"): ("transform", 1), (ui._hotspot, "hotspot"): ("hotspot", 1), (sld.sl2viewport, "viewport"): ("viewport", 1), (behavior.Button, "button"): ("button", 1), (layout.Window, "frame"): ("frame", 1), (layout.Window, "window"): ("window", 1), (dragdrop.DragGroup, None): ("draggroup", 'many'), (ui._imagemap, "imagemap"): ("imagemap", 'many'), (layout.Side, "side"): ("side", 'many'), (layout.Grid, "grid"): ("grid", 'many'), (sld.sl2vpgrid, "vpgrid"): ("vpgrid", 'many'), (layout.MultiBox, "fixed"): ("fixed", 'many'), (layout.MultiBox, "vbox"): ("vbox", 'many'), (layout.MultiBox, "hbox"): ("hbox", 'many') } def print_keywords_and_children(self, keywords, children, lineno, needs_colon=False, has_block=False, tag=None, variable=None, atl_transform=None): # This function prints the keyword arguments and child nodes # Used in a displayable screen statement # If lineno is None, we're already inside of a block. # Otherwise, we're on the line that could start a block. wrote_colon = False keywords_by_line = [] current_line = (lineno, []) keywords_somewhere = [] # These can go anywhere inside the block that there's room. if variable is not None: if current_line[0] is None: keywords_somewhere.extend(("as", variable)) else: current_line[1].extend(("as", variable)) if tag is not None: if current_line[0] is None or not self.tag_outside_block: keywords_somewhere.extend(("tag", tag)) else: current_line[1].extend(("tag", tag)) for key, value in keywords: if value is None: value = "" if current_line[0] is None: keywords_by_line.append(current_line) current_line = (0, []) elif current_line[0] is None or value.linenumber > current_line[0]: keywords_by_line.append(current_line) current_line = (value.linenumber, []) current_line[1].extend((key, value)) if keywords_by_line: # Easy case: we have at least one line inside the block that already has keywords. # Just put the ones from keywords_somewhere with them. current_line[1].extend(keywords_somewhere) keywords_somewhere = [] keywords_by_line.append(current_line) last_keyword_line = keywords_by_line[-1][0] children_with_keywords = [] children_after_keywords = [] for i in children: if i.location[1] > last_keyword_line: children_after_keywords.append(i) else: children_with_keywords.append((i.location[1], i)) # the keywords in keywords_by_line[0] go on the line that starts the # block, not in it block_contents = sorted(keywords_by_line[1:] + children_with_keywords, key=itemgetter(0)) if keywords_by_line[0][1]: # this never happens if lineno was None self.write(" %s" % ' '.join(keywords_by_line[0][1])) if keywords_somewhere: # this never happens if there's anything in block_contents # Hard case: we need to put a keyword somewhere inside the block, but we have no idea which line to put it on. if lineno is not None: self.write(":") wrote_colon = True for index, child in enumerate(children_after_keywords): if child.location[1] > self.linenumber + 1: # We have at least one blank line before the next child. Put the keywords here. with self.increase_indent(): self.indent() self.write(' '.join(keywords_somewhere)) self.print_nodes(children_after_keywords[index:], 0 if has_block else 1) break with self.increase_indent(): # Even if we're in a "has" block, we need to indent this child since there will be a keyword line after it. self.print_node(child) else: # No blank lines before any children, so just put the remaining keywords at the end. with self.increase_indent(): self.indent() self.write(' '.join(keywords_somewhere)) else: if block_contents or (not has_block and children_after_keywords): if lineno is not None: self.write(":") wrote_colon = True with self.increase_indent(): for i in block_contents: if isinstance(i[1], list): self.advance_to_line(i[0]) self.indent() self.write(' '.join(i[1])) else: self.print_node(i[1]) elif needs_colon: self.write(":") wrote_colon = True self.print_nodes(children_after_keywords, 0 if has_block else 1) if atl_transform is not None: # "at transform:", possibly preceded by other keywords, and followed by an ATL block # TODO this doesn't always go at the end. Use line numbers to figure out where it goes if not wrote_colon and lineno is not None: self.write(":") wrote_colon = True with self.increase_indent(): self.indent() self.write("at transform:") self.linenumber = self.print_atl_callback(self.linenumber, self.indent_level, atl_transform)
class Decompiler(DecompilerBase): """ An object which hanldes the decompilation of renpy asts to a given stream """ # This dictionary is a mapping of Class: unbount_method, which is used to determine # what method to call for which ast class dispatch = Dispatcher() def __init__(self, out_file=None, decompile_python=False, indentation=' ', printlock=None): super(Decompiler, self).__init__(out_file, indentation, printlock) self.decompile_python = decompile_python self.paired_with = False self.say_inside_menu = None self.label_inside_menu = None def dump(self, ast, indent_level=0): # skip_indent_until_write avoids an initial blank line super(Decompiler, self).dump(ast, indent_level, skip_indent_until_write=True) self.write( "\n# Decompiled by unrpyc: https://github.com/CensoredUsername/unrpyc\n" ) def print_node(self, ast): # We special-case line advancement for TranslateString in its print # method, so don't advance lines for it here. if hasattr(ast, 'linenumber') and not isinstance( ast, renpy.ast.TranslateString): self.advance_to_line(ast.linenumber) # It doesn't matter what line "block:" is on. The loc of a RawBlock # refers to the first statement inside the block, which we advance # to from print_atl. elif hasattr(ast, 'loc') and not isinstance(ast, renpy.atl.RawBlock): self.advance_to_line(ast.loc[1]) func = self.dispatch.get(type(ast), None) if func: func(self, ast) else: # This node type is unknown self.print_unknown(ast) # ATL printing functions def print_atl(self, ast): self.advance_to_line(ast.loc[1]) self.indent_level += 1 if ast.statements: self.print_nodes(ast.statements) # If a statement ends with a colon but has no block after it, loc will # get set to ('', 0). That isn't supposed to be valid syntax, but it's # the only thing that can generate that. elif ast.loc != ('', 0): self.indent() self.write("pass") self.indent_level -= 1 @dispatch(renpy.atl.RawMultipurpose) def print_atl_rawmulti(self, ast): self.indent() warp_words = WordConcatenator(False) # warpers if ast.warp_function: warp_words.append("warp", ast.warp_function, ast.duration) elif ast.warper: warp_words.append(ast.warper, ast.duration) elif ast.duration != "0": warp_words.append("pause", ast.duration) warp = warp_words.join() words = WordConcatenator(warp and warp[-1] != ' ', True) # revolution if ast.revolution: words.append(ast.revolution) # circles if ast.circles != "0": words.append("circles %s" % ast.circles) # splines spline_words = WordConcatenator(False) for name, expressions in ast.splines: spline_words.append(name) for expression in expressions: spline_words.append("knot", expression) words.append(spline_words.join()) # properties property_words = WordConcatenator(False) for key, value in ast.properties: property_words.append(key, value) words.append(property_words.join()) # with expression_words = WordConcatenator(False) # TODO There's a lot of cases where pass isn't needed, since we could # reorder stuff so there's never 2 expressions in a row. (And it's never # necessary for the last one, but we don't know what the last one is # since it could get reordered.) needs_pass = len(ast.expressions) > 1 for (expression, with_expression) in ast.expressions: expression_words.append(expression) if with_expression: expression_words.append("with", with_expression) if needs_pass: expression_words.append("pass") words.append(expression_words.join()) self.write(warp + words.join()) @dispatch(renpy.atl.RawBlock) def print_atl_rawblock(self, ast): self.indent() self.write("block:") self.print_atl(ast) @dispatch(renpy.atl.RawChild) def print_atl_rawchild(self, ast): for child in ast.children: self.indent() self.write("contains:") self.print_atl(child) @dispatch(renpy.atl.RawChoice) def print_atl_rawchoice(self, ast): for chance, block in ast.choices: self.indent() self.write("choice") if chance != "1.0": self.write(" %s" % chance) self.write(":") self.print_atl(block) if (self.index + 1 < len(self.block) and isinstance( self.block[self.index + 1], renpy.atl.RawChoice)): self.indent() self.write("pass") @dispatch(renpy.atl.RawContainsExpr) def print_atl_rawcontainsexpr(self, ast): self.indent() self.write("contains %s" % ast.expression) @dispatch(renpy.atl.RawEvent) def print_atl_rawevent(self, ast): self.indent() self.write("event %s" % ast.name) @dispatch(renpy.atl.RawFunction) def print_atl_rawfunction(self, ast): self.indent() self.write("function %s" % ast.expr) @dispatch(renpy.atl.RawOn) def print_atl_rawon(self, ast): for name, block in sorted(ast.handlers.items(), key=lambda i: i[1].loc[1]): self.indent() self.write("on %s:" % name) self.print_atl(block) @dispatch(renpy.atl.RawParallel) def print_atl_rawparallel(self, ast): for block in ast.blocks: self.indent() self.write("parallel:") self.print_atl(block) if (self.index + 1 < len(self.block) and isinstance( self.block[self.index + 1], renpy.atl.RawParallel)): self.indent() self.write("pass") @dispatch(renpy.atl.RawRepeat) def print_atl_rawrepeat(self, ast): self.indent() self.write("repeat") if ast.repeats: self.write(" %s" % ast.repeats) # not sure if this is even a string @dispatch(renpy.atl.RawTime) def print_atl_rawtime(self, ast): self.indent() self.write("time %s" % ast.time) # Displayable related functions def print_imspec(self, imspec): if imspec[1] is not None: begin = "expression %s" % imspec[1] else: begin = " ".join(imspec[0]) words = WordConcatenator(begin and begin[-1] != ' ', True) if imspec[2] is not None: words.append("as %s" % imspec[2]) if len(imspec[6]) > 0: words.append("behind %s" % ', '.join(imspec[6])) if imspec[4] != "master": words.append("onlayer %s" % imspec[4]) if imspec[5] is not None: words.append("zorder %s" % imspec[5]) if len(imspec[3]) > 0: words.append("at %s" % ', '.join(imspec[3])) self.write(begin + words.join()) return words.needs_space @dispatch(renpy.ast.Image) def print_image(self, ast): self.indent() self.write("image %s" % ' '.join(ast.imgname)) if ast.code is not None: self.write(" = %s" % ast.code.source) else: if hasattr(ast, "atl") and ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.Transform) def print_transform(self, ast): self.indent() self.write("transform %s" % ast.varname) if ast.parameters is not None: self.write(reconstruct_paraminfo(ast.parameters)) if hasattr(ast, "atl") and ast.atl is not None: self.write(":") self.print_atl(ast.atl) # Directing related functions @dispatch(renpy.ast.Show) def print_show(self, ast): self.indent() self.write("show ") needs_space = self.print_imspec(ast.imspec) if self.paired_with: if needs_space: self.write(" ") self.write("with %s" % self.paired_with) self.paired_with = True if hasattr(ast, "atl") and ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.ShowLayer) def print_showlayer(self, ast): self.indent() self.write("show layer %s" % ast.layer) if ast.at_list: self.write(" at %s" % ', '.join(ast.at_list)) if hasattr(ast, "atl") and ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.Scene) def print_scene(self, ast): self.indent() self.write("scene") if ast.imspec is None: if ast.layer != "master": self.write(" onlayer %s" % ast.layer) needs_space = True else: self.write(" ") needs_space = self.print_imspec(ast.imspec) if self.paired_with: if needs_space: self.write(" ") self.write("with %s" % self.paired_with) self.paired_with = True if hasattr(ast, "atl") and ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.Hide) def print_hide(self, ast): self.indent() self.write("hide ") needs_space = self.print_imspec(ast.imspec) if self.paired_with: if needs_space: self.write(" ") self.write("with %s" % self.paired_with) self.paired_with = True @dispatch(renpy.ast.With) def print_with(self, ast): # the 'paired' attribute indicates that this with # and with node afterwards are part of a postfix # with statement. detect this and process it properly if hasattr(ast, "paired") and ast.paired is not None: # Sanity check. check if there's a matching with statement two nodes further if not (isinstance(self.block[self.index + 2], renpy.ast.With) and self.block[self.index + 2].expr == ast.paired): raise Exception("Unmatched paired with {0} != {1}".format( repr(self.paired_with), repr(ast.expr))) self.paired_with = ast.paired elif self.paired_with: # Check if it was consumed by a show/scene statement if self.paired_with is not True: self.write(" with %s" % ast.expr) self.paired_with = False else: self.indent() self.write("with %s" % ast.expr) self.paired_with = False # Flow control @dispatch(renpy.ast.Label) def print_label(self, ast): # If a Call block preceded us, it printed us as "from" if (self.index and isinstance(self.block[self.index - 1], renpy.ast.Call)): return remaining_blocks = len(self.block) - self.index # See if we're the label for a menu, rather than a standalone label. if remaining_blocks > 1 and not ast.block and ast.parameters is None: next_ast = self.block[self.index + 1] if (hasattr(next_ast, 'linenumber') and next_ast.linenumber == ast.linenumber and (isinstance(next_ast, renpy.ast.Menu) or (remaining_blocks > 2 and isinstance(next_ast, renpy.ast.Say) and self.say_belongs_to_menu(next_ast, self.block[self.index + 2])))): self.label_inside_menu = ast return self.indent() self.write("label %s%s%s:" % (ast.name, reconstruct_paraminfo(ast.parameters), " hide" if hasattr(ast, 'hide') and ast.hide else "")) self.print_nodes(ast.block, 1) @dispatch(renpy.ast.Jump) def print_jump(self, ast): self.indent() self.write("jump ") if ast.expression: self.write("expression %s" % ast.target) else: self.write(ast.target) @dispatch(renpy.ast.Call) def print_call(self, ast): self.indent() words = WordConcatenator(False) words.append("call") if ast.expression: words.append("expression") words.append(ast.label) if ast.arguments is not None: if ast.expression: words.append("pass") words.append(reconstruct_arginfo(ast.arguments)) # We don't have to check if there's enough elements here, # since a Label or a Pass is always emitted after a Call. next_block = self.block[self.index + 1] if isinstance(next_block, renpy.ast.Label): words.append("from %s" % next_block.name) self.write(words.join()) @dispatch(renpy.ast.Return) def print_return(self, ast): if (ast.expression is None and self.parent is None and self.index + 1 == len(self.block) and self.index and ast.linenumber == self.block[self.index - 1].linenumber): # As of Ren'Py commit 356c6e34, a return statement is added to # the end of each rpyc file. Don't include this in the source. return self.indent() self.write("return") if ast.expression is not None: self.write(" %s" % ast.expression) @dispatch(renpy.ast.If) def print_if(self, ast): statement = First("if %s:", "elif %s:") for i, (condition, block) in enumerate(ast.entries): # The non-Unicode string "True" is the condition for else:. if (i + 1) == len(ast.entries) and isinstance(condition, str): self.indent() self.write("else:") else: self.advance_to_line(condition.linenumber) self.indent() self.write(statement() % condition) self.print_nodes(block, 1) @dispatch(renpy.ast.While) def print_while(self, ast): self.indent() self.write("while %s:" % ast.condition) self.print_nodes(ast.block, 1) @dispatch(renpy.ast.Pass) def print_pass(self, ast): if (self.index and isinstance(self.block[self.index - 1], renpy.ast.Call)): return if (self.index > 1 and isinstance(self.block[self.index - 2], renpy.ast.Call) and isinstance(self.block[self.index - 1], renpy.ast.Label) and self.block[self.index - 2].linenumber == ast.linenumber): return self.indent() self.write("pass") def should_come_before(self, first, second): return first.linenumber < second.linenumber @dispatch(renpy.ast.Init) def print_init(self, ast): # A bunch of statements can have implicit init blocks # Define has a default priority of 0, screen of -500 and image of 990 if len(ast.block) == 1 and ( (ast.priority == -500 and isinstance(ast.block[0], renpy.ast.Screen)) or (ast.priority == 0 and isinstance(ast.block[0], (renpy.ast.Define, renpy.ast.Default, renpy.ast.Transform, renpy.ast.Style))) or (ast.priority == 990 and isinstance(ast.block[0], renpy.ast.Image)) ) and not (self.should_come_before(ast, ast.block[0])): # If they fulfil this criteria we just print the contained statement self.print_nodes(ast.block) # translatestring statements are split apart and put in an init block. elif (len(ast.block) > 0 and ast.priority == 0 and all( isinstance(i, renpy.ast.TranslateString) for i in ast.block) and all(i.language == ast.block[0].language for i in ast.block[1:])): self.print_nodes(ast.block) else: self.indent() self.write("init") if ast.priority: self.write(" %d" % ast.priority) if len(ast.block ) == 1 and ast.linenumber >= ast.block[0].linenumber: self.write(" ") self.skip_indent_until_write = True self.print_nodes(ast.block) else: self.write(":") self.print_nodes(ast.block, 1) @dispatch(renpy.ast.Menu) def print_menu(self, ast): self.indent() self.write("menu") if self.label_inside_menu is not None: self.write(" %s" % self.label_inside_menu.name) self.label_inside_menu = None self.write(":") self.indent_level += 1 if self.say_inside_menu is not None: self.print_say(self.say_inside_menu, inmenu=True) self.say_inside_menu = None if ast.with_ is not None: self.indent() self.write("with %s" % ast.with_) if ast.set is not None: self.indent() self.write("set %s" % ast.set) for label, condition, block in ast.items: if isinstance(condition, unicode): self.advance_to_line(condition.linenumber) self.indent() self.write('"%s"' % string_escape(label)) if block is not None: if isinstance(condition, unicode): self.write(" if %s" % condition) self.write(":") self.print_nodes(block, 1) self.indent_level -= 1 # Programming related functions @dispatch(renpy.ast.Python) def print_python(self, ast, early=False): self.indent() code = ast.code.source if code[0] == '\n': code = code[1:] self.write("python") if early: self.write(" early") if ast.hide: self.write(" hide") if hasattr(ast, "store") and ast.store != "store": self.write(" in ") # Strip prepended "store." self.write(ast.store[6:]) self.write(":") self.indent_level += 1 self.write_lines(split_logical_lines(code)) self.indent_level -= 1 else: self.write("$ %s" % code) @dispatch(renpy.ast.EarlyPython) def print_earlypython(self, ast): self.print_python(ast, early=True) @dispatch(renpy.ast.Define) @dispatch(renpy.ast.Default) def print_define(self, ast): self.indent() if isinstance(ast, renpy.ast.Default): name = "default" else: name = "define" if not hasattr(ast, "store") or ast.store == "store": self.write("%s %s = %s" % (name, ast.varname, ast.code.source)) else: self.write("%s %s.%s = %s" % (name, ast.store[6:], ast.varname, ast.code.source)) # Specials # Returns whether a Say statement immediately preceding a Menu statement # actually belongs inside of the Menu statement. def say_belongs_to_menu(self, say, menu): return (not say.interact and say.who is not None and say.with_ is None and (not hasattr(say, "attributes") or say.attributes is None) and isinstance(menu, renpy.ast.Menu) and menu.items[0][2] is not None and not self.should_come_before(say, menu)) @dispatch(renpy.ast.Say) def print_say(self, ast, inmenu=False): if (not inmenu and self.index + 1 < len(self.block) and self.say_belongs_to_menu(ast, self.block[self.index + 1])): self.say_inside_menu = ast return self.indent() if ast.who is not None: self.write("%s " % ast.who) if hasattr(ast, 'attributes') and ast.attributes is not None: for i in ast.attributes: self.write("%s " % i) self.write('"%s"' % string_escape(ast.what)) if not ast.interact and not inmenu: self.write(" nointeract") if ast.with_ is not None: self.write(" with %s" % ast.with_) @dispatch(renpy.ast.UserStatement) def print_userstatement(self, ast): self.indent() self.write(ast.line) @dispatch(renpy.ast.Style) def print_style(self, ast): keywords = {ast.linenumber: WordConcatenator(False, True)} # These don't store a line number, so just put them on the first line if ast.parent is not None: keywords[ast.linenumber].append("is %s" % ast.parent) if ast.clear: keywords[ast.linenumber].append("clear") if ast.take is not None: keywords[ast.linenumber].append("take %s" % ast.take) for delname in ast.delattr: keywords[ast.linenumber].append("del %s" % delname) # These do store a line number if ast.variant is not None: if ast.variant.linenumber not in keywords: keywords[ast.variant.linenumber] = WordConcatenator(False) keywords[ast.variant.linenumber].append("variant %s" % ast.variant) for key, value in ast.properties.iteritems(): if value.linenumber not in keywords: keywords[value.linenumber] = WordConcatenator(False) keywords[value.linenumber].append("%s %s" % (key, value)) keywords = sorted([(k, v.join()) for k, v in keywords.items()], key=itemgetter(0)) self.indent() self.write("style %s" % ast.style_name) if keywords[0][1]: self.write(" %s" % keywords[0][1]) if len(keywords) > 1: self.write(":") self.indent_level += 1 for i in keywords[1:]: self.advance_to_line(i[0]) self.indent() self.write(i[1]) self.indent_level -= 1 # Translation functions @dispatch(renpy.ast.Translate) def print_translate(self, ast): self.indent() self.write("translate %s %s:" % (ast.language or "None", ast.identifier)) self.print_nodes(ast.block, 1) @dispatch(renpy.ast.EndTranslate) def print_endtranslate(self, ast): # an implicitly added node which does nothing... pass @dispatch(renpy.ast.TranslateString) def print_translatestring(self, ast): # Was the last node a translatestrings node? if not (self.index and isinstance(self.block[self.index - 1], renpy.ast.TranslateString) and self.block[self.index - 1].language == ast.language): self.indent() self.write("translate %s strings:" % ast.language or "None") # TranslateString's linenumber refers to the line with "old", not to the # line with "translate %s strings:" self.advance_to_line(ast.linenumber) self.indent_level += 1 self.indent() self.write('old "%s"' % string_escape(ast.old)) self.indent() self.write('new "%s"' % string_escape(ast.new)) self.indent_level -= 1 @dispatch(renpy.ast.TranslateBlock) def print_translateblock(self, ast): self.indent() self.write("translate %s " % (ast.language or "None")) self.skip_indent_until_write = True self.print_nodes(ast.block) # Screens @dispatch(renpy.ast.Screen) def print_screen(self, ast): screen = ast.screen if isinstance(screen, renpy.screenlang.ScreenLangScreen): self.linenumber = screendecompiler.pprint( self.out_file, screen, self.indent_level, self.linenumber, self.decompile_python, self.skip_indent_until_write) self.skip_indent_until_write = False elif isinstance(screen, renpy.sl2.slast.SLScreen): self.linenumber = sl2decompiler.pprint( self.out_file, screen, self.indent_level, self.linenumber, self.skip_indent_until_write) self.skip_indent_until_write = False else: self.print_unknown(screen)
class SLDecompiler(DecompilerBase): """ an object which handles the decompilation of renpy screen language 1 screens to a given stream """ # This dictionary is a mapping of string: unbound_method, which is used to determine # what method to call for which statement dispatch = Dispatcher() def __init__(self, out_file=None, decompile_python=False, indentation=" ", printlock=None): super(SLDecompiler, self).__init__(out_file, indentation, printlock) self.decompile_python = decompile_python self.should_advance_to_line = True self.is_root = True def dump(self, ast, indent_level=0, linenumber=1, skip_indent_until_write=False): self.indent_level = indent_level self.linenumber = linenumber self.skip_indent_until_write = skip_indent_until_write self.print_screen(ast) return self.linenumber def advance_to_line(self, linenumber): if self.should_advance_to_line: super(SLDecompiler, self).advance_to_line(linenumber) def save_state(self): return (super(SLDecompiler, self).save_state(), self.should_advance_to_line, self.is_root) def commit_state(self, state): super(SLDecompiler, self).commit_state(state[0]) def rollback_state(self, state): self.should_advance_to_line = state[1] self.is_root = state[2] super(SLDecompiler, self).rollback_state(state[0]) def to_source(self, node): return codegen.to_source(node, self.indentation, False, True) @contextmanager def not_root(self): # Whenever anything except screen itself prints any child nodes, it # should be inside a "with self.not_root()" block. It doesn't matter if # you catch more inside of the with block than you need, as long as you # don't fall back to calling print_python() from inside it. is_root = self.is_root self.is_root = False try: yield finally: self.is_root = is_root # Entry point functions def print_screen(self, ast): # Here we do the processing of the screen statement, and we # switch over to parsing of the python string representation # Print the screen statement and create the block self.indent() self.write("screen %s" % ast.name) # If we have parameters, print them. if hasattr(ast, "parameters") and ast.parameters: self.write(reconstruct_paraminfo(ast.parameters)) if ast.tag: self.write(" tag %s" % ast.tag) keywords = {ast.code.location[1]: WordConcatenator(False, True)} for key in ('modal', 'zorder', 'variant', 'predict'): value = getattr(ast, key) # Non-Unicode strings are default values rather than user-supplied # values, so we don't need to write them out. if isinstance(value, unicode): if value.linenumber not in keywords: keywords[value.linenumber] = WordConcatenator(False, True) keywords[value.linenumber].append("%s %s" % (key, value)) keywords = sorted([(k, v.join()) for k, v in keywords.items()], key=itemgetter(0)) # so the first one is right if self.decompile_python: self.print_keywords_and_nodes(keywords, None, True) with self.increase_indent(): self.indent() self.write("python:") with self.increase_indent(): # The first line is always "_1 = (_name, 0)", which gets included # even if the python: block is the only thing in the screen. Don't # include ours, since if we do, it'll be included twice when # recompiled. self.write_lines( self.to_source(ast.code.source).splitlines()[1:]) else: self.print_keywords_and_nodes(keywords, ast.code.source.body, False) def split_nodes_at_headers(self, nodes): if not nodes: return [] rv = [nodes[:1]] parent_id = self.parse_header(nodes[0]) if parent_id is None: raise Exception( "First node passed to split_nodes_at_headers was not a header") for i in nodes[1:]: if self.parse_header(i) == parent_id: rv.append([i]) header = i else: rv[-1].append(i) return rv def print_nodes(self, nodes, extra_indent=0, has_block=False): # Print a block of statements, splitting it up on one level. # The screen language parser emits lines in the shape _0 = (_0, 0) from which indentation can be revealed. # It translates roughly to "id = (parent_id, index_in_parent_children)". When parsing a block # parse the first header line to find the parent_id, and then split around headers with the same parent id # in this block. if has_block and not nodes: raise BadHasBlockException() split = self.split_nodes_at_headers(nodes) with self.increase_indent(extra_indent): for i in split: self.print_node(i[0], i[1:], has_block) def get_first_line(self, nodes): if self.get_dispatch_key(nodes[0]): return nodes[0].value.lineno elif self.is_renpy_for(nodes): return nodes[1].target.lineno elif self.is_renpy_if(nodes): return nodes[0].test.lineno else: # We should never get here, but just in case... return nodes[0].lineno def make_printable_keywords(self, keywords, lineno): keywords = [(i.arg, simple_expression_guard(self.to_source(i.value)), i.value.lineno) for i in keywords if not (isinstance(i.value, ast.Name) and ( (i.arg == 'id' and i.value.id.startswith('_')) or (i.arg == 'scope' and i.value.id == '_scope')))] # Sort the keywords according to what line they belong on # The first element always exists for the line the block starts on, # even if there's no keywords that go on it keywords_by_line = [] current_line = [] for i in keywords: if i[2] > lineno: keywords_by_line.append((lineno, ' '.join(current_line))) lineno = i[2] current_line = [] current_line.extend(i[:2]) keywords_by_line.append((lineno, ' '.join(current_line))) return keywords_by_line def print_keywords_and_nodes(self, keywords, nodes, needs_colon): # Keywords and child nodes can be mixed with each other, so they need # to be printed at the same time. This function takes each list and # combines them into one, then prints it. # # This function assumes line numbers of nodes before keywords are # correct, which is the case for the "screen" statement itself. if keywords: if keywords[0][1]: self.write(" %s" % keywords[0][1]) if len(keywords) != 1: needs_colon = True if nodes: nodelists = [(self.get_first_line(i[1:]), i) for i in self.split_nodes_at_headers(nodes)] needs_colon = True else: nodelists = [] if needs_colon: self.write(":") stuff_to_print = sorted(keywords[1:] + nodelists, key=itemgetter(0)) with self.increase_indent(): for i in stuff_to_print: # Nodes are lists. Keywords are ready-to-print strings. if type(i[1]) == list: self.print_node(i[1][0], i[1][1:]) else: self.advance_to_line(i[0]) self.indent() self.write(i[1]) def get_lines_used_by_node(self, node): state = self.save_state() self.print_node(node[0], node[1:]) linenumber = self.linenumber self.rollback_state(state) return linenumber - self.linenumber def print_buggy_keywords_and_nodes(self, keywords, nodes, needs_colon, has_block): # Keywords and child nodes can be mixed with each other, so they need # to be printed at the same time. This function takes each list and # combines them into one, then prints it. # # This function assumes line numbers of nodes before keywords are # incorrect, which is the case for everything except the "screen" # statement itself. last_keyword_lineno = None if keywords: if keywords[0][1]: self.write(" %s" % keywords[0][1]) remaining_keywords = keywords[1:] if remaining_keywords: needs_colon = True last_keyword_lineno = remaining_keywords[-1][0] if nodes: nodelists = [(self.get_first_line(i[1:]), i) for i in self.split_nodes_at_headers(nodes)] else: nodelists = [] for key, value in enumerate(nodelists): if last_keyword_lineno is None or value[0] > last_keyword_lineno: nodes_before_keywords = nodelists[:key] nodes_after_keywords = nodelists[key:] break else: nodes_before_keywords = nodelists nodes_after_keywords = [] if nodes_before_keywords or (not has_block and nodes_after_keywords): needs_colon = True if needs_colon: self.write(":") with self.increase_indent(): should_advance_to_line = self.should_advance_to_line self.should_advance_to_line = False while nodes_before_keywords: if not remaining_keywords: # Something went wrong. We already printed the last keyword, # yet there's still nodes left that should have been printed # before the last keyword. Just print them now. for i in nodes_before_keywords: self.print_node(i[1][0], i[1][1:]) break # subtract 1 line since .indent() uses 1 lines_to_go = remaining_keywords[0][0] - self.linenumber - 1 next_node = nodes_before_keywords[0][1] if lines_to_go >= self.get_lines_used_by_node(next_node): self.print_node(next_node[0], next_node[1:]) nodes_before_keywords.pop(0) elif not should_advance_to_line or lines_to_go <= 0: self.indent() self.write(remaining_keywords.pop(0)[1]) else: self.write("\n" * lines_to_go) self.should_advance_to_line = should_advance_to_line for i in remaining_keywords: self.advance_to_line(i[0]) self.indent() self.write(i[1]) with self.increase_indent(1 if not has_block else 0): for i in nodes_after_keywords: self.print_node(i[1][0], i[1][1:]) def get_dispatch_key(self, node): if (isinstance(node, ast.Expr) and isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Attribute) and isinstance(node.value.func.value, ast.Name)): return node.value.func.value.id, node.value.func.attr else: return None def print_node(self, header, code, has_block=False): # Here we derermine how to handle a statement. # To do this we look at how the first line in the statement code starts, after the header. # Then we call the appropriate function as specified in ui_function_dict. # If the statement is unknown, we can still emit valid screen code by just # stuffing it inside a python block. # There's 3 categories of things that we can convert to screencode: # if statements, for statements, and function calls of the # form "first.second(...)". Anything else gets converted to Python. dispatch_key = self.get_dispatch_key(code[0]) if dispatch_key: func = self.dispatch.get(dispatch_key, self.print_python.__func__) if has_block: if func not in (self.print_onechild.__func__, self.print_manychildren.__func__): raise BadHasBlockException() func(self, header, code, True) else: func(self, header, code) elif has_block: raise BadHasBlockException() elif self.is_renpy_for(code): self.print_for(header, code) elif self.is_renpy_if(code): self.print_if(header, code) else: self.print_python(header, code) # Helper printing functions def print_args(self, node): if node.args: self.write(" " + " ".join([ simple_expression_guard(self.to_source(i)) for i in node.args ])) # Node printing functions def print_python(self, header, code): # This function handles any statement which is a block but couldn't logically be # Translated to a screen statement. # # Ren'Py's line numbers are really, really buggy. Here's a summary: # If we're not directly under the root screen, and a keyword for our # parent follows us, then all of our line numbers will be equal to the # line number of that keyword. # If we're not directly under the root screen, and no keywords for our # parent follow us, then header.lineno is the line number of whatever # it is that preceded us (which is completely useless). # If we're directly under the root "screen", then header.lineno is the # line that "$" or "python:" appeared on. # If we're not a child followed by a keyword, and "$" was used, then # code[0].lineno is the line that the code actually starts on, but if # "python:" was used, then all of code's line numbers will be 1 greater # than the line each one should be. source = self.to_source( ast.Module(body=code, lineno=code[0].lineno, col_offset=0)).rstrip().lstrip('\n') lines = source.splitlines() if len(split_logical_lines(source)) == 1 and ( (not self.is_root and code[0].lineno < self.linenumber + 3) or header.lineno >= code[0].lineno): # This is only one logical line, so it's possible that it was $, # and either it's not in the root (so we don't know what the # original source used), or it is in the root and we know it used $. # Also, if we don't know for sure what was used, but we have enough # room to use a "python" block, then use it instead, since it'll # result in everything taking up one fewer line (since it'll use # one more, but start two sooner). self.advance_to_line(code[0].lineno) self.indent() self.write("$ %s" % lines[0]) self.write_lines(lines[1:]) else: # Either this is more than one logical line, so it has to be a # python block, or it was in the root and we can tell that it was # originally a python block. if self.is_root: self.advance_to_line(header.lineno) self.indent() self.write("python:") self.advance_to_line(code[0].lineno - 1) with self.increase_indent(): self.write_lines(lines) def is_renpy_if(self, nodes): return len(nodes) == 1 and isinstance(nodes[0], ast.If) and ( nodes[0].body and self.parse_header(nodes[0].body[0])) and ( not nodes[0].orelse or self.is_renpy_if(nodes[0].orelse) or self.parse_header(nodes[0].orelse[0])) def is_renpy_for(self, nodes): return (len(nodes) == 2 and isinstance(nodes[0], ast.Assign) and len(nodes[0].targets) == 1 and isinstance(nodes[0].targets[0], ast.Name) and re.match(r"_[0-9]+$", nodes[0].targets[0].id) and isinstance(nodes[0].value, ast.Num) and nodes[0].value.n == 0 and isinstance(nodes[1], ast.For) and not nodes[1].orelse and nodes[1].body and self.parse_header(nodes[1].body[0]) and isinstance(nodes[1].body[-1], ast.AugAssign) and isinstance(nodes[1].body[-1].op, ast.Add) and isinstance(nodes[1].body[-1].target, ast.Name) and re.match(r"_[0-9]+$", nodes[1].body[-1].target.id) and isinstance(nodes[1].body[-1].value, ast.Num) and nodes[1].body[-1].value.n == 1) def strip_parens(self, text): if text and text[0] == '(' and text[-1] == ')': return text[1:-1] else: return text def print_if(self, header, code): # Here we handle the if statement. It might be valid python but we can check for this by # checking for the header that should normally occur within the if statement. # The if statement parser might also generate a second header if there's more than one screen # statement enclosed in the if/elif/else statements. We'll take care of that too. self.advance_to_line(self.get_first_line(code)) self.indent() self.write("if %s:" % self.strip_parens(self.to_source(code[0].test))) if (len(code[0].body) >= 2 and self.parse_header(code[0].body[0]) and self.parse_header(code[0].body[1])): body = code[0].body[1:] else: body = code[0].body with self.not_root(): self.print_nodes(body, 1) if code[0].orelse: if self.is_renpy_if(code[0].orelse): self.advance_to_line(code[0].orelse[0].test.lineno) self.indent() self.write("el") # beginning of "elif" self.skip_indent_until_write = True self.print_if(header, code[0].orelse) else: self.indent() self.write("else:") if (len(code[0].orelse) >= 2 and self.parse_header(code[0].orelse[0]) and self.parse_header(code[0].orelse[1])): orelse = code[0].orelse[1:] else: orelse = code[0].orelse self.print_nodes(orelse, 1) def print_for(self, header, code): # Here we handle the for statement. Note that the for statement generates some extra python code to # Keep track of it's header indices. The first one is ignored by the statement parser, # the second line is just ingored here. line = code[1] self.advance_to_line(self.get_first_line(code)) self.indent() self.write("for %s in %s:" % (self.strip_parens( self.to_source(line.target)), self.to_source(line.iter))) if (len(line.body) >= 3 and self.parse_header(line.body[0]) and self.parse_header(line.body[1])): body = line.body[1:] else: body = line.body with self.not_root(): self.print_nodes(body[:-1], 1) @dispatch(('renpy', 'use_screen')) def print_use(self, header, code): # This function handles the use statement, which translates into a python expression "renpy.use_screen". # It would technically be possible for this to be a python statement, but the odds of this are very small. # renpy itself will insert some kwargs, we'll delete those and then parse the command here. if (len(code) != 1 or not code[0].value.args or not isinstance(code[0].value.args[0], ast.Str)): return self.print_python(header, code) args, kwargs, exargs, exkwargs = self.parse_args(code[0]) kwargs = [(key, value) for key, value in kwargs if not (key == '_scope' or key == '_name')] self.advance_to_line(self.get_first_line(code)) self.indent() self.write("use %s" % code[0].value.args[0].s) args.pop(0) arglist = [] if args or kwargs or exargs or exkwargs: self.write("(") arglist.extend(args) arglist.extend("%s=%s" % i for i in kwargs) if exargs: arglist.append("*%s" % exargs) if exkwargs: arglist.append("**%s" % exkwargs) self.write(", ".join(arglist)) self.write(")") @dispatch(('_scope', 'setdefault')) def print_default(self, header, code): if (len(code) != 1 or code[0].value.keywords or code[0].value.kwargs or len(code[0].value.args) != 2 or code[0].value.starargs or not isinstance(code[0].value.args[0], ast.Str)): return self.print_python(header, code) self.advance_to_line(self.get_first_line(code)) self.indent() self.write( "default %s = %s" % (code[0].value.args[0].s, self.to_source(code[0].value.args[1]))) # These never have a ui.close() at the end @dispatch(('ui', 'add')) @dispatch(('ui', 'imagebutton')) @dispatch(('ui', 'input')) @dispatch(('ui', 'key')) @dispatch(('ui', 'label')) @dispatch(('ui', 'text')) @dispatch(('ui', 'null')) @dispatch(('ui', 'mousearea')) @dispatch(('ui', 'textbutton')) @dispatch(('ui', 'timer')) @dispatch(('ui', 'bar')) @dispatch(('ui', 'vbar')) @dispatch(('ui', 'hotbar')) @dispatch(('ui', 'on')) @dispatch(('ui', 'image')) def print_nochild(self, header, code): if len(code) != 1: self.print_python(header, code) return line = code[0] self.advance_to_line(self.get_first_line(code)) self.indent() self.write(line.value.func.attr) self.print_args(line.value) with self.not_root(): self.print_buggy_keywords_and_nodes( self.make_printable_keywords(line.value.keywords, line.value.lineno), None, False, False) # These functions themselves don't have a ui.close() at the end, but # they're always immediately followed by one that does (usually # ui.child_or_fixed(), but also possibly one set with "has") @dispatch(('ui', 'button')) @dispatch(('ui', 'frame')) @dispatch(('ui', 'transform')) @dispatch(('ui', 'viewport')) @dispatch(('ui', 'window')) @dispatch(('ui', 'drag')) @dispatch(('ui', 'hotspot_with_child')) def print_onechild(self, header, code, has_block=False): # We expect to have at least ourself, one child, and ui.close() if len(code) < 3 or self.get_dispatch_key(code[-1]) != ('ui', 'close'): if has_block: raise BadHasBlockException() self.print_python(header, code) return line = code[0] name = line.value.func.attr if name == 'hotspot_with_child': name = 'hotspot' if self.get_dispatch_key(code[1]) != ('ui', 'child_or_fixed'): # Handle the case where a "has" statement was used if has_block: # Ren'Py lets users nest "has" blocks for some reason, and it # puts the ui.close() statement in the wrong place when they do. # Since we checked for ui.close() being in the right place # before, the only way we could ever get here is if a user added # one inside a python block at the end. If this happens, turn # the whole outer block into Python instead of screencode. raise BadHasBlockException() if not self.parse_header(code[1]): self.print_python(header, code) return block = code[1:] state = self.save_state() try: self.advance_to_line(self.get_first_line(code)) self.indent() self.write(name) self.print_args(line.value) with self.not_root(): self.print_buggy_keywords_and_nodes( self.make_printable_keywords(line.value.keywords, line.value.lineno), None, True, False) with self.increase_indent(): if len(block) > 1 and isinstance(block[1], ast.Expr): # If this isn't true, we'll get a BadHasBlockException # later anyway. This check is just to keep it from being # an exception that we can't handle. self.advance_to_line(block[1].value.lineno) self.indent() self.write("has ") self.skip_indent_until_write = True self.print_nodes(block, 1, True) except BadHasBlockException as e: self.rollback_state(state) self.print_python(header, code) else: self.commit_state(state) else: # Remove ourself, ui.child_or_fixed(), and ui.close() block = code[2:-1] if block and not self.parse_header(block[0]): if has_block: raise BadHasBlockException() self.print_python(header, code) return if not has_block: self.advance_to_line(self.get_first_line(code)) self.indent() self.write(name) self.print_args(line.value) with self.not_root(): self.print_buggy_keywords_and_nodes( self.make_printable_keywords(line.value.keywords, line.value.lineno), block, False, has_block) # These always have a ui.close() at the end @dispatch(('ui', 'fixed')) @dispatch(('ui', 'grid')) @dispatch(('ui', 'hbox')) @dispatch(('ui', 'side')) @dispatch(('ui', 'vbox')) @dispatch(('ui', 'imagemap')) @dispatch(('ui', 'draggroup')) def print_manychildren(self, header, code, has_block=False): if (self.get_dispatch_key(code[-1]) != ('ui', 'close') or (len(code) != 2 and not self.parse_header(code[1]))): if has_block: raise BadHasBlockException() self.print_python(header, code) return line = code[0] block = code[1:-1] if not has_block: self.advance_to_line(self.get_first_line(code)) self.indent() self.write(line.value.func.attr) self.print_args(line.value) with self.not_root(): self.print_buggy_keywords_and_nodes( self.make_printable_keywords(line.value.keywords, line.value.lineno), block, False, has_block) # Parsing functions def parse_header(self, header): # Given a Python AST node, returns the parent ID if the node represents # a header, or None otherwise. if (isinstance(header, ast.Assign) and len(header.targets) == 1 and isinstance(header.targets[0], ast.Name) and re.match(r"_[0-9]+$", header.targets[0].id) and isinstance(header.value, ast.Tuple) and len(header.value.elts) == 2 and isinstance(header.value.elts[0], ast.Name)): parent_id = header.value.elts[0].id index = header.value.elts[1] if re.match(r"_([0-9]+|name)$", parent_id) and (isinstance(index, ast.Num) or (isinstance(index, ast.Name) and re.match(r"_[0-9]+$", index.id))): return parent_id return None def parse_args(self, node): return ([self.to_source(i) for i in node.value.args], [ (i.arg, self.to_source(i.value)) for i in node.value.keywords ], node.value.starargs and self.to_source(node.value.starargs), node.value.kwargs and self.to_source(node.value.kwargs))
class TestcaseDecompiler(DecompilerBase): """ An object which handles the decompilation of renpy testcase statements """ # This dictionary is a mapping of Class: unbound_method, which is used to determine # what method to call for which testast class dispatch = Dispatcher() def print_node(self, ast): if hasattr(ast, 'linenumber'): self.advance_to_line(ast.linenumber) self.dispatch.get(type(ast), type(self).print_unknown)(self, ast) @dispatch(testast.Python) def print_python(self, ast): self.indent() code = ast.code.source if code[0] == '\n': self.write("python:") with self.increase_indent(): self.write_lines(split_logical_lines(code[1:])) else: self.write("$ %s" % code) @dispatch(testast.Assert) def print_assert(self, ast): self.indent() self.write('assert %s' % ast.expr) @dispatch(testast.Jump) def print_jump(self, ast): self.indent() self.write('jump %s' % ast.target) @dispatch(testast.Call) def print_call(self, ast): self.indent() self.write('call %s' % ast.target) @dispatch(testast.Action) def print_action(self, ast): self.indent() self.write('run %s' % ast.expr) @dispatch(testast.Pause) def print_pause(self, ast): self.indent() self.write('pause %s' % ast.expr) @dispatch(testast.Label) def print_label(self, ast): self.indent() self.write('label %s' % ast.name) @dispatch(testast.Type) def print_type(self, ast): self.indent() if len(ast.keys[0]) == 1: self.write('type "%s"' % string_escape(''.join(ast.keys))) else: self.write('type %s' % ast.keys[0]) if ast.pattern is not None: self.write(' pattern "%s"' % string_escape(ast.pattern)) if hasattr(ast, 'position') and ast.position is not None: self.write(' pos %s' % ast.position) @dispatch(testast.Drag) def print_drag(self, ast): self.indent() self.write('drag %s' % ast.points) if ast.button != 1: self.write(' button %d' % ast.button) if ast.pattern is not None: self.write(' pattern "%s"' % string_escape(ast.pattern)) if ast.steps != 10: self.write(' steps %d' % ast.steps) @dispatch(testast.Move) def print_move(self, ast): self.indent() self.write('move %s' % ast.position) if ast.pattern is not None: self.write(' pattern "%s"' % string_escape(ast.pattern)) @dispatch(testast.Click) def print_click(self, ast): self.indent() if ast.pattern is not None: self.write('"%s"' % string_escape(ast.pattern)) else: self.write('click') if hasattr(ast, 'button') and ast.button != 1: self.write(' button %d' % ast.button) if hasattr(ast, 'position') and ast.position is not None: self.write(' pos %s' % ast.position) if hasattr(ast, 'always') and ast.always: self.write(' always') @dispatch(testast.Until) def print_until(self, ast): if hasattr(ast.right, 'linenumber'): # We don't have our own line number, and it's not guaranteed that left has a line number. # Go to right's line number now since we can't go to it after we print left. self.advance_to_line(ast.right.linenumber) self.print_node(ast.left) self.write(' until ') self.skip_indent_until_write = True self.print_node(ast.right)