Skip to content

Instantly share code, notes, and snippets.

@twilight-sparkle-irl
Last active July 18, 2020 07:41
Show Gist options
  • Save twilight-sparkle-irl/9db99db66ec97515f0ab94a38b9e4b9c to your computer and use it in GitHub Desktop.
Save twilight-sparkle-irl/9db99db66ec97515f0ab94a38b9e4b9c to your computer and use it in GitHub Desktop.
renpy monkeypatch bullshit, using CensoredUsername's work

renpy monkeypatching magic
made for DDLC, probably works on other stuff in...ways

requires picklemagic
honestly just download unrpyc and put this all in the un.rpyc folder

by default this

  1. edits any python inline with config.keymap['game_menu'].remove('mouseup_3') in it to have print("Forgery python loaded!") at the top
  2. edits any attempt to set config.developer to anything but True to set it to True
  3. forces every dialogue, as best it can, to be variations on "hello"

i'm not proud of this

import os
import os.path as path
import sys
import types
import re
# ctrl-f "!!!" to find the places where it does visible effects
# ensures that the directory leading up to a file exists
def ensure_dir(filename):
dir = path.dirname(filename)
if dir and not path.exists(dir):
os.makedirs(dir)
# make log file
logfile = path.join(os.getcwd(), "forgery.txt")
ensure_dir(logfile) # absolutely not necessary
l = open(logfile,'w')
# replaces the function <name> with <replacement> in <clazz>
# i just copy and pasted this off of stackoverflow, tbh
def patch(clazz, name, replacement):
def wrap_original(orig):
def wrapper(*args, **kwargs):
return replacement(orig, *args, **kwargs)
return wrapper
orig = getattr(clazz, name)
setattr(clazz, name, wrap_original(orig))
# generates a new PyCode object based on another
def new_code(code, loc=('<none>',1), mode='exec', orig=None):
if orig: # if orig isn't none, base everything off that
loc = (orig.location[0], orig.location[1])
mode = orig.mode
return sys.modules['renpy.ast'].PyCode(code, loc, mode)
def init_patch(initcode):
for i, (prio, obj) in enumerate(initcode):
# example python hook
if obj.__class__ == sys.modules['renpy.ast'].Python: # !!! print()
# in definitions.rpy
if "config.keymap['game_menu'].remove('mouseup_3')" in obj.code.source and "Forgery python loaded!" not in obj.code.source:
l.write('- initcode[{0}]<Python>: {1}\n'.format(i,obj))
# inject code at the top
obj.code = new_code('print("Forgery python loaded!")\n{0}'.format(obj.code.source),orig=obj.code)
l.write('- Code injected\n')
# example define replacement
elif obj.__class__ == sys.modules['renpy.ast'].Define: # !!! Enable developer console
# in definitions.rpy, if setting config.developer to anything but True
if obj.store == 'store.config' and obj.varname == 'developer' and obj.code.source != 'True':
l.write('- initcode[{0}]<Define>: {1}[{2}] = {3}\n'.format(i,obj.store,obj.varname,obj.code.source))
obj.code = new_code('True', orig=obj.code) # make a PyCode that's just "True" based on the original, then replace it
l.write('- Patched to {0}\n'.format(obj.code.source)) # success
def dark_descent(stmts,depth):
# stmts is passed from the finish_load hook
for i, stmt in enumerate(stmts):
l.write('{0} {1}: '.format('-'*depth,i)) # logging, print header for current level
if stmt.__class__ == sys.modules['renpy.ast'].Say: # !!! Make everyone say Hello
l.write('{0}\n'.format(stmt.diff_info())) # this is dialogue! print it as it should look
punctuation = re.match('^[A-Z][^?!.]*([?.!])$', stmt.what) # get the punctuation
if punctuation is not None: punctuation = punctuation.group(1) # please get the punctuation
else: punctuation = '.' # if we can't get the punctuation settle for a period
stmt.what = "Hello{0}".format(punctuation) # print a hello with the punctuation from the dialogue
else: l.write('{0}\n'.format(stmt)) # this is not dialogue. just print the tag, we don't want 100MB log files
if hasattr(stmt,'block'): # if the object contains objects
if depth > 0x7F: # if we've descended 127 levels, we're probably stuck (DDLC only goes 5 levels deep)
l.write('\nWhat the hell? Depth exceeded 127, bailing out\n\n') # TODO: test on MAS or whatever choice-intensive mod i can find
return # hopefully this works, i'm not making a 127 level ren'py VN just to test
dark_descent(stmt.block, depth+1) # call dark_descent again on this new object while adding one level
# just a method because maybe initialize will do something else some day
def override_renpy():
if not sys.modules['renpy.script']: # might not be loaded into renpy?
l.write('Something is terribly wrong: no renpy.script\n')
return
# the actual meat and bones: the hook module
# params are basically useless except for initcode and orig, i just kept them because python would complain otherwise
def finish_load(orig, self, stmts, initcode, check_names=True, filename=None):
# https://github.com/renpy/renpy/blob/master/renpy/ast.py replace things based on this
l.write('- {0}\n'.format(filename or '???')) # what file are we in?
init_patch(initcode) # TODO: envelope this into dark_descent, hopefully?
dark_descent(stmts, 2) # descend into the syntax tree
orig(self, stmts, initcode, check_names, filename) # continue original function
patch(sys.modules['renpy.script'].Script,'finish_load',finish_load) # inject hook module
def initialize(): # where forgery.py calls in
l.write("forgery active\n") # say hello
l.write("python ver: {0}\n\n".format(sys.version)) # post python ver (hold out from when i was just testing)
try:
override_renpy()
except Exception, e: # this just handles if an exception happens in a way that renpy will be unhelpful for
l.write('\n')
traceback = sys.modules['traceback']
traceback.print_exc(None, l)
#!/usr/bin/env python
# Copyright (c) 2014 CensoredUsername
#
# 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 zlib
import argparse
import os, sys
import minimize
from os import path
parser = argparse.ArgumentParser(description="Pack stuff into hi.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("-m", "--magic-path", dest="magic", action="store", default="picklemagic",
help="In case picklemagic isn't in the python search path you can specify its folder here")
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()
sys.path.append(path.abspath(args.magic))
protocol = int(args.protocol)
try:
import pickleast as p
except ImportError:
exit("Could not import pickleast. Are you sure it's in pythons module search path?")
def Module(name, filename, munge_globals=True):
with open(filename, "rb" if p.PY2 else "r") as f:
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)
return p.Module(name, code)
def Exec(code):
if args.minimize:
# In exec, we should always munge globals
code = minimize.minimize(code, True, True, args.obfuscate, args.obfuscate)
return p.Exec(code)
pack_folder = path.dirname(path.abspath(__file__))
base_folder = path.dirname(pack_folder)
# this is basically the only part i edited
code = p.ExecTranspile("""
_0
import forge
forge.initialize()
# fake normalcy
from renpy import script_version
from renpy.game import script
({'version': script_version, 'key': script.key}, [])
""", (
Module("forge", path.join(pack_folder, "forgery-script.py")),
))
unrpyc = zlib.compress(
p.optimize(
p.dumps(code, protocol),
protocol),
9)
with open(path.join(pack_folder, "hi.rpyc"), "wb") as f:
f.write(unrpyc)
if args.debug:
print("File length = {0}".format(len(unrpyc)))
import pickletools
data = zlib.decompress(unrpyc)
with open(path.join(pack_folder, "un.dis"), "wb" if p.PY2 else "w") as f:
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 p.PY3 and isinstance(arg, str):
arg = arg.encode("latin1")
data = zlib.decompress(arg)
break
else:
raise Exception("didn't find the gzipped blob inside")
with open(path.join(pack_folder, "un.dis2"), "wb" if p.PY2 else "w") as f:
pickletools.dis(data, f)
with open(path.join(pack_folder, "un.dis3"), "wb" if p.PY2 else "w") as f:
p.pprint(code, f)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment