# Copyright (c) 2012-2024 Yuri K. Schlesner, CensoredUsername, Jackmcbarn # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. from .util import DecompilerBase, First, WordConcatenator, reconstruct_paraminfo, \ reconstruct_arginfo, string_escape, split_logical_lines, Dispatcher, \ say_get_code, OptionBase from .renpycompat import renpy from operator import itemgetter from io import StringIO from . import sl2decompiler from . import testcasedecompiler from . import atldecompiler from . import astdump __all__ = ["astdump", "magic", "sl2decompiler", "testcasedecompiler", "translate", "util", "Options", "pprint", "Decompiler", "renpycompat"] # Main API # Object that carries configurable decompilation options class Options(OptionBase): def __init__(self, indentation=" ", log=None, translator=None, init_offset=False, sl_custom_names=None): super(Options, self).__init__(indentation=indentation, log=log) # decompilation options self.translator = translator self.init_offset = init_offset self.sl_custom_names = sl_custom_names def pprint(out_file, ast, options=Options()): Decompiler(out_file, options).dump(ast) # Implementation class Decompiler(DecompilerBase): """ An object which handles the decompilation of renpy asts to a given stream """ # This dictionary is a mapping of Class: unbound_method, which is used to determine # what method to call for which ast class dispatch = Dispatcher() def __init__(self, out_file, options): super(Decompiler, self).__init__(out_file, options) self.paired_with = False self.say_inside_menu = None self.label_inside_menu = None self.in_init = False self.missing_init = False self.init_offset = 0 self.most_lines_behind = 0 self.last_lines_behind = 0 self.seen_label = False self.rpy_directive_arguments = [] def advance_to_line(self, linenumber): self.last_lines_behind = max( self.linenumber + (0 if self.skip_indent_until_write else 1) - linenumber, 0) self.most_lines_behind = max(self.last_lines_behind, self.most_lines_behind) super(Decompiler, self).advance_to_line(linenumber) def save_state(self): return (super(Decompiler, self).save_state(), self.paired_with, self.say_inside_menu, self.label_inside_menu, self.in_init, self.missing_init, self.most_lines_behind, self.last_lines_behind, self.seen_label, self.rpy_directive_arguments) def commit_state(self, state): super(Decompiler, self).commit_state(state[0]) def rollback_state(self, state): self.paired_with = state[1] self.say_inside_menu = state[2] self.label_inside_menu = state[3] self.in_init = state[4] self.missing_init = state[5] self.most_lines_behind = state[6] self.last_lines_behind = state[7] self.seen_label = state[8] self.rpy_directive_arguments = state[9] super(Decompiler, self).rollback_state(state[0]) def dump(self, ast): if self.options.translator: self.options.translator.translate_dialogue(ast) if self.options.init_offset and isinstance(ast, (tuple, list)): self.set_best_init_offset(ast) # skip_indent_until_write avoids an initial blank line super(Decompiler, self).dump(ast, skip_indent_until_write=True) # if there's anything we wanted to write out but didn't yet, do it now for m in self.blank_line_queue: m(None) self.write("\n# Decompiled by unrpyc: https://github.com/CensoredUsername/unrpyc\n") assert not self.missing_init, "A required init, init label, or translate block was missing" def print_node(self, ast): # if this is a say node that appears in a menu, we need to not advance to it and skip it if isinstance(ast, renpy.ast.Say): if self.handle_say_possibly_inside_menu(ast): return # We special-case line advancement for some types in their print # methods, so don't advance lines for them here. if hasattr(ast, 'linenumber') and not isinstance( ast, (renpy.ast.TranslateString, renpy.ast.With, renpy.ast.Label, renpy.ast.Pass, renpy.ast.Return) ): self.advance_to_line(ast.linenumber) self.dispatch.get(type(ast), type(self).print_unknown)(self, ast) # ATL subdecompiler hook def print_atl(self, ast): self.linenumber = atldecompiler.pprint( self.out_file, ast, self.options, self.indent_level, self.linenumber, self.skip_indent_until_write ) self.skip_indent_until_write = False # Displayable related functions def print_imspec(self, imspec): if imspec[1] is not None: begin = f'expression {imspec[1]}' else: begin = " ".join(imspec[0]) words = WordConcatenator(begin and begin[-1] != ' ', True) if imspec[2] is not None: words.append(f'as {imspec[2]}') if len(imspec[6]) > 0: words.append(f'behind {", ".join(imspec[6])}') if isinstance(imspec[4], str): words.append(f'onlayer {imspec[4]}') if imspec[5] is not None: words.append(f'zorder {imspec[5]}') if len(imspec[3]) > 0: words.append(f'at {", ".join(imspec[3])}') self.write(begin + words.join()) return words.needs_space @dispatch(renpy.ast.Image) def print_image(self, ast): self.require_init() self.indent() self.write(f'image {" ".join(ast.imgname)}') if ast.code is not None: self.write(f' = {ast.code.source}') else: if ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.Transform) def print_transform(self, ast): self.require_init() self.indent() # If we have an implicit init block with a non-default priority, we need to store the # priority here. priority = "" if isinstance(self.parent, renpy.ast.Init): init = self.parent if (init.priority != self.init_offset and len(init.block) == 1 and not self.should_come_before(init, ast)): priority = f' {init.priority - self.init_offset}' self.write(f'transform{priority} {ast.varname}') if ast.parameters is not None: self.write(reconstruct_paraminfo(ast.parameters)) # atl attribute: since 6.10 if ast.atl is not None: self.write(":") self.print_atl(ast.atl) # Directing related functions @dispatch(renpy.ast.Show) def print_show(self, ast): self.indent() self.write("show ") needs_space = self.print_imspec(ast.imspec) if self.paired_with: if needs_space: self.write(" ") self.write(f'with {self.paired_with}') self.paired_with = True # atl attribute: since 6.10 if ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.ShowLayer) def print_showlayer(self, ast): self.indent() self.write(f'show layer {ast.layer}') if ast.at_list: self.write(f' at {", ".join(ast.at_list)}') if ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.Scene) def print_scene(self, ast): self.indent() self.write("scene") if ast.imspec is None: if isinstance(ast.layer, str): self.write(f' onlayer {ast.layer}') needs_space = True else: self.write(" ") needs_space = self.print_imspec(ast.imspec) if self.paired_with: if needs_space: self.write(" ") self.write(f'with {self.paired_with}') self.paired_with = True # atl attribute: since 6.10 if ast.atl is not None: self.write(":") self.print_atl(ast.atl) @dispatch(renpy.ast.Hide) def print_hide(self, ast): self.indent() self.write("hide ") needs_space = self.print_imspec(ast.imspec) if self.paired_with: if needs_space: self.write(" ") self.write(f'with {self.paired_with}') self.paired_with = True @dispatch(renpy.ast.With) def print_with(self, ast): # the 'paired' attribute indicates that this with # and with node afterwards are part of a postfix # with statement. detect this and process it properly if ast.paired is not None: # Sanity check. check if there's a matching with statement two nodes further if not (isinstance(self.block[self.index + 2], renpy.ast.With) and self.block[self.index + 2].expr == ast.paired): raise Exception(f'Unmatched paired with {self.paired_with!r} != {ast.expr!r}') self.paired_with = ast.paired # paired_with attribute since 6.7.1 elif self.paired_with: # Check if it was consumed by a show/scene statement if self.paired_with is not True: self.write(f' with {ast.expr}') self.paired_with = False else: self.advance_to_line(ast.linenumber) self.indent() self.write(f'with {ast.expr}') self.paired_with = False @dispatch(renpy.ast.Camera) def print_camera(self, ast): self.indent() self.write("camera") if ast.layer != "master": self.write(f' {ast.layer}') if ast.at_list: self.write(f' at {", ".join(ast.at_list)}') if ast.atl is not None: self.write(":") self.print_atl(ast.atl) # Flow control @dispatch(renpy.ast.Label) def print_label(self, ast): # record that we've seen labels in this script file, and cannot strip the final return self.seen_label = True # If a Call block preceded us, it printed us as "from" if (self.index and isinstance(self.block[self.index - 1], renpy.ast.Call)): return # See if we're the label for a menu, rather than a standalone label. if not ast.block and ast.parameters is None: remaining_blocks = len(self.block) - self.index 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): 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)): self.label_inside_menu = ast return self.advance_to_line(ast.linenumber) self.indent() # It's possible that we're an "init label", not a regular label. There's no way to know # if we are until we parse our children, so temporarily redirect all of our output until # that's done, so that we can squeeze in an "init " if we are. out_file = self.out_file self.out_file = StringIO() missing_init = self.missing_init self.missing_init = False try: self.write(f'label {ast.name}{reconstruct_paraminfo(ast.parameters)}' f'{" hide" if ast.hide else ""}:') self.print_nodes(ast.block, 1) finally: if self.missing_init: out_file.write("init ") self.missing_init = missing_init out_file.write(self.out_file.getvalue()) self.out_file = out_file @dispatch(renpy.ast.Jump) def print_jump(self, ast): self.indent() self.write(f'jump {"expression " if ast.expression else ""}{ast.target}') @dispatch(renpy.ast.Call) def print_call(self, ast): self.indent() words = WordConcatenator(False) words.append("call") if ast.expression: words.append("expression") words.append(ast.label) if ast.arguments is not None: if ast.expression: words.append("pass") words.append(reconstruct_arginfo(ast.arguments)) # We don't have to check if there's enough elements here, # since a Label or a Pass is always emitted after a Call. next_block = self.block[self.index + 1] if isinstance(next_block, renpy.ast.Label): words.append(f'from {next_block.name}') self.write(words.join()) @dispatch(renpy.ast.Return) def print_return(self, ast): if (ast.expression is None and self.parent is None and self.index + 1 == len(self.block) and self.index and ( ast.linenumber == self.block[self.index - 1].linenumber or isinstance(self.block[self.index - 1], (renpy.ast.Return, renpy.ast.Jump)) or not self.seen_label ) ): # 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. # As of Ren'Py commit e02c0a9 we can't detect if these return statements # are automatically generated anymore, so now we auto-strip them if # there was a return statement before this one, an unconditional jump, or # if there were no labels in this file so there wouldn't be any script execution anyway. return self.advance_to_line(ast.linenumber) self.indent() self.write("return") if ast.expression is not None: self.write(f' {ast.expression}') @dispatch(renpy.ast.If) def print_if(self, ast): statement = First("if", "elif") for i, (condition, block) in enumerate(ast.entries): # The unicode string "True" is used as the condition for else:. # But if it's an actual expression, it's a renpy.ast.PyExpr if (i + 1) == len(ast.entries) and not isinstance(condition, ( renpy.ast.PyExpr, renpy.astsupport.PyExpr)): self.indent() self.write("else:") else: if (hasattr(condition, 'linenumber')): self.advance_to_line(condition.linenumber) self.indent() self.write(f'{statement()} {condition}:') self.print_nodes(block, 1) @dispatch(renpy.ast.While) def print_while(self, ast): self.indent() self.write(f'while {ast.condition}:') self.print_nodes(ast.block, 1) @dispatch(renpy.ast.Pass) def print_pass(self, ast): if (self.index and isinstance(self.block[self.index - 1], renpy.ast.Call)): return if (self.index > 1 and isinstance(self.block[self.index - 2], renpy.ast.Call) and isinstance(self.block[self.index - 1], renpy.ast.Label) and self.block[self.index - 2].linenumber == ast.linenumber): return self.advance_to_line(ast.linenumber) self.indent() self.write("pass") def should_come_before(self, first, second): return first.linenumber < second.linenumber def require_init(self): if not self.in_init: self.missing_init = True def set_best_init_offset(self, nodes): votes = {} for ast in nodes: if not isinstance(ast, renpy.ast.Init): continue offset = ast.priority # Keep this block in sync with print_init if len(ast.block) == 1 and not self.should_come_before(ast, ast.block[0]): if isinstance(ast.block[0], renpy.ast.Screen): offset -= -500 elif isinstance(ast.block[0], renpy.ast.Testcase): offset -= 500 elif isinstance(ast.block[0], renpy.ast.Image): offset -= 500 votes[offset] = votes.get(offset, 0) + 1 if votes: winner = max(votes, key=votes.get) # It's only worth setting an init offset if it would save # more than one priority specification versus not setting one. if votes.get(0, 0) + 1 < votes[winner]: self.set_init_offset(winner) def set_init_offset(self, offset): def do_set_init_offset(linenumber): # if we got to the end of the file and haven't emitted this yet, # don't bother, since it only applies to stuff below it. if linenumber is None or linenumber - self.linenumber <= 1 or self.indent_level: return True if offset != self.init_offset: self.indent() self.write(f'init offset = {offset}') self.init_offset = offset return False self.do_when_blank_line(do_set_init_offset) @dispatch(renpy.ast.Init) def print_init(self, ast): in_init = self.in_init self.in_init = True try: # A bunch of statements can have implicit init blocks # Define has a default priority of 0, screen of -500 and image of 990 # Keep this block in sync with set_best_init_offset # TODO merge this and require_init into another decorator or something if (len(ast.block) == 1 and (isinstance(ast.block[0], (renpy.ast.Define, renpy.ast.Default, renpy.ast.Transform)) or (ast.priority == -500 + self.init_offset and isinstance(ast.block[0], renpy.ast.Screen)) or (ast.priority == self.init_offset and isinstance(ast.block[0], renpy.ast.Style)) or (ast.priority == 500 + self.init_offset and isinstance(ast.block[0], renpy.ast.Testcase)) or (ast.priority == 0 + self.init_offset and isinstance(ast.block[0], renpy.ast.UserStatement) and ast.block[0].line.startswith("layeredimage ")) or (ast.priority == 500 + self.init_offset and isinstance(ast.block[0], renpy.ast.Image))) and not (self.should_come_before(ast, ast.block[0]))): # If they fulfill this criteria we just print the contained statement self.print_nodes(ast.block) # translatestring statements are split apart and put in an init block. elif (len(ast.block) > 0 and ast.priority == self.init_offset and all(isinstance(i, renpy.ast.TranslateString) for i in ast.block) and all(i.language == ast.block[0].language for i in ast.block[1:])): self.print_nodes(ast.block) else: self.indent() self.write("init") if ast.priority != self.init_offset: self.write(f' {ast.priority - self.init_offset}') if len(ast.block) == 1 and not self.should_come_before(ast, ast.block[0]): self.write(" ") self.skip_indent_until_write = True self.print_nodes(ast.block) else: self.write(":") self.print_nodes(ast.block, 1) finally: self.in_init = in_init def print_say_inside_menu(self): self.print_say(self.say_inside_menu, True) self.say_inside_menu = None def print_menu_item(self, label, condition, block, arguments): self.indent() self.write(f'"{string_escape(label)}"') if arguments is not None: self.write(reconstruct_arginfo(arguments)) if block is not None: # ren'py uses the unicode string "True" as condition when there isn't one. if isinstance(condition, (renpy.ast.PyExpr, renpy.astsupport.PyExpr)): self.write(f' if {condition}') self.write(":") self.print_nodes(block, 1) @dispatch(renpy.ast.Menu) def print_menu(self, ast): self.indent() self.write("menu") if self.label_inside_menu is not None: self.write(f' {self.label_inside_menu.name}') self.label_inside_menu = None # arguments attribute added in 7.1.4 if ast.arguments is not None: self.write(reconstruct_arginfo(ast.arguments)) self.write(":") with self.increase_indent(): if ast.with_ is not None: self.indent() self.write(f'with {ast.with_}') if ast.set is not None: self.indent() self.write(f'set {ast.set}') # item_arguments attribute since 7.1.4 if ast.item_arguments is not None: item_arguments = ast.item_arguments else: item_arguments = [None] * len(ast.items) for (label, condition, block), arguments in zip(ast.items, item_arguments): if self.options.translator: label = self.options.translator.strings.get(label, label) state = None # if the condition is a unicode subclass with a "linenumber" attribute it was # script. # If it isn't ren'py used to insert a "True" string. This string used to be of # type str but nowadays it's of 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 self.print_say_inside_menu() self.advance_to_line(condition.linenumber) elif self.say_inside_menu is not None: # The hard case: we don't know the line number that the menu item is on # So try to put it in, but be prepared to back it out if that puts us # behind on the line number state = self.save_state() self.most_lines_behind = self.last_lines_behind self.print_say_inside_menu() self.print_menu_item(label, condition, block, arguments) if state is not None: # 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: # 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 self.print_say_inside_menu() # Programming related functions @dispatch(renpy.ast.Python) def print_python(self, ast, early=False): self.indent() code = ast.code.source # pre ren'py 8.4, python blocks were stored un-indented with a leading \n # after this, python blocks are stored with indentation, without a leading \n. indented = code and (code[0] == " ") leading_newline = code and (code[0] == "\n") if not (indented or leading_newline): # single-line python statement self.write(f'$ {code}') return if leading_newline: code = code[1:] self.write("python") if early: self.write(" early") if ast.hide: self.write(" hide") # store attribute added in 6.14 if ast.store != "store": self.write(" in ") # Strip prepended "store." self.write(ast.store[6:]) self.write(":") if indented: self.write(f"\n{code}") else: with self.increase_indent(): self.write_lines(split_logical_lines(code)) @dispatch(renpy.ast.EarlyPython) def print_earlypython(self, ast): self.print_python(ast, early=True) @dispatch(renpy.ast.Define) 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. priority = "" if isinstance(self.parent, renpy.ast.Init): init = self.parent if (init.priority != self.init_offset and len(init.block) == 1 and not self.should_come_before(init, ast)): priority = f' {init.priority - self.init_offset}' index = "" # index attribute added in 7.4 if ast.index is not None: index = f'[{ast.index.source}]' # operator attribute added in 7.4 operator = ast.operator # store attribute added in 6.18.2 if ast.store == "store": self.write(f'define{priority} {ast.varname}{index} {operator} {ast.code.source}') else: self.write( f'define{priority} {ast.store[6:]}.{ast.varname}{index} {operator} ' f'{ast.code.source}') @dispatch(renpy.ast.Default) 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. priority = "" if isinstance(self.parent, renpy.ast.Init): init = self.parent if (init.priority != self.init_offset and len(init.block) == 1 and not self.should_come_before(init, ast)): priority = f' {init.priority - self.init_offset}' if ast.store == "store": self.write(f'default{priority} {ast.varname} = {ast.code.source}') else: self.write(f'default{priority} {ast.store[6:]}.{ast.varname} = {ast.code.source}') # Specials # Returns whether a Say statement immediately preceding a Menu statement # actually belongs inside of the Menu statement. def say_belongs_to_menu(self, say, menu): return (not say.interact and say.who is not None and say.with_ is None and 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)) def handle_say_possibly_inside_menu(self, ast): # if this say statement precedes a menu statement, postpone emitting it until we're # handling the menu, and return True so we can know that it needs to be skipped if (self.index + 1 < len(self.block) and self.say_belongs_to_menu(ast, self.block[self.index + 1])): self.say_inside_menu = ast return True else: return False @dispatch(renpy.ast.Say) def print_say(self, ast, inmenu=False): self.indent() self.write(say_get_code(ast, inmenu)) @dispatch(renpy.ast.UserStatement) def print_userstatement(self, ast): self.indent() self.write(ast.line) # block attribute since 6.13.0 if ast.block is not None: with self.increase_indent(): self.print_lex(ast.block) def print_lex(self, lex): for entry in lex: if len(entry) == 4: file, linenumber, content, block = entry else: file, linenumber, _indent, content, block = entry self.advance_to_line(linenumber) self.indent() self.write(content) if block: with self.increase_indent(): self.print_lex(block) @dispatch(renpy.ast.Style) def print_style(self, ast): self.require_init() keywords = {ast.linenumber: WordConcatenator(False, True)} # These don't store a line number, so just put them on the first line if ast.parent is not None: keywords[ast.linenumber].append(f'is {ast.parent}') if ast.clear: keywords[ast.linenumber].append("clear") if ast.take is not None: keywords[ast.linenumber].append(f'take {ast.take}') for delname in ast.delattr: keywords[ast.linenumber].append(f'del {delname}') # These do store a line number if ast.variant is not None: if ast.variant.linenumber not in keywords: keywords[ast.variant.linenumber] = WordConcatenator(False) keywords[ast.variant.linenumber].append(f'variant {ast.variant}') for key, value in ast.properties.items(): if value.linenumber not in keywords: keywords[value.linenumber] = WordConcatenator(False) keywords[value.linenumber].append(f'{key} {value}') keywords = sorted([(k, v.join()) for k, v in keywords.items()], key=itemgetter(0)) self.indent() self.write(f'style {ast.style_name}') if keywords[0][1]: self.write(f' {keywords[0][1]}') if len(keywords) > 1: self.write(":") with self.increase_indent(): for i in keywords[1:]: self.advance_to_line(i[0]) self.indent() self.write(i[1]) # Translation functions @dispatch(renpy.ast.Translate) def print_translate(self, ast): self.indent() self.write(f'translate {ast.language or "None"} {ast.identifier}:') self.print_nodes(ast.block, 1) @dispatch(renpy.ast.EndTranslate) def print_endtranslate(self, ast): # an implicitly added node which does nothing... pass @dispatch(renpy.ast.TranslateString) def print_translatestring(self, ast): self.require_init() # Was the last node a translatestrings node? if not (self.index and isinstance(self.block[self.index - 1], renpy.ast.TranslateString) and self.block[self.index - 1].language == ast.language): self.indent() self.write(f'translate {ast.language or "None"} strings:') # TranslateString's linenumber refers to the line with "old", not to the # line with "translate ... strings: (above)" with self.increase_indent(): self.advance_to_line(ast.linenumber) self.indent() self.write(f'old "{string_escape(ast.old)}"') # newlock attribute since 6.99 if hasattr(ast, "newloc"): self.advance_to_line(ast.newloc[1]) self.indent() self.write(f'new "{string_escape(ast.new)}"') @dispatch(renpy.ast.TranslateBlock) @dispatch(renpy.ast.TranslateEarlyBlock) def print_translateblock(self, ast): self.indent() self.write(f'translate {ast.language or "None"} ') self.skip_indent_until_write = True in_init = self.in_init if (len(ast.block) == 1 and isinstance(ast.block[0], (renpy.ast.Python, renpy.ast.Style))): # Ren'Py counts the TranslateBlock from "translate python" and "translate # style" as an Init. self.in_init = True try: self.print_nodes(ast.block) finally: self.in_init = in_init # an optimized translate block with a single say statement @dispatch(renpy.ast.TranslateSay) def print_translate_say(self, ast): if ast.language: self.indent() self.write(f'translate {ast.language} {ast.identifier}:') with self.increase_indent(): self.print_say(ast, True) else: self.print_say(ast) # Screens @dispatch(renpy.ast.Screen) def print_screen(self, ast): self.require_init() screen = ast.screen if isinstance(screen, renpy.screenlang.ScreenLangScreen): raise Exception( "Decompiling screen language version 1 screens is no longer supported. " "use the legacy branch of unrpyc if this is required" ) if isinstance(screen, renpy.sl2.slast.SLScreen): self.linenumber = sl2decompiler.pprint( self.out_file, screen, self.options, self.indent_level, self.linenumber, self.skip_indent_until_write ) self.skip_indent_until_write = False else: self.print_unknown(screen) # Testcases @dispatch(renpy.ast.Testcase) def print_testcase(self, ast): self.require_init() self.indent() self.write(f'testcase {ast.label}:') self.linenumber = testcasedecompiler.pprint( self.out_file, ast.test.block, self.options, self.indent_level + 1, self.linenumber, self.skip_indent_until_write ) self.skip_indent_until_write = False # Rpy python directives @dispatch(renpy.ast.RPY) def print_rpy_python(self, ast): command, arg = ast.rest assert command == "python" self.rpy_directive_arguments.append(arg) # detect multiple ast.RPY nodes emitted from the same rpy python line next_index = self.index + 1 if (next_index < len(self.block) and isinstance(self.block[next_index], renpy.ast.RPY) and ast.linenumber == self.block[next_index].linenumber and command == self.block[next_index].rest[0]): # these arguments are to be merged with the next statement, so do nothing for now return self.indent() self.write(f"rpy {command} {', '.join(self.rpy_directive_arguments)}") self.rpy_directive_arguments = []