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
208 changes: 171 additions & 37 deletions modloader/modconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os

import subprocess
import threading
import shutil
from urllib2 import urlopen
import json
Expand Down Expand Up @@ -55,30 +56,6 @@ def report_exception(overview, error_str):
#steammgr.HandleException(exception_str)


def remove_mod(mod_name, filename):
"""Remove a mod from the game and reload.

Args:
mod_name (str): The internal name of the mod to be removed
"""
show_message("Removing mod {}...".format(mod_name))
if filename is False:
mod_class = get_mods()[mod_name]
mod_folder = mod_class.__module__
elif filename is True:
mod_folder = mod_name
else:
mod_folder = filename
if mod_folder.isdigit():
steammgr = steamhandler.get_instance()
steammgr.Unsubscribe(int(mod_folder))
shutil.rmtree(os.path.join(os.path.normpath(renpy.config.gamedir), "mods", mod_folder))
print "Sucessfully removed {}, reloading".format(mod_name)
sys.stdout.flush()
show_message("Reloading game...")
_stop_music("modmenu_music")
renpy.exports.reload_script()


@cache
def github_downloadable_mods():
Expand Down Expand Up @@ -127,9 +104,83 @@ def steam_downloadable_mods():
return steam_modlist_preloader.get()


def download_github_mod(download_link, name, show_download=True, reload_script=True):
if show_download:
show_message("Downloading {}".format(name))
class ModmapInstallStatus:
"""Holds the install status of a modmap install request in a thread-safe way."""

def __init__(self, phase=False, use_steam=True):
self._curr_mod_id = None
self._phase = phase
self._use_steam = use_steam

self._lock = threading.RLock()

# as this is treated as the source of this mod suite, we (currently) don't expect it to change during the suite.
def use_steam(self):
return self._use_steam

def set_curr(self, mod_id):
with self._lock:
self._curr_mod_id = mod_id

def get_curr(self):
with self._lock:
return self._curr_mod_id

def set_phase(self, phase):
with self._lock:
self._phase = phase

def get_phase(self):
with self._lock:
return self._phase


def remove_mod(mod_name, filename):
"""Remove a mod from the game and reload.

:param mod_name: The name of the mod to be removed, as a string.
:param filename: If True, mod_name is the path of the mod to be removed. If False, the path is taken from the mod's modclass. If a string, the path of the mod to be removed.
"""
# show_message("Removing mod {}...".format(mod_name))
if filename is False:
mod_class = get_mods()[mod_name]
mod_folder = mod_class.__module__
elif filename is True:
mod_folder = mod_name
else:
mod_folder = filename
if mod_folder.isdigit():
steammgr = steamhandler.get_instance()
steammgr.Unsubscribe(int(mod_folder))
shutil.rmtree(os.path.join(os.path.normpath(renpy.config.gamedir), "mods", mod_folder))

print "Sucessfully removed {}".format(mod_name)


def remove_mods(modmap, install_status=None):
"""Remove all mods in modmap, optionally while supplying status data.

:param modmap: Mapping from modname to filename, as they are in remove_mod. the modlist to remove.
:param install_status: If not None, the ModmapInstallStatus instance used to show current progress.
"""
print "remove_mods called with", modmap, install_status

if install_status is not None:
install_status.set_phase(True)

for mod_name, filename in modmap.iteritems():
if install_status is not None:
install_status.set_curr(mod_name)
remove_mod(mod_name, filename)



def download_github_mod(download_link, name):
"""Download a mod off the Github standard mod repository by its link and name.

:param download_link: The github archive link of the mod.
:param name: The name of the mod to install. this is also the name (path relative to general mod installation path) of the destination directory.
"""
mod_folder = os.path.join(get_mod_path(), name)
if os.path.exists(mod_folder):
shutil.rmtree(mod_folder, ignore_errors=True)
Expand All @@ -139,17 +190,16 @@ def download_github_mod(download_link, name, show_download=True, reload_script=T
root = zip_f.namelist()[0]
os.rename(os.path.join(get_mod_path(), root),
mod_folder)
if reload_script:
show_message("Reloading Game...")
restart_python()


def download_steam_mod(id, name, reload_script=True):
def download_steam_mod(id, name):
"""Download a mod off the Steam workshop based on its id.

:param id: The Steam ID of the mod to install.
:param name: The name of the mod to install. this parameter is ignored, and is there to match the interface of the github install calls.
"""
steammgr = steamhandler.get_instance()
# (id, mod_name, author, desc, image_url)
for i in renpy.config.layers:
renpy.game.context().scene_lists.clear(i)
show_screen("_modloader_download_screen", id, _layer="screens")
done_flag = threading.Event()

def cb(item, success):
# Copy the folder
Expand All @@ -158,11 +208,95 @@ def cb(item, success):
shutil.copytree(src, dest)

steammgr.unregister_callback(steamhandler.PyCallback.Download, cb)
done_flag.set()

steammgr.register_callback(steamhandler.PyCallback.Download, cb)
steammgr.Subscribe(id)

done_flag.wait()


def download_github_mods(modmap, install_status=None):
"""Download all github mods in modmap, optionally while supplying status data.

:param modmap: Mapping from modurl to modname, as they are in download_github_mod. the modlist to install.
:param install_status: If not None, the ModmapInstallStatus instance used to show current progress.
"""
if install_status is not None:
install_status.set_phase(False)

for modid, modname in modmap.iteritems():
if install_status is not None:
install_status.set_curr(modid)
download_github_mod(modid, modname)

def download_steam_mods(modmap, install_status=None):
"""Download all steam mods in modmap, optionally while supplying status data.

:param modmap: Mapping from modid to modname, as they are in download_steam_mod. the modlist to install.
:param install_status: If not None, the ModmapInstallStatus instance used to show current progress.
"""
if install_status is not None:
install_status.set_phase(False)

for modid, modname in modmap.iteritems():
if install_status is not None:
install_status.set_curr(modid)
download_steam_mod(modid, modname)


def apply_mod_changes(add_modmap, remove_modmap, show_status_screen=True, reload_script=None, use_steam=True):
""" Apply the mod changes given in the modlists, optionally showing the mod status screen and reloading when done.

This acts as the main way to visibly apple a suite of mod changes, and should be the one used in most cases.
:param add_modmap: Mapping from modid to modname, as they are in download_steam_mod. the modlist to install.
:param remove_modmap: Mapping from modname to filename, as they are in remove_mod. the modlist to remove.
:param show_status_screen: If True (default), then show the mod changes status screen. If False, doesn't show the mod changes status screen.
:param reload_script: If True, then restart the script once the mod changes are done. If False, no restarting is done. If None (the default), this is set to the value of show_status_screen which allows for 'install visibly then restart' and 'install silently'.
:param use_steam: True (default) uses steam api to install the mods. False uses github api.
:returns: done_flag if reload_script is False, else None. done_flag is a threading.Event which becomes set once the mod is installed. note that this return value can end interactions.
"""
if reload_script is None:
reload_script = show_status_screen

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't make sense - show_status_screen is being used to restart python later

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite follow why it doesn't? it's default allows for two main uses:

  1. when show_status_screen==False, then the mod application is done silently in the background, and therefore it shouldn't restart the game when done as it will be without warning.
  2. when show_status_screen==True, then then the mod application takes over the screen, as it is the main thing To Be Done. as such, it is allowed (and encouraged) to restart, as mod changes generally warrant that.
    Explicitly setting reload_script could be done for more unusual uses, though I'm not sure about their advisability.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was confusing to me here was that it wasn't obvious what type show_status_screen was. Though given we're calling this function in only a few places, do you think it would make sense here to also pass in reload_script=True whenever we want to show the status screen?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see that I didn't describe it well in the comment (as in, only described the True case and not the False case).
As for explicitly passing reload_script=True, I guess that would be clearer... I'll be on that


print "apply_mod_changes called with", add_modmap, remove_modmap, reload_script, show_status_screen, use_steam
if show_status_screen:
apply_status = ModmapInstallStatus(phase=False, use_steam=use_steam)

for i in renpy.config.layers:
renpy.game.context().scene_lists.clear(i)
show_screen("_modloader_download_screen", apply_status, _layer="screens")
else:
apply_status = None

thread_done_flag = threading.Event()

def _apply_loop(add_modmap, remove_modmap, reload_script, apply_status, thread_done_flag):
if use_steam:
download_steam_mods(add_modmap, install_status=apply_status)
else:
download_github_mods(add_modmap, install_status=apply_status)

if apply_status is not None:
apply_status.set_curr(None)
apply_status.set_phase(True)
remove_mods(remove_modmap, install_status=apply_status)

if apply_status is not None:
apply_status.set_curr(None)
thread_done_flag.set()

if reload_script:
restart_python()

steammgr.register_callback(steamhandler.PyCallback.Download, cb)
steammgr.Subscribe(id)
threading.Thread(name="apply_mod_changes__apply_loop", target=_apply_loop, args=(add_modmap, remove_modmap, reload_script, apply_status, thread_done_flag)).start()

if reload_script:
return None
Comment thread
muddyfish marked this conversation as resolved.
else:
return thread_done_flag




class UpdateModtools(Action):
Expand Down
16 changes: 6 additions & 10 deletions modloader/preload.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,12 @@ class Queue:
def __init__(self):
self._queue = collections.deque()
self._pop_condition = threading.Condition()
return


def put(self, item):
with self._pop_condition:
self._queue.append(item)
self._pop_condition.notify()
return

def get(self, block=True, timeout=None):
if not block or timeout == 0: # For these non-blocking cases, We avoid the retrying located below.
Expand All @@ -55,7 +53,6 @@ def get(self, block=True, timeout=None):

def clear(self):
self._queue.clear()
return

def get_nowait(self):
self.get(False)
Expand Down Expand Up @@ -113,8 +110,7 @@ def __init__(self, loading_function, max_workers=1):

self._clear_session_num = 0 # clear() uses session numbers to ensure that once clear is called, all ongoing actions are invalidated
self._clear_session_lock = threading.RLock()

return


def _manage_job_queue(self):
while True:
Expand Down Expand Up @@ -157,7 +153,7 @@ def _load_and_set(self, clear_session, *args):
self._is_loaded[args].set()
# print "Done preloading"
self._call_callbacks((args,), curr_callbacks, clear_session=self._clear_session_num)
return


def load(self, *args):
"""Starts preloading the result of loading_function(*args) if it is not already being loaded.
Expand All @@ -174,7 +170,7 @@ def load(self, *args):

print "({}) Preload not present, Starting... {}".format(self._name, args)
self._job_queue.put((self._clear_session_num, "load", args))
return


def get(self, *args, **kwargs):
"""Get the preloaded data corresponding to args.
Expand Down Expand Up @@ -222,7 +218,7 @@ def clear(self):
self._exception.clear()
self._is_loaded.clear()
self._job_queue.clear()
return


def _is_clear_session_valid(self, clear_session):
with self._clear_session_lock:
Expand All @@ -242,7 +238,7 @@ def register_callback(self, callback):
finised_loads = tuple(self._loaded_data.keys())

self._job_queue.put((self._clear_session_num, "callback", callback, finised_loads))
return


def _call_callbacks(self, loads, callbacks, clear_session):
"""calls each callback in callbacks on each result of loads"""
Expand All @@ -263,7 +259,7 @@ def _call_callbacks(self, loads, callbacks, clear_session):
callback(data, exception)
except Exception as callback_exception: # callback exceptions are ignored and do not affect other callbacks.
print "[] Callback {}({}, {}) raised exception: {}".format(self._name, callback, data, exception, callback_exception)
return



def is_loaded(self, *args):
Expand Down
7 changes: 1 addition & 6 deletions modloader/steamhandler_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,7 @@ def fill_cache_query_cb(array, arr_len):
finally:
print "Cache file write callback done (page {}).".format(page)
fill_cache_query_cb.done.set()

return


fill_cache_query_cb.error = None
self.register_callback(PyCallback.Query, fill_cache_query_cb)
Expand All @@ -172,9 +171,6 @@ def fill_cache_query_cb(array, arr_len):

if fill_cache_query_cb.error is not None:
raise fill_cache_query_cb.error

return

finally:
self.unregister_callback(PyCallback.Query, fill_cache_query_cb)

Expand Down Expand Up @@ -284,7 +280,6 @@ def cb(array, arr_len):

cb.should_run_next = (arr_len == 50)
cb.page_complete.set()
return

cb.page_num = 1
cb.should_run_next = True
Expand Down
4 changes: 2 additions & 2 deletions mods/core/core.rpy
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ screen modmenu tag smallscreen:
#mod management buttons
vbox xalign 0.5 yalign 0.5:
if has_steam():
textbutton "Add mod from workshop":
textbutton "Manage workshop mods":
action [Function(_enter_modmenu, use_steam=True),
Play("audio", "se/sounds/open.ogg"),
Stop("music", fadeout=1.0),
Expand All @@ -56,7 +56,7 @@ screen modmenu tag smallscreen:
style "menubutton2"

if is_github():
textbutton "Add mod from Github":
textbutton "Manage Github mods":
action [Function(_enter_modmenu, use_steam=False),
Play("audio", "se/sounds/open.ogg"),
Stop("music", fadeout=1.0),
Expand Down
Loading