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_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<void()> confirmCb,
std::function<void()> 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<void()> confirm_action,
std::function<void()> 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<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();
void update_mod_list();
void process_game_started();
void apply_color_hack();
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() {
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;

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

View file

@ -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) {

View file

@ -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",

View file

@ -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;

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

View file

@ -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<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;
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<std::filesystem::path> dynamic_lib_files;
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++) {
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<recomp::mods::ZipModFileHandle> extracted_file_handle = std::make_unique<recomp::mods::ZipModFileHandle>(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<char> manifest_bytes = extracted_file_handle.read_file(ManifestFilename, exists);
std::vector<char> 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<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) {
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<std::string> &confirmed_overwrites, Result &result) {
result.error_messages.clear();
void ModInstaller::cancel_mod_installation(const Result &result, std::vector<std::string>& 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<std::string>& 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);
}
}
}

View file

@ -4,6 +4,8 @@
#include <librecomp/game.hpp>
#include <unordered_set>
#include <vector>
#include <string>
namespace recompui {
struct ModInstaller {
@ -11,17 +13,28 @@ namespace recompui {
std::string mod_id;
std::string display_name;
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;
};
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<std::string> error_messages;
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 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();
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<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);
@ -540,6 +559,7 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
mod_entry_floating_view->set_position(Position::Absolute);
mod_entry_floating_view->set_selected(true);
recomp::mods::scan_mods();
refresh_mods();
context.close();
@ -560,6 +580,35 @@ ModMenu::~ModMenu() {
// 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) {
SetProperty("width", "100%");
SetProperty("height", "100%");

View file

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

View file

@ -10,6 +10,7 @@ struct {
recompui::ContextId ui_context;
recompui::Label* prompt_header;
recompui::Label* prompt_label;
recompui::Element* prompt_controls;
recompui::Button* confirm_button;
recompui::Button* cancel_button;
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_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_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_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_controls->set_flex_direction(FlexDirection::Row);
prompt_controls->set_justify_content(JustifyContent::Center);
prompt_controls->set_padding_top(24, Unit::Dp);
prompt_controls->set_padding_bottom(24, Unit::Dp);
prompt_controls->set_padding_left(12, Unit::Dp);
prompt_controls->set_padding_right(12, Unit::Dp);
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_display(Display::Flex);
prompt_state.prompt_controls->set_flex_direction(FlexDirection::Row);
prompt_state.prompt_controls->set_justify_content(JustifyContent::Center);
prompt_state.prompt_controls->set_padding_top(24, Unit::Dp);
prompt_state.prompt_controls->set_padding_bottom(24, Unit::Dp);
prompt_state.prompt_controls->set_padding_left(12, Unit::Dp);
prompt_state.prompt_controls->set_padding_right(12, Unit::Dp);
prompt_state.prompt_controls->set_border_top_width(1.1, Unit::Dp);
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_margin_top(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_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_margin_top(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);
}
void recompui::open_prompt(
const std::string& headerText,
const std::string& contentText,
const std::string& confirmLabelText,
const std::string& cancelLabelText,
std::function<void()> confirmCb,
std::function<void()> cancelCb,
ButtonVariant _confirmVariant,
ButtonVariant _cancelVariant,
bool _focusOnCancel,
const std::string& _returnElementId
) {
std::lock_guard lock{ prompt_state.mutex };
prompt_state.ui_context.open();
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) {
// Must be called while prompt_state.mutex is locked.
void show_prompt(std::function<void()>& prev_cancel_action, bool focus_on_cancel) {
if (!recompui::is_context_shown(prompt_state.ui_context)) {
recompui::show_context(prompt_state.ui_context, "");
}
else {
// Call the previous cancel action to effectively close the previous prompt.
if (prev_cancel_action) {
prev_cancel_action();
}
}
if (focus_on_cancel) {
// TODO nav: focus cancel button
}
else {
// 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();
if (!recompui::is_context_shown(prompt_state.ui_context)) {
recompui::show_context(prompt_state.ui_context, "");
show_prompt(prev_cancel_action, focus_on_cancel);
}
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() {
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) {
// 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: Run this on a background thread and use the callbacks to advance the state instead of blocking.
ModInstaller::Result 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,
""
);
}
}