From bd821e823f2d8550474ea6b4d4e3cb09aad9963c Mon Sep 17 00:00:00 2001 From: Limo Date: Sat, 12 Apr 2025 16:07:02 +0200 Subject: [PATCH] refactor mod import Improved mod matching on import. Improved name and version detection on import. --- CMakeLists.txt | 1 - src/core/addmodinfo.h | 42 ------ src/core/importmodinfo.h | 86 ++++++++++--- src/core/mod.cpp | 57 ++++++-- src/core/mod.h | 25 +++- src/core/moddedapplication.cpp | 207 +++++++----------------------- src/core/moddedapplication.h | 46 ++----- src/core/modinfo.h | 33 +---- src/core/nexus/api.cpp | 57 +++++++- src/core/nexus/api.h | 28 +++- src/core/nexus/file.h | 8 +- src/main.cpp | 23 ++++ src/ui/addmoddialog.cpp | 157 ++++++++++++----------- src/ui/addmoddialog.h | 37 ++---- src/ui/applicationmanager.cpp | 214 +++++++++++++++++++++---------- src/ui/applicationmanager.h | 162 +++++++++++++++-------- src/ui/fomoddialog.cpp | 8 +- src/ui/fomoddialog.h | 8 +- src/ui/mainwindow.cpp | 135 +++++++------------ src/ui/mainwindow.h | 66 ++-------- tests/test_moddedapplication.cpp | 124 +++++++++--------- 21 files changed, 772 insertions(+), 752 deletions(-) delete mode 100644 src/core/addmodinfo.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 992c6a2..76c7861 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -83,7 +83,6 @@ find_package(ZLIB REQUIRED) # Separated for tests set(CORE_SOURCES - src/core/addmodinfo.h src/core/appinfo.h src/core/autotag.cpp src/core/autotag.h diff --git a/src/core/addmodinfo.h b/src/core/addmodinfo.h deleted file mode 100644 index 53e0a2d..0000000 --- a/src/core/addmodinfo.h +++ /dev/null @@ -1,42 +0,0 @@ -/*! - * \file addmodinfo.h - * \brief Contains the AddModInfo struct. - */ - -#pragma once - -#include -#include -#include - - -/*! - * \brief Stores data needed to install a new mod. - */ -struct AddModInfo -{ - /*! \brief Name of the new mod. */ - std::string name; - /*! \brief Version of the new mod. */ - std::string version; - /*! \brief Installer type to be used. */ - std::string installer; - /*! \brief Path to the mods files. */ - std::string source_path; - /*! \brief Ids of deployers to which the new mod will be added. */ - std::vector deployers; - /*! \brief Id of the mod the group of which the new mod will be added to, or -1 for no group. */ - int group; - /*! \brief Flags for the installer. */ - int installer_flags; - /*! \brief If > 0: Remove path components with depth < root_level. */ - int root_level; - /*! \brief Contains pairs of source and destination paths for installation files. */ - std::vector> files; - /*! \brief If true: The newly installed mod will replace the mod specified in group. */ - bool replace_mod = false; - /*! \brief Path to the local archive or directory used to install this mod. */ - std::filesystem::path local_source = ""; - /*! \brief URL from where the mod was downloaded. */ - std::string remote_source = ""; -}; diff --git a/src/core/importmodinfo.h b/src/core/importmodinfo.h index e9a6437..1f3a7c5 100644 --- a/src/core/importmodinfo.h +++ b/src/core/importmodinfo.h @@ -15,30 +15,60 @@ */ struct ImportModInfo { - /*! \brief Describes what import action should be taken. */ - enum Type + /*! \brief Describes which remote source this mod was retreived from. */ + enum RemoteType { - download = 0, - extract = 1 + /*! \brief No remote. */ + local = 0, + /*! \brief NexusMods. */ + nexus = 1 }; + + /*! \brief Describes what import action should be taken. */ + enum ActionType + { + /*! \brief Mod is to be downloaded. */ + download = 0, + /*! \brief Mod archive is to be extracted. */ + extract = 1, + /*! \brief Installation dialog is to be shown. */ + install_dialog = 2, + /*! \brief Mod is to be installed. */ + install = 3 + }; + /*! \brief Target ModdedApplication */ int app_id; - /*! \brief Type of action to be performed. */ - Type type; + /*! \brief ActionType of action to be performed. */ + ActionType action_type; /*! \brief Path to the local file used for extraction or empty if type == download. */ std::filesystem::path local_source; - /*! - * \brief URL used to download the mod. Can be either a URL pointing to the mod itself or - * a NexusMods nxm URL. - */ + /*! \brief Type of remote this mod was retreived from. */ + RemoteType remote_type = local; + /*! \brief Remote URL associated with this mod. */ std::string remote_source = ""; + /*! \brief Remote URL used to request a download URL. */ + std::string remote_request_url = ""; + /*! \brief If this was retreived from a remote source: The mod id on the remote. */ + long remote_mod_id = -1; + /*! \brief If this was retreived from a remote source: The file id on the remote. */ + long remote_file_id = -1; + /*! \brief If this was retreived from a remote source: The mod name on the remote. */ + std::string remote_mod_name = ""; + /*! \brief If this was retreived from a remote source: The file name on the remote. */ + std::string remote_file_name = ""; + /*! \brief If this was retreived from a remote source: The file version on the remote. */ + std::string remote_file_version = ""; + /*! \brief URL used to download the mod. Note: This may only be valid for a limited time period. */ + std::string remote_download_url = ""; + /*! \brief If !=-1: The mod should be added to this mods group after installation. */ + int target_group_id = -1; + /*! \brief Id assigned to this mod by Limo. */ + int limo_mod_id = -1; /*! \brief This is where the mod should be stored after extraction/ download. */ std::filesystem::path target_path; - /*! \brief If remote_source is a NexusMods mod page: The id of the file to be downloaded, else: - * Not set. */ - int nexus_file_id = -1; - /*! \brief If !=-1: The mod should be added to this mods group after installation. */ - int mod_id = -1; + /*! \brief Current location of the mod on disk. */ + std::filesystem::path current_path; /*! \brief Time at which this object was added to the queue. Used for sorting. */ std::chrono::time_point queue_time = std::chrono::high_resolution_clock::now(); @@ -46,16 +76,34 @@ struct ImportModInfo std::string version_overwrite = ""; /*! \brief If this is not empty: Use this as mod name. */ std::string name_overwrite = ""; + /*! \brief Indicates whether the last import action performed was successful. */ + bool last_action_was_successful = true; + /*! \brief Flags for the installer. */ + int installer_flags = 0; + /*! \brief If > 0: Remove path components with depth < root_level. */ + int root_level = 0; + /*! \brief Contains pairs of source and destination paths for installation files. */ + std::vector> files{}; + /*! \brief If true: The newly installed mod will replace the mod specified in group. */ + bool replace_mod = false; + /*! \brief Ids of deployers to which the new mod will be added. */ + std::vector deployers{}; + /*! \brief Installer type to be used. */ + std::string installer; + /*! \brief Name of the new mod. */ + std::string name; + /*! \brief Version of the new mod. */ + std::string version; /*! - * \brief Compares with another ImportModInfo object by their type. + * \brief Compares with another ImportModInfo object by their action_type. * \param other Object to compare to. - * \return True if only this object has type extract, else false. + * \return True if only this object has action_type extract, else false. */ bool operator<(const ImportModInfo& other) const { - if(type == other.type) + if(action_type == other.action_type) return queue_time > other.queue_time; - return type < other.type; + return action_type < other.action_type; } }; diff --git a/src/core/mod.cpp b/src/core/mod.cpp index 80a0e29..4f2f38d 100644 --- a/src/core/mod.cpp +++ b/src/core/mod.cpp @@ -8,23 +8,55 @@ Mod::Mod(int id, const std::string& source_r, const std::time_t& time_r, unsigned long size, - const std::time_t& suppress_time) : + const std::time_t& suppress_time, + long remote_mod_id, + long remote_file_id, + ImportModInfo::RemoteType remote_type) : id(id), name(std::move(name)), version(std::move(version)), install_time(time), local_source(source_l), remote_source(source_r), remote_update_time(time_r), size_on_disk(size), - suppress_update_time(suppress_time) + suppress_update_time(suppress_time), remote_mod_id(remote_mod_id), remote_file_id(remote_file_id), + remote_type(remote_type) {} +Mod::Mod(const Mod& other) +{ + id = other.id; + name = other.name; + version = other.version; + install_time = other.install_time; + local_source = other.local_source; + remote_source = other.remote_source; + remote_update_time = other.remote_update_time; + size_on_disk = other.size_on_disk; + suppress_update_time = other.suppress_update_time; + remote_mod_id = other.remote_mod_id; + remote_file_id = other.remote_file_id; + remote_type = other.remote_type; +} + Mod::Mod(const Json::Value& json) { - Mod(json["id"].asInt(), - json["name"].asString(), - json["version"].asString(), - json["install_time"].asInt64(), - json["local_source"].asString(), - json["remote_source"].asString(), - json["remote_update_time"].asInt64(), - json["size_on_disk"].asInt64(), - json["suppress_update_time"].asInt64()); + long remote_mod_id = -1; + if(json.isMember("remote_mod_id")) + remote_mod_id = json["remote_mod_id"].asInt64(); + long remote_file_id = -1; + if(json.isMember("remote_file_id")) + remote_file_id = json["remote_file_id"].asInt64(); + ImportModInfo::RemoteType remote_type = ImportModInfo::RemoteType::local; + if(json.isMember("remote_type")) + remote_type = static_cast(json["remote_type"].asInt()); + id = json["id"].asInt(); + name = json["name"].asString(); + version = json["version"].asString(); + install_time = json["install_time"].asInt64(); + local_source = json["local_source"].asString(); + remote_source = json["remote_source"].asString(); + remote_update_time = json["remote_update_time"].asInt64(); + size_on_disk = json["size_on_disk"].asInt64(); + suppress_update_time = json["suppress_update_time"].asInt64(); + remote_mod_id = remote_mod_id; + remote_file_id = remote_file_id; + remote_type = remote_type; } Json::Value Mod::toJson() const @@ -39,6 +71,9 @@ Json::Value Mod::toJson() const json["remote_update_time"] = remote_update_time; json["size_on_disk"] = size_on_disk; json["suppress_update_time"] = suppress_update_time; + json["remote_mod_id"] = remote_mod_id; + json["remote_file_id"] = remote_file_id; + json["remote_type"] = static_cast(remote_type); return json; } diff --git a/src/core/mod.h b/src/core/mod.h index 50dfe77..10ef677 100644 --- a/src/core/mod.h +++ b/src/core/mod.h @@ -8,6 +8,7 @@ #include #include #include +#include "importmodinfo.h" /*! @@ -33,6 +34,12 @@ struct Mod unsigned long size_on_disk; /*! \brief Timestamp for when the user requested to suppress current update notifications. */ std::time_t suppress_update_time; + /*! \brief If this was retreived from a remote source: The mod id on the remote. */ + long remote_mod_id = -1; + /*! \brief If this was retreived from a remote source: The file id on the remote. */ + long remote_file_id = -1; + /*! \brief Type of remote this mod was retreived from. */ + ImportModInfo::RemoteType remote_type; /*! * \brief Constructor. Simply initializes members. @@ -46,6 +53,9 @@ struct Mod * \param size Total size of the installed mod on disk. * \param suppress_time Timestamp for when the user requested to suppress current update * notifications. + * \param remote_mod_id If this was retreived from a remote source: The mod id on the remote. + * \param remote_file_id If this was retreived from a remote source: The file id on the remote. + * \param remote_type Type of remote this mod was retreived from. */ Mod(int id, const std::string& name, @@ -55,13 +65,24 @@ struct Mod const std::string& source_r, const std::time_t& time_r, unsigned long size, - const std::time_t& suppress_time); + const std::time_t& suppress_time, + long remote_mod_id, + long remote_file_id, + ImportModInfo::RemoteType remote_type); + /*! + * \brief Copy constructor. Copies all members. + * \param other Copy source + */ + Mod(const Mod& other); /*! * \brief Initializes all members from a JSON object. * \param json The source for member values. */ Mod(const Json::Value& json); - + /*! + * \brief Serializes this struct to a JSON object. + * \return The JSON object. + */ Json::Value toJson() const; /*! * \brief Compares to another mod by id. diff --git a/src/core/moddedapplication.cpp b/src/core/moddedapplication.cpp index eb5f727..43fe227 100644 --- a/src/core/moddedapplication.cpp +++ b/src/core/moddedapplication.cpp @@ -116,17 +116,17 @@ void ModdedApplication::unDeployModsFor(std::vector deployers) updateSettings(true); } -void ModdedApplication::installMod(const AddModInfo& info) +void ModdedApplication::installMod(const ImportModInfo& info) { - if(info.replace_mod && info.group != -1) + if(info.replace_mod && info.target_group_id != -1) { replaceMod(info); return; } ProgressNode progress_node(progress_callback_); - if(info.group >= 0 && !info.deployers.empty()) + if(info.target_group_id >= 0 && !info.deployers.empty()) progress_node.addChildren({ 1.0f, 10.0f, info.deployers.size() > 1 ? 10.0f : 1.0f }); - else if(info.group >= 0 || !info.deployers.empty()) + else if(info.target_group_id >= 0 || !info.deployers.empty()) progress_node.addChildren({ 1, 10 }); else progress_node.addChildren({ 1 }); @@ -140,7 +140,7 @@ void ModdedApplication::installMod(const AddModInfo& info) if(mod_id == std::numeric_limits().max()) throw std::runtime_error("Error: Could not generate new mod id."); last_mod_id_ = mod_id; - const auto mod_size = Installer::install(info.source_path, + const auto mod_size = Installer::install(info.current_path, staging_dir_ / std::to_string(mod_id), info.installer_flags, info.installer, @@ -155,19 +155,22 @@ void ModdedApplication::installMod(const AddModInfo& info) info.remote_source, time_now, mod_size, - time_now); + time_now, + info.remote_mod_id, + info.remote_file_id, + info.remote_type); installer_map_[mod_id] = info.installer; progress_node.child(0).advance(); - if(info.group >= 0) + if(info.target_group_id >= 0) { - if(modHasGroup(info.group)) - addModToGroup(mod_id, group_map_[info.group], &progress_node.child(1)); + if(modHasGroup(info.target_group_id)) + addModToGroup(mod_id, group_map_[info.target_group_id], &progress_node.child(1)); else - createGroup(mod_id, info.group, &progress_node.child(1)); + createGroup(mod_id, info.target_group_id, &progress_node.child(1)); } for(int deployer : info.deployers) - addModToDeployer(deployer, mod_id, true, &progress_node.child(info.group >= 0 ? 2 : 1)); + addModToDeployer(deployer, mod_id, true, &progress_node.child(info.target_group_id >= 0 ? 2 : 1)); for(auto& tag : auto_tags_) tag.updateMods(staging_dir_, std::vector{ mod_id }); @@ -372,15 +375,7 @@ std::vector ModdedApplication::getModInfo() const } mod_info.emplace_back( - mod.id, - mod.name, - mod.version, - mod.install_time, - mod.local_source, - mod.remote_source, - mod.remote_update_time, - mod.size_on_disk, - mod.suppress_update_time, + mod, deployer_names, deployer_ids, statuses, @@ -857,12 +852,6 @@ int ModdedApplication::verifyStagingDir(sfs::path staging_dir) return 0; } -void ModdedApplication::extractArchive(const sfs::path& source, const sfs::path& target) -{ - ProgressNode node(progress_callback_); - Installer::extract(source, target, &node); -} - DeployerInfo ModdedApplication::getDeployerInfo(int deployer) { if(!(deployers_[deployer]->isAutonomous())) @@ -1393,7 +1382,7 @@ void ModdedApplication::deleteAllData() for(const auto& mod : installed_mods_) sfs::remove_all(staging_dir_ / std::to_string(mod.id)); sfs::remove(staging_dir_ / CONFIG_FILE_NAME); - sfs::remove_all(staging_dir_ / download_dir_); + sfs::remove_all(getDownloadDir()); } void ModdedApplication::setAppVersion(const std::string& app_version) @@ -1461,106 +1450,6 @@ void ModdedApplication::suppressUpdateNotification(const std::vector& mod_i updateSettings(true); } -std::string ModdedApplication::getDownloadUrl(const std::string& nxm_url) -{ - return nexus::Api::getDownloadUrl(nxm_url); -} - -std::string ModdedApplication::getDownloadUrlForFile(int nexus_file_id, const std::string& mod_url) -{ - return nexus::Api::getDownloadUrl(mod_url, nexus_file_id); -} - -std::string ModdedApplication::getNexusPageUrl(const std::string& nxm_url) -{ - return nexus::Api::getNexusPageUrl(nxm_url); -} - -std::string ModdedApplication::downloadMod(const std::string& url, - std::function progress_callback) -{ - log_(Log::LOG_DEBUG, "Download URL: " + url); - std::regex url_regex(R"(.*/(.*)\?.*)"); - std::smatch match; - if(!std::regex_match(url, match, url_regex)) - throw std::runtime_error(std::format("Invalid download URL \"{}\"", url)); - sfs::path download_path = staging_dir_ / download_dir_; - if(!sfs::exists(download_path)) - sfs::create_directories(download_path); - sfs::path file_name = match[1].str(); - const std::string file_name_prefix = file_name.stem(); - const std::string extension = file_name.extension(); - int suffix = 1; - while(pu::exists(download_path / file_name)) - { - file_name = file_name_prefix + "(" + std::to_string(suffix) + ")" + extension; - suffix++; - } - std::string file_name_str = file_name.string(); - auto pos = file_name_str.find("%20"); - while(pos != std::string::npos) - { - file_name_str.replace(pos, 3, " "); - pos = file_name_str.find("%20"); - } - file_name = file_name_str; - - std::ofstream fstream(download_path / file_name, std::ios::binary); - if(!fstream.is_open()) - throw std::runtime_error("Failed to write to disk."); - bool message_sent = false; - cpr::Response response = cpr::Download( - fstream, - cpr::Url(url), - cpr::ProgressCallback( - [app = this, &message_sent, &file_name, progress_callback](auto download_total, - auto download_now, - auto upload_total, - auto upload_now, - intptr_t user_data) - { - if(!message_sent && download_total > 0) - { - std::string size_string; - long last_size = 0; - long size = download_total; - int exp = 0; - const std::vector units{ "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB" }; - while(size > 1024 && exp < units.size()) - { - last_size = size; - size /= 1024; - exp++; - } - last_size /= 1.024; - size_string = std::to_string(size); - const int first_digit = (last_size / 100) % 10; - const int second_digit = (last_size / 10) % 10; - if(first_digit != 0 || second_digit != 0) - size_string += "." + std::to_string(first_digit); - if(second_digit != 0) - size_string += std::to_string(second_digit); - size_string += units[exp]; - - app->log_(Log::LOG_INFO, - ("Downloading \"" + file_name.string() + "\" with size: ").c_str() + - size_string + "..."); - message_sent = true; - } - if(download_total != 0) - progress_callback((float)download_now / (float)download_total); - return true; - })); - if(response.status_code != 200) - { - sfs::remove(download_path / file_name); - throw std::runtime_error("Download failed with response: \"" + response.status_line + - "\" (code " + std::to_string(response.status_code) + ")."); - } - fstream.close(); - return (download_path / file_name).string(); -} - ExternalChangesInfo ModdedApplication::getExternalChanges(int deployer) { ExternalChangesInfo info; @@ -1668,6 +1557,11 @@ void ModdedApplication::applyModAction(int deployer, int action, int mod_id) updateSettings(true); } +std::filesystem::path ModdedApplication::getDownloadDir() const +{ + return staging_dir_ / DOWNLOAD_DIR; +} + sfs::path ModdedApplication::iconPath() const { return icon_path_; @@ -1702,18 +1596,8 @@ void ModdedApplication::updateSettings(bool write) for(int i = 0; i < installed_mods_.size(); i++) { - json_settings_["installed_mods"][i]["id"] = installed_mods_[i].id; - json_settings_["installed_mods"][i]["name"] = installed_mods_[i].name; - json_settings_["installed_mods"][i]["version"] = installed_mods_[i].version; + json_settings_["installed_mods"][i] = installed_mods_[i].toJson(); json_settings_["installed_mods"][i]["installer"] = installer_map_[installed_mods_[i].id]; - json_settings_["installed_mods"][i]["install_time"] = installed_mods_[i].install_time; - json_settings_["installed_mods"][i]["local_source"] = installed_mods_[i].local_source.string(); - json_settings_["installed_mods"][i]["remote_source"] = installed_mods_[i].remote_source; - json_settings_["installed_mods"][i]["remote_update_time"] = - installed_mods_[i].remote_update_time; - json_settings_["installed_mods"][i]["size_on_disk"] = installed_mods_[i].size_on_disk; - json_settings_["installed_mods"][i]["suppress_update_time"] = - installed_mods_[i].suppress_update_time; } for(int depl = 0; depl < deployers_.size(); depl++) @@ -1850,15 +1734,7 @@ void ModdedApplication::updateState(bool read) Json::Value installed_mods = json_settings_["installed_mods"]; for(int i = 0; i < installed_mods.size(); i++) { - installed_mods_.emplace_back(installed_mods[i]["id"].asInt(), - installed_mods[i]["name"].asString(), - installed_mods[i]["version"].asString(), - installed_mods[i]["install_time"].asInt64(), - installed_mods[i]["local_source"].asString(), - installed_mods[i]["remote_source"].asString(), - installed_mods[i]["remote_update_time"].asInt64(), - installed_mods[i]["size_on_disk"].asInt64(), - installed_mods[i]["suppress_update_time"].asInt64()); + installed_mods_.emplace_back(installed_mods[i]); std::string installer = installed_mods[i]["installer"].asString(); std::vector types = Installer::INSTALLER_TYPES; if(std::find(types.begin(), types.end(), installer) == types.end()) @@ -2116,9 +1992,9 @@ void ModdedApplication::splitMod(int mod_id, int deployer) continue; const auto mod_dir = staging_dir_ / std::to_string(mod_id) / mod_dir_optional->string(); - AddModInfo info; + ImportModInfo info; info.deployers = { depl }; - info.group = -1; + info.target_group_id = -1; auto iter = str::find_if(installed_mods_, [mod_id](const auto& mod) { return mod.id == mod_id; }); if(iter == installed_mods_.end()) @@ -2129,7 +2005,13 @@ void ModdedApplication::splitMod(int mod_id, int deployer) info.installer_flags = Installer::Flag::preserve_case | Installer::Flag::preserve_directories; info.files = {}; info.root_level = 0; - info.source_path = mod_dir; + info.current_path = mod_dir; + info.local_source = iter->local_source; + info.remote_source = iter->remote_source; + info.remote_mod_id = iter->remote_mod_id; + info.remote_file_id = iter->remote_file_id; + info.remote_type = iter->remote_type; + log_( Log::LOG_WARNING, std::format( @@ -2141,17 +2023,17 @@ void ModdedApplication::splitMod(int mod_id, int deployer) } } -void ModdedApplication::replaceMod(const AddModInfo& info) +void ModdedApplication::replaceMod(const ImportModInfo& info) { - if(!info.replace_mod || info.group == -1) + if(!info.replace_mod || info.target_group_id == -1) { installMod(info); return; } auto index = - str::find_if(installed_mods_, [group = info.group](const Mod& m) { return m.id == group; }); + str::find_if(installed_mods_, [group = info.target_group_id](const Mod& m) { return m.id == group; }); if(index == installed_mods_.end()) - throw std::runtime_error(std::format("Invalid group '{}' for mod '{}'", info.group, info.name)); + throw std::runtime_error(std::format("Invalid group '{}' for mod '{}'", info.target_group_id, info.name)); int mod_id = 0; if(!installed_mods_.empty()) @@ -2164,13 +2046,13 @@ void ModdedApplication::replaceMod(const AddModInfo& info) const sfs::path tmp_replace_dir = staging_dir_ / (std::string("tmp_replace_") + std::to_string(mod_id)); - const auto mod_size = Installer::install(info.source_path, + const auto mod_size = Installer::install(info.current_path, tmp_replace_dir, info.installer_flags, info.installer, info.root_level, info.files); - const sfs::path old_mod_path = staging_dir_ / std::to_string(info.group); + const sfs::path old_mod_path = staging_dir_ / std::to_string(info.target_group_id); sfs::remove_all(old_mod_path); sfs::rename(tmp_replace_dir, old_mod_path); @@ -2181,6 +2063,9 @@ void ModdedApplication::replaceMod(const AddModInfo& info) index->install_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); index->remote_update_time = index->install_time; index->size_on_disk = mod_size; + index->remote_mod_id = info.remote_mod_id; + index->remote_file_id = info.remote_file_id; + index->remote_type = info.remote_type; std::vector weights_profiles; std::vector weights_mods; @@ -2189,7 +2074,7 @@ void ModdedApplication::replaceMod(const AddModInfo& info) { bool was_split = false; update_targets.push_back({}); - if(deployers_[depl]->hasMod(info.group)) + if(deployers_[depl]->hasMod(info.target_group_id)) weights_mods.push_back(deployers_[depl]->getNumMods()); else weights_mods.push_back(0); @@ -2198,14 +2083,14 @@ void ModdedApplication::replaceMod(const AddModInfo& info) for(int prof = 0; prof < profile_names_.size(); prof++) { deployers_[depl]->setProfile(prof); - if(deployers_[depl]->hasMod(info.group)) + if(deployers_[depl]->hasMod(info.target_group_id)) { update_targets[depl].push_back(prof); weights_profiles.push_back(deployers_[depl]->getNumMods()); if(!was_split) { was_split = true; - splitMod(info.group, depl); + splitMod(info.target_group_id, depl); } } } @@ -2218,7 +2103,7 @@ void ModdedApplication::replaceMod(const AddModInfo& info) int i = 0; for(int depl = 0; depl < update_targets.size(); depl++) { - deployers_[depl]->updateDeployedFilesForMod(info.group, &node.child(0).child(depl)); + deployers_[depl]->updateDeployedFilesForMod(info.target_group_id, &node.child(0).child(depl)); for(int prof : update_targets[depl]) { deployers_[depl]->setProfile(prof); @@ -2229,7 +2114,7 @@ void ModdedApplication::replaceMod(const AddModInfo& info) } for(auto& tag : auto_tags_) - tag.updateMods(staging_dir_, std::vector{ info.group }); + tag.updateMods(staging_dir_, std::vector{ info.target_group_id }); updateAutoTagMap(); updateSettings(true); diff --git a/src/core/moddedapplication.h b/src/core/moddedapplication.h index aeaa506..f6991fd 100644 --- a/src/core/moddedapplication.h +++ b/src/core/moddedapplication.h @@ -5,7 +5,6 @@ #pragma once -#include "addmodinfo.h" #include "appinfo.h" #include "autotag.h" #include "backupmanager.h" @@ -74,7 +73,7 @@ public: * \brief Installs a new mod using the given Installer type. * \param info Contains all data needed to install the mod. */ - void installMod(const AddModInfo& info); + void installMod(const ImportModInfo& info); /*! * \brief Uninstalls the given mods, this includes deleting all installed files. * \param mod_id Ids of the mods to be uninstalled. @@ -375,12 +374,6 @@ public: * \return A code indicating success(0), an IO error(1) or an error during JSON parsing(2). */ static int verifyStagingDir(std::filesystem::path staging_dir); - /*! - * \brief Extracts the given archive to the given location. - * \param source Source path. - * \param target Extraction target path. - */ - void extractArchive(const std::filesystem::path& source, const std::filesystem::path& target); /*! * \brief Creates DeployerInfo for one Deployer. * \param deployer Target deployer. @@ -604,31 +597,6 @@ public: * \param mod_ids Ids of the mods for which update notifications are to be disabled. */ void suppressUpdateNotification(const std::vector& mod_ids); - /*! - * \brief Generates a download URL from the given NexusMods nxm Url. - * \param nxm_url The nxm URL used. - * \return The download URL. - */ - std::string getDownloadUrl(const std::string& nxm_url); - /*! - * \brief Generates a download URL from the given NexusMods mod id and file id. - * \param nexus_file_id File id of the mod. - * \param mod_url Url to the mod page on NexusMods. - * \return The download URL. - */ - std::string getDownloadUrlForFile(int nexus_file_id, const std::string& mod_url); - /*! - * \brief Generates a NexusMods mod page URL from the given nxm URL. - * \param nxm_url The nxm Url used. This is usually generated through the NexusMods website. - * \return The NexusMods mod page URL. - */ - std::string getNexusPageUrl(const std::string& nxm_url); - /*! - * \brief Downloads the file from the given url to staging_dir_ / _download. - * \param url Url from which to download the file. - * \return The path to the downloaded file. - */ - std::string downloadMod(const std::string& url, std::function progress_callback); /*! * \brief Checks if files deployed by the given deployer have been externally overwritten. * \param deployer Deployer to check. @@ -674,8 +642,16 @@ public: * \param mod_id Target mod. */ void applyModAction(int deployer, int action, int mod_id); + /*! + * \brief Returns the path used to store downloaded mods. + * \return The download path. + */ + std::filesystem::path getDownloadDir() const; private: + /*! \brief The subdirectory used to store downloads. */ + static inline constexpr std::string DOWNLOAD_DIR = "_download"; + /*! \brief The name of this application. */ std::string name_; /*! \brief Contains the internal state of this object. */ @@ -728,8 +704,6 @@ private: std::vector app_versions_; /*! \brief Callback used to inform about the current task's progress. */ std::function progress_callback_ = [](float f) {}; - /*! \brief The subdirectory used to store downloads. */ - std::string download_dir_ = "_download"; /*! \brief File name used to store exported deployers and auto tags. */ std::string export_file_name = "exported_config"; @@ -778,7 +752,7 @@ private: * \brief Replaces an existing mod with the mod specified by the given argument. * \param info Contains all data needed to install the mod. */ - void replaceMod(const AddModInfo& info); + void replaceMod(const ImportModInfo& info); /*! \brief Updates manual_tag_map_ with the information contained in manual_tags_. */ void updateManualTagMap(); /*! \brief Updates auto_tag_map_ with the information contained in auto_tags_. */ diff --git a/src/core/modinfo.h b/src/core/modinfo.h index 07e4f1d..dc7e727 100644 --- a/src/core/modinfo.h +++ b/src/core/modinfo.h @@ -35,16 +35,7 @@ struct ModInfo /*! * \brief Constructor. Simply initializes members. - * \param id The mod's id. - * \param name The mod's name. - * \param version The mod's version. - * \param install_time Timestamp indicating when the mod was installed. - * \param local_source Source archive for the mod. - * \param remote_source URL from where the mod was downloaded. - * \param remote_update_time Timestamp for when the mod was updated at the remote source. - * \param size Total size of the installed mod on disk. - * \param suppress_time Timestamp for when the user requested to suppress current update - * notifications. + * \param mod Mod object wrapped by this struct. * \param deployer_names Names of all \ref Deployer "deployers" the mod belongs to. * \param deployer_ids Ids of all \ref Deployer "deployers" the mod belongs to. * \param statuses The mods activation status for every \ref Deployer "deployer" it belongs to. @@ -52,15 +43,7 @@ struct ModInfo * \param is_active_member If true: Mod is the active member of it's group. * \param man_tags The names of all manual tags for this mod. */ - ModInfo(int id, - const std::string& name, - const std::string& version, - const std::time_t& install_time, - const std::filesystem::path& local_source, - const std::string& remote_source, - const std::time_t& remote_update_time, - unsigned long size, - const std::time_t& suppress_time, + ModInfo(const Mod& mod, const std::vector& deployer_names, const std::vector& deployer_ids, const std::vector& statuses, @@ -68,16 +51,8 @@ struct ModInfo bool is_active_member, const std::vector& man_tags, const std::vector& au_tags) : - mod(id, - name, - version, - install_time, - local_source, - remote_source, - remote_update_time, - size, - suppress_time), - deployers(std::move(deployer_names)), deployer_ids(std::move(deployer_ids)), + mod(mod), + deployers(deployer_names), deployer_ids(deployer_ids), deployer_statuses(statuses), group(group), is_active_group_member(is_active_member), manual_tags(man_tags), auto_tags(au_tags) {} diff --git a/src/core/nexus/api.cpp b/src/core/nexus/api.cpp index d93d191..83d24e5 100644 --- a/src/core/nexus/api.cpp +++ b/src/core/nexus/api.cpp @@ -1,5 +1,6 @@ #include "api.h" #include "../parseerror.h" +#include #include #include #include @@ -150,11 +151,10 @@ std::string Api::getDownloadUrl(const std::string& mod_url, long file_id) std::string Api::getDownloadUrl(const std::string& nxm_url) { - const std::regex regex( - R"(nxm:\/\/(.+)\/mods\/(\d+)\/files\/(\d+)\?key=(.+)&expires=(\d+)&user_id=(\d+))"); - std::smatch match; - if(!std::regex_match(nxm_url, match, regex)) + const auto match_opt = nxmUrlIsValid(nxm_url); + if(!match_opt) throw std::runtime_error(std::format("Invalid NXM URL: \"{}\"", nxm_url)); + std::smatch match = *match_opt; const std::string domain_name = match[1]; const std::string mod_id = match[2]; const std::string file_id = match[3]; @@ -332,3 +332,52 @@ std::optional> Api::extractDomainAndModId(const std: return { { match[1], std::stoi(match[2]) } }; return {}; } + +bool Api::initModInfo(ImportModInfo& info) +{ + std::vector files; + auto match = nxmUrlIsValid(info.remote_request_url); + if(!match) + return false; + + if(modUrlIsValid(info.remote_source)) + files = getModFiles(info.remote_source); + else + { + info.remote_source = getNexusPageUrl(info.remote_request_url); + files = getModFiles(info.remote_source); + } + + const std::string file_id_str = (*match)[3]; + if(file_id_str.find_first_not_of("0123456789") != std::string::npos) + return false; + + const std::string mod_id_str = (*match)[2]; + if(mod_id_str.find_first_not_of("0123456789") != std::string::npos) + return false; + + long file_id = std::stol(file_id_str); + long mod_id = std::stol(mod_id_str); + auto iter = str::find_if(files, [file_id](File& f){return f.file_id == file_id;}); + if(iter == files.end()) + return false; + + info.remote_mod_id = mod_id; + info.remote_file_id = iter->file_id; + info.remote_file_name = iter->name; + info.remote_file_version = iter->version; + info.remote_type = ImportModInfo::RemoteType::nexus; + + return true; +} + +std::optional Api::nxmUrlIsValid(const std::string& nxm_url) +{ + const std::regex regex( + R"(nxm:\/\/(.+)\/mods\/(\d+)\/files\/(\d+)\?key=(.+)&expires=(\d+)&user_id=(\d+))"); + std::smatch match; + std::regex_match(nxm_url, match, regex); + if(match.empty()) + return {}; + return match; +} diff --git a/src/core/nexus/api.h b/src/core/nexus/api.h index a01a887..3c2b393 100644 --- a/src/core/nexus/api.h +++ b/src/core/nexus/api.h @@ -7,8 +7,10 @@ #include "file.h" #include "mod.h" +#include "../importmodinfo.h" #include #include +#include /*! @@ -136,11 +138,6 @@ public: * \return The API key. */ static std::string getApiKey(); - -private: - /*! \brief The API key used for all operations. */ - inline static std::string api_key_ = ""; - /*! * \brief Extracts the NexusMods domain and mod id from the given mod page URL. * \param url URL to the mod on NexusMods. @@ -148,5 +145,26 @@ private: */ static std::optional> extractDomainAndModId( const std::string& mod_url); + /*! + * \brief Initializes remote members of the given ImportModInfo. + * + * Uses data retreived for the mod associated with the ImportModInfo::remote_source member. + * If remote_source is not valid, uses ImportModInfo::remote_download_url instead. + * + * \param info Mod info to initialize. + * \return True if initialization was successful. + */ + static bool initModInfo(ImportModInfo& info); + /*! + * \brief Checks whether the given string is a valid NexusMods nxm URL. + * \param nxm_url String to check. + * \return A regex match object for the string containing a group for every datum in the + * URL. If the URL is invalid: An empty optional. + */ + static std::optional nxmUrlIsValid(const std::string& nxm_url); + +private: + /*! \brief The API key used for all operations. */ + inline static std::string api_key_ = ""; }; } diff --git a/src/core/nexus/file.h b/src/core/nexus/file.h index 7c3cff2..198959c 100644 --- a/src/core/nexus/file.h +++ b/src/core/nexus/file.h @@ -44,7 +44,7 @@ public: long uid; /*! \brief The file id. */ long file_id; - /*! \brief The name of the actual file on disk. */ + /*! \brief The files display name. */ std::string name; /*! \brief The files version. */ std::string version; @@ -56,15 +56,15 @@ public: bool is_primary; /*! \brief Size of the file in KibiBytes. */ long size; - /*! \brief The files display name- */ + /*! \brief The name of the actual file on disk. */ std::string file_name; /*! \brief Timestamp for when the file was uploaded to NexusMods. */ std::time_t uploaded_time; /*! \brief Mod version to which the file belongs. */ std::string mod_version; - /*! \brief Optional: The URL of a virus scanning website (like virustotal.com) for this file. */ + /*! \brief Optional: The URL of a virus scanning website (e.g. virustotal.com) for this file. */ std::string external_virus_scan_url; - /*! \brief The description if the file. */ + /*! \brief The description of the file. */ std::string description; /*! \brief Size of the file in KibiBytes. */ long size_kb; diff --git a/src/main.cpp b/src/main.cpp index 349eda8..50a28b8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -107,6 +107,7 @@ int main(int argc, char* argv[]) if(pos_args.empty()) { client.sendString("Started"); + std::cout << "Another instance is already running. Sending arguments..." << std::endl; return 2; } std::regex nxm_regex(R"(nxm:\/\/.*\mods\/\d+\/files\/\d+\?.*)"); @@ -116,6 +117,28 @@ int main(int argc, char* argv[]) return 0; } + + + + + + + // TODO: Remove + std::cout << "Info size: " << sizeof(ImportModInfo) << std::endl; + + + + + + + + + + + + + + app.setWindowIcon(QIcon(":/logo.png")); MainWindow w; w.setDebugMode(debug_mode); diff --git a/src/ui/addmoddialog.cpp b/src/ui/addmoddialog.cpp index b1d9cf9..20529af 100644 --- a/src/ui/addmoddialog.cpp +++ b/src/ui/addmoddialog.cpp @@ -145,20 +145,13 @@ int AddModDialog::detectRootLevel(int deployer) const return 0; } -bool AddModDialog::setupDialog(const QString& name, - const QStringList& deployers, +bool AddModDialog::setupDialog(const QStringList& deployers, int cur_deployer, - const QString& path, const QStringList& deployer_paths, - int app_id, const std::vector& autonomous_deployers, const std::vector& case_invariant_deployers, const QString& app_version, - const QString& local_source, - const QString& remote_source, - int mod_id, - const QString& version_overwrite, - const QString& name_overwrite) + const ImportModInfo& info) { groups_.clear(); const auto& mod_infos = mod_list_model_->getModInfo(); @@ -170,16 +163,13 @@ bool AddModDialog::setupDialog(const QString& name, groups_ << (prefix + mod_info.mod.name + " [" + std::to_string(mod_info.mod.id) + "]").c_str(); } + import_mod_info_ = info; ui->content_tree->clear(); ui->root_level_box->setValue(0); ui->name_text->setFocus(); - app_id_ = app_id; - mod_path_ = path; deployer_paths_ = deployer_paths; case_invariant_deployers_ = case_invariant_deployers; app_version_ = app_version; - local_source_ = local_source; - remote_source_ = remote_source; ui->group_combo_box->setCurrentIndex(ADD_TO_GROUP_INDEX); ui->deployer_list->setEnabled(true); ui->group_field->clear(); @@ -192,10 +182,11 @@ bool AddModDialog::setupDialog(const QString& name, ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); ui->group_check->setCheckState(Qt::Unchecked); int mod_index = -1; - if(mod_id != -1) + if(info.target_group_id != -1) { - auto iter = - str::find_if(mod_infos, [mod_id](const ModInfo& info) { return mod_id == info.mod.id; }); + auto iter = str::find_if(mod_infos, + [mod_id = info.target_group_id](const ModInfo& info) + { return mod_id == info.mod.id; }); if(iter != mod_infos.end()) { mod_index = iter - mod_infos.begin(); @@ -207,10 +198,8 @@ bool AddModDialog::setupDialog(const QString& name, std::regex name_regex(R"(-\d+((?:-[\dA-Za-z]+)+)-\d+(?:\(\d+\))?\.(?:zip|7z|rar)$)"); std::smatch match; - std::string name_str = name.toStdString(); - if(!name_overwrite.isEmpty()) - ui->name_text->setText(name_overwrite); - else if(mod_index >= 0 && mod_index < mod_infos.size()) + std::string name_str = info.local_source.filename().string(); + if(mod_index >= 0 && mod_index < mod_infos.size()) { ui->name_text->setText(mod_infos[mod_index].mod.name.c_str()); ui->version_text->setText(mod_infos[mod_index].mod.version.c_str()); @@ -227,11 +216,18 @@ bool AddModDialog::setupDialog(const QString& name, else { ui->version_text->setText("1.0"); - ui->name_text->setText(name); + ui->name_text->setText(name_str.c_str()); } - if(!version_overwrite.isEmpty()) - ui->version_text->setText(version_overwrite); + if(!info.remote_file_name.empty()) + ui->name_text->setText(info.remote_file_name.c_str()); + if(!info.remote_file_version.empty()) + ui->version_text->setText(info.remote_file_version.c_str()); + + if(!info.name_overwrite.empty()) + ui->name_text->setText(info.name_overwrite.c_str()); + if(!info.version_overwrite.empty()) + ui->version_text->setText(info.version_overwrite.c_str()); ui->installer_box->clear(); int root_level = 0; @@ -239,53 +235,65 @@ bool AddModDialog::setupDialog(const QString& name, std::string detected_type; try { - auto signature = Installer::detectInstallerSignature(path.toStdString()); + auto signature = Installer::detectInstallerSignature(info.current_path); std::tie(root_level, prefix, detected_type) = signature; } catch(std::runtime_error& error) { showError(error); - emit addModAborted(mod_path_); + emit addModAborted(info.current_path.c_str()); return false; } if(detected_type == Installer::FOMODINSTALLER) { auto [name, version] = - fomod::FomodInstaller::getMetaData(sfs::path(mod_path_.toStdString()) / prefix); - if(!name.empty() && name_overwrite.isEmpty()) + fomod::FomodInstaller::getMetaData(info.current_path / prefix); + if(!name.empty() && info.name_overwrite.empty()) ui->name_text->setText(name.c_str()); - if(!version.empty() && version_overwrite.isEmpty()) + if(!version.empty() && info.version_overwrite.empty()) ui->version_text->setText(version.c_str()); } const std::string mod_name = ui->name_text->text().toStdString(); - const std::string remote_source_str = remote_source.toStdString(); - auto remote_source_or_name_matches = [&remote_source_str, &mod_name](const auto& pair) + auto remote_source_or_name_matches = + [&mod_info = std::as_const(info), &mod_name](const auto& pair) { const Mod mod = std::get<1>(pair).mod; - return !remote_source_str.empty() && remote_source_str == mod.remote_source || - mod.name == mod_name; + return !mod_info.remote_source.empty() && mod_info.remote_source == mod.remote_source || + mod.name == mod_name || + mod.remote_mod_id != -1 && mod_info.remote_mod_id == mod.remote_mod_id; }; - int group_index = -1; - int max_match_quality = -1; - for(const auto& [i, mod_info] : - mod_infos | stv::enumerate | stv::filter(remote_source_or_name_matches)) + + if(info.target_group_id == -1) { - int match_quality = 0; - if(remote_source_str == mod_info.mod.remote_source) - match_quality += 4; - if(mod_name == mod_info.mod.name) - match_quality += 2; - if(mod_info.is_active_group_member) - match_quality += 1; - if(match_quality > max_match_quality) + int group_index = -1; + int max_match_quality = -1; + for(const auto& [i, mod_info] : + mod_infos | stv::enumerate | stv::filter(remote_source_or_name_matches)) { - max_match_quality = match_quality; - group_index = i; + int match_quality = 0; + if(info.remote_type == mod_info.mod.remote_type) + { + if(mod_info.mod.remote_mod_id != -1 && mod_info.mod.remote_mod_id == info.remote_mod_id) + match_quality += 16; + if(mod_info.mod.remote_file_id != -1 && mod_info.mod.remote_file_id == info.remote_file_id) + match_quality += 8; + } + if(info.remote_source == mod_info.mod.remote_source) + match_quality += 4; + if(mod_name == mod_info.mod.name) + match_quality += 2; + if(mod_info.is_active_group_member) + match_quality += 1; + if(match_quality > max_match_quality) + { + max_match_quality = match_quality; + group_index = i; + } } + if(group_index != -1) + ui->group_field->setText(groups_[group_index]); } - if(group_index != -1) - ui->group_field->setText(groups_[group_index]); path_prefix_ = prefix.c_str(); int target_idx = 0; @@ -304,7 +312,7 @@ bool AddModDialog::setupDialog(const QString& name, ui->deployer_list->clear(); std::set selected_deployers; auto settings = QSettings(QCoreApplication::applicationName()); - settings.beginGroup(QString::number(app_id)); + settings.beginGroup(QString::number(info.app_id)); int size = settings.beginReadArray("selected_deployers"); for(int i = 0; i < size; i++) { @@ -315,7 +323,7 @@ bool AddModDialog::setupDialog(const QString& name, try { - auto mod_file_paths = Installer::getArchiveFileNames(path.toStdString()); + auto mod_file_paths = Installer::getArchiveFileNames(info.current_path); int max_depth = 0; for(const auto& path : mod_file_paths) max_depth = std::max(addTreeNode(ui->content_tree, path), max_depth); @@ -325,7 +333,7 @@ bool AddModDialog::setupDialog(const QString& name, catch(std::runtime_error& error) { showError(error); - emit addModAborted(mod_path_); + emit addModAborted(info.current_path.c_str()); return false; } @@ -365,7 +373,7 @@ void AddModDialog::closeEvent(QCloseEvent* event) if(dialog_completed_) return; dialog_completed_ = true; - emit addModAborted(mod_path_); + emit addModAborted(import_mod_info_.current_path.c_str()); QDialog::reject(); } @@ -374,7 +382,7 @@ void AddModDialog::reject() if(dialog_completed_) return; dialog_completed_ = true; - emit addModAborted(mod_path_); + emit addModAborted(import_mod_info_.current_path.c_str()); QDialog::reject(); } @@ -440,7 +448,7 @@ void AddModDialog::on_buttonBox_accepted() group = mod_list_model_->getModInfo()[groups_.indexOf(group_name)].mod.id; std::vector deployers; auto settings = QSettings(QCoreApplication::applicationName()); - settings.beginGroup(QString::number(app_id_)); + settings.beginGroup(QString::number(import_mod_info_.app_id)); settings.beginWriteArray("selected_deployers"); int settings_index = 0; for(int i = 0; i < ui->deployer_list->count(); i++) @@ -455,19 +463,16 @@ void AddModDialog::on_buttonBox_accepted() settings.endArray(); settings.setValue("fomod_target_deployer", ui->fomod_deployer_box->currentIndex()); settings.endGroup(); - std::vector> fomod_files{}; - AddModInfo info{ ui->name_text->text().toStdString(), - ui->version_text->text().toStdString(), - Installer::INSTALLER_TYPES[ui->installer_box->currentIndex()], - mod_path_.toStdString(), - deployers, - group, - options, - ui->root_level_box->value(), - fomod_files, - replace_mod, - local_source_.toStdString(), - remote_source_.toStdString() }; + import_mod_info_.action_type = ImportModInfo::ActionType::install; + import_mod_info_.name = ui->name_text->text().toStdString(); + import_mod_info_.version = ui->version_text->text().toStdString(); + import_mod_info_.installer = Installer::INSTALLER_TYPES[ui->installer_box->currentIndex()]; + import_mod_info_.deployers = deployers; + import_mod_info_.target_group_id = group; + import_mod_info_.installer_flags = options; + import_mod_info_.root_level = ui->root_level_box->value(); + import_mod_info_.files = {}; + import_mod_info_.replace_mod = replace_mod; if(Installer::INSTALLER_TYPES[ui->installer_box->currentIndex()] == Installer::FOMODINSTALLER) { bool case_invariant = true; @@ -480,22 +485,22 @@ void AddModDialog::on_buttonBox_accepted() } } fomod_dialog_->setupDialog( - sfs::path(mod_path_.toStdString()) / path_prefix_.toStdString(), + import_mod_info_.current_path / path_prefix_.toStdString(), deployer_paths_[ui->fomod_deployer_box->currentIndex()].toStdString(), app_version_, - info, - app_id_, + import_mod_info_, + import_mod_info_.app_id, case_invariant); if(!fomod_dialog_->hasSteps()) { - info.files = fomod_dialog_->getResult(); - emit addModAccepted(app_id_, info); + import_mod_info_.files = fomod_dialog_->getResult(); + emit addModAccepted(import_mod_info_.app_id, import_mod_info_); } else fomod_dialog_->show(); } else - emit addModAccepted(app_id_, info); + emit addModAccepted(import_mod_info_.app_id, import_mod_info_); } void AddModDialog::on_group_check_stateChanged(int state) @@ -512,7 +517,7 @@ void AddModDialog::on_buttonBox_rejected() if(dialog_completed_) return; dialog_completed_ = true; - emit addModAborted(mod_path_); + emit addModAborted(import_mod_info_.current_path.c_str()); } void AddModDialog::on_name_text_textChanged(const QString& text) @@ -559,12 +564,12 @@ void AddModDialog::on_group_combo_box_currentIndexChanged(int index) ui->group_check->checkState() == Qt::Unchecked); } -void AddModDialog::onFomodDialogComplete(int app_id, AddModInfo info) +void AddModDialog::onFomodDialogComplete(int app_id, ImportModInfo info) { emit addModAccepted(app_id, info); } void AddModDialog::onFomodDialogAborted() { - emit addModAborted(mod_path_); + emit addModAborted(import_mod_info_.current_path.c_str()); } diff --git a/src/ui/addmoddialog.h b/src/ui/addmoddialog.h index 9e00f93..221aa81 100644 --- a/src/ui/addmoddialog.h +++ b/src/ui/addmoddialog.h @@ -5,6 +5,7 @@ #pragma once +#include "../core/importmodinfo.h" #include "deployerlistmodel.h" #include "modlistmodel.h" #include "ui/fomoddialog.h" @@ -44,37 +45,23 @@ public: /*! * \brief Initializes this dialog with data needed for mod installation. - * \param name Default mod name. * \param deployers Contains all available \ref Deployer "deployers". * \param cur_deployer The currently active Deployer. - * \param path Source path for the new mod. * \param deployer_paths Contains target paths for all non autonomous deployers. - * \param app_id Id of currently active application. * \param autonomous_deployers Vector of bools indicating for each deployer * if that deployer is autonomous. * \param case_invariant_deployers Vector of bools indicating for each deployer * if that deployer is case invariant. - * \param local_source Source archive for the mod. - * \param remote_source URL from where the mod was downloaded. - * \param mod_id If =! -1: Id of the mod to the group of which the new mod should be added by default. - * \param version_overwrite If not empty: Use this to overwrite the default version. - * \param name_overwrite If not empty: Use this to overwrite the default name. + * \param info Contains data relating to the current status of the mod import. * \return True if dialog creation was successful. */ - bool setupDialog(const QString& name, - const QStringList& deployers, + bool setupDialog(const QStringList& deployers, int cur_deployer, - const QString& path, const QStringList& deployer_paths, - int app_id, const std::vector& autonomous_deployers, const std::vector& case_invariant_deployers, const QString& app_version, - const QString& local_source, - const QString& remote_source, - int mod_id, - const QString& version_overwrite, - const QString& name_overwrite); + const ImportModInfo& info); /*! * \brief Closes the dialog and emits a signal indicating installation has been canceled. * \param event The close even sent upon closing the dialog. @@ -86,12 +73,6 @@ public: private: /*! \brief Contains auto-generated UI elements. */ Ui::AddModDialog* ui; - /*! \brief Contains mod ids corresponding to entries in the field. */ - // std::vector mod_ids_; - /*! \brief Source path for the new mod data. */ - QString mod_path_; - /*! \brief Stores the id of the currently active \ref ModdedApplication "application". */ - int app_id_; /*! \brief Holds radio button groups used to select installation options. */ QList option_groups_; /*! \brief Used to color tree nodes which will not be removed. */ @@ -114,10 +95,6 @@ private: static constexpr int REPLACE_MOD_INDEX = 1; /*! \brief App version used for fomod conditions. */ QString app_version_; - /*! \brief Path to the source archive for the mod. */ - QString local_source_; - /*! \brief URL from where the mod was downloaded. */ - QString remote_source_; /*! \brief Indicates whether the dialog has been completed. */ bool dialog_completed_ = false; /*! \brief Model containing all mod related data. */ @@ -126,6 +103,8 @@ private: DeployerListModel* deployer_list_model_; /*! \brief For every deployer: A bool indicating whether that deployer is case invariant. */ std::vector case_invariant_deployers_; + /*! \brief Contains all data related to the current state of the mod installation. */ + ImportModInfo import_mod_info_; /*! * \brief Updates the enabled state of this dialog's OK button to only be enabled when @@ -212,7 +191,7 @@ private slots: * \param app_id Application for which the new mod is to be installed. * \param info Contains all data needed to install the mod. */ - void onFomodDialogComplete(int app_id, AddModInfo info); + void onFomodDialogComplete(int app_id, ImportModInfo info); /*! \brief Called when fomod dialog has been canceled. Emits addModAborted */ void onFomodDialogAborted(); @@ -222,7 +201,7 @@ signals: * \param app_id Application for which the new mod is to be installed. * \param info Contains all data needed to install the mod. */ - void addModAccepted(int app_id, AddModInfo info); + void addModAccepted(int app_id, ImportModInfo info); /*! * \brief Signals mod installation has been aborted. * \param temp_dir Directory used for mod extraction. diff --git a/src/ui/applicationmanager.cpp b/src/ui/applicationmanager.cpp index c8aaf2d..4c53f5d 100644 --- a/src/ui/applicationmanager.cpp +++ b/src/ui/applicationmanager.cpp @@ -1,6 +1,7 @@ #include "applicationmanager.h" #include "../core/deployerfactory.h" #include "../core/installer.h" +#include "../core/pathutils.h" #include #include #include @@ -9,7 +10,107 @@ #include namespace sfs = std::filesystem; +namespace pu = path_utils; +bool performDownload(ImportModInfo& info, ApplicationManager* app_mgr) +{ + app_mgr->sendLogMessage(Log::LOG_DEBUG, + std::format("Downloading from : '{}'", info.remote_download_url)); + std::regex url_regex(R"(.*/(.*)\?.*)"); + std::smatch match; + if(!std::regex_match(info.remote_download_url, match, url_regex)) + throw std::runtime_error(std::format("Invalid download URL \"{}\"", info.remote_download_url)); + sfs::path download_path = info.target_path; + if(!sfs::exists(download_path)) + sfs::create_directories(download_path); + sfs::path file_name = match[1].str(); + const std::string file_name_prefix = file_name.stem(); + const std::string extension = file_name.extension(); + int suffix = 1; + while(pu::exists(download_path / file_name)) + { + file_name = file_name_prefix + "(" + std::to_string(suffix) + ")" + extension; + suffix++; + } + std::string file_name_str = file_name.string(); + auto pos = file_name_str.find("%20"); + while(pos != std::string::npos) + { + file_name_str.replace(pos, 3, " "); + pos = file_name_str.find("%20"); + } + file_name = file_name_str; + + auto progress_callback = [app_mgr](float progress) { app_mgr->sendUpdateProgress(progress); }; + std::ofstream fstream(download_path / file_name, std::ios::binary); + if(!fstream.is_open()) + throw std::runtime_error("Failed to write to disk."); + bool message_sent = false; + cpr::Response response = cpr::Download( + fstream, + cpr::Url(info.remote_download_url), + cpr::ProgressCallback( + [app_mgr, &message_sent, &file_name, progress_callback](auto download_total, + auto download_now, + auto upload_total, + auto upload_now, + intptr_t user_data) + { + if(!message_sent && download_total > 0) + { + std::string size_string; + long last_size = 0; + long size = download_total; + int exp = 0; + const std::vector units{ "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB" }; + while(size > 1024 && exp < units.size()) + { + last_size = size; + size /= 1024; + exp++; + } + last_size /= 1.024; + size_string = std::to_string(size); + const int first_digit = (last_size / 100) % 10; + const int second_digit = (last_size / 10) % 10; + if(first_digit != 0 || second_digit != 0) + size_string += "." + std::to_string(first_digit); + if(second_digit != 0) + size_string += std::to_string(second_digit); + size_string += units[exp]; + + app_mgr->sendLogMessage( + Log::LOG_INFO, + ("Downloading \"" + file_name.string() + "\" with size: ").c_str() + size_string + + "..."); + message_sent = true; + } + if(download_total != 0) + progress_callback((float)download_now / (float)download_total); + return true; + })); + if(response.status_code != 200) + { + sfs::remove(download_path / file_name); + throw std::runtime_error("Download failed with response: \"" + response.status_line + + "\" (code " + std::to_string(response.status_code) + ")."); + } + fstream.close(); + info.local_source = download_path / file_name; + info.current_path = info.local_source; + return true; +} + +bool performExtraction(ImportModInfo& info, ApplicationManager* app_mgr) +{ + info.last_action_was_successful = false; + auto progress_callback = [app_mgr](float progress) { app_mgr->sendUpdateProgress(progress); }; + ProgressNode node(progress_callback); + Installer::extract(info.local_source, info.target_path, &node); + info.current_path = info.target_path; + info.last_action_was_successful = true; + return true; +} ApplicationManager::ApplicationManager(QObject* parent) : QObject{ parent } { @@ -193,11 +294,11 @@ void ApplicationManager::handleAddDeployerError(int code, emit sendError("Error", "Could not write to staging dir " + QString(staging_dir.string().c_str()) + "!"); else if(code == 2) - emit sendError("Error", - "Could not create hard link from\n\"" + QString(staging_dir.string().c_str()) + - "\"\nto\n\"" + QString(dest_dir.string().c_str()) + "\".\n" + - "Ensure that both directories are on the same partition!\n" - "Alternatively: Switch to sym link deployment."); + emit sendError( + "Error", + "Could not create hard link from\n\"" + QString(staging_dir.string().c_str()) + "\"\nto\n\"" + + QString(dest_dir.string().c_str()) + "\".\n" + + "Ensure that both directories are on the same partition!\n" "Alternatively: " "Switch to " "sym " "link deployment."); else if(code == 3) emit sendError("Error", "Could no copy files\n\"" + QString(staging_dir.string().c_str()) + @@ -350,7 +451,7 @@ void ApplicationManager::unDeployModsFor(int app_id, std::vector deployer_i emit completedOperations("Mods undeployed"); } -void ApplicationManager::installMod(int app_id, AddModInfo info) +void ApplicationManager::installMod(int app_id, ImportModInfo info) { bool has_thrown = false; if(appIndexIsValid(app_id)) @@ -625,19 +726,10 @@ void ApplicationManager::sortModsByConflicts(int app_id, int deployer) emit completedOperations("Mods sorted"); } -void ApplicationManager::extractArchive(int app_id, - int mod_id, - QString source, - QString target, - QString remote_source, - QString version, - QString name) +void ApplicationManager::extractArchive(ImportModInfo info) { - bool has_thrown = true; - if(appIndexIsValid(0)) - has_thrown = handleExceptions<&ModdedApplication::extractArchive>( - 0, source.toStdString(), target.toStdString()); - emit extractionComplete(app_id, mod_id, !has_thrown, target, source, remote_source, version, name); + handleExceptionsForFunction(performExtraction, info, this); + emit extractionComplete(info); } void ApplicationManager::addBackupTarget(int app_id, @@ -834,71 +926,59 @@ void ApplicationManager::getNexusPage(int app_id, int mod_id) emit completedOperations(); } -void ApplicationManager::downloadMod(int app_id, QString nxm_url) +void ApplicationManager::downloadMod(ImportModInfo info) { - if(!appIndexIsValid(app_id)) + info.last_action_was_successful = false; + if(!appIndexIsValid(info.app_id)) { emit downloadFailed(); return; } - auto download_url = - handleExceptions(&ModdedApplication::getDownloadUrl, apps_[app_id], nxm_url.toStdString()); - if(!download_url) + if(info.remote_request_url.empty()) + { + auto download_url = handleExceptionsForFunction( + static_cast(nexus::Api::getDownloadUrl), + info.remote_source, + info.remote_file_id); + if(!download_url) + { + emit downloadFailed(); + return; + } + info.remote_download_url = *download_url; + } + else + { + auto download_url = handleExceptionsForFunction( + static_cast(nexus::Api::getDownloadUrl), + info.remote_request_url); + if(!download_url) + { + emit downloadFailed(); + return; + } + info.remote_download_url = *download_url; + } + info.remote_download_url = QUrl(info.remote_download_url.c_str()).toEncoded().toStdString(); + + auto init_successful = handleExceptionsForFunction(nexus::Api::initModInfo, info); + if(!init_successful || !(*init_successful)) { emit downloadFailed(); return; } - auto mod_url = - handleExceptions(&ModdedApplication::getNexusPageUrl, apps_[app_id], nxm_url.toStdString()); - if(!mod_url) - { - emit downloadFailed(); - return; - } - auto file_path = - handleExceptions(&ModdedApplication::downloadMod, - apps_[app_id], - QUrl(download_url->c_str()).toEncoded().toStdString(), - [app_mgr = this](float progress) { app_mgr->sendUpdateProgress(progress); }); - if(!file_path) + info.target_path = apps_[info.app_id].getDownloadDir(); + auto download_successful = handleExceptionsForFunction(performDownload, info, this); + if(!download_successful) { emit downloadFailed(); return; } - emit downloadComplete(app_id, -1, file_path->c_str(), mod_url->c_str()); -} - -void ApplicationManager::downloadModFile(int app_id, int mod_id, int nexus_file_id, QString mod_url) -{ - if(!appIndexIsValid(app_id)) - { - emit downloadFailed(); - return; - } - - auto download_url = handleExceptions( - &ModdedApplication::getDownloadUrlForFile, apps_[app_id], nexus_file_id, mod_url.toStdString()); - if(!download_url) - { - emit downloadFailed(); - return; - } - - auto file_path = - handleExceptions(&ModdedApplication::downloadMod, - apps_[app_id], - QUrl(download_url->c_str()).toEncoded().toStdString(), - [app_mgr = this](float progress) { app_mgr->sendUpdateProgress(progress); }); - if(!file_path) - { - emit downloadFailed(); - return; - } - - emit downloadComplete(app_id, mod_id, file_path->c_str(), mod_url); + info.last_action_was_successful = true; + emit downloadComplete(info); } void ApplicationManager::checkForModUpdates(int app_id) diff --git a/src/ui/applicationmanager.h b/src/ui/applicationmanager.h index 181ca93..64620a1 100644 --- a/src/ui/applicationmanager.h +++ b/src/ui/applicationmanager.h @@ -72,6 +72,11 @@ public: * \param enabled New status. */ void enableExceptions(bool enabled); + /*! + * \brief Informs about the progress in the current task by emitting \ref updateProgress. + * \param progress The progress. + */ + void sendUpdateProgress(float progress); private: /*! @@ -240,6 +245,91 @@ private: return ret_value; } + /*! + * \brief Wrapper for generic functions. Catches specific exception types and sends an error + * message to the gui if an exception was thrown. + * \param f Function to run. + * \param args Arguments for the function. + * \return If no exception was thrown: The return value of the function, + * else: An empty optional. + */ + template + auto handleExceptionsForFunction(Func&& f, Args&&... args) + -> std::optional(args)...))> + { + decltype((f)(std::forward(args)...)) ret_value; + std::string message; + bool has_thrown = false; + try + { + ret_value = (f)(std::forward(args)...); + } + catch(Json::RuntimeError& error) + { + has_thrown = true; + message = error.what(); + if(throw_exceptions_) + throw error; + } + catch(Json::LogicError& error) + { + has_thrown = true; + + message = error.what(); + if(throw_exceptions_) + throw error; + } + catch(ParseError& error) + { + has_thrown = true; + message = error.what(); + if(throw_exceptions_) + throw error; + } + catch(std::ios_base::failure& error) + { + has_thrown = true; + message = error.what(); + if(throw_exceptions_) + throw error; + } + catch(CompressionError& error) + { + has_thrown = true; + message = error.what(); + if(throw_exceptions_) + throw error; + } + catch(std::runtime_error& error) + { + has_thrown = true; + message = error.what(); + if(throw_exceptions_) + throw error; + } + catch(std::invalid_argument& error) + { + has_thrown = true; + message = error.what(); + if(throw_exceptions_) + throw error; + } + catch(...) + { + has_thrown = true; + message = "An unexpected error occured!"; + if(throw_exceptions_) + throw std::runtime_error("An unexpected error occured!"); + } + + if(has_thrown) + { + emit sendError("Error", message.c_str()); + return {}; + } + return ret_value; + } + /*! \brief Contains every ModdedApplication handled by this object. */ std::vector apps_; /*! \brief If true: Do not catch exceptions. */ @@ -296,11 +386,6 @@ private: * \param message Message of the exception thrown during parsing. */ void handleParseError(std::string path, std::string message); - /*! - * \brief Informs about the progress in the current task by emitting \ref updateProgress. - * \param progress The progress. - */ - void sendUpdateProgress(float progress); /*! \brief Counter for the number of instances of this class. */ inline static int number_of_instances_ = 0; @@ -379,23 +464,9 @@ signals: void sendError(QString title, QString message); /*! * \brief Emitted after archive extraction is complete. - * \param app_id \ref ModdedApplication "application" for which the mod has been extracted. - * \param mod_id Id of the mod for which the file was to be extracted or -1 if this is a new mod. - * \param success False if exception has been thrown. - * \param extracted_path Path to which the mod was extracted. - * \param local_source Source archive for the mod. - * \param remote_source URL from where the mod was downloaded. - * \param version If not empty: Use this to overwrite the default version. - * \param name If not empty: Use this to overwrite the default name. + * \param info Contains extraction directory as ImportModInfo::current_path. */ - void extractionComplete(int app_id, - int mod_id, - bool success, - QString extracted_path, - QString local_source, - QString remote_source, - QString version, - QString name); + void extractionComplete(ImportModInfo info); /*! * \brief Sends a log message to the logging window. * \param log_level Type of message. @@ -424,13 +495,9 @@ signals: void sendNexusPage(int app_id, int mod_id, nexus::Page page); /*! * \brief Signals successful completion of a mod download. - * \param app_id App for which the mod has been downloaded. - * \param mod_id Id of the mod for which the file is to be downloaded. - * This is the limo internal mod id, NOT the NexusMods id. - * \param file_path Path to the downloaded file. - * \param mod_url Url from which the mod was downloaded. + * \param info ImportModInfo::local_source contains the download directory. */ - void downloadComplete(int app_id, int mod_id, QString file_path, QString mod_url); + void downloadComplete(ImportModInfo info); /*! \brief Signals a failed download. */ void downloadFailed(); /*! @@ -500,7 +567,7 @@ public slots: * \param app_id The target \ref ModdedApplication "application". * \param info Contains all data needed to install the mod. */ - void installMod(int app_id, AddModInfo info); + void installMod(int app_id, ImportModInfo info); /*! * \brief Uninstalls the given mods for one \ref ModdedApplication "application", this includes * deleting all installed files. @@ -728,21 +795,10 @@ public slots: void sortModsByConflicts(int app_id, int deployer); /*! * \brief Extracts the given archive to the given location. - * \param app_id \ref ModdedApplication "application" for which the mod is to be extracted. - * \param mod_id Id of the mod for which the file is to be extracted or -1 if this is a new mod. - * \param source Source path. - * \param target Extraction target path. - * \param remote_source URL from where the mod was downloaded. - * \param version If not empty: Use this to overwrite the default version. - * \param name If not empty: Use this to overwrite the default name. + * \param info Contains archive source path in ImportModInfo::local_source + * and extraction target path in ImportModInfo::target_path. */ - void extractArchive(int app_id, - int mod_id, - QString source, - QString target, - QString remote_source, - QString version, - QString name); + void extractArchive(ImportModInfo info); /*! * \brief Adds a new target file or directory to be managed by the BackupManager of given * ModdedApplication. @@ -912,22 +968,14 @@ public slots: */ void getNexusPage(int app_id, int mod_id); /*! - * \brief Downloads a mod from nexusmods using the given nxm_url. - * \param app_id App for which the mod is to be downloaded. The mod is downloaded to the apps - * staging directory. - * \param nxm_url Url containing all information needed for the download. + * \brief Downloads a mod from nexusmods + * + * The mod is downloaded to the target apps staging directory. Uses either the given nxm URL + * or the mod and file id for the download. + * + * \param info Contains either a nxm URL or nexus mod and file ids. */ - void downloadMod(int app_id, QString nxm_url); - /*! - * \brief Downloads the file with the given id for the given mod url from nexusmods. - * \param app_id App for which the mod is to be downloaded. The mod is downloaded to the apps - * staging directory. - * \param mod_id Id of the mod for which the file is to be downloaded. - * This is the Limo internal mod id, NOT the NexusMods id. - * \param nexus_file_id File id of the mod. - * \param mod_url Url to the mod page on NexusMods. - */ - void downloadModFile(int app_id, int mod_id, int nexus_file_id, QString mod_url); + void downloadMod(ImportModInfo info); /*! * \brief Checks for available mod updates on NexusMods. * \param app_id App for which mod updates are to be checked. diff --git a/src/ui/fomoddialog.cpp b/src/ui/fomoddialog.cpp index cb6b4fb..be01d5b 100644 --- a/src/ui/fomoddialog.cpp +++ b/src/ui/fomoddialog.cpp @@ -34,12 +34,12 @@ FomodDialog::~FomodDialog() void FomodDialog::setupDialog(const sfs::path& config_file, const sfs::path& target_path, const QString& app_version, - const AddModInfo& info, + const ImportModInfo& info, int app_id, bool paths_are_case_invariant) { dialog_completed_ = false; - add_mod_info_ = info; + import_mod_info_ = info; app_id_ = app_id; paths_are_case_invariant_ = paths_are_case_invariant; installer_->init(config_file, paths_are_case_invariant, target_path, app_version.toStdString()); @@ -284,8 +284,8 @@ void FomodDialog::onNextButtonPressed() } else { - add_mod_info_.files = result_; - emit addModAccepted(app_id_, add_mod_info_); + import_mod_info_.files = result_; + emit addModAccepted(app_id_, import_mod_info_); } accept(); } diff --git a/src/ui/fomoddialog.h b/src/ui/fomoddialog.h index 4aa3441..b4cff07 100644 --- a/src/ui/fomoddialog.h +++ b/src/ui/fomoddialog.h @@ -5,7 +5,7 @@ #pragma once -#include "../core/addmodinfo.h" +#include "../core/importmodinfo.h" #include "../core/fomod/fomodinstaller.h" #include #include @@ -48,7 +48,7 @@ public: void setupDialog(const std::filesystem::path& config_file, const std::filesystem::path& target_path, const QString& app_version, - const AddModInfo& info, + const ImportModInfo& info, int app_id, bool paths_are_case_invariant); /*! @@ -86,7 +86,7 @@ private: /*! \brief If true: This dialog is non interactive. */ bool has_no_steps_; /*! \brief Contains necessary data to install the mod upon dialog completion. */ - AddModInfo add_mod_info_; + ImportModInfo import_mod_info_; /*! \brief Application for which the new mod is to be installed. */ int app_id_; /*! \brief Indicates whether the dialog has been completed. */ @@ -153,7 +153,7 @@ signals: * \param app_id Application for which the new mod is to be installed. * \param info Contains all data needed to install the mod. */ - void addModAccepted(int app_id, AddModInfo info); + void addModAccepted(int app_id, ImportModInfo info); /*! \brief Signals mod installation has been aborted. */ void addModAborted(); }; diff --git a/src/ui/mainwindow.cpp b/src/ui/mainwindow.cpp index 2800ba1..653c06b 100644 --- a/src/ui/mainwindow.cpp +++ b/src/ui/mainwindow.cpp @@ -50,7 +50,6 @@ Q_DECLARE_METATYPE(std::unordered_set); Q_DECLARE_METATYPE(QList>); Q_DECLARE_METATYPE(EditApplicationInfo); Q_DECLARE_METATYPE(EditDeployerInfo); -Q_DECLARE_METATYPE(AddModInfo); Q_DECLARE_METATYPE(std::vector); Q_DECLARE_METATYPE(Log::LogLevel); Q_DECLARE_METATYPE(std::vector); @@ -62,6 +61,7 @@ Q_DECLARE_METATYPE(nexus::Page); Q_DECLARE_METATYPE(ExternalChangesInfo); Q_DECLARE_METATYPE(FileChangeChoices); Q_DECLARE_METATYPE(Tool); +Q_DECLARE_METATYPE(ImportModInfo); MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWindow) @@ -130,15 +130,14 @@ void MainWindow::setCmdArgument(std::string argument) argument.erase(0, 1); if(argument.ends_with('\"')) argument.erase(argument.size() - 1, 1); - std::regex nxm_regex(R"(nxm:\/\/.*\mods\/\d+\/files\/\d+\?.*)"); - std::smatch match; - if(std::regex_match(argument, match, nxm_regex)) + + if(nexus::Api::nxmUrlIsValid(argument)) { Log::debug("Received download request for \"" + argument + "\"."); ImportModInfo info; info.app_id = currentApp(); - info.type = ImportModInfo::download; - info.remote_source = argument; + info.action_type = ImportModInfo::download; + info.remote_request_url = argument; mod_import_queue_.push(info); } } @@ -175,7 +174,6 @@ void MainWindow::setupConnections() qRegisterMetaType>>(); qRegisterMetaType(); qRegisterMetaType(); - qRegisterMetaType(); qRegisterMetaType>(); qRegisterMetaType(); qRegisterMetaType>(); @@ -187,6 +185,7 @@ void MainWindow::setupConnections() qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); + qRegisterMetaType(); connect(this, &MainWindow::getModInfo, app_manager_, &ApplicationManager::getModInfo); @@ -380,8 +379,6 @@ void MainWindow::setupConnections() this, &MainWindow::onDownloadComplete); connect(app_manager_, &ApplicationManager::downloadFailed, this, &MainWindow::onDownloadFailed); - connect(this, &MainWindow::downloadModFile, - app_manager_, &ApplicationManager::downloadModFile); connect(this, &MainWindow::checkForModUpdates, app_manager_, &ApplicationManager::checkForModUpdates); connect(this, &MainWindow::checkModsForUpdates, @@ -853,51 +850,41 @@ void MainWindow::showEditDeployerDialog(int deployer) void MainWindow::importMod() { - auto info = mod_import_queue_.top(); + ImportModInfo info = mod_import_queue_.top(); setBusyStatus(true); - if(info.type == ImportModInfo::download) + if(info.action_type == ImportModInfo::download) { if(!initNexusApiKey()) { - mod_import_queue_.pop(); setBusyStatus(false); if(!mod_import_queue_.empty()) importMod(); return; } setStatusMessage("Downloading mod"); - if(info.mod_id != -1) - emit downloadModFile( - info.app_id, info.mod_id, info.nexus_file_id, info.remote_source.c_str()); - else - emit downloadMod(info.app_id, info.remote_source.c_str()); - return; + emit downloadMod(info); } - - if(std::filesystem::exists(info.target_path)) + else if(info.action_type == ImportModInfo::extract) { - try + if(std::filesystem::exists(info.target_path)) { - std::filesystem::remove_all(info.target_path); - } - catch(std::filesystem::filesystem_error& error) - { - onReceiveError( - "File system error", - std::format("Error while trying to delete '{}'", info.target_path.string()).c_str()); - setBusyStatus(false); - return; + try + { + std::filesystem::remove_all(info.target_path); + } + catch(std::filesystem::filesystem_error& error) + { + onReceiveError( + "File system error", + std::format("Error while trying to delete '{}'", info.target_path.string()).c_str()); + setBusyStatus(false); + return; + } } + Log::info("Importing mod '" + info.local_source.string() + "'"); + setStatusMessage("Importing mod"); + emit extractArchive(info); } - Log::info("Importing mod '" + info.local_source.string() + "'"); - setStatusMessage("Importing mod"); - emit extractArchive(info.app_id, - info.mod_id, - info.local_source.c_str(), - info.target_path.c_str(), - info.remote_source.c_str(), - info.version_overwrite.c_str(), - info.name_overwrite.c_str()); } void MainWindow::setBusyStatus(bool busy, bool show_progress_bar, bool disable_app_launch) @@ -1371,7 +1358,7 @@ void MainWindow::onModAdded(QList paths) { ImportModInfo info; info.app_id = currentApp(); - info.type = ImportModInfo::extract; + info.action_type = ImportModInfo::extract; info.local_source = url.path().toStdString(); info.target_path = ui->info_sdir_label->text().toStdString(); info.target_path /= temp_dir_.toStdString(); @@ -1381,7 +1368,7 @@ void MainWindow::onModAdded(QList paths) importMod(); } -void MainWindow::onAddModDialogAccept(int app_id, AddModInfo info) +void MainWindow::onAddModDialogAccept(int app_id, ImportModInfo info) { setBusyStatus(true); setStatusMessage(QString("Installing \"") + info.name.c_str() + "\""); @@ -1402,7 +1389,7 @@ void MainWindow::onGetModInfo(std::vector mod_info) mod_list_proxy_->updateRowCountLabel(); if(!is_initialized_ && !mod_import_queue_.empty()) { - auto info = mod_import_queue_.top(); + ImportModInfo info = mod_import_queue_.top(); mod_import_queue_.pop(); info.app_id = currentApp(); info.queue_time = std::chrono::high_resolution_clock::now(); @@ -1943,8 +1930,6 @@ void MainWindow::onModAddedToGroup(int mod_id, int target_id) void MainWindow::onAddModAborted(QString temp_dir) { - if(!mod_import_queue_.empty()) - mod_import_queue_.pop(); Log::info("Mod installation aborted"); bool abort = true; if(!mod_import_queue_.empty()) @@ -1996,24 +1981,16 @@ void MainWindow::onModMovedTo(int from, int to) emit getDeployerInfo(currentApp(), currentDeployer()); } -void MainWindow::onExtractionComplete(int app_id, - int mod_id, - bool success, - QString extracted_path, - QString local_source, - QString remote_source, - QString version, - QString name) +void MainWindow::onExtractionComplete(ImportModInfo info) { setBusyStatus(false); - if(!success) + mod_import_queue_.pop(); + if(!info.last_action_was_successful) { - if(!mod_import_queue_.empty()) - mod_import_queue_.pop(); if(!mod_import_queue_.empty()) importMod(); setStatusMessage("Import failed", 3000); - Log::error("Failed to import mod \"" + local_source.toStdString() + "\""); + Log::error("Failed to import mod \"" + info.local_source.string() + "\""); return; } setStatusMessage("Mod imported", 3000); @@ -2031,28 +2008,22 @@ void MainWindow::onExtractionComplete(int app_id, deployer_paths.append( ui->info_deployer_list->item(i, getColumnIndex(ui->info_deployer_list, "Target"))->text()); } - std::filesystem::path mod_path = local_source.toStdString(); - bool was_successful = add_mod_dialog_->setupDialog(mod_path.filename().c_str(), - deployers, + info.action_type = ImportModInfo::ActionType::install_dialog; + bool was_successful = add_mod_dialog_->setupDialog(deployers, deployer, - extracted_path, deployer_paths, - app_id, auto_deployers, app_info_.deployer_is_case_invariant, ui->info_version_label->text(), - local_source, - remote_source, - mod_id, - version, - name); + info); if(was_successful) { setBusyStatus(true, false); add_mod_dialog_->show(); } else - onReceiveError("Error", ("Failed to import mod from \"" + mod_path.string() + "\"").c_str()); + onReceiveError("Error", + ("Failed to import mod from \"" + info.local_source.string() + "\"").c_str()); } void MainWindow::onSettingsDialogComplete() @@ -3167,16 +3138,15 @@ void MainWindow::onReceiveIpcMessage(QString message) activateWindow(); if(message == "Started") return; - std::regex nxm_regex(R"(nxm:\/\/.*\mods\/\d+\/files\/\d+\?.*)"); - std::smatch match; + std::string message_str = message.toStdString(); - if(std::regex_match(message_str, match, nxm_regex)) + if(nexus::Api::nxmUrlIsValid(message_str)) { Log::debug("Received download request for \"" + message.toStdString() + "\"."); ImportModInfo info; info.app_id = currentApp(); - info.type = ImportModInfo::download; - info.remote_source = message.toStdString(); + info.action_type = ImportModInfo::download; + info.remote_request_url = message.toStdString(); mod_import_queue_.push(info); if(mod_import_queue_.size() == 1) importMod(); @@ -3185,16 +3155,11 @@ void MainWindow::onReceiveIpcMessage(QString message) Log::debug("Unknown IPC message: \"" + message.toStdString() + "\""); } -void MainWindow::onDownloadComplete(int app_id, int mod_id, QString file_path, QString mod_url) +void MainWindow::onDownloadComplete(ImportModInfo info) { onCompletedOperations("Download complete"); mod_import_queue_.pop(); - ImportModInfo info; - info.type = ImportModInfo::extract; - info.app_id = app_id; - info.mod_id = mod_id; - info.local_source = file_path.toStdString(); - info.remote_source = mod_url.toStdString(); + info.action_type = ImportModInfo::extract; info.target_path = ui->info_sdir_label->text().toStdString(); info.target_path /= temp_dir_.toStdString(); mod_import_queue_.push(info); @@ -3209,10 +3174,10 @@ void MainWindow::onModDownloadRequested(int app_id, { ImportModInfo info; info.app_id = app_id; - info.type = ImportModInfo::download; - info.nexus_file_id = file_id; + info.action_type = ImportModInfo::download; + info.remote_file_id = file_id; info.remote_source = mod_url.toStdString(); - info.mod_id = mod_id; + info.target_group_id = mod_id; info.version_overwrite = version.toStdString(); mod_import_queue_.push(info); if(mod_import_queue_.size() == 1) @@ -3248,10 +3213,10 @@ void MainWindow::on_actionReinstall_From_Local_triggered() ImportModInfo info; info.app_id = currentApp(); - info.type = ImportModInfo::extract; + info.action_type = ImportModInfo::extract; info.local_source = local_source; info.remote_source = remote_source; - info.mod_id = mod_id; + info.target_group_id = mod_id; info.target_path = ui->info_sdir_label->text().toStdString(); info.target_path /= temp_dir_.toStdString(); info.name_overwrite = name; @@ -3318,8 +3283,6 @@ void MainWindow::on_actionSuppress_Update_triggered() void MainWindow::onModInstallationComplete(bool success) { onCompletedOperations(success ? "Installation complete" : "Installation failed"); - if(!mod_import_queue_.empty()) - mod_import_queue_.pop(); if(!mod_import_queue_.empty()) importMod(); } diff --git a/src/ui/mainwindow.h b/src/ui/mainwindow.h index 4161407..c398040 100644 --- a/src/ui/mainwindow.h +++ b/src/ui/mainwindow.h @@ -485,7 +485,7 @@ public slots: * \param app_id Application for which the new mod is to be installed. * \param info Contains all data needed to install the mod. */ - void onAddModDialogAccept(int app_id, AddModInfo info); + void onAddModDialogAccept(int app_id, ImportModInfo info); /*! * \brief Called when a mod gets disabled/ enabled in ui->deployer_list. * Updates the mods state. @@ -631,23 +631,9 @@ public slots: void onModMovedTo(int from, int to); /*! * \brief Called after archive has been extracted. Installs the newly extracted mod. - * \param app_id Target app for the mod. - * \param mod_id Id of the mod for which the file is to be extracted or -1 if this is a new mod. - * \param success False when exception was thrown. - * \param extracted_path Path to which the mod was extracted. - * \param local_source Source archive for the mod. - * \param remote_source URL from where the mod was downloaded. - * \param version If not empty: Use this to overwrite the default version. - * \param name If not empty: Use this to overwrite the default name. + * \param info Contains data for the extracted mod. */ - void onExtractionComplete(int app_id, - int mod_id, - bool success, - QString extracted_path, - QString local_source, - QString remote_source, - QString version, - QString name); + void onExtractionComplete(ImportModInfo info); /*! \brief Called when the settings dialog has completed. Updates state with new settings. */ void onSettingsDialogComplete(); /*! @@ -980,13 +966,9 @@ private slots: void onReceiveIpcMessage(QString message); /*! * \brief Begins extraction of downloaded mod. - * \param app_id App for which the mod has been downloaded. - * \param mod_id If !=-1: The downloaded mod should be added to this mods group after - * installation. - * \param file_path Path to the downloaded file. - * \param mod_url Url from which the mod was downloaded. + * \param info Contains all relevant data for the extraction process. */ - void onDownloadComplete(int app_id, int mod_id, QString file_path, QString mod_url); + void onDownloadComplete(ImportModInfo info); /*! * \brief Downloads a mod from nexusmods using the given mod_url. Only works if the given * api key belongs to a premium user. @@ -1153,7 +1135,7 @@ signals: * \param app_id The target \ref ModdedApplication "application". * \param info Contains all data needed to install the mod. */ - void installMod(int app_id, AddModInfo info); + void installMod(int app_id, ImportModInfo info); /*! * \brief Uninstalls the given mods for one \ref ModdedApplication "application", this includes * deleting all installed files. @@ -1389,21 +1371,9 @@ signals: void sortModsByConflicts(int app_id, int deployer); /*! * \brief Extracts an archive to target directory. - * \param app_id \ref ModdedApplication "application" for which the mod has been extracted. - * \param mod_id Id of the mod for which the file is to be extracted or -1 if this is a new mod. - * \param source Source path. - * \param target Target path - * \param remote_source URL from where the mod was downloaded. - * \param version If not empty: Use this to overwrite the default version. - * \param name If not empty: Use this to overwrite the default name. + * \param info Contains all data needed to extract the mod archive. */ - void extractArchive(int app_id, - int mod_id, - QString source, - QString target, - QString remote_source, - QString version, - QString name); + void extractArchive(ImportModInfo info); /*! * \brief Requests info about backups for one ModdedApplication. * \param app_id Target app. @@ -1551,23 +1521,11 @@ signals: */ void getNexusPage(int app_id, int mod_id); /*! - * \brief Downloads a mod from nexusmods using the given nxm_url. - * \param app_id App for which the mod is to be downloaded. The mod is downloaded to the apps - * staging directory. - * \param nxm_url Url containing all information needed for the download. + * \brief Downloads a mod from remote using the given import info. + * \param info Contains either the ImportModInfo::remote_request_url or + * ImportModInfo::remote_mod_id and ImportModInfo::remote_file_id. */ - void downloadMod(int app_id, QString nxm_url); - /*! - * \brief Downloads a mod from nexusmods using the given mod_url. Only works if the given - * api key belongs to a premium user. - * \param app_id App for which the mod is to be downloaded. The mod is downloaded to the apps - * staging directory. - * \param mod_id Id of the mod for which the file is to be downloaded. This is the limo internal - * mod id, NOT the NexusMods id. - * \param file_id NexusMods file id of the target file. - * \param mod_url Url to the NexusMods page of the mod. - */ - void downloadModFile(int app_id, int mod_id, int file_id, QString mod_url); + void downloadMod(ImportModInfo info); /*! * \brief Checks for available mod updates on NexusMods. * \param app_id App for which mod updates are to be checked. diff --git a/tests/test_moddedapplication.cpp b/tests/test_moddedapplication.cpp index 20666b5..755bfcd 100644 --- a/tests/test_moddedapplication.cpp +++ b/tests/test_moddedapplication.cpp @@ -14,24 +14,26 @@ TEST_CASE("Mods are installed", "[app]") { resetStagingDir(); ModdedApplication app(DATA_DIR / "staging", "test"); - AddModInfo info{ - "mod 0", "1.0", Installer::SIMPLEINSTALLER, DATA_DIR / "source" / "mod0.tar.gz", {}, -1, - INSTALLER_FLAGS, 0 - }; + ImportModInfo info; + info.name = "mod 0"; + info.version = "1.0"; + info.installer = Installer::SIMPLEINSTALLER; + info.current_path = DATA_DIR / "source" / "mod0.tar.gz"; + info.installer_flags = INSTALLER_FLAGS; app.installMod(info); verifyDirsAreEqual(DATA_DIR / "staging" / "0", DATA_DIR / "source" / "0"); info.name = "mod 2"; - info.source_path = DATA_DIR / "source" / "mod2.tar.gz"; + info.current_path = DATA_DIR / "source" / "mod2.tar.gz"; app.installMod(info); verifyDirsAreEqual(DATA_DIR / "staging" / "1", DATA_DIR / "source" / "2"); info.name = "mod 1"; - info.source_path = DATA_DIR / "source" / "mod1.zip"; + info.current_path = DATA_DIR / "source" / "mod1.zip"; app.installMod(info); verifyDirsAreEqual(DATA_DIR / "staging" / "2", DATA_DIR / "source" / "1"); info.name = "mod 0->2"; - info.source_path = DATA_DIR / "source" / "mod2.tar.gz"; - info.group = 0; + info.current_path = DATA_DIR / "source" / "mod2.tar.gz"; + info.target_group_id = 0; info.replace_mod = true; app.installMod(info); verifyDirsAreEqual(DATA_DIR / "staging" / "0", DATA_DIR / "source" / "2"); @@ -46,20 +48,20 @@ TEST_CASE("Deployers are added", "[app]") resetAppDir(); ModdedApplication app(DATA_DIR / "staging", "test"); app.addDeployer({ DeployerFactory::SIMPLEDEPLOYER, "depl0", DATA_DIR / "app", Deployer::hard_link }); - AddModInfo info{ "mod 0", - "1.0", - Installer::SIMPLEINSTALLER, - DATA_DIR / "source" / "mod0.tar.gz", - { 0 }, - -1, - INSTALLER_FLAGS, - 0 }; + ImportModInfo info; + info.name = "mod 0"; + info.version = "1.0"; + info.installer = Installer::SIMPLEINSTALLER; + info.current_path = DATA_DIR / "source" / "mod0.tar.gz"; + info.deployers = {0}; + info.installer_flags = INSTALLER_FLAGS; + info.root_level = 0; app.installMod(info); info.name = "mod 1"; - info.source_path = DATA_DIR / "source" / "mod1.zip"; + info.current_path = DATA_DIR / "source" / "mod1.zip"; app.installMod(info); info.name = "mod 2"; - info.source_path = DATA_DIR / "source" / "mod2.tar.gz"; + info.current_path = DATA_DIR / "source" / "mod2.tar.gz"; app.installMod(info); app.deployMods(); verifyDirsAreEqual(DATA_DIR / "app", DATA_DIR / "target" / "mod012", true); @@ -75,20 +77,20 @@ TEST_CASE("State is saved", "[app]") app.addProfile(EditProfileInfo{ "test profile", "", -1 }); app.addTool({"t1", "", "command string"}); app.addTool({"t4", "", "/bin/prog.exe", true, 220, "/tmp", {{"VAR_1", "VAL_1"}}, "-arg", "-parg"}); - AddModInfo info{ "mod 0", - "1.0", - Installer::SIMPLEINSTALLER, - DATA_DIR / "source" / "mod0.tar.gz", - { 0 }, - -1, - INSTALLER_FLAGS, - 0 }; + ImportModInfo info; + info.name = "mod 0"; + info.version = "1.0"; + info.installer = Installer::SIMPLEINSTALLER; + info.current_path = DATA_DIR / "source" / "mod0.tar.gz"; + info.deployers = {0}; + info.installer_flags = INSTALLER_FLAGS; + info.root_level = 0; app.installMod(info); info.name = "mod 1"; - info.source_path = DATA_DIR / "source" / "mod1.zip"; + info.current_path = DATA_DIR / "source" / "mod1.zip"; app.installMod(info); info.name = "mod 2"; - info.source_path = DATA_DIR / "source" / "mod2.tar.gz"; + info.current_path = DATA_DIR / "source" / "mod2.tar.gz"; info.deployers = { 0, 1 }; app.installMod(info); @@ -117,18 +119,18 @@ TEST_CASE("Groups update loadorders", "[app]") ModdedApplication app(DATA_DIR / "staging", "test"); app.addDeployer({ DeployerFactory::SIMPLEDEPLOYER, "depl0", DATA_DIR / "app", Deployer::hard_link }); app.addDeployer({ DeployerFactory::SIMPLEDEPLOYER, "depl1", DATA_DIR / "app_2", Deployer::hard_link }); - AddModInfo info{ "mod 0", - "1.0", - Installer::SIMPLEINSTALLER, - DATA_DIR / "source" / "mod0.tar.gz", - { 0 }, - -1, - INSTALLER_FLAGS, - 0 }; + ImportModInfo info; + info.name = "mod 0"; + info.version = "1.0"; + info.installer = Installer::SIMPLEINSTALLER; + info.current_path = DATA_DIR / "source" / "mod0.tar.gz"; + info.deployers = {0}; + info.installer_flags = INSTALLER_FLAGS; + info.root_level = 0; app.installMod(info); info.name = "mod 1"; info.deployers = { 0, 1 }; - info.source_path = DATA_DIR / "source" / "mod1.zip"; + info.current_path = DATA_DIR / "source" / "mod1.zip"; app.installMod(info); app.createGroup(1, 0); REQUIRE_THAT(app.getLoadorder(0), Catch::Matchers::Equals(app.getLoadorder(1))); @@ -138,7 +140,7 @@ TEST_CASE("Groups update loadorders", "[app]") REQUIRE_THAT(app.getLoadorder(0), Catch::Matchers::Equals(std::vector>{ { 0, true } })); info.name = "mod 2"; - info.source_path = DATA_DIR / "source" / "mod2.tar.gz"; + info.current_path = DATA_DIR / "source" / "mod2.tar.gz"; app.installMod(info); REQUIRE_THAT( app.getLoadorder(0), @@ -174,14 +176,14 @@ TEST_CASE("Mods are split", "[app]") "depl2", DATA_DIR / "source" / "split" / "targets" / "d", Deployer::hard_link }); - AddModInfo info{ "mod 0", - "1.0", - Installer::SIMPLEINSTALLER, - DATA_DIR / "source" / "split" / "mod", - { 0 }, - -1, - INSTALLER_FLAGS, - 0 }; + ImportModInfo info; + info.name = "mod 0"; + info.version = "1.0"; + info.installer = Installer::SIMPLEINSTALLER; + info.current_path = DATA_DIR / "source" / "split" / "mod"; + info.deployers = {0}; + info.installer_flags = INSTALLER_FLAGS; + info.root_level = 0; app.installMod(info); sfs::remove(DATA_DIR / "staging" / "lmm_mods.json"); sfs::remove(DATA_DIR / "staging" / ".lmm_mods.json.bak"); @@ -194,22 +196,22 @@ TEST_CASE("Mods are uninstalled", "[app]") ModdedApplication app(DATA_DIR / "staging", "test"); app.addDeployer({ DeployerFactory::SIMPLEDEPLOYER, "depl0", DATA_DIR / "app", Deployer::hard_link }); app.addDeployer({ DeployerFactory::SIMPLEDEPLOYER, "depl1", DATA_DIR / "app_2", Deployer::hard_link }); - AddModInfo info{ "mod 0", - "1.0", - Installer::SIMPLEINSTALLER, - DATA_DIR / "source" / "mod0.tar.gz", - { 0 }, - -1, - INSTALLER_FLAGS, - 0 }; + ImportModInfo info; + info.name = "mod 0"; + info.version = "1.0"; + info.installer = Installer::SIMPLEINSTALLER; + info.current_path = DATA_DIR / "source" / "mod0.tar.gz"; + info.deployers = {0}; + info.installer_flags = INSTALLER_FLAGS; + info.root_level = 0; app.installMod(info); info.name = "mod 1"; - info.source_path = DATA_DIR / "source" / "mod1.zip"; + info.current_path = DATA_DIR / "source" / "mod1.zip"; app.installMod(info); info.name = "mod 2"; - info.source_path = DATA_DIR / "source" / "mod2.tar.gz"; + info.current_path = DATA_DIR / "source" / "mod2.tar.gz"; info.deployers = { 0, 1 }; - info.group = 1; + info.target_group_id = 1; app.installMod(info); app.uninstallMods({ 0, 2 }); auto mod_info = app.getModInfo(); @@ -224,13 +226,13 @@ TEST_CASE("Mods are uninstalled", "[app]") verifyDirsAreEqual(DATA_DIR / "staging", DATA_DIR / "target" / "remove" / "simple"); info.deployers = { 0 }; - info.group = -1; + info.target_group_id = -1; info.name = "mod 0"; - info.source_path = DATA_DIR / "source" / "mod0.tar.gz"; + info.current_path = DATA_DIR / "source" / "mod0.tar.gz"; app.installMod(info); info.name = "mod 2"; - info.source_path = DATA_DIR / "source" / "mod2.tar.gz"; - info.group = 1; + info.current_path = DATA_DIR / "source" / "mod2.tar.gz"; + info.target_group_id = 1; app.installMod(info); app.uninstallGroupMembers({ 1 }); mod_info = app.getModInfo();