diff --git a/decompiler/__init__.py b/decompiler/__init__.py index fad40d33..6fa0b5db 100644 --- a/decompiler/__init__.py +++ b/decompiler/__init__.py @@ -33,6 +33,7 @@ import screendecompiler import sl2decompiler import testcasedecompiler +import atldecompiler import codegen import astdump @@ -130,163 +131,17 @@ def print_node(self, ast): # 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 + # ATL subdecompiler hook 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 - # I think something changed about the handling of pause, that last special case doesn't look necessary anymore - # as a proper pause warper exists now but we'll keep it around for backwards compatability - 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) - + self.linenumber = atldecompiler.pprint( + self.out_file, ast, self.options, + self.indent_level, self.linenumber, self.skip_indent_until_write + ) + self.skip_indent_until_write = False # Displayable related functions @@ -969,31 +824,8 @@ def print_screen(self, ast): self.skip_indent_until_write = False elif isinstance(screen, renpy.sl2.slast.SLScreen): - def print_atl_callback(linenumber, indent_level, atl): - # screen language occasionally has to jump back to this decompiler to emit atl code - # for this, we need to overwrite some state of this decompiler temporarily with the - # state of the screenlang decompiler - # TODO: remove this very dirty hack and move atl decompiling to a separate - # atl decompiler - - # overwrite state - old_linenumber = self.linenumber - self.linenumber = linenumber - old_skip_indent_until_write = self.skip_indent_until_write - self.skip_indent_until_write = False - - with self.increase_indent(indent_level - self.indent_level): - self.print_atl(atl) - - # restore state - new_linenumber = self.linenumber - self.linenumber = old_linenumber - self.skip_indent_until_write = old_skip_indent_until_write - - return new_linenumber - self.linenumber = sl2decompiler.pprint( - self.out_file, screen, self.options, print_atl_callback, + self.out_file, screen, self.options, self.indent_level, self.linenumber, self.skip_indent_until_write ) self.skip_indent_until_write = False diff --git a/decompiler/atldecompiler.py b/decompiler/atldecompiler.py new file mode 100644 index 00000000..4fdf931c --- /dev/null +++ b/decompiler/atldecompiler.py @@ -0,0 +1,198 @@ + + +from __future__ import unicode_literals +from util import DecompilerBase, WordConcatenator, Dispatcher + +import renpy + +def pprint(out_file, ast, options, + indent_level=0, linenumber=1, skip_indent_until_write=False): + return ATLDecompiler(out_file, options).dump( + ast, indent_level, linenumber, skip_indent_until_write) + +class ATLDecompiler(DecompilerBase): + """ + An object that handles decompilation of atl blocks from the ren'py AST + """ + + dispatch = Dispatcher() + + def dump(self, ast, indent_level=0, linenumber=1, skip_indent_until_write=False): + # At this point, the preceding ":" has been written, and indent hasn't been increased yet. + # There's no common syntax for starting an ATL node, and the base block that is created + # is just a RawBlock. normally RawBlocks are created witha block: statement so we cannot + # just reuse the node for that. Instead, we implement the top level node directly here + self.indent_level = indent_level + self.linenumber = linenumber + self.skip_indent_until_write = skip_indent_until_write + + self.print_block(ast) + + return self.linenumber + + def print_node(self, ast): + # Line advancement logic: + if hasattr(ast, "loc"): + if isinstance(ast, renpy.atl.RawBlock): + # note: the location property of a RawBlock points to the first line of the block, + # not the statement that created it. + # it can also contain the following nonsense if there was no block for some reason. + if ast.loc != ('', 0): + self.advance_to_line(ast.loc[1] - 1) + + else: + self.advance_to_line(ast.loc[1]) + + self.dispatch.get(type(ast), type(self).print_unknown)(self, ast) + + def print_block(self, block): + # Prints a block of ATL statements + # block is a renpy.atl.RawBlock instance. + with self.increase_indent(): + if block.statements: + self.print_nodes(block.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, so we do not write "pass" then. + elif block.loc != ('', 0): + + # if there were no contents insert a pass node to keep syntax valid. + self.indent() + self.write("pass") + + @dispatch(renpy.atl.RawMultipurpose) + def print_atl_rawmulti(self, ast): + warp_words = WordConcatenator(False) + + # warpers + # I think something changed about the handling of pause, that last special case doesn't look necessary anymore + # as a proper pause warper exists now but we'll keep it around for backwards compatability + 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_block(ast) + + @dispatch(renpy.atl.RawChild) + def print_atl_rawchild(self, ast): + for child in ast.children: + self.indent() + self.write("contains:") + self.print_block(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_block(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_block(block) + + @dispatch(renpy.atl.RawParallel) + def print_atl_rawparallel(self, ast): + for block in ast.blocks: + self.indent() + self.write("parallel:") + self.print_block(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) diff --git a/decompiler/sl2decompiler.py b/decompiler/sl2decompiler.py index 6fdf4b35..2fc8156e 100644 --- a/decompiler/sl2decompiler.py +++ b/decompiler/sl2decompiler.py @@ -25,6 +25,8 @@ from util import DecompilerBase, First, reconstruct_paraminfo, \ reconstruct_arginfo, split_logical_lines, Dispatcher +import atldecompiler + from renpy import ui, sl2 from renpy.ast import PyExpr from renpy.text import text @@ -33,9 +35,9 @@ # Main API -def pprint(out_file, ast, options, print_atl_callback, +def pprint(out_file, ast, options, indent_level=0, linenumber=1, skip_indent_until_write=False): - return SL2Decompiler(out_file, options, print_atl_callback).dump( + return SL2Decompiler(out_file, options).dump( ast, indent_level, linenumber, skip_indent_until_write) # Implementation @@ -45,9 +47,8 @@ class SL2Decompiler(DecompilerBase): An object which handles the decompilation of renpy screen language 2 screens to a given stream """ - def __init__(self, out_file, options, print_atl_callback): + def __init__(self, out_file, options): super(SL2Decompiler, self).__init__(out_file, options) - self.print_atl_callback = print_atl_callback # This dictionary is a mapping of Class: unbound_method, which is used to determine # what method to call for which slast class @@ -531,8 +532,12 @@ def print_keyword_or_child(self, item, first_line=False, has_block=False): assert not has_block, "cannot start a block on the same line as an at transform block" self.write(sep()) self.write("at transform:") - self.linenumber = self.print_atl_callback(self.linenumber, self.indent_level, item[3]) + self.linenumber = atldecompiler.pprint( + self.out_file, item[3], self.options, + self.indent_level, self.linenumber, self.skip_indent_until_write + ) + self.skip_indent_until_write = False return if ty == "keywords_broken": diff --git a/un.rpyc/compile.py b/un.rpyc/compile.py index a8724eb3..1d4b0441 100755 --- a/un.rpyc/compile.py +++ b/un.rpyc/compile.py @@ -141,9 +141,10 @@ def Exec(code): _3 # testcasedecompiler _4 # screendecompiler -_5 # sl2decompiler -_6 # decompiler -_7 # unrpyc +_5 # atldecompiler +_6 # sl2decompiler +_7 # decompiler +_8 # unrpyc from unrpyc import decompile_game decompile_game() @@ -164,6 +165,7 @@ def Exec(code): Module("codegen", path.join(base_folder, "decompiler/codegen.py")), Module("testcasedecompiler", path.join(base_folder, "decompiler/testcasedecompiler.py")), Module("screendecompiler", path.join(base_folder, "decompiler/screendecompiler.py")), + Module("atldecompiler", path.join(base_folder, "decompiler/atldecompiler.py")), Module("sl2decompiler", path.join(base_folder, "decompiler/sl2decompiler.py")), Module("decompiler", path.join(base_folder, "decompiler/__init__.py")), Module("unrpyc", path.join(pack_folder, "unrpyc-compile.py")) @@ -175,6 +177,7 @@ def Exec(code): Module("codegen", path.join(base_folder, "decompiler/codegen.py")), Module("testcasedecompiler", path.join(base_folder, "decompiler/testcasedecompiler.py")), Module("screendecompiler", path.join(base_folder, "decompiler/screendecompiler.py")), + Module("atldecompiler", path.join(base_folder, "decompiler/atldecompiler.py")), Module("sl2decompiler", path.join(base_folder, "decompiler/sl2decompiler.py")), Module("decompiler", path.join(base_folder, "decompiler/__init__.py")), Module("unrpyc", path.join(pack_folder, "unrpyc-compile.py")) @@ -189,6 +192,7 @@ def Exec(code): rpy_two = p.GetItem(p.Sequence( Module("testcasedecompiler", path.join(base_folder, "decompiler/testcasedecompiler.py")), Module("screendecompiler", path.join(base_folder, "decompiler/screendecompiler.py")), + Module("atldecompiler", path.join(base_folder, "decompiler/atldecompiler.py")), Module("sl2decompiler", path.join(base_folder, "decompiler/sl2decompiler.py")), Module("decompiler", path.join(base_folder, "decompiler/__init__.py")), Module("unrpyc", path.join(pack_folder, "unrpyc-compile.py"))