diff --git a/docs/app/usage/main.rst b/docs/app/usage/main.rst index 8cbe1321..bf7a80d0 100644 --- a/docs/app/usage/main.rst +++ b/docs/app/usage/main.rst @@ -27,6 +27,11 @@ LOOT is able to sort plugins ghosted by Wrye Bash, and can extract Bash Tags and Any errors encountered during sorting or masterlist update will be displayed on the "General Information" card. +Load Order Backups +^^^^^^^^^^^^^^^^^^ + +Before a sorted load order is applied, LOOT saves a backup of the current load order as a ``loadorder.bak.0`` text file in LOOT's data folder for the current game. Up to three load order backups are retained: ``loadorder.bak.0`` is the backup from the most recent load order change, ``loadorder.bak.1`` is the second-most recent backup, and ``loadorder.bak.2`` is the third-most recent backup. + Search ------ diff --git a/src/backend/game/game.cpp b/src/backend/game/game.cpp index 349a3d54..c3c2a0f3 100644 --- a/src/backend/game/game.cpp +++ b/src/backend/game/game.cpp @@ -236,6 +236,7 @@ std::vector Game::GetLoadOrder() const { } void Game::SetLoadOrder(const std::vector& loadOrder) const { + BackupLoadOrder(loadOrder_, LootPaths::getLootDataPath() / FolderName()); LoadOrderHandler::SetLoadOrder(loadOrder); loadOrder_ = loadOrder; } diff --git a/src/backend/game/load_order_handler.cpp b/src/backend/game/load_order_handler.cpp index e1a6a451..67381e5e 100644 --- a/src/backend/game/load_order_handler.cpp +++ b/src/backend/game/load_order_handler.cpp @@ -166,4 +166,23 @@ void LoadOrderHandler::SetLoadOrder(const std::vector& loadOrder) c throw std::system_error(ret, libloadorder_category(), err); } } +void LoadOrderHandler::BackupLoadOrder(const std::vector& loadOrder, + const boost::filesystem::path & backupDirectory) { + const int maxBackupIndex = 2; + boost::format filenameFormat = boost::format("loadorder.bak.%1%"); + + boost::filesystem::path backupFilePath = backupDirectory / (filenameFormat % 2).str(); + if (boost::filesystem::exists(backupFilePath)) + boost::filesystem::remove(backupFilePath); + + for (int i = maxBackupIndex - 1; i > -1; --i) { + const boost::filesystem::path backupFilePath = backupDirectory / (filenameFormat % i).str(); + if (boost::filesystem::exists(backupFilePath)) + boost::filesystem::rename(backupFilePath, backupDirectory / (filenameFormat % (i + 1)).str()); + } + + boost::filesystem::ofstream out(backupDirectory / (filenameFormat % 0).str()); + for (const auto &plugin : loadOrder) + out << plugin << std::endl; +} } diff --git a/src/backend/game/load_order_handler.h b/src/backend/game/load_order_handler.h index e2640d4b..cdb9680e 100644 --- a/src/backend/game/load_order_handler.h +++ b/src/backend/game/load_order_handler.h @@ -47,6 +47,9 @@ public: bool IsPluginActive(const std::string& pluginName) const; void SetLoadOrder(const std::vector& loadOrder) const; + + static void BackupLoadOrder(const std::vector& loadOrder, + const boost::filesystem::path& backupDirectory); private: lo_game_handle gh_; }; diff --git a/src/tests/backend/game/load_order_handler_test.h b/src/tests/backend/game/load_order_handler_test.h index bac9e0ed..41d6a9b5 100644 --- a/src/tests/backend/game/load_order_handler_test.h +++ b/src/tests/backend/game/load_order_handler_test.h @@ -33,7 +33,44 @@ namespace loot { namespace test { class LoadOrderHandlerTest : public CommonGameTestFixture { protected: + LoadOrderHandlerTest() : loadOrderToSet_({ + masterFile, + blankEsm, + blankMasterDependentEsm, + blankDifferentEsm, + blankDifferentMasterDependentEsm, + blankDifferentEsp, + blankDifferentPluginDependentEsp, + blankEsp, + blankMasterDependentEsp, + blankDifferentMasterDependentEsp, + blankPluginDependentEsp, + }), + loadOrderBackupFile0(localPath / "loadorder.bak.0"), + loadOrderBackupFile1(localPath / "loadorder.bak.1"), + loadOrderBackupFile2(localPath / "loadorder.bak.2"), + loadOrderBackupFile3(localPath / "loadorder.bak.3") {} + + void TearDown() { + CommonGameTestFixture::TearDown(); + + boost::filesystem::remove(loadOrderBackupFile0); + boost::filesystem::remove(loadOrderBackupFile1); + boost::filesystem::remove(loadOrderBackupFile2); + } + + void initialiseHandler() { + GameSettings game(GetParam()); + game.SetGamePath(dataPath.parent_path()); + ASSERT_NO_THROW(loadOrderHandler_.Init(game, localPath)); + } + LoadOrderHandler loadOrderHandler_; + std::vector loadOrderToSet_; + const boost::filesystem::path loadOrderBackupFile0; + const boost::filesystem::path loadOrderBackupFile1; + const boost::filesystem::path loadOrderBackupFile2; + const boost::filesystem::path loadOrderBackupFile3; }; // Pass an empty first argument, as it's a prefix for the test instantation, @@ -78,9 +115,7 @@ TEST_P(LoadOrderHandlerTest, isPluginActiveShouldThrowIfTheHandlerHasNotBeenInit } TEST_P(LoadOrderHandlerTest, isPluginActiveShouldReturnCorrectPluginStatesAfterInitialisation) { - GameSettings game(GetParam()); - game.SetGamePath(dataPath.parent_path()); - ASSERT_NO_THROW(loadOrderHandler_.Init(game, localPath)); + initialiseHandler(); EXPECT_TRUE(loadOrderHandler_.IsPluginActive(masterFile)); EXPECT_TRUE(loadOrderHandler_.IsPluginActive(blankEsm)); @@ -92,55 +127,121 @@ TEST_P(LoadOrderHandlerTest, getLoadOrderShouldThrowIfTheHandlerHasNotBeenInitia } TEST_P(LoadOrderHandlerTest, getLoadOrderShouldReturnTheCurrentLoadOrder) { - GameSettings game(GetParam()); - game.SetGamePath(dataPath.parent_path()); - ASSERT_NO_THROW(loadOrderHandler_.Init(game, localPath)); + initialiseHandler(); ASSERT_EQ(getLoadOrder(), loadOrderHandler_.GetLoadOrder()); } TEST_P(LoadOrderHandlerTest, setLoadOrderShouldThrowIfTheHandlerHasNotBeenInitialised) { - std::vector loadOrder({ - masterFile, - blankEsm, - blankMasterDependentEsm, - blankDifferentEsm, - blankDifferentMasterDependentEsm, - blankDifferentEsp, - blankDifferentPluginDependentEsp, - blankEsp, - blankMasterDependentEsp, - blankDifferentMasterDependentEsp, - blankPluginDependentEsp, - }); - - EXPECT_THROW(loadOrderHandler_.SetLoadOrder(loadOrder), std::system_error); + EXPECT_THROW(loadOrderHandler_.SetLoadOrder(loadOrderToSet_), std::system_error); } TEST_P(LoadOrderHandlerTest, setLoadOrderShouldSetTheLoadOrder) { - GameSettings game(GetParam()); - game.SetGamePath(dataPath.parent_path()); - ASSERT_NO_THROW(loadOrderHandler_.Init(game, localPath)); + initialiseHandler(); - std::vector loadOrder({ - masterFile, - blankEsm, - blankMasterDependentEsm, - blankDifferentEsm, - blankDifferentMasterDependentEsm, - blankDifferentEsp, - blankDifferentPluginDependentEsp, - blankEsp, - blankMasterDependentEsp, - blankDifferentMasterDependentEsp, - blankPluginDependentEsp, - }); - EXPECT_NO_THROW(loadOrderHandler_.SetLoadOrder(loadOrder)); + EXPECT_NO_THROW(loadOrderHandler_.SetLoadOrder(loadOrderToSet_)); if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) - loadOrder.erase(begin(loadOrder)); + loadOrderToSet_.erase(begin(loadOrderToSet_)); - EXPECT_EQ(loadOrder, getLoadOrder()); + EXPECT_EQ(loadOrderToSet_, getLoadOrder()); +} + +TEST_P(LoadOrderHandlerTest, backupLoadOrderShouldCreateABackupOfTheCurrentLoadOrder) { + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile0)); + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile1)); + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile2)); + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile3)); + + ASSERT_NO_THROW(LoadOrderHandler::BackupLoadOrder(loadOrderToSet_, localPath)); + + EXPECT_TRUE(boost::filesystem::exists(loadOrderBackupFile0)); + EXPECT_FALSE(boost::filesystem::exists(loadOrderBackupFile1)); + EXPECT_FALSE(boost::filesystem::exists(loadOrderBackupFile2)); + EXPECT_FALSE(boost::filesystem::exists(loadOrderBackupFile3)); + + auto loadOrder = readFileLines(loadOrderBackupFile0); + + EXPECT_EQ(loadOrderToSet_, loadOrder); +} + +TEST_P(LoadOrderHandlerTest, backupLoadOrderShouldRollOverExistingBackups) { + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile0)); + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile1)); + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile2)); + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile3)); + + ASSERT_NO_THROW(LoadOrderHandler::BackupLoadOrder(loadOrderToSet_, localPath)); + + auto firstSetLoadOrder = loadOrderToSet_; + + ASSERT_NE(blankPluginDependentEsp, loadOrderToSet_[9]); + ASSERT_NE(blankDifferentMasterDependentEsp, loadOrderToSet_[10]); + loadOrderToSet_[9] = blankPluginDependentEsp; + loadOrderToSet_[10] = blankDifferentMasterDependentEsp; + + ASSERT_NO_THROW(LoadOrderHandler::BackupLoadOrder(loadOrderToSet_, localPath)); + + EXPECT_TRUE(boost::filesystem::exists(loadOrderBackupFile0)); + EXPECT_TRUE(boost::filesystem::exists(loadOrderBackupFile1)); + EXPECT_FALSE(boost::filesystem::exists(loadOrderBackupFile2)); + EXPECT_FALSE(boost::filesystem::exists(loadOrderBackupFile3)); + + auto loadOrder = readFileLines(loadOrderBackupFile0); + EXPECT_EQ(loadOrderToSet_, loadOrder); + + loadOrder = readFileLines(loadOrderBackupFile1); + EXPECT_EQ(firstSetLoadOrder, loadOrder); +} + +TEST_P(LoadOrderHandlerTest, backupLoadOrderShouldKeepUpToThreeBackups) { + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile0)); + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile1)); + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile2)); + ASSERT_FALSE(boost::filesystem::exists(loadOrderBackupFile3)); + + ASSERT_NO_THROW(LoadOrderHandler::BackupLoadOrder(loadOrderToSet_, localPath)); + + auto firstSetLoadOrder = loadOrderToSet_; + + ASSERT_NE(blankPluginDependentEsp, loadOrderToSet_[9]); + ASSERT_NE(blankDifferentMasterDependentEsp, loadOrderToSet_[10]); + loadOrderToSet_[9] = blankPluginDependentEsp; + loadOrderToSet_[10] = blankDifferentMasterDependentEsp; + + ASSERT_NO_THROW(LoadOrderHandler::BackupLoadOrder(loadOrderToSet_, localPath)); + + auto secondSetLoadOrder = loadOrderToSet_; + + ASSERT_NE(blankMasterDependentEsp, loadOrderToSet_[7]); + ASSERT_NE(blankEsp, loadOrderToSet_[8]); + loadOrderToSet_[7] = blankMasterDependentEsp; + loadOrderToSet_[8] = blankEsp; + + ASSERT_NO_THROW(LoadOrderHandler::BackupLoadOrder(loadOrderToSet_, localPath)); + + auto thirdSetLoadOrder = loadOrderToSet_; + + ASSERT_NE(blankMasterDependentEsm, loadOrderToSet_[7]); + ASSERT_NE(blankDifferentEsm, loadOrderToSet_[8]); + loadOrderToSet_[7] = blankMasterDependentEsm; + loadOrderToSet_[8] = blankDifferentEsm; + + ASSERT_NO_THROW(LoadOrderHandler::BackupLoadOrder(loadOrderToSet_, localPath)); + + EXPECT_TRUE(boost::filesystem::exists(loadOrderBackupFile0)); + EXPECT_TRUE(boost::filesystem::exists(loadOrderBackupFile1)); + EXPECT_TRUE(boost::filesystem::exists(loadOrderBackupFile2)); + EXPECT_FALSE(boost::filesystem::exists(loadOrderBackupFile3)); + + auto loadOrder = readFileLines(loadOrderBackupFile0); + EXPECT_EQ(loadOrderToSet_, loadOrder); + + loadOrder = readFileLines(loadOrderBackupFile1); + EXPECT_EQ(thirdSetLoadOrder, loadOrder); + + loadOrder = readFileLines(loadOrderBackupFile2); + EXPECT_EQ(secondSetLoadOrder, loadOrder); } } } diff --git a/src/tests/common_game_test_fixture.h b/src/tests/common_game_test_fixture.h index d309afa6..ded54708 100644 --- a/src/tests/common_game_test_fixture.h +++ b/src/tests/common_game_test_fixture.h @@ -100,6 +100,22 @@ protected: ASSERT_FALSE(boost::filesystem::exists(dataPath / (blankMasterDependentEsm + ".ghost"))); } + std::vector readFileLines(const boost::filesystem::path& file) { + boost::filesystem::ifstream in(file); + + std::vector lines; + while (in) { + std::string line; + std::getline(in, line); + + if (!line.empty()) { + lines.push_back(line); + } + } + + return lines; + } + std::vector getLoadOrder() { std::vector actual; if (isLoadOrderTimestampBased(GetParam())) { @@ -125,17 +141,10 @@ protected: actual.push_back(line); } } else { - boost::filesystem::ifstream in(localPath / "plugins.txt"); - while (in) { - std::string line; - std::getline(in, line); - - if (!line.empty()) { - if (line[0] == '*') - line = line.substr(1); - - actual.push_back(line); - } + actual = readFileLines(localPath / "plugins.txt"); + for (auto& line : actual) { + if (line[0] == '*') + line = line.substr(1); } }