refactor mod import

Improved mod matching on import.
Improved name and version detection on import.
This commit is contained in:
Limo
2025-04-12 16:07:02 +02:00
parent aa3933138f
commit bd821e823f
21 changed files with 772 additions and 752 deletions

View File

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

View File

@@ -1,42 +0,0 @@
/*!
* \file addmodinfo.h
* \brief Contains the AddModInfo struct.
*/
#pragma once
#include <filesystem>
#include <string>
#include <vector>
/*!
* \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<int> 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<std::pair<std::filesystem::path, std::filesystem::path>> 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 = "";
};

View File

@@ -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<std::chrono::high_resolution_clock> 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<std::pair<std::filesystem::path, std::filesystem::path>> 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<int> 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;
}
};

View File

@@ -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<ImportModInfo::RemoteType>(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<int>(remote_type);
return json;
}

View File

@@ -8,6 +8,7 @@
#include <filesystem>
#include <json/json.h>
#include <string>
#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.

View File

@@ -116,17 +116,17 @@ void ModdedApplication::unDeployModsFor(std::vector<int> 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<int>().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<int>{ mod_id });
@@ -372,15 +375,7 @@ std::vector<ModInfo> 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<int>& 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<void(float)> 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<std::string> 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<std::string> 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<float> weights_profiles;
std::vector<float> 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<int>{ info.group });
tag.updateMods(staging_dir_, std::vector<int>{ info.target_group_id });
updateAutoTagMap();
updateSettings(true);

View File

@@ -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<int>& 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<void(float)> 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<std::string> app_versions_;
/*! \brief Callback used to inform about the current task's progress. */
std::function<void(float)> 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_. */

View File

@@ -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<std::string>& deployer_names,
const std::vector<int>& deployer_ids,
const std::vector<bool>& statuses,
@@ -68,16 +51,8 @@ struct ModInfo
bool is_active_member,
const std::vector<std::string>& man_tags,
const std::vector<std::string>& 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)
{}

View File

@@ -1,5 +1,6 @@
#include "api.h"
#include "../parseerror.h"
#include <iostream>
#include <json/json.h>
#include <ranges>
#include <regex>
@@ -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<std::pair<std::string, int>> Api::extractDomainAndModId(const std:
return { { match[1], std::stoi(match[2]) } };
return {};
}
bool Api::initModInfo(ImportModInfo& info)
{
std::vector<File> 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<std::smatch> 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;
}

View File

@@ -7,8 +7,10 @@
#include "file.h"
#include "mod.h"
#include "../importmodinfo.h"
#include <cpr/cpr.h>
#include <string>
#include <regex>
/*!
@@ -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<std::pair<std::string, int>> 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<std::smatch> nxmUrlIsValid(const std::string& nxm_url);
private:
/*! \brief The API key used for all operations. */
inline static std::string api_key_ = "";
};
}

View File

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

View File

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

View File

@@ -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<bool>& autonomous_deployers,
const std::vector<bool>& 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<int> 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<int> 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<std::pair<sfs::path, sfs::path>> 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());
}

View File

@@ -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<bool>& autonomous_deployers,
const std::vector<bool>& 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<int> 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<QButtonGroup*> 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<bool> 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.

View File

@@ -1,6 +1,7 @@
#include "applicationmanager.h"
#include "../core/deployerfactory.h"
#include "../core/installer.h"
#include "../core/pathutils.h"
#include <QCoreApplication>
#include <QDebug>
#include <QMessageBox>
@@ -9,7 +10,107 @@
#include <regex>
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<std::string> 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<int> 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<std::string (*)(const std::string&, long)>(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<std::string (*)(const std::string&)>(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)

View File

@@ -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<typename Func, typename... Args>
auto handleExceptionsForFunction(Func&& f, Args&&... args)
-> std::optional<decltype((f)(std::forward<Args>(args)...))>
{
decltype((f)(std::forward<Args>(args)...)) ret_value;
std::string message;
bool has_thrown = false;
try
{
ret_value = (f)(std::forward<Args>(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<ModdedApplication> 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.

View File

@@ -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();
}

View File

@@ -5,7 +5,7 @@
#pragma once
#include "../core/addmodinfo.h"
#include "../core/importmodinfo.h"
#include "../core/fomod/fomodinstaller.h"
#include <QButtonGroup>
#include <QDialog>
@@ -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();
};

View File

@@ -50,7 +50,6 @@ Q_DECLARE_METATYPE(std::unordered_set<int>);
Q_DECLARE_METATYPE(QList<QList<QString>>);
Q_DECLARE_METATYPE(EditApplicationInfo);
Q_DECLARE_METATYPE(EditDeployerInfo);
Q_DECLARE_METATYPE(AddModInfo);
Q_DECLARE_METATYPE(std::vector<bool>);
Q_DECLARE_METATYPE(Log::LogLevel);
Q_DECLARE_METATYPE(std::vector<int>);
@@ -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<QList<QList<QString>>>();
qRegisterMetaType<EditApplicationInfo>();
qRegisterMetaType<EditDeployerInfo>();
qRegisterMetaType<AddModInfo>();
qRegisterMetaType<std::vector<bool>>();
qRegisterMetaType<Log::LogLevel>();
qRegisterMetaType<std::vector<int>>();
@@ -187,6 +185,7 @@ void MainWindow::setupConnections()
qRegisterMetaType<ExternalChangesInfo>();
qRegisterMetaType<FileChangeChoices>();
qRegisterMetaType<Tool>();
qRegisterMetaType<ImportModInfo>();
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<QUrl> 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<QUrl> 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<ModInfo> 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();
}

View File

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

View File

@@ -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<std::tuple<int, bool>>{ { 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();