From 3ede23e5b71ce658a37cb81187e289c5f6fb0b3c Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Wed, 30 Apr 2025 08:43:37 +0100 Subject: [PATCH] Add support for Oblivion Remastered --- README.md | 2 +- include/loot/enum/game_type.h | 4 +- src/api/api.cpp | 2 + src/api/game/game.cpp | 3 ++ src/api/game/load_order_handler.cpp | 2 + src/api/metadata/condition_evaluator.cpp | 1 + src/api/plugin.cpp | 39 +++++++++++++------ src/tests/api/internals/game/game_test.h | 8 ++-- .../internals/game/load_order_handler_test.h | 3 +- src/tests/api/internals/plugin_test.h | 21 ++++++---- src/tests/common_game_test_fixture.h | 27 +++++++++---- src/tests/test_helpers.h | 3 +- 12 files changed, 82 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 5dca21b8..15b3992d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## Introduction -LOOT is a plugin load order optimisation tool for Starfield, TES III: Morrowind, TES IV: Oblivion, TES V: Skyrim, TES V: Skyrim Special Edition, TES V: Skyrim VR, Fallout 3, Fallout: New Vegas, Fallout 4, Fallout 4 VR and OpenMW. It is designed to assist mod users in avoiding detrimental conflicts, by automatically calculating a load order that satisfies all plugin dependencies and maximises each plugin's impact on the user's game. +LOOT is a plugin load order optimisation tool for Starfield, TES III: Morrowind, TES IV: Oblivion, TES IV: Oblivion Remastered, TES V: Skyrim, TES V: Skyrim Special Edition, TES V: Skyrim VR, Fallout 3, Fallout: New Vegas, Fallout 4, Fallout 4 VR and OpenMW. It is designed to assist mod users in avoiding detrimental conflicts, by automatically calculating a load order that satisfies all plugin dependencies and maximises each plugin's impact on the user's game. LOOT also provides some load order error checking, including checks for requirements, incompatibilities and cyclic dependencies. In addition, it provides a large number of plugin-specific usage notes, bug warnings and Bash Tag suggestions. diff --git a/include/loot/enum/game_type.h b/include/loot/enum/game_type.h index ccec865c..8a1edc01 100644 --- a/include/loot/enum/game_type.h +++ b/include/loot/enum/game_type.h @@ -52,7 +52,9 @@ enum struct GameType : unsigned int { /** Starfield */ starfield, /** OpenMW */ - openmw + openmw, + /** The Elder Scrolls IV: Oblivion Remastered */ + oblivionRemastered }; } diff --git a/src/api/api.cpp b/src/api/api.cpp index 4a99a3f6..d9b67f09 100644 --- a/src/api/api.cpp +++ b/src/api/api.cpp @@ -56,6 +56,8 @@ const char* DescribeGameType(GameType gameType) { return "Starfield"; case GameType::openmw: return "OpenMW"; + case GameType::oblivionRemastered: + return "The Elder Scrolls IV: Oblivion Remastered"; default: return "Unknown"; } diff --git a/src/api/game/game.cpp b/src/api/game/game.cpp index 1e8e9550..b36337c2 100644 --- a/src/api/game/game.cpp +++ b/src/api/game/game.cpp @@ -120,6 +120,9 @@ std::filesystem::path Game::DataPath() const { return gamePath_ / "Data Files"; } else if (type_ == GameType::openmw) { return gamePath_ / "resources" / "vfs"; + } else if (type_ == GameType::oblivionRemastered) { + return gamePath_ / "OblivionRemastered" / "Content" / "Dev" / "ObvData" / + "Data"; } else { return gamePath_ / "Data"; } diff --git a/src/api/game/load_order_handler.cpp b/src/api/game/load_order_handler.cpp index f071dc65..36873dde 100644 --- a/src/api/game/load_order_handler.cpp +++ b/src/api/game/load_order_handler.cpp @@ -52,6 +52,8 @@ unsigned int mapGameId(GameType gameType) { return LIBLO_GAME_STARFIELD; case GameType::openmw: return LIBLO_GAME_OPENMW; + case GameType::oblivionRemastered: + return LIBLO_GAME_OBLIVION_REMASTERED; default: throw std::logic_error("Unexpected game type"); } diff --git a/src/api/metadata/condition_evaluator.cpp b/src/api/metadata/condition_evaluator.cpp index 89cc8989..bff1c6e3 100644 --- a/src/api/metadata/condition_evaluator.cpp +++ b/src/api/metadata/condition_evaluator.cpp @@ -60,6 +60,7 @@ int mapGameType(GameType gameType) { case GameType::tes3: return LCI_GAME_MORROWIND; case GameType::tes4: + case GameType::oblivionRemastered: return LCI_GAME_OBLIVION; case GameType::tes5: return LCI_GAME_SKYRIM; diff --git a/src/api/plugin.cpp b/src/api/plugin.cpp index 51b7fe29..1e666ffd 100644 --- a/src/api/plugin.cpp +++ b/src/api/plugin.cpp @@ -152,7 +152,8 @@ std::vector FindAssociatedArchives( // This assumes that Skyrim VR works the same way as Skyrim SE. return FindAssociatedArchivesWithSuffixes( pluginPath, BSA_FILE_EXTENSION, {"", " - Textures"}); - case GameType::tes4: { + case GameType::tes4: + case GameType::oblivionRemastered: { // Oblivion .esp files can load archives which begin with the plugin // basename. if (!boost::iends_with(pluginPath.filename().u8string(), ".esp")) { @@ -189,7 +190,8 @@ void HandleEspluginError(unsigned int returnCode, std::string_view operation) { return; } - auto err = fmt::format("esplugin failed to {}. Error code: {}", operation, returnCode); + auto err = fmt::format( + "esplugin failed to {}. Error code: {}", operation, returnCode); const char* e = nullptr; esp_get_error_message(&e); @@ -207,7 +209,7 @@ void HandleEspluginError(unsigned int returnCode, std::string_view operation) { throw std::system_error(returnCode, esplugin_category(), err); } -template +template void HandleEspluginError(unsigned int returnCode, std::string_view message, Args... args) { @@ -228,17 +230,30 @@ void HandleEspluginError(unsigned int returnCode, return HandleEspluginError(returnCode, getMessage()); } +std::string GetPluginName(GameType gameType, + const std::filesystem::path& pluginPath) { + return gameType == GameType::openmw + ? pluginPath.filename().u8string() + : loot::TrimDotGhostExtension(pluginPath.filename().u8string()); +} + +std::unique_ptr<::Plugin, decltype(&esp_plugin_free)> MakeEspluginPtr() { + return std::unique_ptr<::Plugin, decltype(&esp_plugin_free)>(nullptr, + esp_plugin_free); +} + +bool ShouldIgnoreMasterFlag(GameType gameType) { + return gameType == GameType::openmw || + gameType == GameType::oblivionRemastered; +} + Plugin::Plugin(const GameType gameType, const GameCache& gameCache, const std::filesystem::path& pluginPath, const bool headerOnly) : - name_(gameType == GameType::openmw - ? pluginPath.filename().u8string() - : TrimDotGhostExtension(pluginPath.filename().u8string())), - esPlugin( - std::unique_ptr<::Plugin, decltype(&esp_plugin_free)>(nullptr, - esp_plugin_free)), - ignoreMasterFlag_(gameType == GameType::openmw), + name_(GetPluginName(gameType, pluginPath)), + esPlugin(MakeEspluginPtr()), + ignoreMasterFlag_(ShouldIgnoreMasterFlag(gameType)), isEmpty_(true) { auto logger = getLogger(); @@ -595,8 +610,7 @@ std::string Plugin::GetDescription() const { } std::unique_ptr -Plugin::GetPluginsMetadata( - const std::vector& plugins) { +Plugin::GetPluginsMetadata(const std::vector& plugins) { if (plugins.empty()) { return std::unique_ptr( @@ -638,6 +652,7 @@ unsigned int Plugin::GetEspluginGameId(GameType gameType) { case GameType::openmw: return ESP_GAME_MORROWIND; case GameType::tes4: + case GameType::oblivionRemastered: return ESP_GAME_OBLIVION; case GameType::tes5: return ESP_GAME_SKYRIM; diff --git a/src/tests/api/internals/game/game_test.h b/src/tests/api/internals/game/game_test.h index 307da352..2f1946df 100644 --- a/src/tests/api/internals/game/game_test.h +++ b/src/tests/api/internals/game/game_test.h @@ -60,9 +60,11 @@ TEST_P(GameTest, constructingShouldStoreTheGivenValues) { } #ifndef _WIN32 -TEST_P(GameTest, - constructingShouldThrowOnLinuxIfLocalPathIsNotGivenExceptForMorrowind) { - if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) { +TEST_P( + GameTest, + constructingShouldThrowOnLinuxIfLocalPathIsNotGivenExceptForMorrowindOpenMWAndOblivionRemastered) { + if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw || + GetParam() == GameType::oblivionRemastered) { EXPECT_NO_THROW(Game(GetParam(), gamePath)); } else { EXPECT_THROW(Game(GetParam(), gamePath), std::system_error); diff --git a/src/tests/api/internals/game/load_order_handler_test.h b/src/tests/api/internals/game/load_order_handler_test.h index abb6bee3..a09e38e7 100644 --- a/src/tests/api/internals/game/load_order_handler_test.h +++ b/src/tests/api/internals/game/load_order_handler_test.h @@ -147,7 +147,8 @@ TEST_P(LoadOrderHandlerTest, constructorShouldNotThrowIfNoLocalPathIsSet) { #else TEST_P(LoadOrderHandlerTest, constructorShouldNotThrowIfNoLocalPathIsSetAndGameTypeIsMorrowind) { - if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) { + if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw || + GetParam() == GameType::oblivionRemastered) { EXPECT_NO_THROW(LoadOrderHandler(GetParam(), gamePath)); } else { EXPECT_THROW(LoadOrderHandler(GetParam(), gamePath), std::system_error); diff --git a/src/tests/api/internals/plugin_test.h b/src/tests/api/internals/plugin_test.h index 1f5a6814..4c7e198a 100644 --- a/src/tests/api/internals/plugin_test.h +++ b/src/tests/api/internals/plugin_test.h @@ -284,7 +284,8 @@ TEST_P(PluginTest, loadingHeaderOnlyShouldReadHeaderData) { EXPECT_EQ(blankEsm, plugin.GetName()); EXPECT_TRUE(plugin.GetMasters().empty()); - if (GetParam() == GameType::openmw) { + if (GetParam() == GameType::openmw || + GetParam() == GameType::oblivionRemastered) { EXPECT_FALSE(plugin.IsMaster()); } else { EXPECT_TRUE(plugin.IsMaster()); @@ -294,7 +295,8 @@ TEST_P(PluginTest, loadingHeaderOnlyShouldReadHeaderData) { if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) { EXPECT_FLOAT_EQ(1.2f, plugin.GetHeaderVersion().value()); - } else if (GetParam() == GameType::tes4) { + } else if (GetParam() == GameType::tes4 || + GetParam() == GameType::oblivionRemastered) { EXPECT_FLOAT_EQ(0.8f, plugin.GetHeaderVersion().value()); } else if (GetParam() == GameType::starfield) { EXPECT_FLOAT_EQ(0.96f, plugin.GetHeaderVersion().value()); @@ -316,7 +318,8 @@ TEST_P(PluginTest, loadingWholePluginShouldReadHeaderData) { EXPECT_EQ(blankEsm, plugin.GetName()); EXPECT_TRUE(plugin.GetMasters().empty()); - if (GetParam() == GameType::openmw) { + if (GetParam() == GameType::openmw || + GetParam() == GameType::oblivionRemastered) { EXPECT_FALSE(plugin.IsMaster()); } else { EXPECT_TRUE(plugin.IsMaster()); @@ -326,7 +329,8 @@ TEST_P(PluginTest, loadingWholePluginShouldReadHeaderData) { if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) { EXPECT_FLOAT_EQ(1.2f, plugin.GetHeaderVersion().value()); - } else if (GetParam() == GameType::tes4) { + } else if (GetParam() == GameType::tes4 || + GetParam() == GameType::oblivionRemastered) { EXPECT_FLOAT_EQ(0.8f, plugin.GetHeaderVersion().value()); } else if (GetParam() == GameType::starfield) { EXPECT_FLOAT_EQ(0.96f, plugin.GetHeaderVersion().value()); @@ -508,7 +512,8 @@ TEST_P( .LoadsArchive(); if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw || - GetParam() == GameType::tes4) + GetParam() == GameType::tes4 || + GetParam() == GameType::oblivionRemastered) EXPECT_FALSE(loadsArchive); else EXPECT_TRUE(loadsArchive); @@ -575,7 +580,8 @@ TEST_P( if (GetParam() == GameType::tes4 || GetParam() == GameType::fo3 || GetParam() == GameType::fonv || GetParam() == GameType::fo4 || - GetParam() == GameType::fo4vr) { + GetParam() == GameType::fo4vr || + GetParam() == GameType::oblivionRemastered) { EXPECT_TRUE(loadsArchive); } else { EXPECT_FALSE(loadsArchive); @@ -595,7 +601,8 @@ TEST_P( if (GetParam() == GameType::tes4 || GetParam() == GameType::fo3 || GetParam() == GameType::fonv || GetParam() == GameType::fo4 || - GetParam() == GameType::fo4vr) { + GetParam() == GameType::fo4vr || + GetParam() == GameType::oblivionRemastered) { EXPECT_TRUE(loadsArchive); } else { EXPECT_FALSE(loadsArchive); diff --git a/src/tests/common_game_test_fixture.h b/src/tests/common_game_test_fixture.h index f5ffd0f6..5dbdc35e 100644 --- a/src/tests/common_game_test_fixture.h +++ b/src/tests/common_game_test_fixture.h @@ -40,7 +40,7 @@ along with LOOT. If not, see namespace loot { namespace test { -static const std::array ALL_GAME_TYPES = { +static const std::array ALL_GAME_TYPES = { GameType::tes3, GameType::tes4, GameType::tes5, @@ -52,6 +52,7 @@ static const std::array ALL_GAME_TYPES = { GameType::fo4vr, GameType::starfield, GameType::openmw, + GameType::oblivionRemastered, }; class CommonGameTestFixture : public ::testing::Test { @@ -238,8 +239,11 @@ protected: } } for (const auto& plugin : loadOrder) actual.push_back(plugin.second); - } else if (gameType_ == GameType::tes5) { - std::ifstream in(localPath / "loadorder.txt"); + } else if (gameType_ == GameType::tes5 || + gameType_ == GameType::oblivionRemastered) { + const auto& parentPath = + gameType_ == GameType::oblivionRemastered ? dataPath : localPath; + std::ifstream in(parentPath / "loadorder.txt"); while (in) { std::string line; std::getline(in, line); @@ -420,7 +424,8 @@ private: std::string getMasterFile() const { if (gameType_ == GameType::tes3 || gameType_ == GameType::openmw) return "Morrowind.esm"; - else if (gameType_ == GameType::tes4) + else if (gameType_ == GameType::tes4 || + gameType_ == GameType::oblivionRemastered) return "Oblivion.esm"; else if (gameType_ == GameType::tes5 || gameType_ == GameType::tes5se || gameType_ == GameType::tes5vr) @@ -442,6 +447,8 @@ private: return "resources/vfs"; } else if (gameType_ == GameType::tes3) { return "Data Files"; + } else if (gameType_ == GameType::oblivionRemastered) { + return "OblivionRemastered/Content/Dev/ObvData/Data"; } else { return "Data"; } @@ -453,6 +460,7 @@ private: case GameType::openmw: return 0x790DC6FB; case GameType::tes4: + case GameType::oblivionRemastered: return 0x374E2A6F; case GameType::starfield: return 0xDE586309; @@ -479,7 +487,9 @@ private: } } } else { - std::ofstream out(localPath / "Plugins.txt"); + const auto& parentPath = + gameType_ == GameType::oblivionRemastered ? dataPath : localPath; + std::ofstream out(parentPath / "Plugins.txt"); for (const auto& plugin : loadOrder) { if (supportsLightPlugins(gameType_)) { if (plugin.second) @@ -506,8 +516,11 @@ private: } modificationTime += std::chrono::seconds(60); } - } else if (gameType_ == GameType::tes5) { - std::ofstream out(localPath / "loadorder.txt"); + } else if (gameType_ == GameType::tes5 || + gameType_ == GameType::oblivionRemastered) { + const auto& parentPath = + gameType_ == GameType::oblivionRemastered ? dataPath : localPath; + std::ofstream out(parentPath / "loadorder.txt"); for (const auto& plugin : loadOrder) out << plugin.first << std::endl; } } diff --git a/src/tests/test_helpers.h b/src/tests/test_helpers.h index a40d23c0..9b8d5a68 100644 --- a/src/tests/test_helpers.h +++ b/src/tests/test_helpers.h @@ -42,7 +42,8 @@ std::filesystem::path getSourcePluginsPath(GameType gameType) { using std::filesystem::absolute; if (gameType == GameType::tes3 || gameType == GameType::openmw) { return absolute("./testing-plugins/Morrowind/Data Files"); - } else if (gameType == GameType::tes4) { + } else if (gameType == GameType::tes4 || + gameType == GameType::oblivionRemastered) { return absolute("./testing-plugins/Oblivion/Data"); } else if (gameType == GameType::starfield) { return absolute("./testing-plugins/Starfield/Data");