Files
IWaniHugThatGator-Demo-Public/game/00src/mod_menu.rpy
2024-04-21 13:45:17 -05:00

1008 lines
54 KiB
Plaintext

# 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))
# 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 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
# Don't load the mod if there's mod breaking errors
if mod_exception:
continue
# 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
##
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
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
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"
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 790
scrollbars "vertical"
vscrollbar_unscrollable "hide"
mousewheel True
draggable True
pagekeys True
if len(mod_menu_metadata) != 0 or len(mod_menu_access) != 0:
vbox:
at truecenter
for i, x in enumerate(mod_menu_metadata):
hbox:
xsize 129
ysize 129
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)
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"))
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
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"))
# 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"
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"))
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 'contain'
# 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, "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(x, "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)