diff --git a/decompiler/__init__.py b/decompiler/__init__.py index da79f59..5c6154c 100644 --- a/decompiler/__init__.py +++ b/decompiler/__init__.py @@ -31,7 +31,8 @@ from . import atldecompiler from . import astdump -__all__ = ["astdump", "magic", "sl2decompiler", "testcasedecompiler", "translate", "util", "Options", "pprint", "Decompiler", "renpycompat"] +__all__ = ["astdump", "magic", "sl2decompiler", "testcasedecompiler", "translate", "util", + "Options", "pprint", "Decompiler", "renpycompat"] # Main API @@ -74,13 +75,15 @@ def __init__(self, out_file, options): self.last_lines_behind = 0 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.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.in_init, self.missing_init, self.most_lines_behind, self.last_lines_behind) + return (super(Decompiler, self).save_state(), self.paired_with, self.say_inside_menu, + self.label_inside_menu, 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]) @@ -113,7 +116,10 @@ def dump(self, ast): 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)): + 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) self.dispatch.get(type(ast), type(self).print_unknown)(self, ast) @@ -171,11 +177,14 @@ 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. + # 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): + if (init.priority != self.init_offset + and len(init.block) == 1 + and not self.should_come_before(init, ast)): priority = f' {init.priority - self.init_offset}' self.write(f'transform{priority} {ast.varname}') if ast.parameters is not None: @@ -259,8 +268,8 @@ def print_with(self, ast): # with statement. detect this and process it properly if 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): + if not (isinstance(self.block[self.index + 2], renpy.ast.With) + and self.block[self.index + 2].expr == ast.paired): raise Exception(f'Unmatched paired with {self.paired_with!r} != {ast.expr!r}') self.paired_with = ast.paired @@ -306,17 +315,18 @@ def print_label(self, ast): if remaining_blocks > 1: # Label followed by a menu next_ast = self.block[self.index + 1] - if isinstance(next_ast, renpy.ast.Menu) and next_ast.linenumber == ast.linenumber: + if (isinstance(next_ast, renpy.ast.Menu) + and next_ast.linenumber == ast.linenumber): self.label_inside_menu = ast return if remaining_blocks > 2: # Label, followed by a say, followed by a menu next_next_ast = self.block[self.index + 2] - if (isinstance(next_ast, renpy.ast.Say) and - isinstance(next_next_ast, renpy.ast.Menu) and - next_next_ast.linenumber == ast.linenumber and - self.say_belongs_to_menu(next_ast, next_next_ast)): + if (isinstance(next_ast, renpy.ast.Say) + and isinstance(next_next_ast, renpy.ast.Menu) + and next_next_ast.linenumber == ast.linenumber + and self.say_belongs_to_menu(next_ast, next_next_ast)): self.label_inside_menu = ast return @@ -371,9 +381,11 @@ def print_call(self, ast): @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): + 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 @@ -396,7 +408,7 @@ def print_if(self, ast): self.indent() self.write("else:") else: - if(hasattr(condition, 'linenumber')): + if (hasattr(condition, 'linenumber')): self.advance_to_line(condition.linenumber) self.indent() self.write(f'{statement()} {condition}:') @@ -412,14 +424,13 @@ def print_while(self, ast): @dispatch(renpy.ast.Pass) def print_pass(self, ast): - if (self.index and - isinstance(self.block[self.index - 1], renpy.ast.Call)): + 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): + 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) @@ -478,29 +489,29 @@ def print_init(self, ast): # 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 - # 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 + self.init_offset and isinstance(ast.block[0], renpy.ast.Image))) and not ( - self.should_come_before(ast, ast.block[0])): + 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.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:])): + 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: @@ -572,19 +583,24 @@ def print_menu(self, ast): 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 type unicode, just not of type PyExpr + # 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 type unicode, just not of type PyExpr # todo: this check probably doesn't work in ren'py 8 if isinstance(condition, str) 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 + 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 + # 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() @@ -592,17 +608,21 @@ def print_menu(self, ast): 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 + # state[7] is the saved value of self.last_lines_behind + if self.most_lines_behind > state[7]: + # 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 + # state[6] is the saved value of self.most_lines_behind + self.most_lines_behind = max(state[6], 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 + # 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 @@ -641,11 +661,14 @@ def print_define(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. + # 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): + if (init.priority != self.init_offset + and len(init.block) == 1 + and not self.should_come_before(init, ast)): priority = f' {init.priority - self.init_offset}' index = "" @@ -669,11 +692,14 @@ def print_default(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. + # 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): + if (init.priority != self.init_offset + and len(init.block) == 1 + and not self.should_come_before(init, ast)): priority = f' {init.priority - self.init_offset}' if ast.store == "store": @@ -686,19 +712,21 @@ def print_default(self, ast): # 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 - 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)) + return (not say.interact + and say.who is not None + and say.with_ is None + and 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 this say statement precedes a menu statement, postpone emitting it until we're handling - # the menu - if (not inmenu and self.index + 1 < len(self.block) and - self.say_belongs_to_menu(ast, self.block[self.index + 1])): + # if this say statement precedes a menu statement, postpone emitting it until we're + # handling the menu + 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 @@ -782,9 +810,9 @@ def print_endtranslate(self, ast): 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): + 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(f'translate {ast.language or "None"} strings:') @@ -809,8 +837,10 @@ def print_translateblock(self, ast): 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. + 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) diff --git a/decompiler/astdump.py b/decompiler/astdump.py index 41baf85..038d16b 100644 --- a/decompiler/astdump.py +++ b/decompiler/astdump.py @@ -20,7 +20,6 @@ import sys import inspect -import ast as py_ast import renpy def pprint(out_file, ast, comparable=False, no_pyexpr=False): @@ -47,7 +46,9 @@ def __init__(self, out_file=None, no_pyexpr=False, def dump(self, ast): self.linenumber = 1 self.indent = 0 - self.passed = [] # We'll keep a stack of objects which we've traversed here so we don't recurse endlessly on circular references + # We'll keep a stack of objects which we've traversed here so we don't recurse + # endlessly on circular references + self.passed = [] self.passed_where = [] self.print_ast(ast) @@ -132,7 +133,7 @@ def should_print_key(self, ast, key): elif key == 'serial': ast.serial = 0 elif key == 'col_offset': - ast.col_offset = 0 # TODO maybe make this match? + ast.col_offset = 0 # TODO maybe make this match? elif key == 'name' and type(ast.name) == tuple: name = ast.name[0] if isinstance(name, str): @@ -140,48 +141,58 @@ def should_print_key(self, ast, key): ast.name = (name.split(b'/')[-1], 0, 0) elif key == 'location' and type(ast.location) == tuple: if len(ast.location) == 4: - ast.location = (ast.location[0].split('/')[-1].split('\\')[-1], ast.location[1], ast.location[2], 0) + ast.location = (ast.location[0].split('/')[-1].split('\\')[-1], + ast.location[1], ast.location[2], 0) elif len(ast.location) == 3: - ast.location = (ast.location[0].split('/')[-1].split('\\')[-1], ast.location[1], 0) + ast.location = (ast.location[0].split('/')[-1].split('\\')[-1], + ast.location[1], 0) elif len(ast.location) == 2: - ast.location = (ast.location[0].split('/')[-1].split('\\')[-1], ast.location[1]) + ast.location = (ast.location[0].split('/')[-1].split('\\')[-1], + ast.location[1]) elif key == 'loc' and type(ast.loc) == tuple: ast.loc = (ast.loc[0].split('/')[-1].split('\\')[-1], ast.loc[1]) elif key == 'filename': ast.filename = ast.filename.split('/')[-1].split('\\')[-1] - elif (key == 'parameters' and ast.parameters is None and - isinstance(ast, renpy.screenlang.ScreenLangScreen)): + elif (key == 'parameters' + and ast.parameters is None + and isinstance(ast, renpy.screenlang.ScreenLangScreen)): # When no parameters exist, some versions of Ren'Py set parameters # to None and some don't set it at all. return False - elif (key == 'hide' and ast.hide == False and - (isinstance(ast, renpy.ast.Python) or - isinstance(ast, renpy.ast.Label))): + elif (key == 'hide' + and ast.hide is False + and (isinstance(ast, renpy.ast.Python) + or isinstance(ast, renpy.ast.Label))): # When hide isn't set, some versions of Ren'Py set it to False and # some don't set it at all. return False - elif (key == 'attributes' and ast.attributes is None and - isinstance(ast, renpy.ast.Say)): + elif (key == 'attributes' + and ast.attributes is None + and isinstance(ast, renpy.ast.Say)): # When no attributes are set, some versions of Ren'Py set it to None # and some don't set it at all. return False - elif (key == 'temporary_attributes' and ast.temporary_attributes is None and - isinstance(ast, renpy.ast.Say)): + elif (key == 'temporary_attributes' + and ast.temporary_attributes is None + and isinstance(ast, renpy.ast.Say)): # When no temporary attributes are set, some versions of Ren'Py set # it to None and some don't set it at all. return False - elif (key == 'rollback' and ast.rollback == 'normal' and - isinstance(ast, renpy.ast.Say)): + elif (key == 'rollback' + and ast.rollback == 'normal' + and isinstance(ast, renpy.ast.Say)): # When rollback is normal, some versions of Ren'Py set it to 'normal' # and some don't set it at all. return False - elif (key == 'block' and ast.block == [] and - isinstance(ast, renpy.ast.UserStatement)): + elif (key == 'block' + and ast.block == [] + and isinstance(ast, renpy.ast.UserStatement)): # When there's no block, some versions of Ren'Py set it to None # and some don't set it at all. return False - elif (key == 'store' and ast.store == 'store' and - isinstance(ast, renpy.ast.Python)): + elif (key == 'store' + and ast.store == 'store' + and isinstance(ast, renpy.ast.Python)): # When a store isn't specified, some versions of Ren'Py set it to # "store" and some don't set it at all. return False @@ -200,7 +211,7 @@ def print_object(self, ast): # prints the values of relevant attributes in a dictionary-like way # it will not print anything which is a bound method or starts with a _ self.p('<') - self.p(str(ast.__class__)[8:-2] if hasattr(ast, '__class__') else str(ast)) + self.p(str(ast.__class__)[8:-2] if hasattr(ast, '__class__') else str(ast)) keys = list(i for i in dir(ast) if self.should_print_key(ast, i)) if keys: diff --git a/decompiler/atldecompiler.py b/decompiler/atldecompiler.py index 73bf274..626be14 100644 --- a/decompiler/atldecompiler.py +++ b/decompiler/atldecompiler.py @@ -35,10 +35,11 @@ class ATLDecompiler(DecompilerBase): 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 + # 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 @@ -86,8 +87,9 @@ 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 + # 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: @@ -168,8 +170,8 @@ def print_atl_rawchoice(self, ast): self.write(f' {chance}') self.write(":") self.print_block(block) - if (self.index + 1 < len(self.block) and - isinstance(self.block[self.index + 1], renpy.atl.RawChoice)): + if (self.index + 1 < len(self.block) + and isinstance(self.block[self.index + 1], renpy.atl.RawChoice)): self.indent() self.write("pass") @@ -204,8 +206,8 @@ def print_atl_rawparallel(self, ast): 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)): + if (self.index + 1 < len(self.block) + and isinstance(self.block[self.index + 1], renpy.atl.RawParallel)): self.indent() self.write("pass") diff --git a/decompiler/renpycompat.py b/decompiler/renpycompat.py index a9238d7..e6ef388 100644 --- a/decompiler/renpycompat.py +++ b/decompiler/renpycompat.py @@ -161,7 +161,8 @@ def __setstate__(self, state): def pickle_safe_loads(buffer: bytes): - return magic.safe_loads(buffer, CLASS_FACTORY, {"collections",}, encoding="ASCII", errors="strict") + return magic.safe_loads( + buffer, CLASS_FACTORY, {"collections"}, encoding="ASCII", errors="strict") def pickle_safe_dumps(buffer: bytes): diff --git a/decompiler/sl2decompiler.py b/decompiler/sl2decompiler.py index 0ef95e5..73e9ef1 100644 --- a/decompiler/sl2decompiler.py +++ b/decompiler/sl2decompiler.py @@ -18,9 +18,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import sys -from operator import itemgetter - from .util import DecompilerBase, First, reconstruct_paraminfo, \ reconstruct_arginfo, split_logical_lines, Dispatcher @@ -43,7 +40,8 @@ def pprint(out_file, ast, options, class SL2Decompiler(DecompilerBase): """ - An object which handles the decompilation of renpy screen language 2 screens to a given stream + An object which handles the decompilation of renpy screen language 2 screens to a given + stream """ def __init__(self, out_file, options): @@ -109,9 +107,10 @@ def print_block(self, ast, immediate_block=False): # for showif, if and use, no keyword properties on the same line are allowed # for custom displayables, they are allowed. # - # immediate_block: boolean, indicates that no keyword properties are before the :, and that - # a block is required - first_line, other_lines = self.sort_keywords_and_children(ast, immediate_block=immediate_block) + # immediate_block: boolean, indicates that no keyword properties are before the :, and + # that a block is required + first_line, other_lines = self.sort_keywords_and_children( + ast, immediate_block=immediate_block) has_block = immediate_block or bool(other_lines) @@ -251,13 +250,17 @@ def print_displayable(self, ast, has_block=False): # since it results in cleaner code. # if we're not already in a has block, and have a single child that's a displayable, - # which itself has children, and the line number of this child is after any atl transform or keyword - # we can safely use a has statement - 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])): + # which itself has children, and the line number of this child is after any atl + # transform or keyword we can safely use a has statement + 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])): first_line, other_lines = self.sort_keywords_and_children(ast, ignore_children=True) self.print_keyword_or_child(first_line, first_line=True, has_block=True) @@ -289,45 +292,45 @@ def print_displayable(self, ast, has_block=False): self.print_keyword_or_child(line) displayable_names = { - (behavior.AreaPicker, "default"): ("areapicker", 1), - (behavior.Button, "button"): ("button", 1), - (behavior.DismissBehavior, "default"): ("dismiss", 0), - (behavior.Input, "input"): ("input", 0), - (behavior.MouseArea, 0): ("mousearea", 0), - (behavior.MouseArea, None): ("mousearea", 0), - (behavior.OnEvent, 0): ("on", 0), - (behavior.OnEvent, None): ("on", 0), - (behavior.Timer, "default"): ("timer", 0), - (dragdrop.Drag, "drag"): ("drag", 1), - (dragdrop.Drag, None): ("drag", 1), - (dragdrop.DragGroup, None): ("draggroup", 'many'), - (im.image, "default"): ("image", 0), - (layout.Grid, "grid"): ("grid", 'many'), - (layout.MultiBox, "fixed"): ("fixed", 'many'), - (layout.MultiBox, "hbox"): ("hbox", 'many'), - (layout.MultiBox, "vbox"): ("vbox", 'many'), - (layout.NearRect, "default"): ("nearrect", 1), - (layout.Null, "default"): ("null", 0), - (layout.Side, "side"): ("side", 'many'), - (layout.Window, "frame"): ("frame", 1), - (layout.Window, "window"): ("window", 1), - (motion.Transform, "transform"): ("transform", 1), - (sld.sl2add, None): ("add", 0), - (sld.sl2bar, None): ("bar", 0), - (sld.sl2vbar, None): ("vbar", 0), - (sld.sl2viewport, "viewport"): ("viewport", 1), - (sld.sl2vpgrid, "vpgrid"): ("vpgrid", 'many'), - (text.Text, "text"): ("text", 0), - (transform.Transform, "transform"):("transform", 1), - (ui._add, None): ("add", 0), - (ui._hotbar, "hotbar"): ("hotbar", 0), - (ui._hotspot, "hotspot"): ("hotspot", 1), - (ui._imagebutton, "image_button"): ("imagebutton", 0), - (ui._imagemap, "imagemap"): ("imagemap", 'many'), - (ui._key, None): ("key", 0), - (ui._label, "label"): ("label", 0), - (ui._textbutton, "button"): ("textbutton", 0), - (ui._textbutton, 0): ("textbutton", 0), + (behavior.AreaPicker, "default"): ("areapicker", 1), + (behavior.Button, "button"): ("button", 1), + (behavior.DismissBehavior, "default"): ("dismiss", 0), + (behavior.Input, "input"): ("input", 0), + (behavior.MouseArea, 0): ("mousearea", 0), + (behavior.MouseArea, None): ("mousearea", 0), + (behavior.OnEvent, 0): ("on", 0), + (behavior.OnEvent, None): ("on", 0), + (behavior.Timer, "default"): ("timer", 0), + (dragdrop.Drag, "drag"): ("drag", 1), + (dragdrop.Drag, None): ("drag", 1), + (dragdrop.DragGroup, None): ("draggroup", 'many'), + (im.image, "default"): ("image", 0), + (layout.Grid, "grid"): ("grid", 'many'), + (layout.MultiBox, "fixed"): ("fixed", 'many'), + (layout.MultiBox, "hbox"): ("hbox", 'many'), + (layout.MultiBox, "vbox"): ("vbox", 'many'), + (layout.NearRect, "default"): ("nearrect", 1), + (layout.Null, "default"): ("null", 0), + (layout.Side, "side"): ("side", 'many'), + (layout.Window, "frame"): ("frame", 1), + (layout.Window, "window"): ("window", 1), + (motion.Transform, "transform"): ("transform", 1), + (sld.sl2add, None): ("add", 0), + (sld.sl2bar, None): ("bar", 0), + (sld.sl2vbar, None): ("vbar", 0), + (sld.sl2viewport, "viewport"): ("viewport", 1), + (sld.sl2vpgrid, "vpgrid"): ("vpgrid", 'many'), + (text.Text, "text"): ("text", 0), + (transform.Transform, "transform"): ("transform", 1), + (ui._add, None): ("add", 0), + (ui._hotbar, "hotbar"): ("hotbar", 0), + (ui._hotspot, "hotspot"): ("hotspot", 1), + (ui._imagebutton, "image_button"): ("imagebutton", 0), + (ui._imagemap, "imagemap"): ("imagemap", 'many'), + (ui._key, None): ("key", 0), + (ui._label, "label"): ("label", 0), + (ui._textbutton, "button"): ("textbutton", 0), + (ui._textbutton, 0): ("textbutton", 0), } def sort_keywords_and_children(self, node, immediate_block=False, ignore_children=False): @@ -350,20 +353,25 @@ def sort_keywords_and_children(self, node, immediate_block=False, ignore_childre start_lineno = (block_lineno + 1) if immediate_block else block_lineno # these ones are optional - keyword_tag = getattr(node, "tag", None) # only used by SLScreen - keyword_as = getattr(node, "variable", None) # only used by SLDisplayable - atl_transform = getattr(node, "atl_transform", None) # all three can have it, but it is an optional property anyway + keyword_tag = getattr(node, "tag", None) # only used by SLScreen + keyword_as = getattr(node, "variable", None) # only used by SLDisplayable + # all three can have it, but it is an optional property anyway + atl_transform = getattr(node, "atl_transform", None) # keywords - # pre 7.7/8.2: keywords at the end of a line could not have an argument and the parser was okay with that. - keywords_by_line = [(value.linenumber if value else None, "keyword" if value else "broken", (name, value)) for name, value in keywords] + # pre 7.7/8.2: keywords at the end of a line could not have an argument and the parser + # was okay with that. + keywords_by_line = [(value.linenumber if value else None, + "keyword" if value else "broken", + (name, value)) for name, value in keywords] # children children_by_line = [(child.location[1], "child", child) for child in children] - # now we have to determine the order of all things. Multiple keywords can go on the same line, but not children. - # we don't want to completely trust lineno's, even if they're utterly wrong we still should spit out a decent file - # also, keywords and children are supposed to be in order from the start, so we shouldn't scramble that. + # now we have to determine the order of all things. Multiple keywords can go on the + # same line, but not children. we don't want to completely trust lineno's, even if + # they're utterly wrong we still should spit out a decent file also, keywords and + # children are supposed to be in order from the start, so we shouldn't scramble that. # merge keywords and childrens into a single ordered list # list of lineno, type, contents @@ -371,7 +379,8 @@ def sort_keywords_and_children(self, node, immediate_block=False, ignore_childre keywords_by_line.reverse() children_by_line.reverse() while keywords_by_line and children_by_line: - # broken keywords: always emit before any children, so we can merge them with the previous keywords easily + # broken keywords: always emit before any children, so we can merge them with the + # previous keywords easily if keywords_by_line[-1][0] is None: contents_in_order.append(keywords_by_line.pop()) @@ -444,12 +453,15 @@ def sort_keywords_and_children(self, node, immediate_block=False, ignore_childre current_keyword_line = (lineno, "keywords", [content]) elif ty == "broken": - contents_grouped.append((current_keyword_line[0], "keywords_broken", current_keyword_line[2], content)) + contents_grouped.append( + (current_keyword_line[0], "keywords_broken", + current_keyword_line[2], content)) current_keyword_line = None elif ty == "atl": if current_keyword_line[0] == lineno: - contents_grouped.append((lineno, "keywords_atl", current_keyword_line[2], content)) + contents_grouped.append( + (lineno, "keywords_atl", current_keyword_line[2], content)) current_keyword_line = None else: contents_grouped.append(current_keyword_line) @@ -459,8 +471,9 @@ def sort_keywords_and_children(self, node, immediate_block=False, ignore_childre if current_keyword_line is not None: contents_grouped.append(current_keyword_line) - # We need to assign linenos to any broken keywords that don't have them. Best guess is the previous lineno + 1 - # unless that doesn't exist, in which case it's the first available line + # We need to assign linenos to any broken keywords that don't have them. Best guess + # is the previous lineno + 1 unless that doesn't exist, in which case it's the first + # available line for i in range(len(contents_grouped)): lineno = contents_grouped[i][0] ty = contents_grouped[i][1] @@ -475,10 +488,10 @@ def sort_keywords_and_children(self, node, immediate_block=False, ignore_childre contents_grouped[i] = (lineno, "keywords_broken", [], contents) # these two keywords have no lineno information with them - # additionally, since 7.3 upwards, tag cannot be placed on the same line as `screen` for - # whatever reason. - # it is currently impossible to have both an `as` and a `tag` keyword in the same displayble - # `as` is only used for displayables, `tag` for screens. + # additionally, since 7.3 upwards, tag cannot be placed on the same line as `screen` + # for whatever reason. + # it is currently impossible to have both an `as` and a `tag` keyword in the same + # displayble `as` is only used for displayables, `tag` for screens. # strategies: # - if there's several empty lines before any line, we can make some new lines for them # - if the first line is a keyword line, we can merge them with it @@ -505,7 +518,8 @@ def sort_keywords_and_children(self, node, immediate_block=False, ignore_childre # really hard to know where inbetween children it'd be safe # to put it in else: - contents_grouped.insert(0, (block_lineno + 1, "keywords", [("tag", keyword_tag)])) + contents_grouped.insert( + 0, (block_lineno + 1, "keywords", [("tag", keyword_tag)])) if keyword_as: # if there's no content, put it on the first available line @@ -535,7 +549,8 @@ def sort_keywords_and_children(self, node, immediate_block=False, ignore_childre - # if there's no content on the first line, insert an empty line, to make processing easier. + # if there's no content on the first line, insert an empty line, to make processing + # easier. if immediate_block or not contents_grouped or contents_grouped[0][0] != block_lineno: contents_grouped.insert(0, (block_lineno, "keywords", [])) diff --git a/decompiler/testcasedecompiler.py b/decompiler/testcasedecompiler.py index 58497e7..6df04ff 100644 --- a/decompiler/testcasedecompiler.py +++ b/decompiler/testcasedecompiler.py @@ -143,8 +143,9 @@ def print_scroll(self, ast): @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. + # 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 ") diff --git a/decompiler/translate.py b/decompiler/translate.py index 2139b3d..78d2ffa 100644 --- a/decompiler/translate.py +++ b/decompiler/translate.py @@ -22,7 +22,6 @@ import renpy import hashlib -import re from copy import copy class Translator: @@ -59,7 +58,7 @@ def unique_identifier(self, label, digest): # Adapted from Ren'Py's Restructurer.create_translate def create_translate(self, block): if self.saving_translations: - return [] # Doesn't matter, since we're throwing this away in this case + return [] # Doesn't matter, since we're throwing this away in this case md5 = hashlib.md5() @@ -98,7 +97,9 @@ def create_translate(self, block): return new_block def walk(self, ast, f): - if isinstance(ast, (renpy.ast.Init, renpy.ast.Label, renpy.ast.While, renpy.ast.Translate, renpy.ast.TranslateBlock)): + if isinstance( + ast, (renpy.ast.Init, renpy.ast.Label, renpy.ast.While, renpy.ast.Translate, + renpy.ast.TranslateBlock)): f(ast.block) elif isinstance(ast, renpy.ast.Menu): for i in ast.items: @@ -110,8 +111,8 @@ def walk(self, ast, f): # Adapted from Ren'Py's Restructurer.callback def translate_dialogue(self, children): - new_children = [ ] - group = [ ] + new_children = [] + group = [] for i in children: @@ -123,7 +124,8 @@ def translate_dialogue(self, children): self.label = i.name self.alternate = None - if self.saving_translations and isinstance(i, renpy.ast.TranslateString) and i.language == self.language: + if self.saving_translations and isinstance( + i, renpy.ast.TranslateString) and i.language == self.language: self.strings[i.old] = i.new if not isinstance(i, renpy.ast.Translate): @@ -137,7 +139,7 @@ def translate_dialogue(self, children): group.append(i) tl = self.create_translate(group) new_children.extend(tl) - group = [ ] + group = [] elif hasattr(i, 'translatable') and i.translatable: group.append(i) @@ -146,13 +148,13 @@ def translate_dialogue(self, children): if group: tl = self.create_translate(group) new_children.extend(tl) - group = [ ] + group = [] new_children.append(i) if group: nodes = self.create_translate(group) new_children.extend(nodes) - group = [ ] + group = [] children[:] = new_children diff --git a/decompiler/util.py b/decompiler/util.py index 0cf97da..2d97793 100644 --- a/decompiler/util.py +++ b/decompiler/util.py @@ -46,8 +46,8 @@ def __init__(self, out_file=None, options=OptionBase()): self.linenumber = 0 # the indentation level we're at self.indent_level = 0 - # a boolean that can be set to make the next call to indent() not insert a newline and indent - # useful when a child node can continue on the same line as the parent node + # a boolean that can be set to make the next call to indent() not insert a newline and + # indent useful when a child node can continue on the same line as the parent node # advance_to_line will also cancel this if it changes the lineno self.skip_indent_until_write = False @@ -102,8 +102,13 @@ def save_state(self): """ Save our current state. """ - state = (self.out_file, self.skip_indent_until_write, self.linenumber, - self.block_stack, self.index_stack, self.indent_level, self.blank_line_queue) + state = (self.out_file, + self.skip_indent_until_write, + self.linenumber, + self.block_stack, + self.index_stack, + self.indent_level, + self.blank_line_queue) self.out_file = StringIO() return state @@ -119,8 +124,13 @@ def rollback_state(self, state): """ Roll back to a saved state. """ - (self.out_file, self.skip_indent_until_write, self.linenumber, - self.block_stack, self.index_stack, self.indent_level, self.blank_line_queue) = state + (self.out_file, + self.skip_indent_until_write, + self.linenumber, + self.block_stack, + self.index_stack, + self.indent_level, + self.blank_line_queue) = state def advance_to_line(self, linenumber): # If there was anything that we wanted to do as soon as we found a blank line, @@ -227,7 +237,9 @@ def reconstruct_paraminfo(paraminfo): already_accounted = set(name for name, default in paraminfo.positional_only) already_accounted.update(name for name, default in paraminfo.keyword_only) - other = [(name, default) for name, default in paraminfo.parameters if name not in already_accounted] + other = [(name, default) + for name, default in paraminfo.parameters + if name not in already_accounted] for name, default in paraminfo.positional_only: rv.append(sep()) @@ -296,8 +308,9 @@ def reconstruct_paraminfo(paraminfo): # ren'py 7.7/8.2 and above. # positional only, /, positional or keyword, *, keyword only, *** # prescence of the / is indicated by positional only arguments being present - # prescence of the * (if no *args) are present is indicated by keyword only args being present. - state = 1 # (0 = positional only, 1 = pos/key, 2 = keyword only) + # prescence of the * (if no *args) are present is indicated by keyword only args + # being present. + state = 1 # (0 = positional only, 1 = pos/key, 2 = keyword only) for parameter in paraminfo.parameters.values(): rv.append(sep()) @@ -385,7 +398,7 @@ def reconstruct_arginfo(arginfo): return "".join(rv) -def string_escape(s): # TODO see if this needs to work like encode_say_string elsewhere +def string_escape(s): # TODO see if this needs to work like encode_say_string elsewhere s = s.replace('\\', '\\\\') s = s.replace('"', '\\"') s = s.replace('\n', '\\n') @@ -450,8 +463,8 @@ def match(self, regexp): def python_string(self, clear_whitespace=True): # parse strings the ren'py way (don't parse docstrings, no b/r in front allowed) - # edit: now parses docstrings correctly. There was a degenerate case where '''string'string''' would - # result in issues + # edit: now parses docstrings correctly. There was a degenerate case where + # '''string'string''' would result in issues if clear_whitespace: return self.match(r"""(u?(?P"(?:"")?|'(?:'')?).*?(?<=[^\\])(?:\\\\)*(?P=a))""") else: @@ -503,18 +516,15 @@ def name(self): return word def simple_expression(self): - # test if the start string was a simple expression - start = self.pos - # check if there's anything in here acctually if self.eol(): return False # parse anything which can be called or have attributes requested - if not(self.python_string() or - self.number() or - self.container() or - self.name()): + if not (self.python_string() + or self.number() + or self.container() + or self.name()): return False while not self.eol(): @@ -549,7 +559,9 @@ def split_logical_lines(self): while self.pos < self.length: c = self.string[self.pos] - if c == '\n' and not contained and (not self.pos or self.string[self.pos - 1] != '\\'): + if (c == '\n' + and not contained + and (not self.pos or self.string[self.pos - 1] != '\\')): lines.append(self.string[startpos:self.pos]) # the '\n' is not included in the emitted line self.pos += 1 @@ -573,7 +585,7 @@ def split_logical_lines(self): if self.python_string(False): continue - self.re(r'\w+| +|.') # consume a word, whitespace or one symbol + self.re(r'\w+| +|.') # consume a word, whitespace or one symbol if self.pos != startpos: lines.append(self.string[startpos:]) @@ -629,7 +641,7 @@ def encode_say_string(s): # Adapted from Ren'Py's Say.get_code def say_get_code(ast, inmenu=False): - rv = [ ] + rv = [] if ast.who: rv.append(ast.who) diff --git a/deobfuscate.py b/deobfuscate.py index a63b121..0b5fc41 100644 --- a/deobfuscate.py +++ b/deobfuscate.py @@ -20,8 +20,8 @@ -# This file contains documented strategies used against known obfuscation techniques and some machinery -# to test them against. +# This file contains documented strategies used against known obfuscation techniques and +# some machinery to test them against. # Architecture is pretty simple. There's at least two steps in unpacking the rpyc format. # RPYC2 is an archive format that can contain multiple streams (referred to as slots) @@ -32,7 +32,6 @@ # being layers of base64, string-escape, hex-encoding, zlib-compression, etc. # We handle this by just trying these by checking if they fit. -import os import zlib import struct import base64 @@ -75,7 +74,7 @@ def extract_slot_rpyc(f, slot): slots = {} while position + 12 <= len(data): - slotid, start, length = struct.unpack(" 1000: + if arg and (isinstance(arg, str) + or p.PY3 and isinstance(arg, bytes)) and len(arg) > 1000: if p.PY3 and isinstance(arg, str): arg = arg.encode("latin1") diff --git a/un.rpyc/unrpyc-compile.py b/un.rpyc/unrpyc-compile.py index a1aa1f6..fc660df 100644 --- a/un.rpyc/unrpyc-compile.py +++ b/un.rpyc/unrpyc-compile.py @@ -21,7 +21,6 @@ import sys from pathlib import Path import traceback -import struct import decompiler from decompiler.renpycompat import pickle_safe_loads diff --git a/unrpyc.py b/unrpyc.py index 9d1b4ca..0c99a83 100755 --- a/unrpyc.py +++ b/unrpyc.py @@ -99,7 +99,7 @@ def read_ast_from_file(in_file): chunks[slot] = raw_contents[start: start + length] - if not 1 in chunks: + if 1 not in chunks: raise Exception( "Unable to find the right slot to load from the rpyc file. The file header " "structure has been changed.") @@ -108,7 +108,7 @@ def read_ast_from_file(in_file): try: contents = zlib.decompress(contents) - except Exception as e: + except Exception: raise Exception( "Did not find a zlib compressed blob where it was expected. Either the header has been " "modified or the file structure has been changed.") from None @@ -119,8 +119,8 @@ def read_ast_from_file(in_file): with printlock: print( "Warning: analysis found signs that this .rpyc file was generated by ren'py \n" - f' version {version} or below, while this unrpyc version targets ren\'py version 8. \n' - " Decompilation will still be attempted, but errors or incorrect \n" + f' version {version} or below, while this unrpyc version targets ren\'py \n' + " version 8. Decompilation will still be attempted, but errors or incorrect \n" " decompilation might occur. ") _, stmts = pickle_safe_loads(contents) @@ -145,7 +145,7 @@ def decompile_rpyc(input_filename, overwrite=False, dump=False, if not overwrite and out_filename.exists(): print("Output file already exists. Pass --clobber to overwrite.") - return False # Don't stop decompiling if one file already exists + return False # Don't stop decompiling if one file already exists with input_filename.open('rb') as in_file: if try_harder: @@ -155,8 +155,7 @@ def decompile_rpyc(input_filename, overwrite=False, dump=False, with out_filename.open('w', encoding='utf-8') as out_file: if dump: - astdump.pprint(out_file, ast, comparable=comparable, - no_pyexpr=no_pyexpr) + astdump.pprint(out_file, ast, comparable=comparable, no_pyexpr=no_pyexpr) else: options = decompiler.Options(printlock=printlock, translator=translator, init_offset=init_offset, sl_custom_names=sl_custom_names) @@ -227,7 +226,7 @@ def worker(arg_tup): comparable=args.comparable, translator=translator, init_offset=args.init_offset, try_harder=args.try_harder, sl_custom_names=args.sl_custom_names) - except Exception as e: + except Exception: with printlock: print(f'Error while decompiling {filename}:') print(traceback.format_exc()) @@ -441,12 +440,12 @@ def traverse(inpath): bad = results.count(False) if bad == 0: - print(f'Decompilation of {good} script file{"s" if good>1 else ""} successful') + print(f'Decompilation of {good} script file{"s" if good > 1 else ""} successful') elif good == 0: - print(f'Decompilation of {bad} file{"s" if bad>1 else ""} failed') + print(f'Decompilation of {bad} file{"s" if bad > 1 else ""} failed') else: - print(f'Decompilation of {good} file{"s" if good>1 else ""} successful\n' - f'but decompilation of {bad} file{"s" if bad>1 else ""} failed') + print(f'Decompilation of {good} file{"s" if good > 1 else ""} successful\n' + f'but decompilation of {bad} file{"s" if bad > 1 else ""} failed') if __name__ == '__main__':