default persistent.mods_enabled = set() default mods_parsed = set() init python: import json import os import renpy.error as rpy_error mods_list = dict() def import_mods(): global mods_list all_files = renpy.list_files() if renpy.android: # Include files outside the application archive and strip the directory path. # Normally it wouldn't be necessary but `renpy.list_files` does not list files outside archives on android. for dir in config.searchpath: all_files.extend([os.path.join(path.replace(dir, ""), name) for path, _, files in os.walk(dir) for name in files]) mods = filter(lambda x: x.endswith(".json"), all_files) for i, manifest in enumerate(mods): path = os.path.split(manifest)[0] files = filter(lambda x: path in x, all_files) scripts = filter(lambda x: x.endswith(".rpym"), files) logo = "{}/logo.webp".format(path) if not renpy.loadable(logo): logo = "#000" # Read manifest with renpy.file(manifest) as f: data = json.load(f) modname = data.get("Name", None) if not modname: continue mods_list[modname] = data mods_list[modname]["Files"] = files mods_list[modname]["Path"] = path mods_list[modname]["LoadOrder"] = i # TODO: Make load order customisable mods_list[modname]["Logo"] = logo for mod in list(persistent.mods_enabled): if not mods_list.get(mod, None): persistent.mods_enabled.remove(mod) return def parse_mods(): if main_menu or _menu: return for mod in list(persistent.mods_enabled): if mod in mods_parsed: continue path = mods_list[mod]["Path"] files = mods_list[mod]["Files"] for file in files: if not file.endswith(".rpym"): continue fn = os.path.split(file)[1] with renpy.file(file) as s: data = s.read() print "Loading '{}'".format(mod) #renpy.load_module(os.path.splitext(file)[0]) try: renpy.load_string(data, filename="game/{}/{}".format(path, fn)) except Exception as e: print "Loading '{}' has failed.\nFile: {}\nError: {}".format(mod, fn, e) mods_parsed.add(mod) renpy.execute_default_statement(False) return def toggle_mod(mod): if not main_menu: renpy.notify("Mods can be enabled or disabled from the main menu only.") return mods = persistent.mods_enabled if mod in mods: renpy.notify("Mod disabled.") mods.remove(mod) else: renpy.notify("Mod Enabled.") mods.add(mod) # # Custom parser w/ exception handling # def parse_script(fn, filedata=None, linenumber=1): renpy.game.exception_info = 'While parsing ' + fn + '.' try: lines = renpy.parser.list_logical_lines(fn, filedata, linenumber) nested = renpy.parser.group_logical_lines(lines) except ParseError as e: renpy.parser.parse_errors.append(e.message) if not fn.endswith(".rpym"): return None l = renpy.parser.Lexer(nested) rv = renpy.parser.parse_block(l) if renpy.parser.parse_errors: if fn.endswith(".rpym"): renpy.store.report_parse_errors(fn, linenumber, renpy.parser.parse_errors) return None if rv: rv.append(renpy.parser.ast.Return((rv[-1].filename, rv[-1].linenumber), None)) return rv def report_parse_errors(file, linenumber, errors): if not errors: return False dp, fn = os.path.split(file) error_f, error_fn = rpy_error.open_error_file(os.path.join(dp, "errors.txt"), "w") with error_f: error_f.write(u"\ufeff") # BOM error_f.write(u"I'm sorry, but errors were detected in your mod script.\nPlease correct the errors listed below, and try again.\n\n") for i in errors: if not isinstance(i, str): i = str(i, "utf-8", "replace") error_f.write(i) error_f.write(u"\n\n") # We need to remove reported errors to avoid incorrectly detecting .rpym format in the next call. renpy.parser.parse_errors.remove(i) error_f.write(u"Game Version: {}\nRen'Py Version: {}\n{}".format(renpy.store.version, renpy.version_only, str(time.ctime()))) try: if renpy.game.args.command == "run": # @UndefinedVariable renpy.exports.launch_editor([error_fn], 1, transient=1) renpy.exports.launch_editor([file], linenumber) except: pass return True # We need to monkey patch our new parser with exception handlers for mods. renpy.parser.parse = parse_script # Note: Exception handling doesn't seem to be necessary at the moment. # Note2: Catching runtime errors and finding their file of origin might be tricky... # Note3: Might be worth 'sandboxing' mods parsing in the future # to be able to catch all errors prior to executing them on main store. Maybe. # Note4: Pickling monkey patched functions that are stored does not seem to be possible from within load_string # due to saves pickling their data much earlier than mods are loaded. # # def exception_handler(short, full, file): # renpy.display.error.report_exception(short, full, file) # define config.exception_handler = exception_handler init: $ import_mods() $ config.after_load_callbacks.append(parse_mods)