From 85d71da086f75c2b67f5f434565ae303ca3020c3 Mon Sep 17 00:00:00 2001 From: Map Date: Fri, 4 Oct 2024 18:06:23 -0500 Subject: [PATCH 01/11] 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 From 7d2203dc0443f232b45365ac5223e717c7a488c1 Mon Sep 17 00:00:00 2001 From: Map Date: Fri, 4 Oct 2024 20:55:43 -0500 Subject: [PATCH 02/11] make the mod menu work (not testing mods) and slightly better the mod processing code --- game/gui/button/menubuttons/check.png | Bin 0 -> 541 bytes game/gui/button/menubuttons/checkbox.png | Bin 0 -> 3123 bytes game/gui/button/menubuttons/cross.png | Bin 0 -> 913 bytes game/gui/button/menubuttons/down.png | Bin 0 -> 645 bytes game/gui/button/menubuttons/up.png | Bin 0 -> 637 bytes game/src/mod_menu.rpy | 159 +++++++++++++---------- game/src/rounded_corners.rpy | 91 +++++++++++++ 7 files changed, 180 insertions(+), 70 deletions(-) create mode 100644 game/gui/button/menubuttons/check.png create mode 100644 game/gui/button/menubuttons/checkbox.png create mode 100644 game/gui/button/menubuttons/cross.png create mode 100644 game/gui/button/menubuttons/down.png create mode 100644 game/gui/button/menubuttons/up.png create mode 100644 game/src/rounded_corners.rpy diff --git a/game/gui/button/menubuttons/check.png b/game/gui/button/menubuttons/check.png new file mode 100644 index 0000000000000000000000000000000000000000..fad09ab8485d8efec234257e60ea7c87415f0e18 GIT binary patch literal 541 zcmV+&0^{xAh}sN- z@W#k=X-sK7^46vh2f;KjtRYXDBBg@k<`BXmk-!iKmm6{rA#w;zmy<(9;qZD-?%)q1 z)PL!NH{65&4r51NO zod=CTmgQTg(`lxX!cEUmHXIH|0la!V9vhXkR4N_nPdE6?=kps>(nut-g(Bm&z#x~) z9Z*Sw!QecIUdXx@i^UL?6scCLy!MkvmSyRhrPp^3p{B+m|qV{R%jf_42xDrYj8?Wv_mC=_BeeZLwvmn7+gN@|cwrH;UNhXG6b f*yr=j(D=Rq+-LLMep&Os00000NkvXXu0mjfH&^lr literal 0 HcmV?d00001 diff --git a/game/gui/button/menubuttons/checkbox.png b/game/gui/button/menubuttons/checkbox.png new file mode 100644 index 0000000000000000000000000000000000000000..962a8c42d6c36d60e498675881758cde1261c70d GIT binary patch literal 3123 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 N002ovPDHLkV1g4b#;yPW literal 0 HcmV?d00001 diff --git a/game/gui/button/menubuttons/cross.png b/game/gui/button/menubuttons/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..49b72b27848a840358e3aa08faba8e0d1df3a402 GIT binary patch literal 913 zcmV;C18)3@P)P0015c1^@s6+3dC300001b5ch_0olnc ze*gdg32;bRa{vGf6951U69E94oEQKA12#!SK~#90tyW)1Q(+WerAa+R|Ext|AVISn z3{2e9paL}((TYLsVX+4#7j0rbtj)pxC<|eXjEcYq>7jgS!H_u}C5-JsC3+Ej7zCnY z4jm2|4g_~QXWiM^y<2zJ`Gbqso!>dX?>qN==X??gKq{4Mqgv>2|vtD>^ux&Q>6$h){ZbeEfPPKG>QuO~pk4vXhgO zw*|Mpwzl???q+jya|$b`9Cu@5;|opG&d!ckPynG)sp>PC%oG(WS8ec{9;*mBEN@<; z(VVMF1mW}fZs6AGw=|hd*NS$0hu;#!o{hy~T~!MdF*i47=2lyKd;1Zz!TV=w$Y!%1 z(EfBDB)-w$tYAc|)t)^R5ya~1>Id#Fpf~9bO~B=HsUc>rOu9lH z9UUiY78o2Hya2(LcprmBI2;!2ZGwO}A31TqGHh>Skr^8sQyf?TR(YQ(oiW&5PjRaa zdmEl9(E?ttR|@{iL?8zTu`q;iBw%1FgiTen2n2aca|HkUs2y57PSPwXh7=y$ss&b7 zRy5$WO9K2d7!1521np&jTWxrz1O;q1+eJ<_<)){n`Li0(+1Ys<>=CL0P4&5;NSSJS z^FO}XZ03!G(b3V$X@gZfU^ufH&xz2&U^-f85#YdUYHH$7(vgvo%baT3i$o%7!L3UQ zm%0YHo&2Ce!^6YRc*L!(t@kuXa8Y|#jX)6-6B7!iwDlx=dU~4o3GsMb%SqExUtiyO zF(QaiDCDFIg$C*`7@#FwWlCo?69@$Ei4{Po)oQ%%&eK>63kw4h$lXmiM0U6!zhY5S n`_}1nt#-TJit+F-WTf{GVpkk_jMc@W00000NkvXXu0mjfA!#fHUQ~$bi(U_xR0T$t1Ar1_z$%FKfS=4 zH*X3Vl$Dj4UcGv?n{KxL|9>GmI+~9G1SU?L=t(bI&z?P-!ph1DwwHmQpP%vP&!2nf zVDE*Pm>4bwWKdmQ?FE$hNyE7Qd-?L^cqS$$6nhz@q@)<0JbAK{R<;7csqpY{P83TS z85zNJOG}F*t!zDg`g9}%E&z;ch7TV;Y@~s`$9#NzIB;4F0wpCSAYXr@qNTr&9z7a{ z5x3}okB^Vx!Gi}IsA%z_pr9ZYyq3b^y12O50;u6HHEca`;)EZ5ucE~@FoiFp#6oDc z++$~F$40cpAW%?H0P^)`3he&5ZQC|KSSS(=K#Af0{rjsZ@bPXhFE7$8g@NScWV8Qd zmSzVJ9&`hlL9&lQfR~q-;rjLKi%9e-h}vvoVnQJQ5CBsC9E9(9>}Bxw_SOK} z@eWH|e_6I{nK^B21p;PfW(HuvFah0GphFhv>FLqTUN8Vwaf%>cBiZ|H!GZ-;N>Ky= zuo=^Z6u0vg6&2}XFN2$#n-b8@H=wwkI(4ckeQbq-{rmU#1LJmywzf81?Pc)y_ZJ1m ftpbQmPALTdshKXST^Wcw00000NkvXXu0mjf^a>>b literal 0 HcmV?d00001 diff --git a/game/gui/button/menubuttons/up.png b/game/gui/button/menubuttons/up.png new file mode 100644 index 0000000000000000000000000000000000000000..cd474329ddd23047d7f4123e7f04166e765e7caa GIT binary patch literal 637 zcmV-@0)qXCP)NSm+%sJ_2kV!*-K$$9@eFF7Y$k{1Or99=&`FOk+4W;boKRrHRHOoYdX$6fADa8 z^L^isk8j=#AsR86On-li#p10(p|CZ(#~p{mK?Z}t0oF0QUK{Cj+CeG(jx|iBQsP>q zHk*wMhr^dt2G{S|9(OXC%x(Dk^qevFcDPENPA8d6CI?gn_D*n*-xHS0<+kDZp%w!P z_a1VQx?CIdrh}cc_Wod zr91TBxn0B=Vjs`uu8h~~C8N=3?rhZ{_A|@TO+KH$OD~?=g>K{obV)rP51Z{9oYSSP za_kr6Ue_#C=2NLu?xDyp4c$&b?vvSCOXKtT*jp;Mij*-PkH4aZS#!tba#=+FU{x`5 zqefU$$D$|-NIcXsTQzcS4Yh78Ew0z=_mL#&=ypo2R(raXG!zP%5dXmlsZ1jf2rTY8 z8jU_c@NWafv|6q134$Izu!MHQYzE(`~4f2gvyf_kH>cqbjCqU8;!>PmEBsc zR?_KoPIyRVTESp&^O86gi|yj3q~{1NqTOykyD)1uTS<~Wax 0.0) ? radius.xy : radius.zw; + float r = (uv_minus_center.y > 0.0) ? xy.x : xy.y; + return r; + } + """, fragment_200=""" + vec2 center = u_model_size.xy / 2.0; + vec2 uv = (v_tex_coord.xy * u_model_size.xy); + + vec2 uv_minus_center = uv - center; + float radius = get_radius(uv_minus_center, u_radius); + + vec4 color = texture2D(tex0, v_tex_coord); + + if (u_relative != 0.0) { + float side_size; + if (u_relative == 1.0) { + side_size = u_model_size.x; + } else if (u_relative == 2.0) { + side_size = u_model_size.y; + } else if (u_relative == 3.0) { + side_size = min(u_model_size.x, u_model_size.y); + } else { + side_size = max(u_model_size.x, u_model_size.y); + } + + radius *= side_size; + } + + if (u_outline_width > 0.0) { + vec2 center_outline = center - u_outline_width; + + float crop1 = rounded_rectangle(uv - center, center, radius); + float crop2 = rounded_rectangle(uv - center, center_outline, radius - u_outline_width); + + float coeff1 = smoothstep(1.0, -1.0, crop1); + float coeff2 = smoothstep(1.0, -1.0, crop2); + + float outline_coeff = (coeff1 - coeff2); + + gl_FragColor = mix(vec4(0.0), mix(color, u_outline_color, outline_coeff), coeff1); + } + else { + float crop = rounded_rectangle(uv_minus_center, center, radius); + gl_FragColor = mix(color, vec4(0.0), smoothstep(0.0, 1.0, crop)); + } + """) \ No newline at end of file From dabac1f5f0d9a76ae2687262a0d2fbdb32dd9e7d Mon Sep 17 00:00:00 2001 From: Map Date: Sat, 5 Oct 2024 08:04:35 -0500 Subject: [PATCH 03/11] Implement internal 'en' language, and backport splashscreen fixes from Wani --- game/script.rpy | 14 ++++++++++++++ game/src/image_definitions.rpy | 12 ++++++++++++ game/src/splashscreen.rpy | 24 +++++------------------- game/src/translation.rpy | 6 +++--- game/tl/en/mod-stuff-goes-here.txt | 0 5 files changed, 34 insertions(+), 22 deletions(-) create mode 100644 game/tl/en/mod-stuff-goes-here.txt diff --git a/game/script.rpy b/game/script.rpy index 7032e3c..f02b0e5 100644 --- a/game/script.rpy +++ b/game/script.rpy @@ -54,6 +54,20 @@ init python: if renpy.seen_image("fang tail"): renpy.mark_image_seen("fang_tail_movie") + # Determine the splash type for the Snoot game logo + $ persistent.splashtype = random.randint(0,2000 - 1) + + +label before_main_menu: + # Force users pre-Patch 11 to use english if they were on 'None' + if preferences.language == None: + $ preferences.language = 'en' + + # Call initial language setup screen + if (persistent.languaged_up is None): + $ preferences.set_volume('ui', config.default_sfx_volume) + $ persistent.languaged_up = True + call screen lang_sel label start: diff --git a/game/src/image_definitions.rpy b/game/src/image_definitions.rpy index 504a351..788c90c 100644 --- a/game/src/image_definitions.rpy +++ b/game/src/image_definitions.rpy @@ -352,6 +352,18 @@ init 1 python: # aight, time for the stuff that isn't gallery required. +### SPLASHSCREEN + +image caveintrosequence: + "caveintro" + alpha 0 + time 0.5 + linear 3.5 alpha 1 + time 10 + linear 1 alpha 0 + + + ### OTHER diff --git a/game/src/splashscreen.rpy b/game/src/splashscreen.rpy index e520124..cfaf3bc 100644 --- a/game/src/splashscreen.rpy +++ b/game/src/splashscreen.rpy @@ -1,24 +1,10 @@ label splashscreen: - $ persistent.splashtype = random.randint(0,2000 - 1) - - image caveintrosequence: - "caveintro" - alpha 0 - time 0.5 - linear 3.5 alpha 1 - time 10 - linear 1 alpha 0 - - show caveintrosequence - play sound 'audio/OST/startup.ogg' - pause 11.2 - stop sound - - if (persistent.languaged_up is None): - $ persistent.languaged_up = True - $ preferences.set_volume('ui', config.default_sfx_volume) # hack - call screen lang_sel + if not renpy.get_autoreload(): + show caveintrosequence + play sound 'audio/OST/startup.ogg' + pause 11.2 + stop sound return diff --git a/game/src/translation.rpy b/game/src/translation.rpy index 1bc58a2..7912a47 100644 --- a/game/src/translation.rpy +++ b/game/src/translation.rpy @@ -36,7 +36,7 @@ init python: notice = _("NOTICE: Please keep in mind this is a fan translation, and as such it may not be completely accurate to the original intent of any written lines.") languages = [ - {'image': 'gui/flag/USofA.png', 'name': 'English', 'value': None }, + {'image': 'gui/flag/USofA.png', 'name': 'English', 'value': 'en' }, {'image': 'gui/flag/Mexico.png', 'name': 'Español', 'value': 'es'}, {'image': 'gui/flag/Rus.png', 'name': 'Русский', 'value': 'ru'}, {'image': 'gui/flag/Poland.png', 'name': 'Polski', 'value': 'pl'}, @@ -106,7 +106,7 @@ screen lang_sel(): imagebutton: idle darkie(languages[i]["image"]) hover glowie(languages[i]["image"]) - action If(languages[i]["value"] in persistent.seenWarning or languages[i]["value"] == None, + action If(languages[i]["value"] in persistent.seenWarning or languages[i]["value"] == 'en'', true = [Language(languages[i]["value"]), MainMenu(False,False)], # Important to change the language before calling notice. Otherwise it will be in english. false = [Language(languages[i]["value"]), AddToSet(set=persistent.seenWarning, value=languages[i]["value"]), Show(screen="OkPrompt", message=notice, go_menu=True)] @@ -122,7 +122,7 @@ screen lang_button(lang): spacing 15 textbutton lang["name"]: activate_sound "audio/ui/uiRollover.wav" - action If(lang["value"] in persistent.seenWarning or lang["value"] == None, + action If(lang["value"] in persistent.seenWarning or lang["value"] == 'en', true = [Language(lang["value"])], false = [Language(lang["value"]), AddToSet(set=persistent.seenWarning, value=lang["value"]), Show(screen="OkPrompt", message=notice, go_menu=False)] ) diff --git a/game/tl/en/mod-stuff-goes-here.txt b/game/tl/en/mod-stuff-goes-here.txt new file mode 100644 index 0000000..e69de29 From f0892157aba628fed79aa56e524e4995931581d4 Mon Sep 17 00:00:00 2001 From: Map Date: Sat, 5 Oct 2024 08:06:13 -0500 Subject: [PATCH 04/11] Remove redundant mod var setup --- game/script.rpy | 6 ------ 1 file changed, 6 deletions(-) diff --git a/game/script.rpy b/game/script.rpy index f02b0e5..0b4391e 100644 --- a/game/script.rpy +++ b/game/script.rpy @@ -14,12 +14,6 @@ #Why yes all my code was formerly in one massive file called "script" thats 28k lines long, how could you tell? #Licensed under the GNU AGPL v3, for more information check snootgame.xyz or the LICENSE file that should have came with this work. -init -1 python: - # Modding Support variables - # All mod rpy files must run a small init python script - mod_dir = "mods/" - mod_menu_access = [] - init python: import webbrowser # This is for the ch2 "look the link up" choice import random From 6c283a8dcd152fabb3a2cb616e4b9b9490d6e968 Mon Sep 17 00:00:00 2001 From: Map Date: Sat, 5 Oct 2024 08:06:25 -0500 Subject: [PATCH 05/11] add back in mod menu options --- game/options.rpy | 5 +++++ game/screens.rpy | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/game/options.rpy b/game/options.rpy index 4e16c0e..ce0527b 100644 --- a/game/options.rpy +++ b/game/options.rpy @@ -139,6 +139,11 @@ default persistent.enable_debug_scores = config.developer default persistent.enable_chapter_select = config.developer default persistent.lewd = False default persistent.autoup = False +default persistent.show_mod_screenshots = True + +init -1000 python: + if persistent.newmods_default_state == None: + persistent.newmods_default_state = True init python: # No idea what this does diff --git a/game/screens.rpy b/game/screens.rpy index a5bba53..56b92c0 100644 --- a/game/screens.rpy +++ b/game/screens.rpy @@ -1005,6 +1005,12 @@ screen preferences(): textbutton _("Enable Debug Scores") action ToggleVariable("persistent.enable_debug_scores", True, False) textbutton _("Enable Chapter Select") action ToggleVariable("persistent.enable_chapter_select", True, False) + vbox: + style_prefix "check" + label _("Mods") + textbutton _("Show Mod Screenshots") action [Function(onclick_audio, persistent.show_mod_screenshots), ToggleVariable("persistent.show_mod_screenshots", True, False)] + textbutton _("Enable New Mods") action [Function(onclick_audio, persistent.newmods_default_state), ToggleVariable("persistent.newmods_default_state", True, False)] + if not main_menu: if config.developer and persistent.enable_debug_scores: $ debug_story_variables(False) From 6a407f4630ac2c2c3aff6aeebf444d3a6d921d7a Mon Sep 17 00:00:00 2001 From: Map Date: Sat, 5 Oct 2024 14:07:09 -0500 Subject: [PATCH 06/11] fix typo blunders that crash the game --- game/script.rpy | 2 +- game/src/translation.rpy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/game/script.rpy b/game/script.rpy index 0b4391e..66e2307 100644 --- a/game/script.rpy +++ b/game/script.rpy @@ -49,7 +49,7 @@ init python: renpy.mark_image_seen("fang_tail_movie") # Determine the splash type for the Snoot game logo - $ persistent.splashtype = random.randint(0,2000 - 1) + persistent.splashtype = random.randint(0,2000 - 1) label before_main_menu: diff --git a/game/src/translation.rpy b/game/src/translation.rpy index 7912a47..2cca593 100644 --- a/game/src/translation.rpy +++ b/game/src/translation.rpy @@ -106,7 +106,7 @@ screen lang_sel(): imagebutton: idle darkie(languages[i]["image"]) hover glowie(languages[i]["image"]) - action If(languages[i]["value"] in persistent.seenWarning or languages[i]["value"] == 'en'', + action If(languages[i]["value"] in persistent.seenWarning or languages[i]["value"] == 'en', true = [Language(languages[i]["value"]), MainMenu(False,False)], # Important to change the language before calling notice. Otherwise it will be in english. false = [Language(languages[i]["value"]), AddToSet(set=persistent.seenWarning, value=languages[i]["value"]), Show(screen="OkPrompt", message=notice, go_menu=True)] From 8dfa492442e74db2aa8f2dbc3acfc866929eb064 Mon Sep 17 00:00:00 2001 From: Map Date: Sat, 5 Oct 2024 14:15:11 -0500 Subject: [PATCH 07/11] forgor to return the fucking label that puts you into the main menu --- game/script.rpy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/game/script.rpy b/game/script.rpy index 66e2307..f2aae31 100644 --- a/game/script.rpy +++ b/game/script.rpy @@ -63,6 +63,8 @@ label before_main_menu: $ persistent.languaged_up = True call screen lang_sel + return + label start: $ toggle_debug() From e54c950ef6ae8c9dd6d2b4dd1673e5828465f52d Mon Sep 17 00:00:00 2001 From: Map Date: Sat, 5 Oct 2024 18:12:28 -0500 Subject: [PATCH 08/11] Fix UI issues. Tighten up mod display to include more buttons at once, showing the original while in android. Expands mod buttons to fill the space the menu panel provides. Redone mod button images to fit aesthetically Imports misc files for modding, including the readme and flags --- game/gui/button/menubuttons/check.png | Bin 541 -> 3202 bytes game/gui/button/menubuttons/checkbox.png | Bin 3123 -> 3861 bytes game/gui/button/menubuttons/cross.png | Bin 913 -> 3494 bytes game/gui/button/menubuttons/down.png | Bin 645 -> 551 bytes game/gui/button/menubuttons/up.png | Bin 637 -> 541 bytes game/gui/mod_frame.png | Bin 0 -> 2275 bytes game/mods/-DISABLEALLMODS.txt | 0 game/mods/-NOLOADORDER.txt | 0 game/mods/-NOMODS.txt | 0 game/mods/README.md | 149 ++++++++++++++++------- game/src/mod_menu.rpy | 55 ++++++--- 11 files changed, 142 insertions(+), 62 deletions(-) create mode 100644 game/gui/mod_frame.png create mode 100644 game/mods/-DISABLEALLMODS.txt create mode 100644 game/mods/-NOLOADORDER.txt create mode 100644 game/mods/-NOMODS.txt diff --git a/game/gui/button/menubuttons/check.png b/game/gui/button/menubuttons/check.png index fad09ab8485d8efec234257e60ea7c87415f0e18..1ea6b4656acb3ff53ad12f0fcb160c0e9763103e 100644 GIT binary patch delta 3197 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 delta 515 zcmV+e0{s1g8Jz@>B!2;OQb$4o*~u(_00009a7bBm000XU000XU0RWnu7ytkP!bwCy zRCt`tlrcyGVHALshzN|dAzXrp+6;p5#>jMOOldvx)}{~#!894r51NOod=CTmgQTg(`lxX!cEUmHXIH|0la!V9vhXk zR4N_nPdE6?=kps>(nut-g(Bm&z#x~)9Z*Sw!QecIUdXx@i^UL?6scCLy!MkvmSyRh zrPp|+UGgqGpJRlnc=QWWLMVzG?649mm}d4VWFFPqKokA2ft zt0j{26N|-Mm@gCxUYr6WUoMv~#(zIhuh-v5ajjOn_I&&OHdR&MiL}IV+|oqiX0v&2 zoYtR=D^Mc7m2f!hnn;SI)9D@U51>@16H5_T)^_YRG5jRY4u@lIDlvj}{Y5HgGMVkE zrAR0gVl;if8aJ0D>4ZvZkV>VFz;}lMOZ(X8^Ucusz5(24^WA<~^S=N9002ovPDHLk FV1mmx@P+^Y diff --git a/game/gui/button/menubuttons/checkbox.png b/game/gui/button/menubuttons/checkbox.png index 962a8c42d6c36d60e498675881758cde1261c70d..39f41e161fb7cea657b6e90e367335f7bd329c52 100644 GIT binary patch delta 3802 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 Ss3 delta 890 zcmV-=1BLvi8<7W)B!2;OQb$4o*~u(_00009a7bBm000XU000XU0RWnu7ytkRHc3Q5 zRCt`NR$oX{VH97bNj*gWtVLiTL9-kTOx)9;0yP%Vib3sRu?Hm=ZDKyG&B6XC3t^0m ziogfyp?qndX?>qN= z=X??gKq{4PSR@f`~9~LEKo!;ne3-UK(W2O?W$P-(bm>hmq;YO(cr9LM61=F zJrohd>VN9$2ktJQH|Y*dz~yqOA!e>jx=CL0P4&5;NSSJS^FO}XZ03!G(b3V$X@gZfU^ufH&xz2& zU^-f85#YdUYHH$7(vgvo%baT3i$o%7!L3UQm%0YHo&2Ce!^6YRc*L!(t@kuXa8Y|# zjdMT|6B82(rnL1WdwP1B_X+WMT+2z*QeR)+c`+i0P$=Z23xx*iFBqUDTxCjUH4_K~ z?uivZsMTt`?#|O#3kwSa63E?6I7D{1AirW!Q~TEGbgg!~-HP$>FJz?m4`Npwd5qP? Qq5uE@07*qoM6N<$g439jT>t<8 diff --git a/game/gui/button/menubuttons/down.png b/game/gui/button/menubuttons/down.png index b921b86b9f634bc71dca2c125319a77bf5943507..64a5f78ef98f0f0e2ac8521e9b11083d59b34fdb 100644 GIT binary patch delta 518 zcmV+h0{Q)g1*Zg%D}M_T000XT0n*)m`~Uy|8+1ijbW?9;ba!ELWdK8EY;$>YAX9X8 zWNB|8RBvx=!KdMT00059NklMjH#k!Uq_lh0g>uYT+ZIK`>vSKcTr?LXO4B#eZ*F=S^EiaXUhptJ)C8ZyWapt9h8H+pG5QsU(OL^0 z#xyongf8Jco`0+k_JZL#Mk<0oiPYIF>iq58fvhN11MR0&mO0@DLXZqOZlG%KgWuzA!#fHUQ~$bi(U_ zxR0T$t1Ar1_z$%FKfS=4H*X3Vl$Dj4UcGv?n{KxL|9>GmI+~9G1SU?L=t(bI&z?P- z!ph1DwwHmQpP%vP&!2nfVDE*Pm>4bwWKdmQ?FE$hNyE7Qdw==z<#;A0CKP)aq@<)6 zo;-Q7lUB9@!Kv`@a849U85tSDbW2N%Bdu&befo4H11ndUp`f537QB|i;<~uF*aE2GFEwmE zapHs@ey^g%HGeRLFQddlXtvyAXJ^Mow8bD$P*4E!^=As~{<&@2Ha}P>5)D9!;r{*m zt0?gCZZ9t{(kz96oK!ZM!VE6act5>^HWGM{Xy?b{dv9|vH{d<$Gt$!_5>}3cI4b=yl`I(5g{<(GQ zRy&X$vf>sUaB^}ooH})CCJ`Q9Wnf@HxrHDAb9q8Sf)>!=5BTDC$&w{j)Ug!}08{u> zT(Cwzy zFaTC@ie4aJBiZ|H!GZ-;N>Ky=uo=^Z6u0vg6&2}XFN2$#n-b8@H=wwkI(4ckeQbq- z{rmU#1LJmywzf81?Pc)y_ZJ1mtpbQmPALTdshKXST^Wcw00000NkvXXu0mjfsO%!# diff --git a/game/gui/button/menubuttons/up.png b/game/gui/button/menubuttons/up.png index cd474329ddd23047d7f4123e7f04166e765e7caa..b1af7ac44836fd5eb34869a8d35e747f5c64b204 100644 GIT binary patch delta 508 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 literal 0 HcmV?d00001 diff --git a/game/mods/-DISABLEALLMODS.txt b/game/mods/-DISABLEALLMODS.txt new file mode 100644 index 0000000..e69de29 diff --git a/game/mods/-NOLOADORDER.txt b/game/mods/-NOLOADORDER.txt new file mode 100644 index 0000000..e69de29 diff --git a/game/mods/-NOMODS.txt b/game/mods/-NOMODS.txt new file mode 100644 index 0000000..e69de29 diff --git a/game/mods/README.md b/game/mods/README.md index 478b583..1808721 100644 --- a/game/mods/README.md +++ b/game/mods/README.md @@ -1,55 +1,120 @@ -To normal users installing multiple mods: It's recommended to only have one mod folder installed in the mods directory in case two mods have duplicate `green_fang_story` label, otherwise the game will error out if mods conflict each other, this includes not only labels but images & sound too. +--- MOD LOADER USAGE --- -The game loads an alternate storyline.rpy, this allows you to control the flow of the game's storytelling -Examples include: -- You want to inject more stuff inbetween chapters, maybe you hate time skip writing -- You want to have more of an after story kind of ordeal, for example expanding Ending 2 -- You want to replace the entire story route -You can still call the vanilla game's chapters like the intro (call chapter_1) for example but you might want to either copy the vanilla scripts and mix in your edits to have full control +If there's problems with installed mods - like if an enabled mod makes the game crash on startup - there's 3 file flags you can apply to work around it, checked only in the root of the mods folder: +- Any file starting with "DISABLEALLMODS". This will force disable all currently installed mods, but will still load all their metadata. Removing this flag will return the mod states to how it was previously. +- Any file starting with "NOLOADORDER". This will erase the load order/states and always load mods in folder alphabetical order, with mod states depending on the "Enable New Mods" option in the preferences menu. +- Any file starting with "NOMODS". This turns off mod loading entirely by making the game not find any metadata files to load. +These file flags already exist in the mods folder, but renamed to not trigger in-game. Enable them by renaming the file to take off the first hyphen. -You will need to learn bit of Ren'Py & bit of actual Python anyways but you don't have to think too hard in learning anything new other than familiarizing with this game's script.rpy's already defined Character objects and images, you can freely ignore most of the UI and so on for the inexperienced but passionate artist and/or writer. +When ordering the mods in the mod menu, know that not all mod code will be loaded according to the order. Ren'Py has a feature called 'init' that will run code at certain mod-defined stages (priorities) of the engine starting up, so if one mod's init block is set to run at an earlier priority than another mod's, it doesn't what order it is in the mod loader, it will always load that init first. The only time when the order comes into effect is if 2 mod's init blocks run at the same priority, or aren't running in an init block. -Textbox limitation: ~300 characters / four lines, the maximum in the vanilla game's script is around roughly ~278 and that only barely overflows to four lines, so <200 characters / three lines of text should be fine. ---- Ideal file structure of your mod --- +--- MOD CREATION PRE-REQUSITES --- + +Before modding, you may want to do either of these things: + +-Download Snoot Game as a renpy project and launch through the SDK so you can have easy access to debugging and other QoL tools, including dev mode. (Download the repo in this link: https://git.cavemanon.xyz/Cavemanon/SnootGame) +-Open script.rpy and put 'config.developer = True' somewhere in the 'init python' block to have access to renpy's dev mode. + +Pick the latter option if you're lazy, since you don't seriously need to use the SDK for most things. + + +--- MOD CRREATION --- + +When creating a mod, make a new folder within the 'mods' directory at the root of the game directory and name it whatever you want. Inside that folder, make a file called 'metadata.json', and follow the JSON file format to implement details about your mod. An example would be: + +``` +{ + "ID": "234234u9SDjjasdhl23", + "Name": "Test Mod", + "Label": "test_mod_label", + "Version": "1.0", + "Authors": [ "Author1", "Author2", "Author3" ], + "Links": "Link1", + "Description": "This contains the description of my mod" +} +``` + +Make sure there isn't a comma at the end of the last entry in your JSON. + +Below is all the possible entries you can put in, and explanations for what they do. Note that you don't need to put all of these in your metadata file, and infact the only hard requirement is the "ID" entry. + + "ID" : The ID of your mod. Required to be able to load your mod at all, as it is used by the mod loader for mod orders and enabling/disabling. Make this as unique of a string as you can, like a hash. Smash your keyboard if you must. This is how the game knows to differentiate your mod from others (And can be used by other mods to find if a user has your mod installed, if they so choose). + + "Name" : The name of your mod. If this doesn't exist, the game will assume the name of your mod folder. + + "Label" : The label to jump to start your mod story. If this doesn't exist, the button this mod will appear as will do nothing when clicked. Useful if you're only modifying something relating to the base game. + + "Version" : The version number of your mod. + + "Authors" : The authors of your mod. This can be a list of strings or just a string. If it's just a string, it will display in the mod details pane with only "Author:" instead of "Authors:" + + "Links" : The links to download your mod and/or advertisement. This can be a list of strings or just a string. If it's just a string, it will display in the mod details pane with only "Link:" instead of "Links:". In order for this to be useful, use the 'a' text tag to make these texts hyperlinks. + + "Description" : The description of your mod. + + "Mobile Description" : The description of your mod, but only appearing while playing Snoot on Android. If this doesn't exist, it will assume the contents of the description entry. Otherwise, you can copy your description text here and format it however you think it fits for Android. + + "Display" : How your mod button appears if there's an icon image detected. This can be set to "name" - which only displays the mod name - "icon" - which only displays the icon, taking up the entire button - or "both" - which displays the name and icon together, with the icon miniaturized and to the side of the name. This defaults to "both" if it doesn't exist, and if an icon image is not present, it will fall back to "name" mode. + + "Thumbnail Displayable" : What displayable to use for the thumbnail when the mod has loaded scripts. If this doesn't exist, the game will use the thumbnail image found alongside your metadata file + + "Icon Displayable" : What displayable to use for the icon when the mod has loaded scripts. If this doesn't exist, the game will use the icon image found alongside your metadata file + + "Screenshot Displayables" : What displayables to use for screenshots when the mod has loaded scripts. This should be a list of strings. The game will choose each displayable in the list over images found alongside the metadata file sequentially, so if there's 5 screenshot images and 3 screenshot displayables, the last 2 will still display the screenshot images, and the first 3 will display the screenshot displayables. If there's more displayables than images, then the mod will appear to gain screenshots in the mod details pane when the mod is enabled. If you enter empty strings in the list ( "" ), the game will interpret that as a deliberate skipping over to load screenshot images instead of displayables, so if your list consists of '[ "", "my_displayable" ]' and there's any number of screenshot images found, the first screenshot will still show a screenshot image, and only the next one will show a displayable. If this entry doesn't exist, it will just use the screenshot images found alongside your metadata file. + +In the same directory as your metadata file, there's image files you can put in the to make your mod more appealing. These can be any of Ren'Py's supported image file types, so the file extension here is just for demonstration: + + -'thumbnail.png' will appear as a banner for your mod, at the top of the mod details pane + -'icon.png' will show a small image next your mod name or take up the entire button depending on what your "Display" entry is set to. + -'screenshot(number).png' will show screenshots at the bottom of the mod details pane. The '(number)' is a placeholder for a number that represents what order your screenshots appear in. For example, you can have 'screenshot1.png', 'screenshot2.png', and 'screenshot3.png' in your mod directory, and they will all appear in the mod details pane in order. These numbers don't need to be strictly sequential, they can be any number as long as they are integers, and the order will be derived from ASCII ordering. + +As the game loads the metadata, it will also collect scripts for loading. When you make your mod scripts, ALL OF THEM NEED TO HAVE .rpym EXTENSIONS. This is important for being able to control mods with the mod loader, otherwise there would be mod conflicts galore. If you use .rpy extensions to make your mod scripts, they will work, but they will not be manageable by the mod loader. This means you should only use them for development, or if you're not using the mod loader anyway - such as adding a translation to the base game. + +Additionally, Ren'Py will not load any files from the mods folder automatically, so any and all audio/image/video files need to be manually defined to be usable. + +--- TRANSLATION --- + +For disambiguation, a "language code" refers to the code Ren'Py uses to refer to languages program-wide, and can be found on the folder names in "game/tl", with "None" being the default language (Representing a fallback language, but usually meaning English. Internally, it's an actual None variable) + +For translating mods, you should use the Ren'Py SDK to automatically generate translations from mod scripts. Make sure to use a language code that Snoot supports when you do so, so you don't accidentally create a new language. Put them into an organized directory in your mod and change them to use .rpym extensions so that translations don't activate unless your mod is as well (This prevents potential conflicts from other mods from interface/string translations). + +For assets, the easiest way is to put them in the "tl" folder and have the same filepath to your mod asset as it is from the "game" folder to your asset, just like how the officially supported translations are. This will make your mod less portable, but at least there's no namespace or file conflicts for translating images. If you're deadset on making your mod portable, you'll have to make conditionals in your mod to swap out images depending on the language, which means other people translating your mod can't just add a file somewhere and be done with it, they will need to edit your mod scripts as well. + +For translating metadata, you can translate the .json file by creating a new .json and naming it "metadata_(language code).json" (Ex: "metadata_es.json" for spanish). Fill out the .json how you would with the normal metadata file, but with your translated strings. The game will automatically pick this up and replace the strings (or add, if there's no 'None' variant of an entry) according to your language. Note that translating the ID or Label will do nothing, so you will always need a metadata.json file with at least an ID entry even if your mod isn't in english. + +For the images found alongisde your metadata, you can do the same thing as your .json to replace them. Simply append your language code to your image filename like "thumbnail_es.png", "icon_es.png", etc.. For screenshots, the number in your filename will determine if any given translated image will replace another image, or add it inbetween images. So if you have "screenshot4.png", and you have "screenshot2_es.png" and "screenshot5_es.png", all screenshots will show up if your language is in spanish. If there's "screenshot4_es.png" in the directory, then it will replace "screenshot4.png". The final screenshot display will always take the english screenshots as a base before replacing the images with whatever translated images that exist. + + +--- TIPS --- + +The Ren'Py documentation is your friend, but it is also a bitch-ass friend. It will sometimes be notoriously unhelpful, so consult other renpy dudes from the Ren'Py discord, your snoot communities, or youtube. This may also be of interest, as it will link to other documentation as well as many interesting libraries you can use: + https://github.com/methanoliver/awesome-renpy + +Link to Ren'Py documentation: + https://www.renpy.org/doc/html/ + +When making the file structure for your mod, this is the ideal. Keep it nice and organized, preferably keeping the root of your mods folder clear of eveything but metadata related stuff: +``` In the root of the mods folder: folder_of_your_mod_name - - name_of_storyline.rpy + - metadata.json + - Your various image metadatas -> asset_folder - asset.png -> script_folder - - script.rpy - - name_of_storyline.rpy + - script.rpym + -> etc. folders... + - etc. files... ``` -init python: - # Modding Support variables - # All mod rpy files must have title of their mod (this shows up on a button) - # and finally the label that controls the flow of dialogue - - mod_menu_access += [{ - 'Name': "Mod Name", - 'Label': "mod_storyline" - }]; - -image template_sample = Image("mods/folder_of_your_mod_name/asset_folder/asset.png") - -label mod_storyline: - call chapter_1_new +To start your mod, just make it under the label you defined in your metadata file in a mod script. An example in a newly created "mymodscript.rpym": +``` + label my_mod_label: + scene my_background + show my_sprite + "blah blah dialogue" ``` - script_folder/script.rpy -``` -label chapter_1_new: - show template_sample at scenter - "Sample Text" +Textbox limitation: ~300 characters / four lines, the maximum in the vanilla game's script is around roughly ~278 and that only barely overflows to four lines, so <200 characters / three lines of text should be fine. - hide template_sample - play music 'audio/OST/Those Other Two Weirdos.ogg' - show anon neutral flip at aright with dissolve - A "Sample Text" - - return -``` - -The funny thing is I don't even like 'fanfictions' to begin with but this mod support allows these 'fanfictions', ironic. +If a user has many mods installed, it would be annoying to need to keep track of which mods to enable or disable because of code conflicts, so make your variables/functions unique. Since Ren'Py's own scripting doesn't support namespaced stuff, a very simple way is to prefix all your variables/functions with a mod-unique name, similar to your mod ID. An example would be "mymodisfantastic2349234_function_or_variable". Kinda ugly, but manageable with Ctrl-F and string replacement if there's problems. If there must be code conflicts, let the user know in your mod description! \ No newline at end of file diff --git a/game/src/mod_menu.rpy b/game/src/mod_menu.rpy index bbf3dc3..f86a490 100644 --- a/game/src/mod_menu.rpy +++ b/game/src/mod_menu.rpy @@ -708,10 +708,10 @@ screen mod_menu(): use mod_menu_top_buttons(_("Return"), ShowMenu("extras")) viewport: - xpos 1338 - ypos 179 - xmaximum 540 - ymaximum 790 + xpos 1260 + ypos 200 + xmaximum 600 + ymaximum 869 scrollbars "vertical" vscrollbar_unscrollable "hide" @@ -731,7 +731,12 @@ screen mod_menu(): at truecenter style_prefix None spacing 5 - ysize 200 + + if renpy.variant(["mobile", "steam_deck"]): + ysize 200 + else: + ysize 160 + # Move mod up button if i!=0: button: @@ -741,8 +746,8 @@ screen mod_menu(): activate_sound "audio/ui/snd_ui_click.wav" - 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")) + idle_foreground Transform("gui/button/menubuttons/up.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#445ABB")) + hover_foreground Transform("gui/button/menubuttons/up.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#00FF03")) action Function(swapMods, i, i-1) else: add Null(30,30) at truecenter @@ -750,16 +755,21 @@ screen mod_menu(): button: at truecenter style_prefix "main_menu" + + # Manual adjustment to make the arrow buttons closer to the mod toggle button + if not renpy.variant(["mobile", "steam_deck"]): + ysize 65 + 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 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")) + idle_foreground Transform("gui/button/menubuttons/check.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#00ff40")) + hover_foreground Transform("gui/button/menubuttons/check.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#ffffff")) 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")) + idle_foreground Transform("gui/button/menubuttons/cross.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#db1a1a")) + hover_foreground Transform("gui/button/menubuttons/cross.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#ffffff")) # Move mod down button if i!=len(mod_menu_metadata)-1: @@ -770,8 +780,8 @@ screen mod_menu(): action Function(swapMods, i, i+1) activate_sound "audio/ui/snd_ui_click.wav" - 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")) + idle_foreground Transform("gui/button/menubuttons/down.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#445ABB")) + hover_foreground Transform("gui/button/menubuttons/down.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#00FF03")) else: add Null(30,30) at truecenter @@ -801,11 +811,10 @@ screen mod_menu(): action NullAction() frame: - xsize 350 + xsize 475 ymaximum 2000 if mod_button_enabled: background Frame("gui/button/menubuttons/template_idle.png", 12, 12) - hover_background Transform(Frame("gui/button/menubuttons/template_idle.png", 12, 12), matrixcolor = BrightnessMatrix(0.1)) else: background Transform(Frame("gui/button/menubuttons/template_idle.png", 12, 12),matrixcolor=SaturationMatrix(0.5)) @@ -832,18 +841,24 @@ screen mod_menu(): # Only here for backwards compatibility to legacy mods for x in mod_menu_access: hbox: + xsize 129 + ysize 129 add Null(88) + vbox: + if renpy.variant(["mobile", "steam_deck"]): + ysize 200 + else: + ysize 160 button: at truecenter activate_sound "audio/ui/snd_ui_click.wav" action Start(x["Label"]) frame: - xsize 350 + xsize 475 ymaximum 2000 background Frame("gui/button/menubuttons/template_idle.png", 12, 12) padding (5, 5) - hover_background Transform(Frame("gui/button/menubuttons/template_idle.png", 12, 12), matrixcolor = BrightnessMatrix(0.1)) text x["Name"] xalign 0.5 yalign 0.5 size 34 textalign 0.5 else: @@ -862,9 +877,9 @@ screen mod_menu(): if mod_metadata != {}: viewport: xmaximum 1190 - ymaximum 930 - xpos 10 - ypos 140 + ymaximum 1030 + xpos 15 + ypos 40 scrollbars "vertical" vscrollbar_unscrollable "hide" mousewheel True From aa0eff4377deeeb1fc526928a6b0a52c0d210b70 Mon Sep 17 00:00:00 2001 From: Map Date: Sun, 6 Oct 2024 14:27:48 -0500 Subject: [PATCH 09/11] move mod buttons slightly higher, and create a seperate 'start' button for starting mods on Andoird --- .../button/menubuttons/template_full_idle.png | Bin 0 -> 18127 bytes game/src/mod_menu.rpy | 53 +++++++++++++----- 2 files changed, 40 insertions(+), 13 deletions(-) create mode 100644 game/gui/button/menubuttons/template_full_idle.png diff --git a/game/gui/button/menubuttons/template_full_idle.png b/game/gui/button/menubuttons/template_full_idle.png new file mode 100644 index 0000000000000000000000000000000000000000..6c1ecde05c99af986f7ca86cdad0401710d403a4 GIT binary patch literal 18127 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 literal 0 HcmV?d00001 diff --git a/game/src/mod_menu.rpy b/game/src/mod_menu.rpy index f86a490..286cc5e 100644 --- a/game/src/mod_menu.rpy +++ b/game/src/mod_menu.rpy @@ -696,11 +696,13 @@ screen mod_menu(): # The top 2 buttons hbox: xpos 1272 - ypos 50 + ypos 30 spacing 8 use mod_menu_top_buttons(_("Reload Mods"), SetScreenVariable("reload_game", True)): + # For some reason, Function() will instantly reload the game upon entering the mod menu, and put it in an infinite loop, so doing this python jank + # is the only way if reload_game: python: reload_game = False @@ -709,9 +711,9 @@ screen mod_menu(): viewport: xpos 1260 - ypos 200 - xmaximum 600 - ymaximum 869 + ypos 180 + xmaximum 637 + ymaximum 889 scrollbars "vertical" vscrollbar_unscrollable "hide" @@ -805,7 +807,8 @@ screen mod_menu(): activate_sound "audio/ui/snd_ui_click.wav" hovered SetScreenVariable("mod_metadata", x) - if mod_button_enabled: + # Clicking the mod button starts the mod on PC, but we have to click a seperate button to start on Android. + if mod_button_enabled and not renpy.variant(["mobile", "steam_deck"]): action Start(x["Label"]) else: action NullAction() @@ -875,11 +878,37 @@ screen mod_menu(): # Displays the mod metadata on the left side # This has two seperate viewports for error display because renpy is retarded if mod_metadata != {}: + + # Mod play button for android + # I'm too fuckin tired to make this not shit and just put this and the viewport into a vbox, forgive me + $ mod_has_label_android = renpy.variant(["mobile", "steam_deck"]) and mod_metadata.get("Label") != None + if mod_has_label_android: + $ mod_button_alpha = 1.0 if mod_metadata["Enabled"] == True else 0.4 # Fade mod button out if mod is disabled + button: + xpos 13 + ypos 928 + transform: + alpha mod_button_alpha + frame: + xsize 1190 + ysize 129 + + background Frame("gui/button/menubuttons/template_full_idle.png", 12, 12) + text _("Start") xalign 0.5 yalign 0.5 size 50 + + action Start(mod_metadata["Label"]) + activate_sound "audio/ui/snd_ui_click.wav" + + + # Mod details pane viewport: xmaximum 1190 - ymaximum 1030 + if mod_has_label_android: + ymaximum 900 + else: + ymaximum 1050 xpos 15 - ypos 40 + ypos 15 scrollbars "vertical" vscrollbar_unscrollable "hide" mousewheel True @@ -989,9 +1018,9 @@ screen mod_menu(): elif len(mod_menu_errorcodes) != 0: viewport: xmaximum 1190 - ymaximum 920 - xpos 10 - ypos 150 + ymaximum 1050 + xpos 15 + ypos 15 scrollbars "vertical" vscrollbar_unscrollable "hide" mousewheel True @@ -1034,9 +1063,7 @@ screen mod_menu_top_buttons(text, action): background Frame("gui/button/menubuttons/template_idle.png", 12, 12) text text xalign 0.5 yalign 0.5 size 34 - # 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 action - activate_sound "audio/ui/snd_ui_click.wav" + activate_sound "audio/ui/snd_ui_click.wav" transclude \ No newline at end of file From b1719c4b2121c35318c46f8fc941b9189f024630 Mon Sep 17 00:00:00 2001 From: Map Date: Sun, 6 Oct 2024 19:53:53 -0500 Subject: [PATCH 10/11] make the mod toggle button disappear if the mod doesn't have any valid scripts loaded --- game/src/mod_menu.rpy | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/game/src/mod_menu.rpy b/game/src/mod_menu.rpy index 286cc5e..24bb1fb 100644 --- a/game/src/mod_menu.rpy +++ b/game/src/mod_menu.rpy @@ -753,6 +753,7 @@ screen mod_menu(): action Function(swapMods, i, i-1) else: add Null(30,30) at truecenter + # Enablin/disabling mods button button: at truecenter @@ -762,16 +763,18 @@ screen mod_menu(): if not renpy.variant(["mobile", "steam_deck"]): ysize 65 - 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 - if persistent.enabled_mods[i][1]: - idle_foreground Transform("gui/button/menubuttons/check.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#00ff40")) - hover_foreground Transform("gui/button/menubuttons/check.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#ffffff")) - else: - idle_foreground Transform("gui/button/menubuttons/cross.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#db1a1a")) - hover_foreground Transform("gui/button/menubuttons/cross.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#ffffff")) + if x["Scripts"]: + action Function(toggle_persistent_mods, i) + activate_sound "audio/ui/snd_ui_click.wav" + if persistent.enabled_mods[i][1]: + idle_foreground Transform("gui/button/menubuttons/check.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#00ff40")) + hover_foreground Transform("gui/button/menubuttons/check.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#ffffff")) + else: + idle_foreground Transform("gui/button/menubuttons/cross.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#db1a1a")) + hover_foreground Transform("gui/button/menubuttons/cross.png",xalign=0.5,yalign=0.5,matrixcolor=TintMatrix("#ffffff")) + # Move mod down button if i!=len(mod_menu_metadata)-1: From a266c9508ca9e61be0c7fbaccc0d311bd39797d1 Mon Sep 17 00:00:00 2001 From: Map Date: Sun, 6 Oct 2024 20:30:04 -0500 Subject: [PATCH 11/11] Fix NOMODS flag clearing the load order Mods that don't have loadable scripts are forced on --- game/src/mod_menu.rpy | 691 +++++++++++++++++++++--------------------- 1 file changed, 349 insertions(+), 342 deletions(-) diff --git a/game/src/mod_menu.rpy b/game/src/mod_menu.rpy index 24bb1fb..801f5e7 100644 --- a/game/src/mod_menu.rpy +++ b/game/src/mod_menu.rpy @@ -190,380 +190,387 @@ init -999 python: # 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 languages that has an associated metadata language file 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. The "None" key will be the fallback if a name of user's - # current language is not there. - 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 (presumably 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 load_metadata: + for file in loadable_mod_metadata: + mod_data_final = {} + mod_jsonfail_list = [] # List of languages that has an associated metadata language file 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. The "None" key will be the fallback if a name of user's + # current language is not there. + mod_name = {} if mod_in_root_folder: - print("//////////// ERROR IN ROOT FOLDER MOD:") + mod_name["Folder"] = None # 'None' will make it default to 'in root of mods folder' when used in the errorcode conversion. else: - print(f"//////////// ERROR IN MOD '{mod_folder_name}':") - print(" "+str(e)) - print("//////////// END OF ERROR") - mod_exception = True - mod_jsonfail_list.append("None") + mod_name["Folder"] = mod_folder_name - 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 mod_data, into the base of the final mod dict. - move_key_to_dict(mod_data, mod_data_final, "ID") - move_key_to_dict(mod_data, mod_data_final, "Label") - # Then store the rest like any other language, just our default one. - mod_data_final['None'] = mod_data + # Quickly get the names of mods for debugging information, and in the process get raw values from each metadata file that exists. - # 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" # Finds the metadata file. ex: metadata_es.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 }]) + # Make the base metadata file (presumably 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 - 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 + mod_jsonfail_list.append("None") - # Since lang keys will only be added to the mod data dict if their respective metadata successfully loaded, no need to check if they can. - 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] + if not mod_jsonfail_list: + if _preferences.language == None and isinstance(mod_data.get("Name"), str): + mod_name["None"] = mod_data["Name"] - # 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. - # So we gotta make all lists revertable - for x in lang_data.keys(): - if type(lang_data[x]) == python_list: - lang_data[x] = renpy.revertable.RevertableList(lang_data[x]) + # Move these non-language specific pairs out of mod_data, into the base of the final mod dict. + move_key_to_dict(mod_data, mod_data_final, "ID") + move_key_to_dict(mod_data, mod_data_final, "Label") + # Then store the rest like any other language, just our default one. + mod_data_final['None'] = mod_data - # 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 value_isnt_valid_string(lang_data, "Name"): - 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 value_isnt_valid_string(lang_data, "Version"): - 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 value_isnt_valid_string(lang_data, "Authors") and value_isnt_valid_list(lang_data, "Authors"): - 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 value_isnt_valid_string(lang_data, "Links") and value_isnt_valid_list(lang_data, "Links"): - 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 value_isnt_valid_string(lang_data, "Description"): - mod_menu_errorcodes.append([ ModError.Description_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) - lang_data["Description"] = None - - if value_isnt_valid_string(lang_data, "Mobile Description"): - 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 value_isnt_valid_string(lang_data, "Icon Displayable"): - mod_menu_errorcodes.append([ ModError.Icon_Displayable_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) - lang_data["Icon Displayable"] = None - - if value_isnt_valid_string(lang_data, "Thumbnail Displayable"): - 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:] + # 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" # Finds the metadata file. ex: metadata_es.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: - number = trimmed_string + print(f"//////////// ERROR FOR {lang} METADATA IN MOD '{mod_folder_name}':") + print(" "+str(e)) + print("//////////// END OF ERROR") + mod_jsonfail_list.append(lang) - # See if we can extract the number - try: - converted_number = int(number) - except: - continue + # 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 - if not is_valid_metadata_image(this_file, mod_name): - continue + # 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 }]) - 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)) + # + # 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 if they can. + 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. + # So we gotta make all lists revertable + 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 value_isnt_valid_string(lang_data, "Name"): + 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 value_isnt_valid_string(lang_data, "Version"): + 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 value_isnt_valid_string(lang_data, "Authors") and value_isnt_valid_list(lang_data, "Authors"): + 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 value_isnt_valid_string(lang_data, "Links") and value_isnt_valid_list(lang_data, "Links"): + 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 value_isnt_valid_string(lang_data, "Description"): + mod_menu_errorcodes.append([ ModError.Description_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Description"] = None + + if value_isnt_valid_string(lang_data, "Mobile Description"): + 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 value_isnt_valid_string(lang_data, "Icon Displayable"): + mod_menu_errorcodes.append([ ModError.Icon_Displayable_Not_String, { "mod_name": mod_name, "lang_code": lang_key }]) + lang_data["Icon Displayable"] = None + + if value_isnt_valid_string(lang_data, "Thumbnail Displayable"): + 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 - # Don't load the mod if there's mod breaking errors - if mod_exception: - continue + 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 - # We're now gonna clean up the screenshots to be more usable as-is. - # Refine collected screenshots so that translated screenshots use the 'None' 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] + 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 'None' 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"] - 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]) - # 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 + # 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_data_final[lang_key]["Screenshot Displayables"] = None + mod_screenshots = mod_data_final[lang_key]["Screenshots"] - # Store the collected scripts and screenshots - mod_data_final["Scripts"] = mod_scripts + if mod_data_final[lang_key].get("Screenshot Displayables") != None: + mod_displayable_list = mod_screenshots.copy() - # Make our mod loadable - mod_menu_metadata.append(mod_data_final) - mod_name_list.append(mod_name) # This will mirror mod_menu_metadata + 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: + # 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: - if mod["ID"] == saved_mod_id: - mod["Enabled"] = saved_mod_state + 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: + # If this mod doesn't have any loadable scripts, treat it as on (Say, if a mod changed something in the base game and the label points there) + if not mod["Scripts"]: + mod["Enabled"] = True + # Otherwise set mods to the default state + else: + mod["Enabled"] = persistent.newmods_default_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 - mod_menu_metadata = temp_list + # Rewrite enabled_mods to reflect the new mod order, and load all the mods + persistent.enabled_mods.clear() - # 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"] ] ) + 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 + # 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: