diff --git a/decompiler/__init__.py b/decompiler/__init__.py index 8c29a01..8472c1a 100644 --- a/decompiler/__init__.py +++ b/decompiler/__init__.py @@ -31,7 +31,9 @@ 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 +76,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 +117,9 @@ 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 @@ -313,10 +322,10 @@ def print_label(self, ast): 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 +380,10 @@ 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 @@ -412,14 +422,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 +487,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 +581,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 +606,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 +659,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 +690,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 +710,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 +808,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:') @@ -810,7 +836,8 @@ def print_translateblock(self, ast): 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. + # 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 1b30d58..9efdf0f 100644 --- a/decompiler/astdump.py +++ b/decompiler/astdump.py @@ -46,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) @@ -131,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) is tuple: name = ast.name[0] if isinstance(name, str): @@ -139,48 +141,59 @@ def should_print_key(self, ast, key): ast.name = (name.split(b'/')[-1], 0, 0) elif key == 'location' and type(ast.location) is 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) is 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 is 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 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 a5d82e1..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 b7b3e04..73e9ef1 100644 --- a/decompiler/sl2decompiler.py +++ b/decompiler/sl2decompiler.py @@ -40,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): @@ -106,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) @@ -248,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) @@ -286,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): @@ -347,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 @@ -368,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()) @@ -441,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) @@ -456,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] @@ -472,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 @@ -502,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 @@ -532,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 18c5024..78d2ffa 100644 --- a/decompiler/translate.py +++ b/decompiler/translate.py @@ -58,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() @@ -97,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: @@ -122,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): diff --git a/decompiler/util.py b/decompiler/util.py index cb10f57..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: @@ -508,10 +521,10 @@ def simple_expression(self): 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(): @@ -546,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 @@ -570,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:]) diff --git a/deobfuscate.py b/deobfuscate.py index 5a95dfb..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) @@ -151,8 +151,8 @@ def extract_slot_headerscan(f, slot): @extractor def extract_slot_zlibscan(f, slot): """ - Slot extractor for things that fucked with the header structure to the point where it's easier - to just not bother with it and instead we just look for valid zlib chunks directly. + Slot extractor for things that fucked with the header structure to the point where it's + easier to just not bother with it and instead we just look for valid zlib chunks directly. """ f.seek(0) data = f.read() @@ -200,7 +200,8 @@ def decrypt_hex(data, count): @decryptor def decrypt_base64(data, count): - if not all(i in b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=\n" for i in count.keys()): + if not all(i in b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=\n" + for i in count.keys()): return None try: return base64.b64decode(data) @@ -223,8 +224,8 @@ def decrypt_string_escape(data, count): def assert_is_normal_rpyc(f): """ Analyze the structure of a single rpyc file object for correctness. - Does not actually say anything about the _contents_ of that section, just that we were able - to slice it out of there. + Does not actually say anything about the _contents_ of that section, just that we were + able to slice it out of there. If succesful, returns the uncompressed contents of the first storage slot. """ @@ -243,11 +244,11 @@ def assert_is_normal_rpyc(f): try: uncompressed = zlib.decompress(raw_data) except zlib.error: - raise ValueError("Did not find RENPY RPC2 header, but interpretation as legacy file failed") + raise ValueError( + "Did not find RENPY RPC2 header, but interpretation as legacy file failed") return uncompressed - else: if len(header) < 46: # 10 bytes header + 4 * 9 bytes content table diff --git a/testcases/validate_expected.py b/testcases/validate_expected.py index 34ae0f6..c147bf6 100644 --- a/testcases/validate_expected.py +++ b/testcases/validate_expected.py @@ -26,9 +26,9 @@ import subprocess ROOT = Path(__file__).parent -ORIGINAL = ROOT / "originals" # original .rpy files -EXPECTED = ROOT / "expected" # expected result from decompiling .rpyc files -COMPILED = ROOT / "compiled" # .rpyc files from compiling original +ORIGINAL = ROOT / "originals" # original .rpy files +EXPECTED = ROOT / "expected" # expected result from decompiling .rpyc files +COMPILED = ROOT / "compiled" # .rpyc files from compiling original def normalize(source: Path, dest: Path): with source.open("r", encoding="utf-8-sig") as fin: @@ -39,7 +39,8 @@ def normalize(source: Path, dest: Path): if not l or l.startswith("#"): continue - # strip any comments in general (yes this ignores that they might be inside a string) + # strip any comments in general (yes this ignores that they might be inside + # a string) if "#" in line: line, _ = line.split("#", 1) @@ -64,10 +65,16 @@ def process_recursively(source_dir, dest_dir, function): function(source_item, dest_item) def main(): - parser = argparse.ArgumentParser(description="Testcase utilities. Compares `expected` with `originals`") - - parser.add_argument('-u', '--update', dest='update', action='store_true', - help="update the contents of 'expected' with .rpy files found in 'compiled' before running") + parser = argparse.ArgumentParser( + description="Testcase utilities. Compares `expected` with `originals`") + + parser.add_argument( + '-u', + '--update', + dest='update', + action='store_true', + help="Update the contents of 'expected' with .rpy files found in 'compiled' before " + "running") args = parser.parse_args() @@ -88,4 +95,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/un.rpyc/compile.py b/un.rpyc/compile.py index 0a2c8a0..d2e9d67 100755 --- a/un.rpyc/compile.py +++ b/un.rpyc/compile.py @@ -24,21 +24,41 @@ import argparse import base64 from pathlib import Path - from corrupy import pickleast as p, minimize -parser = argparse.ArgumentParser(description="Pack unpryc into un.rpyc which can be ran from inside renpy") - -parser.add_argument("-d", "--debug", dest="debug", action="store_true", help="Create debug files") - -parser.add_argument("-p", "--protocol", dest="protocol", action="store", default="1", - help="The pickle protocol used for packing the pickles. default is 1, options are 0, 1 and 2") - -parser.add_argument("-r", "--raw", dest="minimize", action="store_false", - help="Don't minimize the compiler modules") - -parser.add_argument("-o", "--obfuscate", dest="obfuscate", action="store_true", - help="Enable extra minification measures which do not really turn down the filesize but make the source a lot less readable") +parser = argparse.ArgumentParser( + description="Pack unpryc into un.rpyc which can be ran from inside renpy") + +parser.add_argument( + "-d", + "--debug", + dest="debug", + action="store_true", + help="Create debug files") + +parser.add_argument( + "-p", + "--protocol", + dest="protocol", + action="store", + default="1", + help="The pickle protocol used for packing the pickles. Default is 1, options " + "are 0, 1 and 2") + +parser.add_argument( + "-r", + "--raw", + dest="minimize", + action="store_false", + help="Don't minimize the compiler modules") + +parser.add_argument( + "-o", + "--obfuscate", + dest="obfuscate", + action="store_true", + help="Enable extra minification measures which do not really turn down the filesize but " + "make the source a lot less readable") args = parser.parse_args() @@ -49,14 +69,14 @@ def Module(name, filename, munge_globals=True, retval=False, package=None): code = f.read() if args.minimize: # in modules only locals are worth optimizing - code = minimize.minimize(code, True, args.obfuscate and munge_globals, args.obfuscate, args.obfuscate) + code = minimize.minimize( + code, True, args.obfuscate and munge_globals, args.obfuscate, args.obfuscate) if package: return p.Sequence( p.DeclareModule(name, retval=retval), p.SetItem(p.Imports(name, "__dict__"), "__package__", package), p.DefineModule(name, code), - reversed=True - ) + reversed=True) else: return p.Module(name, code, retval=retval) @@ -214,20 +234,13 @@ def Exec(code): """ unrpyc = zlib.compress( - p.optimize( - p.dumps(decompiler_rpyc, protocol), - protocol), -9) + p.optimize(p.dumps(decompiler_rpyc, protocol), protocol), 9) bytecoderpyb = zlib.compress( - p.optimize( - p.dumps(decompiler_rpyb, protocol), - protocol), -9) + p.optimize(p.dumps(decompiler_rpyb, protocol), protocol), 9) -unrpy = rpy_base.format( - repr(base64.b64encode(p.optimize(p.dumps(decompiler_items, protocol), protocol))) -) +unrpy = rpy_base.format(repr(base64.b64encode( + p.optimize(p.dumps(decompiler_items, protocol), protocol)))) with (PACK_FOLDER / "un.rpyc").open("wb") as f: @@ -250,8 +263,8 @@ def Exec(code): pickletools.dis(data, f) for com, arg, _ in pickletools.genops(data): - if arg and (isinstance(arg, str) or - p.PY3 and isinstance(arg, bytes)) and len(arg) > 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/unrpyc.py b/unrpyc.py index 609f127..e9eb3ed 100755 --- a/unrpyc.py +++ b/unrpyc.py @@ -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)