diff options
Diffstat (limited to 'unrpyc')
-rw-r--r-- | unrpyc/LICENSE | 17 | ||||
-rw-r--r-- | unrpyc/Makefile | 16 | ||||
-rw-r--r-- | unrpyc/README | 15 | ||||
-rw-r--r-- | unrpyc/decompiler.py | 499 | ||||
-rwxr-xr-x | unrpyc/fix.js | 6 | ||||
-rw-r--r-- | unrpyc/unrpyc.py | 140 |
6 files changed, 693 insertions, 0 deletions
diff --git a/unrpyc/LICENSE b/unrpyc/LICENSE new file mode 100644 index 0000000..8a8ff9b --- /dev/null +++ b/unrpyc/LICENSE @@ -0,0 +1,17 @@ +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. diff --git a/unrpyc/Makefile b/unrpyc/Makefile new file mode 100644 index 0000000..86dcac3 --- /dev/null +++ b/unrpyc/Makefile @@ -0,0 +1,16 @@ +all: $(patsubst %.rpyc,%.json,$(wildcard *.rpyc)) + +%.json.o: %.rpyc *.py + python unrpyc.py --clobber $< $@ + +%.json: %.json.o + node fix.js $< $@ + +clean: + rm -f *.json.o *.json + +test: all + jshint --show-non-errors *.json + +.PHONY: all clean test +.PRECIOUS: %.json.o diff --git a/unrpyc/README b/unrpyc/README new file mode 100644 index 0000000..c046fe8 --- /dev/null +++ b/unrpyc/README @@ -0,0 +1,15 @@ +This is a !@#$ed up version of unrpyc to generate sort-of JSON from Ren'Py .rpyc files. + +It's made for Katawa Shoujo script files; trying to use it on anything else will probably break horribly. + +How to use: + +1. Copy Ren'Py distribution into "renpy" folder. +2. Execute "2to3 -w renpy/*.py". +3. Put the rpyc files from the official KS distribution in here. +4. Run "make" with appropriate -j options (yay auto-parallelization) +5. Copy the resulting .json files to /www/scripts. + +If you want to hack on the code, have fun. This is all the documentation you get. + +https://github.com/yuriks/unrpyc diff --git a/unrpyc/decompiler.py b/unrpyc/decompiler.py new file mode 100644 index 0000000..c6046f6 --- /dev/null +++ b/unrpyc/decompiler.py @@ -0,0 +1,499 @@ +# Copyright (c) 2012 Yuri K. Schlesner +# +# 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. + +import ast as python_ast +import renpy.ast as ast +import renpy.atl as atl + +DECOMPILE_SCREENS = False +firstLabel = True +warnedATL = False + +def pretty_print_ast(out_file, ast): + out_file.write('{') + for stmt in ast: + print_statement(out_file, stmt, 0) + out_file.write(']}') + +def indent(f, level): + # Print indentation + f.write(' ' * level) + +def print_statement(f, statement, indent_level=0): + indent(f, indent_level) + + func = statement_printer_dict.get(type(statement), print_Unknown) + func(f, statement, indent_level) + +def escape_string(s): + s = s.replace('"', '\\"') + s = s.replace('\n', '\\n') + s = s.replace('\t', '\\t') + return s + +# TODO "choice" and "parallel" blocks are greedily combined +# so we need a "pass" statement to separate them if +# multiple of the same block are immediately after +# each other. +def print_atl(f, atl_block, indent_level): + if not warnedATL: + global warnedATL + warnedATL = True + print("ATL not yet implemented") + return + if not atl_block.statements: + indent(f, indent_level) + f.write('pass\n') + for stmt in atl_block.statements: + indent(f, indent_level) + + if type(stmt) is atl.RawMultipurpose: + # warper + if stmt.warp_function: + f.write("warp %s" % (stmt.warp_function.strip(), )) + f.write(" %s " % (stmt.duration.strip(), )) + elif stmt.warper: + f.write(stmt.warper) + f.write(" %s " % (stmt.duration.strip(), )) + elif stmt.duration.strip() != '0': + f.write('pause') + f.write(" %s" % (stmt.duration.strip(), )) + + # revolution + if stmt.revolution: + f.write("%s " % (stmt.revolution, )) + + # circles + if stmt.circles != "0": + f.write("circles %s " % (stmt.circles.strip(), )) + + # splines + for (name, exprs) in stmt.splines: + f.write("%s " % (name, )) + for expr in exprs: + f.write("knot %s " % (expr.strip(), )) + + # properties + for (k, v) in stmt.properties: + f.write("%s %s " % (k, v.strip())) + + # with + for (expr, with_expr) in stmt.expressions: + f.write("%s " % (expr.strip(), )) + if with_expr: + f.write("with %s " % (with_expr, )) + + f.write("\n") + + elif type(stmt) is atl.RawBlock: + # what does stmt.animation do? + f.write("block:\n") + print_atl(f, stmt, indent_level + 1) + + elif type(stmt) is atl.RawChoice: + first = True + for (chance, block) in stmt.choices: + if first: + first = False + else: + indent(f, indent_level) + + f.write("choice") + if chance != "1.0": + f.write(" %s" % (chance, )) + f.write(":\n") + print_atl(f, block, indent_level + 1) + + elif type(stmt) is atl.RawContainsExpr: + f.write("contains %s\n" % (stmt.expression, )) + + elif type(stmt) is atl.RawEvent: + f.write("event %s\n" % (stmt.name, )) + + elif type(stmt) is atl.RawFunction: + f.write("function %s\n" % (stmt.expr, )) + + elif type(stmt) is atl.RawOn: + first = True + for name, block in list(stmt.handlers.items()): + if first: + first = False + else: + indent(f, indent_level) + + f.write("on %s:\n" % (name, )) + print_atl(f, block, indent_level + 1) + + elif type(stmt) is atl.RawParallel: + first = True + for block in stmt.blocks: + if first: + first = False + else: + indent(f, indent_level) + + f.write("parallel:\n") + print_atl(f, block, indent_level + 1) + + elif type(stmt) is atl.RawRepeat: + f.write("repeat") + if stmt.repeats: + f.write(" %s" % (stmt.repeats, )) # not sure if this is even a string + f.write("\n") + + elif type(stmt) is atl.RawTime: + f.write("time %s\n" % (stmt.time, )) + + else: + f.write("TODO atl.%s\n" % type(stmt).__name__) + +def print_imspec(f, imspec): + if imspec[1] is not None: # Expression + f.write('expression ') + f.write(escape_string(imspec[1])) + else: # Image name + delim = '' + for s in imspec[0]: + f.write(delim + escape_string(s)) + delim = '", "' + + # at + if len(imspec[3]) > 0: + f.write(' at ') + delim = '' + for s in imspec[3]: + f.write(delim + escape_string(s)) + delim = ', ' + + # as + if imspec[2] is not None: + f.write(" as %s" % (escape_string(imspec[2]), )) + + # behind + if len(imspec[6]) > 0: + f.write('", "behind", "') + delim = '' + for s in imspec[6]: + f.write(delim + escape_string(s)) + delim = ', ' + + f.write('"],') + +def print_Label(f, stmt, indent_level): + if firstLabel: + global firstLabel + firstLabel = False + else: + f.write("],\n") + f.write("\"%s\": [\n" % (stmt.name, )) + if stmt.parameters is not None: + print("Error: label params") + + for sub_stmt in stmt.block: + print_statement(f, sub_stmt, indent_level + 1) + +def print_Say(f, stmt, indent_level): + if stmt.who is not None: + f.write('["%s", ' % (escape_string(stmt.who), )) + else: + f.write("[") + f.write("\"%s\"]," % (escape_string(stmt.what), )) + if stmt.with_ is not None: + pass + #f.write(" with %s" % (stmt.with_, )) + f.write('\n') + +def print_Jump(f, stmt, indent_level): + f.write("jump ") + if stmt.expression: + # TODO expression + f.write("expression TODO") + else: + f.write(stmt.target) + f.write('\n') + +def print_Scene(f, stmt, indent_level): + f.write('["scene", "') + print_imspec(f, stmt.imspec) + + # with isn't handled here, but split in several statements + + f.write('\n') + if stmt.atl is not None: + print_atl(f, stmt.atl, indent_level+1) + +def print_With(f, stmt, indent_level): + f.seek(-3, 1) + f.write(', "%s"],\n' % (escape_string(stmt.expr), )) + +def print_Show(f, stmt, indent_level): + f.write('["show", "') + print_imspec(f, stmt.imspec) + + # with isn't handled here, but split in several statements + + if stmt.atl is not None: + f.write('\n') + print_atl(f, stmt.atl, indent_level+1) + else: + f.write('\n') + +def print_Hide(f, stmt, indent_level): + f.write('["hide", "') + print_imspec(f, stmt.imspec) + + # with isn't handled here, but split in several statements + + f.write('\n') + +class PrintRenPython(python_ast.NodeVisitor): + def __init__(self, f): + self.f = f + + def visit_Attribute(self, node): + return '"%s"' % node.attr + + def visit_Call(self, node): + self.f.write('[') + self.f.write(self.visit(node.func)) + self.f.write(', ') + self.f.write(', '.join(map(self.visit, node.args))) + self.f.write('],\n') + + def quote(self, string): + return '"%s"' % string + + def visit_Dict(self, node): + return self.quote(python_ast.dump(node)) + + def visit_Num(self, node): + return str(node.n) + + def visit_Name(self, node): + return self.quote(node.id) + + def visit_Str(self, node): + return self.quote(escape_string(node.s)) + +def print_Python(f, stmt, indent_level, early=False): + code_src = stmt.code.source + + stripped_code = code_src.strip() + + if stripped_code.count('\n') == 0: + stmt = compile(code_src, '<unknown>', 'exec', python_ast.PyCF_ONLY_AST).body[0] + PrintRenPython(f).visit(stmt) + else: + f.write("python") + if early: + f.write(" early") + if stmt.hide: + f.write(" hide") + f.write(":\n") + + for line in code_src.splitlines(True): + indent(f, indent_level + 1) + f.write(line) + +def print_Return(f, stmt, indent_level): + if stmt.expression is not None: + f.write(' "%s",' % (stmt.expression, )) + + f.write('\n') + +def print_UserStatement(f, stmt, indent_level): + f.write('["%s"],\n' % (escape_string(stmt.line).replace(' ', '", "'), )) + +def print_Init(f, stmt, indent_level): + f.write("init") + if stmt.priority != 0: + f.write(" %d" % (stmt.priority, )) + f.write(":\n") + for s in stmt.block: + print_statement(f, s, indent_level + 1) + +def print_Image(f, stmt, indent_level): + f.write("image %s" % (' '.join(stmt. imgname), )) + if stmt.code is not None: + f.write(" = %s\n" % (stmt.code.source, )) + else: + f.write("\n") + print_atl(f, stmt.atl, indent_level + 1) + +def print_Transform(f, stmt, indent_level): + f.write("transform %s" % (stmt.varname, )) + if stmt.parameters is not None: + print_params(f, stmt.parameters) + + f.write("\n") + print_atl(f, stmt.atl, indent_level + 1) + +def print_Menu(f, stmt, indent_level): + f.write('["menu", ') + + first = True + + for item in stmt.items: + indent(f, indent_level + 1) + + if first and item[2] is not None: + first = False + f.write(' {\n') + + # caption + f.write("\"%s\"" % (escape_string(item[0]), )) + + if first and item[2] is None: + first = False + f.write(', {') + + if item[2] is not None: + f.write(':') + for inner_stmt in item[2]: + print_statement(f, inner_stmt, indent_level + 2) + + f.write('}]\n') + +def print_Pass(f, stmt, indent_level): + f.write("pass\n") + +def print_Call(f, stmt, indent_level): + f.write("call ") + if stmt.expression: + f.write("expression %s" % (stmt.label, )) + else: + f.write(stmt.label) + + if stmt.arguments is not None: + if stmt.expression: + f.write("pass ") + print_args(f, stmt.arguments) + + f.write('\n') + +def print_If(f, stmt, indent_level): + f.write("if %s:\n" % (stmt.entries[0][0], )) + for inner_stmt in stmt.entries[0][1]: + print_statement(f, inner_stmt, indent_level + 1) + + if len(stmt.entries) >= 2: + if stmt.entries[-1][0].strip() == 'True': + else_entry = stmt.entries[-1] + elif_entries = stmt.entries[1:-1] + else: + else_entry = None + elif_entries = stmt.entries + + for case in elif_entries: + indent(f, indent_level) + f.write("elif %s:\n" % (case[0], )) + for inner_stmt in case[1]: + print_statement(f, inner_stmt, indent_level + 1) + + if else_entry is not None: + indent(f, indent_level) + f.write("else:\n") + for inner_stmt in else_entry[1]: + print_statement(f, inner_stmt, indent_level + 1) + +def print_EarlyPython(f, stmt, indent_level): + print_Python(f, stmt, indent_level, early=True) + +# TODO extrapos, extrakw? +def print_args(f, arginfo): + if arginfo is None: + return + + f.write("(") + + first = True + for (name, val) in arginfo.arguments: + if first: + first = False + else: + f.write(", ") + + if name is not None: + f.write("%s = " % (name, )) + f.write(val) + + f.write(")") + +# TODO positional? +def print_params(f, paraminfo): + f.write("(") + + first = True + for param in paraminfo.parameters: + if first: + first = False + else: + f.write(", ") + + f.write(param[0]) + + if (param[1] is not None) and ('None' not in param[1]): + f.write(" = %s" % param[1]) + if paraminfo.extrapos: + f.write(", ") + f.write("*%s" % paraminfo.extrapos) + if paraminfo.extrakw: + f.write(", ") + f.write("**%s" % paraminfo.extrakw) + + f.write(")") + +# Print while command, from http://forum.cheatengine.org/viewtopic.php?p=5377683 +def print_While(f, stmt, indent_level): + f.write("while %s:\n" % (stmt.condition, )) + for inner_stmt in stmt.block: + print_statement(f, inner_stmt, indent_level + 1) + +# Print define command, by iAmGhost +def print_Define(f, stmt, indent_level): + f.write("define %s = %s\n" % (stmt.varname, stmt.code.source,)) + + +statement_printer_dict = { + ast.Label: print_Label, + ast.Say: print_Say, + ast.Jump: print_Jump, + ast.Scene: print_Scene, + ast.With: print_With, + ast.Show: print_Show, + ast.Hide: print_Hide, + ast.Python: print_Python, + ast.Return: print_Return, + ast.UserStatement: print_UserStatement, + ast.Init: print_Init, + ast.Image: print_Image, + ast.Transform: print_Transform, + ast.Menu: print_Menu, + ast.Pass: print_Pass, + ast.Call: print_Call, + ast.If: print_If, + ast.While: print_While, + ast.Define: print_Define, + ast.EarlyPython: print_EarlyPython, + } + +def print_Unknown(f, stmt, indent_level): + print(("Unknown AST node: %s" % (type(stmt).__name__, ))) + f.write("<<<UNKNOWN NODE %s>>>\n" % (type(stmt).__name__, )) diff --git a/unrpyc/fix.js b/unrpyc/fix.js new file mode 100755 index 0000000..88a9646 --- /dev/null +++ b/unrpyc/fix.js @@ -0,0 +1,6 @@ +var fs = require('fs'); + +fs.readFile(process.argv[2], function (err, data) { + if (err) throw err; + fs.writeFile(process.argv[3], JSON.stringify(eval('(' + data + ')'))); +}); diff --git a/unrpyc/unrpyc.py b/unrpyc/unrpyc.py new file mode 100644 index 0000000..f423cc8 --- /dev/null +++ b/unrpyc/unrpyc.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python2 + +# Copyright (c) 2012 Yuri K. Schlesner +# +# 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. + +import optparse +import os.path +import sys +import pickle as pickle +import codecs +import glob +import itertools +import zlib + +class Dummy: + def record_pycode(self,*args,**kwargs): + return + all_pycode = [] + +def import_renpy(basedir=None): + #import renpy from another location. + if basedir: + sys.path.append(basedir) + global renpy + global decompiler + + # Needed for pickle to read the AST + try: + import renpy + except ImportError: + print("\nFailed at importing renpy. Are you sure that the renpy directory can be found in sys.path or the current working directory?\n") + raise + # try to import as much renpy modules as possible, but some modules might not exist + # in older ren'py versions. + try: import renpy.log + except: pass + try: import renpy.display + except: pass + try: import renpy.object + except: pass + try: + import renpy.game + renpy.game.script = Dummy() + except: pass + try: import renpy.loader + except: pass + try: import renpy.ast + except: pass + try: import renpy.atl + except: pass + try: import renpy.curry + except: pass + try: import renpy.easy + except: pass + try: import renpy.execution + except: pass + try: import renpy.loadsave + except: pass + try: import renpy.parser + except: pass + try: import renpy.python + except: pass + try: import renpy.script + except: pass + try: import renpy.statements + except: pass + try: import renpy.style + except: pass + + import decompiler + if basedir: + sys.path.remove(basedir) + + +def read_ast_from_file(in_file): + raw_contents = zlib.decompress(in_file.read()) + data, stmts = pickle.loads(raw_contents) + return stmts + +def decompile_rpyc(input_filename, out_filename, overwrite=False): + # Output filename is input filename but with .rpy extension + path, ext = os.path.splitext(input_filename) + + print(("Decompiling %s to %s..." % (input_filename, out_filename))) + + if not overwrite and os.path.exists(out_filename): + print("Output file already exists. Pass --clobber to overwrite.") + return False # Don't stop decompiling if one file already exists + + with open(input_filename, 'rb') as in_file: + ast = read_ast_from_file(in_file) + + with codecs.open(out_filename, 'w', encoding='utf-8') as out_file: + decompiler.pretty_print_ast(out_file, ast) + return True + +if __name__ == "__main__": + parser = optparse.OptionParser( + usage="usage: %prog [options] input output", + version="%prog 0.1") + + parser.add_option('-c', '--clobber', action='store_true', dest='clobber', + default=False, help="overwrites existing output files") + + parser.add_option('-b', '--basedir', action='store', dest='basedir', + help="specify the game base directory in which the 'renpy' directory is located") + + options, args = parser.parse_args() + + + if options.basedir: + import_renpy(options.basedir) + else: + import_renpy() + + if len(args) != 2: + parser.print_help(); + parser.error("Incorrect number of arguments: expected 2, got %d." % (len(args))) + + decompile_rpyc(args[0], args[1], options.clobber) +else: + import_renpy() + |