|
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) |