Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion modloader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,24 @@ def resolve_dependencies():

modinfo.mod_load_order[:] = mod_load_order

def mid_init_utter_restart():
"""Restart the game entirely (in case of late installed mods)"""
class Dummy:
def kill_textures_and_surfaces(self):
pass

def deinit(self):
pass

def finish_pending(self):
pass

renpy.game.interface = Dummy()
renpy.display.interface = Dummy()
renpy.display.draw = Dummy()
renpy.exports.utter_restart()



def main(reload_mods=False):
"""Load the mods"""
Expand Down Expand Up @@ -200,8 +218,30 @@ def main(reload_mods=False):

modinfo.reset_mods()

modules = []
modules = [] # List of modules already reloaded by rreload
valid_files = ['.DS_Store']
zip_files = [
f for f in modinfo.get_mod_folders()
if f.lower().endswith('.zip') and len(f) > 4
]
if zip_files:
modzip_imported_before = 'modloader.modzip' in sys.modules
import modloader.modzip
unpacked = False
for zip_file in zip_files:
modinfo.get_mod_folders().remove(zip_file)
zip_path = os.path.join(get_mod_path(), zip_file)
unpacked_path = modloader.modzip.unpack(zip_path, verbose=True)
unpacked = unpacked or bool(unpacked_path)
if unpacked:
if modzip_imported_before:
raise ImportError("Game appears to have found new mods to unpack after restarting. Please quit and restart the game.\nIf the error persists, unpack all mods manually and remove the zip files from the mods directory.")
# Because we unpacked a mod, we need to make sure Ren'y has parsed
# the new files before we continue. Since that's sketchy at best,
# we just restart the game so we can act like the unpacked mods
# were there all along.
mid_init_utter_restart()

for mod in modinfo.get_mod_folders():
if mod in valid_files:
modinfo.get_mod_folders().remove(mod)
Expand Down
78 changes: 78 additions & 0 deletions modloader/modzip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os.path
import shutil
import zipfile

__all__ = ('unpack',)

def _zip_security_check(zip_path, member_name):
def fail(msg):
# Note: We _intentionally_ don't print the member_name here,
# as it could be used to abuse the error message to give the
# user malicious instructions.
# It's up to the user to complain about the error message to
# the mod author, who can fix the zip if they're not malicious.
raise EnvironmentError("Zip file {} contains {} in a member path. For security reasons this is not allowed.".format(zip_path, msg))
if '..' in member_name:
fail('".." (directory climbing)')
if '\\' in member_name:
fail('"\\" (backslash)')
if member_name.startswith('/'):
fail('absolute paths')
if member_name.startswith('./'):
fail('relative paths')
return True

def _unpack_zip_path_to_path(zip_file, subpath, target_path, debug=False):
if subpath and subpath[-1] != '/':
subpath += '/'
for name in zip_file.namelist():
if name.startswith(subpath) and name != subpath:
zipinfo_obj = zip_file.getinfo(name)
old_filename = zipinfo_obj.filename
zipinfo_obj.filename = name[len(subpath):]
if debug:
print(zipinfo_obj.filename, '=>', os.path.join(target_path, zipinfo_obj.filename))
zip_file.extract(name, target_path)
zipinfo_obj.filename = old_filename

def _shallowest_init_path(zip_file, zip_path):
# If there's an __init__.py file in the root of the zip, extract there.
# Otherwise, extract the shallowest __init__.py file that doesn't have
# another __init__.py file at the same depth.
# If there are multiple __init__.py files at the same depth, error out.
# If there are no __init__.py files, error out.
init_files = [f for f in zip_file.namelist() if f.endswith('__init__.py')]
if len(init_files) == 0:
raise EnvironmentError("Zip file {} does not appear to be a packaged mod. It is missing an __init__.py file.".format(zip_path))
# Sort the init profiles by path depth
init_files.sort(key=lambda x: x.count('/'))
# Check if the first two items have the same depth, if so error out
if len(init_files) > 1 and init_files[0].count('/') == init_files[1].count('/'):
raise EnvironmentError("Zip file {} contains multiple __init__.py files at the shallowest depth. Cannot determine which to extract.".format(zip_path))
# Extract the folder containing the __init__.py file
return init_files[0]

def unpack(zip_path, verbose=False, debug=False):
# Check for an unpacked folder at the same location as the zip but without the .zip extension
folder_path = zip_path[:-4]
if os.path.isdir(folder_path):
# Check if the zip file is newer than the folder
if os.path.getmtime(zip_path) < os.path.getmtime(folder_path):
if verbose:
print('Folder "{}" is up to date from zip "{}". Skipping...'.format(folder_path, zip_path))
return None
else:
if verbose:
print('Folder "{}" is outdated from zip "{}". Replacing...'.format(folder_path, zip_path))
shutil.rmtree(folder_path)
with zipfile.ZipFile(zip_path, 'r') as z:
for name in z.namelist():
_zip_security_check(zip_path, name)
init_path = _shallowest_init_path(z, zip_path)
# We don't use the os.path module to split the path because the zip
# standard uses forward slashes as path separators.
mod_path = init_path.rsplit('/', 1)[0] if '/' in init_path else ''
if verbose:
print('Extracting "{}" into "{}"'.format(os.path.join(zip_path,mod_path), folder_path))
_unpack_zip_path_to_path(z, mod_path, folder_path, debug=debug)
return folder_path