diff --git a/CMakeLists.txt b/CMakeLists.txt index 40eb45dc..2c58a9bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,12 +49,13 @@ set (LOOT_SRC "${CMAKE_SOURCE_DIR}/src/backend/metadata.cpp" "${CMAKE_SOURCE_DIR}/src/backend/game.cpp" "${CMAKE_SOURCE_DIR}/src/backend/helpers.cpp" "${CMAKE_SOURCE_DIR}/src/backend/globals.cpp" - "${CMAKE_SOURCE_DIR}/src/backend/generators.cpp") + "${CMAKE_SOURCE_DIR}/src/backend/generators.cpp" + "${CMAKE_SOURCE_DIR}/src/backend/sort.cpp") set (LOOT_GUI_SRC ${LOOT_SRC} # Code the API doesn't need. "${CMAKE_SOURCE_DIR}/src/backend/graph.cpp" - "${CMAKE_SOURCE_DIR}/src/backend/network.cpp" + "${CMAKE_SOURCE_DIR}/src/backend/git.cpp" # Actual GUI code. "${CMAKE_SOURCE_DIR}/src/gui/main_win.cpp" "${CMAKE_SOURCE_DIR}/src/gui/handler.cpp" diff --git a/src/api/api.cpp b/src/api/api.cpp index 192cb183..5325af0d 100644 --- a/src/api/api.cpp +++ b/src/api/api.cpp @@ -158,6 +158,21 @@ unsigned int c_error(const unsigned int code, const std::string& what) { return c_error(loot::error(code, what.c_str())); } +//////////////////////////////////// +// Dummy Masterlist member functions +//////////////////////////////////// + +// The API doesn't depend on libgit2, by not compiling ".git.cpp", so the member functions are defined +// below as dummies. + +namespace loot { + void Masterlist::GetGitInfo(boost::filesystem::path& path) {} + + void Masterlist::Update(Game& game, const unsigned int language) { + this->MetadataList::Load(game.MasterlistPath()); + } +} + ////////////////////////////// // Error Handling Functions diff --git a/src/backend/game.cpp b/src/backend/game.cpp index 32bc7774..b40ffd56 100644 --- a/src/backend/game.cpp +++ b/src/backend/game.cpp @@ -28,8 +28,11 @@ #include "error.h" #include "metadata.h" #include "parsers.h" +#include "streams.h" +#include "generators.h" #include +#include using namespace std; @@ -59,6 +62,128 @@ namespace loot { return games; } + size_t SelectGame(const YAML::Node& settings, const std::vector& games, const std::string& cmdLineGame) { + string preferredGame(cmdLineGame); + if (preferredGame.empty()) { + // Get preferred game from settings. + if (settings["Game"] && settings["Game"].as() != "auto") + preferredGame = settings["Game"].as(); + else if (settings["Last Game"] && settings["Last Game"].as() != "auto") + preferredGame = settings["Last Game"].as(); + } + + // Get index of preferred game if there is one. + for (size_t i = 0; i < games.size(); ++i) { + if (preferredGame.empty() && games[i].IsInstalled()) + return i; + else if (!preferredGame.empty() && preferredGame == games[i].FolderName() && games[i].IsInstalled()) + return i; + } + throw error(error::no_game_detected, "None of the supported games were detected."); + } + + // MetadataList member functions + //------------------------------ + + void MetadataList::Load(const boost::filesystem::path& filepath) { + plugins.clear(); + messages.clear(); + + BOOST_LOG_TRIVIAL(debug) << "Loading file: " << filepath; + + loot::ifstream in(filepath); + YAML::Node metadataList = YAML::Load(in); + in.close(); + + if (metadataList["plugins"]) + plugins = metadataList["plugins"].as< list >(); + if (metadataList["globals"]) + messages = metadataList["globals"].as< list >(); + + BOOST_LOG_TRIVIAL(debug) << "File loaded successfully."; + } + + void MetadataList::Save(const boost::filesystem::path& filepath) { + YAML::Emitter yout; + yout.SetIndent(2); + yout << YAML::BeginMap + << YAML::Key << "plugins" << YAML::Value << plugins + << YAML::Key << "globals" << YAML::Value << messages + << YAML::EndMap; + + loot::ofstream uout(filepath); + uout << yout.c_str(); + uout.close(); + } + + bool MetadataList::operator == (const MetadataList& rhs) const { + if (this->plugins.size() != rhs.plugins.size() || this->messages.size() != rhs.messages.size()) { + BOOST_LOG_TRIVIAL(info) << "Metadata edited for some plugin, new and old userlists differ in size."; + return false; + } + else { + for (const auto& rhsPlugin : rhs.plugins) { + const auto it = std::find(this->plugins.begin(), this->plugins.end(), rhsPlugin); + + if (it == this->plugins.end()) { + BOOST_LOG_TRIVIAL(info) << "Metadata added for plugin: " << it->Name(); + return false; + } + + if (!it->DiffMetadata(rhsPlugin).HasNameOnly()) { + BOOST_LOG_TRIVIAL(info) << "Metadata edited for plugin: " << it->Name(); + return false; + } + } + // Messages are compared exactly by the '==' operator, so there's no need to do a more + // fine-grained check. + for (const auto& rhsMessage : rhs.messages) { + const auto it = std::find(this->messages.begin(), this->messages.end(), rhsMessage); + + if (it == this->messages.end()) { + return false; + } + } + } + return true; + } + + // Masterlist member functions + //---------------------------- + + void Masterlist::Load(Game& game, const unsigned int language) { + try { + Update(game, language); + } + catch (error& e) { + if (e.code() != error::ok) { + // Error wasn't a parsing error. Need to try parsing masterlist if it exists. + try { + MetadataList::Load(game.MasterlistPath()); + } + catch (...) {} + } + throw e; + } + } + + std::string Masterlist::GetRevision(const boost::filesystem::path& path) { + if (revision.empty()) + GetGitInfo(path); + + return revision; + } + + std::string Masterlist::GetDate(const boost::filesystem::path& path) { + if (date.empty()) + GetGitInfo(path); + + return date; + } + + // Game member functions + //---------------------- + Game::Game() : id(Game::autodetect) {} Game::Game(const unsigned int gameCode, const std::string& folder) : id(gameCode) { @@ -514,19 +639,49 @@ namespace loot { } void Game::LoadPlugins(bool headersOnly) { - //Add all plugins in data folder not already in the hashset to the hashset, and load them. - for (fs::directory_iterator it(DataPath()); it != fs::directory_iterator(); ++it) { + boost::thread_group group; + uintmax_t meanFileSize = 0; + unordered_map tempMap; + std::vector groupPlugins; + //First calculate the mean plugin size. Store it temporarily in a map to reduce filesystem lookups and file size recalculation. + for (fs::directory_iterator it(this->DataPath()); it != fs::directory_iterator(); ++it) { if (fs::is_regular_file(it->status()) && IsPlugin(it->path().string())) { - const string filename = it->path().filename().string(); - if (plugins.find(filename) == plugins.end()) - plugins.insert(std::pair(filename, Plugin(filename))); + uintmax_t fileSize = fs::file_size(it->path()); + meanFileSize += fileSize; + + tempMap.emplace(it->path().filename().string(), fileSize); } } + meanFileSize /= tempMap.size(); //Rounding error, but not important. - for (auto &pluginPair: plugins) { - pluginPair.second = Plugin(*this, pluginPair.second.Name(), headersOnly); + //Now load plugins. + for (const auto &pluginPair : tempMap) { + + BOOST_LOG_TRIVIAL(info) << "Found plugin: " << pluginPair.first; + + auto plugin = plugins.emplace(pluginPair.first, Plugin(pluginPair.first)); + + if (pluginPair.second > meanFileSize) { + BOOST_LOG_TRIVIAL(trace) << "Creating individual loading thread for: " << pluginPair.first; + group.create_thread([this, plugin, headersOnly]() { + BOOST_LOG_TRIVIAL(trace) << "Loading " << plugin.first->second.Name() << " individually."; + plugin.first->second = Plugin(*this, plugin.first->first, headersOnly); + }); + } + else { + groupPlugins.push_back(&plugin.first->second); + } } + group.create_thread([this, &groupPlugins, headersOnly]() { + for (auto plugin : groupPlugins) { + const std::string name = plugin->Name(); + BOOST_LOG_TRIVIAL(trace) << "Loading " << plugin->Name() << " as part of a group."; + *plugin = Plugin(*this, name, headersOnly); + } + }); + + group.join_all(); } void Game::CreateLOOTGameFolder() { diff --git a/src/backend/game.h b/src/backend/game.h index b0a63aed..7da286de 100644 --- a/src/backend/game.h +++ b/src/backend/game.h @@ -40,6 +40,8 @@ #include namespace loot { + + class Game; /* Each Game object should store the config details specific to that game. It should also store the plugin and masterlist data for that game. @@ -51,6 +53,33 @@ namespace loot { data. Plugin data should be loaded as header-only and as full data. */ + class MetadataList { + public: + void Load(const boost::filesystem::path& filepath); + void Save(const boost::filesystem::path& filepath); + + bool operator == (const MetadataList& rhs) const; //Compares content. + + std::list plugins; + std::list messages; + }; + + class Masterlist : public MetadataList { + public: + + void Load(Game& game, const unsigned int language); //Handles update with load fallback. + void Update(Game& game, const unsigned int language); + + std::string GetRevision(const boost::filesystem::path& path); + std::string GetDate(const boost::filesystem::path& path); + + private: + void GetGitInfo(const boost::filesystem::path& path); + + std::string revision; + std::string date; + }; + class Game { public: //Game functions. @@ -82,7 +111,6 @@ namespace loot { boost::filesystem::path ReportDataPath() const; //Game plugin functions. - bool IsActive(const std::string& plugin) const; void GetLoadOrder(std::list& loadOrder) const; @@ -92,12 +120,15 @@ namespace loot { void RedatePlugins(); //Change timestamps to match load order (Skyrim only). void LoadPlugins(bool headersOnly); //Loads all installed plugins. + void SortPrep(const unsigned int language, std::list& messages, std::function progressCallback); + std::list Sort(const unsigned int language, std::list& messages, std::function progressCallback); + //Caches for condition results, active plugins and CRCs. std::unordered_map conditionCache; //Holds lowercased strings. std::unordered_map crcCache; //Holds lowercased strings. //Plugin data and metadata lists. - MetadataList masterlist; + Masterlist masterlist; MetadataList userlist; std::unordered_map plugins; //Map so that plugin data can be edited. @@ -128,6 +159,8 @@ namespace loot { }; std::vector GetGames(const YAML::Node& settings); + + size_t SelectGame(const YAML::Node& settings, const std::vector& games, const std::string& cmdLineGame); } #endif diff --git a/src/backend/network.cpp b/src/backend/git.cpp similarity index 91% rename from src/backend/network.cpp rename to src/backend/git.cpp index f3d33d1e..8202403e 100644 --- a/src/backend/network.cpp +++ b/src/backend/git.cpp @@ -22,11 +22,11 @@ . */ -#include "network.h" #include "error.h" #include "parsers.h" #include "streams.h" #include "helpers.h" +#include "game.h" #include #include @@ -92,11 +92,6 @@ namespace loot { std::string ui_message; }; - int progress_cb(const char *str, int len, void *data) { - BOOST_LOG_TRIVIAL(info) << string(str, len); - return 0; - } - bool are_files_equal(const void * buf1, size_t buf1_size, const void * buf2, size_t buf2_size) { if (buf1_size != buf2_size) return false; @@ -114,12 +109,17 @@ namespace loot { return git_repository_open_ext(NULL, path.string().c_str(), GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) == 0; } - std::pair GetMasterlistRevision(const Game& game) { - if (!fs::exists(game.MasterlistPath().parent_path() / ".git")) { - return pair("Unknown: Git repository missing", "Unknown: Git repository missing"); + void Masterlist::GetGitInfo(const boost::filesystem::path& path) { + if (!fs::exists(path.parent_path() / ".git")) { + revision = "Unknown: Git repository missing"; + date = "Unknown: Git repository missing"; + return; + } + else if (!fs::exists(path)) { + revision = "N/A: No masterlist present"; + date = "N/A: No masterlist present"; + return; } - else if (!fs::exists(game.MasterlistPath())) - return pair("N/A: No masterlist present", "N/A: No masterlist present"); else { /* Compares HEAD to the working dir. 1. Get an object for the masterlist in HEAD. @@ -128,9 +128,9 @@ namespace loot { 4. Compare the file and blob buffers. */ git_handler git; - git.ui_message = "An error occurred while trying to read the local masterlist's version. If this error happens again, try deleting the \".git\" folder in \"%LOCALAPPDATA%\\LOOT\\" + game.FolderName() + "\"."; + git.ui_message = "An error occurred while trying to read the local masterlist's version. If this error happens again, try deleting the \".git\" folder in " + path.parent_path().string() + "."; BOOST_LOG_TRIVIAL(debug) << "Existing repository found, attempting to open it."; - git.call(git_repository_open(&git.repo, game.MasterlistPath().parent_path().string().c_str())); + git.call(git_repository_open(&git.repo, path.parent_path().string().c_str())); BOOST_LOG_TRIVIAL(trace) << "Getting HEAD masterlist object."; git.call(git_revparse_single(&git.obj, git.repo, "HEAD:masterlist.yaml")); @@ -140,7 +140,7 @@ namespace loot { BOOST_LOG_TRIVIAL(debug) << "Opening masterlist in working directory."; std::string mlist; - loot::ifstream ifile(game.MasterlistPath().string().c_str(), ios::binary); + loot::ifstream ifile(path, ios::binary); if (ifile.fail()) throw error(error::path_read_fail, "Couldn't open masterlist."); ifile.unsetf(ios::skipws); // No white space skipping! @@ -153,7 +153,6 @@ namespace loot { BOOST_LOG_TRIVIAL(debug) << "Comparing files."; if (are_files_equal(git_blob_rawcontent(git.blob), git_blob_rawsize(git.blob), mlist.data(), mlist.length())) { - string revision, date; //Need to get the HEAD object, because the individual file has a different SHA. git_object_free(git.obj); git.obj = nullptr; //Just to be safe. @@ -176,15 +175,17 @@ namespace loot { out << boost::locale::as::ftime("%Y-%m-%d") << dateTime; date = out.str(); - return pair(revision, date); + return; } else { - return pair("Unknown: Masterlist edited", "Unknown: Masterlist edited"); + revision = "Unknown: Masterlist edited"; + date = "Unknown: Masterlist edited"; + return; } } } - std::pair UpdateMasterlist(Game& game, std::list& parsingErrors, std::list& plugins, std::list& messages, const unsigned int language) { + void Masterlist::Update(Game& game, const unsigned int language) { git_handler git; fs::path repo_path = game.MasterlistPath().parent_path(); string repo_branch = game.RepoBranch(); @@ -387,7 +388,7 @@ namespace loot { // and try again. bool parsingFailed = false; - string revision, date; + std::string parsingError; git.ui_message = "An error occurred while trying to read information on the updated masterlist. If this error happens again, try deleting the \".git\" folder in \"%LOCALAPPDATA%\\LOOT\\" + game.FolderName() + "\"."; do { // Get some descriptive info about what was checked out. @@ -428,14 +429,7 @@ namespace loot { //Now try parsing the masterlist. BOOST_LOG_TRIVIAL(debug) << "Testing masterlist parsing."; try { - loot::ifstream in(game.MasterlistPath()); - YAML::Node mlist = YAML::Load(in); - in.close(); - - if (mlist["globals"]) - messages = mlist["globals"].as< list >(); - if (mlist["plugins"]) - plugins = mlist["plugins"].as< list >(); + this->MetadataList::Load(game.MasterlistPath()); for (auto &plugin: plugins) { plugin.EvalAllConditions(game, language); @@ -466,10 +460,12 @@ namespace loot { BOOST_LOG_TRIVIAL(trace) << "Performing a Git checkout of HEAD."; git.call(git_checkout_head(git.repo, &checkout_opts)); - parsingErrors.push_back(loot::Message(loot::Message::error, boost::locale::translate("Masterlist revision").str() + " " + string(revision) + ": " + e.what() + " " + boost::locale::translate("Rolled back to the previous revision.").str())); + if (parsingError.empty()) + parsingError = boost::locale::translate("Masterlist revision").str() + " " + string(revision) + ": " + e.what() + " " + boost::locale::translate("Rolled back to the previous revision.").str(); } } while (parsingFailed); - return pair(revision, date); + if (!parsingError.empty()) + throw error(error::ok, parsingError); //Throw an OK because the process still completed in a successful state. } } diff --git a/src/backend/graph.cpp b/src/backend/graph.cpp index bd9f49ec..fd5bb6c8 100644 --- a/src/backend/graph.cpp +++ b/src/backend/graph.cpp @@ -90,7 +90,7 @@ namespace loot { return false; } - void Sort(const PluginGraph& graph, std::list& plugins) { + std::list Sort(const PluginGraph& graph) { //Topological sort requires an index map, which std::list-based VertexList graphs don't have, so one needs to be built separately. @@ -105,15 +105,19 @@ namespace loot { std::list sortedVertices; boost::topological_sort(graph, std::front_inserter(sortedVertices), boost::vertex_index_map(v_index_map)); + /* Sorting now evaluates conditions inside the graph, so existing plugins list is missing + data present in the graph, so we need to swap the two lists. */ BOOST_LOG_TRIVIAL(info) << "Calculated order: "; - list tempPlugins; + list plugins; for (const auto &vertex: sortedVertices) { BOOST_LOG_TRIVIAL(info) << '\t' << graph[vertex].Name(); - tempPlugins.push_back(graph[vertex].Name()); + plugins.push_back(graph[vertex]); } + return plugins; + //Now sort exist plugins list according to order in tempPlugins. - plugins.sort([tempPlugins](const Plugin& first, const Plugin& second){ + /*plugins.sort([tempPlugins](const Plugin& first, const Plugin& second){ //Find both plugins, and compare distances from beginning. auto fIt = find(tempPlugins.begin(), tempPlugins.end(), first); auto sIt = find(tempPlugins.begin(), tempPlugins.end(), second); @@ -123,6 +127,7 @@ namespace loot { return distance(tempPlugins.begin(), fIt) < distance(tempPlugins.begin(), sIt); }); + */ } void CheckForCycles(const PluginGraph& graph) { diff --git a/src/backend/graph.h b/src/backend/graph.h index 2133c1db..366b7d2a 100644 --- a/src/backend/graph.h +++ b/src/backend/graph.h @@ -54,7 +54,7 @@ namespace loot { bool GetVertexByName(const PluginGraph& graph, const std::string& name, vertex_t& vertex); - void Sort(const PluginGraph& graph, std::list& plugins); + std::list Sort(const PluginGraph& graph); void CheckForCycles(const PluginGraph& graph); diff --git a/src/backend/metadata.cpp b/src/backend/metadata.cpp index 94d22e60..c6538d2d 100644 --- a/src/backend/metadata.cpp +++ b/src/backend/metadata.cpp @@ -763,24 +763,6 @@ namespace loot { return boost::filesystem::exists(game.DataPath() / (name.substr(0, name.length() - 3) + "bsa")); } - void MetadataList::Load(boost::filesystem::path& filepath) { - plugins.clear(); - messages.clear(); - - BOOST_LOG_TRIVIAL(debug) << "Loading file: " << filepath; - - loot::ifstream in(filepath); - YAML::Node metadataList = YAML::Load(in); - in.close(); - - if (metadataList["plugins"]) - plugins = metadataList["plugins"].as< list >(); - if (metadataList["globals"]) - messages = metadataList["globals"].as< list >(); - - BOOST_LOG_TRIVIAL(debug) << "File loaded successfully."; - } - size_t plugin_hash::operator () (const Plugin& p) const { size_t seed = 0; boost::hash_combine(seed, p.Name()); diff --git a/src/backend/metadata.h b/src/backend/metadata.h index f7d76591..9633c40d 100644 --- a/src/backend/metadata.h +++ b/src/backend/metadata.h @@ -243,14 +243,6 @@ namespace loot { size_t numOverrideRecords; }; - class MetadataList { - public: - void Load(boost::filesystem::path& filepath); - - std::list plugins; - std::list messages; - }; - struct plugin_hash : std::unary_function { size_t operator () (const Plugin& p) const; }; diff --git a/src/backend/network.h b/src/backend/network.h deleted file mode 100644 index 51922343..00000000 --- a/src/backend/network.h +++ /dev/null @@ -1,40 +0,0 @@ -/* LOOT - - A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and - Fallout: New Vegas. - - Copyright (C) 2012-2014 WrinklyNinja - - This file is part of LOOT. - - LOOT is free software: you can redistribute - it and/or modify it under the terms of the GNU General Public License - as published by the Free Software Foundation, either version 3 of - the License, or (at your option) any later version. - - LOOT is distributed in the hope that it will - be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with LOOT. If not, see - . -*/ - -#ifndef __LOOT_NETWORK__ -#define __LOOT_NETWORK__ - -#include -#include - -#include "game.h" -#include "metadata.h" - -namespace loot { - - std::pair UpdateMasterlist(Game& game, std::list& parsingErrors, std::list& plugins, std::list& messages, const unsigned int language); - - std::pair GetMasterlistRevision(const Game& game); -} -#endif diff --git a/src/backend/sort.cpp b/src/backend/sort.cpp new file mode 100644 index 00000000..178528e7 --- /dev/null +++ b/src/backend/sort.cpp @@ -0,0 +1,203 @@ +/* LOOT + +A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and +Fallout: New Vegas. + +Copyright (C) 2014 WrinklyNinja + +This file is part of LOOT. + +LOOT is free software: you can redistribute +it and/or modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, either version 3 of +the License, or (at your option) any later version. + +LOOT is distributed in the hope that it will +be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with LOOT. If not, see +. +*/ + +#include "game.h" +#include "helpers.h" +#include "graph.h" + +#include +#include +#include +#include + +using namespace std; + +using boost::format; + +namespace loc = boost::locale; +namespace fs = boost::filesystem; + +namespace loot { + + void Game::SortPrep(const unsigned int language, std::list& messages, std::function progressCallback) { + boost::thread_group group; + + BOOST_LOG_TRIVIAL(info) << "Using message language: " << Language(language).Name(); + + /////////////////////////////////////////////////////// + // Load Plugins & Lists + /////////////////////////////////////////////////////// + + progressCallback("Reading installed plugins..."); + + group.create_thread([this, language, &messages]() { + try { + this->masterlist.Load(*this, language); + } + catch (exception &e) { + messages.push_back(loot::Message(loot::Message::error, (format(loc::translate("Masterlist parsing failed. Details: %1%")) % e.what()).str())); + } + }); + group.create_thread([this]() { + this->LoadPlugins(false); + }); + group.join_all(); + + //Now load userlist. + if (fs::exists(this->UserlistPath())) { + BOOST_LOG_TRIVIAL(debug) << "Parsing userlist at: " << this->UserlistPath(); + + try { + this->userlist.Load(this->UserlistPath()); + } + catch (exception& e) { + BOOST_LOG_TRIVIAL(error) << "Userlist parsing failed. Details: " << e.what(); + messages.push_back(loot::Message(loot::Message::error, (format(loc::translate("Userlist parsing failed. Details: %1%")) % e.what()).str())); + } + } + + /////////////////////////////////////////////////////// + // Evaluate Global Messages + /////////////////////////////////////////////////////// + + progressCallback("Evaluating global messages..."); + + //Merge all global message lists. + BOOST_LOG_TRIVIAL(debug) << "Merging all global message lists."; + if (!this->masterlist.messages.empty()) + messages.insert(messages.end(), this->masterlist.messages.begin(), this->masterlist.messages.end()); + if (!this->userlist.messages.empty()) + messages.insert(messages.end(), this->userlist.messages.begin(), this->userlist.messages.end()); + + //Evaluate any conditions in the global messages. + BOOST_LOG_TRIVIAL(debug) << "Evaluating global message conditions."; + try { + list::iterator it = messages.begin(); + while (it != messages.end()) { + if (!it->EvalCondition(*this, language)) + it = messages.erase(it); + else + ++it; + } + } + catch (std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "A global message contains a condition that could not be evaluated. Details: " << e.what(); + messages.push_back(loot::Message(loot::Message::error, (format(loc::translate("A global message contains a condition that could not be evaluated. Details: %1%")) % e.what()).str())); + } + + //////////////////////////////////////////////////////// + // Slim down masterlist + //////////////////////////////////////////////////////// + // + // Userlist data gets replaced every time sorting is looped, so there's no point evaluating it + // outside the loop, but the masterlist can be slimmed down now. + + progressCallback("Filtering masterlist..."); + + std::list tempMasterlistPlugins; + for (const auto &plugin : this->plugins) { + list::iterator pos = std::find(this->masterlist.plugins.begin(), this->masterlist.plugins.end(), plugin.second); + + if (pos != this->masterlist.plugins.end()) { + // The plugin exists in the masterlist, store a copy of its metadata. + tempMasterlistPlugins.push_back(*pos); + } + } + // Now replace the current full masterlist plugin metadata list with the install-specific one. + this->masterlist.plugins = tempMasterlistPlugins; + } + + + std::list Game::Sort(const unsigned int language, std::list& messages, std::function progressCallback) { + //Create a plugin graph containing the plugin and masterlist data. + loot::PluginGraph graph; + + progressCallback("Building plugin graph..."); + BOOST_LOG_TRIVIAL(info) << "Merging masterlist, userlist into plugin list, evaluating conditions and checking for install validity."; + for (const auto &plugin : this->plugins) { + vertex_t v = boost::add_vertex(plugin.second, graph); + list::iterator pos; + BOOST_LOG_TRIVIAL(trace) << "Merging for plugin \"" << graph[v].Name() << "\""; + + //Check if there is a plugin entry in the masterlist. This will also find matching regex entries. + pos = std::find(this->masterlist.plugins.begin(), this->masterlist.plugins.end(), graph[v]); + + if (pos != this->masterlist.plugins.end()) { + BOOST_LOG_TRIVIAL(trace) << "Merging masterlist data down to plugin list data."; + graph[v].MergeMetadata(*pos); + } + + //Check if there is a plugin entry in the userlist. This will also find matching regex entries. + pos = std::find(this->userlist.plugins.begin(), this->userlist.plugins.end(), graph[v]); + + if (pos != this->userlist.plugins.end() && pos->Enabled()) { + BOOST_LOG_TRIVIAL(trace) << "Merging userlist data down to plugin list data."; + graph[v].MergeMetadata(*pos); + } + + //Now that items are merged, evaluate any conditions they have. + BOOST_LOG_TRIVIAL(trace) << "Evaluate conditions for merged plugin data."; + try { + graph[v].EvalAllConditions(*this, language); + } + catch (std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "\"" << graph[v].Name() << "\" contains a condition that could not be evaluated. Details: " << e.what(); + messages.push_back(loot::Message(loot::Message::error, (format(loc::translate("\"%1%\" contains a condition that could not be evaluated. Details: %2%")) % graph[v].Name() % e.what()).str())); + } + + //Also check install validity. + BOOST_LOG_TRIVIAL(trace) << "Checking that the current install is valid according to this plugin's data."; + graph[v].CheckInstallValidity(*this); + } + + BOOST_LOG_TRIVIAL(info) << "Building the plugin dependency graph..."; + + //Now add the interactions between plugins to the graph as edges. + std::map overriddenPriorities; + BOOST_LOG_TRIVIAL(debug) << "Adding non-overlap edges."; + loot::AddSpecificEdges(graph, overriddenPriorities); + + BOOST_LOG_TRIVIAL(debug) << "Adding priority edges."; + loot::AddPriorityEdges(graph); + + BOOST_LOG_TRIVIAL(debug) << "Adding overlap edges."; + loot::AddOverlapEdges(graph); + + progressCallback("Checking for graph cycles..."); + + BOOST_LOG_TRIVIAL(info) << "Checking to see if the graph is cyclic."; + loot::CheckForCycles(graph); + + for (const auto &overriddenPriority : overriddenPriorities) { + vertex_t vertex; + if (loot::GetVertexByName(graph, overriddenPriority.first, vertex)) { + graph[vertex].Priority(overriddenPriority.second); + } + } + + BOOST_LOG_TRIVIAL(info) << "Performing a topological sort."; + progressCallback("Performing topological sort..."); + return loot::Sort(graph); + } +} \ No newline at end of file diff --git a/src/gui/editor.cpp b/src/gui/editor.cpp index 7b45d834..1d9d78c1 100644 --- a/src/gui/editor.cpp +++ b/src/gui/editor.cpp @@ -1041,8 +1041,10 @@ void EditorPanel::ApplyCurrentEdits() { ApplyEdits(currentPlugin); } -const std::list& EditorPanel::GetNewUserlist() const { - return _editedPlugins; +loot::MetadataList EditorPanel::GetNewUserlist() const { + loot::MetadataList newUserlist; + newUserlist.plugins = _editedPlugins; + return newUserlist; } loot::Plugin EditorPanel::GetMasterData(const wxString& plugin) const { @@ -1197,7 +1199,7 @@ void MiniEditor::OnResize(wxSizeEvent& event) { event.Skip(); } -const std::list& MiniEditor::GetNewUserlist() const { +loot::MetadataList MiniEditor::GetNewUserlist() const { return editorPanel->GetNewUserlist(); } @@ -1206,7 +1208,7 @@ const std::list& MiniEditor::GetNewUserlist() const { // Full Editor Class /////////////////////////////////// -FullEditor::FullEditor(wxWindow *parent, const wxString& title, wxPoint pos, wxSize size, const std::string userlistPath, const std::list& basePlugins, std::list& editedPlugins, const unsigned int language, const loot::Game& game, YAML::Node &settings) : wxFrame(parent, wxID_ANY, title, pos, size), _userlistPath(userlistPath), _settings(settings) { +FullEditor::FullEditor(wxWindow *parent, const wxString& title, wxPoint pos, wxSize size, const boost::filesystem::path& userlistPath, const std::list& basePlugins, std::list& editedPlugins, const unsigned int language, const loot::Game& game, YAML::Node &settings) : wxFrame(parent, wxID_ANY, title, pos, size), _userlistPath(userlistPath), _settings(settings) { //Set up content. editorPanel = new EditorPanel(this, basePlugins, editedPlugins, language, game); applyBtn = new wxButton(this, BUTTON_Apply, translate("Save Changes")); @@ -1245,17 +1247,8 @@ void FullEditor::OnQuit(wxCommandEvent& event) { BOOST_LOG_TRIVIAL(debug) << "Saving metadata edits to userlist."; - //Save edits to userlist. - YAML::Emitter yout; - yout.SetIndent(2); - yout << YAML::BeginMap - << YAML::Key << "plugins" << YAML::Value << editorPanel->GetNewUserlist() - << YAML::EndMap; - - boost::filesystem::path p(_userlistPath); - loot::ofstream out(p); - out << yout.c_str(); - out.close(); + loot::MetadataList userlist = editorPanel->GetNewUserlist(); + userlist.Save(_userlistPath); } Close(); } diff --git a/src/gui/editor.h b/src/gui/editor.h index 21e1971a..ef18a0cc 100644 --- a/src/gui/editor.h +++ b/src/gui/editor.h @@ -26,7 +26,9 @@ #include "ids.h" #include "misc.h" + #include "../backend/metadata.h" +#include "../backend/game.h" #include #include @@ -69,7 +71,7 @@ public: void SetSimpleView(bool on = true); void ApplyCurrentEdits(); - const std::list& GetNewUserlist() const; + loot::MetadataList GetNewUserlist() const; void OnPluginSelect(wxListEvent& event); void OnPluginListRightClick(wxListEvent& event); @@ -134,7 +136,7 @@ public: void OnApply(wxCommandEvent& event); void OnResize(wxSizeEvent& event); - const std::list& GetNewUserlist() const; + loot::MetadataList GetNewUserlist() const; private: EditorPanel * editorPanel; wxStaticText * descText; @@ -144,7 +146,7 @@ private: class FullEditor : public wxFrame { public: - FullEditor(wxWindow *parent, const wxString& title, wxPoint pos, wxSize size, const std::string userlistPath, const std::list& basePlugins, std::list& editedPlugins, const unsigned int language, const loot::Game& game, YAML::Node &settings); + FullEditor(wxWindow *parent, const wxString& title, wxPoint pos, wxSize size, const boost::filesystem::path& userlistPath, const std::list& basePlugins, std::list& editedPlugins, const unsigned int language, const loot::Game& game, YAML::Node &settings); void OnQuit(wxCommandEvent& event); void OnClose(wxCloseEvent &event); @@ -153,7 +155,7 @@ private: wxButton * applyBtn; wxButton * cancelBtn; - const std::string _userlistPath; + const boost::filesystem::path _userlistPath; YAML::Node& _settings; }; #endif diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 859a2e7e..0c559f40 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -33,20 +33,13 @@ #include "../backend/error.h" #include "../backend/helpers.h" #include "../backend/generators.h" -#include "../backend/network.h" #include "../backend/streams.h" -#include "../backend/graph.h" -#include #include -#include -#include #include -#include #include #include -#include #include #include #include @@ -55,7 +48,6 @@ #include #include #include -#include #include #include @@ -71,123 +63,6 @@ using boost::format; namespace fs = boost::filesystem; namespace loc = boost::locale; - -struct plugin_loader { - plugin_loader(loot::Plugin& plugin, loot::Game& game) : _plugin(plugin), _game(game) { - } - - void operator () () { - _plugin = loot::Plugin(_game, _plugin.Name(), false); - } - - loot::Plugin& _plugin; - loot::Game& _game; - string _filename; - bool _b; -}; - -struct plugin_list_loader { - plugin_list_loader(list& plugins, loot::Game& game) : _plugins(plugins), _game(game) {} - - void operator () () { - for (auto &plugin : _plugins) { - if (skipPlugins.find(plugin.Name()) == skipPlugins.end()) { - plugin = loot::Plugin(_game, plugin.Name(), false); - } - } - } - - list& _plugins; - loot::Game& _game; - set skipPlugins; -}; - -struct masterlist_updater_parser { - masterlist_updater_parser(bool doUpdate, loot::Game& game, list& errors, list& plugins, list& messages, string& revision, string& date, const unsigned int language) : _doUpdate(doUpdate), _game(game), _errors(errors), _plugins(plugins), _messages(messages), _revision(revision), _date(date), _language(language) {} - - void operator () () { - - if (_doUpdate) { - BOOST_LOG_TRIVIAL(debug) << "Updating masterlist"; - try { - pair ret = UpdateMasterlist(_game, _errors, _plugins, _messages, _language); - _revision = ret.first; - _date = ret.second; - } catch (std::exception& e) { - _plugins.clear(); - _messages.clear(); - BOOST_LOG_TRIVIAL(error) << "Masterlist update failed. Details: " << e.what(); - _errors.push_back(loot::Message(loot::Message::error, (format(loc::translate("Masterlist update failed. Details: %1%")) % e.what()).str())); - //Try getting masterlist revision anyway. - try { - pair ret = GetMasterlistRevision(_game); - _revision = ret.first; - _date = ret.second; - } - catch (std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "Masterlist revision check failed. Details: " << e.what(); - _errors.push_back(loot::Message(loot::Message::error, (format(loc::translate("Masterlist revision check failed. Details: %1%")) % e.what()).str())); - } - } - } - else { - BOOST_LOG_TRIVIAL(debug) << "Getting masterlist revision"; - try { - pair ret = GetMasterlistRevision(_game); - _revision = ret.first; - _date = ret.second; - } - catch (std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "Masterlist revision check failed. Details: " << e.what(); - _errors.push_back(loot::Message(loot::Message::error, (format(loc::translate("Masterlist revision check failed. Details: %1%")) % e.what()).str())); - } - } - - if (_plugins.empty() && _messages.empty() && fs::exists(_game.MasterlistPath())) { - - BOOST_LOG_TRIVIAL(debug) << "Parsing masterlist..."; - try { - loot::ifstream in(_game.MasterlistPath()); - YAML::Node mlist = YAML::Load(in); - in.close(); - - if (mlist["globals"]) - _messages = mlist["globals"].as< list >(); - if (mlist["plugins"]) - _plugins = mlist["plugins"].as< list >(); - } catch (YAML::Exception& e) { - BOOST_LOG_TRIVIAL(error) << "Masterlist parsing failed. Details: " << e.what(); - _errors.push_back(loot::Message(loot::Message::error, (format(loc::translate("Masterlist parsing failed. Details: %1%")) % e.what()).str())); - } - BOOST_LOG_TRIVIAL(debug) << "Finished parsing masterlist."; - - } - - if (_revision.empty()) { - if (fs::exists(_game.MasterlistPath())) - _revision = loc::translate("Unknown"); - else - _revision = loc::translate("No masterlist"); - } - - if (_date.empty()) { - if (fs::exists(_game.MasterlistPath())) - _date = loc::translate("Unknown"); - else - _date = loc::translate("No masterlist"); - } - } - - bool _doUpdate; - loot::Game& _game; - list& _errors; - list& _plugins; - list& _messages; - string& _revision; - string& _date; - unsigned int _language; -}; - bool LOOT::OnInit() { //Check if GUI is already running. @@ -355,7 +230,6 @@ bool LOOT::OnInit() { BOOST_LOG_TRIVIAL(debug) << "Selecting game."; string target; - int gameIndex = -1; wxCmdLineEntryDesc cmdLineDesc[2]; cmdLineDesc[0].kind = wxCMD_LINE_OPTION; @@ -381,45 +255,25 @@ bool LOOT::OnInit() { break; } - if (target.empty()) { - if (_settings["Game"] && _settings["Game"].as() != "auto") - target = _settings["Game"].as(); - else if (_settings["Last Game"] && _settings["Last Game"].as() != "auto") - target = _settings["Last Game"].as(); + size_t gameIndex(0); + try { + gameIndex = SelectGame(_settings, _games, target); } - - if (!target.empty()) { - for (size_t i=0, max=_games.size(); i < max; ++i) { - if (target == _games[i].FolderName() && _games[i].IsInstalled()) - gameIndex = i; - } + catch (exception &) { + BOOST_LOG_TRIVIAL(error) << "None of the supported games were detected."; + wxMessageBox( + translate("Error: None of the supported games were detected."), + translate("LOOT: Error"), + wxOK | wxICON_ERROR, + nullptr); + return false; } - if (gameIndex < 0) { - //Set gameIndex to the first installed game. - for (size_t i=0, max=_games.size(); i < max; ++i) { - if (_games[i].IsInstalled()) { - gameIndex = i; - break; - } - } - if (gameIndex < 0) { - BOOST_LOG_TRIVIAL(error) << "None of the supported games were detected."; - wxMessageBox( - translate("Error: None of the supported games were detected."), - translate("LOOT: Error"), - wxOK | wxICON_ERROR, - nullptr); - return false; - } - } - loot::Game & _game(_games[gameIndex]); - BOOST_LOG_TRIVIAL(debug) << "Game selected is " << _game.Name(); + BOOST_LOG_TRIVIAL(debug) << "Game selected is " << _games[gameIndex].Name(); //Now that game is selected, initialise it. BOOST_LOG_TRIVIAL(debug) << "Initialising game-specific settings."; try { - _game.Init(); - *find(_games.begin(), _games.end(), _game) = _game; //Sync changes. + _games[gameIndex].Init(); } catch (std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Game-specific settings could not be initialised. " << e.what(); wxMessageBox( @@ -457,7 +311,7 @@ bool LOOT::OnInit() { //Create launcher window. BOOST_LOG_TRIVIAL(debug) << "Opening the main LOOT window."; - Launcher * launcher = new Launcher(wxT("LOOT"), _settings, &_game, _games, pos, size); + Launcher * launcher = new Launcher(wxT("LOOT"), _settings, _games, gameIndex, pos, size); launcher->SetIcon(wxIconLocation("LOOT.exe")); launcher->Show(); @@ -466,7 +320,7 @@ bool LOOT::OnInit() { return true; } -Launcher::Launcher(const wxChar *title, YAML::Node& settings, Game * game, vector& games, wxPoint pos, wxSize size) : wxFrame(nullptr, wxID_ANY, title, pos, size), _game(game), _settings(settings), _games(games) { +Launcher::Launcher(const wxChar *title, YAML::Node& settings, vector& games, size_t currentGame, wxPoint pos, wxSize size) : wxFrame(nullptr, wxID_ANY, title, pos, size), _settings(settings), _games(games), _currentGame(currentGame) { //Initialise menu items. wxMenuBar * MenuBar = new wxMenuBar(); @@ -496,7 +350,7 @@ Launcher::Launcher(const wxChar *title, YAML::Node& settings, Game * game, vecto //Game menu - set up initial item states here too. for (size_t i=0,max=_games.size(); i < max; ++i) { wxMenuItem * item = GameMenu->AppendRadioItem(MENU_LowestDynamicGameID + i, FromUTF8(_games[i].Name())); - if (*_game == _games[i]) + if (_games[_currentGame] == _games[i]) item->Check(); if (_games[i].IsInstalled()) @@ -543,13 +397,13 @@ Launcher::Launcher(const wxChar *title, YAML::Node& settings, Game * game, vecto if (!fs::exists(g_path_report)) ViewButton->Enable(false); - if (_game->Id() == loot::Game::tes5) + if (_games[_currentGame].Id() == loot::Game::tes5) RedatePluginsItem->Enable(true); else RedatePluginsItem->Enable(false); //Set title bar text. - SetTitle(FromUTF8("LOOT - " + _game->Name())); + SetTitle(FromUTF8("LOOT - " + _games[_currentGame].Name())); //Now set the layout and sizes. SetMenuBar(MenuBar); @@ -583,7 +437,7 @@ void Launcher::OnClose(wxCloseEvent& event) { _settings["windows"]["main"] = main; //Record game settings. - _settings["Last Game"] = _game->FolderName(); + _settings["Last Game"] = _games[_currentGame].FolderName(); _settings["Games"] = _games; //Save settings. @@ -613,7 +467,7 @@ void Launcher::OnViewLastReport(wxCommandEvent& event) { } //Create viewer window. BOOST_LOG_TRIVIAL(debug) << "Opening viewer window..."; - Viewer *viewer = new Viewer(this, translate("LOOT: Report Viewer"), FromUTF8(ToFileURL(g_path_report.string() + "?data=" + _game->ReportDataPath().string())), pos, size, _settings); + Viewer *viewer = new Viewer(this, translate("LOOT: Report Viewer"), FromUTF8(ToFileURL(g_path_report.string() + "?data=" + _games[_currentGame].ReportDataPath().string())), pos, size, _settings); viewer->Show(); BOOST_LOG_TRIVIAL(debug) << "Report displayed."; } @@ -644,10 +498,10 @@ void Launcher::OnOpenSettings(wxCommandEvent& event) { void Launcher::OnGameChange(wxCommandEvent& event) { BOOST_LOG_TRIVIAL(debug) << "Changing current game..."; - _game = &_games[event.GetId() - MENU_LowestDynamicGameID]; + _currentGame = event.GetId() - MENU_LowestDynamicGameID; try { - _game->Init(); //In case it hasn't already been done. - BOOST_LOG_TRIVIAL(debug) << "New game is " << _game->Name(); + _games[_currentGame].Init(); //In case it hasn't already been done. + BOOST_LOG_TRIVIAL(debug) << "New game is " << _games[_currentGame].Name(); } catch (std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Game-specific settings could not be initialised." << e.what(); @@ -657,8 +511,8 @@ void Launcher::OnGameChange(wxCommandEvent& event) { wxOK | wxICON_ERROR, nullptr); } - SetTitle(FromUTF8("LOOT - " + _game->Name())); - if (_game->Id() == loot::Game::tes5) + SetTitle(FromUTF8("LOOT - " + _games[_currentGame].Name())); + if (_games[_currentGame].Id() == loot::Game::tes5) RedatePluginsItem->Enable(true); else RedatePluginsItem->Enable(false); @@ -724,21 +578,16 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { BOOST_LOG_TRIVIAL(debug) << "Beginning sorting process."; - YAML::Node mlist, ulist; - list messages, mlist_messages, ulist_messages; - list mlist_plugins, ulist_plugins; - list plugins; - boost::thread_group group; - string revision, date; + list messages; + unsigned int lang; - wxProgressDialog *progDia = new wxProgressDialog(translate("LOOT: Working..."),translate("LOOT working..."), 1000, this, wxPD_APP_MODAL|wxPD_AUTO_HIDE|wxPD_ELAPSED_TIME); + wxProgressDialog *progDia = new wxProgressDialog(translate("LOOT: Working..."), translate("LOOT working..."), 1000, this, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); - /////////////////////////////////////////////////////// - // Load Plugins & Lists - /////////////////////////////////////////////////////// + function progressCallback([progDia](const std::string& message) { + progDia->Pulse(FromUTF8(message)); + }); //Set language. - unsigned int lang; if (_settings["Language"]) lang = Language(_settings["Language"].as()).Code(); else @@ -746,112 +595,11 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { BOOST_LOG_TRIVIAL(info) << "Using message language: " << Language(lang).Name(); - bool doUpdate = _settings["Update Masterlist"] && _settings["Update Masterlist"].as(); - masterlist_updater_parser mup(doUpdate, *_game, messages, mlist_plugins, mlist_messages, revision, date, lang); - group.create_thread(mup); - - //First calculate the mean plugin size. Store it temporarily in a map to reduce filesystem lookups and file size recalculation. - size_t meanFileSize = 0; - boost::unordered_map tempMap; - for (fs::directory_iterator it(_game->DataPath()); it != fs::directory_iterator(); ++it) { - if (fs::is_regular_file(it->status()) && IsPlugin(it->path().string())) { - - size_t fileSize = fs::file_size(it->path()); - meanFileSize += fileSize; - - tempMap.emplace(it->path().filename().string(), fileSize); - } - } - meanFileSize /= tempMap.size(); - - //Now load plugins. - plugin_list_loader pll(plugins, *_game); - for (const auto &pluginPair: tempMap) { - - BOOST_LOG_TRIVIAL(info) << "Found plugin: " << pluginPair.first; - - plugins.push_back(loot::Plugin(pluginPair.first)); - - if (pluginPair.second > meanFileSize) { - pll.skipPlugins.insert(pluginPair.first); - plugin_loader pl(plugins.back(), *_game); - group.create_thread(pl); - } - - progDia->Pulse(); - } - group.create_thread(pll); - group.join_all(); - - //Now load userlist. - if (fs::exists(_game->UserlistPath())) { - BOOST_LOG_TRIVIAL(debug) << "Parsing userlist at: " << _game->UserlistPath(); - - try { - loot::ifstream in(_game->UserlistPath()); - YAML::Node ulist = YAML::Load(in); - in.close(); - - if (ulist["plugins"]) - ulist_plugins = ulist["plugins"].as< list >(); - } catch (YAML::ParserException& e) { - BOOST_LOG_TRIVIAL(error) << "Userlist parsing failed. Details: " << e.what(); - messages.push_back(loot::Message(loot::Message::error, (format(loc::translate("Userlist parsing failed. Details: %1%")) % e.what()).str())); - } - if (ulist["plugins"]) - ulist_plugins = ulist["plugins"].as< list >(); - } - - progDia->Pulse(); - /////////////////////////////////////////////////////// - // Merge & Check Metadata + // Load Plugins & Lists /////////////////////////////////////////////////////// - if (fs::exists(_game->MasterlistPath()) || fs::exists(_game->UserlistPath())) { - - - //Merge all global message lists. - BOOST_LOG_TRIVIAL(debug) << "Merging all global message lists."; - if (!mlist_messages.empty()) - messages.insert(messages.end(), mlist_messages.begin(), mlist_messages.end()); - if (!ulist_messages.empty()) - messages.insert(messages.end(), ulist_messages.begin(), ulist_messages.end()); - - //Evaluate any conditions in the global messages. - BOOST_LOG_TRIVIAL(debug) << "Evaluating global message conditions."; - try { - list::iterator it=messages.begin(); - while (it != messages.end()) { - if (!it->EvalCondition(*_game, lang)) - it = messages.erase(it); - else - ++it; - } - } - catch (std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "A global message contains a condition that could not be evaluated. Details: " << e.what(); - messages.push_back(loot::Message(loot::Message::error, (format(loc::translate("A global message contains a condition that could not be evaluated. Details: %1%")) % e.what()).str())); - } - - //Merge plugin list and masterlist. - BOOST_LOG_TRIVIAL(debug) << "Merging plugin list and masterlist data."; - for (auto &plugin : plugins) { - BOOST_LOG_TRIVIAL(trace) << "Merging for plugin \"" << plugin.Name() << "\""; - - //Check if there is a plugin entry in the masterlist. This will also find matching regex entries. - list::iterator pos = std::find(mlist_plugins.begin(), mlist_plugins.end(), plugin); - - if (pos != mlist_plugins.end()) { - BOOST_LOG_TRIVIAL(trace) << "Merging masterlist data down to plugin list data."; - plugin.MergeMetadata(*pos); - } - - progDia->Pulse(); - } - } - - progDia->Update(800, translate("Building plugin graph...")); + _games[_currentGame].SortPrep(lang, messages, progressCallback); /////////////////////////////////////////////////////// // Build Graph Edges & Sort @@ -867,78 +615,14 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { */ - //Check for back-edges, then perform a topological sort. + list plugins; try { bool applyLoadOrder = false; do { - //Create a plugin graph containing the plugin and masterlist data. - loot::PluginGraph graph; - for (auto &plugin : plugins) { - vertex_t v = boost::add_vertex(plugin, graph); - } - - BOOST_LOG_TRIVIAL(info) << "Merging userlist into plugin list/masterlist, evaluating conditions and checking for install validity."; - loot::vertex_it vit, vitend; - for (boost::tie(vit, vitend) = boost::vertices(graph); vit != vitend; ++vit) { - BOOST_LOG_TRIVIAL(trace) << "Merging for plugin \"" << graph[*vit].Name() << "\""; - - //Check if there is a plugin entry in the userlist. This will also find matching regex entries. - list::iterator pos = std::find(ulist_plugins.begin(), ulist_plugins.end(), graph[*vit]); - - if (pos != ulist_plugins.end() && pos->Enabled()) { - BOOST_LOG_TRIVIAL(trace) << "Merging userlist data down to plugin list data."; - graph[*vit].MergeMetadata(*pos); - } - - progDia->Pulse(); - - //Now that items are merged, evaluate any conditions they have. - BOOST_LOG_TRIVIAL(trace) << "Evaluate conditions for merged plugin data."; - try { - graph[*vit].EvalAllConditions(*_game, lang); - } - catch (std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "\"" << graph[*vit].Name() << "\" contains a condition that could not be evaluated. Details: " << e.what(); - messages.push_back(loot::Message(loot::Message::error, (format(loc::translate("\"%1%\" contains a condition that could not be evaluated. Details: %2%")) % graph[*vit].Name() % e.what()).str())); - } - - progDia->Pulse(); - - //Also check install validity. - BOOST_LOG_TRIVIAL(trace) << "Checking that the current install is valid according to this plugin's data."; - graph[*vit].CheckInstallValidity(*_game); - - progDia->Pulse(); - } - - BOOST_LOG_TRIVIAL(info) << "Building the plugin dependency graph..."; - - //Now add the interactions between plugins to the graph as edges. - std::map overriddenPriorities; - BOOST_LOG_TRIVIAL(debug) << "Adding non-overlap edges."; - loot::AddSpecificEdges(graph, overriddenPriorities); - - BOOST_LOG_TRIVIAL(debug) << "Adding priority edges."; - loot::AddPriorityEdges(graph); - - BOOST_LOG_TRIVIAL(debug) << "Adding overlap edges."; - loot::AddOverlapEdges(graph); - - BOOST_LOG_TRIVIAL(info) << "Checking to see if the graph is cyclic."; - loot::CheckForCycles(graph); - - for (const auto &overriddenPriority: overriddenPriorities) { - vertex_t vertex; - if (loot::GetVertexByName(graph, overriddenPriority.first, vertex)) { - graph[vertex].Priority(overriddenPriority.second); - } - } - - progDia->Pulse(); - - BOOST_LOG_TRIVIAL(info) << "Performing a topological sort."; - loot::Sort(graph, plugins); + + // Perform sort. + plugins = _games[_currentGame].Sort(lang, messages, progressCallback); progDia->Destroy(); progDia = nullptr; @@ -952,10 +636,11 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { GetWindowSizePos(_settings["windows"]["editor"], pos, size); } - MiniEditor editor(this, translate("LOOT: Calculated Load Order"), pos, size, plugins, ulist_plugins, *_game); + // Display mini editor. + MiniEditor editor(this, translate("LOOT: Calculated Load Order"), pos, size, plugins, _games[_currentGame].userlist.plugins, _games[_currentGame]); long ret = editor.ShowModal(); - const std::list& newUserlist = editor.GetNewUserlist(); + MetadataList newUserlist = editor.GetNewUserlist(); //Record window settings. YAML::Node node; @@ -966,34 +651,11 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { _settings["windows"]["editor"] = node; - //Need to determine if any new edits have been made. - bool haveNewEdits = false; - if (newUserlist.size() != ulist_plugins.size()) { - BOOST_LOG_TRIVIAL(info) << "Metadata edited for some plugin, new and old userlists differ in size."; - haveNewEdits = true; - } - else { - for (const auto& newEdit : newUserlist) { - const auto it = std::find(ulist_plugins.begin(), ulist_plugins.end(), newEdit); - if (it == ulist_plugins.end()) { - BOOST_LOG_TRIVIAL(info) << "Metadata added for plugin: " << it->Name(); - haveNewEdits = true; - break; - } - - if (!it->DiffMetadata(newEdit).HasNameOnly()) { - BOOST_LOG_TRIVIAL(info) << "Metadata edited for plugin: " << it->Name(); - haveNewEdits = true; - break; - } - } - } - if (ret != wxID_APPLY) { applyLoadOrder = false; break; } - else if (!haveNewEdits) { + else if (_games[_currentGame].userlist == newUserlist) { applyLoadOrder = true; break; } @@ -1002,19 +664,10 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { progDia = new wxProgressDialog(translate("LOOT: Working..."), translate("Recalculating load order..."), 1000, this, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); //User accepted edits, now apply them, then loop. - ulist_plugins = newUserlist; + _games[_currentGame].userlist = newUserlist; //Save edits to userlist. - BOOST_LOG_TRIVIAL(info) << "Saving edited userlist."; - YAML::Emitter yout; - yout.SetIndent(2); - yout << YAML::BeginMap - << YAML::Key << "plugins" << YAML::Value << ulist_plugins - << YAML::EndMap; - - loot::ofstream uout(_game->UserlistPath()); - uout << yout.c_str(); - uout.close(); + _games[_currentGame].userlist.Save(_games[_currentGame].UserlistPath()); //Now loop. } @@ -1024,7 +677,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { //Applying the load order. BOOST_LOG_TRIVIAL(debug) << "Setting load order."; try { - _game->SetLoadOrder(plugins); + _games[_currentGame].SetLoadOrder(plugins); } catch (std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Failed to set the load order. Details: " << e.what(); @@ -1055,12 +708,12 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { BOOST_LOG_TRIVIAL(debug) << "Generating report..."; try { - GenerateReportData(*_game, + GenerateReportData(_games[_currentGame], messages, plugins, - revision, - date, - doUpdate); + _games[_currentGame].masterlist.GetRevision(_games[_currentGame].MasterlistPath()), + _games[_currentGame].masterlist.GetDate(_games[_currentGame].MasterlistPath()), + true); } catch (std::exception& e) { wxMessageBox( FromUTF8(format(loc::translate("Error: %1%")) % e.what()), @@ -1084,7 +737,7 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { } //Create viewer window. - Viewer *viewer = new Viewer(this, translate("LOOT: Report Viewer"), FromUTF8(ToFileURL(g_path_report.string() + "?data=" + _game->ReportDataPath().string())), pos, size, _settings); + Viewer *viewer = new Viewer(this, translate("LOOT: Report Viewer"), FromUTF8(ToFileURL(g_path_report.string() + "?data=" + _games[_currentGame].ReportDataPath().string())), pos, size, _settings); viewer->Show(); BOOST_LOG_TRIVIAL(debug) << "Report display successful. Sorting process complete."; @@ -1093,70 +746,49 @@ void Launcher::OnSortPlugins(wxCommandEvent& event) { void Launcher::OnEditMetadata(wxCommandEvent& event) { //Should probably check for masterlist updates before opening metadata editor. - list installed, mlist_plugins, ulist_plugins; + list installed; + unsigned int lang; - wxProgressDialog *progDia = new wxProgressDialog(translate("LOOT: Working..."),translate("LOOT working..."), 1000, this, wxPD_APP_MODAL|wxPD_AUTO_HIDE|wxPD_ELAPSED_TIME); + wxProgressDialog *progDia = new wxProgressDialog(translate("LOOT: Working..."), translate("LOOT working..."), 1000, this, wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME); + + //Set language. + if (_settings["Language"]) + lang = Language(_settings["Language"].as()).Code(); + else + lang = loot::Language::any; //Scan for installed plugins. BOOST_LOG_TRIVIAL(debug) << "Reading installed plugins' headers."; - _game->LoadPlugins(true); + _games[_currentGame].LoadPlugins(true); //Sort plugins into their load order. list loadOrder; - _game->GetLoadOrder(loadOrder); + _games[_currentGame].GetLoadOrder(loadOrder); for (const auto &pluginName: loadOrder) { - const auto pos = _game->plugins.find(pluginName); + const auto pos = _games[_currentGame].plugins.find(pluginName); - if (pos != _game->plugins.end()) + if (pos != _games[_currentGame].plugins.end()) installed.push_back(pos->second); } //Parse masterlist. - if (fs::exists(_game->MasterlistPath())) { + if (fs::exists(_games[_currentGame].MasterlistPath())) { BOOST_LOG_TRIVIAL(debug) << "Parsing masterlist."; - YAML::Node mlist; - try { - loot::ifstream in(_game->MasterlistPath()); - mlist = YAML::Load(in); - in.close(); - } catch (YAML::ParserException& e) { - BOOST_LOG_TRIVIAL(error) << "Masterlist parsing failed. " << e.what(); - wxMessageBox( - FromUTF8(format(loc::translate("Error: Masterlist parsing failed. %1%")) % e.what()), - translate("LOOT: Error"), - wxOK | wxICON_ERROR, - this); - } - if (mlist["plugins"]) - mlist_plugins = mlist["plugins"].as< list >(); + _games[_currentGame].masterlist.Load(_games[_currentGame], lang); } progDia->Pulse(); //Parse userlist. - if (fs::exists(_game->UserlistPath())) { + if (fs::exists(_games[_currentGame].UserlistPath())) { BOOST_LOG_TRIVIAL(debug) << "Parsing userlist."; - YAML::Node ulist; - try { - loot::ifstream in(_game->UserlistPath()); - ulist = YAML::Load(in); - in.close(); - } catch (YAML::ParserException& e) { - BOOST_LOG_TRIVIAL(error) << "Userlist parsing failed. " << e.what(); - wxMessageBox( - FromUTF8(format(loc::translate("Error: Userlist parsing failed. %1%")) % e.what()), - translate("LOOT: Error"), - wxOK | wxICON_ERROR, - this); - } - if (ulist["plugins"]) - ulist_plugins = ulist["plugins"].as< list >(); + _games[_currentGame].userlist.Load(_games[_currentGame].UserlistPath()); } progDia->Pulse(); //Merge the masterlist down into the installed mods list. BOOST_LOG_TRIVIAL(debug) << "Merging the masterlist down into the installed mods list."; - for (const auto &plugin: mlist_plugins) { + for (const auto &plugin: _games[_currentGame].masterlist.plugins) { auto pos = find(installed.begin(), installed.end(), plugin); if (pos != installed.end()) @@ -1167,20 +799,13 @@ void Launcher::OnEditMetadata(wxCommandEvent& event) { //Add empty entries for any userlist entries that aren't installed. BOOST_LOG_TRIVIAL(debug) << "Padding the installed mods list to match the plugins in the userlist."; - for (const auto &plugin : ulist_plugins) { + for (const auto &plugin : _games[_currentGame].userlist.plugins) { if (find(installed.begin(), installed.end(), plugin) == installed.end()) installed.push_back(loot::Plugin(plugin.Name())); } progDia->Pulse(); - //Set language. - unsigned int lang; - if (_settings["Language"]) - lang = Language(_settings["Language"].as()).Code(); - else - lang = loot::Language::any; - //Load window size/pos settings. wxSize size = wxDefaultSize; wxPoint pos = wxDefaultPosition; @@ -1190,7 +815,7 @@ void Launcher::OnEditMetadata(wxCommandEvent& event) { //Create editor window. BOOST_LOG_TRIVIAL(debug) << "Opening editor window."; - FullEditor *editor = new FullEditor(this, translate("LOOT: Metadata Editor"), pos, size, _game->UserlistPath().string(), installed, ulist_plugins, lang, *_game, _settings); + FullEditor *editor = new FullEditor(this, translate("LOOT: Metadata Editor"), pos, size, _games[_currentGame].UserlistPath().string(), installed, _games[_currentGame].userlist.plugins, lang, _games[_currentGame], _settings); progDia->Destroy(); @@ -1204,7 +829,7 @@ void Launcher::OnRedatePlugins(wxCommandEvent& event) { if (dia->ShowModal() == wxID_YES) { BOOST_LOG_TRIVIAL(debug) << "Redating plugins."; try { - _game->RedatePlugins(); + _games[_currentGame].RedatePlugins(); } catch (std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Failed to redate plugins. " << e.what(); wxMessageBox( diff --git a/src/gui/main.h b/src/gui/main.h index a854de19..ea646ca3 100644 --- a/src/gui/main.h +++ b/src/gui/main.h @@ -45,7 +45,7 @@ private: class Launcher : public wxFrame { public: - Launcher(const wxChar *title, YAML::Node& settings, loot::Game * inGame, std::vector& games, wxPoint pos, wxSize size); + Launcher(const wxChar *title, YAML::Node& settings, std::vector& games, size_t currentGame, wxPoint pos, wxSize size); void OnSortPlugins(wxCommandEvent& event); void OnEditMetadata(wxCommandEvent& event); @@ -65,9 +65,9 @@ private: wxMenuItem * RedatePluginsItem; wxButton * ViewButton; - loot::Game * _game; YAML::Node& _settings; //LOOT Settings. std::vector& _games; + size_t _currentGame; void GetWindowSizePos(const YAML::Node& node, wxPoint& pos, wxSize& size); };