WTS/game/modules/mods.rpy

184 lines
5.9 KiB
Plaintext
Raw Normal View History

2022-05-16 23:48:22 +00:00
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)