From 11c84659cfe8fb7e78f603068102b2d5224cfbc5 Mon Sep 17 00:00:00 2001 From: Mr-Wiseguy Date: Sun, 6 Apr 2025 03:57:50 -0400 Subject: [PATCH] Finish drag and drop mod installation, disable mod refresh button and code mod toggle when game starts --- include/recomp_ui.h | 39 +++++-- lib/N64ModernRuntime | 2 +- src/game/input.cpp | 6 ++ src/ui/core/ui_context.cpp | 9 ++ src/ui/elements/ui_element.cpp | 6 ++ src/ui/ui_config.cpp | 4 +- src/ui/ui_mod_details_panel.cpp | 4 + src/ui/ui_mod_details_panel.h | 1 + src/ui/ui_mod_installer.cpp | 184 +++++++++++++++++++------------- src/ui/ui_mod_installer.h | 17 ++- src/ui/ui_mod_menu.cpp | 55 +++++++++- src/ui/ui_mod_menu.h | 5 +- src/ui/ui_prompt.cpp | 177 ++++++++++++++++++++++-------- src/ui/ui_state.cpp | 109 ++++++++++++++++++- 14 files changed, 474 insertions(+), 144 deletions(-) diff --git a/include/recomp_ui.h b/include/recomp_ui.h index dbcc513..5d30c1d 100644 --- a/include/recomp_ui.h +++ b/include/recomp_ui.h @@ -52,6 +52,7 @@ namespace recompui { bool is_context_capturing_input(); bool is_context_capturing_mouse(); bool is_any_context_shown(); + ContextId try_close_current_context(); ContextId get_launcher_context_id(); ContextId get_config_context_id(); @@ -79,19 +80,35 @@ namespace recompui { }; void init_prompt_context(); - void open_prompt( - const std::string& headerText, - const std::string& contentText, - const std::string& confirmLabelText, - const std::string& cancelLabelText, - std::function confirmCb, - std::function cancelCb, - ButtonVariant _confirmVariant = ButtonVariant::Success, - ButtonVariant _cancelVariant = ButtonVariant::Error, - bool _focusOnCancel = true, - const std::string& _returnElementId = "" + void open_choice_prompt( + const std::string& header_text, + const std::string& content_text, + const std::string& confirm_label_text, + const std::string& cancel_label_text, + std::function confirm_action, + std::function cancel_action, + ButtonVariant confirm_variant = ButtonVariant::Success, + ButtonVariant cancel_variant = ButtonVariant::Error, + bool focus_on_cancel = true, + const std::string& return_element_id = "" ); + void open_info_prompt( + const std::string& header_text, + const std::string& content_text, + const std::string& okay_label_text, + std::function okay_action, + ButtonVariant okay_variant = ButtonVariant::Error, + const std::string& return_element_id = "" + ); + void open_notification( + const std::string& header_text, + const std::string& content_text, + const std::string& return_element_id = "" + ); + void close_prompt(); bool is_prompt_open(); + void update_mod_list(); + void process_game_started(); void apply_color_hack(); void get_window_size(int& width, int& height); diff --git a/lib/N64ModernRuntime b/lib/N64ModernRuntime index ac15308..db1b1a1 160000 --- a/lib/N64ModernRuntime +++ b/lib/N64ModernRuntime @@ -1 +1 @@ -Subproject commit ac153080f5200a08488d6d329812bf53bdab03b2 +Subproject commit db1b1a10822c6f7aa9d8bd4f930e820f11daed3e diff --git a/src/game/input.cpp b/src/game/input.cpp index 13c1bdf..16775e7 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -296,6 +296,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) { void recomp::handle_events() { SDL_Event cur_event; + static bool started = false; static bool exited = false; while (SDL_PollEvent(&cur_event) && !exited) { exited = sdl_event_filter(nullptr, &cur_event); @@ -312,6 +313,11 @@ void recomp::handle_events() { SDL_ShowCursor(cursor_visible ? SDL_ENABLE : SDL_DISABLE); SDL_SetRelativeMouseMode(cursor_locked ? SDL_TRUE : SDL_FALSE); } + + if (!started && ultramodern::is_game_started()) { + started = true; + recompui::process_game_started(); + } } constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A; diff --git a/src/ui/core/ui_context.cpp b/src/ui/core/ui_context.cpp index 397d6d8..d24559d 100644 --- a/src/ui/core/ui_context.cpp +++ b/src/ui/core/ui_context.cpp @@ -344,6 +344,15 @@ void recompui::ContextId::close() { } } +recompui::ContextId recompui::try_close_current_context() { + if (opened_context_id != ContextId::null()) { + ContextId prev_context = opened_context_id; + opened_context_id.close(); + return prev_context; + } + return ContextId::null(); +} + void recompui::ContextId::process_updates() { // Ensure a context is currently opened by this thread. if (opened_context_id == ContextId::null()) { diff --git a/src/ui/elements/ui_element.cpp b/src/ui/elements/ui_element.cpp index 6e67bfa..a6160f7 100644 --- a/src/ui/elements/ui_element.cpp +++ b/src/ui/elements/ui_element.cpp @@ -1,5 +1,6 @@ #include "RmlUi/Core/StringUtilities.h" +#include "recomp_ui.h" #include "ui_element.h" #include "../core/ui_context.h" @@ -134,6 +135,7 @@ void Element::handle_event(const Event& event) { } void Element::ProcessEvent(Rml::Event &event) { + ContextId prev_context = recompui::try_close_current_context(); ContextId context = ContextId::null(); Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument(); if (doc != nullptr) { @@ -198,6 +200,10 @@ void Element::ProcessEvent(Rml::Event &event) { if (context != ContextId::null() && did_open) { context.close(); } + + if (prev_context != ContextId::null()) { + prev_context.open(); + } } void Element::set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value) { diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp index 5d20c7e..4ca933e 100644 --- a/src/ui/ui_config.cpp +++ b/src/ui/ui_config.cpp @@ -165,7 +165,7 @@ void apply_graphics_config(void) { void close_config_menu() { if (ultramodern::renderer::get_graphics_config() != new_options) { - recompui::open_prompt( + recompui::open_choice_prompt( "Graphics options have changed", "Would you like to apply or discard the changes?", "Apply", @@ -192,7 +192,7 @@ void close_config_menu() { } void zelda64::open_quit_game_prompt() { - recompui::open_prompt( + recompui::open_choice_prompt( "Are you sure you want to quit?", "Any progress since your last save will be lost.", "Quit", diff --git a/src/ui/ui_mod_details_panel.cpp b/src/ui/ui_mod_details_panel.cpp index 00fb557..d416f69 100644 --- a/src/ui/ui_mod_details_panel.cpp +++ b/src/ui/ui_mod_details_panel.cpp @@ -75,6 +75,10 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) { ModDetailsPanel::~ModDetailsPanel() { } +void ModDetailsPanel::disable_toggle() { + enable_toggle->set_enabled(false); +} + void ModDetailsPanel::set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled) { cur_details = details; diff --git a/src/ui/ui_mod_details_panel.h b/src/ui/ui_mod_details_panel.h index 0f06f06..281a13f 100644 --- a/src/ui/ui_mod_details_panel.h +++ b/src/ui/ui_mod_details_panel.h @@ -17,6 +17,7 @@ public: void set_mod_details(const recomp::mods::ModDetails& details, const std::string &thumbnail, bool toggle_checked, bool toggle_enabled, bool toggle_label_visible, bool configure_enabled); void set_mod_toggled_callback(std::function callback); void set_mod_configure_pressed_callback(std::function callback); + void disable_toggle(); private: recomp::mods::ModDetails cur_details; Container *thumbnail_container = nullptr; diff --git a/src/ui/ui_mod_installer.cpp b/src/ui/ui_mod_installer.cpp index 5df2d3b..35bbc00 100644 --- a/src/ui/ui_mod_installer.cpp +++ b/src/ui/ui_mod_installer.cpp @@ -34,7 +34,7 @@ namespace recompui { installation.mod_id = manifest.mod_id; installation.display_name = manifest.display_name; installation.mod_version = manifest.version; - installation.mod_files.emplace_back(target_path); + installation.mod_file = target_path; } } else if (file_path.extension() == ".rtz") { @@ -44,36 +44,43 @@ namespace recompui { if (exists) { installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str())); installation.display_name = installation.mod_id; - installation.mod_version = recomp::Version(); - installation.mod_files.emplace_back(target_path); + installation.mod_version = recomp::Version(0, 0, 0); + installation.mod_file = target_path; } } std::error_code ec; if (exists) { - std::filesystem::copy(file_path, target_path, ec); + std::filesystem::copy(file_path, target_write_path, ec); if (ec) { - result.error_messages.emplace_back(std::format("Unable to copy to {}.", target_write_path.string())); + result.error_messages.emplace_back(std::format("Unable to install {} to mod directory.", file_path.filename().string())); return; } } else { - result.error_messages.emplace_back(std::format("Unable to install {} as it does not seem to be a mod.", file_path.string())); + result.error_messages.emplace_back(std::format("{} is not a mod.", file_path.string())); std::filesystem::remove(target_write_path, ec); return; } - for (const std::filesystem::path &path : installation.mod_files) { - if (std::filesystem::exists(path, ec)) { - installation.needs_overwrite_confirmation = true; - break; + if (std::filesystem::exists(installation.mod_file, ec)) { + installation.needs_overwrite_confirmation = true; + } + if (!installation.needs_overwrite_confirmation) { + // This check isn't really needed as additional_files will be empty for a single mod installation, + // but it's good to have in case this logic ever changes. + for (const std::filesystem::path &path : installation.additional_files) { + if (std::filesystem::exists(path, ec)) { + installation.needs_overwrite_confirmation = true; + break; + } } } result.pending_installations.emplace_back(installation); } - void start_package_mod_installation(recomp::mods::ZipModFileHandle &file_handle, std::function progress_callback, ModInstaller::Result &result) { + void start_package_mod_installation(const std::filesystem::path &path, recomp::mods::ZipModFileHandle &file_handle, std::function progress_callback, ModInstaller::Result &result) { std::error_code ec; char filename[1024]; std::filesystem::path mods_directory = recomp::mods::get_mods_directory(); @@ -81,6 +88,7 @@ namespace recompui { mz_uint num_files = mz_zip_reader_get_num_files(file_handle.archive.get()); std::list dynamic_lib_files; std::list::iterator first_nrm_iterator = result.pending_installations.end(); + bool found_mod = false; for (mz_uint i = 0; i < num_files; i++) { mz_uint filename_length = mz_zip_reader_get_filename(zip_archive, i, filename, sizeof(filename)); if (filename_length == 0) { @@ -89,40 +97,42 @@ namespace recompui { std::filesystem::path target_path = mods_directory / std::u8string_view((const char8_t *)(filename)); if ((target_path.extension() == ".rtz") || (target_path.extension() == ".nrm")) { + found_mod = true; ModInstaller::Installation installation; std::filesystem::path target_write_path = target_path.u8string() + NewExtension; std::ofstream output_stream(target_write_path, std::ios::binary); if (!output_stream.is_open()) { - result.error_messages.emplace_back(std::format("Unable to open {} for writing.", target_write_path.string())); + result.error_messages.emplace_back(std::format("Unable to write to mod directory.")); continue; } if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) { output_stream.close(); std::filesystem::remove(target_write_path, ec); - result.error_messages.emplace_back(std::format("Unable to extract to {}.", target_write_path.string())); + result.error_messages.emplace_back(std::format("Failed to install {} to mod directory.", path.filename().string())); continue; } output_stream.close(); if (output_stream.bad()) { std::filesystem::remove(target_write_path, ec); - result.error_messages.emplace_back(std::format("Unable to write to {}.", target_write_path.string())); + result.error_messages.emplace_back(std::format("Failed to install {} to mod directory.", path.filename().string())); continue; } // Try to load the extracted file as a mod file handle. recomp::mods::ModOpenError open_error; - recomp::mods::ZipModFileHandle extracted_file_handle(target_write_path, open_error); + std::unique_ptr extracted_file_handle = std::make_unique(target_write_path, open_error); if (open_error != recomp::mods::ModOpenError::Good) { - result.error_messages.emplace_back(std::format("Unable to open {}.", target_write_path.string())); + result.error_messages.emplace_back(std::format("Invalid mod ({}) in {}.", target_path.filename().string(), path.filename().string())); + extracted_file_handle.reset(); std::filesystem::remove(target_write_path, ec); continue; } // Check for the existence of the manifest file. bool exists = false; - std::vector manifest_bytes = extracted_file_handle.read_file(ManifestFilename, exists); + std::vector manifest_bytes = extracted_file_handle->read_file(ManifestFilename, exists); if (exists) { // Parse the manifest file to check for its validity. std::string error; @@ -134,31 +144,39 @@ namespace recompui { installation.mod_id = manifest.mod_id; installation.display_name = manifest.display_name; installation.mod_version = manifest.version; - installation.mod_files.emplace_back(target_path); + installation.mod_file = target_path; } } else if (target_path.extension() == ".rtz") { // When it's an rtz file, check if the texture database file exists. - exists = mz_zip_reader_locate_file(extracted_file_handle.archive.get(), TextureDatabaseFilename, nullptr, 0) >= 0; + exists = mz_zip_reader_locate_file(extracted_file_handle->archive.get(), TextureDatabaseFilename, nullptr, 0) >= 0; if (exists) { installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str())); installation.display_name = installation.mod_id; installation.mod_version = recomp::Version(); - installation.mod_files.emplace_back(target_path); + installation.mod_file = target_path; } } if (!exists) { - result.error_messages.emplace_back(std::format("Unable to install {} as it does not seem to be a mod.", target_path.filename().string())); + result.error_messages.emplace_back(std::format("Invalid mod ({}) in {}.", target_path.filename().string(), path.filename().string())); + extracted_file_handle.reset(); std::filesystem::remove(target_write_path, ec); continue; } - for (const std::filesystem::path &path : installation.mod_files) { - if (std::filesystem::exists(path, ec)) { - installation.needs_overwrite_confirmation = true; - break; + if (std::filesystem::exists(installation.mod_file, ec)) { + installation.needs_overwrite_confirmation = true; + } + if (!installation.needs_overwrite_confirmation) { + // This check isn't really needed as additional_files will be empty at this point, + // but it's good to have in case this logic ever changes. + for (const std::filesystem::path &path : installation.additional_files) { + if (std::filesystem::exists(path, ec)) { + installation.needs_overwrite_confirmation = true; + break; + } } } @@ -181,21 +199,21 @@ namespace recompui { std::filesystem::path target_write_path = target_path.u8string() + NewExtension; std::ofstream output_stream(target_write_path, std::ios::binary); if (!output_stream.is_open()) { - result.error_messages.emplace_back(std::format("Unable to open {} for writing.", target_write_path.string())); + result.error_messages.emplace_back(std::format("Failed to install {} to mod directory.", path.filename().string())); continue; } if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) { output_stream.close(); std::filesystem::remove(target_write_path, ec); - result.error_messages.emplace_back(std::format("Unable to extract to {}.", target_write_path.string())); + result.error_messages.emplace_back(std::format("Failed to install {} to mod directory.", path.filename().string())); continue; } output_stream.close(); if (output_stream.bad()) { std::filesystem::remove(target_write_path, ec); - result.error_messages.emplace_back(std::format("Unable to write to {}.", target_write_path.string())); + result.error_messages.emplace_back(std::format("Failed to install {} to mod directory.", path.filename().string())); continue; } @@ -207,14 +225,11 @@ namespace recompui { if (first_nrm_iterator != result.pending_installations.end()) { // Associate all these files to the first mod that is found. for (const std::filesystem::path &path : dynamic_lib_files) { - first_nrm_iterator->mod_files.emplace_back(path); + first_nrm_iterator->additional_files.emplace_back(path); // Run verification against for overwrite confirmations. - for (const std::filesystem::path &path : first_nrm_iterator->mod_files) { - if (std::filesystem::exists(path, ec)) { - first_nrm_iterator->needs_overwrite_confirmation = true; - break; - } + if (std::filesystem::exists(path, ec)) { + first_nrm_iterator->needs_overwrite_confirmation = true; } } } @@ -226,8 +241,43 @@ namespace recompui { } } } + + if (!found_mod) { + result.error_messages.emplace_back(std::format("No mods found in {}.", path.filename().string())); + } } + void remove_and_rename(std::vector& error_messages, const std::filesystem::path& path) { + std::error_code ec; + std::filesystem::path old_path(path.u8string() + OldExtension); + std::filesystem::path new_path(path.u8string() + NewExtension); + + // Rename the current path to a temporary old path, but only if the current path already exists. + if (std::filesystem::exists(path, ec)) { + std::filesystem::remove(old_path, ec); + std::filesystem::rename(path, old_path, ec); + if (ec) { + // If it fails, remove the new path. + std::filesystem::remove(new_path, ec); + error_messages.emplace_back(std::format("Unable to rename {}.", path.filename().string())); + return; + } + } + + // Rename the new path to the current path. + std::filesystem::rename(new_path, path, ec); + if (ec) { + // If it fails, remove the new path and also restore the temporary old path to the current path. + std::filesystem::remove(new_path, ec); + std::filesystem::rename(old_path, path, ec); + error_messages.emplace_back(std::format("Unable to rename {}.", path.filename().string())); + return; + } + + // If nothing failed, just remove the temporary old path. + std::filesystem::remove(old_path, ec); + }; + void ModInstaller::start_mod_installation(const std::list &file_paths, std::function progress_callback, Result &result) { result = Result(); @@ -235,64 +285,46 @@ namespace recompui { recomp::mods::ModOpenError open_error; recomp::mods::ZipModFileHandle file_handle(path, open_error); if (open_error != recomp::mods::ModOpenError::Good) { - result.error_messages = { std::format("File %s is not a valid container.", path.string()) }; + result.error_messages.emplace_back(std::format("{} is not a valid zip or mod.", path.filename().string())); continue; } // First we verify if the container itself isn't a mod already. + // TODO hook into the runtime's container registration to check the extension instead of using hardcoded values. if ((path.extension() == ".rtz") || (path.extension() == ".nrm")) { start_single_mod_installation(path, file_handle, progress_callback, result); } else { // Scan the container for compatible mods instead. This is the case for packages made by users or how they're tipically uploaded to Thunderstore. - start_package_mod_installation(file_handle, progress_callback, result); + start_package_mod_installation(path, file_handle, progress_callback, result); } } } - void ModInstaller::finish_mod_installation(const std::unordered_set &confirmed_overwrites, Result &result) { - result.error_messages.clear(); + void ModInstaller::cancel_mod_installation(const Result &result, std::vector& error_messages) { + error_messages.clear(); + + std::error_code ec; + // Delete all the files that were extracted for all mods. + for (const Installation &installation : result.pending_installations) { + std::filesystem::path new_path(installation.mod_file.u8string() + NewExtension); + std::filesystem::remove(new_path, ec); + for (const std::filesystem::path &path : installation.additional_files) { + std::filesystem::path new_path(path.u8string() + NewExtension); + std::filesystem::remove(new_path, ec); + } + } + } + + void ModInstaller::finish_mod_installation(const Result &result, std::vector& error_messages) { + error_messages.clear(); std::error_code ec; for (const Installation &installation : result.pending_installations) { - if (installation.needs_overwrite_confirmation && !confirmed_overwrites.contains(installation.mod_id)) { - // If the user hasn't confirmed this overwrite, simply delete all the files that were extracted for this mod. - for (const std::filesystem::path &path : installation.mod_files) { - std::filesystem::path new_path(path.u8string() + NewExtension); - std::filesystem::remove(new_path, ec); - } - - continue; - } - - for (const std::filesystem::path &path : installation.mod_files) { - std::filesystem::path old_path(path.u8string() + OldExtension); - std::filesystem::path new_path(path.u8string() + NewExtension); - - // Rename the current path to a temporary old path, but only if the current path already exists. - if (std::filesystem::exists(path, ec)) { - std::filesystem::remove(old_path, ec); - std::filesystem::rename(path, old_path, ec); - if (ec) { - // If it fails, remove the new path. - std::filesystem::remove(new_path, ec); - result.error_messages.emplace_back(std::format("Unable to rename {}.", path.string())); - continue; - } - } - - // Rename the new path to the current path. - std::filesystem::rename(new_path, path, ec); - if (ec) { - // If it fails, remove the new path and also restore the temporary old path to the current path. - std::filesystem::remove(new_path, ec); - std::filesystem::rename(old_path, path, ec); - result.error_messages.emplace_back(std::format("Unable to rename {}.", path.string())); - continue; - } - - // If nothing failed, just remove the temporary old path. - std::filesystem::remove(old_path, ec); + // Overwrite the mod files. + remove_and_rename(error_messages, installation.mod_file); + for (const std::filesystem::path &path : installation.additional_files) { + remove_and_rename(error_messages, path); } } } diff --git a/src/ui/ui_mod_installer.h b/src/ui/ui_mod_installer.h index 7a9df6e..fb08ed0 100644 --- a/src/ui/ui_mod_installer.h +++ b/src/ui/ui_mod_installer.h @@ -4,6 +4,8 @@ #include #include +#include +#include namespace recompui { struct ModInstaller { @@ -11,17 +13,28 @@ namespace recompui { std::string mod_id; std::string display_name; recomp::Version mod_version; - std::list mod_files; + std::filesystem::path mod_file; + std::list additional_files; bool needs_overwrite_confirmation = false; }; + struct Confirmation { + std::string old_display_name; + std::string new_display_name; + std::string old_mod_id; + std::string new_mod_id; + recomp::Version old_version; + recomp::Version new_version; + }; + struct Result { std::list error_messages; std::list pending_installations; }; static void start_mod_installation(const std::list &file_paths, std::function progress_callback, Result &result); - static void finish_mod_installation(const std::unordered_set &confirmed_overwrites, Result &result); + static void cancel_mod_installation(const Result& result, std::vector& errors); + static void finish_mod_installation(const Result &result, std::vector& errors); }; }; diff --git a/src/ui/ui_mod_menu.cpp b/src/ui/ui_mod_menu.cpp index 206c5de..9be9a53 100644 --- a/src/ui/ui_mod_menu.cpp +++ b/src/ui/ui_mod_menu.cpp @@ -212,7 +212,7 @@ void ModMenu::refresh_mods() { } recomp::mods::scan_mods(); - mod_details = recomp::mods::get_mod_details(game_mod_id); + mod_details = recomp::mods::get_all_mod_details(game_mod_id); create_mod_list(); } @@ -336,7 +336,7 @@ void ModMenu::mod_dragged(uint32_t mod_index, EventDrag drag) { // Re-order the mods and update all the details on the menu. recomp::mods::set_mod_index(game_mod_id, mod_details[mod_index].mod_id, mod_drag_target_index); - mod_details = recomp::mods::get_mod_details(game_mod_id); + mod_details = recomp::mods::get_all_mod_details(game_mod_id); for (size_t i = 0; i < mod_entry_buttons.size(); i++) { mod_entry_buttons[i]->set_mod_details(mod_details[i]); mod_entry_buttons[i]->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[i].mod_id)); @@ -471,6 +471,25 @@ void ModMenu::create_mod_list() { } } +void ModMenu::process_event(const Event &e) { + if (e.type == EventType::Update) { + if (mods_dirty) { + refresh_mods(); + mods_dirty = false; + } + if (ultramodern::is_game_started()) { + refresh_button->set_enabled(false); + } + if (active_mod_index != -1) { + bool auto_enabled = recomp::mods::is_mod_auto_enabled(mod_details[active_mod_index].mod_id); + bool toggle_enabled = !auto_enabled && (mod_details[active_mod_index].runtime_toggleable || !ultramodern::is_game_started()); + if (!toggle_enabled) { + mod_details_panel->disable_toggle(); + } + } + } +} + ModMenu::ModMenu(Element *parent) : Element(parent) { game_mod_id = "mm"; @@ -526,7 +545,7 @@ ModMenu::ModMenu(Element *parent) : Element(parent) { footer_container->set_border_bottom_right_radius(16.0f); { refresh_button = context.create_element