From 78192072534f34d44c169b5729486f82f918e834 Mon Sep 17 00:00:00 2001 From: LoafyLemon Date: Tue, 7 Mar 2023 19:29:29 +0000 Subject: [PATCH] Mods loader * Simplified and improved mods loading process by utilising the newly added include_module method * Added sanity checks for mods toggles * Added scripts list to mods list dict * Added ToggleMod action * Require restart to load mods * Fixed mods not loading at certain times --- game/scripts/gui/mods.rpy | 8 +- game/scripts/mods.rpy | 211 +++++++++++--------------------------- 2 files changed, 66 insertions(+), 153 deletions(-) diff --git a/game/scripts/gui/mods.rpy b/game/scripts/gui/mods.rpy index daf0040e..64ecf7d3 100644 --- a/game/scripts/gui/mods.rpy +++ b/game/scripts/gui/mods.rpy @@ -9,12 +9,15 @@ screen mods(): default selection = next(iter(mods_list.keys())) default checkbox_enabled = gui.theme("check_true") default checkbox_disabled = gui.theme("check_false") + default mods_enabled_now = set(persistent.mods_enabled) + $ awaits_restart = bool(mods_enabled_now != persistent.mods_enabled) fixed: ## The grid of file slots. hbox: spacing 5 + vpgrid: cols 1 scrollbars "vertical" @@ -37,8 +40,9 @@ screen mods(): $ enabled = bool(name in persistent.mods_enabled) $ selected = (name == selection) + if selected: - $ action = Function(toggle_mod, name) + $ action = ToggleMod(name) else: $ action = SetScreenVariable("selection", name) @@ -70,6 +74,8 @@ screen mods(): else: add checkbox_disabled align (0.95, 0.5) + if awaits_restart: + text "Awaiting game restart to apply changes..." frame: style gui.theme("frame") diff --git a/game/scripts/mods.rpy b/game/scripts/mods.rpy index 5e494166..7c914dc6 100644 --- a/game/scripts/mods.rpy +++ b/game/scripts/mods.rpy @@ -1,174 +1,81 @@ default persistent.mods_enabled = set() -default mods_parsed = set() -init python: - import json - import os - import renpy.error as rpy_error - from renpy.parser import ParseError +init -1000: + python: + import json + import os - mods_list = dict() + mods_list = dict() - def import_mods(): - global mods_list + def mods_import(): + global mods_list - all_files = renpy.list_files() - mods = [x for x in all_files if x.endswith(".json")] + all_files = renpy.list_files() + mods = [x for x in all_files if x.endswith(".json")] - for i, manifest in enumerate(mods): - path = os.path.split(manifest)[0] - files = [x for x in all_files if path in x] - scripts = [x for x in files if x.endswith(".rpym")] - logo = "{}/logo.webp".format(path) + for i, manifest in enumerate(mods): + path = os.path.split(manifest)[0] + files = [x for x in all_files if path in x] + scripts = [x for x in files if x.endswith(".rpym")] + logo = "{}/logo.webp".format(path) - if not renpy.loadable(logo): - logo = "#000" + if not renpy.loadable(logo): + logo = "#000" - # Read manifest - with renpy.open_file(manifest) as f: - data = json.load(f) + # Read manifest + with renpy.open_file(manifest) as f: + data = json.load(f) - modname = data.get("Name", None) + 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"): + if not modname: continue - fn = os.path.split(file)[1] + mods_list[modname] = data + mods_list[modname]["Files"] = files + mods_list[modname]["Scripts"] = scripts + mods_list[modname]["Path"] = path + mods_list[modname]["LoadOrder"] = i # TODO: Make load order customisable + mods_list[modname]["Logo"] = logo - with renpy.open_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.") + for mod in list(persistent.mods_enabled): + if not mods_list.get(mod, None): + persistent.mods_enabled.remove(mod) return - mods = persistent.mods_enabled + def mods_init(): + for i in persistent.mods_enabled: + for j in mods_list[i]["Scripts"]: + name = os.path.splitext(j)[0] - if mod in mods: - renpy.notify("Mod disabled.") - mods.remove(mod) - else: - renpy.notify("Mod Enabled.") - mods.add(mod) + try: + renpy.include_module(name) + except Exception as e: + renpy.error(e) + persistent.mods_enabled.remove(i) - # - # Custom parser w/ exception handling - # + mods_import() + mods_init() - def parse_script(fn, filedata=None, linenumber=1): - renpy.game.exception_info = 'While parsing ' + fn + '.' +init python: + @renpy.pure + class ToggleMod(Action, NoRollback): + def __init__(self, name): + self.name = name - 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) + def __call__(self): + if not main_menu: + renpy.notify("Mods can be enabled or disabled from within the main menu only.") + return - if not fn.endswith(".rpym"): - return None + mods = persistent.mods_enabled + name = self.name - l = renpy.parser.Lexer(nested) + if name in mods: + renpy.notify("Mod disabled. Remember to restart the game for the changes to take effect.") + mods.remove(name) + else: + renpy.notify("Mod Enabled. Remember to restart the game for the changes to take effect.") + mods.add(name) - 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("\ufeff") # BOM - error_f.write("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("\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("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 - - config.start_callbacks.extend([import_mods, parse_mods]) - config.after_load_callbacks.append(parse_mods) + renpy.restart_interaction()