diff --git a/game/gui/button/menubuttons/check.png b/game/gui/button/menubuttons/check.png new file mode 100644 index 0000000..fad09ab Binary files /dev/null and b/game/gui/button/menubuttons/check.png differ diff --git a/game/gui/button/menubuttons/checkbox.png b/game/gui/button/menubuttons/checkbox.png new file mode 100644 index 0000000..962a8c4 Binary files /dev/null and b/game/gui/button/menubuttons/checkbox.png differ diff --git a/game/gui/button/menubuttons/cross.png b/game/gui/button/menubuttons/cross.png new file mode 100644 index 0000000..49b72b2 Binary files /dev/null and b/game/gui/button/menubuttons/cross.png differ diff --git a/game/gui/button/menubuttons/down.png b/game/gui/button/menubuttons/down.png new file mode 100644 index 0000000..b921b86 Binary files /dev/null and b/game/gui/button/menubuttons/down.png differ diff --git a/game/gui/button/menubuttons/up.png b/game/gui/button/menubuttons/up.png new file mode 100644 index 0000000..cd47432 Binary files /dev/null and b/game/gui/button/menubuttons/up.png differ diff --git a/game/src/mod_menu.rpy b/game/src/mod_menu.rpy index ee88be9..bbf3dc3 100644 --- a/game/src/mod_menu.rpy +++ b/game/src/mod_menu.rpy @@ -121,8 +121,13 @@ init -999 python: mod_menu_moddir = android_mods_path + + # + # Helper functions + # + # 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): + def is_valid_metadata_image(filename, mod_name): error_trigger = True for ext in valid_image_filetypes: @@ -134,6 +139,23 @@ init -999 python: return True + # Moves a key from one list to another, deleting the key from the source list + def move_key_to_dict(source_dict, destination_dict, key): + if key in source_dict.keys(): + destination_dict[key] = source_dict[key] + del source_dict[key] + + # Checks to see if the key in the metadata dict exists/is correct, and tries to set a default if it isn't. + # For keys that are set to None, it will be treated as if it doesn't exist + def value_isnt_valid_string(metadata, key): + return metadata.get(key) != None and not isinstance(metadata.get(key), str) + + def value_isnt_valid_list(metadata, key): + return metadata.get(key) != None and not isinstance(metadata.get(key), list) + + + + # 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 @@ -170,7 +192,7 @@ init -999 python: 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_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 @@ -178,7 +200,8 @@ init -999 python: # 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. + # 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. @@ -188,7 +211,7 @@ init -999 python: # 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. + # 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: @@ -205,19 +228,15 @@ init -999 python: 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"] + # 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 # 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" + 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))) @@ -241,7 +260,7 @@ init -999 python: mod_menu_errorcodes.append([ ModError.Metadata_Fail, { "mod_name": mod_name, "lang_code": lang_code }]) # - # Sanitize/Clean metadata values + # Sanitize/Clean metadata values # # Make sure our main metadata loaded @@ -266,18 +285,19 @@ init -999 python: 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. + # 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 lang_data.get("Name") != None and not isinstance(lang_data.get("Name"), str): + 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 }]) @@ -293,12 +313,12 @@ init -999 python: 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): + 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 lang_data.get("Authors") != None and (not isinstance(lang_data.get("Authors"), str) and not isinstance(lang_data.get("Authors"), list)): + 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): @@ -313,7 +333,7 @@ init -999 python: 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)): + 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): @@ -325,11 +345,11 @@ init -999 python: if lang_data["Links"] == []: lang_data["Links"] = None - if lang_data.get("Description") != None and not isinstance(lang_data.get("Description"), str): + 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 lang_data.get("Mobile Description") != None and not isinstance(lang_data.get("Mobile Description"), str): + 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 @@ -348,11 +368,11 @@ init -999 python: 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): + 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 lang_data.get("Thumbnail Displayable") != None and not isinstance(lang_data.get("Thumbnail Displayable"), str): + 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 @@ -448,7 +468,7 @@ init -999 python: # 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) + # 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": @@ -626,10 +646,6 @@ init python: ## 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): @@ -658,14 +674,15 @@ init python: 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 @@ -675,28 +692,24 @@ screen mod_menu(): 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 + # The top 2 buttons + hbox: + xpos 1272 + ypos 50 - if reload_game: - python: - reload_game = False - renpy.reload_script() + spacing 8 + use mod_menu_top_buttons(_("Reload Mods"), SetScreenVariable("reload_game", True)): + if reload_game: + python: + reload_game = False + renpy.reload_script() + use mod_menu_top_buttons(_("Return"), ShowMenu("extras")) - viewport at tf_modmenu_slide: + viewport: xpos 1338 - ypos 279 + ypos 179 xmaximum 540 ymaximum 790 @@ -791,10 +804,10 @@ screen mod_menu(): 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)) + 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/title_button.png", 12, 12),matrixcolor=SaturationMatrix(0.5)) + background Transform(Frame("gui/button/menubuttons/template_idle.png", 12, 12),matrixcolor=SaturationMatrix(0.5)) padding (5, 5) @@ -828,29 +841,21 @@ screen mod_menu(): frame: xsize 350 ymaximum 2000 - background Frame("gui/button/menubuttons/title_button.png", 12, 12) + background Frame("gui/button/menubuttons/template_idle.png", 12, 12) padding (5, 5) - hover_background Transform(Frame("gui/button/menubuttons/title_button.png", 12, 12), matrixcolor = BrightnessMatrix(0.1)) + 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: 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))] + 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 @@ -885,7 +890,7 @@ screen mod_menu(): add mod_thumbnail fit 'scale-down' # Mod details - # Omit checking for mod name, since we'll always have some kind of mod name. + # Omits 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: @@ -987,8 +992,6 @@ screen mod_menu(): 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) @@ -1005,4 +1008,20 @@ screen mod_screenshot_preview(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 + key ["mouseup_1", "mouseup_3"] action Hide("mod_screenshot_preview", dissolve) + +screen mod_menu_top_buttons(text, action): + button: + frame: + xmaximum 300 + ymaximum 129 + + 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" + + transclude \ No newline at end of file diff --git a/game/src/rounded_corners.rpy b/game/src/rounded_corners.rpy new file mode 100644 index 0000000..37d5a1a --- /dev/null +++ b/game/src/rounded_corners.rpy @@ -0,0 +1,91 @@ +# RoundedCorners() rounds the corners of a displayable you give it + +python early: + import collections, pygame_sdl2 as pygame + + def normalize_color(col): + a = col[3] / 255.0 + r = a * col[0] / 255.0 + g = a * col[1] / 255.0 + b = a * col[2] / 255.0 + return (r, g, b, a) + + _rounded_corners_relative = { + None: 0.0, + "min": 1.0, + "max": 2.0, + "width": 3.0, + "height": 4.0, + } + + def RoundedCorners(child, radius, relative=None, outline_width=0.0, outline_color="#fff", **kwargs): + if not isinstance(radius, tuple): radius = (radius,) * 4 + relative = _rounded_corners_relative[relative] + outline_color = normalize_color(Color(outline_color)) + return Transform(child, mesh=True, shader="shader.rounded_corners", u_radius=radius, u_relative=relative, u_outline_color=outline_color, u_outline_width=outline_width, **kwargs) + + CurriedRoundedCorners = renpy.curry(RoundedCorners) + + renpy.register_shader("shader.rounded_corners", variables=""" + uniform vec4 u_radius; + uniform float u_outline_width; + uniform vec4 u_outline_color; + uniform float u_relative; + uniform sampler2D tex0; + attribute vec2 a_tex_coord; + varying vec2 v_tex_coord; + uniform vec2 u_model_size; + """, vertex_200=""" + v_tex_coord = a_tex_coord; + """, fragment_functions=""" + float rounded_rectangle(vec2 p, vec2 b, float r) { + return length(max(abs(p) - b + r, 0.0)) - r; + } + + float get_radius(vec2 uv_minus_center, vec4 radius) { + vec2 xy = (uv_minus_center.x > 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