From 85d71da086f75c2b67f5f434565ae303ca3020c3 Mon Sep 17 00:00:00 2001 From: Map Date: Fri, 4 Oct 2024 18:06:23 -0500 Subject: [PATCH] import wani mod script --- game/src/mod_menu.rpy | 1048 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 977 insertions(+), 71 deletions(-) diff --git a/game/src/mod_menu.rpy b/game/src/mod_menu.rpy index 338da1e..ee88be9 100644 --- a/game/src/mod_menu.rpy +++ b/game/src/mod_menu.rpy @@ -1,102 +1,1008 @@ +# This is one big clusterfuck so let's break it down: + +# All mods have a format that is strictly'ish followed - Metadata json files are a dict of values that represent a mod name, if they jump to a label, ID's, +# descriptions, etc. and are needed to load any mod scripts at all (And scripts are the only way mods can add anything into the game). Given Ren'Py's lack of support +# for namespaced variables, metadata files are designed as such that they don't have mod conflicts so all mods can have their metadata shown at once, even if +# their respective mod is disabled, so the metadata of alls mods gets loaded into mod_menu_metadata. + +# For this big ass init block, metadata loading has these phases: +# 1. Mod metadata collection (Searches through all files, checks for mod disabling files, and picks out metadata.json files) +# 2. Mod metadata sanitization/error handling (Searches through all metadata.json files, warning of errors and safeguards bad input) +# 3. Mod metadata image/script collection (Searches for images besides metadata.json, loading the thumbnail, icon, and screenshots if they're there, and language +# dependant metadata files such as jsons with translated strings or images) +# 4. Add to mod_menu_metadata (Metadata is added to the mod_menu_metadata list) +# 5. Mod order organizing (Metadata is initially loaded in file alphabetical order, so it rearranges mod_menu_metadata according to load order +# and applies whether a mod is supposed to be disabled or not) +# 6. Mod loading (Loads mod scripts according to mod_menu_metadata) + +# Entries that don't exist in mod_menu_metadata or end up as 'None' by the end of the mod loading should use the .get method for accessing values. This way modders +# don't need to implement every single entry into metadata.json (But makes the code here messier) + +# When an index in mod_menu_metadata is filled out, each mod should look a little something like this (but more sporadically organized), depending on what's +# actually filled out in the metadata files: +# { +# "ID": "my_mod_id", +# "Enabled: True +# "Scripts": [ "mods/My Mod/my_mod_script1", "mods/My Mod/my_mod_script2", "mods/My Mod/my_mod_script3" ] +# "Label": "my_mod_label", +# "None": { +# "Name": "My Mod Name" +# "Version": "1.0", +# "Authors": [ "Author1", "Author2", "Author3" ], +# "Links": "Link1", +# "Description": "My Mod Description", +# "Mobile Description": "My Mod Description", +# "Display": "icon", +# "Thumbnail Displayable": "my_thumbnail_displayable" +# "Icon Displayable": "my_icon_displayable" +# "Screenshot Displayables": [ "my_screenshot_displayable1", "my_screenshot_displayable2", "mods/My Mod/screenshot3.png", "my_screenshot_displayable4" ] +# "Thumbnail": "mods/My Mod/thumbnail.png" +# "Icon": "mods/My Mod/icon.png" +# "Screenshots": [ "mods/My Mod/screenshot1.png", "mods/My Mod/screenshot2.png", "mods/My Mod/screenshot3.png" ] +# }, +# "es": +# "Name": "My Mod Name but in spanish" +# "Version": "1.0", +# "Authors": [ "Author1", "Author2", "Author3" ], +# "Links": "Link1", +# "Description": "My Mod Description but in spanish", +# "Mobile Description": "My Mod Description but in spanish", +# "Display": "icon", +# "Thumbnail Displayable": "my_thumbnail_displayable_es" +# "Icon Displayable": "my_icon_displayable_es" +# "Screenshot Displayables": [ "my_screenshot_displayable1_es", "my_screenshot_displayable2_es", "mods/My Mod/screenshot3.png", "my_screenshot_displayable4_es" ] +# "Thumbnail": "mods/My Mod/thumbnail_es.png" +# "Icon": "mods/My Mod/icon_es.png" +# "Screenshots": [ "mods/My Mod/screenshot1_es.png", "mods/My Mod/screenshot2.png", "mods/My Mod/screenshot3_es.png" ] +# }, +# "ru": { +# "Name": "My Mod Name but in russian" +# etc... +# }, +# etc... +# } +# +# Note that some keys may exist, but simple be 'None', likely as a result of improperly filled out metadata files. + + +init -999 python: + import json + from enum import Enum + + # + # Modding system setup + # + + class ModError(Enum): + Metadata_Fail = 0 + Name_Not_String = 1 + Label_Not_String = 2 + Display_Not_String = 3 + Display_Invalid_Mode = 4 + Version_Not_String = 5 + Authors_Not_String_Or_List = 6 + Authors_Contents_Not_String = 7 + Links_Not_String_Or_List = 8 + Links_Contents_Not_String = 9 + Description_Not_String = 10 + Mobile_Description_Not_String = 11 + Screenshot_Displayables_Not_List = 12 + Screenshot_Displayables_Contents_Not_String = 13 + Icon_Displayable_Not_String = 14 + Thumbnail_Displayable_Not_String = 15 + No_ID = 16 + ID_Not_String = 17 + Similar_IDs_Found = 18 + Installed_Incorrectly = 19 + Invalid_Image_Extension = 20 + Invalid_Language_Code = 21 + + + mods_dir = "mods/" # The root mod folder. Important that you keep the slash at the end. + mod_menu_moddir = ".../game/" + mods_dir # The visual mod dir name + mod_menu_access = [] # This variable exists for legacy mods to be usable. + + # A list containing tuples that contain a mod's ID and if the mod is enabled. The game references this list to activate mods and order them from + # first to last index + if persistent.enabled_mods == None: + persistent.enabled_mods = [] + + valid_image_filetypes = [ ".png", ".jpg", ".webp", ".avif", ".svg", ".gif", ".bmp" ] + + # Makes loading mods on android possible. It creates folders for android mods, changing mod_menu_moddir as necessary if the user is playing on Android. + if renpy.android and not config.developer: + android_mods_path = os.path.join(os.environ["ANDROID_PUBLIC"], "game", mods_dir) + try: + # We have to create both the 'game' and 'mods' folder for android. + os.mkdir(mods_dir) + os.mkdir(android_mods_path) + except: + pass + + mod_menu_moddir = android_mods_path + + # Determines if a filename with it's extension is valid for renpy's image displaying and for our specified mod metadata. + def is_valid_metadata_image(filename, name_of_mod): + error_trigger = True + + for ext in valid_image_filetypes: + if this_file.endswith(ext): + error_trigger = False + if error_trigger: + mod_menu_errorcodes.append([ ModError.Invalid_Image_Extension, { "mod_name": mod_name, "name_of_file": filename }]) + return False + + return True + + + # Start finding mods. Find json files within each folder and mod disablers in the mods directory if there's any. + # NOMODS - Disables mod loading entirely + # NOLOADORDER - Erases load order/mod states. + # DISABLEALLMODS - Temporarily disables all mods, but still loading their metadata, and returns them back to their original state when this is removed. + all_mod_files = [ i for i in renpy.list_files() if i.startswith(mods_dir) ] + load_metadata = True + load_mods = True + loadable_mod_metadata = [] + for i in all_mod_files: + if i.startswith(mods_dir + "NOMODS"): + load_metadata = False + loadable_mod_metadata = [] + if i.startswith(mods_dir + "NOLOADORDER"): + persistent.enabled_mods.clear() + if i.startswith(mods_dir + "DISABLEALLMODS"): + load_mods = False + + if load_metadata and i.endswith("/metadata.json"): + loadable_mod_metadata.append(i) + + # + # Get and store metadata + # + + # Where all mod info will be stored + mod_menu_metadata = [] + # A list that contains tuples that contain an error code and a dictionary containing metadata regarding the error such as + # which mod it's referring to. Used to construct error strings outside of this python block, to workaround Ren'Py's + # translation system not working this early. + mod_menu_errorcodes = [] + # Contains the mod_name's of previous mods that have successfully loaded, mirroring mod_menu_metadata while in this loop. Only used for ID checking. + mod_name_list = [] + + for file in loadable_mod_metadata: + mod_data_final = {} + mod_jsonfail_list = [] # List of langauges that has an associated metadata that failed to load. + mod_preferred_modname = [] + mod_exception = False + mod_in_root_folder = file.count("/", len(mods_dir)) is 0 + mod_folder_name = file.split("/")[-2] + # mod_name is used only to display debugging information via mod_menu_errorcodes. Contains the mod folder name and whatever translations of + # the mod's name that exist. Kind of a cursed implemnetation but with how early error reporting this is before solidifying the mod name + # this is just what I came up with. + # Other than what's directly defined here, it will contain mod names by their language code if they exist. English will go by the "None" key. + mod_name = {} + if mod_in_root_folder: + mod_name["Folder"] = None # 'None' will make it default to 'in root of mods folder' when used in the errorcode conversion. + else: + mod_name["Folder"] = mod_folder_name + + + # Quickly get the names of mods for debugging information, and in the process get raw values from each metadata file that exists. + + # Make the base metadata file (english) organized like a lang_data object, moving the ID to the mod_data_final object. + try: + mod_data = json.load(renpy.open_file(file)) + except Exception as e: + if mod_in_root_folder: + print("//////////// ERROR IN ROOT FOLDER MOD:") + else: + print(f"//////////// ERROR IN MOD '{mod_folder_name}':") + print(" "+str(e)) + print("//////////// END OF ERROR") + mod_exception = True + mod_jsonfail_list.append("None") + + if not mod_jsonfail_list: + if _preferences.language == None and isinstance(mod_data.get("Name"), str): + mod_name["None"] = mod_data["Name"] + + # Move these non-language specific pairs out of the way, into the base of the final mod dict. + if "ID" in mod_data.keys(): + mod_data_final["ID"] = mod_data["ID"] + del mod_data["ID"] + if "Label" in mod_data.keys(): + mod_data_final["Label"] = mod_data["Label"] + del mod_data["Label"] + # Then store the rest like any other language, just our default one. + mod_data_final['None'] = mod_data + + # Find language metadata files in the same place as our original metadata file, and then get values from it. + for lang in renpy.known_languages(): + lang_file = file[:-5] + "_" + lang + ".json" + if renpy.loadable(lang_file): + try: + lang_data = (json.load(renpy.open_file(lang_file))) + except Exception as e: + if mod_in_root_folder: + print(f"//////////// ERROR FOR {lang} METADATA IN ROOT FOLDER MOD:") + else: + print(f"//////////// ERROR FOR {lang} METADATA IN MOD '{mod_folder_name}':") + print(" "+str(e)) + print("//////////// END OF ERROR") + mod_jsonfail_list.append(lang) + + # Attempt to use this mod's translation of it's name if it matches the user's language preference. + if not lang in mod_jsonfail_list: + if _preferences.language == lang and isinstance(lang_data.get("Name"), str): + mod_name[lang] = lang_data["Name"] + mod_data_final[lang] = lang_data + + # Finally report if any of the jsons failed to load, now that we have the definitive list of mod names we could display. + for lang_code in mod_jsonfail_list: + mod_menu_errorcodes.append([ ModError.Metadata_Fail, { "mod_name": mod_name, "lang_code": lang_code }]) + + # + # Sanitize/Clean metadata values + # + + # Make sure our main metadata loaded + if not "None" in mod_jsonfail_list: + if mod_data_final.get("Label") != None and not isinstance(mod_data_final.get("Label"), str): + mod_menu_errorcodes.append([ ModError.Label_Not_String, { "mod_name": mod_name }]) + mod_data_final["Label"] = None + + # If we don't have an ID, don't put it in the mod loader + if mod_data_final.get("ID") == None: + mod_menu_errorcodes.append([ ModError.No_ID, { "mod_name": mod_name }]) + mod_exception = True + elif not isinstance(mod_data_final["ID"], str): + mod_menu_errorcodes.append([ ModError.ID_Not_String, { "mod_name": mod_name }]) + mod_exception = True + else: + # Detect already loaded metadata that has the same ID as our current one, and if so, don't load them + # We'll never get a match for the first mod loaded, so this is fine. + for i, x in enumerate(mod_menu_metadata): + if x["ID"] == mod_data_final["ID"]: + mod_menu_errorcodes.append([ ModError.Similar_IDs_Found, { "mod_name": mod_name, "mod_name2": mod_name_list[i] }]) + mod_exception = True + break + + # Since lang keys will only be added to the mod data dict if their respective metadata successfully loaded, no need to check. + for lang_key in mod_data_final.keys(): + if lang_key is "None" or lang_key in renpy.known_languages(): + lang_data = mod_data_final[lang_key] + + # The JSON object returns an actual python list, but renpy only works with it's own list object and the automation for this fails with JSON. + for x in lang_data.keys(): + if type(lang_data[x]) == python_list: + lang_data[x] = renpy.revertable.RevertableList(lang_data[x]) + + # Automatically give the name of the mod from the folder it's using if there's no defined name, but report an error if one is defined but not a string + if lang_data.get("Name") != None and not isinstance(lang_data.get("Name"), str): + if lang_data.get("Name") != None: + mod_menu_errorcodes.append([ ModError.Name_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) + + lang_data["Name"] = mod_folder_name + + # Default "Display" to 'both' mode + if lang_data.get("Display") != None: + if not isinstance(lang_data.get("Display"), str): + if lang_data.get("Display") != None: + mod_menu_errorcodes.append([ ModError.Display_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Display"] = "both" + elif lang_data["Display"] not in ["both","icon","name"]: + mod_menu_errorcodes.append([ ModError.Display_Invalid_Mode, { "mod_name": mod_name, "display_mode": lang_data["Display"], "lang_code": lang_key }]) + lang_data["Display"] = "both" + + if lang_data.get("Version") != None and not isinstance(lang_data.get("Version"), str): + mod_menu_errorcodes.append([ ModError.Version_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Version"] = None + + # See if "Authors" is a list or string, and if it's a list search through the contents of the list to check if they're valid strings + if lang_data.get("Authors") != None and (not isinstance(lang_data.get("Authors"), str) and not isinstance(lang_data.get("Authors"), list)): + mod_menu_errorcodes.append([ ModError.Authors_Not_String_Or_List, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Authors"] = None + elif isinstance(lang_data.get("Authors"), list): + # Search through and call out entries that aren't strings + for i, s in enumerate(lang_data["Authors"]): + if not isinstance(s, str): + mod_menu_errorcodes.append([ ModError.Authors_Contents_Not_String, { "mod_name": mod_name, "author_number": i + 1, "lang_code": lang_key }]) + # And then mutate the list to only include strings + lang_data["Authors"][:] = [x for x in lang_data["Authors"] if isinstance(x, str)] + + if lang_data["Authors"] == []: + lang_data["Authors"] = None + + # Do the same as 'Authors' to 'Links' + if lang_data.get("Links") != None and (not isinstance(lang_data.get("Links"), str) and not isinstance(lang_data.get("Links"), list)): + mod_menu_errorcodes.append([ ModError.Links_Not_String_Or_List, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Links"] = None + elif isinstance(lang_data.get("Links"), list): + for i, s in enumerate(lang_data["Links"]): + if not isinstance(s, str): + mod_menu_errorcodes.append([ ModError.Links_Contents_Not_String, { "mod_name": mod_name, "link_number": i + 1, "lang_code": lang_key }]) + lang_data["Links"][:] = [x for x in lang_data["Links"] if isinstance(x, str)] + + if lang_data["Links"] == []: + lang_data["Links"] = None + + if lang_data.get("Description") != None and not isinstance(lang_data.get("Description"), str): + mod_menu_errorcodes.append([ ModError.Description_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Description"] = None + + if lang_data.get("Mobile Description") != None and not isinstance(lang_data.get("Mobile Description"), str): + mod_menu_errorcodes.append([ ModError.Mobile_Description_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Mobile Description"] = None + + if lang_data.get("Screenshot Displayables") != None: + if not isinstance(lang_data.get("Screenshot Displayables"), list): + mod_menu_errorcodes.append([ ModError.Screenshot_Displayables_Not_List, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Screenshot Displayables"] = None + else: + # Instead of remaking the list to only include strings, replace the non-strings with empty strings ("") so subsequent strings will still be in the right + # place when eventually showing displayable screenshots over non-displayable ones + for i, s in enumerate(lang_data["Screenshot Displayables"]): + if not isinstance(s, str): + mod_menu_errorcodes.append([ ModError.Screenshot_Displayables_Contents_Not_String, { "mod_name": mod_name, "screenshot_number": i + 1, "lang_code": lang_key }]) + s = "" + + if lang_data["Screenshot Displayables"] == []: + lang_data["Screenshot Displayables"] = None + + if lang_data.get("Icon Displayable") != None and not isinstance(lang_data.get("Icon Displayable"), str): + mod_menu_errorcodes.append([ ModError.Icon_Displayable_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Icon Displayable"] = None + + if lang_data.get("Thumbnail Displayable") != None and not isinstance(lang_data.get("Thumbnail Displayable"), str): + mod_menu_errorcodes.append([ ModError.Thumbnail_Displayable_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Thumbnail Displayable"] = None + + # If our mod does not follow the structure of 'mods/Mod Name/metadata.json', don't load them + this_mod_dir = file[len(mods_dir):-len("metadata.json")] # If the metadata file is in another place, this will connect the filepath between it and the mods folder. + if file.count("/", len(mods_dir)) > 1: + good_folder = "'{color=#ffbdbd}" + mods_dir + mod_folder_name + "/{/color}'" + curr_folder = "'{color=#ffbdbd}" + mods_dir + this_mod_dir + "{/color}'" + mod_menu_errorcodes.append([ ModError.Installed_Incorrectly, { "mod_name": mod_name, "good_dir": good_folder, "curr_dir": curr_folder }]) + mod_exception = True + elif mod_in_root_folder: + mod_menu_errorcodes.append([ ModError.Installed_Incorrectly, { "mod_name": mod_name, "good_dir": mods_dir + "My Mod/", "curr_dir": mods_dir }]) + mod_exception = True + + # + # Collect mod scripts and metadata images + # + + mod_scripts = [] + mod_screenshots = {} + this_mod_dir_length = len(mods_dir + this_mod_dir) + for i in all_mod_files: + if i.startswith(mods_dir + this_mod_dir): + # Collect mod scripts + if not mod_exception and i.endswith(".rpym"): + mod_scripts.append(i[:-5]) + continue + + # This will only allow files that are at the root of the mod folder and have one period. + elif i.count("/", this_mod_dir_length) == 0 and i.count(".", this_mod_dir_length) == 1: + this_file = i[this_mod_dir_length:] + + if this_file.startswith("thumbnail."): + if is_valid_metadata_image(this_file, mod_name): + mod_data_final["None"]["Thumbnail"] = i + elif this_file.startswith("thumbnail_"): + trimmed_string = this_file[len("thumbnail_"):this_file.find(".")] + for lang in renpy.known_languages(): + if lang == trimmed_string: + if is_valid_metadata_image(this_file, mod_name): + mod_data_final[lang]["Thumbnail"] = i + + + elif this_file.startswith("icon."): + if is_valid_metadata_image(this_file, mod_name): + if mod_data_final.get("None") == None: + mod_data_final["None"] = {} + mod_data_final["None"]["Icon"] = i + elif this_file.startswith("icon_"): + trimmed_string = this_file[len("icon_"):this_file.find(".")] + for lang in renpy.known_languages(): + if lang == trimmed_string: + if is_valid_metadata_image(this_file, mod_name): + if mod_data_final.get(lang) == None: + mod_data_final["None"] = {} + mod_data_final[lang]["Icon"] = i + + + elif this_file.startswith("screenshot"): + # Disect the string after "screenshot" for the number and possible lang code + trimmed_string = this_file[len("screenshot"):this_file.find(".")] + number = "" + lang_code = "" + seperator = trimmed_string.find("_") + if seperator != -1: + number = trimmed_string[:seperator] + lang_code = trimmed_string[seperator + 1:] + else: + number = trimmed_string + + # See if we can extract the number + try: + converted_number = int(number) + except: + continue + + if not is_valid_metadata_image(this_file, mod_name): + continue + + if seperator == -1: + if mod_screenshots.get("None") == None: + mod_screenshots["None"] = [] + mod_screenshots["None"].append((i, converted_number)) + elif lang_code in renpy.known_languages(): + if mod_screenshots.get(lang_code) == None: + mod_screenshots[lang_code] = [] + mod_screenshots[lang_code].append((i, converted_number)) + + + # Don't load the mod if there's mod breaking errors + if mod_exception: + continue + + # We're now gonna clean up the screenshots to be more usable as-is. + + # Refine collected screenshots so that translated screenshots use the english screenshots (the ones without a lang code) + # as a base, and then either replacing or adding the translated screenshots according to their order/number. + for lang_key in mod_screenshots.keys(): + if lang_key != "None": + if mod_screenshots.get("None") == None: + temp_list_with_indexes = mod_screenshots[lang_key] + else: + temp_list_with_indexes = mod_screenshots["None"] + for i, lang_images in enumerate(mod_screenshots[lang_key]): + u = 0 + while u < len(temp_list_with_indexes): + if lang_images[1] > temp_list_with_indexes[u][1] and u == len(temp_list_with_indexes) - 1: + temp_list_with_indexes.append(lang_images) + break + elif lang_images[1] == temp_list_with_indexes[u][1]: + temp_list_with_indexes[u] = lang_images + break + elif lang_images[1] < temp_list_with_indexes[u][1]: + temp_list_with_indexes.insert(u, lang_images) + break + + u += 1 + else: + temp_list_with_indexes = mod_screenshots["None"] + + # Get rid of the tuples and just leave the screenshot files + mod_data_final[lang_key]["Screenshots"] = [] + for i in temp_list_with_indexes: + mod_data_final[lang_key]["Screenshots"].append(i[0]) + + # Make a copy of the current screenshots list, then put displayable screenshots wherever the values of "Screenshot Displayables" correspond in this list + # mod_screenshots will return an empty list if there were no screenshot files to begin with, so this works fine. + for lang_key in mod_data_final.keys(): + if lang_key is "None" or lang_key in renpy.known_languages(): + if mod_data_final[lang_key].get("Screenshots") == None: + mod_screenshots = [] + else: + mod_screenshots = mod_data_final[lang_key]["Screenshots"] + + if mod_data_final[lang_key].get("Screenshot Displayables") != None: + mod_displayable_list = mod_screenshots.copy() + + for i, x in enumerate(mod_data_final[lang_key]["Screenshot Displayables"]): + if i < len(mod_screenshots): + if x != "": + mod_displayable_list[i] = x + else: + mod_displayable_list.append(x) + + if mod_displayable_list != []: + mod_data_final[lang_key]["Screenshot Displayables"] = mod_displayable_list + else: + mod_data_final[lang_key]["Screenshot Displayables"] = None + + # Store the collected scripts and screenshots + mod_data_final["Scripts"] = mod_scripts + + # Make our mod loadable + mod_menu_metadata.append(mod_data_final) + mod_name_list.append(mod_name) # This will mirror mod_menu_metadata + + + # Sort mod metadata list according to enabled_mods, while dropping mods from enabled_mods if they aren't installed + # This will also apply the state of a mod if it's supposed to be enabled/disabled + # The effect will be that mod_menu_metadata is sorted from first to last to be loaded + temp_list = [] + for saved_mod_id, saved_mod_state in persistent.enabled_mods: + for mod in mod_menu_metadata: + if mod["ID"] == saved_mod_id: + mod["Enabled"] = saved_mod_state + temp_list.append(mod) + break + + # Now inverse search to find new mods and append them to metadata list. New mods are by default enabled, and are the last to be loaded + for mod in mod_menu_metadata: + mod_not_found = True + for saved_mod_id in persistent.enabled_mods: + if mod["ID"] == saved_mod_id[0]: + mod_not_found = False + if mod_not_found: + mod["Enabled"] = persistent.newmods_default_state + temp_list.append(mod) + + mod_menu_metadata = temp_list + + # Rewrite enabled_mods to reflect the new mod order, and load all the mods + persistent.enabled_mods.clear() + for mod in mod_menu_metadata: + persistent.enabled_mods.append( [ mod["ID"], mod["Enabled"] ] ) + + # Making the load_mods check here makes it so the NOLOAD flag doesn't overwrite the previously saved load order + if load_mods and mod["Enabled"]: + for script in mod["Scripts"]: + renpy.include_module(script) + else: + mod["Enabled"] = False + +# Now convert our errorcodes to errorstrings +init python: + def return_translated_mod_name(mod_dict): + if _preferences.language == None and "None" in mod_dict.keys(): + return "'{color=#ffbdbd}" + mod_dict["None"] + "{/color}'" + elif _preferences.language in mod_dict.keys(): + return "'{color=#ffbdbd}" + mod_dict[_preferences.language] + "{/color}'" + else: + if mod_dict["Folder"] == None: + return __("the root of the mods folder") + else: + return "'{color=#ffbdbd}" + mod_dict["Folder"] + "{/color}'" + def convert_errorcode_to_errorstring(error_code, var_dict): + if var_dict.get("mod_name") != None: + mod_name = return_translated_mod_name(var_dict["mod_name"]) + if var_dict.get("mod_name2") != None: + mod_name2 = return_translated_mod_name(var_dict["mod_name2"]) + + lang_code = var_dict.get("lang_code") + if lang_code == "None" or lang_code == None: + lang_code = "" + else: + lang_code = __(" for '{color=#ffbdbd}") + lang_code + __("{/color}' language") + + name_of_file = var_dict.get('name_of_file') + if name_of_file != None: + name_of_file = "'{color=#ffbdbd}" + name_of_file + "{/color}'" + + if error_code == ModError.Metadata_Fail: + if lang_code == "": + return __("{color=#ff1e1e}Mod in ") + mod_name + __(" failed to load: Metadata is formatted incorrectly. Check log.txt or console for more info.{/color}") + else: + return __("{color=#ff8b1f}Metadata in ") + mod_name + lang_code + _(" is formatted incorrectly. Check log.txt or console for more info.{/color}") + elif error_code == ModError.Name_Not_String: + return __("{color=#ff8b1f}Mod's name in ") + mod_name + lang_code + _(" is not a string.{/color}") + elif error_code == ModError.Label_Not_String: + return __("{color=#ff8b1f}Mod's label in ") + mod_name + lang_code + _(" is not a string.{/color}") + elif error_code == ModError.Display_Not_String: + return __("{color=#ff8b1f}Display mode in ") + mod_name + lang_code + _(" is not a string.{/color}") + elif error_code == ModError.Display_Invalid_Mode: + return __("{color=#ff8b1f}Display mode in ") + mod_name + lang_code + _(" is not valid. Valid options are 'both', 'icon' and 'name', not ") + var_dict['display_mode'] + __(".{/color}") + elif error_code == ModError.Version_Not_String: + return __("{color=#ff8b1f}Mod's version in ") + mod_name + lang_code + _(" is not a string.{/color}") + elif error_code == ModError.Authors_Not_String_Or_List: + return __("{color=#ff8b1f}Mod's authors in ") + mod_name + lang_code + _(" is not a string or list.{/color}") + elif error_code == ModError.Authors_Contents_Not_String: + return __("{color=#ff8b1f}Author ") + var_dict['author_number'] + __(" in ") + mod_name + lang_code + _(" is not a string.{/color}") + elif error_code == ModError.Links_Not_String_Or_List: + return __("{color=#ff8b1f}Mod's links in ") + mod_name + lang_code + __(" is not a string or list.{/color}") + elif error_code == ModError.Links_Contents_Not_String: + return __("{color=#ff8b1f}Link ") + var_dict['link_number'] + __(" in ") + mod_name + lang_code + __(" is not a string.{/color}") + elif error_code == ModError.Description_Not_String: + return __("{color=#ff8b1f}Mod's description in ") + mod_name + lang_code + _(" is not a string.{/color}") + elif error_code == ModError.Mobile_Description_Not_String: + return __("{color=#ff8b1f}Mod's mobile description in ") + mod_name + lang_code + _(" is not a string.{/color}") + elif error_code == ModError.Screenshot_Displayables_Not_List: + return __("{color=#ff8b1f}Mod's screenshot displayables in ") + mod_name + lang_code + _(" is not a list.{/color}") + elif error_code == ModError.Screenshot_Displayables_Contents_Not_String: + return __("{color=#ff8b1f}Screenshot Displayable ") + var_dict['screenshot_number'] + __(" in ") + mod_name + lang_code + _(" is not a string.{/color}") + elif error_code == ModError.Icon_Displayable_Not_String: + return __("{color=#ff8b1f}Mod's icon displayable in ") + mod_name + lang_code + _(" is not a string.{/color}") + elif error_code == ModError.Thumbnail_Displayable_Not_String: + return __("{color=#ff8b1f}Mod's thumbnail displayable in ") + mod_name + lang_code + _(" is not a string.{/color}") + elif error_code == ModError.No_ID: + return __("{color=#ff1e1e}Mod in ") + mod_name + __(" failed to load: Does not have a mod ID.{/color}") + elif error_code == ModError.ID_Not_String: + return __("{color=#ff1e1e}Mod in ") + mod_name + __(" failed to load: ID is not a string.{/color}") + elif error_code == ModError.Similar_IDs_Found: + return __("{color=#ff1e1e}Mod in ") + mod_name + __(" failed to load: Another mod ") + mod_name2 + __(" has the same ID.{/color}") + elif error_code == ModError.Installed_Incorrectly: + return __("{color=#ff1e1e}Mod in ") + mod_name + __(" is not installed correctly.\nMake sure it's structure is ") + var_dict['good_dir'] + __(" instead of ") + var_dict['curr_dir'] + __(".{/color}") + elif error_code == ModError.Invalid_Image_Extension: + return __("{color=#ff8b1f}") + name_of_file + __(" image for mod in ") + mod_name + lang_code + _(" has an incompatible file extension. {a=https://www.renpy.org/doc/html/displayables.html#images}Only use images that Ren'Py supports!{/a}{/color}") + # Mod Menu screen ############################################################ ## ## Handles jumping to the mods scripts ## Could be more lean but if this is going to one of last time I touch the UI, ## then fine by me ## -#similar to quick_button funcs -screen mod_menu_button(filename, label, function): - button: - xmaximum 600 - ymaximum 129 - action function - if 'Back' in label or 'Return' in label or 'Quit' in label or 'Main Menu' in label: - activate_sound "audio/ui/uiBack.wav" - else: - activate_sound "audio/ui/uiClick.wav" - fixed: - add filename xalign 0.5 yalign 0.5 zoom 0.9 - text label xalign 0.5 yalign 0.5 xanchor 0.5 size 34 -# arr is [{ -# 'Name': string (name that appears on the button) -# 'Label': string (jump label) -# }, { .. } ] -# Reuse the same image string and keep things 'neat'. -screen mod_menu_buttons(filename, arr): - for x in arr: - use mod_menu_button(filename, x['Name'], Start(x['Label']) ) +transform tf_modmenu_slide: + xoffset 600 + linear 0.25 xoffset 0 + +# Some gay python workarounds for screen jank +init python: + def toggle_persistent_mods(index): + if persistent.enabled_mods[index][1] == True: + persistent.enabled_mods[index][1] = False + elif persistent.enabled_mods[index][1] == False: + persistent.enabled_mods[index][1] = True + def swapList(sl,pos1,pos2): + temp = sl[pos1] + sl[pos1] = sl[pos2] + sl[pos2] = temp + def swapMods(idx1,idx2): + swapList(mod_menu_metadata,idx1,idx2) + swapList(persistent.enabled_mods,idx1,idx2) + + # All operations that use this function need to be able to parse "None" as a safeguard. + def return_translated_metadata(mod_metadata, key): + if _preferences.language != None and _preferences.language in mod_metadata.keys() and mod_metadata[_preferences.language].get(key) != None: + return mod_metadata[_preferences.language][key] + elif "None" in mod_metadata.keys(): + return mod_metadata["None"].get(key) + else: + return None + + +default persistent.seenModWarning = False screen mod_menu(): - + key "game_menu" action ShowMenu("extras") tag menu style_prefix "main_menu" add gui.main_menu_background + add "gui/title_overlay.png" + add "gui/overlay/sidemenu.png" at tf_modmenu_slide - frame: - xsize 420 - yfill True - background "gui/overlay/main_menu.png" + default mod_metadata = {} + default reload_game = False + default mod_button_enabled = True + default mod_button_alpha = 1.0 + default mod_screenshot_list = None + default mod_icon = None + default mod_thumbnail = None -#side_yfill True - vbox: - xpos 1940 - yalign 0.03 - if persistent.splashtype == 1: - add "gui/sneedgame.png" - else: - add "gui/snootgame.png" + button at tf_modmenu_slide: + xpos 1455 + ypos 150 + xmaximum 300 + ymaximum 129 + # For some reason, Function() will instantly reload the game upon entering the mod menu, and put it in an infinite loop, so it's using a workaround + # with this variable. + action SetScreenVariable("reload_game", True) + activate_sound "audio/ui/snd_ui_click.wav" - viewport: - # this could be better but its ok for now - xpos 1885-540 + add "gui/button/menubuttons/menu_button.png" xalign 0.5 yalign 0.5 + text _("Reload Mods") xalign 0.5 yalign 0.5 size 34 + + if reload_game: + python: + reload_game = False + renpy.reload_script() + + + viewport at tf_modmenu_slide: + xpos 1338 + ypos 279 xmaximum 540 - ymaximum 0.8 - ypos 200 - yinitial 0 - if len(mod_menu_access) > 5: # Hides the scrollbar when not needed. Ideally nobody would install more than one mod at the same time, but oh well - scrollbars "vertical" + ymaximum 790 + + scrollbars "vertical" + vscrollbar_unscrollable "hide" mousewheel True draggable True pagekeys True - - if len(mod_menu_access) != 0: + if len(mod_menu_metadata) != 0 or len(mod_menu_access) != 0: vbox: - use mod_menu_button("gui/button/menubuttons/template_idle.png", _("Return"), ShowMenu("main_menu")) - spacing 18 - use mod_menu_buttons("gui/button/menubuttons/template_idle.png", mod_menu_access ) - else: - use mod_menu_button("gui/button/menubuttons/template_idle.png", _("Return"), ShowMenu("main_menu")) - text _("You have no mods! \nInstall some in:\n\"[moddir]\""): - style_prefix "navigation" - size 45 - text_align 0.5 - outlines [(3, "#445ABB", absolute(0), absolute(0))] at truecenter + for i, x in enumerate(mod_menu_metadata): + hbox: + xsize 129 + ysize 129 -############################# -# Stuff for mods in android # -############################# + vbox: + at truecenter + style_prefix None + spacing 5 + ysize 200 + # Move mod up button + if i!=0: + button: + at truecenter + style_prefix "main_menu" + add Null(30,30) -init python: - - import os + activate_sound "audio/ui/snd_ui_click.wav" - if renpy.android and not config.developer: + idle_foreground Transform("gui/button/menubuttons/up.png",xalign=0.5,yalign=0.5) + hover_foreground Transform("gui/button/menubuttons/up.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#fbff18")) + action Function(swapMods, i, i-1) + else: + add Null(30,30) at truecenter + # Enablin/disabling mods button + button: + at truecenter + style_prefix "main_menu" + action Function(toggle_persistent_mods, i) + activate_sound "audio/ui/snd_ui_click.wav" + add "gui/button/menubuttons/checkbox.png" xalign 0.5 yalign 0.5 - moddir = os.path.join(os.environ["ANDROID_PUBLIC"], "game") + if persistent.enabled_mods[i][1]: + idle_foreground Transform("gui/button/menubuttons/check.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#32a852")) + hover_foreground Transform("gui/button/menubuttons/check.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#fbff18")) + else: + idle_foreground Transform("gui/button/menubuttons/cross.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#a83232")) + hover_foreground Transform("gui/button/menubuttons/cross.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#fbff18")) - try: - # We have to create both the 'game' and 'mods' folder for android. - os.mkdir(moddir) - os.mkdir(os.path.join(moddir, "mods")) - except: - pass + # Move mod down button + if i!=len(mod_menu_metadata)-1: + button: + at truecenter + style_prefix "main_menu" + add Null(30,30) + action Function(swapMods, i, i+1) + activate_sound "audio/ui/snd_ui_click.wav" - else: - moddir = ".../game" + idle_foreground Transform("gui/button/menubuttons/down.png",xalign=0.5,yalign=0.5) + hover_foreground Transform("gui/button/menubuttons/down.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#fbff18")) - moddir += "/mods/" \ No newline at end of file + else: + add Null(30,30) at truecenter + + # The main mod button that displays the mod name and potential icon. + python: + mod_button_enabled = (x["Enabled"] == True) and (x.get("Label") != None) + mod_button_alpha = 1.0 if x["Enabled"] == True else 0.4 # Fade mod buttons out if their mod is disabled + + if x['Enabled'] == True and return_translated_metadata(x, "Icon Displayable") != None: + mod_icon = return_translated_metadata(x, "Icon Displayable") + elif return_translated_metadata(x, "Icon") != None: + mod_icon = return_translated_metadata(x, "Icon") + else: + mod_icon = None + + button: + at transform: + truecenter + alpha mod_button_alpha + activate_sound "audio/ui/snd_ui_click.wav" + hovered SetScreenVariable("mod_metadata", x) + + if mod_button_enabled: + action Start(x["Label"]) + else: + action NullAction() + + frame: + xsize 350 + ymaximum 2000 + if mod_button_enabled: + background Frame("gui/button/menubuttons/title_button.png", 12, 12) + hover_background Transform(Frame("gui/button/menubuttons/title_button.png", 12, 12), matrixcolor = BrightnessMatrix(0.1)) + else: + background Transform(Frame("gui/button/menubuttons/title_button.png", 12, 12),matrixcolor=SaturationMatrix(0.5)) + + padding (5, 5) + + # Display mod name and/or icon + if return_translated_metadata(x, "Display") == "icon" and mod_icon != None: + add RoundedCorners(mod_icon, radius=(5, 5, 5, 5)) xsize 342 fit "scale-down" at truecenter + elif return_translated_metadata(x, "Display") == "both" or return_translated_metadata(x, "Display") == None and mod_icon != None: + hbox: + spacing 20 + at truecenter + add mod_icon xysize (100, 100) fit "contain" at truecenter + if mod_button_enabled: + text return_translated_metadata(x, "Name") xalign 0.5 yalign 0.5 size 34 textalign 0.5 at truecenter + else: + text return_translated_metadata(x, "Name") xalign 0.5 yalign 0.5 size 34 textalign 0.5 at truecenter hover_color "#FFFFFF" + else: + if mod_button_enabled: + text return_translated_metadata(x, "Name") xalign 0.5 yalign 0.5 size 34 textalign 0.5 + else: + text return_translated_metadata(x, "Name") xalign 0.5 yalign 0.5 size 34 textalign 0.5 hover_color "#FFFFFF" + + # Only here for backwards compatibility to legacy mods + for x in mod_menu_access: + hbox: + add Null(88) + button: + at truecenter + activate_sound "audio/ui/snd_ui_click.wav" + action Start(x["Label"]) + + frame: + xsize 350 + ymaximum 2000 + background Frame("gui/button/menubuttons/title_button.png", 12, 12) + padding (5, 5) + hover_background Transform(Frame("gui/button/menubuttons/title_button.png", 12, 12), matrixcolor = BrightnessMatrix(0.1)) + text x["Name"] xalign 0.5 yalign 0.5 size 34 textalign 0.5 + + else: + fixed: + ymaximum 600 # This is the stupidest fucking hack fix + + if achievement.steamapi: + text _("You have no mods! \nInstall some in:\n\"{color=#abd7ff}[mod_menu_moddir]{/color}\"\nOr download some from the Steam Workshop!"): + style_prefix "navigation" + size 45 + text_align 0.5 + xalign 0.5 yalign 0.5 + outlines [(3, "#342F6C", absolute(0), absolute(0))] + else: + text _("You have no mods! \nInstall some in:\n\"{color=#abd7ff}[mod_menu_moddir]{/color}\""): + style_prefix "navigation" + size 45 + text_align 0.5 + xalign 0.5 yalign 0.5 + outlines [(3, "#342F6C", absolute(0), absolute(0))] + + # Displays the mod metadata on the left side + # This has two seperate viewports for error display because renpy is retarded + if mod_metadata != {}: + viewport: + xmaximum 1190 + ymaximum 930 + xpos 10 + ypos 140 + scrollbars "vertical" + vscrollbar_unscrollable "hide" + mousewheel True + draggable True + vbox: + style_prefix "mod_menu" + + # Thumbnail + python: + if mod_metadata["Enabled"] == True and return_translated_metadata(mod_metadata, "Thumbnail Displayable") != None: + mod_thumbnail = return_translated_metadata(mod_metadata, "Thumbnail Displayable") + elif return_translated_metadata(mod_metadata, "Thumbnail") != None: + mod_thumbnail = return_translated_metadata(mod_metadata, "Thumbnail") + else: + mod_thumbnail = None + + if mod_thumbnail: + frame: + background None + xpadding 30 + bottom_padding 30 + xalign 0.5 + add mod_thumbnail fit 'scale-down' + + # Mod details + # Omit checking for mod name, since we'll always have some kind of mod name. + # This will also not show anything if there's only a mod name, since we already show one in the mod button. + if return_translated_metadata(mod_metadata, "Version") != None or return_translated_metadata(mod_metadata, "Authors") != None or return_translated_metadata(mod_metadata, "Links") != None: + frame: + background Frame("gui/mod_frame.png", 30, 30) + padding (30, 30) + xfill True + + vbox: + if return_translated_metadata(mod_metadata, "Name") != None: + hbox: + text _("Name: ") + text return_translated_metadata(mod_metadata, "Name") + if return_translated_metadata(mod_metadata, "Version") != None: + hbox: + text _("Version: ") + text return_translated_metadata(mod_metadata, "Version") + if return_translated_metadata(mod_metadata, "Authors") != None: + if isinstance(return_translated_metadata(mod_metadata, "Authors"), list): + hbox: + text _("Authors: ") + text ", ".join(return_translated_metadata(mod_metadata, "Authors")) + else: + hbox: + text _("Author: ") + text return_translated_metadata(mod_metadata, "Authors") + if return_translated_metadata(mod_metadata, "Links") != None: + if isinstance(return_translated_metadata(mod_metadata, "Links"), list): + hbox: + text _("Links: ") + text ", ".join(return_translated_metadata(mod_metadata, "Links")) + else: + hbox: + text _("Link: ") + text return_translated_metadata(mod_metadata, "Links") + + # Description + if return_translated_metadata(mod_metadata, "Description") != None or return_translated_metadata(mod_metadata, "Mobile Description") != None: + frame: + background Frame("gui/mod_frame.png", 30, 30) + padding (30, 30) + xfill True + + # If there's no mobile description, display the regular description on Android. + if (not renpy.android or return_translated_metadata(mod_metadata, "Mobile Description") == None) and (return_translated_metadata(mod_metadata, "Description") != None): + text return_translated_metadata(mod_metadata, "Description") + elif return_translated_metadata(mod_metadata, "Mobile Description") != None: + text return_translated_metadata(mod_metadata, "Mobile Description") + + # Screenshots + python: + if mod_metadata["Enabled"] == True and return_translated_metadata(mod_metadata, "Screenshot Displayables") != None: + mod_screenshot_list = return_translated_metadata(mod_metadata, "Screenshot Displayables") + elif return_translated_metadata(mod_metadata, "Screenshots") != None: + mod_screenshot_list = return_translated_metadata(mod_metadata, "Screenshots") + else: + mod_screenshot_list = None + + if persistent.show_mod_screenshots and mod_screenshot_list: + frame: + background Frame("gui/mod_frame.png", 30, 30) + padding (30, 30) + xfill True + + hbox: + xoffset 12 + box_wrap True + box_wrap_spacing 25 + spacing 25 + + for i in mod_screenshot_list: + imagebutton: + at transform: + ysize 200 subpixel True + fit "scale-down" + xalign 0.5 yalign 0.5 + + idle i + hover Transform(i, matrixcolor=BrightnessMatrix(0.1)) + action Show("mod_screenshot_preview", Dissolve(0.5), img=i) + + elif len(mod_menu_errorcodes) != 0: + viewport: + xmaximum 1190 + ymaximum 920 + xpos 10 + ypos 150 + scrollbars "vertical" + vscrollbar_unscrollable "hide" + mousewheel True + draggable True + vbox: + style_prefix "mod_menu" + frame: + background Frame("gui/mod_frame.png", 30, 30) + padding (30, 30) + xfill True + vbox: + spacing 25 + for t in mod_menu_errorcodes: + text convert_errorcode_to_errorstring(t[0], t[1]) + + use extrasnavigation + + if not persistent.seenModWarning: + $ persistent.seenModWarning = True + use OkPrompt(_("Installing mods is dangerous since you are running unknown code in your computer. Only install mods from sources that you trust.\n\nIf you have problems with installed mods, check the README.md in the root of the mods folder."), False) + +style mod_menu_text: + size 34 + +# A copy of the image previewer found in the phone library, so that the phone library can still be modular if someone takes it out. +screen mod_screenshot_preview(img): + modal True + add Solid("#000") at Transform(alpha=0.6) + + add img: + align (0.5, 0.5) + fit "scale-down" + + key ["mouseup_1", "mouseup_3"] action Hide("mod_screenshot_preview", dissolve) \ No newline at end of file