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
41 changes: 38 additions & 3 deletions modloader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,41 @@ def report_mod_errors(errors): # NoReturn
raise


def report_modlist_errors(errors): # NoReturn
"""
Reports a modlist error to the user by displaying the modlist
error screen.

As the only actions available on the modlist error screen
are to "Reload" and "Quit", this function is guaranteed
not to return to the caller (except through Ren'Py's
control exceptions' unwinding.)

The structure of this function is taken from
report_mod_errors
"""
if not renpy.exports.has_screen("_modlist_errors"):
return True

renpy.display.error.init_display()

reload_action = renpy.exports.utter_restart

try:
renpy.game.invoke_in_new_context(
renpy.display.error.call_exception_screen,
"_modlist_errors",
reload_action=reload_action,
errors=errors,
)
except renpy.game.CONTROL_EXCEPTIONS:
raise
except:
renpy.display.log.write("While handling exception:")
renpy.display.log.exception()
raise


def resolve_dependencies():
"""Resolve mod dependencies and create mod load order"""
from modloader import modinfo
Expand Down Expand Up @@ -238,9 +273,9 @@ def main(reload_mods=False):

report_duplicate_labels()

if has_steam():
steammgr = get_instance()
steammgr.CachePersonas()
# if has_steam():
# steammgr = get_instance()
# steammgr.CachePersonas()

# By appending the mod folder to the import path we can do something like
# `import test` to import the mod named test in the mod folder.
Expand Down
137 changes: 123 additions & 14 deletions modloader/modconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from cStringIO import StringIO
import zipfile
from collections import namedtuple
import threading

import renpy
from renpy.audio.music import stop as _stop_music
Expand All @@ -21,6 +22,7 @@
if workshop_enabled:
from steam_workshop.steam_config import has_valid_signature
import steam_workshop.steamhandler as steamhandler
import steamhandler_extensions


BRANCHES_API = "https://api.github.com/repos/AWSW-Modding/AWSW-Modtools/branches"
Expand Down Expand Up @@ -98,21 +100,128 @@ def github_downloadable_mods():
return sorted(data, key=lambda mod: mod[1].lower())


@cache
def steam_downloadable_mods():
# A different format,
# (id, mod_name, author, desc, image_url)
mods = []
for mod in sorted(steamhandler.get_instance().GetAllItems(), key=lambda mod: mod[1]):
file_id = mod[0]
create_time, modify_time, signature = mod[5:8]
is_valid, verified = has_valid_signature(file_id, create_time, modify_time, signature)
if is_valid:
mods.append(list(mod[:5]))
mods[-1][3] += "\n\nVerified by {}".format(verified.username.replace("<postmaster@example.com>", ""))

class SteamModlist:
"""Manages the steam modlist, as is gotten by the steam_downloadable_mods method.
It supports loading the modlist in a separate thread via the load method,
And caching such results.
This is needed as loading the modlist takes quite a while,
And is an operation we would much rather do at startup, without delaying anything else.
Any exceptions raised in the loading process will be available through the get_exception() method.
Once the load finishes, Only one of the get() and get_exception() methods will return a value, while the other will return None.
If the load is successful, then get() will return a value. if the load raised an exception, then get_exception() will return a value.
"""

def __init__(self):
self._loading_thread = None
self._loading_thread_lock = threading.Lock()
self._loaded_data = None
self._exception = None
self._is_done = threading.Event()
return

def _loading_function(self):
"""Loads and verifies the steam modlist data.
It must take no arguments, and return a single value: the loaded data.
It may raise an exception, in which case it'll be stored in self._exception.
"""

# A different format,
# (id, mod_name, author, desc, image_url)

# This uses GetAllItems(), Which is affected by the QueryApi crash.
# therefore, steamhandler_extensions are preferred
mods = []
for mod in sorted(steamhandler_extensions.get_instance().GetAllItems(), key=lambda mod: mod[1]):
if mod[1] == "Modtools":
continue # The modtools themselves need not be here (as they're already present and can't be removed using themselves), nor should the signing system complain about them...
file_id = mod[0]
create_time, modify_time, signature = mod[5:8]
is_valid, verified = has_valid_signature(file_id, create_time, modify_time, signature)
if is_valid:
mods.append(list(mod[:5]))
mods[-1][3] += "\n\nVerified by {}".format(verified.username.replace("<postmaster@example.com>", ""))
else:
print "NOT VALID SIG", mod[1] # Note: printing only the mod name, instead of the whole thing SIGNIFICANTLY speeds up this call
return mods

def _load_and_set(self):
"""Calls the _loading_function and sets the internal values based on it's results."""
try:
mods = self._loading_function()
self._loaded_data = mods
print "Finished steam modlist load without errors"
except Exception as e:
print "Finished steam modlist load with errors"
self._exception = e
self._exception.traceback = sys.exc_info()[2]

self._is_done.set()
print "Done loading steam modlist"
return

def load(self):
"""Starts loading the steam modlist data if it is not already being loaded.
This method starts a thread which loads the modlist data.
It guarantees that for any number of repeated calls to it from any number of threads, only one loading thread will be started.
Once the data is loaded, it is available through the get method.
"""
with self._loading_thread_lock:
if self._loading_thread is None:
print "Steam modlist thread not present, Starting..."
self._loading_thread = threading.Thread(target=self._load_and_set, name=u"Thread-load-{}".format(self._load_and_set.__name__))
self._loading_thread.start()
else:
print "Steam modlist thread already present"
return self._loading_thread

def get(self):
"""Get the steam modlist data.
If the data has already loaded, this method returns with it immediately,
Otherwise, load() is called, and this method blocks until the completion of the data loading thread.
"""
if self._is_done.is_set():
# Note: while the value of is_set can change between checking it here and referring to _loaded_data,
# It can only change from False to True.
# In that case the load() method has been called before and is currently finishing,
# And it'll be called again here, ignored, and _is_done will be waited upon, which will finish only once _loaded_data is available.

print "Steam modlist data already available"
else:
print "Steam modlist data not available, calling load"
self.load()
self._is_done.wait()
print "Loading done, fetching data"
return self._loaded_data

def get_exception(self):
"""Get the raised exception, If the load raised an exception.
If the data has already loaded, this method returns with it immediately,
Otherwise, load() is called, and this method blocks until the completion of the data loading thread.
"""
if self._is_done.is_set():
# Identical logic to self.get()

print "Steam modlist exception already available"
else:
print "NOT VALID SIG", mod
return mods
print "Steam modlist data not available, calling load"
self.load()
self._is_done.wait()
print "Loading done, fetching exception"
return self._exception

def is_done(self):
return self._is_done.is_set()

def wait(self, timeout=None):
return self._is_done.wait(timeout)


steam_mod_list = SteamModlist()


def steam_downloadable_mods():
return steam_mod_list.get()


def download_github_mod(download_link, name, show_download=True, reload_script=True):
Expand Down
49 changes: 49 additions & 0 deletions modloader/patch_errorhandling_screens.rpym
Original file line number Diff line number Diff line change
Expand Up @@ -476,3 +476,52 @@ screen _modloader_errors(errors,reload_action):

# Tooltip.
text tt.value


# Screen for displaying steam modlist loading errors
# Note: this is copied over from _modloader_errors
screen _modlist_errors(errors,reload_action):
modal True
zorder 1090

default tt = __Tooltip("")

frame:
style_group ""

has side "t c b":
spacing gui._scale(10)

label _("Could not load the steam modlist.")

viewport:
id "viewport"
child_size (4000, None)
mousewheel True
scrollbars "both"
xfill True
yfill True

has vbox

text errors substitute False

vbox:

hbox:
spacing gui._scale(25)

textbutton _("Reload"):
action reload_action
hovered tt.action(_("Reloads the game from disk. (The same as quitting and restarting.)"))

vbox:
xfill True

textbutton _("Quit"):
action __ErrorQuit()
hovered tt.action(_("Quits the game."))
xalign 1.0

# Tooltip.
text tt.value
Loading