Finish drag and drop mod installation, disable mod refresh button and code mod toggle when game starts

This commit is contained in:
Mr-Wiseguy 2025-04-06 03:57:50 -04:00
parent 75c3669961
commit 11c84659cf
14 changed files with 474 additions and 144 deletions

View file

@ -52,6 +52,7 @@ namespace recompui {
bool is_context_capturing_input(); bool is_context_capturing_input();
bool is_context_capturing_mouse(); bool is_context_capturing_mouse();
bool is_any_context_shown(); bool is_any_context_shown();
ContextId try_close_current_context();
ContextId get_launcher_context_id(); ContextId get_launcher_context_id();
ContextId get_config_context_id(); ContextId get_config_context_id();
@ -79,19 +80,35 @@ namespace recompui {
}; };
void init_prompt_context(); void init_prompt_context();
void open_prompt( void open_choice_prompt(
const std::string& headerText, const std::string& header_text,
const std::string& contentText, const std::string& content_text,
const std::string& confirmLabelText, const std::string& confirm_label_text,
const std::string& cancelLabelText, const std::string& cancel_label_text,
std::function<void()> confirmCb, std::function<void()> confirm_action,
std::function<void()> cancelCb, std::function<void()> cancel_action,
ButtonVariant _confirmVariant = ButtonVariant::Success, ButtonVariant confirm_variant = ButtonVariant::Success,
ButtonVariant _cancelVariant = ButtonVariant::Error, ButtonVariant cancel_variant = ButtonVariant::Error,
bool _focusOnCancel = true, bool focus_on_cancel = true,
const std::string& _returnElementId = "" 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<void()> 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(); bool is_prompt_open();
void update_mod_list();
void process_game_started();
void apply_color_hack(); void apply_color_hack();
void get_window_size(int& width, int& height); void get_window_size(int& width, int& height);

@ -1 +1 @@
Subproject commit ac153080f5200a08488d6d329812bf53bdab03b2 Subproject commit db1b1a10822c6f7aa9d8bd4f930e820f11daed3e

View file

@ -296,6 +296,7 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
void recomp::handle_events() { void recomp::handle_events() {
SDL_Event cur_event; SDL_Event cur_event;
static bool started = false;
static bool exited = false; static bool exited = false;
while (SDL_PollEvent(&cur_event) && !exited) { while (SDL_PollEvent(&cur_event) && !exited) {
exited = sdl_event_filter(nullptr, &cur_event); exited = sdl_event_filter(nullptr, &cur_event);
@ -312,6 +313,11 @@ void recomp::handle_events() {
SDL_ShowCursor(cursor_visible ? SDL_ENABLE : SDL_DISABLE); SDL_ShowCursor(cursor_visible ? SDL_ENABLE : SDL_DISABLE);
SDL_SetRelativeMouseMode(cursor_locked ? SDL_TRUE : SDL_FALSE); 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; constexpr SDL_GameControllerButton SDL_CONTROLLER_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A;

View file

@ -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() { void recompui::ContextId::process_updates() {
// Ensure a context is currently opened by this thread. // Ensure a context is currently opened by this thread.
if (opened_context_id == ContextId::null()) { if (opened_context_id == ContextId::null()) {

View file

@ -1,5 +1,6 @@
#include "RmlUi/Core/StringUtilities.h" #include "RmlUi/Core/StringUtilities.h"
#include "recomp_ui.h"
#include "ui_element.h" #include "ui_element.h"
#include "../core/ui_context.h" #include "../core/ui_context.h"
@ -134,6 +135,7 @@ void Element::handle_event(const Event& event) {
} }
void Element::ProcessEvent(Rml::Event &event) { void Element::ProcessEvent(Rml::Event &event) {
ContextId prev_context = recompui::try_close_current_context();
ContextId context = ContextId::null(); ContextId context = ContextId::null();
Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument(); Rml::ElementDocument* doc = event.GetTargetElement()->GetOwnerDocument();
if (doc != nullptr) { if (doc != nullptr) {
@ -198,6 +200,10 @@ void Element::ProcessEvent(Rml::Event &event) {
if (context != ContextId::null() && did_open) { if (context != ContextId::null() && did_open) {
context.close(); context.close();
} }
if (prev_context != ContextId::null()) {
prev_context.open();
}
} }
void Element::set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value) { void Element::set_attribute(const Rml::String &attribute_key, const Rml::String &attribute_value) {

View file

@ -165,7 +165,7 @@ void apply_graphics_config(void) {
void close_config_menu() { void close_config_menu() {
if (ultramodern::renderer::get_graphics_config() != new_options) { if (ultramodern::renderer::get_graphics_config() != new_options) {
recompui::open_prompt( recompui::open_choice_prompt(
"Graphics options have changed", "Graphics options have changed",
"Would you like to apply or discard the changes?", "Would you like to apply or discard the changes?",
"Apply", "Apply",
@ -192,7 +192,7 @@ void close_config_menu() {
} }
void zelda64::open_quit_game_prompt() { void zelda64::open_quit_game_prompt() {
recompui::open_prompt( recompui::open_choice_prompt(
"Are you sure you want to quit?", "Are you sure you want to quit?",
"Any progress since your last save will be lost.", "Any progress since your last save will be lost.",
"Quit", "Quit",

View file

@ -75,6 +75,10 @@ ModDetailsPanel::ModDetailsPanel(Element *parent) : Element(parent) {
ModDetailsPanel::~ModDetailsPanel() { 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) { 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; cur_details = details;

View file

@ -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_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<void(bool)> callback); void set_mod_toggled_callback(std::function<void(bool)> callback);
void set_mod_configure_pressed_callback(std::function<void()> callback); void set_mod_configure_pressed_callback(std::function<void()> callback);
void disable_toggle();
private: private:
recomp::mods::ModDetails cur_details; recomp::mods::ModDetails cur_details;
Container *thumbnail_container = nullptr; Container *thumbnail_container = nullptr;

View file

@ -34,7 +34,7 @@ namespace recompui {
installation.mod_id = manifest.mod_id; installation.mod_id = manifest.mod_id;
installation.display_name = manifest.display_name; installation.display_name = manifest.display_name;
installation.mod_version = manifest.version; installation.mod_version = manifest.version;
installation.mod_files.emplace_back(target_path); installation.mod_file = target_path;
} }
} }
else if (file_path.extension() == ".rtz") { else if (file_path.extension() == ".rtz") {
@ -44,36 +44,43 @@ namespace recompui {
if (exists) { if (exists) {
installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str())); installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str()));
installation.display_name = installation.mod_id; installation.display_name = installation.mod_id;
installation.mod_version = recomp::Version(); installation.mod_version = recomp::Version(0, 0, 0);
installation.mod_files.emplace_back(target_path); installation.mod_file = target_path;
} }
} }
std::error_code ec; std::error_code ec;
if (exists) { if (exists) {
std::filesystem::copy(file_path, target_path, ec); std::filesystem::copy(file_path, target_write_path, ec);
if (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; return;
} }
} }
else { 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); std::filesystem::remove(target_write_path, ec);
return; return;
} }
for (const std::filesystem::path &path : installation.mod_files) { if (std::filesystem::exists(installation.mod_file, ec)) {
if (std::filesystem::exists(path, ec)) { installation.needs_overwrite_confirmation = true;
installation.needs_overwrite_confirmation = true; }
break; 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); result.pending_installations.emplace_back(installation);
} }
void start_package_mod_installation(recomp::mods::ZipModFileHandle &file_handle, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, ModInstaller::Result &result) { void start_package_mod_installation(const std::filesystem::path &path, recomp::mods::ZipModFileHandle &file_handle, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, ModInstaller::Result &result) {
std::error_code ec; std::error_code ec;
char filename[1024]; char filename[1024];
std::filesystem::path mods_directory = recomp::mods::get_mods_directory(); 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()); mz_uint num_files = mz_zip_reader_get_num_files(file_handle.archive.get());
std::list<std::filesystem::path> dynamic_lib_files; std::list<std::filesystem::path> dynamic_lib_files;
std::list<ModInstaller::Installation>::iterator first_nrm_iterator = result.pending_installations.end(); std::list<ModInstaller::Installation>::iterator first_nrm_iterator = result.pending_installations.end();
bool found_mod = false;
for (mz_uint i = 0; i < num_files; i++) { for (mz_uint i = 0; i < num_files; i++) {
mz_uint filename_length = mz_zip_reader_get_filename(zip_archive, i, filename, sizeof(filename)); mz_uint filename_length = mz_zip_reader_get_filename(zip_archive, i, filename, sizeof(filename));
if (filename_length == 0) { if (filename_length == 0) {
@ -89,40 +97,42 @@ namespace recompui {
std::filesystem::path target_path = mods_directory / std::u8string_view((const char8_t *)(filename)); std::filesystem::path target_path = mods_directory / std::u8string_view((const char8_t *)(filename));
if ((target_path.extension() == ".rtz") || (target_path.extension() == ".nrm")) { if ((target_path.extension() == ".rtz") || (target_path.extension() == ".nrm")) {
found_mod = true;
ModInstaller::Installation installation; ModInstaller::Installation installation;
std::filesystem::path target_write_path = target_path.u8string() + NewExtension; std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
std::ofstream output_stream(target_write_path, std::ios::binary); std::ofstream output_stream(target_write_path, std::ios::binary);
if (!output_stream.is_open()) { 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; continue;
} }
if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) { if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) {
output_stream.close(); output_stream.close();
std::filesystem::remove(target_write_path, ec); 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; continue;
} }
output_stream.close(); output_stream.close();
if (output_stream.bad()) { if (output_stream.bad()) {
std::filesystem::remove(target_write_path, ec); 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; continue;
} }
// Try to load the extracted file as a mod file handle. // Try to load the extracted file as a mod file handle.
recomp::mods::ModOpenError open_error; recomp::mods::ModOpenError open_error;
recomp::mods::ZipModFileHandle extracted_file_handle(target_write_path, open_error); std::unique_ptr<recomp::mods::ZipModFileHandle> extracted_file_handle = std::make_unique<recomp::mods::ZipModFileHandle>(target_write_path, open_error);
if (open_error != recomp::mods::ModOpenError::Good) { 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); std::filesystem::remove(target_write_path, ec);
continue; continue;
} }
// Check for the existence of the manifest file. // Check for the existence of the manifest file.
bool exists = false; bool exists = false;
std::vector<char> manifest_bytes = extracted_file_handle.read_file(ManifestFilename, exists); std::vector<char> manifest_bytes = extracted_file_handle->read_file(ManifestFilename, exists);
if (exists) { if (exists) {
// Parse the manifest file to check for its validity. // Parse the manifest file to check for its validity.
std::string error; std::string error;
@ -134,31 +144,39 @@ namespace recompui {
installation.mod_id = manifest.mod_id; installation.mod_id = manifest.mod_id;
installation.display_name = manifest.display_name; installation.display_name = manifest.display_name;
installation.mod_version = manifest.version; installation.mod_version = manifest.version;
installation.mod_files.emplace_back(target_path); installation.mod_file = target_path;
} }
} }
else if (target_path.extension() == ".rtz") { else if (target_path.extension() == ".rtz") {
// When it's an rtz file, check if the texture database file exists. // 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) { if (exists) {
installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str())); installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str()));
installation.display_name = installation.mod_id; installation.display_name = installation.mod_id;
installation.mod_version = recomp::Version(); installation.mod_version = recomp::Version();
installation.mod_files.emplace_back(target_path); installation.mod_file = target_path;
} }
} }
if (!exists) { 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); std::filesystem::remove(target_write_path, ec);
continue; continue;
} }
for (const std::filesystem::path &path : installation.mod_files) { if (std::filesystem::exists(installation.mod_file, ec)) {
if (std::filesystem::exists(path, ec)) { installation.needs_overwrite_confirmation = true;
installation.needs_overwrite_confirmation = true; }
break; 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::filesystem::path target_write_path = target_path.u8string() + NewExtension;
std::ofstream output_stream(target_write_path, std::ios::binary); std::ofstream output_stream(target_write_path, std::ios::binary);
if (!output_stream.is_open()) { 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; continue;
} }
if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) { if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) {
output_stream.close(); output_stream.close();
std::filesystem::remove(target_write_path, ec); 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; continue;
} }
output_stream.close(); output_stream.close();
if (output_stream.bad()) { if (output_stream.bad()) {
std::filesystem::remove(target_write_path, ec); 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; continue;
} }
@ -207,14 +225,11 @@ namespace recompui {
if (first_nrm_iterator != result.pending_installations.end()) { if (first_nrm_iterator != result.pending_installations.end()) {
// Associate all these files to the first mod that is found. // Associate all these files to the first mod that is found.
for (const std::filesystem::path &path : dynamic_lib_files) { 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. // Run verification against for overwrite confirmations.
for (const std::filesystem::path &path : first_nrm_iterator->mod_files) { if (std::filesystem::exists(path, ec)) {
if (std::filesystem::exists(path, ec)) { first_nrm_iterator->needs_overwrite_confirmation = true;
first_nrm_iterator->needs_overwrite_confirmation = true;
break;
}
} }
} }
} }
@ -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<std::string>& 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<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result) { void ModInstaller::start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result) {
result = Result(); result = Result();
@ -235,64 +285,46 @@ namespace recompui {
recomp::mods::ModOpenError open_error; recomp::mods::ModOpenError open_error;
recomp::mods::ZipModFileHandle file_handle(path, open_error); recomp::mods::ZipModFileHandle file_handle(path, open_error);
if (open_error != recomp::mods::ModOpenError::Good) { 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; continue;
} }
// First we verify if the container itself isn't a mod already. // 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")) { if ((path.extension() == ".rtz") || (path.extension() == ".nrm")) {
start_single_mod_installation(path, file_handle, progress_callback, result); start_single_mod_installation(path, file_handle, progress_callback, result);
} }
else { 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. // 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<std::string> &confirmed_overwrites, Result &result) { void ModInstaller::cancel_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
result.error_messages.clear(); 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<std::string>& error_messages) {
error_messages.clear();
std::error_code ec; std::error_code ec;
for (const Installation &installation : result.pending_installations) { for (const Installation &installation : result.pending_installations) {
if (installation.needs_overwrite_confirmation && !confirmed_overwrites.contains(installation.mod_id)) { // Overwrite the mod files.
// If the user hasn't confirmed this overwrite, simply delete all the files that were extracted for this mod. remove_and_rename(error_messages, installation.mod_file);
for (const std::filesystem::path &path : installation.mod_files) { for (const std::filesystem::path &path : installation.additional_files) {
std::filesystem::path new_path(path.u8string() + NewExtension); remove_and_rename(error_messages, path);
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);
} }
} }
} }

View file

@ -4,6 +4,8 @@
#include <librecomp/game.hpp> #include <librecomp/game.hpp>
#include <unordered_set> #include <unordered_set>
#include <vector>
#include <string>
namespace recompui { namespace recompui {
struct ModInstaller { struct ModInstaller {
@ -11,17 +13,28 @@ namespace recompui {
std::string mod_id; std::string mod_id;
std::string display_name; std::string display_name;
recomp::Version mod_version; recomp::Version mod_version;
std::list<std::filesystem::path> mod_files; std::filesystem::path mod_file;
std::list<std::filesystem::path> additional_files;
bool needs_overwrite_confirmation = false; 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 { struct Result {
std::list<std::string> error_messages; std::list<std::string> error_messages;
std::list<Installation> pending_installations; std::list<Installation> pending_installations;
}; };
static void start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result); static void start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result);
static void finish_mod_installation(const std::unordered_set<std::string> &confirmed_overwrites, Result &result); static void cancel_mod_installation(const Result& result, std::vector<std::string>& errors);
static void finish_mod_installation(const Result &result, std::vector<std::string>& errors);
}; };
}; };

View file

@ -212,7 +212,7 @@ void ModMenu::refresh_mods() {
} }
recomp::mods::scan_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(); 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. // 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); 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++) { 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_details(mod_details[i]);
mod_entry_buttons[i]->set_mod_thumbnail(generate_thumbnail_src_for_mod(mod_details[i].mod_id)); 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) { ModMenu::ModMenu(Element *parent) : Element(parent) {
game_mod_id = "mm"; game_mod_id = "mm";
@ -526,7 +545,7 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
footer_container->set_border_bottom_right_radius(16.0f); footer_container->set_border_bottom_right_radius(16.0f);
{ {
refresh_button = context.create_element<Button>(footer_container, "Refresh", recompui::ButtonStyle::Primary); refresh_button = context.create_element<Button>(footer_container, "Refresh", recompui::ButtonStyle::Primary);
refresh_button->add_pressed_callback(std::bind(&ModMenu::refresh_mods, this)); refresh_button->add_pressed_callback([this](){ recomp::mods::scan_mods(); this->refresh_mods(); });
context.create_element<Label>(footer_container, "⚠ UNDER CONSTRUCTION ⚠", LabelStyle::Small); context.create_element<Label>(footer_container, "⚠ UNDER CONSTRUCTION ⚠", LabelStyle::Small);
@ -540,6 +559,7 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
mod_entry_floating_view->set_position(Position::Absolute); mod_entry_floating_view->set_position(Position::Absolute);
mod_entry_floating_view->set_selected(true); mod_entry_floating_view->set_selected(true);
recomp::mods::scan_mods();
refresh_mods(); refresh_mods();
context.close(); context.close();
@ -560,6 +580,35 @@ ModMenu::~ModMenu() {
// Placeholder class until the rest of the UI refactor is finished. // Placeholder class until the rest of the UI refactor is finished.
recompui::ModMenu* mod_menu;
void recompui::update_mod_list() {
if (mod_menu) {
recompui::ContextId ui_context = recompui::get_config_context_id();
bool opened = ui_context.open_if_not_already();
mod_menu->set_mods_dirty();
mod_menu->queue_update();
if (opened) {
ui_context.close();
}
}
}
void recompui::process_game_started() {
if (mod_menu) {
recompui::ContextId ui_context = recompui::get_config_context_id();
bool opened = ui_context.open_if_not_already();
mod_menu->queue_update();
if (opened) {
ui_context.close();
}
}
}
ElementModMenu::ElementModMenu(const Rml::String &tag) : Rml::Element(tag) { ElementModMenu::ElementModMenu(const Rml::String &tag) : Rml::Element(tag) {
SetProperty("width", "100%"); SetProperty("width", "100%");
SetProperty("height", "100%"); SetProperty("height", "100%");

View file

@ -65,6 +65,7 @@ class ModMenu : public Element {
public: public:
ModMenu(Element *parent); ModMenu(Element *parent);
virtual ~ModMenu(); virtual ~ModMenu();
void set_mods_dirty() { mods_dirty = true; }
private: private:
void refresh_mods(); void refresh_mods();
void open_mods_folder(); void open_mods_folder();
@ -76,6 +77,7 @@ private:
void mod_string_option_changed(const std::string &id, const std::string &value); void mod_string_option_changed(const std::string &id, const std::string &value);
void mod_number_option_changed(const std::string &id, double value); void mod_number_option_changed(const std::string &id, double value);
void create_mod_list(); void create_mod_list();
void process_event(const Event &e) override;
Container *body_container = nullptr; Container *body_container = nullptr;
Container *list_container = nullptr; Container *list_container = nullptr;
@ -96,6 +98,7 @@ private:
std::vector<recomp::mods::ModDetails> mod_details{}; std::vector<recomp::mods::ModDetails> mod_details{};
std::unordered_set<std::string> loaded_thumbnails; std::unordered_set<std::string> loaded_thumbnails;
std::string game_mod_id; std::string game_mod_id;
bool mods_dirty = false;
ConfigSubMenu *config_sub_menu; ConfigSubMenu *config_sub_menu;
}; };
@ -104,8 +107,6 @@ class ElementModMenu : public Rml::Element {
public: public:
ElementModMenu(const Rml::String& tag); ElementModMenu(const Rml::String& tag);
virtual ~ElementModMenu(); virtual ~ElementModMenu();
private:
ModMenu *mod_menu;
}; };
} // namespace recompui } // namespace recompui

View file

@ -10,6 +10,7 @@ struct {
recompui::ContextId ui_context; recompui::ContextId ui_context;
recompui::Label* prompt_header; recompui::Label* prompt_header;
recompui::Label* prompt_label; recompui::Label* prompt_label;
recompui::Element* prompt_controls;
recompui::Button* confirm_button; recompui::Button* confirm_button;
recompui::Button* cancel_button; recompui::Button* cancel_button;
std::function<void()> confirm_action; std::function<void()> confirm_action;
@ -95,26 +96,26 @@ void recompui::init_prompt_context() {
prompt_content->set_border_color(Color{ 255, 255, 255, 51 }); prompt_content->set_border_color(Color{ 255, 255, 255, 51 });
prompt_content->set_background_color(Color{ 8, 7, 13, 229 }); prompt_content->set_background_color(Color{ 8, 7, 13, 229 });
prompt_state.prompt_header = context.create_element<Label>(prompt_content, "Graphics options have changed", LabelStyle::Large); prompt_state.prompt_header = context.create_element<Label>(prompt_content, "", LabelStyle::Large);
prompt_state.prompt_header->set_margin(24, Unit::Dp); prompt_state.prompt_header->set_margin(24, Unit::Dp);
prompt_state.prompt_label = context.create_element<Label>(prompt_content, "Would you like to apply or discard these changes?", LabelStyle::Small); prompt_state.prompt_label = context.create_element<Label>(prompt_content, "", LabelStyle::Small);
prompt_state.prompt_label->set_margin(24, Unit::Dp); prompt_state.prompt_label->set_margin(24, Unit::Dp);
prompt_state.prompt_label->set_margin_top(0); prompt_state.prompt_label->set_margin_top(0);
Element* prompt_controls = context.create_element<Element>(prompt_content); prompt_state.prompt_controls = context.create_element<Element>(prompt_content);
prompt_controls->set_display(Display::Flex); prompt_state.prompt_controls->set_display(Display::Flex);
prompt_controls->set_flex_direction(FlexDirection::Row); prompt_state.prompt_controls->set_flex_direction(FlexDirection::Row);
prompt_controls->set_justify_content(JustifyContent::Center); prompt_state.prompt_controls->set_justify_content(JustifyContent::Center);
prompt_controls->set_padding_top(24, Unit::Dp); prompt_state.prompt_controls->set_padding_top(24, Unit::Dp);
prompt_controls->set_padding_bottom(24, Unit::Dp); prompt_state.prompt_controls->set_padding_bottom(24, Unit::Dp);
prompt_controls->set_padding_left(12, Unit::Dp); prompt_state.prompt_controls->set_padding_left(12, Unit::Dp);
prompt_controls->set_padding_right(12, Unit::Dp); prompt_state.prompt_controls->set_padding_right(12, Unit::Dp);
prompt_controls->set_border_top_width(1.1, Unit::Dp); prompt_state.prompt_controls->set_border_top_width(1.1, Unit::Dp);
prompt_controls->set_border_top_color({ 255, 255, 255, 25 }); prompt_state.prompt_controls->set_border_top_color({ 255, 255, 255, 25 });
prompt_state.confirm_button = context.create_element<Button>(prompt_controls, "", ButtonStyle::Primary); prompt_state.confirm_button = context.create_element<Button>(prompt_state.prompt_controls, "", ButtonStyle::Primary);
prompt_state.confirm_button->set_min_width(185.0f, Unit::Dp); prompt_state.confirm_button->set_min_width(185.0f, Unit::Dp);
prompt_state.confirm_button->set_margin_top(0); prompt_state.confirm_button->set_margin_top(0);
prompt_state.confirm_button->set_margin_bottom(0); prompt_state.confirm_button->set_margin_bottom(0);
@ -131,7 +132,7 @@ void recompui::init_prompt_context() {
confirm_hover_style->set_background_color(Color{ 69, 208, 67, 76 }); confirm_hover_style->set_background_color(Color{ 69, 208, 67, 76 });
confirm_hover_style->set_color(Color{ 242, 242, 242, 255 }); confirm_hover_style->set_color(Color{ 242, 242, 242, 255 });
prompt_state.cancel_button = context.create_element<Button>(prompt_controls, "", ButtonStyle::Primary); prompt_state.cancel_button = context.create_element<Button>(prompt_state.prompt_controls, "", ButtonStyle::Primary);
prompt_state.cancel_button->set_min_width(185.0f, Unit::Dp); prompt_state.cancel_button->set_min_width(185.0f, Unit::Dp);
prompt_state.cancel_button->set_margin_top(0); prompt_state.cancel_button->set_margin_top(0);
prompt_state.cancel_button->set_margin_bottom(0); prompt_state.cancel_button->set_margin_bottom(0);
@ -197,47 +198,133 @@ void style_button(recompui::Button* button, recompui::ButtonVariant variant) {
disabled_style->set_color(disabled_color); disabled_style->set_color(disabled_color);
} }
void recompui::open_prompt( // Must be called while prompt_state.mutex is locked.
const std::string& headerText, void show_prompt(std::function<void()>& prev_cancel_action, bool focus_on_cancel) {
const std::string& contentText, if (!recompui::is_context_shown(prompt_state.ui_context)) {
const std::string& confirmLabelText, recompui::show_context(prompt_state.ui_context, "");
const std::string& cancelLabelText, }
std::function<void()> confirmCb, else {
std::function<void()> cancelCb, // Call the previous cancel action to effectively close the previous prompt.
ButtonVariant _confirmVariant, if (prev_cancel_action) {
ButtonVariant _cancelVariant, prev_cancel_action();
bool _focusOnCancel, }
const std::string& _returnElementId }
) {
std::lock_guard lock{ prompt_state.mutex };
prompt_state.ui_context.open(); if (focus_on_cancel) {
prompt_state.prompt_header->set_text(headerText);
prompt_state.prompt_label->set_text(contentText);
prompt_state.confirm_button->set_text(confirmLabelText);
prompt_state.cancel_button->set_text(cancelLabelText);
prompt_state.confirm_action = confirmCb;
prompt_state.cancel_action = cancelCb;
prompt_state.return_element_id = _returnElementId;
style_button(prompt_state.confirm_button, _confirmVariant);
style_button(prompt_state.cancel_button, _cancelVariant);
if (_focusOnCancel) {
// TODO nav: focus cancel button // TODO nav: focus cancel button
} }
else { else {
// TODO nav: focus confirm button // TODO nav: focus confirm button
} }
}
void recompui::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<void()> confirm_action,
std::function<void()> cancel_action,
ButtonVariant confirm_variant,
ButtonVariant cancel_variant,
bool focus_on_cancel,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.confirm_button->set_display(Display::Block);
prompt_state.cancel_button->set_display(Display::Block);
prompt_state.confirm_button->set_text(confirm_label_text);
prompt_state.cancel_button->set_text(cancel_label_text);
prompt_state.confirm_action = confirm_action;
prompt_state.cancel_action = cancel_action;
prompt_state.return_element_id = return_element_id;
style_button(prompt_state.confirm_button, confirm_variant);
style_button(prompt_state.cancel_button, cancel_variant);
prompt_state.ui_context.close(); prompt_state.ui_context.close();
if (!recompui::is_context_shown(prompt_state.ui_context)) { show_prompt(prev_cancel_action, focus_on_cancel);
recompui::show_context(prompt_state.ui_context, ""); }
void recompui::open_info_prompt(
const std::string& header_text,
const std::string& content_text,
const std::string& okay_label_text,
std::function<void()> okay_action,
ButtonVariant okay_variant,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::Flex);
prompt_state.confirm_button->set_display(Display::None);
prompt_state.cancel_button->set_display(Display::Block);
prompt_state.cancel_button->set_text(okay_label_text);
prompt_state.confirm_action = {};
prompt_state.cancel_action = okay_action;
prompt_state.return_element_id = return_element_id;
style_button(prompt_state.cancel_button, okay_variant);
prompt_state.ui_context.close();
show_prompt(prev_cancel_action, true);
}
void recompui::open_notification(
const std::string& header_text,
const std::string& content_text,
const std::string& return_element_id
) {
std::lock_guard lock{ prompt_state.mutex };
std::function<void()> prev_cancel_action = std::move(prompt_state.cancel_action);
prompt_state.ui_context.open();
prompt_state.prompt_header->set_text(header_text);
prompt_state.prompt_label->set_text(content_text);
prompt_state.prompt_controls->set_display(Display::None);
prompt_state.confirm_button->set_display(Display::None);
prompt_state.cancel_button->set_display(Display::None);
prompt_state.confirm_action = {};
prompt_state.cancel_action = {};
prompt_state.return_element_id = return_element_id;
prompt_state.ui_context.close();
show_prompt(prev_cancel_action, false);
}
void recompui::close_prompt() {
std::lock_guard lock{ prompt_state.mutex };
if (recompui::is_context_shown(prompt_state.ui_context)) {
if (prompt_state.cancel_action) {
prompt_state.cancel_action();
}
recompui::hide_context(prompt_state.ui_context);
} }
} }
bool recompui::is_prompt_open() { bool recompui::is_prompt_open() {
return false; std::lock_guard lock{ prompt_state.mutex };
return recompui::is_context_shown(prompt_state.ui_context);
} }

View file

@ -837,8 +837,113 @@ void recompui::release_image(const std::string &src) {
} }
void recompui::drop_files(const std::list<std::filesystem::path> &file_list) { void recompui::drop_files(const std::list<std::filesystem::path> &file_list) {
// Prevent mod installation after the game has started.
if (ultramodern::is_game_started()) {
return;
}
recompui::open_notification("Installing Mods", "Please Wait");
// TODO: Needs a progress callback and a prompt for every mod that needs to be confirmed to be overwritten. // TODO: Needs a progress callback and a prompt for every mod that needs to be confirmed to be overwritten.
// TODO: Run this on a background thread and use the callbacks to advance the state instead of blocking.
ModInstaller::Result result; ModInstaller::Result result;
ModInstaller::start_mod_installation(file_list, nullptr, result); ModInstaller::start_mod_installation(file_list, nullptr, result);
ModInstaller::finish_mod_installation({}, result);
recompui::close_prompt();
if (!result.error_messages.empty()) {
std::string error_label = std::accumulate(result.error_messages.begin(), result.error_messages.end(), std::string{},
[](const std::string &lhs, const std::string &rhs)
{
return lhs.empty() ? rhs : lhs + '\n' + rhs;
});
recompui::open_info_prompt("Error Installing Mods", error_label, "OK", {}, recompui::ButtonVariant::Tertiary);
std::vector<std::string> dummy_error_messages{};
ModInstaller::cancel_mod_installation(result, dummy_error_messages);
return;
}
std::vector<ModInstaller::Confirmation> confirmations{};
for (const ModInstaller::Installation& pending_install : result.pending_installations) {
if (pending_install.needs_overwrite_confirmation) {
// Get the mod details for the current mod at this file path.
std::string old_mod_id = recomp::mods::get_mod_id_from_filename(pending_install.mod_file.filename());
std::optional<recomp::mods::ModDetails> old_mod_details = {};
if (!old_mod_id.empty()) {
old_mod_details = recomp::mods::get_details_for_mod(old_mod_id);
}
if (old_mod_details) {
confirmations.emplace_back(ModInstaller::Confirmation {
.old_display_name = old_mod_details->display_name,
.new_display_name = pending_install.display_name,
.old_mod_id = old_mod_details->mod_id,
.new_mod_id = pending_install.mod_id,
.old_version = old_mod_details->version,
.new_version = pending_install.mod_version
});
}
else {
confirmations.emplace_back(ModInstaller::Confirmation {
.old_display_name = "?",
.new_display_name = pending_install.display_name,
.old_mod_id = "",
.new_mod_id = pending_install.mod_id,
.old_version = recomp::Version{0, 0, 0, ""},
.new_version = pending_install.mod_version
});
}
}
}
if (confirmations.empty()) {
std::vector<std::string> error_messages{};
ModInstaller::finish_mod_installation(result, error_messages);
// TODO show errors
}
else {
std::string prompt_text = std::accumulate(confirmations.begin(), confirmations.end(), std::string{},
[](const std::string &cur_text, const ModInstaller::Confirmation &confirmation)
{
std::string new_text{};
if (confirmation.old_display_name == confirmation.new_display_name) {
new_text = confirmation.old_display_name + " (" + confirmation.old_version.to_string() + " -> " + confirmation.new_version.to_string() + ")";
}
else {
new_text =
confirmation.old_display_name + " (" + confirmation.old_version.to_string() + ") -> " +
confirmation.new_display_name + " (" + confirmation.new_version.to_string() + ")";
}
return cur_text.empty() ? new_text : cur_text + '\n' + new_text;
});
// open prompt where confirm finishes the mod installation with the overwritten files
recompui::open_choice_prompt("Overwrite Mods?",
prompt_text,
"Overwrite",
"Cancel",
[result]() {
std::vector<std::string> error_messages{};
recomp::mods::close_mods();
ModInstaller::finish_mod_installation(result, error_messages);
recomp::mods::scan_mods();
ContextId old_context = recompui::get_current_context();
old_context.close();
recompui::update_mod_list();
old_context.open();
// TODO show errors
},
[result]() {
std::vector<std::string> error_messages{};
ModInstaller::cancel_mod_installation(result, error_messages);
// TODO show errors
},
recompui::ButtonVariant::Success,
recompui::ButtonVariant::Error,
true,
""
);
}
} }