Implement support for OpenMW

Most of the complexity is handled by libloadorder, but it's worth noting that:

- The game path is OpenMW's install path, not Morrowind's
- OpenMW doesn't force master-flagged plugins to load before others
- OpenMW doesn't provide a way to record the load order of inactive plugins
- .omwgame and .omwaddon plugins are equivalent to .esm and .esp respectively, while .omwscripts plugins have a completely different format with none of the metadata that libloot uses.
- OpenMW effectively relies on additional data paths to load Morrowind's (and mods') files, and the last directory listed that contains a given filename is used to load a file with that filename, with the main data path effectively being the first listed.
- I've disabled support for ghosted plugins for OpenMW because it makes the multi-path stuff more confusing and may not provide any benefit.
This commit is contained in:
Oliver Hamlet
2025-02-01 21:36:03 +00:00
parent 2d3192683f
commit 84c0cca534
24 changed files with 698 additions and 173 deletions
+2
View File
@@ -54,6 +54,8 @@ const char* DescribeGameType(GameType gameType) {
return "The Elder Scrolls III: Morrowind";
case GameType::starfield:
return "Starfield";
case GameType::openmw:
return "OpenMW";
default:
return "Unknown";
}
+50 -34
View File
@@ -89,6 +89,7 @@ bool IsMicrosoftStoreInstall(const GameType gameType,
case GameType::tes5:
case GameType::tes5vr:
case GameType::fo4vr:
case GameType::openmw:
return false;
default:
throw std::logic_error("Unrecognised game type");
@@ -115,42 +116,20 @@ std::filesystem::path GetUserDocumentsPath(
#endif
}
std::vector<std::filesystem::path> GetAdditionalDataPaths(
const GameType gameType,
const std::filesystem::path& dataPath,
const std::filesystem::path& gameLocalPath) {
const auto gamePath = dataPath.parent_path();
if (gameType == GameType::fo4 &&
IsMicrosoftStoreInstall(gameType, gamePath)) {
return {gamePath / MS_FO4_AUTOMATRON_DATA_PATH,
gamePath / MS_FO4_NUKA_WORLD_DATA_PATH,
gamePath / MS_FO4_WASTELAND_DATA_PATH,
gamePath / MS_FO4_TEXTURE_PACK_DATA_PATH,
gamePath / MS_FO4_VAULT_TEC_DATA_PATH,
gamePath / MS_FO4_FAR_HARBOR_DATA_PATH,
gamePath / MS_FO4_CONTRAPTIONS_DATA_PATH};
}
if (gameType == GameType::starfield) {
return {GetUserDocumentsPath(gameLocalPath) / "My Games" / "Starfield" /
"Data"};
}
return {};
}
std::filesystem::path ResolvePluginPath(
GameType gameType,
const std::filesystem::path& dataPath,
const std::filesystem::path& pluginPath) {
auto absolutePath =
pluginPath.is_absolute() ? pluginPath : dataPath / pluginPath;
// In case the plugin is ghosted.
if (!std::filesystem::exists(absolutePath)) {
if (gameType != GameType::openmw && !std::filesystem::exists(absolutePath)) {
const auto logger = loot::getLogger();
if (logger) {
logger->debug("Could not find plugin at {}, adding {} file extension", absolutePath.u8string(), loot::GHOST_FILE_EXTENSION);
logger->debug("Could not find plugin at {}, adding {} file extension",
absolutePath.u8string(),
loot::GHOST_FILE_EXTENSION);
}
absolutePath += loot::GHOST_FILE_EXTENSION;
}
@@ -182,6 +161,35 @@ std::vector<std::filesystem::path> FindArchives(
return archivePaths;
}
std::filesystem::path FindPlugin(
GameType gameType,
const std::filesystem::path& dataPath,
const std::vector<std::filesystem::path>& additionalDataPaths,
const std::string& pluginName) {
const auto relativePath = std::filesystem::u8path(pluginName);
const auto finder = [&](const auto& path) {
const auto resolvedPath = ResolvePluginPath(gameType, path, relativePath);
return std::filesystem::exists(resolvedPath);
};
if (gameType == GameType::openmw) {
const auto it = std::find_if(
additionalDataPaths.rbegin(), additionalDataPaths.rend(), finder);
if (it != additionalDataPaths.rend()) {
return *it;
}
} else {
const auto it = std::find_if(
additionalDataPaths.begin(), additionalDataPaths.end(), finder);
if (it != additionalDataPaths.end()) {
return *it;
}
}
return dataPath / relativePath;
}
}
namespace loot {
@@ -193,9 +201,8 @@ Game::Game(const GameType gameType,
loadOrderHandler_(type_, gamePath_, localDataPath),
conditionEvaluator_(
std::make_shared<ConditionEvaluator>(GetType(), DataPath())),
database_(ApiDatabase(conditionEvaluator_)),
additionalDataPaths_(
::GetAdditionalDataPaths(GetType(), DataPath(), localDataPath)) {
database_(ApiDatabase(conditionEvaluator_)) {
additionalDataPaths_ = loadOrderHandler_.GetAdditionalDataPaths();
conditionEvaluator_->SetAdditionalDataPaths(additionalDataPaths_);
}
@@ -204,6 +211,8 @@ GameType Game::GetType() const { return type_; }
std::filesystem::path Game::DataPath() const {
if (type_ == GameType::tes3) {
return gamePath_ / "Data Files";
} else if (type_ == GameType::openmw) {
return gamePath_ / "resources" / "vfs";
} else {
return gamePath_ / "Data";
}
@@ -237,7 +246,8 @@ void Game::SetAdditionalDataPaths(
}
bool Game::IsValidPlugin(const std::filesystem::path& pluginPath) const {
return Plugin::IsValid(GetType(), ResolvePluginPath(DataPath(), pluginPath));
return Plugin::IsValid(GetType(),
ResolvePluginPath(GetType(), DataPath(), pluginPath));
}
void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
@@ -285,7 +295,12 @@ void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
logger->trace("Starting plugin loading.");
}
const auto masterPath = DataPath() / u8path(masterFilename_);
const auto masterPath = GetType() == GameType::openmw
? FindPlugin(GetType(),
DataPath(),
GetAdditionalDataPaths(),
masterFilename_)
: DataPath() / u8path(masterFilename_);
std::for_each(
std::execution::par_unseq,
pluginPaths.begin(),
@@ -293,7 +308,7 @@ void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
[&](const std::filesystem::path& pluginPath) {
try {
const auto resolvedPluginPath =
ResolvePluginPath(DataPath(), pluginPath);
ResolvePluginPath(GetType(), DataPath(), pluginPath);
const bool loadHeader =
loadHeadersOnly ||
@@ -312,7 +327,8 @@ void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
});
if (!loadHeadersOnly &&
(GetType() == GameType::tes3 || GetType() == GameType::starfield)) {
(GetType() == GameType::tes3 || GetType() == GameType::openmw ||
GetType() == GameType::starfield)) {
auto plugins = cache_.GetPlugins();
const auto pluginsMetadata = Plugin::GetPluginsMetadata(plugins);
for (auto& plugin : plugins) {
+24
View File
@@ -50,6 +50,8 @@ unsigned int mapGameId(GameType gameType) {
return LIBLO_GAME_FO4VR;
case GameType::starfield:
return LIBLO_GAME_STARFIELD;
case GameType::openmw:
return LIBLO_GAME_OPENMW;
default:
throw std::logic_error("Unexpected game type");
}
@@ -208,6 +210,28 @@ std::filesystem::path LoadOrderHandler::GetActivePluginsFilePath() const {
return filePath;
}
std::vector<std::filesystem::path> LoadOrderHandler::GetAdditionalDataPaths() const {
const auto logger = getLogger();
if (logger) {
logger->trace("Getting additional data paths.");
}
char** pathArr = nullptr;
size_t pathArrSize = 0;
const unsigned int ret = lo_get_additional_plugins_directories(gh_.get(), &pathArr, &pathArrSize);
HandleError("get additional data paths", ret);
std::vector<std::filesystem::path> loadOrder;
for (size_t i = 0; i < pathArrSize; i += 1) {
loadOrder.push_back(std::filesystem::u8path(std::string(pathArr[i])));
}
lo_free_string_array(pathArr, pathArrSize);
return loadOrder;
}
void LoadOrderHandler::SetLoadOrder(
const std::vector<std::string>& loadOrder) const {
auto logger = getLogger();
+2
View File
@@ -51,6 +51,8 @@ public:
std::filesystem::path GetActivePluginsFilePath() const;
std::vector<std::filesystem::path> GetAdditionalDataPaths() const;
bool IsPluginActive(const std::string& pluginName) const;
void SetLoadOrder(const std::vector<std::string>& loadOrder) const;
+6 -4
View File
@@ -51,7 +51,8 @@ void HandleError(const std::string operation, int returnCode) {
logger->error(err);
}
throw ConditionSyntaxError(returnCode, loot_condition_interpreter_category(), err);
throw ConditionSyntaxError(
returnCode, loot_condition_interpreter_category(), err);
}
int mapGameType(GameType gameType) {
@@ -76,6 +77,8 @@ int mapGameType(GameType gameType) {
return LCI_GAME_FALLOUT_4_VR;
case GameType::starfield:
return LCI_GAME_STARFIELD;
case GameType::openmw:
return LCI_GAME_OPENMW;
default:
throw std::runtime_error(
"Unrecognised game type encountered while mapping for condition "
@@ -90,9 +93,8 @@ ConditionEvaluator::ConditionEvaluator(const GameType gameType,
lci_state_destroy)) {
lci_state* state = nullptr;
int result = lci_state_create(&state,
mapGameType(gameType),
dataPath.u8string().c_str());
int result = lci_state_create(
&state, mapGameType(gameType), dataPath.u8string().c_str());
HandleError("create state object for condition evaluation", result);
lciState_ = std::unique_ptr<lci_state, decltype(&lci_state_destroy)>(
+102 -14
View File
@@ -135,6 +135,7 @@ std::vector<std::filesystem::path> FindAssociatedArchives(
const std::filesystem::path& pluginPath) {
switch (gameType) {
case GameType::tes3:
case GameType::openmw:
return {};
case GameType::tes5:
// Skyrim (non-SE) plugins can only load BSAs that have exactly the same
@@ -229,18 +230,24 @@ Plugin::Plugin(const GameType gameType,
const GameCache& gameCache,
std::filesystem::path pluginPath,
const bool headerOnly) :
name_(TrimDotGhostExtension(pluginPath.filename().u8string())),
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),
isEmpty_(true) {
auto logger = getLogger();
try {
Load(pluginPath, gameType, headerOnly);
if (gameType != GameType::openmw ||
pluginPath.extension() != ".omwscripts") {
Load(pluginPath, gameType, headerOnly);
auto ret = esp_plugin_is_empty(esPlugin.get(), &isEmpty_);
HandleEspluginError(ret, "check if \"{}\" is empty", name_);
auto ret = esp_plugin_is_empty(esPlugin.get(), &isEmpty_);
HandleEspluginError(ret, "check if \"{}\" is empty", name_);
}
archivePaths_ = FindAssociatedArchives(gameType, gameCache, pluginPath);
@@ -284,6 +291,10 @@ Plugin::Plugin(const GameType gameType,
}
void Plugin::ResolveRecordIds(Vec_PluginMetadata* pluginsMetadata) const {
if (esPlugin == nullptr) {
return;
}
auto ret = esp_plugin_resolve_record_ids(esPlugin.get(), pluginsMetadata);
HandleEspluginError(ret, "resolve the record IDs of \"{}\"", name_);
}
@@ -291,6 +302,10 @@ void Plugin::ResolveRecordIds(Vec_PluginMetadata* pluginsMetadata) const {
std::string Plugin::GetName() const { return name_; }
std::optional<float> Plugin::GetHeaderVersion() const {
if (esPlugin == nullptr) {
return std::nullopt;
}
float version = 0.0f;
const auto ret = esp_plugin_header_version(esPlugin.get(), &version);
@@ -308,6 +323,10 @@ std::optional<std::string> Plugin::GetVersion() const {
}
std::vector<std::string> Plugin::GetMasters() const {
if (esPlugin == nullptr) {
return {};
}
char** masters = nullptr;
size_t numMasters = 0;
const auto ret = esp_plugin_masters(esPlugin.get(), &masters, &numMasters);
@@ -325,6 +344,10 @@ std::vector<Tag> Plugin::GetBashTags() const { return tags_; }
std::optional<uint32_t> Plugin::GetCRC() const { return crc_; }
bool Plugin::IsMaster() const {
if (ignoreMasterFlag_ || esPlugin == nullptr) {
return false;
}
bool isMaster = false;
const auto ret = esp_plugin_is_master(esPlugin.get(), &isMaster);
HandleEspluginError(ret, "check if \"{}\" is a master", name_);
@@ -333,6 +356,10 @@ bool Plugin::IsMaster() const {
}
bool Plugin::IsLightPlugin() const {
if (esPlugin == nullptr) {
return false;
}
bool isLightPlugin = false;
const auto ret = esp_plugin_is_light_plugin(esPlugin.get(), &isLightPlugin);
HandleEspluginError(ret, "check if \"{}\" is a light plugin", name_);
@@ -341,6 +368,10 @@ bool Plugin::IsLightPlugin() const {
}
bool Plugin::IsMediumPlugin() const {
if (esPlugin == nullptr) {
return false;
}
bool isMediumPlugin = false;
const auto ret = esp_plugin_is_medium_plugin(esPlugin.get(), &isMediumPlugin);
HandleEspluginError(ret, "check if \"{}\" is a medium plugin", name_);
@@ -349,6 +380,10 @@ bool Plugin::IsMediumPlugin() const {
}
bool Plugin::IsUpdatePlugin() const {
if (esPlugin == nullptr) {
return false;
}
bool isUpdatePlugin = false;
const auto ret = esp_plugin_is_update_plugin(esPlugin.get(), &isUpdatePlugin);
HandleEspluginError(ret, "check if \"{}\" is an update plugin", name_);
@@ -357,6 +392,10 @@ bool Plugin::IsUpdatePlugin() const {
}
bool Plugin::IsBlueprintPlugin() const {
if (esPlugin == nullptr) {
return false;
}
bool isBlueprintPlugin = false;
const auto ret =
esp_plugin_is_blueprint_plugin(esPlugin.get(), &isBlueprintPlugin);
@@ -366,6 +405,10 @@ bool Plugin::IsBlueprintPlugin() const {
}
bool Plugin::IsValidAsLightPlugin() const {
if (esPlugin == nullptr) {
return false;
}
bool isValid = false;
const auto ret =
esp_plugin_is_valid_as_light_plugin(esPlugin.get(), &isValid);
@@ -375,6 +418,10 @@ bool Plugin::IsValidAsLightPlugin() const {
}
bool Plugin::IsValidAsMediumPlugin() const {
if (esPlugin == nullptr) {
return false;
}
bool isValid = false;
const auto ret =
esp_plugin_is_valid_as_medium_plugin(esPlugin.get(), &isValid);
@@ -385,6 +432,10 @@ bool Plugin::IsValidAsMediumPlugin() const {
}
bool Plugin::IsValidAsUpdatePlugin() const {
if (esPlugin == nullptr) {
return false;
}
bool isValid = false;
const auto ret =
esp_plugin_is_valid_as_update_plugin(esPlugin.get(), &isValid);
@@ -399,9 +450,17 @@ bool Plugin::IsEmpty() const { return isEmpty_; }
bool Plugin::LoadsArchive() const { return !archivePaths_.empty(); }
bool Plugin::DoRecordsOverlap(const PluginInterface& plugin) const {
if (esPlugin == nullptr) {
return false;
}
try {
auto& otherPlugin = dynamic_cast<const Plugin&>(plugin);
if (otherPlugin.esPlugin == nullptr) {
return false;
}
bool doPluginsOverlap = false;
const auto ret = esp_plugin_do_records_overlap(
esPlugin.get(), otherPlugin.esPlugin.get(), &doPluginsOverlap);
@@ -424,6 +483,10 @@ bool Plugin::DoRecordsOverlap(const PluginInterface& plugin) const {
}
size_t Plugin::GetOverrideRecordCount() const {
if (esPlugin == nullptr) {
return 0;
}
size_t overrideRecordCount;
const auto ret =
esp_plugin_count_override_records(esPlugin.get(), &overrideRecordCount);
@@ -433,6 +496,10 @@ size_t Plugin::GetOverrideRecordCount() const {
}
uint32_t Plugin::GetRecordAndGroupCount() const {
if (esPlugin == nullptr) {
return 0;
}
uint32_t recordAndGroupCount = 0;
const auto ret =
esp_plugin_record_and_group_count(esPlugin.get(), &recordAndGroupCount);
@@ -515,6 +582,10 @@ void Plugin::Load(const std::filesystem::path& path,
}
std::string Plugin::GetDescription() const {
if (esPlugin == nullptr) {
return "";
}
char* description = nullptr;
const auto ret = esp_plugin_description(esPlugin.get(), &description);
HandleEspluginError(ret, "read the description of \"{}\"", name_);
@@ -530,7 +601,7 @@ std::string Plugin::GetDescription() const {
}
std::unique_ptr<Vec_PluginMetadata, decltype(&esp_plugins_metadata_free)>
Plugin::GetPluginsMetadata(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)>(
@@ -540,7 +611,10 @@ Plugin::GetPluginsMetadata(std::vector<const Plugin*> plugins) {
std::vector<const ::Plugin*> esPlugins;
esPlugins.reserve(plugins.size());
for (const auto& plugin : plugins) {
esPlugins.push_back(plugin->esPlugin.get());
const auto esPlugin = plugin->esPlugin.get();
if (esPlugin != nullptr) {
esPlugins.push_back(plugin->esPlugin.get());
}
}
Vec_PluginMetadata* pluginsMetadata = nullptr;
@@ -566,6 +640,7 @@ std::string GetArchiveFileExtension(const GameType gameType) {
unsigned int Plugin::GetEspluginGameId(GameType gameType) {
switch (gameType) {
case GameType::tes3:
case GameType::openmw:
return ESP_GAME_MORROWIND;
case GameType::tes4:
return ESP_GAME_OBLIVION;
@@ -589,18 +664,31 @@ unsigned int Plugin::GetEspluginGameId(GameType gameType) {
}
bool hasPluginFileExtension(std::string filename, GameType gameType) {
if (boost::iends_with(filename, GHOST_FILE_EXTENSION)) {
if (gameType != GameType::openmw &&
boost::iends_with(filename, GHOST_FILE_EXTENSION)) {
filename =
filename.substr(0, filename.length() - GHOST_FILE_EXTENSION_LENGTH);
}
bool isEspOrEsm = boost::iends_with(filename, ".esp") ||
boost::iends_with(filename, ".esm");
bool isEsl = (gameType == GameType::fo4 || gameType == GameType::fo4vr ||
gameType == GameType::tes5se || gameType == GameType::tes5vr ||
gameType == GameType::starfield) &&
boost::iends_with(filename, ".esl");
if (boost::iends_with(filename, ".esp") ||
boost::iends_with(filename, ".esm")) {
return true;
}
return isEspOrEsm || isEsl;
if (gameType == GameType::openmw &&
(boost::iends_with(filename, ".omwaddon") ||
boost::iends_with(filename, ".omwgame") ||
boost::iends_with(filename, ".omwscripts"))) {
return true;
}
if ((gameType == GameType::fo4 || gameType == GameType::fo4vr ||
gameType == GameType::tes5se || gameType == GameType::tes5vr ||
gameType == GameType::starfield) &&
boost::iends_with(filename, ".esl")) {
return true;
}
return false;
}
}
+5 -4
View File
@@ -93,8 +93,7 @@ public:
static std::unique_ptr<Vec_PluginMetadata,
decltype(&esp_plugins_metadata_free)>
GetPluginsMetadata(
std::vector<const Plugin*>);
GetPluginsMetadata(const std::vector<const Plugin*>& plugins);
private:
void Load(const std::filesystem::path& path,
@@ -106,8 +105,10 @@ private:
std::string name_;
std::unique_ptr<::Plugin, decltype(&esp_plugin_free)> esPlugin;
bool isEmpty_; // Does the plugin contain any records other than the TES4
// header?
bool ignoreMasterFlag_{false};
bool isEmpty_{false}; // Does the plugin contain any records other than the
// TES4
// header?
std::optional<std::string> version_; // Obtained from description field.
std::optional<uint32_t> crc_;
std::vector<Tag> tags_;
@@ -95,7 +95,8 @@ INSTANTIATE_TEST_SUITE_P(,
GameType::fo4vr,
GameType::tes5vr,
GameType::tes3,
GameType::starfield));
GameType::starfield,
GameType::openmw));
TEST_P(CreateGameHandleTest,
shouldSucceedIfPassedValidParametersWithRelativePaths) {
@@ -124,7 +125,7 @@ TEST_P(CreateGameHandleTest, shouldSucceedIfPassedALocalPathThatDoesNotExist) {
TEST_P(CreateGameHandleTest, shouldThrowIfPassedALocalPathThatIsNotADirectory) {
EXPECT_THROW(CreateGameHandle(GetParam(), gamePath, dataPath / blankEsm),
std::invalid_argument);
std::invalid_argument);
}
#ifdef _WIN32
+63 -11
View File
@@ -83,7 +83,8 @@ INSTANTIATE_TEST_SUITE_P(,
GameType::fo4vr,
GameType::tes5vr,
GameType::tes3,
GameType::starfield));
GameType::starfield,
GameType::openmw));
TEST_P(GameInterfaceTest, setAdditionalDataPathsShouldDoThat) {
const auto paths = std::vector<std::filesystem::path>{
@@ -135,7 +136,16 @@ TEST_P(
}
TEST_P(GameInterfaceTest, loadPluginsShouldTrimDotGhostFileExtensions) {
handle_->LoadPlugins({blankMasterDependentEsm + ".ghost"}, true);
if (GetParam() == GameType::openmw) {
// Ghosting is not supported for OpenMW.
EXPECT_THROW(
handle_->LoadPlugins({blankMasterDependentEsm + ".ghost"}, true),
std::invalid_argument);
return;
} else {
handle_->LoadPlugins({blankMasterDependentEsm + ".ghost"}, true);
}
EXPECT_EQ(1, handle_->GetLoadedPlugins().size());
ASSERT_NO_THROW(handle_->GetPlugin(blankMasterDependentEsm));
@@ -245,11 +255,32 @@ TEST_P(GameInterfaceTest, getLoadOrderShouldReturnTheCurrentLoadOrder) {
std::filesystem::remove(dataPath / std::filesystem::u8path(nonAsciiEsm));
// Set no additional data paths to avoid picking up non-test plugins on PCs
// which have Starfield or Fallout 4 installed.
handle_->SetAdditionalDataPaths({});
// which have Starfield or Fallout 4 installed. Don't clear the additional
// data paths for OpenMW because they come from test config.
if (GetParam() != GameType::openmw) {
handle_->SetAdditionalDataPaths({});
}
handle_->LoadCurrentLoadOrderState();
ASSERT_EQ(getLoadOrder(), handle_->GetLoadOrder());
if (GetParam() == GameType::openmw) {
ASSERT_EQ(std::vector<std::string>({
blankDifferentEsm,
blankDifferentMasterDependentEsm,
blankDifferentEsp,
blankDifferentPluginDependentEsp,
blankMasterDependentEsm,
blankMasterDependentEsp,
blankEsp,
blankPluginDependentEsp,
masterFile,
blankEsm,
blankDifferentMasterDependentEsp,
}),
handle_->GetLoadOrder());
} else {
ASSERT_EQ(getLoadOrder(), handle_->GetLoadOrder());
}
}
TEST_P(GameInterfaceTest, setLoadOrderShouldSetTheLoadOrder) {
@@ -257,8 +288,11 @@ TEST_P(GameInterfaceTest, setLoadOrderShouldSetTheLoadOrder) {
std::filesystem::remove(dataPath / std::filesystem::u8path(nonAsciiEsm));
// Set no additional data paths to avoid picking up non-test plugins on PCs
// which have Starfield or Fallout 4 installed.
handle_->SetAdditionalDataPaths({});
// which have Starfield or Fallout 4 installed. Don't clear the additional
// data paths for OpenMW because they come from test config.
if (GetParam() != GameType::openmw) {
handle_->SetAdditionalDataPaths({});
}
handle_->LoadCurrentLoadOrderState();
@@ -278,6 +312,20 @@ TEST_P(GameInterfaceTest, setLoadOrderShouldSetTheLoadOrder) {
blankEsp,
blankMasterDependentEsp,
};
} else if (GetParam() == GameType::openmw) {
loadOrder = {
blankDifferentMasterDependentEsm,
blankDifferentPluginDependentEsp,
blankDifferentEsm,
blankDifferentEsp,
blankMasterDependentEsm,
blankMasterDependentEsp,
blankPluginDependentEsp,
blankEsp,
masterFile,
blankDifferentMasterDependentEsp,
blankEsm,
};
} else {
loadOrder = {
masterFile,
@@ -302,11 +350,15 @@ TEST_P(GameInterfaceTest, setLoadOrderShouldSetTheLoadOrder) {
EXPECT_EQ(loadOrder, handle_->GetLoadOrder());
if (gameSupportsEsl) {
loadOrder.erase(std::begin(loadOrder));
}
// It's not possible to persist the load order of inactive plugins for
// OpenMW.
if (GetParam() != GameType::openmw) {
if (gameSupportsEsl) {
loadOrder.erase(std::begin(loadOrder));
}
EXPECT_EQ(loadOrder, getLoadOrder());
EXPECT_EQ(loadOrder, getLoadOrder());
}
}
}
}
+28 -6
View File
@@ -58,7 +58,8 @@ INSTANTIATE_TEST_SUITE_P(,
GameType::fo4vr,
GameType::tes5vr,
GameType::tes3,
GameType::starfield));
GameType::starfield,
GameType::openmw));
TEST_P(GameTest, constructingShouldStoreTheGivenValues) {
Game game = Game(GetParam(), gamePath, localPath);
@@ -70,7 +71,7 @@ TEST_P(GameTest, constructingShouldStoreTheGivenValues) {
#ifndef _WIN32
TEST_P(GameTest,
constructingShouldThrowOnLinuxIfLocalPathIsNotGivenExceptForMorrowind) {
if (GetParam() == GameType::tes3) {
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
EXPECT_NO_THROW(Game(GetParam(), gamePath));
} else {
EXPECT_THROW(Game(GetParam(), gamePath), std::system_error);
@@ -118,6 +119,9 @@ TEST_P(
"My Games" / "Starfield" / "Data";
EXPECT_TRUE(boost::ends_with(game.GetAdditionalDataPaths()[0].u8string(),
expectedSuffix.u8string()));
} else if (GetParam() == GameType::openmw) {
EXPECT_EQ(std::vector<std::filesystem::path>{localPath / "data"},
game.GetAdditionalDataPaths());
} else {
EXPECT_TRUE(game.GetAdditionalDataPaths().empty());
}
@@ -195,6 +199,23 @@ TEST_P(GameTest, isValidPluginShouldUseAbsolutePathsAsGiven) {
EXPECT_TRUE(game.IsValidPlugin(path));
}
TEST_P(
GameTest,
isValidPluginShouldTryGhostedPathIfGivenPluginDoesNotExistExceptForOpenMW) {
const Game game(GetParam(), gamePath, localPath);
if (GetParam() == GameType::openmw) {
// This wasn't done for OpenMW during common setup.
const auto pluginPath =
game.DataPath() / (blankMasterDependentEsm + ".ghost");
std::filesystem::rename(dataPath / blankMasterDependentEsm, pluginPath);
EXPECT_FALSE(game.IsValidPlugin(blankMasterDependentEsm));
} else {
EXPECT_TRUE(game.IsValidPlugin(blankMasterDependentEsm));
}
}
TEST_P(
GameTest,
loadPluginsWithHeadersOnlyTrueShouldLoadTheHeadersOfAllInstalledPlugins) {
@@ -289,8 +310,8 @@ TEST_P(GameTest, loadPluginsShouldFindArchivesInAdditionalDataPaths) {
archiveFileExtension);
const auto ba2Path2 =
gamePath / ("../../Fallout 4- Nuka-World (PC)/Content/Data/DLCNukaWorld "
"- Voices_it" +
archiveFileExtension);
"- Voices_it" +
archiveFileExtension);
touch(ba2Path1);
touch(ba2Path2);
@@ -371,10 +392,11 @@ TEST_P(
std::filesystem::remove(dataPath / pluginName);
if (GetParam() == GameType::tes3 || GetParam() == GameType::starfield) {
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw ||
GetParam() == GameType::starfield) {
try {
game.LoadPlugins({blankMasterDependentEsm}, false);
FAIL();
FAIL();
} catch (const std::system_error& e) {
EXPECT_EQ(ESP_ERROR_PLUGIN_METADATA_NOT_FOUND, e.code().value());
EXPECT_EQ(esplugin_category(), e.code().category());
@@ -57,6 +57,8 @@ protected:
std::vector<std::string> getEarlyLoadingPlugins() {
switch (GetParam()) {
case GameType::openmw:
return {"builtin.omwscripts"};
case GameType::tes5:
return {"Skyrim.esm"};
case GameType::tes5se:
@@ -111,7 +113,8 @@ INSTANTIATE_TEST_SUITE_P(,
GameType::fo3,
GameType::fonv,
GameType::fo4,
GameType::tes5se));
GameType::tes5se,
GameType::openmw));
TEST_P(LoadOrderHandlerTest, constructorShouldThrowIfNoGamePathIsSet) {
EXPECT_THROW(LoadOrderHandler(GetParam(), ""), std::invalid_argument);
@@ -129,7 +132,7 @@ TEST_P(LoadOrderHandlerTest, constructorShouldNotThrowIfNoLocalPathIsSet) {
#else
TEST_P(LoadOrderHandlerTest,
constructorShouldNotThrowIfNoLocalPathIsSetAndGameTypeIsMorrowind) {
if (GetParam() == GameType::tes3) {
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
EXPECT_NO_THROW(LoadOrderHandler(GetParam(), gamePath));
} else {
EXPECT_THROW(LoadOrderHandler(GetParam(), gamePath), std::system_error);
@@ -179,7 +182,24 @@ TEST_P(LoadOrderHandlerTest, getLoadOrderShouldReturnTheCurrentLoadOrder) {
auto loadOrderHandler = createHandler();
loadOrderHandler.LoadCurrentState();
ASSERT_EQ(getLoadOrder(), loadOrderHandler.GetLoadOrder());
if (GetParam() == GameType::openmw) {
EXPECT_EQ(std::vector<std::string>({
blankDifferentEsm,
blankDifferentMasterDependentEsm,
blankDifferentEsp,
blankDifferentPluginDependentEsp,
blankMasterDependentEsm,
blankMasterDependentEsp,
blankEsp,
blankPluginDependentEsp,
masterFile,
blankEsm,
blankDifferentMasterDependentEsp,
}),
loadOrderHandler.GetLoadOrder());
} else {
ASSERT_EQ(getLoadOrder(), loadOrderHandler.GetLoadOrder());
}
}
TEST_P(LoadOrderHandlerTest,
@@ -209,6 +229,45 @@ TEST_P(LoadOrderHandlerTest,
loadOrderHandler.GetEarlyLoadingPlugins());
}
TEST_P(LoadOrderHandlerTest, getAdditionalDataPathsShouldReturnValidData) {
if (GetParam() == GameType::fo4) {
// Create the file that indicates it's a Microsoft Store install.
touch(gamePath / "appxmanifest.xml");
}
auto loadOrderHandler = createHandler();
if (GetParam() == GameType::fo4) {
const auto basePath = gamePath / ".." / "..";
EXPECT_EQ(std::vector<std::filesystem::path>(
{basePath / "Fallout 4- Automatron (PC)" / "Content" / "Data",
basePath / "Fallout 4- Nuka-World (PC)" / "Content" / "Data",
basePath / "Fallout 4- Wasteland Workshop (PC)" / "Content" /
"Data",
basePath / "Fallout 4- High Resolution Texture Pack" /
"Content" / "Data",
basePath / "Fallout 4- Vault-Tec Workshop (PC)" / "Content" /
"Data",
basePath / "Fallout 4- Far Harbor (PC)" / "Content" / "Data",
basePath / "Fallout 4- Contraptions Workshop (PC)" /
"Content" / "Data"}),
loadOrderHandler.GetAdditionalDataPaths());
} else if (GetParam() == GameType::starfield) {
ASSERT_EQ(1, loadOrderHandler.GetAdditionalDataPaths().size());
const auto expectedSuffix = std::filesystem::u8path("Documents") /
"My Games" / "Starfield" / "Data";
EXPECT_TRUE(boost::ends_with(
loadOrderHandler.GetAdditionalDataPaths()[0].u8string(),
expectedSuffix.u8string()));
} else if (GetParam() == GameType::openmw) {
EXPECT_EQ(std::vector<std::filesystem::path>{localPath / "data"},
loadOrderHandler.GetAdditionalDataPaths());
} else {
EXPECT_TRUE(loadOrderHandler.GetAdditionalDataPaths().empty());
}
}
TEST_P(LoadOrderHandlerTest, setLoadOrderShouldSetTheLoadOrder) {
auto loadOrderHandler = createHandler();
loadOrderHandler.LoadCurrentState();
@@ -218,7 +277,14 @@ TEST_P(LoadOrderHandlerTest, setLoadOrderShouldSetTheLoadOrder) {
if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se)
loadOrderToSet_.erase(begin(loadOrderToSet_));
EXPECT_EQ(loadOrderToSet_, getLoadOrder());
if (GetParam() == GameType::openmw) {
// Can't set the load order positions of inactive plugins,
// this reads what libloadorder has cached in memory instead of
// what was actually saved.
EXPECT_EQ(loadOrderToSet_, loadOrderHandler.GetLoadOrder());
} else {
EXPECT_EQ(loadOrderToSet_, getLoadOrder());
}
}
TEST_P(LoadOrderHandlerTest, setExternalPluginPathsShouldAcceptAnEmptyVector) {
@@ -91,7 +91,8 @@ INSTANTIATE_TEST_SUITE_P(,
GameType::fo3,
GameType::fonv,
GameType::fo4,
GameType::tes5se));
GameType::tes5se,
GameType::openmw));
TEST_P(ConditionEvaluatorTest,
evaluateShouldReturnTrueForAnEmptyConditionString) {
+112 -31
View File
@@ -85,7 +85,7 @@ protected:
dataPath / blankMasterDependentArchive);
ASSERT_TRUE(
std::filesystem::exists(dataPath / blankMasterDependentArchive));
} else if (GetParam() == GameType::tes3) {
} else if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
touch(dataPath / blankArchive);
blankMasterDependentArchive = "Blank - Master Dependent.bsa";
@@ -191,7 +191,26 @@ INSTANTIATE_TEST_SUITE_P(,
GameType::fo4vr,
GameType::tes5vr,
GameType::tes3,
GameType::starfield));
GameType::starfield,
GameType::openmw));
TEST_P(PluginTest, constructorShouldTrimGhostExtensionExceptForOpenMW) {
const auto pluginPath =
game_.DataPath() / (blankMasterDependentEsm + ".ghost");
if (GetParam() == GameType::openmw) {
// This wasn't done for OpenMW during common setup.
std::filesystem::rename(dataPath / blankMasterDependentEsm, pluginPath);
}
Plugin plugin(game_.GetType(), game_.GetCache(), pluginPath, true);
if (GetParam() == GameType::openmw) {
EXPECT_EQ(pluginPath.filename().u8string(), plugin.GetName());
} else {
EXPECT_EQ(blankMasterDependentEsm, plugin.GetName());
}
}
TEST_P(PluginTest, loadingShouldHandleNonAsciiFilenamesCorrectly) {
Plugin plugin(game_.GetType(),
@@ -209,11 +228,15 @@ TEST_P(PluginTest, loadingHeaderOnlyShouldReadHeaderData) {
EXPECT_EQ(blankEsm, plugin.GetName());
EXPECT_TRUE(plugin.GetMasters().empty());
EXPECT_TRUE(plugin.IsMaster());
if (GetParam() == GameType::openmw) {
EXPECT_FALSE(plugin.IsMaster());
} else {
EXPECT_TRUE(plugin.IsMaster());
}
EXPECT_FALSE(plugin.IsEmpty());
EXPECT_EQ("5.0", plugin.GetVersion());
if (GetParam() == GameType::tes3) {
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
EXPECT_FLOAT_EQ(1.2f, plugin.GetHeaderVersion().value());
} else if (GetParam() == GameType::tes4) {
EXPECT_FLOAT_EQ(0.8f, plugin.GetHeaderVersion().value());
@@ -237,11 +260,15 @@ TEST_P(PluginTest, loadingWholePluginShouldReadHeaderData) {
EXPECT_EQ(blankEsm, plugin.GetName());
EXPECT_TRUE(plugin.GetMasters().empty());
EXPECT_TRUE(plugin.IsMaster());
if (GetParam() == GameType::openmw) {
EXPECT_FALSE(plugin.IsMaster());
} else {
EXPECT_TRUE(plugin.IsMaster());
}
EXPECT_FALSE(plugin.IsEmpty());
EXPECT_EQ("5.0", plugin.GetVersion());
if (GetParam() == GameType::tes3) {
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
EXPECT_FLOAT_EQ(1.2f, plugin.GetHeaderVersion().value());
} else if (GetParam() == GameType::tes4) {
EXPECT_FLOAT_EQ(0.8f, plugin.GetHeaderVersion().value());
@@ -253,12 +280,13 @@ TEST_P(PluginTest, loadingWholePluginShouldReadHeaderData) {
}
TEST_P(PluginTest, loadingWholePluginShouldReadFields) {
Plugin plugin(game_.GetType(),
game_.GetCache(),
game_.DataPath() / (blankMasterDependentEsm + ".ghost"),
false);
const auto pluginName = GetParam() == GameType::openmw
? blankMasterDependentEsm
: blankMasterDependentEsm + ".ghost";
Plugin plugin(
game_.GetType(), game_.GetCache(), game_.DataPath() / pluginName, false);
if (GetParam() == GameType::tes3) {
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
Plugin master(
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsm, false);
const auto pluginsMetadata = Plugin::GetPluginsMetadata({&master});
@@ -297,6 +325,30 @@ TEST_P(PluginTest, loadingANonMasterPluginShouldReadTheMasterFlagAsFalse) {
EXPECT_FALSE(plugin.IsMaster());
}
TEST_P(PluginTest, loadingWholePluginShouldSucceedForOpenMWPlugins) {
const auto omwgame = "Blank.omwgame";
const auto omwaddon = "Blank.omwaddon";
const auto omwscripts = "Blank.omwscripts";
std::filesystem::rename(dataPath / blankEsm, dataPath / omwgame);
std::filesystem::rename(dataPath / blankEsp, dataPath / omwaddon);
std::ofstream out(dataPath / omwscripts);
out.close();
EXPECT_NO_THROW(
Plugin(game_.GetType(), game_.GetCache(), dataPath / omwgame, false));
EXPECT_NO_THROW(
Plugin(game_.GetType(), game_.GetCache(), dataPath / omwaddon, false));
if (GetParam() == GameType::openmw) {
EXPECT_NO_THROW(Plugin(
game_.GetType(), game_.GetCache(), dataPath / omwscripts, false));
} else {
EXPECT_THROW(
Plugin(game_.GetType(), game_.GetCache(), dataPath / omwscripts, false),
std::system_error);
}
}
TEST_P(
PluginTest,
isLightPluginShouldBeTrueForAPluginWithEslFileExtensionForFallout4AndSkyrimSeAndFalseOtherwise) {
@@ -399,7 +451,8 @@ TEST_P(
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsm, true)
.LoadsArchive();
if (GetParam() == GameType::tes3 || GetParam() == GameType::tes4)
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw ||
GetParam() == GameType::tes4)
EXPECT_FALSE(loadsArchive);
else
EXPECT_TRUE(loadsArchive);
@@ -416,7 +469,8 @@ TEST_P(
true)
.LoadsArchive();
if (GetParam() == GameType::tes3 || GetParam() == GameType::starfield)
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw ||
GetParam() == GameType::starfield)
EXPECT_FALSE(loadsArchive);
else
EXPECT_TRUE(loadsArchive);
@@ -431,7 +485,7 @@ TEST_P(
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsp, true)
.LoadsArchive();
if (GetParam() == GameType::tes3)
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw)
EXPECT_FALSE(loadsArchive);
else
EXPECT_TRUE(loadsArchive);
@@ -597,10 +651,12 @@ TEST_P(PluginTest,
doRecordsOverlapShouldReturnFalseForTwoPluginsWithOnlyHeadersLoaded) {
Plugin plugin1(
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsm, true);
Plugin plugin2(game_.GetType(),
game_.GetCache(),
game_.DataPath() / (blankMasterDependentEsm + ".ghost"),
true);
const auto pluginName = GetParam() == GameType::openmw
? blankMasterDependentEsm
: blankMasterDependentEsm + ".ghost";
Plugin plugin2(
game_.GetType(), game_.GetCache(), game_.DataPath() / pluginName, true);
EXPECT_FALSE(plugin1.DoRecordsOverlap(plugin2));
EXPECT_FALSE(plugin2.DoRecordsOverlap(plugin1));
@@ -626,13 +682,14 @@ TEST_P(PluginTest,
doRecordsOverlapShouldReturnTrueIfOnePluginOverridesTheOthersRecords) {
const auto plugin1Name =
GetParam() == GameType::starfield ? blankFullEsm : blankEsm;
const auto plugin2Name = GetParam() == GameType::openmw
? blankMasterDependentEsm
: blankMasterDependentEsm + ".ghost";
Plugin plugin1(
game_.GetType(), game_.GetCache(), game_.DataPath() / plugin1Name, false);
Plugin plugin2(game_.GetType(),
game_.GetCache(),
game_.DataPath() / (blankMasterDependentEsm + ".ghost"),
false);
Plugin plugin2(
game_.GetType(), game_.GetCache(), game_.DataPath() / plugin2Name, false);
if (GetParam() == GameType::starfield) {
plugin1.ResolveRecordIds(nullptr);
@@ -649,7 +706,7 @@ TEST_P(PluginTest, getRecordAndGroupCountShouldReturnTheHeaderFieldValue) {
Plugin plugin(
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsm, true);
if (GetParam() == GameType::tes3) {
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
EXPECT_EQ(10u, plugin.GetRecordAndGroupCount());
} else if (GetParam() == GameType::tes4) {
EXPECT_EQ(14u, plugin.GetRecordAndGroupCount());
@@ -665,7 +722,7 @@ TEST_P(PluginTest,
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsp, false)
.GetAssetCount();
if (GetParam() == GameType::tes3) {
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
EXPECT_EQ(0, assetCount);
} else if (GetParam() == GameType::fo4 || GetParam() == GameType::fo4vr ||
GetParam() == GameType::starfield) {
@@ -690,7 +747,7 @@ TEST_P(PluginTest,
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsp, false);
OtherPluginType plugin2;
if (GetParam() == GameType::tes3) {
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
EXPECT_FALSE(plugin1.DoAssetsOverlap(plugin2));
} else {
EXPECT_THROW(plugin1.DoAssetsOverlap(plugin2), std::invalid_argument);
@@ -734,7 +791,7 @@ TEST_P(PluginTest,
game_.DataPath() / blankMasterDependentEsp,
false);
if (GetParam() == GameType::tes3) {
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
// Morrowind plugins can't load assets.
EXPECT_FALSE(plugin1.DoAssetsOverlap(plugin2));
EXPECT_FALSE(plugin2.DoAssetsOverlap(plugin1));
@@ -744,16 +801,20 @@ TEST_P(PluginTest,
}
}
TEST_P(PluginTest,
hasPluginFileExtensionShouldBeTrueIfFileEndsInDotEspOrDotEsm) {
class HasPluginFileExtensionTest : public ::testing::TestWithParam<GameType> {};
INSTANTIATE_TEST_SUITE_P(,
HasPluginFileExtensionTest,
::testing::ValuesIn(ALL_GAME_TYPES));
TEST_P(HasPluginFileExtensionTest, shouldBeTrueIfFileEndsInDotEspOrDotEsm) {
EXPECT_TRUE(hasPluginFileExtension("file.esp", GetParam()));
EXPECT_TRUE(hasPluginFileExtension("file.esm", GetParam()));
EXPECT_FALSE(hasPluginFileExtension("file.bsa", GetParam()));
}
TEST_P(
PluginTest,
hasPluginFileExtensionShouldBeTrueIfFileEndsInDotEslOnlyForFallout4AndLater) {
TEST_P(HasPluginFileExtensionTest,
shouldBeTrueIfFileEndsInDotEslOnlyForFallout4AndLater) {
bool result = hasPluginFileExtension("file.esl", GetParam());
EXPECT_EQ(GetParam() == GameType::fo4 || GetParam() == GameType::fo4vr ||
@@ -763,6 +824,26 @@ TEST_P(
result);
}
TEST_P(HasPluginFileExtensionTest, shouldTrimGhostExtensionExceptForOpenMW) {
if (GetParam() == GameType::openmw) {
EXPECT_FALSE(hasPluginFileExtension("file.esp.ghost", GetParam()));
EXPECT_FALSE(hasPluginFileExtension("file.esm.ghost", GetParam()));
} else {
EXPECT_TRUE(hasPluginFileExtension("file.esp.ghost", GetParam()));
EXPECT_TRUE(hasPluginFileExtension("file.esm.ghost", GetParam()));
}
EXPECT_FALSE(hasPluginFileExtension("file.bsa.ghost", GetParam()));
}
TEST_P(HasPluginFileExtensionTest, shouldRecogniseOpenMWPluginExtensions) {
EXPECT_EQ(GetParam() == GameType::openmw,
hasPluginFileExtension("file.omwgame", GetParam()));
EXPECT_EQ(GetParam() == GameType::openmw,
hasPluginFileExtension("file.omwaddon", GetParam()));
EXPECT_EQ(GetParam() == GameType::openmw,
hasPluginFileExtension("file.omwscripts", GetParam()));
}
TEST(equivalent, shouldReturnTrueIfGivenEqualPathsThatExist) {
auto path1 = std::filesystem::path("./testing-plugins/LICENSE");
auto path2 = std::filesystem::path("./testing-plugins/LICENSE");
@@ -119,7 +119,8 @@ INSTANTIATE_TEST_SUITE_P(,
::testing::Values(GameType::tes3,
GameType::tes4,
GameType::fo4,
GameType::starfield));
GameType::starfield,
GameType::openmw));
TEST_P(PluginSortTest, sortingWithNoLoadedPluginsShouldReturnAnEmptyList) {
std::vector<std::string> sorted = SortPlugins(game_, game_.GetLoadOrder());
@@ -131,11 +132,31 @@ TEST_P(PluginSortTest,
sortingShouldNotMakeUnnecessaryChangesToAnExistingLoadOrder) {
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
std::vector<std::string> expectedSortedOrder = getLoadOrder();
std::vector<std::string> expectedSortedOrder;
if (GetParam() == GameType::openmw) {
// The existing load order for OpenMW doesn't have plugins loading after
// their masters, because the game doesn't enforce that, and the test
// setup cannot enforce the positions of inactive plugins.
expectedSortedOrder = {
blankDifferentEsm,
blankDifferentMasterDependentEsm,
blankDifferentEsp,
blankDifferentPluginDependentEsp,
blankEsm,
blankMasterDependentEsm,
blankMasterDependentEsp,
blankEsp,
blankPluginDependentEsp,
masterFile,
blankDifferentMasterDependentEsp,
};
} else {
expectedSortedOrder = getLoadOrder();
}
// Check stability by running the sort 100 times.
for (int i = 0; i < 100; i++) {
std::vector<std::string> sorted = SortPlugins(game_, game_.GetLoadOrder());
auto sorted = SortPlugins(game_, game_.GetLoadOrder());
ASSERT_EQ(expectedSortedOrder, sorted) << " for sort " << i;
}
}
@@ -218,6 +239,26 @@ TEST_P(PluginSortTest,
blankDifferentEsp,
blankMasterDependentEsp,
};
} else if (GetParam() == GameType::openmw) {
// OpenMW's starting order is different, so more metadata is needed to see
// a change.
plugin = PluginMetadata(blankEsp);
plugin.SetGroup("A");
game_.GetDatabase().SetPluginUserMetadata(plugin);
expectedSortedOrder = {
blankDifferentEsm,
blankDifferentMasterDependentEsm,
blankDifferentEsp,
blankDifferentPluginDependentEsp,
blankEsp,
blankEsm,
blankMasterDependentEsm,
blankMasterDependentEsp,
blankPluginDependentEsp,
masterFile,
blankDifferentMasterDependentEsp,
};
} else {
expectedSortedOrder = {
masterFile,
@@ -288,6 +329,20 @@ TEST_P(PluginSortTest,
blankDifferentEsp,
blankMasterDependentEsp,
};
} else if (GetParam() == GameType::openmw) {
expectedSortedOrder = {
blankDifferentEsp,
blankDifferentPluginDependentEsp,
blankEsp,
blankPluginDependentEsp,
masterFile,
blankDifferentEsm,
blankDifferentMasterDependentEsm,
blankDifferentMasterDependentEsp,
blankEsm,
blankMasterDependentEsm,
blankMasterDependentEsp,
};
} else {
expectedSortedOrder = {
masterFile,
@@ -346,6 +401,26 @@ TEST_P(PluginSortTest,
blankEsp,
blankMasterDependentEsp,
};
} else if (GetParam() == GameType::openmw) {
// OpenMW's starting order is different, so more metadata is needed to see
// a change.
plugin = PluginMetadata(blankEsp);
plugin.SetLoadAfterFiles({File(blankDifferentMasterDependentEsp)});
game_.GetDatabase().SetPluginUserMetadata(plugin);
expectedSortedOrder = {
blankDifferentEsm,
blankDifferentMasterDependentEsm,
blankDifferentEsp,
blankDifferentPluginDependentEsp,
blankEsm,
blankMasterDependentEsm,
blankMasterDependentEsp,
blankDifferentMasterDependentEsp,
blankEsp,
blankPluginDependentEsp,
masterFile,
};
} else {
expectedSortedOrder = {
masterFile,
@@ -394,6 +469,26 @@ TEST_P(PluginSortTest,
blankEsp,
blankMasterDependentEsp,
};
} else if (GetParam() == GameType::openmw) {
// OpenMW's starting order is different, so more metadata is needed to see
// a change.
plugin = PluginMetadata(blankEsp);
plugin.SetRequirements({File(blankDifferentMasterDependentEsp)});
game_.GetDatabase().SetPluginUserMetadata(plugin);
expectedSortedOrder = {
blankDifferentEsm,
blankDifferentMasterDependentEsm,
blankDifferentEsp,
blankDifferentPluginDependentEsp,
blankEsm,
blankMasterDependentEsm,
blankMasterDependentEsp,
blankDifferentMasterDependentEsp,
blankEsp,
blankPluginDependentEsp,
masterFile,
};
} else {
expectedSortedOrder = {
masterFile,
@@ -520,6 +615,11 @@ TEST_P(PluginSortTest,
TEST_P(
PluginSortTest,
sortingShouldThrowIfMasterlistRequirementEdgeWouldContradictMasterFlags) {
if (GetParam() == GameType::openmw) {
// OpenMW doesn't require master-flagged plugins to load before others.
return;
}
using std::endl;
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
@@ -550,6 +650,11 @@ TEST_P(
TEST_P(PluginSortTest,
sortingShouldThrowIfUserRequirementEdgeWouldContradictMasterFlags) {
if (GetParam() == GameType::openmw) {
// OpenMW doesn't require master-flagged plugins to load before others.
return;
}
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
PluginMetadata plugin(blankEsm);
@@ -573,6 +678,11 @@ TEST_P(PluginSortTest,
TEST_P(PluginSortTest,
sortingShouldThrowIfMasterlistLoadAfterEdgeWouldContradictMasterFlags) {
if (GetParam() == GameType::openmw) {
// OpenMW doesn't require master-flagged plugins to load before others.
return;
}
using std::endl;
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
@@ -603,6 +713,11 @@ TEST_P(PluginSortTest,
TEST_P(PluginSortTest,
sortingShouldThrowIfUserLoadAfterEdgeWouldContradictMasterFlags) {
if (GetParam() == GameType::openmw) {
// OpenMW doesn't require master-flagged plugins to load before others.
return;
}
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
PluginMetadata plugin(blankEsm);
@@ -62,15 +62,6 @@ protected:
return loadedPluginInterfaces;
}
std::vector<ComparableFilename> getNativeLoadOrder() {
std::vector<ComparableFilename> wideLoadOrder;
for (const auto &pluginName : getLoadOrder()) {
wideLoadOrder.push_back(ToComparableFilename(pluginName));
}
return wideLoadOrder;
}
Game game_;
const std::string blankEslEsp;
};
@@ -82,7 +73,8 @@ INSTANTIATE_TEST_SUITE_P(,
::testing::Values(GameType::tes3,
GameType::tes4,
GameType::fo4,
GameType::starfield));
GameType::starfield,
GameType::openmw));
TEST_P(PluginSortingDataTest, lightFlaggedEspFilesShouldNotBeTreatedAsMasters) {
if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) {
@@ -97,22 +89,26 @@ TEST_P(PluginSortingDataTest, lightFlaggedEspFilesShouldNotBeTreatedAsMasters) {
dynamic_cast<const PluginSortingInterface *>(game_.GetPlugin(blankEsp)),
PluginMetadata(),
PluginMetadata(),
getNativeLoadOrder());
{});
EXPECT_FALSE(esp.IsMaster());
auto master = PluginSortingData(
dynamic_cast<const PluginSortingInterface *>(game_.GetPlugin(blankEsm)),
PluginMetadata(),
PluginMetadata(),
getNativeLoadOrder());
EXPECT_TRUE(master.IsMaster());
{});
if (GetParam() == GameType::openmw) {
EXPECT_FALSE(master.IsMaster());
} else {
EXPECT_TRUE(master.IsMaster());
}
if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) {
auto lightMaster = PluginSortingData(
dynamic_cast<const PluginSortingInterface *>(game_.GetPlugin(blankEsl)),
PluginMetadata(),
PluginMetadata(),
getNativeLoadOrder());
{});
EXPECT_TRUE(lightMaster.IsMaster());
auto lightPlugin =
@@ -120,7 +116,7 @@ TEST_P(PluginSortingDataTest, lightFlaggedEspFilesShouldNotBeTreatedAsMasters) {
game_.GetPlugin(blankEslEsp)),
PluginMetadata(),
PluginMetadata(),
getNativeLoadOrder());
{});
EXPECT_FALSE(lightPlugin.IsMaster());
}
}
@@ -133,7 +129,7 @@ TEST_P(PluginSortingDataTest,
dynamic_cast<const Plugin *>(game_.GetPlugin(blankMasterDependentEsm)),
PluginMetadata(),
PluginMetadata(),
getNativeLoadOrder());
{});
if (GetParam() == GameType::starfield) {
EXPECT_EQ(1, plugin.GetOverrideRecordCount());
} else {
@@ -152,7 +148,7 @@ TEST_P(PluginSortingDataTest,
PluginSortingData(dynamic_cast<const Plugin *>(game_.GetPlugin(blankEsm)),
PluginMetadata(),
PluginMetadata(),
getNativeLoadOrder());
{});
if (GetParam() == GameType::starfield) {
EXPECT_TRUE(plugin.IsBlueprintMaster());
} else {
@@ -163,21 +159,21 @@ TEST_P(PluginSortingDataTest,
dynamic_cast<const Plugin *>(game_.GetPlugin(blankDifferentEsm)),
PluginMetadata(),
PluginMetadata(),
getNativeLoadOrder());
{});
EXPECT_FALSE(plugin.IsBlueprintMaster());
plugin =
PluginSortingData(dynamic_cast<const Plugin *>(game_.GetPlugin(blankEsp)),
PluginMetadata(),
PluginMetadata(),
getNativeLoadOrder());
{});
EXPECT_FALSE(plugin.IsBlueprintMaster());
plugin = PluginSortingData(
dynamic_cast<const Plugin *>(game_.GetPlugin(blankDifferentEsp)),
PluginMetadata(),
PluginMetadata(),
getNativeLoadOrder());
{});
EXPECT_FALSE(plugin.IsBlueprintMaster());
}
}
+44 -9
View File
@@ -27,17 +27,33 @@ along with LOOT. If not, see
#include <gtest/gtest.h>
#include <array>
#include <boost/algorithm/string.hpp>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <map>
#include <unordered_set>
#include "loot/enum/game_type.h"
#include "tests/test_helpers.h"
namespace loot {
namespace test {
static const std::array<GameType, 11> ALL_GAME_TYPES = {
GameType::tes3,
GameType::tes4,
GameType::tes5,
GameType::tes5se,
GameType::tes5vr,
GameType::fo3,
GameType::fonv,
GameType::fo4,
GameType::fo4vr,
GameType::starfield,
GameType::openmw,
};
class CommonGameTestFixture : public ::testing::TestWithParam<GameType> {
protected:
CommonGameTestFixture() :
@@ -46,7 +62,7 @@ protected:
german("de"),
missingPath(rootTestPath / "missing"),
gamePath(rootTestPath / "games" / "game"),
dataPath(gamePath / getPluginsFolder()),
dataPath(gamePath / getPluginsFolder()),
localPath(rootTestPath / "local" / "game"),
metadataFilesPath(rootTestPath / "metadata"),
masterFile(getMasterFile()),
@@ -147,12 +163,16 @@ protected:
// Set initial load order and active plugins.
setLoadOrder(getInitialLoadOrder());
// Ghost a plugin.
ASSERT_NO_THROW(std::filesystem::rename(
dataPath / blankMasterDependentEsm,
dataPath / (blankMasterDependentEsm + ".ghost")));
ASSERT_FALSE(exists(dataPath / blankMasterDependentEsm));
ASSERT_TRUE(exists(dataPath / (blankMasterDependentEsm + ".ghost")));
// Ghost a plugin, except for OpenMW.
if (GetParam() != GameType::openmw) {
ASSERT_NO_THROW(std::filesystem::rename(
dataPath / blankMasterDependentEsm,
dataPath / (blankMasterDependentEsm + ".ghost")));
ASSERT_FALSE(exists(dataPath / blankMasterDependentEsm));
ASSERT_TRUE(exists(dataPath / (blankMasterDependentEsm + ".ghost")));
} else {
touch(gamePath / "openmw.cfg");
}
// Write out an non-empty, non-plugin file.
std::ofstream out(dataPath / nonPluginFile);
@@ -226,6 +246,10 @@ protected:
if (!line.empty())
actual.push_back(line);
}
} else if (GetParam() == GameType::openmw) {
throw std::runtime_error(
"OpenMW's load order derivation is too complicated to replicate "
"accurately just for a test.");
} else {
actual = readFileLines(localPath / "Plugins.txt");
for (auto& line : actual) {
@@ -377,7 +401,7 @@ protected:
private:
std::string getMasterFile() const {
if (GetParam() == GameType::tes3)
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw)
return "Morrowind.esm";
else if (GetParam() == GameType::tes4)
return "Oblivion.esm";
@@ -397,7 +421,9 @@ private:
}
std::string getPluginsFolder() const {
if (GetParam() == GameType::tes3) {
if (GetParam() == GameType::openmw) {
return "resources/vfs";
} else if (GetParam() == GameType::tes3) {
return "Data Files";
} else {
return "Data";
@@ -407,6 +433,7 @@ private:
uint32_t getBlankEsmCrc() const {
switch (GetParam()) {
case GameType::tes3:
case GameType::openmw:
return 0x790DC6FB;
case GameType::tes4:
return 0x374E2A6F;
@@ -426,6 +453,14 @@ private:
out << "GameFile0=" << plugin.first << std::endl;
}
}
} else if (GetParam() == GameType::openmw) {
std::ofstream out(localPath / "openmw.cfg");
for (const auto& plugin : loadOrder) {
if (plugin.second) {
out << "content=" << plugin.first << std::endl;
}
}
} else {
std::ofstream out(localPath / "Plugins.txt");
for (const auto& plugin : loadOrder) {
+1 -1
View File
@@ -40,7 +40,7 @@ bool supportsLightPlugins(GameType gameType) {
std::filesystem::path getSourcePluginsPath(GameType gameType) {
using std::filesystem::absolute;
if (gameType == GameType::tes3) {
if (gameType == GameType::tes3 || gameType == GameType::openmw) {
return absolute("./testing-plugins/Morrowind/Data Files");
} else if (gameType == GameType::tes4) {
return absolute("./testing-plugins/Oblivion/Data");