mirror of
https://github.com/limo-app/limo.git
synced 2025-12-23 23:07:52 -05:00
add file pattern based root level detection
This commit is contained in:
@@ -174,6 +174,8 @@ set(CORE_SOURCES
|
||||
src/core/tool.h
|
||||
src/core/versionchangelog.cpp
|
||||
src/core/versionchangelog.h
|
||||
src/core/wildcardmatching.cpp
|
||||
src/core/wildcardmatching.h
|
||||
)
|
||||
|
||||
add_library(core OBJECT ${CORE_SOURCES})
|
||||
@@ -318,6 +320,8 @@ set(PROJECT_SOURCES
|
||||
src/ui/overwritebackupdialog.ui
|
||||
src/ui/passwordfield.cpp
|
||||
src/ui/passwordfield.h
|
||||
src/ui/rootlevelcondition.cpp
|
||||
src/ui/rootlevelcondition.h
|
||||
src/ui/settingsdialog.cpp
|
||||
src/ui/settingsdialog.h
|
||||
src/ui/settingsdialog.ui
|
||||
|
||||
@@ -78,4 +78,6 @@ struct AppInfo
|
||||
* \brief For every deployer: Whether or not it is case invariant.
|
||||
*/
|
||||
std::vector<bool> deployer_is_case_invariant{};
|
||||
/*! \brief Steam app id. Or -1 if not a Steam app. */
|
||||
long steam_app_id;
|
||||
};
|
||||
|
||||
@@ -163,7 +163,7 @@ bool Bg3Deployer::initPluginFile()
|
||||
{
|
||||
pugi::xml_node attr = node.find_child_by_attribute("id", "UUID");
|
||||
const std::string uuid = attr.attribute("value").value();
|
||||
if(uuid != Bg3Plugin::BG3_VANILLA_MOD_UUID)
|
||||
if(!Bg3Plugin::BG3_VANILLA_UUIDS.contains(uuid))
|
||||
plugins_.emplace_back(uuid, true);
|
||||
}
|
||||
|
||||
@@ -413,7 +413,7 @@ void Bg3Deployer::writePluginsPrivate() const
|
||||
{
|
||||
pugi::xml_node attr = mod.find_child_by_attribute("id", "UUID");
|
||||
const std::string uuid = attr.attribute("value").value();
|
||||
if(uuid != Bg3Plugin::BG3_VANILLA_MOD_UUID)
|
||||
if(!Bg3Plugin::BG3_VANILLA_UUIDS.contains(uuid))
|
||||
nodes_to_remove.push_back(mod);
|
||||
}
|
||||
for(const auto& node : nodes_to_remove)
|
||||
|
||||
@@ -38,7 +38,7 @@ Bg3Plugin::Bg3Plugin(const std::string& xml_string) : xml_string_(xml_string)
|
||||
dependency.find_child_by_attribute("id", "UUID").attribute("value").value();
|
||||
const std::string name =
|
||||
dependency.find_child_by_attribute("id", "Name").attribute("value").value();
|
||||
if(!uuid.empty() && uuid != BG3_VANILLA_MOD_UUID)
|
||||
if(!uuid.empty() && !BG3_VANILLA_UUIDS.contains(uuid))
|
||||
dependencies_.emplace_back(uuid, name);
|
||||
}
|
||||
}
|
||||
@@ -166,5 +166,5 @@ bool Bg3Plugin::isValidPlugin(const std::string& xml_string)
|
||||
if(uuid_node.empty())
|
||||
return false;
|
||||
const std::string uuid = uuid_node.attribute("value").value();
|
||||
return !uuid.empty() && uuid != BG3_VANILLA_MOD_UUID;
|
||||
return !uuid.empty() && !BG3_VANILLA_UUIDS.contains(uuid);
|
||||
}
|
||||
|
||||
@@ -99,8 +99,11 @@ public:
|
||||
*/
|
||||
static bool isValidPlugin(const std::string& xml_string);
|
||||
|
||||
/*! \brief UUID of the GustavDev plugin. */
|
||||
static constexpr char BG3_VANILLA_MOD_UUID[] = "28ac9ce2-2aba-8cda-b3b5-6e922f71b6b8";
|
||||
/*! \brief UUIDS of the Gustav plugins for different game versions. */
|
||||
static inline const std::set<std::string> BG3_VANILLA_UUIDS{
|
||||
"28ac9ce2-2aba-8cda-b3b5-6e922f71b6b8",
|
||||
"cb555efe-2d9e-131f-8195-a89329d218ea"
|
||||
};
|
||||
|
||||
private:
|
||||
/*! \brief Xml representation of this plugin. */
|
||||
|
||||
@@ -36,4 +36,6 @@ struct EditApplicationInfo
|
||||
std::string icon_path;
|
||||
/*! \brief Version of the app. This is used for FOMOD conditions. */
|
||||
std::string app_version;
|
||||
/*! \brief Steam app id of the added application. */
|
||||
long steam_app_id;
|
||||
};
|
||||
|
||||
@@ -209,13 +209,14 @@ void Installer::uninstall(const sfs::path& mod_path, const std::string& type)
|
||||
sfs::remove_all(mod_path);
|
||||
}
|
||||
|
||||
std::vector<sfs::path> Installer::getArchiveFileNames(const sfs::path& path)
|
||||
std::vector<std::pair<sfs::path, bool>> Installer::getArchiveFileNames(const sfs::path& path)
|
||||
{
|
||||
std::vector<sfs::path> file_names;
|
||||
|
||||
std::vector<std::pair<sfs::path, bool>> file_names;
|
||||
if(sfs::is_directory(path))
|
||||
{
|
||||
for(const auto& dir_entry : sfs::recursive_directory_iterator(path))
|
||||
file_names.push_back(pu::getRelativePath(dir_entry.path(), path));
|
||||
file_names.emplace_back(pu::getRelativePath(dir_entry.path(), path), sfs::is_directory(path));
|
||||
return file_names;
|
||||
}
|
||||
struct archive* source;
|
||||
@@ -226,7 +227,7 @@ std::vector<sfs::path> Installer::getArchiveFileNames(const sfs::path& path)
|
||||
if(archive_read_open_filename(source, path.string().c_str(), 10240) != ARCHIVE_OK)
|
||||
throw CompressionError("Could not open archive file.");
|
||||
while(archive_read_next_header(source, &entry) == ARCHIVE_OK)
|
||||
file_names.push_back(archive_entry_pathname(entry));
|
||||
file_names.emplace_back(archive_entry_pathname(entry), archive_entry_filetype(entry) == AE_IFDIR);
|
||||
if(archive_read_free(source) != ARCHIVE_OK)
|
||||
throw CompressionError("Parsing of archive failed.");
|
||||
return file_names;
|
||||
@@ -246,11 +247,11 @@ std::tuple<int, std::string, std::string> Installer::detectInstallerSignature(
|
||||
};
|
||||
const auto files = getArchiveFileNames(source);
|
||||
int max_length = 0;
|
||||
for(const auto& file : files)
|
||||
for(const auto& [file, _] : files)
|
||||
max_length = std::max(max_length, pu::getPathLength(file));
|
||||
for(int root_level = 0; root_level < max_length; root_level++)
|
||||
{
|
||||
for(const auto& file : files)
|
||||
for(const auto& [file, _] : files)
|
||||
{
|
||||
const auto [head, tail] = pu::removePathComponents(file, root_level);
|
||||
if(str_equals(path, tail))
|
||||
|
||||
@@ -101,9 +101,11 @@ public:
|
||||
/*!
|
||||
* \brief Recursively reads all file and directory names from given archive.
|
||||
* \param path Path to given archive.
|
||||
* \return Vector of paths within the archive.
|
||||
* \return Vector of paths within the archive and bools indicating whether that path points to
|
||||
* a directory.
|
||||
*/
|
||||
static std::vector<std::filesystem::path> getArchiveFileNames(const std::filesystem::path& path);
|
||||
static std::vector<std::pair<std::filesystem::path, bool>> getArchiveFileNames(
|
||||
const std::filesystem::path& path);
|
||||
/*!
|
||||
* \brief Identifies the appropriate installer type from given source archive or
|
||||
* directory.
|
||||
|
||||
@@ -252,6 +252,7 @@ void LootDeployer::sortModsByConflicts(std::optional<ProgressNode*> progress_nod
|
||||
log_(Log::LOG_WARNING, "LOOT: Requirement '" + file + "' not met for '" + plugin + "'");
|
||||
}
|
||||
}
|
||||
log_(Log::LOG_DEBUG, std::format("LOOT: App type {}", static_cast<int>(app_type_)));
|
||||
log_(Log::LOG_INFO,
|
||||
std::format("LOOT: Total Plugins: {}, Master: {}, Standard: {}, Light: {}",
|
||||
new_plugins.size(),
|
||||
|
||||
@@ -36,13 +36,13 @@ Mod::Mod(const Mod& other)
|
||||
|
||||
Mod::Mod(const Json::Value& json)
|
||||
{
|
||||
long remote_mod_id = -1;
|
||||
remote_mod_id = -1;
|
||||
if(json.isMember("remote_mod_id"))
|
||||
remote_mod_id = json["remote_mod_id"].asInt64();
|
||||
long remote_file_id = -1;
|
||||
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;
|
||||
remote_type = ImportModInfo::RemoteType::local;
|
||||
if(json.isMember("remote_type"))
|
||||
remote_type = static_cast<ImportModInfo::RemoteType>(json["remote_type"].asInt());
|
||||
id = json["id"].asInt();
|
||||
@@ -54,9 +54,6 @@ Mod::Mod(const Json::Value& json)
|
||||
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,7 +39,7 @@ struct Mod
|
||||
/*! \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;
|
||||
ImportModInfo::RemoteType remote_type = ImportModInfo::RemoteType::local;
|
||||
|
||||
/*!
|
||||
* \brief Constructor. Simply initializes members.
|
||||
|
||||
@@ -469,6 +469,7 @@ AppInfo ModdedApplication::getAppInfo() const
|
||||
info.command = command_;
|
||||
info.num_mods = installed_mods_.size();
|
||||
info.app_version = app_versions_[current_profile_];
|
||||
info.steam_app_id = steam_app_id_;
|
||||
for(const auto& deployer : deployers_)
|
||||
{
|
||||
info.deployers.push_back(deployer->getName());
|
||||
@@ -1655,6 +1656,8 @@ void ModdedApplication::updateSettings(bool write)
|
||||
json_settings_["auto_tags"][i] = auto_tags_[i].toJson();
|
||||
}
|
||||
|
||||
json_settings_["steam_app_id"] = steam_app_id_;
|
||||
|
||||
if(write)
|
||||
writeSettings();
|
||||
}
|
||||
@@ -1884,6 +1887,12 @@ void ModdedApplication::updateState(bool read)
|
||||
updateAutoTagMap();
|
||||
}
|
||||
|
||||
steam_app_id_ = -1;
|
||||
if(json_settings_.isMember("steam_app_id"))
|
||||
steam_app_id_ = json_settings_["steam_app_id"].asInt64();
|
||||
else
|
||||
updateSteamAppId();
|
||||
|
||||
updateSteamIconPath();
|
||||
}
|
||||
|
||||
@@ -2219,3 +2228,37 @@ void ModdedApplication::updateSteamIconPath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModdedApplication::updateSteamAppId()
|
||||
{
|
||||
if(steam_app_id_ != -1)
|
||||
return;
|
||||
|
||||
std::regex old_path_regex(R"(.*?/steam/appcache/librarycache/(\d+)_icon\.jpg)");
|
||||
std::smatch match;
|
||||
std::string path_str = icon_path_.string();
|
||||
if(std::regex_match(path_str, match, old_path_regex))
|
||||
{
|
||||
steam_app_id_ = std::stol(match[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
std::regex new_path_regex(R"(.*?/steam/appcache/librarycache/(\d+)/.*)");
|
||||
if(std::regex_match(path_str, match, new_path_regex))
|
||||
{
|
||||
steam_app_id_ = std::stol(match[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
std::regex steam_regex(R"(/steamapps/compatdata/(\d+))");
|
||||
for(const auto& depl : deployers_)
|
||||
{
|
||||
std::smatch match;
|
||||
std::string path = depl->getDestPath();
|
||||
if(std::regex_search(path, match, steam_regex))
|
||||
{
|
||||
steam_app_id_ = std::stol(match[1]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,6 +706,8 @@ private:
|
||||
std::function<void(float)> progress_callback_ = [](float f) {};
|
||||
/*! \brief File name used to store exported deployers and auto tags. */
|
||||
std::string export_file_name = "exported_config";
|
||||
/*! \brief Steam app id. Or -1 if not a Steam app. */
|
||||
long steam_app_id_;
|
||||
|
||||
/*!
|
||||
* \brief Updates json_settings_ with the current state of this object.
|
||||
@@ -771,4 +773,6 @@ private:
|
||||
std::string generalizeSteamPath(const std::string& path);
|
||||
/*! \brief If the icon path is a steam path: Update it to the new format. */
|
||||
void updateSteamIconPath();
|
||||
/*! \brief If steam_app_id_ == -1: Try to determine the app id. */
|
||||
void updateSteamAppId();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "tagconditionnode.h"
|
||||
#include "wildcardmatching.h"
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
@@ -113,7 +114,7 @@ bool TagConditionNode::evaluateWithoutInversion(
|
||||
target.end(),
|
||||
target.begin(),
|
||||
[](unsigned char c) { return std::tolower(c); });
|
||||
result = wildcardMatch(target);
|
||||
result = wildcardMatch(target, condition_);
|
||||
}
|
||||
if(result)
|
||||
break;
|
||||
@@ -273,51 +274,6 @@ void TagConditionNode::removeWhitespaces(std::string& expression) const
|
||||
}
|
||||
}
|
||||
|
||||
bool TagConditionNode::wildcardMatch(const std::string& target) const
|
||||
{
|
||||
if(condition_.empty())
|
||||
return false;
|
||||
if(condition_.find_first_not_of("*") == std::string::npos)
|
||||
return true;
|
||||
|
||||
auto condition_strings_ = splitString(condition_);
|
||||
if(condition_.front() != '*' && !target.starts_with(condition_strings_[0]) ||
|
||||
condition_.back() != '*' && !target.ends_with(condition_strings_.back()))
|
||||
return false;
|
||||
|
||||
size_t target_pos = 0;
|
||||
for(const auto& search_string : condition_strings_)
|
||||
{
|
||||
if(target_pos >= target.size())
|
||||
return false;
|
||||
target_pos = target.find(search_string, target_pos);
|
||||
if(target_pos == std::string::npos)
|
||||
return false;
|
||||
target_pos += search_string.size();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> TagConditionNode::splitString(const std::string& input) const
|
||||
{
|
||||
std::vector<std::string> splits;
|
||||
size_t pos = 0;
|
||||
size_t old_pos = 0;
|
||||
while(old_pos != input.size())
|
||||
{
|
||||
pos = input.find('*', old_pos);
|
||||
if(pos == std::string::npos)
|
||||
{
|
||||
splits.push_back(input.substr(old_pos));
|
||||
break;
|
||||
}
|
||||
if(pos - old_pos > 0)
|
||||
splits.push_back(input.substr(old_pos, pos - old_pos));
|
||||
old_pos = pos + 1;
|
||||
}
|
||||
return splits;
|
||||
}
|
||||
|
||||
bool TagConditionNode::operatorOrderIsValid(std::string expression)
|
||||
{
|
||||
constexpr int type_var = 0;
|
||||
|
||||
@@ -128,19 +128,6 @@ private:
|
||||
* \param expression Expression to modify.
|
||||
*/
|
||||
void removeWhitespaces(std::string& expression) const;
|
||||
/*!
|
||||
* \brief Checks if the given string matches this nodes condition_ string.
|
||||
* Uses * as a wildcard.
|
||||
* \param target String to compare to.
|
||||
* \return True if both match.
|
||||
*/
|
||||
bool wildcardMatch(const std::string& target) const;
|
||||
/*!
|
||||
* \brief Splits the given string into substrings seperated by the * wildcard.
|
||||
* \param input String to split.
|
||||
* \return All substrings without the * wildcard.
|
||||
*/
|
||||
std::vector<std::string> splitString(const std::string& input) const;
|
||||
/*!
|
||||
* \brief Checks if the order of operators in the given boolean expression is valid.
|
||||
* \param expression Expression to check.
|
||||
|
||||
47
src/core/wildcardmatching.cpp
Normal file
47
src/core/wildcardmatching.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "wildcardmatching.h"
|
||||
|
||||
|
||||
bool wildcardMatch(const std::string& target, const std::string& expression)
|
||||
{
|
||||
if(expression.empty())
|
||||
return false;
|
||||
if(expression.find_first_not_of("*") == std::string::npos)
|
||||
return true;
|
||||
|
||||
auto expressionstrings_ = splitString(expression);
|
||||
if(expression.front() != '*' && !target.starts_with(expressionstrings_[0]) ||
|
||||
expression.back() != '*' && !target.ends_with(expressionstrings_.back()))
|
||||
return false;
|
||||
|
||||
size_t target_pos = 0;
|
||||
for(const auto& search_string : expressionstrings_)
|
||||
{
|
||||
if(target_pos >= target.size())
|
||||
return false;
|
||||
target_pos = target.find(search_string, target_pos);
|
||||
if(target_pos == std::string::npos)
|
||||
return false;
|
||||
target_pos += search_string.size();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> splitString(const std::string& input)
|
||||
{
|
||||
std::vector<std::string> splits;
|
||||
size_t pos = 0;
|
||||
size_t old_pos = 0;
|
||||
while(old_pos != input.size())
|
||||
{
|
||||
pos = input.find('*', old_pos);
|
||||
if(pos == std::string::npos)
|
||||
{
|
||||
splits.push_back(input.substr(old_pos));
|
||||
break;
|
||||
}
|
||||
if(pos - old_pos > 0)
|
||||
splits.push_back(input.substr(old_pos, pos - old_pos));
|
||||
old_pos = pos + 1;
|
||||
}
|
||||
return splits;
|
||||
}
|
||||
24
src/core/wildcardmatching.h
Normal file
24
src/core/wildcardmatching.h
Normal file
@@ -0,0 +1,24 @@
|
||||
/*!
|
||||
* \file wildcardmatching.h
|
||||
* \brief Contains functions for wildcard matching strings.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/*!
|
||||
* \brief Checks if the given string matches the given expression string.
|
||||
* Uses * as a wildcard.
|
||||
* \param target String to compare to.
|
||||
* \param expression Wildcard expression string.
|
||||
* \return True if both match.
|
||||
*/
|
||||
bool wildcardMatch(const std::string& target, const std::string& expression);
|
||||
/*!
|
||||
* \brief Splits the given string into substrings seperated by the * wildcard.
|
||||
* \param input String to split.
|
||||
* \return All substrings without the * wildcard.
|
||||
*/
|
||||
std::vector<std::string> splitString(const std::string& input);
|
||||
@@ -120,7 +120,7 @@ void AddAppDialog::initConfigForApp()
|
||||
return;
|
||||
}
|
||||
|
||||
config_path /= (std::to_string(app_id_) + ".json");
|
||||
config_path /= (std::to_string(steam_app_id_) + ".json");
|
||||
if(!sfs::exists(config_path))
|
||||
{
|
||||
initDefaultAppConfig();
|
||||
@@ -153,7 +153,7 @@ void AddAppDialog::initConfigForApp()
|
||||
return;
|
||||
}
|
||||
|
||||
Log::debug(std::format("Reading app config for id {}", app_id_));
|
||||
Log::debug(std::format("Reading app config for id {}", steam_app_id_));
|
||||
try
|
||||
{
|
||||
for(int i = 0; i < json["deployers"].size(); i++)
|
||||
@@ -166,7 +166,7 @@ void AddAppDialog::initConfigForApp()
|
||||
if(deployer[key].isNull())
|
||||
{
|
||||
Log::debug(std::format(
|
||||
"App config for deployer {} for app {} does not contain key {}", i, app_id_, key));
|
||||
"App config for deployer {} for app {} does not contain key {}", i, steam_app_id_, key));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,7 @@ void AddAppDialog::initConfigForApp()
|
||||
if(str::find(DeployerFactory::DEPLOYER_TYPES, type) == DeployerFactory::DEPLOYER_TYPES.end())
|
||||
{
|
||||
Log::debug(std::format(
|
||||
"App config for deployer {} for app {} contains unknown type {}", i, app_id_, type));
|
||||
"App config for deployer {} for app {} contains unknown type {}", i, steam_app_id_, type));
|
||||
continue;
|
||||
}
|
||||
info.type = type;
|
||||
@@ -190,7 +190,7 @@ void AddAppDialog::initConfigForApp()
|
||||
{
|
||||
Log::debug(std::format("App config for deployer {} for app {} contains invalid target {}",
|
||||
i,
|
||||
app_id_,
|
||||
steam_app_id_,
|
||||
target_dir));
|
||||
continue;
|
||||
}
|
||||
@@ -208,7 +208,7 @@ void AddAppDialog::initConfigForApp()
|
||||
{
|
||||
Log::debug(std::format("App config for deployer {} for app {} contains invalid mode {}",
|
||||
i,
|
||||
app_id_,
|
||||
steam_app_id_,
|
||||
deploy_mode.toStdString()));
|
||||
continue;
|
||||
}
|
||||
@@ -225,7 +225,7 @@ void AddAppDialog::initConfigForApp()
|
||||
{
|
||||
Log::debug(std::format("App config for deployer {} for app {} contains invalid source {}",
|
||||
i,
|
||||
app_id_,
|
||||
steam_app_id_,
|
||||
source_dir));
|
||||
continue;
|
||||
}
|
||||
@@ -248,12 +248,12 @@ void AddAppDialog::initConfigForApp()
|
||||
catch(const ParseError& e)
|
||||
{
|
||||
Log::debug(std::format(
|
||||
"Failed to read auto tag {} for app with id {}.\nError: {}", i, app_id_, e.what()));
|
||||
"Failed to read auto tag {} for app with id {}.\nError: {}", i, steam_app_id_, e.what()));
|
||||
continue;
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
Log::debug(std::format("Failed to read auto tag {} for app with id {}", i, app_id_));
|
||||
Log::debug(std::format("Failed to read auto tag {} for app with id {}", i, steam_app_id_));
|
||||
continue;
|
||||
}
|
||||
auto_tags_.push_back(json[JSON_AUTO_TAGS_GROUP][i]);
|
||||
@@ -285,7 +285,7 @@ void AddAppDialog::initDefaultAppConfig()
|
||||
deployers_.clear();
|
||||
auto_tags_.clear();
|
||||
|
||||
Log::debug(std::format("Using default config for app {}", app_id_));
|
||||
Log::debug(std::format("Using default config for app {}", steam_app_id_));
|
||||
deployers_.emplace_back(DeployerFactory::CASEMATCHINGDEPLOYER,
|
||||
"Install",
|
||||
steam_install_path_.toStdString(),
|
||||
@@ -303,7 +303,8 @@ void AddAppDialog::setEditMode(const QString& name,
|
||||
const QString& path,
|
||||
const QString& command,
|
||||
const QString& icon_path,
|
||||
int app_id)
|
||||
int app_id,
|
||||
long steam_app_id)
|
||||
{
|
||||
deployers_.clear();
|
||||
auto_tags_.clear();
|
||||
@@ -318,6 +319,7 @@ void AddAppDialog::setEditMode(const QString& name,
|
||||
path_ = path;
|
||||
command_ = command;
|
||||
app_id_ = app_id;
|
||||
steam_app_id_ = steam_app_id;
|
||||
enableOkButton(true);
|
||||
edit_mode_ = true;
|
||||
ui->move_dir_box->setVisible(true);
|
||||
@@ -368,6 +370,7 @@ void AddAppDialog::on_buttonBox_accepted()
|
||||
info.staging_dir = ui->path_field->text().toStdString();
|
||||
info.command = ui->command_field->text().toStdString();
|
||||
info.icon_path = ui->icon_field->text().toStdString();
|
||||
info.steam_app_id = steam_app_id_;
|
||||
if(edit_mode_)
|
||||
{
|
||||
info.move_staging_dir = ui->move_dir_box->checkState() == Qt::Checked;
|
||||
@@ -397,7 +400,7 @@ void AddAppDialog::onApplicationImported(QString name,
|
||||
{
|
||||
ui->name_field->setText(name);
|
||||
ui->command_field->setText("xdg-open steam://rungameid/" + app_id);
|
||||
app_id_ = app_id.toInt();
|
||||
steam_app_id_ = app_id.toLong();
|
||||
steam_install_path_ = install_dir;
|
||||
steam_prefix_path_ = prefix_path;
|
||||
ui->icon_field->setText(icon_path);
|
||||
|
||||
@@ -79,6 +79,8 @@ private:
|
||||
QString command_;
|
||||
/*! \brief Id of the edited \ref ModdedApplication "application". */
|
||||
int app_id_;
|
||||
/*! \brief Steam app id of the new application, or -1 if not a steam app. */
|
||||
long steam_app_id_;
|
||||
/*! \brief Path to imported steam applications installation directory. */
|
||||
QString steam_install_path_ = "";
|
||||
/*! \brief Path to imported steam applications prefix directory. */
|
||||
@@ -127,13 +129,15 @@ public:
|
||||
* \ref ModdedApplication "application".
|
||||
* \param command Current command to run the edited \ref ModdedApplication "application".
|
||||
* \param app_id Id of the edited \ref ModdedApplication "application".
|
||||
* \param steam_app_id Steam app id. Or -1 if not a Steam app.
|
||||
*/
|
||||
void setEditMode(const QString& name,
|
||||
const QString& app_version,
|
||||
const QString& path,
|
||||
const QString& command,
|
||||
const QString& icon_path,
|
||||
int app_id);
|
||||
int app_id,
|
||||
long steam_app_id);
|
||||
/*!
|
||||
* \brief Initializes this dialog to allow creating a new
|
||||
* \ref ModdedApplication "application".
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "../core/log.h"
|
||||
#include "../core/pathutils.h"
|
||||
#include "fomoddialog.h"
|
||||
#include "qdebug.h"
|
||||
#include "ui_addmoddialog.h"
|
||||
#include <QGroupBox>
|
||||
#include <QMessageBox>
|
||||
@@ -78,7 +77,6 @@ void AddModDialog::updateOkButton()
|
||||
|
||||
void AddModDialog::colorTreeNodes(QTreeWidgetItem* node, int cur_depth, int root_level)
|
||||
{
|
||||
qDebug() << node->text(0);
|
||||
if(cur_depth < root_level)
|
||||
{
|
||||
node->setForeground(0, COLOR_REMOVE_);
|
||||
@@ -103,7 +101,6 @@ int AddModDialog::detectRootLevel(int deployer) const
|
||||
{
|
||||
bool is_case_invariant = case_invariant_deployers_[deployer];
|
||||
sfs::path deployer_path = deployer_paths_[deployer].toStdString();
|
||||
|
||||
auto cur_item = ui->content_tree->invisibleRootItem();
|
||||
if(cur_item->childCount() != 1)
|
||||
return 0;
|
||||
@@ -145,13 +142,38 @@ int AddModDialog::detectRootLevel(int deployer) const
|
||||
return 0;
|
||||
}
|
||||
|
||||
int AddModDialog::computeFomodBoxIndexFromDeployer(int index) const
|
||||
{
|
||||
int fomod_index = -1;
|
||||
for(int i = 0; i < index; i++)
|
||||
{
|
||||
if(!autonomous_deployers_[i])
|
||||
fomod_index++;
|
||||
}
|
||||
return fomod_index;
|
||||
}
|
||||
|
||||
int AddModDialog::computeDeployerFromFomodBoxIndex(int index) const
|
||||
{
|
||||
int deployer = -1;
|
||||
int fomod_index = -1;
|
||||
for(bool is_autonomous : autonomous_deployers_)
|
||||
{
|
||||
deployer++;
|
||||
if(!is_autonomous && ++fomod_index == index)
|
||||
break;
|
||||
}
|
||||
return deployer;
|
||||
}
|
||||
|
||||
bool AddModDialog::setupDialog(const QStringList& deployers,
|
||||
int cur_deployer,
|
||||
const QStringList& deployer_paths,
|
||||
const std::vector<bool>& autonomous_deployers,
|
||||
const std::vector<bool>& case_invariant_deployers,
|
||||
const QString& app_version,
|
||||
const ImportModInfo& info)
|
||||
const ImportModInfo& info,
|
||||
const std::vector<RootLevelCondition>& root_level_conditions)
|
||||
{
|
||||
groups_.clear();
|
||||
const auto& mod_infos = mod_list_model_->getModInfo();
|
||||
@@ -181,6 +203,8 @@ bool AddModDialog::setupDialog(const QStringList& deployers,
|
||||
ui->group_field->setCompleter(completer_.get());
|
||||
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
|
||||
ui->group_check->setCheckState(Qt::Unchecked);
|
||||
autonomous_deployers_ = autonomous_deployers;
|
||||
|
||||
int mod_index = -1;
|
||||
if(info.target_group_id != -1)
|
||||
{
|
||||
@@ -246,8 +270,7 @@ bool AddModDialog::setupDialog(const QStringList& deployers,
|
||||
}
|
||||
if(detected_type == Installer::FOMODINSTALLER)
|
||||
{
|
||||
auto [name, version] =
|
||||
fomod::FomodInstaller::getMetaData(info.current_path / prefix);
|
||||
auto [name, version] = fomod::FomodInstaller::getMetaData(info.current_path / prefix);
|
||||
if(!name.empty() && info.name_overwrite.empty())
|
||||
ui->name_text->setText(name.c_str());
|
||||
if(!version.empty() && info.version_overwrite.empty())
|
||||
@@ -324,11 +347,12 @@ bool AddModDialog::setupDialog(const QStringList& deployers,
|
||||
try
|
||||
{
|
||||
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);
|
||||
ui->root_level_box->setMaximum(std::max(max_depth - 1, 0));
|
||||
ui->root_level_box->setValue(root_level);
|
||||
directory_tree_depth_ = 0;
|
||||
for(const auto& [path, is_directory] : mod_file_paths)
|
||||
directory_tree_depth_ =
|
||||
std::max(addTreeNode(ui->content_tree, path, is_directory), directory_tree_depth_);
|
||||
ui->root_level_box->setMaximum(std::max(directory_tree_depth_ - 1, 0));
|
||||
ui->root_level_box->setValue(std::min(std::max(0, root_level), directory_tree_depth_ - 1));
|
||||
}
|
||||
catch(std::runtime_error& error)
|
||||
{
|
||||
@@ -337,8 +361,19 @@ bool AddModDialog::setupDialog(const QStringList& deployers,
|
||||
return false;
|
||||
}
|
||||
|
||||
ui->fomod_deployer_box->clear();
|
||||
bool root_level_checked = false;
|
||||
for(const auto& condition : root_level_conditions)
|
||||
{
|
||||
if(auto level = condition.detectRootLevel(ui->content_tree->invisibleRootItem());
|
||||
level && *level >= 0)
|
||||
{
|
||||
root_level_checked = true;
|
||||
ui->root_level_box->setValue(std::min(std::max(0, *level), directory_tree_depth_ - 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ui->fomod_deployer_box->clear();
|
||||
for(int i = 0; i < deployers.size(); i++)
|
||||
{
|
||||
const bool is_target = selected_deployers.contains(i) | (i == cur_deployer);
|
||||
@@ -349,19 +384,19 @@ bool AddModDialog::setupDialog(const QStringList& deployers,
|
||||
{
|
||||
root_level_checked = true;
|
||||
root_level = detectRootLevel(i);
|
||||
ui->root_level_box->setValue(root_level);
|
||||
ui->root_level_box->setValue(std::min(std::max(0, root_level), directory_tree_depth_ - 1));
|
||||
}
|
||||
}
|
||||
auto item = new QListWidgetItem(deployers[i], ui->deployer_list);
|
||||
item->setCheckState(is_target ? Qt::Checked : Qt::Unchecked);
|
||||
item->setHidden(autonomous_deployers[i]);
|
||||
}
|
||||
|
||||
int fomod_target_deployer = settings.value("fomod_target_deployer", -1).toInt();
|
||||
if(fomod_target_deployer >= 0 && fomod_target_deployer < ui->fomod_deployer_box->count())
|
||||
ui->fomod_deployer_box->setCurrentIndex(fomod_target_deployer);
|
||||
else if(cur_deployer >= 0 && cur_deployer < ui->fomod_deployer_box->count())
|
||||
ui->fomod_deployer_box->setCurrentIndex(cur_deployer);
|
||||
else if(int depl_index = computeFomodBoxIndexFromDeployer(cur_deployer);
|
||||
depl_index >= 0 && depl_index < ui->fomod_deployer_box->count())
|
||||
ui->fomod_deployer_box->setCurrentIndex(depl_index);
|
||||
settings.endGroup();
|
||||
|
||||
dialog_completed_ = false;
|
||||
@@ -416,7 +451,7 @@ int AddModDialog::addTreeNode(QTreeWidgetItem* parent, const sfs::path& cur_path
|
||||
return addTreeNode(child, removeRoot(cur_path)) + 1;
|
||||
}
|
||||
|
||||
int AddModDialog::addTreeNode(QTreeWidget* tree, const sfs::path& cur_path)
|
||||
int AddModDialog::addTreeNode(QTreeWidget* tree, const sfs::path& cur_path, bool is_directory)
|
||||
{
|
||||
if(cur_path.empty())
|
||||
return 0;
|
||||
@@ -430,6 +465,7 @@ int AddModDialog::addTreeNode(QTreeWidget* tree, const sfs::path& cur_path)
|
||||
auto item = new QTreeWidgetItem(tree);
|
||||
item->setText(0, cur_text);
|
||||
item->setForeground(0, COLOR_KEEP_);
|
||||
item->setData(0, Qt::UserRole, is_directory);
|
||||
return addTreeNode(item, removeRoot(cur_path)) + 1;
|
||||
}
|
||||
|
||||
@@ -486,7 +522,8 @@ void AddModDialog::on_buttonBox_accepted()
|
||||
}
|
||||
fomod_dialog_->setupDialog(
|
||||
import_mod_info_.current_path / path_prefix_.toStdString(),
|
||||
deployer_paths_[ui->fomod_deployer_box->currentIndex()].toStdString(),
|
||||
deployer_paths_[computeDeployerFromFomodBoxIndex(ui->fomod_deployer_box->currentIndex())]
|
||||
.toStdString(),
|
||||
app_version_,
|
||||
import_mod_info_,
|
||||
import_mod_info_.app_id,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "../core/importmodinfo.h"
|
||||
#include "deployerlistmodel.h"
|
||||
#include "modlistmodel.h"
|
||||
#include "rootlevelcondition.h"
|
||||
#include "ui/fomoddialog.h"
|
||||
#include <QButtonGroup>
|
||||
#include <QCompleter>
|
||||
@@ -47,12 +48,13 @@ public:
|
||||
* \brief Initializes this dialog with data needed for mod installation.
|
||||
* \param deployers Contains all available \ref Deployer "deployers".
|
||||
* \param cur_deployer The currently active Deployer.
|
||||
* \param deployer_paths Contains target paths for all non autonomous deployers.
|
||||
* \param deployer_paths Contains target paths for all deployers.
|
||||
* \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 info Contains data relating to the current status of the mod import.
|
||||
* \param root_level_conditions Contains all root level conditions for the current app.
|
||||
* \return True if dialog creation was successful.
|
||||
*/
|
||||
bool setupDialog(const QStringList& deployers,
|
||||
@@ -61,7 +63,8 @@ public:
|
||||
const std::vector<bool>& autonomous_deployers,
|
||||
const std::vector<bool>& case_invariant_deployers,
|
||||
const QString& app_version,
|
||||
const ImportModInfo& info);
|
||||
const ImportModInfo& info,
|
||||
const std::vector<RootLevelCondition>& root_level_conditions);
|
||||
/*!
|
||||
* \brief Closes the dialog and emits a signal indicating installation has been canceled.
|
||||
* \param event The close even sent upon closing the dialog.
|
||||
@@ -105,6 +108,10 @@ private:
|
||||
std::vector<bool> case_invariant_deployers_;
|
||||
/*! \brief Contains all data related to the current state of the mod installation. */
|
||||
ImportModInfo import_mod_info_;
|
||||
/*! \brief Depth of the current directory tree. */
|
||||
int directory_tree_depth_;
|
||||
/*! \brief Contains bools for every deployer indicating whether that deployer is autonomous. */
|
||||
std::vector<bool> autonomous_deployers_;
|
||||
|
||||
/*!
|
||||
* \brief Updates the enabled state of this dialog's OK button to only be enabled when
|
||||
@@ -117,8 +124,9 @@ private:
|
||||
* Then adds all subsequent path components as children to the new node.
|
||||
* \param tree Target QTreeWidget.
|
||||
* \param cur_path Source path.
|
||||
* \param is_directory If True: Given path points to a directory.
|
||||
*/
|
||||
int addTreeNode(QTreeWidget* tree, const std::filesystem::path& cur_path);
|
||||
int addTreeNode(QTreeWidget* tree, const std::filesystem::path& cur_path, bool is_directory);
|
||||
/*!
|
||||
* \brief Adds the root path element of given path as a root node to the given parent node.
|
||||
* Then adds all subsequent path components as children to the new node.
|
||||
@@ -154,6 +162,18 @@ private:
|
||||
* \return The detected root level.
|
||||
*/
|
||||
int detectRootLevel(int deployer) const;
|
||||
/*!
|
||||
* \brief Computes the index in the fomod deployer combo box from the given deployer index.
|
||||
* \param index Deployer index.
|
||||
* \return Fomod deployer box index.
|
||||
*/
|
||||
int computeFomodBoxIndexFromDeployer(int index) const;
|
||||
/*!
|
||||
* \brief Computes the deployer index from the given index in the fomod deployer combo box.
|
||||
* \param index Fomod deployer box index.
|
||||
* \return Deployer index.
|
||||
*/
|
||||
int computeDeployerFromFomodBoxIndex(int index) const;
|
||||
|
||||
private slots:
|
||||
/*! \brief Closes the dialog and emits a signal for completion. */
|
||||
|
||||
@@ -12,6 +12,7 @@ IpcClient::~IpcClient()
|
||||
bool IpcClient::connect()
|
||||
{
|
||||
socket_->connectToServer(IpcServer::server_name);
|
||||
socket_->waitForConnected(50);
|
||||
return socket_->state() == QLocalSocket::ConnectedState;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
namespace str = std::ranges;
|
||||
namespace stv = std::views;
|
||||
namespace sfs = std::filesystem;
|
||||
|
||||
|
||||
Q_DECLARE_METATYPE(std::vector<ModInfo>);
|
||||
@@ -856,6 +857,7 @@ void MainWindow::importMod()
|
||||
{
|
||||
if(!initNexusApiKey())
|
||||
{
|
||||
mod_import_queue_.pop();
|
||||
setBusyStatus(false);
|
||||
if(!mod_import_queue_.empty())
|
||||
importMod();
|
||||
@@ -1351,6 +1353,82 @@ bool MainWindow::versionIsLessOrEqual(QString current_version, QString target_ve
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::initRootLevelConditions()
|
||||
{
|
||||
root_level_conditions_.clear();
|
||||
if(app_info_.steam_app_id == -1)
|
||||
return;
|
||||
|
||||
sfs::path config_path =
|
||||
sfs::path(is_a_flatpak_ ? "/app" : APP_INSTALL_PREFIX) / "share/limo/steam_app_configs";
|
||||
// Overwrite for local build
|
||||
if(!is_a_flatpak_ && sfs::exists("steam_app_configs"))
|
||||
config_path = "steam_app_configs";
|
||||
Log::debug("Config path: " + config_path.string());
|
||||
if(!sfs::exists(config_path))
|
||||
{
|
||||
Log::debug(
|
||||
"Could not find \"steam_app_configs\" directory. " "Make sure Limo is installed correctly");
|
||||
return;
|
||||
}
|
||||
|
||||
config_path /= (std::to_string(app_info_.steam_app_id) + ".json");
|
||||
if(!sfs::exists(config_path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Json::Value json;
|
||||
std::ifstream file(config_path, std::fstream::binary);
|
||||
if(!file.is_open())
|
||||
{
|
||||
Log::debug("Failed to open app settings file at: " + config_path.string());
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
file >> json;
|
||||
}
|
||||
catch(Json::Exception& e)
|
||||
{
|
||||
Log::debug("Failed to read from app settings file at: " + config_path.string() +
|
||||
". Error was: " + e.what());
|
||||
return;
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
Log::debug("Failed to read from app settings file at: " + config_path.string());
|
||||
return;
|
||||
}
|
||||
|
||||
if(!json.isMember(JSON_ROOT_LEVEL_KEY))
|
||||
return;
|
||||
for(int i = 0; i < json[JSON_ROOT_LEVEL_KEY].size(); i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
root_level_conditions_.emplace_back(json[JSON_ROOT_LEVEL_KEY][i]);
|
||||
}
|
||||
catch(std::runtime_error& e)
|
||||
{
|
||||
Log::debug(std::format("Failed to parse root level config for app {}. \nError: {}",
|
||||
app_info_.steam_app_id,
|
||||
e.what()));
|
||||
}
|
||||
catch(Json::Exception& e)
|
||||
{
|
||||
Log::debug(std::format("Failed to parse root level config for app {}. \nError: {}",
|
||||
app_info_.steam_app_id,
|
||||
e.what()));
|
||||
}
|
||||
catch(...)
|
||||
{
|
||||
Log::debug(std::format("Unknown error while parsing root level config for app {}.",
|
||||
app_info_.steam_app_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onModAdded(QList<QUrl> paths)
|
||||
{
|
||||
const bool was_empty = mod_import_queue_.empty();
|
||||
@@ -1518,10 +1596,7 @@ void MainWindow::onGetApplicationNames(QStringList names, QStringList icon_paths
|
||||
if(icon_paths[i] == "")
|
||||
ui->app_selection_box->addItem(names[i]);
|
||||
else
|
||||
{
|
||||
ui->app_selection_box->addItem(QIcon(icon_paths[i]), names[i]);
|
||||
qDebug() << icon_paths[i];
|
||||
}
|
||||
ui->app_selection_box->setItemData(
|
||||
ui->app_selection_box->count() - 1, icon_paths[i], Qt::UserRole);
|
||||
}
|
||||
@@ -1825,6 +1900,8 @@ void MainWindow::onGetAppInfo(AppInfo app_info)
|
||||
connect(button, &QPushButton::clicked, this, &MainWindow::onAddToolClicked);
|
||||
ui->info_tool_list->setCellWidget(ui->info_tool_list->rowCount() - 1, 0, button);
|
||||
ignore_tool_changes_ = false;
|
||||
|
||||
initRootLevelConditions();
|
||||
}
|
||||
|
||||
void MainWindow::onApplicationEdited(EditApplicationInfo info, int app_id)
|
||||
@@ -2002,12 +2079,8 @@ void MainWindow::onExtractionComplete(ImportModInfo info)
|
||||
ui->app_tab_widget->currentIndex() == 2 ? ui->deployer_selection_box->currentIndex() : -1;
|
||||
const std::vector<bool> auto_deployers = getAutonomousDeployers();
|
||||
QStringList deployer_paths;
|
||||
for(int i = 0; i < ui->info_deployer_list->rowCount(); i++)
|
||||
{
|
||||
if(!auto_deployers[i])
|
||||
deployer_paths.append(
|
||||
ui->info_deployer_list->item(i, getColumnIndex(ui->info_deployer_list, "Target"))->text());
|
||||
}
|
||||
for(const auto& path : app_info_.target_dirs)
|
||||
deployer_paths.append(path.c_str());
|
||||
info.action_type = ImportModInfo::ActionType::install_dialog;
|
||||
bool was_successful = add_mod_dialog_->setupDialog(deployers,
|
||||
deployer,
|
||||
@@ -2015,7 +2088,8 @@ void MainWindow::onExtractionComplete(ImportModInfo info)
|
||||
auto_deployers,
|
||||
app_info_.deployer_is_case_invariant,
|
||||
ui->info_version_label->text(),
|
||||
info);
|
||||
info,
|
||||
root_level_conditions_);
|
||||
if(was_successful)
|
||||
{
|
||||
setBusyStatus(true, false);
|
||||
@@ -2404,7 +2478,8 @@ void MainWindow::on_edit_app_button_clicked()
|
||||
ui->info_sdir_label->text(),
|
||||
ui->info_command_label->text(),
|
||||
ui->app_selection_box->currentData(Qt::UserRole).toString(),
|
||||
currentApp());
|
||||
currentApp(),
|
||||
app_info_.steam_app_id);
|
||||
setBusyStatus(true, false);
|
||||
add_app_dialog_->show();
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "ui/externalchangesdialog.h"
|
||||
#include "ui/ipcserver.h"
|
||||
#include "ui/listaction.h"
|
||||
#include "ui/rootlevelcondition.h"
|
||||
#include "ui/tagcheckbox.h"
|
||||
#include "versionboxdelegate.h"
|
||||
#include <QCloseEvent>
|
||||
@@ -126,6 +127,8 @@ private:
|
||||
static inline const QString deploy_mode_sym_link = "Sym Link";
|
||||
/*! \brief Display string for copy deployment. */
|
||||
static inline const QString deploy_mode_copy = "Copy";
|
||||
/*! \brief JSON key for the root level conditions in the per steam app config file. */
|
||||
static inline constexpr char JSON_ROOT_LEVEL_KEY[] = "root_level_conditions";
|
||||
/*! \brief True if the button used to reorder load orders is being pressed. */
|
||||
bool move_button_pressed_ = false;
|
||||
/*! \brief Stores the row containing the currently held down move button for load orders. */
|
||||
@@ -319,6 +322,8 @@ private:
|
||||
QString previous_app_version_;
|
||||
/*! \brief Contains data about the currently active application. */
|
||||
AppInfo app_info_;
|
||||
/*! \brief Used to detect root levels during mod installation for the current app. */
|
||||
std::vector<RootLevelCondition> root_level_conditions_;
|
||||
|
||||
/*! \brief Creates signal/ slot connections between this and the ApplicationManager. */
|
||||
void setupConnections();
|
||||
@@ -472,6 +477,8 @@ private:
|
||||
* is invalid, true if current_version is invalid.
|
||||
*/
|
||||
bool versionIsLessOrEqual(QString current_version, QString target_version);
|
||||
/*! \brief Initializes the root level conditions for the current app. */
|
||||
void initRootLevelConditions();
|
||||
|
||||
public slots:
|
||||
/*!
|
||||
|
||||
101
src/ui/rootlevelcondition.cpp
Normal file
101
src/ui/rootlevelcondition.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "rootlevelcondition.h"
|
||||
#include "../core/wildcardmatching.h"
|
||||
#include <queue>
|
||||
#include <regex>
|
||||
|
||||
|
||||
RootLevelCondition::RootLevelCondition(MatcherType matcher_type,
|
||||
TargetType target_type,
|
||||
bool case_invariant,
|
||||
bool stop_on_branch,
|
||||
const std::string& expression,
|
||||
int level_offset) :
|
||||
matcher_type_(matcher_type), target_type_(target_type), case_invariant_matching_(case_invariant),
|
||||
stop_on_branch_(stop_on_branch), expression_(expression), level_offset_(level_offset)
|
||||
{}
|
||||
|
||||
RootLevelCondition::RootLevelCondition(const Json::Value& json)
|
||||
{
|
||||
const std::vector<std::string> required_keys = { JSON_MATCHER_TYPE_KEY,
|
||||
JSON_EXPRESSION_KEY,
|
||||
JSON_TARGET_TYPE_KEY };
|
||||
for(const auto& key : required_keys)
|
||||
if(!json.isMember(key))
|
||||
throw std::runtime_error(std::format("Missing json key: '{}'.", key));
|
||||
|
||||
const std::string matcher_type = json[JSON_MATCHER_TYPE_KEY].asString();
|
||||
if(matcher_type == JSON_MATCHER_SIMPLE_VALUE)
|
||||
matcher_type_ = simple;
|
||||
else if(matcher_type == JSON_MATCHER_REGEX_VALUE)
|
||||
matcher_type_ = regex;
|
||||
else
|
||||
throw std::runtime_error(std::format("Invalid matcher type: '{}'.", matcher_type));
|
||||
|
||||
const std::string target_type = json[JSON_TARGET_TYPE_KEY].asString();
|
||||
if(target_type == JSON_TARGET_ANY_VALUE)
|
||||
target_type_ = any;
|
||||
else if(target_type == JSON_TARGET_FILE_VALUE)
|
||||
target_type_ = file;
|
||||
else if(target_type == JSON_TARGET_DIRECTORY_VALUE)
|
||||
target_type_ = directory;
|
||||
else
|
||||
throw std::runtime_error(std::format("Invalid target type: '{}'.", target_type));
|
||||
|
||||
expression_ = json[JSON_EXPRESSION_KEY].asString();
|
||||
case_invariant_matching_ = false;
|
||||
if(json.isMember(JSON_CASE_INVARIANT_KEY))
|
||||
case_invariant_matching_ = json[JSON_CASE_INVARIANT_KEY].asBool();
|
||||
stop_on_branch_ = true;
|
||||
if(json.isMember(JSON_STOP_KEY))
|
||||
stop_on_branch_ = json[JSON_STOP_KEY].asBool();
|
||||
level_offset_ = 0;
|
||||
if(json.isMember(JSON_OFFSET_KEY))
|
||||
level_offset_ = json[JSON_OFFSET_KEY].asInt();
|
||||
}
|
||||
|
||||
std::optional<int> RootLevelCondition::detectRootLevel(QTreeWidgetItem* root_node,
|
||||
int cur_level) const
|
||||
{
|
||||
if(root_node->childCount() == 0)
|
||||
return {};
|
||||
|
||||
auto compare_levels = [](const auto& pair_l, const auto& pair_r)
|
||||
{ return pair_l.second > pair_r.second; };
|
||||
std::priority_queue<std::pair<QTreeWidgetItem*, int>,
|
||||
std::vector<std::pair<QTreeWidgetItem*, int>>,
|
||||
decltype(compare_levels)>
|
||||
remaining_nodes{ compare_levels };
|
||||
|
||||
for(int i = 0; i < root_node->childCount(); i++)
|
||||
remaining_nodes.push({ root_node->child(i), cur_level });
|
||||
|
||||
std::regex expression_regex;
|
||||
if(matcher_type_ == regex)
|
||||
expression_regex.assign(expression_);
|
||||
|
||||
while(!remaining_nodes.empty())
|
||||
{
|
||||
auto [node, level] = remaining_nodes.top();
|
||||
remaining_nodes.pop();
|
||||
const bool is_directory = node->data(0, Qt::UserRole).toBool();
|
||||
if(target_type_ == any || target_type_ == file && !is_directory ||
|
||||
target_type_ == directory && is_directory)
|
||||
{
|
||||
const std::string cur_text = case_invariant_matching_ ? node->text(0).toLower().toStdString()
|
||||
: node->text(0).toStdString();
|
||||
if(matcher_type_ == regex)
|
||||
{
|
||||
std::smatch match;
|
||||
if(std::regex_match(cur_text, match, expression_regex))
|
||||
return level - level_offset_;
|
||||
}
|
||||
else if(wildcardMatch(cur_text, expression_))
|
||||
return level - level_offset_;
|
||||
}
|
||||
|
||||
for(int i = 0; i < node->childCount(); i++)
|
||||
remaining_nodes.push({ node->child(i), level + 1 });
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
110
src/ui/rootlevelcondition.h
Normal file
110
src/ui/rootlevelcondition.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/*!
|
||||
* \file rootlevelcondition.h
|
||||
* \brief Header for the RootLevelCondition class.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QTreeWidgetItem>
|
||||
#include <json/json.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
|
||||
/*!
|
||||
* \brief Used to find the root level during mod installation using a regex or wildcard expression.
|
||||
*/
|
||||
class RootLevelCondition
|
||||
{
|
||||
public:
|
||||
/*! \brief Type of string matcher to use on file names. */
|
||||
enum MatcherType
|
||||
{
|
||||
/*! \brief Wildcard matcher. */
|
||||
simple,
|
||||
/*! \brief Regex matcher. */
|
||||
regex
|
||||
};
|
||||
|
||||
/*! \brief Describes what file type the expression should be matched against. */
|
||||
enum TargetType
|
||||
{
|
||||
/*! \brief Any file. */
|
||||
any,
|
||||
/*! \brief Only non directories. */
|
||||
file,
|
||||
/*! \brief Only directories. */
|
||||
directory
|
||||
};
|
||||
|
||||
/*!
|
||||
* \brief Simply initializes members.
|
||||
* \param matcher_type Type of file name matcher to use.
|
||||
* \param target_type Only files of this type will be used for matching.
|
||||
* \param case_invariant If true: Use case invariant matching.
|
||||
* \param stop_on_branch If true: Stop root level detection as soon as a directory contains more
|
||||
* than one entry.
|
||||
* \param expression Used for matching.
|
||||
* \param level_offset Offset to add to the detected root level.
|
||||
*/
|
||||
RootLevelCondition(MatcherType matcher_type,
|
||||
TargetType target_type,
|
||||
bool case_invariant,
|
||||
bool stop_on_branch,
|
||||
const std::string& expression,
|
||||
int level_offset);
|
||||
/*!
|
||||
* \brief Deserializes a RootLevelCondition from the given JSON object.
|
||||
* \param json Contains values for members.
|
||||
*/
|
||||
RootLevelCondition(const Json::Value& json);
|
||||
|
||||
/*!
|
||||
* \brief Recursively matches the given expression to every node in the given (sub)tree.
|
||||
*
|
||||
* WARNING: The root node itself is ignored for matching.
|
||||
*
|
||||
* \param root_node Root of the (sub)tree for which to match.
|
||||
* \param cur_level Current level in the tree.
|
||||
* \return If possible: The level at which the expression first matches a root_node's text.
|
||||
*/
|
||||
std::optional<int> detectRootLevel(QTreeWidgetItem* root_node, int cur_level = 0) const;
|
||||
|
||||
private:
|
||||
/*! \brief JSON key name for the matcher type. */
|
||||
static inline constexpr char JSON_MATCHER_TYPE_KEY[] = "matcher_type";
|
||||
/*! \brief JSON key name for the target type. */
|
||||
static inline constexpr char JSON_TARGET_TYPE_KEY[] = "target_type";
|
||||
/*! \brief JSON key name for case invariance.. */
|
||||
static inline constexpr char JSON_CASE_INVARIANT_KEY[] = "case_invariant";
|
||||
/*! \brief JSON key name for the expression. */
|
||||
static inline constexpr char JSON_EXPRESSION_KEY[] = "expression";
|
||||
/*! \brief JSON key name for stop on branch. */
|
||||
static inline constexpr char JSON_STOP_KEY[] = "stop_on_branch";
|
||||
/*! \brief JSON key name the level offset. */
|
||||
static inline constexpr char JSON_OFFSET_KEY[] = "level_offset";
|
||||
/*! \brief JSON value for the wildcard matcher. */
|
||||
static inline constexpr char JSON_MATCHER_SIMPLE_VALUE[] = "simple";
|
||||
/*! \brief JSON value for the regex matcher. */
|
||||
static inline constexpr char JSON_MATCHER_REGEX_VALUE[] = "regex";
|
||||
/*! \brief JSON value for any file type. */
|
||||
static inline constexpr char JSON_TARGET_ANY_VALUE[] = "any";
|
||||
/*! \brief JSON value for non directory file type. */
|
||||
static inline constexpr char JSON_TARGET_FILE_VALUE[] = "file";
|
||||
/*! \brief JSON value for directory file type. */
|
||||
static inline constexpr char JSON_TARGET_DIRECTORY_VALUE[] = "directory";
|
||||
|
||||
/*! \brief Type of file name matcher to use. */
|
||||
MatcherType matcher_type_;
|
||||
/*! \brief Only files of this type will be used for matching. */
|
||||
TargetType target_type_;
|
||||
/*! \brief If true: Use case invariant matching. */
|
||||
bool case_invariant_matching_;
|
||||
/*! \brief If true: Stop root level detection as soon as a directory contains more
|
||||
* than one entry. */
|
||||
bool stop_on_branch_;
|
||||
/*! \brief Used for matching. */
|
||||
std::string expression_;
|
||||
/*! \brief Offset to add to the detected root level. */
|
||||
int level_offset_;
|
||||
};
|
||||
@@ -20,7 +20,7 @@
|
||||
"target_dir": "$STEAM_PREFIX_PATH$/users/steamuser/Local Settings/Application Data/Fallout 3",
|
||||
"deploy_mode": "hard link",
|
||||
"source_dir": "$STEAM_INSTALL_PATH$/Data"
|
||||
},
|
||||
}
|
||||
],
|
||||
"auto_tags":
|
||||
[
|
||||
@@ -50,5 +50,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"root_level_conditions" :
|
||||
[
|
||||
{
|
||||
"case_invariant" : true,
|
||||
"expression" : ".*\\.es[plm]",
|
||||
"level_offset" : 0,
|
||||
"matcher_type" : "regex",
|
||||
"stop_on_branch" : false,
|
||||
"target_type" : "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "The Elder Scrolls IV: Oblivion Game of the Year Edition",
|
||||
"name": "Oblivion: GOTY",
|
||||
"deployers":
|
||||
[
|
||||
{
|
||||
@@ -20,7 +20,7 @@
|
||||
"target_dir": "$STEAM_PREFIX_PATH$/users/steamuser/Local Settings/Application Data/Oblivion",
|
||||
"deploy_mode": "hard link",
|
||||
"source_dir": "$STEAM_INSTALL_PATH$/Data"
|
||||
},
|
||||
}
|
||||
],
|
||||
"auto_tags":
|
||||
[
|
||||
@@ -50,5 +50,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"root_level_conditions" :
|
||||
[
|
||||
{
|
||||
"case_invariant" : true,
|
||||
"expression" : ".*\\.es[plm]",
|
||||
"level_offset" : 0,
|
||||
"matcher_type" : "regex",
|
||||
"stop_on_branch" : false,
|
||||
"target_type" : "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Fallout 3: Game of the Year Edition",
|
||||
"name": "Fallout 3: GOTY",
|
||||
"deployers":
|
||||
[
|
||||
{
|
||||
@@ -20,7 +20,7 @@
|
||||
"target_dir": "$STEAM_PREFIX_PATH$/users/steamuser/Local Settings/Application Data/Fallout 3 goty",
|
||||
"deploy_mode": "hard link",
|
||||
"source_dir": "$STEAM_INSTALL_PATH$/Data"
|
||||
},
|
||||
}
|
||||
],
|
||||
"auto_tags":
|
||||
[
|
||||
@@ -50,5 +50,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"root_level_conditions" :
|
||||
[
|
||||
{
|
||||
"case_invariant" : true,
|
||||
"expression" : ".*\\.es[plm]",
|
||||
"level_offset" : 0,
|
||||
"matcher_type" : "regex",
|
||||
"stop_on_branch" : false,
|
||||
"target_type" : "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -114,5 +114,16 @@
|
||||
"type" : "Loot Deployer"
|
||||
}
|
||||
],
|
||||
"name" : "Fallout: New Vegas"
|
||||
"name" : "Fallout: New Vegas",
|
||||
"root_level_conditions" :
|
||||
[
|
||||
{
|
||||
"case_invariant" : true,
|
||||
"expression" : ".*\\.es[plm]",
|
||||
"level_offset" : 0,
|
||||
"matcher_type" : "regex",
|
||||
"stop_on_branch" : false,
|
||||
"target_type" : "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
"target_dir": "$STEAM_PREFIX_PATH$/users/steamuser/Local Settings/Application Data/Fallout 4",
|
||||
"deploy_mode": "hard link",
|
||||
"source_dir": "$STEAM_INSTALL_PATH$/Data"
|
||||
},
|
||||
}
|
||||
],
|
||||
"auto_tags":
|
||||
[
|
||||
@@ -50,5 +50,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"root_level_conditions" :
|
||||
[
|
||||
{
|
||||
"case_invariant" : true,
|
||||
"expression" : ".*\\.es[plm]",
|
||||
"level_offset" : 0,
|
||||
"matcher_type" : "regex",
|
||||
"stop_on_branch" : false,
|
||||
"target_type" : "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -157,5 +157,16 @@
|
||||
"type" : "Loot Deployer"
|
||||
}
|
||||
],
|
||||
"name" : "Skyrim SE"
|
||||
}
|
||||
"name" : "Skyrim SE",
|
||||
"root_level_conditions" :
|
||||
[
|
||||
{
|
||||
"case_invariant" : true,
|
||||
"expression" : ".*\\.es[plm]",
|
||||
"level_offset" : 0,
|
||||
"matcher_type" : "regex",
|
||||
"stop_on_branch" : false,
|
||||
"target_type" : "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"target_dir": "$STEAM_PREFIX_PATH$/users/steamuser/Local Settings/Application Data/Skyrim Special Edition",
|
||||
"deploy_mode": "hard link",
|
||||
"source_dir": "$STEAM_INSTALL_PATH$/Data"
|
||||
},
|
||||
}
|
||||
],
|
||||
"auto_tags":
|
||||
[
|
||||
@@ -50,5 +50,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"root_level_conditions" :
|
||||
[
|
||||
{
|
||||
"case_invariant" : true,
|
||||
"expression" : ".*\\.es[plm]",
|
||||
"level_offset" : 0,
|
||||
"matcher_type" : "regex",
|
||||
"stop_on_branch" : false,
|
||||
"target_type" : "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"target_dir": "$STEAM_PREFIX_PATH$/users/steamuser/Local Settings/Application Data/Skyrim Special Edition",
|
||||
"deploy_mode": "hard link",
|
||||
"source_dir": "$STEAM_INSTALL_PATH$/Data"
|
||||
},
|
||||
}
|
||||
],
|
||||
"auto_tags":
|
||||
[
|
||||
@@ -50,5 +50,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"root_level_conditions" :
|
||||
[
|
||||
{
|
||||
"case_invariant" : true,
|
||||
"expression" : ".*\\.es[plm]",
|
||||
"level_offset" : 0,
|
||||
"matcher_type" : "regex",
|
||||
"stop_on_branch" : false,
|
||||
"target_type" : "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "The Elder Scrolls IV: Oblivion Game of the Year Edition Deluxe",
|
||||
"name": "Oblivion: GOTY Deluxe",
|
||||
"deployers":
|
||||
[
|
||||
{
|
||||
@@ -20,7 +20,7 @@
|
||||
"target_dir": "$STEAM_PREFIX_PATH$/users/steamuser/Local Settings/Application Data/Oblivion",
|
||||
"deploy_mode": "hard link",
|
||||
"source_dir": "$STEAM_INSTALL_PATH$/Data"
|
||||
},
|
||||
}
|
||||
],
|
||||
"auto_tags":
|
||||
[
|
||||
@@ -50,5 +50,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"root_level_conditions" :
|
||||
[
|
||||
{
|
||||
"case_invariant" : true,
|
||||
"expression" : ".*\\.es[plm]",
|
||||
"level_offset" : 0,
|
||||
"matcher_type" : "regex",
|
||||
"stop_on_branch" : false,
|
||||
"target_type" : "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user