#include "moddedapplication.h" #include "deployerfactory.h" #include "installer.h" #include "parseerror.h" #include "pathutils.h" #include "reversedeployer.h" #include #include #include #include namespace sfs = std::filesystem; namespace str = std::ranges; namespace pu = path_utils; ModdedApplication::ModdedApplication(sfs::path staging_dir, std::string name, std::string command, std::filesystem::path icon_path, std::string app_version) : name_(name), staging_dir_(staging_dir), command_(command), icon_path_(icon_path) { if(sfs::exists(staging_dir / CONFIG_FILE_NAME)) updateState(true); else { addProfile({ "Default", app_version, -1 }); updateSettings(true); } sfs::copy(staging_dir_ / CONFIG_FILE_NAME, staging_dir_ / ("." + CONFIG_FILE_NAME + ".bak"), sfs::copy_options::overwrite_existing); } void ModdedApplication::deployMods() { std::vector deployers; for(int i = 0; i < deployers_.size(); i++) deployers.push_back(i); deployModsFor(deployers); } void ModdedApplication::deployModsFor(std::vector deployers) { str::sort(deployers, [this](int depl_l, int depl_r) { return this->deployers_[depl_l]->getDeployPriority() < this->deployers_[depl_r]->getDeployPriority(); }); std::vector weights; for(int deployer : deployers) { const int num_mods = deployers_[deployer]->getNumMods(); // Reverse deployer operations are faster than other operations if(deployers_[deployer]->getType() == DeployerFactory::REVERSEDEPLOYER) weights.push_back((int)(num_mods / 8)); else if(deployers_[deployer]->isAutonomous() || num_mods == 0) weights.push_back(1); else weights.push_back(num_mods); } ProgressNode node(progress_callback_, weights); for(auto [i, deployer] : str::enumerate_view(deployers)) { const auto mod_sizes = deployers_[deployer]->deploy(&(node.child(i))); if(!deployers_[deployer]->isAutonomous()) { for(const auto [mod_id, mod_size] : mod_sizes) { auto mod_iter = str::find_if(installed_mods_, [id = mod_id](const Mod& m) { return m.id == id; }); if(mod_iter != installed_mods_.end()) mod_iter->size_on_disk = mod_size; } } } updateSettings(true); } void ModdedApplication::unDeployMods() { std::vector deployers; for(int i = 0; i < deployers_.size(); i++) deployers.push_back(i); unDeployModsFor(deployers); } void ModdedApplication::unDeployModsFor(std::vector deployers) { str::sort(deployers, [this](int depl_l, int depl_r) { return this->deployers_[depl_l]->getDeployPriority() < this->deployers_[depl_r]->getDeployPriority(); }); std::vector weights; for(int deployer : deployers) { const int num_mods = deployers_[deployer]->getNumMods(); if(deployers_[deployer]->isAutonomous() || num_mods == 0) weights.push_back(1); else weights.push_back(num_mods); } ProgressNode node(progress_callback_, weights); for(auto [i, deployer] : str::enumerate_view(deployers)) deployers_[deployer]->unDeploy(&(node.child(i))); updateSettings(true); } void ModdedApplication::installMod(const ImportModInfo& info) { if(info.replace_mod && info.target_group_id != -1) { replaceMod(info); return; } ProgressNode progress_node(progress_callback_); 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.target_group_id >= 0 || !info.deployers.empty()) progress_node.addChildren({ 1, 10 }); else progress_node.addChildren({ 1 }); progress_node.child(0).setTotalSteps(1); int mod_id = 0; if(!installed_mods_.empty()) mod_id = std::max_element(installed_mods_.begin(), installed_mods_.end())->id + 1; while(pu::exists(staging_dir_ / std::to_string(mod_id)) && mod_id < std::numeric_limits().max()) mod_id++; if(mod_id == std::numeric_limits().max()) throw std::runtime_error("Error: Could not generate new mod id."); last_mod_id_ = mod_id; const auto mod_size = Installer::install(info.current_path, staging_dir_ / std::to_string(mod_id), info.installer_flags, info.installer, info.root_level, info.files); const auto time_now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); installed_mods_.emplace_back(mod_id, info.name, info.version, time_now, info.local_source, info.remote_source, time_now, mod_size, 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.target_group_id >= 0) { if(modHasGroup(info.target_group_id)) addModToGroup(mod_id, group_map_[info.target_group_id], &progress_node.child(1)); else 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.target_group_id >= 0 ? 2 : 1)); for(auto& tag : auto_tags_) tag.updateMods(staging_dir_, std::vector{ mod_id }); updateAutoTagMap(); updateSettings(true); } void ModdedApplication::uninstallMods(const std::vector& mod_ids, const std::string& installer_type) { std::vector weights; std::vector> update_targets; for(int depl = 0; depl < deployers_.size(); depl++) update_targets.push_back({}); for(int mod_id : mod_ids) { if(group_map_.contains(mod_id)) removeModFromGroup(mod_id, false); auto mod_iter = std::find_if( installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); if(mod_iter == installed_mods_.end()) continue; for(int depl = 0; depl < deployers_.size(); depl++) { if(deployers_[depl]->isAutonomous()) continue; for(int prof = 0; prof < profile_names_.size(); prof++) { deployers_[depl]->setProfile(prof); if(deployers_[depl]->removeMod(mod_id) && str::find(update_targets[depl], prof) == update_targets[depl].end()) { update_targets[depl].push_back(prof); weights.push_back(deployers_[depl]->getNumMods()); } } deployers_[depl]->setProfile(current_profile_); } installed_mods_.erase(mod_iter); std::string installer = Installer::SIMPLEINSTALLER; if(installer_type == "" && installer_map_.contains(mod_id)) installer = installer_map_[mod_id]; Installer::uninstall(staging_dir_ / std::to_string(mod_id), installer); for(auto& tag : manual_tags_) tag.removeMod(mod_id); } ProgressNode node(progress_callback_, weights); int i = 0; for(int depl = 0; depl < update_targets.size(); depl++) { for(int prof : update_targets[depl]) { deployers_[depl]->setProfile(prof); deployers_[depl]->updateConflictGroups(&node.child(i)); i++; } deployers_[depl]->setProfile(current_profile_); } updateSettings(true); } void ModdedApplication::changeLoadorder(int deployer, int from_index, int to_index) { deployers_[deployer]->changeLoadorder(from_index, to_index); updateSettings(true); } void ModdedApplication::addModToDeployer(int deployer, int mod_id, bool update_conflicts, std::optional progress_node) { if(!deployers_[deployer]->isAutonomous()) { const bool was_added = deployers_[deployer]->addMod(mod_id); ProgressNode node(progress_callback_); if(update_conflicts && was_added) deployers_[deployer]->updateConflictGroups(progress_node ? progress_node : &node); else if(progress_node) { (*progress_node)->setTotalSteps(1); (*progress_node)->advance(); } splitMod(mod_id, deployer); updateSettings(true); } } void ModdedApplication::removeModFromDeployer(int deployer, int mod_id, bool update_conflicts, std::optional progress_node) { if(!deployers_[deployer]->isAutonomous()) { const bool was_removed = deployers_[deployer]->removeMod(mod_id); ProgressNode node(progress_callback_); if(update_conflicts && was_removed) deployers_[deployer]->updateConflictGroups(progress_node ? progress_node : &node); else if(progress_node) { (*progress_node)->setTotalSteps(1); (*progress_node)->advance(); } updateSettings(true); } } void ModdedApplication::setModStatus(int deployer, int mod_id, bool status) { deployers_[deployer]->setModStatus(mod_id, status); updateSettings(true); } void ModdedApplication::addDeployer(const EditDeployerInfo& info) { std::string source_dir = staging_dir_; const bool is_autonomous = DeployerFactory::AUTONOMOUS_DEPLOYERS.at(info.type); if(info.type == DeployerFactory::REVERSEDEPLOYER) { long id = 0; source_dir = staging_dir_ / std::format("rev_depl_{}", id); while(pu::exists(source_dir)) source_dir = staging_dir_ / std::format("rev_depl_{}", ++id); } else if(is_autonomous) source_dir = info.source_dir; deployers_.push_back(DeployerFactory::makeDeployer(info.type, source_dir, info.target_dir, info.name, info.deploy_mode, info.separate_profile_dirs, info.update_ignore_list)); deployers_.back()->setEnableUnsafeSorting(info.enable_unsafe_sorting); for(int i = 0; i < profile_names_.size(); i++) deployers_.back()->addProfile(); deployers_.back()->setProfile(current_profile_); deployers_.back()->setLog(log_); if(!is_autonomous) { for(int i = 0; i < installed_mods_.size(); i++) { for(int depl = 0; depl < deployers_.size(); depl++) { if(deployers_[depl]->hasMod(installed_mods_[i].id)) splitMod(installed_mods_[i].id, depl); } } } updateSettings(true); } void ModdedApplication::removeDeployer(int deployer, bool cleanup) { if(cleanup) deployers_[deployer]->cleanup(); deployers_.erase(deployers_.begin() + deployer); updateSettings(true); } std::vector ModdedApplication::getDeployerNames() const { std::vector names; for(const auto& deployer : deployers_) names.push_back(deployer->getName()); return names; } std::vector ModdedApplication::getModInfo() const { std::vector mod_info{}; for(const auto& mod : installed_mods_) { std::vector deployer_names; std::vector deployer_ids; std::vector statuses; for(int i = 0; i < deployers_.size(); i++) { if(deployers_[i]->isAutonomous()) continue; auto status = deployers_[i]->getModStatus(mod.id); if(status) { deployer_names.push_back(deployers_[i]->getName()); deployer_ids.push_back(i); statuses.push_back(*status); } } int group = -1; bool is_active = false; if(group_map_.contains(mod.id)) { group = group_map_.at(mod.id); is_active = active_group_members_[group] == mod.id; } mod_info.emplace_back( mod, deployer_names, deployer_ids, statuses, group, is_active, manual_tag_map_.contains(mod.id) ? manual_tag_map_.at(mod.id) : std::vector{}, auto_tag_map_.contains(mod.id) ? auto_tag_map_.at(mod.id) : std::vector{}); } return mod_info; } std::vector> ModdedApplication::getLoadorder(int deployer) const { return deployers_[deployer]->getLoadorder(); } const sfs::path& ModdedApplication::getStagingDir() const { return staging_dir_; } void ModdedApplication::setStagingDir(std::string staging_dir, bool move_existing) { if(staging_dir == staging_dir_) return; if(move_existing) { for(const auto& mod : installed_mods_) { std::string mod_dir = std::to_string(mod.id); sfs::rename(staging_dir_ / mod_dir, sfs::path(staging_dir) / mod_dir); } sfs::rename(staging_dir_ / CONFIG_FILE_NAME, sfs::path(staging_dir) / CONFIG_FILE_NAME); } staging_dir_ = staging_dir; updateState(true); } const std::string& ModdedApplication::name() const { return name_; } void ModdedApplication::setName(const std::string& newName) { name_ = newName; updateSettings(true); } int ModdedApplication::getNumDeployers() const { return deployers_.size(); } const std::string& ModdedApplication::getConfigFileName() const { return CONFIG_FILE_NAME; } void ModdedApplication::changeModName(int mod_id, const std::string& new_name) { auto iter = std::find_if( installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); if(iter == installed_mods_.end()) throw std::runtime_error("Error: Unknown mod id: " + std::to_string(mod_id)); iter->name = new_name; updateSettings(true); } std::vector ModdedApplication::getFileConflicts(int deployer, int mod_id, bool show_disabled) const { ProgressNode node(progress_callback_); auto conflicts = deployers_[deployer]->getFileConflicts(mod_id, show_disabled, &node); if(deployers_[deployer]->isAutonomous()) return conflicts; for(auto& [_, ids, names] : conflicts) { for(int id : ids) names.push_back(getModName(id)); } return conflicts; } AppInfo ModdedApplication::getAppInfo() const { AppInfo info; info.name = name_; info.staging_dir = staging_dir_.string(); info.command = command_; info.num_mods = installed_mods_.size(); info.app_version = app_versions_[current_profile_]; for(const auto& deployer : deployers_) { info.deployers.push_back(deployer->getName()); info.deployer_types.push_back(deployer->getType()); info.target_dirs.push_back(deployer->getDestPath()); info.deployer_source_dirs.push_back(deployer->getSourcePath()); info.deployer_mods.push_back(deployer->getNumMods()); info.deploy_modes.push_back(deployer->getDeployMode()); info.deployer_is_case_invariant.push_back(deployer->isCaseInvariant()); } info.tools = tools_; for(const auto& tag : manual_tags_) info.num_mods_per_manual_tag[tag.getName()] = tag.getNumMods(); for(const auto& tag : auto_tags_) { info.num_mods_per_auto_tag[tag.getName()] = tag.getNumMods(); info.auto_tags[tag.getName()] = { tag.getExpression(), tag.getConditions() }; } return info; } void ModdedApplication::addTool(const Tool& tool) { tools_.push_back(tool); updateSettings(true); } void ModdedApplication::removeTool(int tool_id) { if(tool_id < tools_.size() && tool_id >= 0) { tools_.erase(tools_.begin() + tool_id); updateSettings(true); } } std::vector ModdedApplication::getTools() const { return tools_; } const std::string& ModdedApplication::command() const { return command_; } void ModdedApplication::setCommand(const std::string& newCommand) { command_ = newCommand; updateSettings(true); } void ModdedApplication::editDeployer(int deployer, const EditDeployerInfo& info) { if(deployers_[deployer]->getType() == info.type) { deployers_[deployer]->setName(info.name); deployers_[deployer]->setDestPath(info.target_dir); deployers_[deployer]->setDeployMode(info.deploy_mode); deployers_[deployer]->setEnableUnsafeSorting(info.enable_unsafe_sorting); } else { if(info.type == DeployerFactory::REVERSEDEPLOYER) { long id = 0; sfs::path source_dir = staging_dir_ / std::format("rev_depl_{}", id); while(pu::exists(source_dir)) source_dir = staging_dir_ / std::format("rev_depl_{}", ++id); json_settings_["deployers"][deployer]["source_path"] = source_dir.string(); json_settings_["deployers"][deployer]["update_profiles"] = true; } else json_settings_["deployers"][deployer]["source_path"] = info.source_dir; json_settings_["deployers"][deployer]["name"] = info.name; json_settings_["deployers"][deployer]["dest_path"] = info.target_dir; json_settings_["deployers"][deployer]["type"] = info.type; json_settings_["deployers"][deployer]["deploy_mode"] = info.deploy_mode; json_settings_["deployers"][deployer]["enable_unsafe_sorting"] = info.enable_unsafe_sorting; updateState(); } if(deployers_[deployer]->isAutonomous() && info.type != DeployerFactory::REVERSEDEPLOYER) deployers_[deployer]->setSourcePath(info.source_dir); if(info.type == DeployerFactory::REVERSEDEPLOYER) { auto depl = static_cast(deployers_[deployer].get()); depl->enableSeparateDirs(info.separate_profile_dirs); if(!info.update_ignore_list && depl->getNumIgnoredFiles() != 0) depl->deleteIgnoredFiles(); else if(info.update_ignore_list && depl->getNumIgnoredFiles() == 0) depl->updateIgnoredFiles(true); } updateSettings(true); } std::unordered_set ModdedApplication::getModConflicts(int deployer, int mod_id) { ProgressNode node(progress_callback_); return deployers_[deployer]->getModConflicts(mod_id, &node); } void ModdedApplication::setProfile(int profile) { if(profile < 0 || profile >= profile_names_.size()) return; bak_man_.setProfile(profile); for(const auto& deployer : deployers_) deployer->setProfile(profile); current_profile_ = profile; } void ModdedApplication::addProfile(const EditProfileInfo& info) { profile_names_.push_back(info.name); app_versions_.push_back(info.app_version); for(const auto& deployer : deployers_) deployer->addProfile(info.source); bak_man_.addProfile(info.source); updateSettings(true); } void ModdedApplication::removeProfile(int profile) { if(profile < 0 || profile >= profile_names_.size()) return; for(const auto& deployer : deployers_) deployer->removeProfile(profile); profile_names_.erase(profile_names_.begin() + profile); app_versions_.erase(app_versions_.begin() + profile); bak_man_.removeProfile(profile); if(profile == current_profile_) setProfile(0); else if(profile < current_profile_) setProfile(current_profile_ - 1); updateSettings(true); } std::vector ModdedApplication::getProfileNames() const { return profile_names_; } void ModdedApplication::editProfile(int profile, const EditProfileInfo& info) { if(profile < 0 || profile >= profile_names_.size()) return; profile_names_[profile] = info.name; app_versions_[profile] = info.app_version; updateSettings(true); } void ModdedApplication::editTool(int tool_id, const Tool& new_tool) { if(tool_id >= 0 && tool_id < tools_.size()) tools_[tool_id] = new_tool; updateSettings(true); } std::tuple ModdedApplication::verifyDeployerDirectories() { std::tuple ret{ 0, "", "" }; for(const auto& depl : deployers_) { auto [cur_code, message] = depl->verifyDirectories(); if(cur_code) { ret = { cur_code, depl->destPath(), message }; } } return ret; } void ModdedApplication::addModToGroup(int mod_id, int group, std::optional progress_node) { if(group < 0 || group >= groups_.size() || group_map_.contains(mod_id)) return; groups_[group].push_back(mod_id); group_map_[mod_id] = group; active_group_members_[group] = mod_id; ProgressNode node(progress_callback_); updateDeployerGroups(progress_node ? progress_node : &node); updateSettings(true); } void ModdedApplication::removeModFromGroup(int mod_id, bool update_conflicts, std::optional progress_node) { if(!group_map_.contains(mod_id)) return; int group = group_map_[mod_id]; groups_[group].erase(std::find(groups_[group].begin(), groups_[group].end(), mod_id)); if(!groups_[group].empty()) { active_group_members_[group] = groups_[group][0]; std::vector> update_targets; std::vector weights; for(int depl = 0; depl < deployers_.size(); depl++) { update_targets.push_back({}); if(deployers_[depl]->isAutonomous()) continue; for(int prof = 0; prof < profile_names_.size(); prof++) { deployers_[depl]->setProfile(prof); auto loadorder = deployers_[depl]->getLoadorder(); auto iter = str::find_if( loadorder, [mod_id](const auto& tuple) { return std::get<0>(tuple) == mod_id; }); if(iter != loadorder.end()) { deployers_[depl]->addMod(active_group_members_[group], std::get<1>(*iter), false); deployers_[depl]->changeLoadorder(loadorder.size(), iter - loadorder.begin()); update_targets[depl].push_back(prof); weights.push_back(loadorder.size()); } } deployers_[depl]->setProfile(current_profile_); } ProgressNode node = progress_node ? **progress_node : ProgressNode(progress_callback_); if(!update_conflicts) { node.setTotalSteps(1); node.advance(); } else { node.addChildren(weights); int i = 0; for(int depl = 0; depl < update_targets.size(); depl++) { for(int prof : update_targets[depl]) { deployers_[depl]->setProfile(prof); deployers_[depl]->updateConflictGroups(&node.child(i)); i++; } deployers_[depl]->setProfile(current_profile_); } } } if(groups_[group].size() == 1) group_map_.erase(groups_[group][0]); if(groups_[group].size() < 2) { groups_.erase(groups_.begin() + group); active_group_members_.erase(active_group_members_.begin() + group); for(auto& pair : group_map_) { if(pair.second > group) pair.second--; } } group_map_.erase(mod_id); updateSettings(true); } void ModdedApplication::createGroup(int first_mod_id, int second_mod_id, std::optional progress_node) { if(group_map_.contains(first_mod_id)) { addModToGroup(second_mod_id, group_map_[first_mod_id]); return; } if(group_map_.contains(second_mod_id)) { addModToGroup(first_mod_id, group_map_[second_mod_id]); return; } groups_.push_back({ first_mod_id, second_mod_id }); int group = groups_.size() - 1; group_map_[first_mod_id] = group; group_map_[second_mod_id] = group; active_group_members_.push_back(first_mod_id); ProgressNode node(progress_callback_); updateDeployerGroups(progress_node ? progress_node : &node); updateSettings(true); } void ModdedApplication::changeActiveGroupMember(int group, int mod_id, std::optional progress_node) { if(group < 0 || group >= groups_.size() || std::find(groups_[group].begin(), groups_[group].end(), mod_id) == groups_[group].end()) return; active_group_members_[group] = mod_id; ProgressNode node(progress_callback_); updateDeployerGroups(progress_node ? progress_node : &node); updateSettings(true); } void ModdedApplication::changeModVersion(int mod_id, const std::string& new_version) { auto iter = std::find_if( installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); if(iter == installed_mods_.end()) throw std::runtime_error("Error: Unknown mod id: " + std::to_string(mod_id)); iter->version = new_version; updateSettings(true); } int ModdedApplication::getNumGroups() { return groups_.size(); } bool ModdedApplication::modHasGroup(int mod_id) { return group_map_.contains(mod_id); } int ModdedApplication::getModGroup(int mod_id) { if(!group_map_.contains(mod_id)) return -1; return group_map_[mod_id]; } void ModdedApplication::sortModsByConflicts(int deployer) { ProgressNode node(progress_callback_); deployers_[deployer]->sortModsByConflicts(&node); updateSettings(true); } std::vector> ModdedApplication::getConflictGroups(int deployer) { return deployers_[deployer]->getConflictGroups(); } void ModdedApplication::updateModDeployers(const std::vector& mod_ids, const std::vector& deployers) { std::vector weights; for(const auto& depl : deployers_) weights.push_back(depl->isAutonomous() ? 1 : depl->getNumMods()); ProgressNode node(progress_callback_, weights); std::optional dummy_node{}; for(int i = 0; i < mod_ids.size(); i++) { const int mod_id = mod_ids[i]; const bool is_last_mod = i == (mod_ids.size() - 1); for(int depl = 0; depl < deployers.size(); depl++) { if(deployers_[depl]->isAutonomous()) continue; if(deployers[depl]) addModToDeployer(depl, mod_id, is_last_mod, is_last_mod ? &node.child(depl) : dummy_node); else removeModFromDeployer( depl, mod_id, is_last_mod, is_last_mod ? &node.child(depl) : dummy_node); } } } int ModdedApplication::verifyStagingDir(sfs::path staging_dir) { try { Json::Value val; std::ifstream file(staging_dir / CONFIG_FILE_NAME, std::fstream::binary); if(file.is_open()) file >> val; file.close(); } catch(std::ios_base::failure& f) { return 1; } catch(Json::RuntimeError& e) { return 2; } return 0; } DeployerInfo ModdedApplication::getDeployerInfo(int deployer) { if(!(deployers_[deployer]->isAutonomous())) { std::map mods_per_tag; for(const auto& tag : manual_tags_) mods_per_tag[tag.getName()] = tag.getNumMods(); const auto loadorder = deployers_[deployer]->getLoadorder(); std::vector mod_names; mod_names.reserve(loadorder.size()); std::vector> manual_tags; manual_tags.reserve(loadorder.size()); std::vector> auto_tags; manual_tags.reserve(loadorder.size()); for(const auto& [id, e] : loadorder) { mod_names.push_back( std::ranges::find_if(installed_mods_, [id = id](auto& mod) { return mod.id == id; })->name); if(manual_tag_map_.contains(id)) manual_tags.push_back(manual_tag_map_.at(id)); else manual_tags.push_back({}); if(auto_tag_map_.contains(id)) auto_tags.push_back(auto_tag_map_.at(id)); else auto_tags.push_back({}); } for(const auto& tag : auto_tags_) { if(mods_per_tag.contains(tag.getName())) mods_per_tag[tag.getName()] += tag.getNumMods(); else mods_per_tag[tag.getName()] = tag.getNumMods(); } return { mod_names, loadorder, deployers_[deployer]->getConflictGroups(), false, manual_tags, auto_tags, mods_per_tag, false, false, deployers_[deployer]->supportsSorting(), deployers_[deployer]->supportsReordering(), deployers_[deployer]->supportsModConflicts(), deployers_[deployer]->supportsFileConflicts(), deployers_[deployer]->supportsFileBrowsing(), deployers_[deployer]->getType(), deployers_[deployer]->idsAreSourceReferences(), {}, deployers_[deployer]->getModActions(), deployers_[deployer]->getValidModActions(), deployers_[deployer]->getEnableUnsafeSorting() }; } else { const auto loadorder = deployers_[deployer]->getLoadorder(); std::vector mod_names; if(deployers_[deployer]->idsAreSourceReferences()) { mod_names.reserve(loadorder.size()); for(const auto& [id, _] : loadorder) { if(id == -1) { mod_names.push_back("Vanilla"); continue; } auto iter = std::ranges::find_if(installed_mods_, [id = id](auto& mod) { return mod.id == id; }); if(iter == installed_mods_.end()) mod_names.push_back("Vanilla"); else mod_names.push_back(iter->name); } } bool separate_dirs = false; bool has_ignored_files = false; if(deployers_[deployer]->getType() == DeployerFactory::REVERSEDEPLOYER) { auto depl = static_cast(deployers_[deployer].get()); separate_dirs = depl->usesSeparateDirs(); has_ignored_files = depl->getNumIgnoredFiles() != 0; } return { deployers_[deployer]->getModNames(), deployers_[deployer]->getLoadorder(), deployers_[deployer]->getConflictGroups(), true, {}, deployers_[deployer]->getAutoTags(), deployers_[deployer]->getAutoTagMap(), separate_dirs, has_ignored_files, deployers_[deployer]->supportsSorting(), deployers_[deployer]->supportsReordering(), deployers_[deployer]->supportsModConflicts(), deployers_[deployer]->supportsFileConflicts(), deployers_[deployer]->supportsFileBrowsing(), deployers_[deployer]->getType(), deployers_[deployer]->idsAreSourceReferences(), mod_names, deployers_[deployer]->getModActions(), deployers_[deployer]->getValidModActions(), deployers_[deployer]->getEnableUnsafeSorting() }; } } void ModdedApplication::setLog(const std::function& newLog) { log_ = newLog; for(auto& deployer : deployers_) deployer->setLog(newLog); } void ModdedApplication::addBackupTarget(const sfs::path& path, const std::string& name, const std::vector& backup_names) { bak_man_.addTarget(path, name, backup_names); updateSettings(true); } void ModdedApplication::removeBackupTarget(int target_id) { if(target_id < 0 || target_id >= bak_man_.getNumTargets()) return; bak_man_.removeTarget(target_id); updateSettings(true); } void ModdedApplication::removeAllBackupTargets() { for(int target = 0; target < bak_man_.getNumTargets(); target++) removeBackupTarget(target); } void ModdedApplication::addBackup(int target_id, const std::string& name, int source) { if(target_id < 0 || target_id >= bak_man_.getNumTargets()) return; bak_man_.addBackup(target_id, name, source); } void ModdedApplication::removeBackup(int target_id, int backup_id) { if(target_id < 0 || target_id >= bak_man_.getNumTargets() || backup_id < 0 || backup_id >= bak_man_.getNumBackups(target_id)) return; bak_man_.removeBackup(target_id, backup_id); } void ModdedApplication::setActiveBackup(int target_id, int backup_id) { if(target_id < 0 || target_id >= bak_man_.getNumTargets() || backup_id < 0 || backup_id >= bak_man_.getNumBackups(target_id)) return; bak_man_.setActiveBackup(target_id, backup_id); } std::vector ModdedApplication::getBackupTargets() const { return bak_man_.getTargets(); } void ModdedApplication::setBackupName(int target_id, int backup_id, const std::string& name) { if(target_id < 0 || target_id >= bak_man_.getNumTargets() || backup_id < 0 || backup_id >= bak_man_.getNumBackups(target_id)) return; bak_man_.setBackupName(target_id, backup_id, name); } void ModdedApplication::setBackupTargetName(int target_id, const std::string& name) { if(target_id < 0 || target_id >= bak_man_.getNumTargets()) return; bak_man_.setBackupTargetName(target_id, name); } void ModdedApplication::overwriteBackup(int target_id, int source_backup, int dest_backup) { if(target_id < 0 || target_id >= bak_man_.getNumTargets()) return; bak_man_.overwriteBackup(target_id, source_backup, dest_backup); } void ModdedApplication::cleanupFailedInstallation() { Installer::cleanupFailedInstallation(staging_dir_, last_mod_id_); auto iter = std::find_if(installed_mods_.begin(), installed_mods_.end(), [this](const Mod& m) { return m.id == this->last_mod_id_; }); if(iter != installed_mods_.end()) uninstallMods({ last_mod_id_ }); last_mod_id_ = -1; } void ModdedApplication::setProgressCallback(const std::function& progress_callback) { progress_callback_ = progress_callback; } void ModdedApplication::uninstallGroupMembers(const std::vector& mod_ids) { std::vector uninstall_targets; for(int active_id : mod_ids) { if(!group_map_.contains(active_id)) continue; for(int mod_id : groups_[group_map_[active_id]]) { if(mod_id != active_id) uninstall_targets.push_back(mod_id); } } uninstallMods(uninstall_targets); } void ModdedApplication::addManualTag(const std::string& tag_name) { if(str::find(manual_tags_, tag_name) != manual_tags_.end()) throw std::runtime_error( std::format("Error: A tag with the name '{}' already exists.", tag_name)); manual_tags_.emplace_back(tag_name); updateSettings(true); } void ModdedApplication::removeManualTag(const std::string& tag_name, bool update_map) { auto iter = str::find(manual_tags_, tag_name); if(iter != manual_tags_.end()) manual_tags_.erase(iter); if(update_map) updateManualTagMap(); updateSettings(true); } void ModdedApplication::changeManualTagName(const std::string& old_name, const std::string& new_name, bool update_map) { auto old_iter = str::find(manual_tags_, old_name); if(old_iter == manual_tags_.end()) return; auto new_iter = str::find(manual_tags_, new_name); if(new_iter != manual_tags_.end()) throw std::runtime_error( std::format("Error: Cannot rename tag '{}', because a tag with the name '{}' already exists.", old_name, new_name)); old_iter->setName(new_name); if(update_map) updateManualTagMap(); updateSettings(true); } void ModdedApplication::addTagsToMods(const std::vector& tag_names, const std::vector& mod_ids) { for(const auto& tag_name : tag_names) { auto tag = str::find(manual_tags_, tag_name); if(tag == manual_tags_.end()) return; for(int mod : mod_ids) tag->addMod(mod); } updateManualTagMap(); updateSettings(true); } void ModdedApplication::removeTagsFromMods(const std::vector& tag_names, const std::vector& mod_ids) { for(const auto& tag_name : tag_names) { auto tag = str::find(manual_tags_, tag_name); if(tag == manual_tags_.end()) return; for(int mod : mod_ids) tag->removeMod(mod); } updateManualTagMap(); updateSettings(true); } void ModdedApplication::setTagsForMods(const std::vector& tag_names, const std::vector mod_ids) { for(auto& tag : manual_tags_) { if(str::find(tag_names, tag) != tag_names.end()) { for(int mod : mod_ids) tag.addMod(mod); } else { for(int mod : mod_ids) tag.removeMod(mod); } } updateManualTagMap(); updateSettings(true); } void ModdedApplication::editManualTags(const std::vector& actions) { auto old_tags = manual_tags_; try { for(const auto& action : actions) { if(action.getType() == EditManualTagAction::ActionType::add) addManualTag(action.getName()); else if(action.getType() == EditManualTagAction::ActionType::remove) removeManualTag(action.getName(), false); else if(action.getType() == EditManualTagAction::ActionType::rename) changeManualTagName(action.getName(), action.getNewName(), false); } } catch(std::runtime_error& e) { manual_tags_ = old_tags; throw e; } updateManualTagMap(); updateSettings(true); } void ModdedApplication::addAutoTag(const std::string& tag_name, const std::string& expression, const std::vector& conditions, bool update) { if(std::find(auto_tags_.begin(), auto_tags_.end(), tag_name) != auto_tags_.end()) throw std::runtime_error( std::format("Error: A tag with the name '{}' already exists.", tag_name)); auto_tags_.emplace_back(tag_name, expression, conditions); auto select_id = [](const auto& mod) { return mod.id; }; if(expression != "") auto_tags_.back().reapplyMods(staging_dir_, str::transform_view(installed_mods_, select_id)); if(update) { updateAutoTagMap(); updateSettings(true); } } void ModdedApplication::addAutoTag(const Json::Value& json_tag, bool update) { if(std::find(auto_tags_.begin(), auto_tags_.end(), json_tag["name"].asString()) != auto_tags_.end()) throw std::runtime_error( std::format("Error: A tag with the name '{}' already exists.", json_tag["name"].asString())); auto_tags_.emplace_back(json_tag); auto select_id = [](const auto& mod) { return mod.id; }; if(json_tag["expression"].asString() != "") auto_tags_.back().reapplyMods(staging_dir_, str::transform_view(installed_mods_, select_id)); if(update) { updateAutoTagMap(); updateSettings(true); } } void ModdedApplication::removeAutoTag(const std::string& tag_name, bool update) { auto iter = std::find(auto_tags_.begin(), auto_tags_.end(), tag_name); if(iter == auto_tags_.end()) return; auto_tags_.erase(iter); if(update) { updateAutoTagMap(); updateSettings(true); } } void ModdedApplication::renameAutoTag(const std::string& old_name, const std::string& new_name, bool update) { auto iter = std::find(auto_tags_.begin(), auto_tags_.end(), old_name); if(iter == auto_tags_.end()) return; if(std::find(auto_tags_.begin(), auto_tags_.end(), new_name) != auto_tags_.end()) throw std::runtime_error( std::format("Error: Cannot rename tag '{}', because a tag with the name '{}' already exists.", old_name, new_name)); iter->setName(new_name); if(update) { updateAutoTagMap(); updateSettings(true); } } void ModdedApplication::changeAutoTagEvaluator(const std::string& tag_name, const std::string& expression, const std::vector& conditions, bool update) { auto iter = std::find(auto_tags_.begin(), auto_tags_.end(), tag_name); if(iter == auto_tags_.end()) return; iter->setEvaluator(expression, conditions); auto select_id = [](const auto& mod) { return mod.id; }; if(update) { iter->reapplyMods(staging_dir_, str::transform_view(installed_mods_, select_id)); updateAutoTagMap(); updateSettings(true); } } void ModdedApplication::editAutoTags(const std::vector& actions) { auto old_tags = auto_tags_; try { std::vector reapply_targets; for(const auto& action : actions) { if(action.getType() == EditAutoTagAction::ActionType::add) addAutoTag(action.getName(), action.getExpression(), action.getConditions(), false); else if(action.getType() == EditAutoTagAction::ActionType::remove) removeAutoTag(action.getName(), false); else if(action.getType() == EditAutoTagAction::ActionType::rename) renameAutoTag(action.getName(), action.getNewName(), false); else if(action.getType() == EditAutoTagAction::ActionType::change_evaluator) { changeAutoTagEvaluator( action.getName(), action.getExpression(), action.getConditions(), false); reapply_targets.push_back(action.getName()); } } if(!reapply_targets.empty()) { log_(Log::LOG_INFO, "Reapplying auto tags with edited conditions to all mods..."); ProgressNode node(progress_callback_); node.addChildren({ 1.0f, std::min(8.0f, (float)reapply_targets.size()) }); node.child(0).setTotalSteps(installed_mods_.size()); std::vector weights; for(const auto& tag : reapply_targets) { auto iter = std::find(auto_tags_.begin(), auto_tags_.end(), tag); if(iter != auto_tags_.end()) weights.push_back(iter->getNumConditions()); } node.child(1).addChildren(weights); for(int i = 0; i < weights.size(); i++) node.child(1).child(i).setTotalSteps(installed_mods_.size()); auto select_id = [](const auto& mod) { return mod.id; }; auto mods = str::transform_view(installed_mods_, select_id); const auto files = AutoTag::readModFiles(staging_dir_, mods, &node.child(0)); for(int i = 0; i < reapply_targets.size(); i++) { auto iter = std::find(auto_tags_.begin(), auto_tags_.end(), reapply_targets[i]); if(iter != auto_tags_.end()) iter->reapplyMods(files, mods, &node.child(1).child(i)); } } } catch(std::runtime_error& e) { auto_tags_ = old_tags; throw e; } updateAutoTagMap(); updateSettings(true); } void ModdedApplication::reapplyAutoTags() { log_(Log::LOG_INFO, "Reapplying auto tags to all mods..."); ProgressNode node(progress_callback_); node.addChildren({ 1.0f, 8.0f }); node.child(0).setTotalSteps(installed_mods_.size()); std::vector weights; for(auto& tag : auto_tags_) weights.push_back(tag.getNumConditions()); node.child(1).addChildren(weights); for(int i = 0; i < weights.size(); i++) node.child(1).child(i).setTotalSteps(installed_mods_.size()); auto select_id = [](const auto& mod) { return mod.id; }; auto mods = str::transform_view(installed_mods_, select_id); const auto files = AutoTag::readModFiles(staging_dir_, mods, &node.child(0)); for(int i = 0; i < auto_tags_.size(); i++) auto_tags_[i].reapplyMods(files, mods, &node.child(1).child(i)); updateAutoTagMap(); updateSettings(true); } void ModdedApplication::updateAutoTags(const std::vector mod_ids) { log_(Log::LOG_INFO, std::format("Reapplying auto tags to {} mods...", mod_ids.size())); ProgressNode node(progress_callback_); node.addChildren( { 1.0f, std::max(1.0f, 8.0f * (float)mod_ids.size() / (float)installed_mods_.size()) }); node.child(0).setTotalSteps(mod_ids.size()); std::vector weights; for(auto& tag : auto_tags_) weights.push_back(tag.getNumConditions()); node.child(1).addChildren(weights); for(int i = 0; i < weights.size(); i++) node.child(1).child(i).setTotalSteps(mod_ids.size()); const auto files = AutoTag::readModFiles(staging_dir_, mod_ids, &node.child(0)); for(int i = 0; i < auto_tags_.size(); i++) auto_tags_[i].updateMods(files, mod_ids, &node.child(1).child(i)); updateAutoTagMap(); updateSettings(true); } void ModdedApplication::deleteAllData() { for(int i = 0; i < deployers_.size(); i++) removeDeployer(i, true); 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(getDownloadDir()); } void ModdedApplication::setAppVersion(const std::string& app_version) { app_versions_[current_profile_] = app_version; updateSettings(true); } void ModdedApplication::setModSources(int mod_id, const std::string& local_source, const std::string& remote_source) { auto iter = std::find_if( installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); if(iter == installed_mods_.end()) throw std::runtime_error("Error: Unknown mod id: " + std::to_string(mod_id)); iter->local_source = local_source; iter->remote_source = remote_source; updateSettings(true); } nexus::Page ModdedApplication::getNexusPage(int mod_id) { auto iter = std::find_if( installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); if(iter == installed_mods_.end()) throw std::runtime_error("Error: Unknown mod id: " + std::to_string(mod_id)); return nexus::Api::getNexusPage(iter->remote_source); } void ModdedApplication::checkForModUpdates() { std::vector target_mod_indices; for(const auto& [i, mod] : str::enumerate_view(installed_mods_)) { if(nexus::Api::modUrlIsValid(mod.remote_source) && mod.remote_update_time <= mod.install_time) target_mod_indices.push_back(i); } performUpdateCheck(target_mod_indices); } void ModdedApplication::checkModsForUpdates(const std::vector& mod_ids) { std::vector target_mod_indices; for(const auto& [i, mod] : str::enumerate_view(installed_mods_)) { if(str::find(mod_ids, mod.id) != mod_ids.end() && nexus::Api::modUrlIsValid(mod.remote_source) && mod.remote_update_time <= mod.install_time) target_mod_indices.push_back(i); } performUpdateCheck(target_mod_indices); } void ModdedApplication::suppressUpdateNotification(const std::vector& mod_ids) { for(int mod_id : mod_ids) { auto iter = std::find_if(installed_mods_.begin(), installed_mods_.end(), [mod_id](const Mod& mod) { return mod.id == mod_id; }); if(iter != installed_mods_.end() && iter->remote_update_time > iter->install_time) iter->suppress_update_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); } updateSettings(true); } ExternalChangesInfo ModdedApplication::getExternalChanges(int deployer) { ExternalChangesInfo info; ProgressNode node(progress_callback_); info.file_changes = deployers_[deployer]->getExternallyModifiedFiles({ &node }); info.deployer_id = deployer; info.deployer_name = deployers_[deployer]->getName(); return info; } void ModdedApplication::keepOrRevertFileModifications( int deployer, const FileChangeChoices& changes_to_keep) const { deployers_[deployer]->keepOrRevertFileModifications(changes_to_keep); } void ModdedApplication::fixInvalidHardLinkDeployers() { for(auto& depl : deployers_) depl->fixInvalidLinkDeployMode(); } void ModdedApplication::exportConfiguration(const std::vector& deployers, const std::vector& auto_tags) { Json::Value json; json["name"] = name_; int i = 0; for(int deployer_id : deployers) { if(deployer_id < 0 || deployer_id >= deployers_.size()) continue; const auto& deployer = deployers_[deployer_id]; json["deployers"][i]["type"] = deployer->getType(); json["deployers"][i]["name"] = deployer->getName(); json["deployers"][i]["target_dir"] = generalizeSteamPath(deployer->getDestPath()); if(deployer->isAutonomous()) json["deployers"][i]["source_dir"] = generalizeSteamPath(deployer->getSourcePath()); // use hard link by default; import will auto change this to sym link when needed json["deployers"][i]["deploy_mode"] = deployer->getDeployMode() == Deployer::copy ? "copy" : "hard_link"; if(deployer->getType() == DeployerFactory::REVERSEDEPLOYER) { auto rev_depl = static_cast(deployer.get()); json["deployers"][i]["uses_separate_dirs"] = rev_depl->usesSeparateDirs(); json["deployers"][i]["update_ignore_list"] = true; } i++; } i = 0; for(const auto& tag : auto_tags) { auto iter = str::find_if(auto_tags_, [name = tag](auto tag) { return tag.getName() == name; }); if(iter == auto_tags_.end()) continue; json["auto_tags"][i] = iter->toJson(); json["auto_tags"][i].removeMember("mod_ids"); i++; } sfs::path path = staging_dir_ / (export_file_name + ".json"); if(pu::exists(path)) { i = 1; do path = staging_dir_ / (export_file_name + std::format("_{}.json", i++)); while(pu::exists(path)); } log_(Log::LOG_INFO, std::format("Exporting configuration for '{}' to '{}'", name_, path.string())); std::ofstream file(path, std::fstream::binary); file << json; } void ModdedApplication::updateIgnoredFiles(int deployer) { if(deployers_[deployer]->getType() != DeployerFactory::REVERSEDEPLOYER) { log_(Log::LOG_DEBUG, "Ignored files can only be updated for ReverseDeployers."); return; } auto depl = static_cast(deployers_[deployer].get()); depl->updateIgnoredFiles(true); } void ModdedApplication::addModToIgnoreList(int deployer, int mod_id) { if(deployers_[deployer]->getType() != DeployerFactory::REVERSEDEPLOYER) { log_(Log::LOG_DEBUG, "Ignored files can only be updated for ReverseDeployers."); return; } auto depl = static_cast(deployers_[deployer].get()); depl->addModToIgnoreList(mod_id); } void ModdedApplication::applyModAction(int deployer, int action, int mod_id) { deployers_[deployer]->applyModAction(action, mod_id); updateSettings(true); } std::filesystem::path ModdedApplication::getDownloadDir() const { return staging_dir_ / DOWNLOAD_DIR; } sfs::path ModdedApplication::iconPath() const { return icon_path_; } void ModdedApplication::setIconPath(const sfs::path& icon_path) { icon_path_ = icon_path; updateSettings(true); } void ModdedApplication::updateSettings(bool write) { json_settings_.clear(); json_settings_["name"] = name_; json_settings_["command"] = command_; json_settings_["icon_path"] = icon_path_.string(); for(int group = 0; group < groups_.size(); group++) { json_settings_["groups"][group]["active_member"] = active_group_members_[group]; for(int i = 0; i < groups_[group].size(); i++) { json_settings_["groups"][group]["members"][i] = groups_[group][i]; } } for(int i = 0; i < profile_names_.size(); i++) json_settings_["profiles"][i]["name"] = profile_names_[i]; for(int i = 0; i < app_versions_.size(); i++) json_settings_["profiles"][i]["app_version"] = app_versions_[i]; for(int i = 0; i < installed_mods_.size(); i++) { json_settings_["installed_mods"][i] = installed_mods_[i].toJson(); json_settings_["installed_mods"][i]["installer"] = installer_map_[installed_mods_[i].id]; } for(int depl = 0; depl < deployers_.size(); depl++) { json_settings_["deployers"][depl]["dest_path"] = deployers_[depl]->getDestPath(); if(deployers_[depl]->isAutonomous()) json_settings_["deployers"][depl]["source_path"] = deployers_[depl]->sourcePath().string(); else json_settings_["deployers"][depl]["source_path"] = staging_dir_.string(); json_settings_["deployers"][depl]["name"] = deployers_[depl]->getName(); json_settings_["deployers"][depl]["type"] = deployers_[depl]->getType(); json_settings_["deployers"][depl]["deploy_mode"] = deployers_[depl]->getDeployMode(); json_settings_["deployers"][depl]["enable_unsafe_sorting"] = deployers_[depl]->getEnableUnsafeSorting(); if(!deployers_[depl]->isAutonomous()) { for(int prof = 0; prof < profile_names_.size(); prof++) { deployers_[depl]->setProfile(prof); json_settings_["deployers"][depl]["profiles"][prof]["name"] = profile_names_[prof]; auto loadorder = deployers_[depl]->getLoadorder(); for(int mod = 0; mod < loadorder.size(); mod++) { json_settings_["deployers"][depl]["profiles"][prof]["loadorder"][mod]["id"] = std::get<0>(loadorder[mod]); json_settings_["deployers"][depl]["profiles"][prof]["loadorder"][mod]["enabled"] = std::get<1>(loadorder[mod]); } auto conflict_groups = deployers_[depl]->getConflictGroups(); for(int group = 0; group < conflict_groups.size(); group++) { for(int i = 0; i < conflict_groups[group].size(); i++) json_settings_["deployers"][depl]["profiles"][prof]["conflict_groups"][group][i] = conflict_groups[group][i]; } } } deployers_[depl]->setProfile(current_profile_); } for(int tool = 0; tool < tools_.size(); tool++) json_settings_["tools"][tool] = tools_[tool].toJson(); const auto targets = bak_man_.getTargets(); for(int i = 0; i < targets.size(); i++) json_settings_["backup_targets"][i]["path"] = targets[i].path.string(); for(int i = 0; i < manual_tags_.size(); i++) json_settings_["manual_tags"][i] = manual_tags_[i].toJson(); for(int i = 0; i < auto_tags_.size(); i++) { if(!auto_tags_[i].getExpression().empty()) json_settings_["auto_tags"][i] = auto_tags_[i].toJson(); } if(write) writeSettings(); } void ModdedApplication::writeSettings() const { sfs::path settings_file_path = staging_dir_ / (CONFIG_FILE_NAME + ".tmp"); std::ofstream file(settings_file_path, std::fstream::binary); if(!file.is_open()) throw std::runtime_error("Error: Could not write to \"" + settings_file_path.string() + "\"."); file << json_settings_; file.close(); sfs::rename(settings_file_path, staging_dir_ / CONFIG_FILE_NAME); } void ModdedApplication::readSettings() { json_settings_.clear(); sfs::path settings_file_path = staging_dir_ / CONFIG_FILE_NAME; std::ifstream file(settings_file_path, std::fstream::binary); if(!file.is_open()) throw std::runtime_error("Error: Could not read from \"" + settings_file_path.string() + "\"."); file >> json_settings_; file.close(); } void ModdedApplication::updateState(bool read) { installed_mods_.clear(); deployers_.clear(); groups_.clear(); group_map_.clear(); active_group_members_.clear(); profile_names_.clear(); bak_man_.reset(); tools_.clear(); profile_names_.clear(); app_versions_.clear(); manual_tags_.clear(); manual_tag_map_.clear(); auto_tags_.clear(); auto_tag_map_.clear(); installer_map_.clear(); if(read) { if(!sfs::exists(staging_dir_ / CONFIG_FILE_NAME)) return; readSettings(); } if(!json_settings_.isMember("name")) throw ParseError("Name is missing in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); name_ = json_settings_["name"].asString(); if(!json_settings_.isMember("command")) throw ParseError("Command is missing in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); command_ = json_settings_["command"].asString(); if(!json_settings_.isMember("icon_path")) throw ParseError("Icon path is missing in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); icon_path_ = json_settings_["icon_path"].asString(); if(!json_settings_.isMember("profiles")) throw ParseError("Profiles are missing in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); Json::Value profiles = json_settings_["profiles"]; for(int i = 0; i < profiles.size(); i++) { profile_names_.push_back(profiles[i]["name"].asString()); app_versions_.push_back(profiles[i]["app_version"].asString()); } Json::Value installed_mods = json_settings_["installed_mods"]; for(int i = 0; i < installed_mods.size(); i++) { installed_mods_.emplace_back(installed_mods[i]); std::string installer = installed_mods[i]["installer"].asString(); std::vector types = Installer::INSTALLER_TYPES; if(std::find(types.begin(), types.end(), installer) == types.end()) throw ParseError("Unknown installer type: " + installer + " in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); installer_map_[installed_mods[i]["id"].asInt()] = installer; } Json::Value groups = json_settings_["groups"]; for(int group = 0; group < groups.size(); group++) { groups_.push_back(std::vector{}); for(int i = 0; i < groups[group]["members"].size(); i++) { int mod_id = groups[group]["members"][i].asInt(); if(std::find_if(installed_mods_.begin(), installed_mods_.end(), [mod_id](const Mod& m) { return m.id == mod_id; }) == installed_mods_.end()) throw ParseError("Unknown mod id in group " + std::to_string(group) + ": " + std::to_string(mod_id) + " in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); if(std::find(groups_[group].begin(), groups_[group].end(), mod_id) != groups_[group].end()) throw ParseError("Duplicate mod id in group " + std::to_string(group) + ": " + std::to_string(mod_id) + " in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); group_map_[mod_id] = group; groups_[group].push_back(mod_id); } int active_member = groups[group]["active_member"].asInt(); if(std::find(groups_[group].begin(), groups_[group].end(), active_member) == groups_[group].end() || !groups[group].isMember("active_member")) throw ParseError("Invalid active group member: " + std::to_string(active_member) + " in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); active_group_members_.push_back(groups[group]["active_member"].asInt()); } Json::Value deployers = json_settings_["deployers"]; for(int depl = 0; depl < deployers.size(); depl++) { std::vector types = DeployerFactory::DEPLOYER_TYPES; std::string type = deployers[depl]["type"].asString(); if(std::find(types.begin(), types.end(), type) == types.end()) throw ParseError("Unknown deployer type: " + type + " in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); Deployer::DeployMode deploy_mode = Deployer::hard_link; if(deployers[depl].isMember("use_copy_deployment")) deploy_mode = deployers[depl]["use_copy_deployment"].asBool() ? Deployer::copy : Deployer::hard_link; else deploy_mode = static_cast(deployers[depl]["deploy_mode"].asInt()); deployers_.push_back( DeployerFactory::makeDeployer(type, sfs::path(deployers[depl]["source_path"].asString()), sfs::path(deployers[depl]["dest_path"].asString()), deployers[depl]["name"].asString(), deploy_mode)); if(deployers[depl].isMember("enable_unsafe_sorting")) deployers_.back()->setEnableUnsafeSorting(deployers[depl]["enable_unsafe_sorting"].asBool()); if(!deployers_[depl]->isAutonomous()) { for(int prof = 0; prof < profile_names_.size(); prof++) { deployers_[depl]->addProfile(); deployers_[depl]->setProfile(prof); Json::Value loadorder = deployers[depl]["profiles"][prof]["loadorder"]; for(int mod = 0; mod < loadorder.size(); mod++) { int mod_id = loadorder[mod]["id"].asInt(); if(std::find_if(installed_mods_.begin(), installed_mods_.end(), [mod_id](const Mod& m) { return m.id == mod_id; }) == installed_mods_.end()) throw ParseError("Unknown mod id in deployers: " + std::to_string(mod_id) + " in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); if(!group_map_.contains(mod_id) || active_group_members_[group_map_[mod_id]] == mod_id && !(deployers_[depl]->isAutonomous())) deployers_[depl]->addMod(mod_id, loadorder[mod]["enabled"].asBool(), false); } Json::Value conflict_groups_json = deployers[depl]["profiles"][prof]["conflict_groups"]; std::vector> conflict_groups; for(int group = 0; group < conflict_groups_json.size(); group++) { std::vector new_group; for(int mod = 0; mod < conflict_groups_json[group].size(); mod++) new_group.push_back(conflict_groups_json[group][mod].asInt()); conflict_groups.push_back(std::move(new_group)); } deployers_[depl]->setConflictGroups(conflict_groups); } } if(type == DeployerFactory::REVERSEDEPLOYER) { if(deployers[depl].get("update_profiles", false).asBool()) { json_settings_["deployers"][depl]["update_profiles"] = false; for(int i = 0; i < profile_names_.size(); i++) deployers_[depl]->addProfile(); } auto rev_depl = static_cast(deployers_[depl].get()); if(rev_depl->getNumProfiles() != profile_names_.size()) throw ParseError(std::format( "Mismatch in profile count for deployer '{}'. {} profiles found, expected {}.", rev_depl->getName(), rev_depl->getNumProfiles(), profile_names_.size())); } deployers_[depl]->setProfile(current_profile_); } Json::Value tools = json_settings_["tools"]; for(int tool = 0; tool < tools.size(); tool++) tools_.emplace_back(tools[tool]); for(int prof = 0; prof < profile_names_.size(); prof++) bak_man_.addProfile(); bak_man_.setProfile(current_profile_); Json::Value backup_targets = json_settings_["backup_targets"]; for(int target = 0; target < backup_targets.size(); target++) bak_man_.addTarget(backup_targets[target]["path"].asString()); bak_man_.setLog(log_); if(json_settings_.isMember("manual_tags")) { for(auto& tag_entry : json_settings_["manual_tags"]) { if(str::find_if(manual_tags_, [name = tag_entry["name"].asString()](auto tag) { return tag.getName() == name; }) != manual_tags_.end()) throw ParseError( std::format("Manual tag \"{}\" found more than once.", tag_entry["name"].asString())); manual_tags_.emplace_back(tag_entry); } updateManualTagMap(); } if(json_settings_.isMember("auto_tags")) { for(auto& tag_entry : json_settings_["auto_tags"]) { if(str::find_if(auto_tags_, [name = tag_entry["name"].asString()](auto tag) { return tag.getName() == name; }) != auto_tags_.end()) throw ParseError( std::format("Auto tag \"{}\" found more than once.", tag_entry["name"].asString())); auto_tags_.emplace_back(tag_entry); } updateAutoTagMap(); } updateSteamIconPath(); } std::string ModdedApplication::getModName(int mod_id) const { auto iter = std::find_if( installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); if(iter == installed_mods_.end()) return ""; return iter->name; } void ModdedApplication::updateDeployerGroups(std::optional progress_node) { std::vector> update_targets; for(int depl = 0; depl < deployers_.size(); depl++) { update_targets.push_back({}); if(deployers_[depl]->isAutonomous()) continue; for(int profile = 0; profile < profile_names_.size(); profile++) { deployers_[depl]->setProfile(profile); std::vector completed_groups(active_group_members_.size()); std::fill(completed_groups.begin(), completed_groups.end(), false); for(const auto& [mod_id, _] : deployers_[depl]->getLoadorder()) { if(!group_map_.contains(mod_id)) continue; const int group = group_map_[mod_id]; if(!completed_groups[group]) { completed_groups[group] = true; if(deployers_[depl]->swapMod(mod_id, active_group_members_[group])) update_targets[depl].push_back(profile); } else if(deployers_[depl]->removeMod(mod_id)) update_targets[depl].push_back(profile); } } deployers_[depl]->setProfile(current_profile_); } if(progress_node) { std::vector weights; for(int depl = 0; depl < update_targets.size(); depl++) { for(int profile : update_targets[depl]) { deployers_[depl]->setProfile(profile); weights.push_back(deployers_[depl]->getNumMods()); } deployers_[depl]->setProfile(current_profile_); } (*progress_node)->addChildren(weights); } int i = 0; for(int depl = 0; depl < update_targets.size(); depl++) { for(int profile : update_targets[depl]) { deployers_[depl]->setProfile(profile); deployers_[depl]->updateConflictGroups(progress_node ? &(*progress_node)->child(i) : std::optional{}); i++; } deployers_[depl]->setProfile(current_profile_); } } void ModdedApplication::splitMod(int mod_id, int deployer) { if(deployers_[deployer]->isAutonomous()) return; std::map managed_sub_dirs; for(int i = 0; i < deployers_.size(); i++) { if(i == deployer || deployers_[i]->isAutonomous()) continue; auto cur_depl_path = deployers_[i]->getDestPath(); if(!cur_depl_path.ends_with("/")) cur_depl_path += "/"; auto target_depl_path = deployers_[deployer]->getDestPath(); if(!target_depl_path.ends_with("/")) target_depl_path += "/"; const auto pos = cur_depl_path.find(target_depl_path); if(pos != std::string::npos) { std::string sub_dir = cur_depl_path.substr(pos + target_depl_path.size()); if(sub_dir.starts_with("/")) sub_dir = sub_dir.substr(1); managed_sub_dirs[i] = sub_dir; } } if(managed_sub_dirs.empty()) return; for(const auto& [depl, dir] : managed_sub_dirs) { const auto mod_dir_optional = pu::pathExists(dir, staging_dir_ / std::to_string(mod_id), deployers_[deployer]->getType() == DeployerFactory::CASEMATCHINGDEPLOYER); if(!mod_dir_optional) continue; const auto mod_dir = staging_dir_ / std::to_string(mod_id) / mod_dir_optional->string(); ImportModInfo info; info.deployers = { depl }; 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()) throw std::runtime_error(std::format("Invalid mod id {}", mod_id)); info.name = iter->name + " [" + deployers_[depl]->getName() + "]"; info.version = iter->version; info.installer = Installer::SIMPLEINSTALLER; info.installer_flags = Installer::Flag::preserve_case | Installer::Flag::preserve_directories; info.files = {}; info.root_level = 0; 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( "Mod '{}' has been split because it contains" " a sub-directory managed by deployer '{}'.", iter->name, deployers_[depl]->getName())); installMod(info); sfs::remove_all(mod_dir); } } void ModdedApplication::replaceMod(const ImportModInfo& info) { if(!info.replace_mod || info.target_group_id == -1) { installMod(info); return; } auto index = 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.target_group_id, info.name)); int mod_id = 0; if(!installed_mods_.empty()) mod_id = std::max_element(installed_mods_.begin(), installed_mods_.end())->id + 1; while(pu::exists(staging_dir_ / std::to_string(mod_id)) && mod_id < std::numeric_limits().max()) mod_id++; if(mod_id == std::numeric_limits().max()) throw std::runtime_error("Error: Could not generate new mod id."); const sfs::path tmp_replace_dir = staging_dir_ / (std::string("tmp_replace_") + std::to_string(mod_id)); 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.target_group_id); sfs::remove_all(old_mod_path); sfs::rename(tmp_replace_dir, old_mod_path); index->name = info.name; index->version = info.version; index->remote_source = info.remote_source; index->local_source = info.local_source; index->install_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); index->remote_update_time = index->install_time; index->size_on_disk = mod_size; index->remote_mod_id = info.remote_mod_id; index->remote_file_id = info.remote_file_id; index->remote_type = info.remote_type; std::vector weights_profiles; std::vector weights_mods; std::vector> update_targets; for(int depl = 0; depl < deployers_.size(); depl++) { bool was_split = false; update_targets.push_back({}); if(deployers_[depl]->hasMod(info.target_group_id)) weights_mods.push_back(deployers_[depl]->getNumMods()); else weights_mods.push_back(0); if(deployers_[depl]->isAutonomous()) continue; for(int prof = 0; prof < profile_names_.size(); prof++) { deployers_[depl]->setProfile(prof); 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.target_group_id, depl); } } } deployers_[depl]->setProfile(current_profile_); } ProgressNode node(progress_callback_, { 10.0f, 6.0f }); node.child(0).addChildren(weights_mods); node.child(1).addChildren(weights_profiles); int i = 0; for(int depl = 0; depl < update_targets.size(); depl++) { deployers_[depl]->updateDeployedFilesForMod(info.target_group_id, &node.child(0).child(depl)); for(int prof : update_targets[depl]) { deployers_[depl]->setProfile(prof); deployers_[depl]->updateConflictGroups(&node.child(1).child(i)); i++; } deployers_[depl]->setProfile(current_profile_); } for(auto& tag : auto_tags_) tag.updateMods(staging_dir_, std::vector{ info.target_group_id }); updateAutoTagMap(); updateSettings(true); } void ModdedApplication::updateManualTagMap() { manual_tag_map_.clear(); for(const auto& mod : installed_mods_) manual_tag_map_[mod.id] = {}; for(const auto& tag : manual_tags_) { for(int mod_id : tag.getMods()) manual_tag_map_[mod_id].push_back(tag.getName()); } } void ModdedApplication::updateAutoTagMap() { auto_tag_map_.clear(); for(const auto& mod : installed_mods_) auto_tag_map_[mod.id] = {}; for(const auto& tag : auto_tags_) { for(int mod_id : tag.getMods()) auto_tag_map_[mod_id].push_back(tag.getName()); } } void ModdedApplication::performUpdateCheck(const std::vector& target_mod_indices) { if(target_mod_indices.empty()) { log_(Log::LOG_INFO, "None of the selected mods has a valid remote source."); return; } log_(Log::LOG_INFO, std::format("Checking for updates for {} mod{}...", target_mod_indices.size(), target_mod_indices.size() > 1 ? "s" : "")); ProgressNode node(progress_callback_); node.setTotalSteps(target_mod_indices.size()); int num_available_updates = 0; for(int i : target_mod_indices) { installed_mods_[i].remote_update_time = nexus::Api::getNexusPage(installed_mods_[i].remote_source).mod.updated_time; if(installed_mods_[i].remote_update_time > installed_mods_[i].install_time) num_available_updates++; node.advance(); } if(num_available_updates > 0) log_(Log::LOG_INFO, std::format("Found updates for {} mod{}.", num_available_updates, num_available_updates == 1 ? "" : "s")); else log_(Log::LOG_INFO, "No mod updates found."); updateSettings(true); } std::string ModdedApplication::generalizeSteamPath(const std::string& path) { std::string modified_path = path; std::regex install_regex(R"((\/.*\/steamapps\/common\/.*?)(?:\/.*)?)"); std::regex prefix_regex( R"((\/.*\/steamapps\/compatdata\/\d+\/pfx\/(?:drive_c|dosdevices\/c:))(?:\/.*)?)"); std::regex home_regex(R"(((?:\/home\/.+?)|~)(?:\/.*)?)"); std::smatch match; if(std::regex_match(path, match, install_regex)) modified_path.replace(0, match[1].length(), "$STEAM_INSTALL_PATH$"); else if(std::regex_match(path, match, prefix_regex)) modified_path.replace(0, match[1].length(), "$STEAM_PREFIX_PATH$"); else if(std::regex_match(path, match, home_regex)) modified_path.replace(0, match[1].length(), "$HOME$"); return modified_path; } void ModdedApplication::updateSteamIconPath() { std::regex old_path_regex(R"((.*?/steam/appcache/librarycache)/(\d+)_icon\.jpg)"); std::smatch match; std::string path_str = icon_path_.string(); std::regex_match(path_str, match, old_path_regex); if(match.empty() || sfs::exists(icon_path_)) return; sfs::path steam_path(match[1].str()); steam_path /= match[2].str(); if(!sfs::exists(steam_path)) return; std::regex name_regex(R"(([0-9a-fA-F]{40})\.jpg)"); for(const auto& dir_entry : sfs::directory_iterator(steam_path)) { const std::string file_name = dir_entry.path().filename(); if(std::regex_match(file_name, name_regex)) { icon_path_ = steam_path / file_name; updateSettings(true); return; } } }