Exemplo n.º 1
0
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)
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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)
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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))
Exemplo n.º 6
0
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)