Add support for Oblivion Remastered

This commit is contained in:
Oliver Hamlet
2025-05-01 18:43:22 +01:00
parent a6f8a358d8
commit 3ede23e5b7
12 changed files with 82 additions and 33 deletions
+1 -1
View File
@@ -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.
+3 -1
View File
@@ -52,7 +52,9 @@ enum struct GameType : unsigned int {
/** Starfield */
starfield,
/** OpenMW */
openmw
openmw,
/** The Elder Scrolls IV: Oblivion Remastered */
oblivionRemastered
};
}
+2
View File
@@ -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";
}
+3
View File
@@ -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";
}
+2
View File
@@ -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");
}
+1
View File
@@ -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;
+27 -12
View File
@@ -152,7 +152,8 @@ std::vector<std::filesystem::path> 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<typename ...Args>
template<typename... Args>
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<Vec_PluginMetadata, decltype(&esp_plugins_metadata_free)>
Plugin::GetPluginsMetadata(
const std::vector<const Plugin*>& plugins) {
Plugin::GetPluginsMetadata(const std::vector<const Plugin*>& plugins) {
if (plugins.empty()) {
return std::unique_ptr<Vec_PluginMetadata,
decltype(&esp_plugins_metadata_free)>(
@@ -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;
+5 -3
View File
@@ -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);
@@ -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);
+14 -7
View File
@@ -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);
+20 -7
View File
@@ -40,7 +40,7 @@ along with LOOT. If not, see
namespace loot {
namespace test {
static const std::array<GameType, 11> ALL_GAME_TYPES = {
static const std::array<GameType, 12> ALL_GAME_TYPES = {
GameType::tes3,
GameType::tes4,
GameType::tes5,
@@ -52,6 +52,7 @@ static const std::array<GameType, 11> 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;
}
}
+2 -1
View File
@@ -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");