184 lines
5.9 KiB
Plaintext
184 lines
5.9 KiB
Plaintext
|
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)
|
||
|
|
||
|
|