mirror of
https://github.com/loot/libloot.git
synced 2026-07-27 14:16:01 -07:00
Add Starfield's My Games path as an additional data path
Starfield will look for plugins and BA2 files there first before checking under the game install path.
This commit is contained in:
+30
-6
@@ -95,23 +95,46 @@ bool IsMicrosoftStoreInstall(const GameType gameType,
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path GetUserDocumentsPath(
|
||||
const std::filesystem::path& gameLocalPath) {
|
||||
#ifdef _WIN32
|
||||
PWSTR path;
|
||||
|
||||
if (SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &path) != S_OK)
|
||||
throw std::system_error(GetLastError(),
|
||||
std::system_category(),
|
||||
"Failed to get user Documents path.");
|
||||
|
||||
std::filesystem::path documentsPath(path);
|
||||
CoTaskMemFree(path);
|
||||
|
||||
return documentsPath;
|
||||
#else
|
||||
// Get the documents path relative to the game's local path.
|
||||
return gameLocalPath.parent_path().parent_path().parent_path() / "Documents";
|
||||
#endif
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> GetAdditionalDataPaths(
|
||||
const GameType gameType,
|
||||
const std::filesystem::path& dataPath) {
|
||||
const std::filesystem::path& dataPath,
|
||||
const std::filesystem::path& gameLocalPath) {
|
||||
const auto gamePath = dataPath.parent_path();
|
||||
|
||||
if (gameType == GameType::fo4 &&
|
||||
IsMicrosoftStoreInstall(gameType, gamePath)) {
|
||||
// All DLC directories are listed before the main data path because DLC
|
||||
// plugins in those directories override any in the main data path.
|
||||
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,
|
||||
dataPath};
|
||||
gamePath / MS_FO4_CONTRAPTIONS_DATA_PATH};
|
||||
}
|
||||
|
||||
if (gameType == GameType::starfield) {
|
||||
return {GetUserDocumentsPath(gameLocalPath) / "My Games" / "Starfield" /
|
||||
"Data"};
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -167,7 +190,8 @@ Game::Game(const GameType gameType,
|
||||
conditionEvaluator_(
|
||||
std::make_shared<ConditionEvaluator>(GetType(), DataPath())),
|
||||
database_(ApiDatabase(conditionEvaluator_)),
|
||||
additionalDataPaths_(::GetAdditionalDataPaths(GetType(), DataPath())) {
|
||||
additionalDataPaths_(
|
||||
::GetAdditionalDataPaths(GetType(), DataPath(), localDataPath)) {
|
||||
conditionEvaluator_->SetAdditionalDataPaths(additionalDataPaths_);
|
||||
}
|
||||
|
||||
|
||||
@@ -98,8 +98,7 @@ TEST_P(GameInterfaceTest, isValidPluginShouldReturnFalseForANonPluginFile) {
|
||||
|
||||
TEST_P(GameInterfaceTest, isValidPluginShouldReturnFalseForAnEmptyFile) {
|
||||
// Write out an empty file.
|
||||
std::ofstream out(dataPath / emptyFile);
|
||||
out.close();
|
||||
touch(dataPath / emptyFile);
|
||||
ASSERT_TRUE(std::filesystem::exists(dataPath / emptyFile));
|
||||
|
||||
EXPECT_FALSE(handle_->IsValidPlugin(emptyFile));
|
||||
|
||||
@@ -33,8 +33,7 @@ namespace test {
|
||||
class GameTest : public CommonGameTestFixture {
|
||||
protected:
|
||||
GameTest() : blankArchive("Blank" + GetArchiveFileExtension(GetParam())) {
|
||||
std::ofstream out(dataPath / blankArchive);
|
||||
out.close();
|
||||
touch(dataPath / blankArchive);
|
||||
}
|
||||
|
||||
void loadInstalledPlugins(Game& game, bool headersOnly) {
|
||||
@@ -101,55 +100,90 @@ TEST_P(GameTest, constructingShouldNotThrowIfGameAndLocalPathsAreNotEmpty) {
|
||||
|
||||
TEST_P(
|
||||
GameTest,
|
||||
constructingForAMicrosoftStoreFallout4InstallShouldSetExternalPathsForTheDlcs) {
|
||||
if (GetParam() != GameType::fo4) {
|
||||
return;
|
||||
constructingForFallout4FromMicrosoftStoreOrStarfieldShouldSetAdditionalDataPaths) {
|
||||
if (GetParam() == GameType::fo4) {
|
||||
// Create the file that indicates it's a Microsoft Store install.
|
||||
touch(dataPath.parent_path() / "appxmanifest.xml");
|
||||
}
|
||||
|
||||
const auto touch = [](const std::filesystem::path& path) {
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
std::ofstream out(path);
|
||||
out.close();
|
||||
};
|
||||
|
||||
// Create the file that indicates it's a Microsoft Store install.
|
||||
touch(dataPath.parent_path() / "appxmanifest.xml");
|
||||
|
||||
// Create a few external files.
|
||||
const auto pluginPath =
|
||||
dataPath.parent_path() /
|
||||
"../../Fallout 4- Automatron (PC)/Content/Data/DLCRobot.esm";
|
||||
const auto ba2Path1 =
|
||||
dataPath.parent_path() /
|
||||
"../../Fallout 4- Far Harbor (PC)/Content/Data/DLCCoast - Main.ba2";
|
||||
const auto ba2Path2 =
|
||||
dataPath.parent_path() /
|
||||
"../../Fallout 4- Nuka-World (PC)/Content/Data/DLCNukaWorld "
|
||||
"- Voices_it.ba2";
|
||||
touch(pluginPath);
|
||||
touch(ba2Path1);
|
||||
touch(ba2Path2);
|
||||
|
||||
Game game = Game(GetParam(), dataPath.parent_path(), localPath);
|
||||
|
||||
EXPECT_NO_THROW(loadInstalledPlugins(game, true));
|
||||
if (GetParam() == GameType::fo4) {
|
||||
const auto basePath = dataPath.parent_path() / ".." / "..";
|
||||
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"}),
|
||||
game.GetAdditionalDataPaths());
|
||||
} else if (GetParam() == GameType::starfield) {
|
||||
ASSERT_EQ(1, game.GetAdditionalDataPaths().size());
|
||||
|
||||
const auto archivePaths = game.GetCache().GetArchivePaths();
|
||||
const auto expectedSuffix = std::filesystem::u8path("Documents") /
|
||||
"My Games" / "Starfield" / "Data";
|
||||
EXPECT_TRUE(boost::ends_with(game.GetAdditionalDataPaths()[0].u8string(),
|
||||
expectedSuffix.u8string()));
|
||||
} else {
|
||||
EXPECT_TRUE(game.GetAdditionalDataPaths().empty());
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_EQ(std::set<std::filesystem::path>(
|
||||
{ba2Path1, ba2Path2, dataPath / blankArchive}),
|
||||
archivePaths);
|
||||
TEST_P(GameTest, setAdditionalDataPathsShouldClearTheConditionCache) {
|
||||
Game game = Game(GetParam(), dataPath.parent_path(), localPath);
|
||||
|
||||
PluginMetadata metadata(blankEsm);
|
||||
metadata.SetLoadAfterFiles(
|
||||
{File("DLCRobot.esm", "", "file(\"DLCRobot.esm\")")});
|
||||
metadata.SetLoadAfterFiles({File("plugin.esp", "", "file(\"plugin.esp\")")});
|
||||
game.GetDatabase().SetPluginUserMetadata(metadata);
|
||||
|
||||
const auto evaluatedMetadata =
|
||||
auto evaluatedMetadata =
|
||||
game.GetDatabase().GetPluginUserMetadata(blankEsm, true).value();
|
||||
EXPECT_TRUE(evaluatedMetadata.GetLoadAfterFiles().empty());
|
||||
|
||||
const auto dataFilePath =
|
||||
dataPath.parent_path().parent_path() / "Data" / "plugin.esp";
|
||||
touch(dataFilePath);
|
||||
game.SetAdditionalDataPaths({dataFilePath.parent_path()});
|
||||
|
||||
evaluatedMetadata =
|
||||
game.GetDatabase().GetPluginUserMetadata(blankEsm, true).value();
|
||||
EXPECT_FALSE(evaluatedMetadata.GetLoadAfterFiles().empty());
|
||||
}
|
||||
|
||||
TEST_P(GameTest,
|
||||
setAdditionalDataPathsShouldUpdateWhereLoadOrderPluginsAreFound) {
|
||||
Game game = Game(GetParam(), dataPath.parent_path(), localPath);
|
||||
|
||||
// Set no additional data paths to avoid picking up non-test plugins on PCs
|
||||
// which have Starfield or Fallout 4 installed.
|
||||
game.SetAdditionalDataPaths({});
|
||||
game.LoadCurrentLoadOrderState();
|
||||
auto loadOrder = game.GetLoadOrder();
|
||||
|
||||
const auto dataFilePath =
|
||||
dataPath.parent_path().parent_path() / "Data" / "plugin.esp";
|
||||
std::filesystem::create_directories(dataFilePath.parent_path());
|
||||
std::filesystem::copy_file(getSourcePluginsPath() / blankEsp, dataFilePath);
|
||||
ASSERT_TRUE(std::filesystem::exists(dataFilePath));
|
||||
|
||||
std::filesystem::last_write_time(
|
||||
dataFilePath,
|
||||
std::filesystem::file_time_type::clock().now() + std::chrono::hours(1));
|
||||
|
||||
game.SetAdditionalDataPaths({dataFilePath.parent_path()});
|
||||
game.LoadCurrentLoadOrderState();
|
||||
|
||||
loadOrder.push_back("plugin.esp");
|
||||
|
||||
EXPECT_EQ(loadOrder, game.GetLoadOrder());
|
||||
}
|
||||
|
||||
TEST_P(GameTest, isValidPluginShouldResolveRelativePathsRelativeToDataPath) {
|
||||
const Game game(GetParam(), dataPath.parent_path(), localPath);
|
||||
|
||||
@@ -240,13 +274,7 @@ TEST_P(
|
||||
EXPECT_EQ(expected, game.GetCache().GetArchivePaths());
|
||||
}
|
||||
|
||||
TEST_P(GameTest, loadPluginsShouldFindArchivesInExternalDataPaths) {
|
||||
const auto touch = [](const std::filesystem::path& path) {
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
std::ofstream out(path);
|
||||
out.close();
|
||||
};
|
||||
|
||||
TEST_P(GameTest, loadPluginsShouldFindArchivesInAdditionalDataPaths) {
|
||||
// Create a couple of external archive files.
|
||||
const std::string archiveFileExtension =
|
||||
GetParam() == GameType::fo4 || GetParam() == GameType::fo4vr ||
|
||||
@@ -290,11 +318,9 @@ TEST_P(GameTest, loadPluginsShouldClearTheArchivesCacheBeforeFindingArchives) {
|
||||
TEST_P(
|
||||
GameTest,
|
||||
loadPluginsShouldNotThrowIfAFilenameHasNonWindows1252EncodableCharacters) {
|
||||
auto path =
|
||||
dataPath / std::filesystem::u8path(
|
||||
u8"\u2551\u00BB\u00C1\u2510\u2557\u00FE\u00C3\u00CE.txt");
|
||||
std::ofstream out(path);
|
||||
out.close();
|
||||
touch(dataPath /
|
||||
std::filesystem::u8path(
|
||||
u8"\u2551\u00BB\u00C1\u2510\u2557\u00FE\u00C3\u00CE.txt"));
|
||||
|
||||
Game game = Game(GetParam(), dataPath.parent_path(), localPath);
|
||||
|
||||
|
||||
@@ -45,11 +45,7 @@ protected:
|
||||
std::filesystem::copy_file(dataPath / blankEsm,
|
||||
dataPath / std::filesystem::u8path(nonAsciiEsm));
|
||||
|
||||
auto nonAsciiPath = dataPath / std::filesystem::u8path(nonAsciiNestedFile);
|
||||
std::filesystem::create_directory(nonAsciiPath.parent_path());
|
||||
|
||||
std::ofstream out(nonAsciiPath);
|
||||
out.close();
|
||||
touch(dataPath / std::filesystem::u8path(nonAsciiNestedFile));
|
||||
|
||||
loadInstalledPlugins();
|
||||
evaluator_.RefreshLoadedPluginsState(game_.GetLoadedPlugins());
|
||||
|
||||
@@ -49,8 +49,7 @@ protected:
|
||||
game_.LoadCurrentLoadOrderState();
|
||||
|
||||
// Write out an empty file.
|
||||
std::ofstream out(dataPath / emptyFile);
|
||||
out.close();
|
||||
touch(dataPath / emptyFile);
|
||||
ASSERT_TRUE(std::filesystem::exists(dataPath / emptyFile));
|
||||
|
||||
#ifndef _WIN32
|
||||
@@ -93,12 +92,10 @@ protected:
|
||||
ASSERT_TRUE(
|
||||
std::filesystem::exists(dataPath / blankMasterDependentArchive));
|
||||
} else if (GetParam() == GameType::tes3) {
|
||||
out.open(dataPath / blankArchive);
|
||||
out.close();
|
||||
touch(dataPath / blankArchive);
|
||||
|
||||
blankMasterDependentArchive = "Blank - Master Dependent.bsa";
|
||||
out.open(dataPath / blankMasterDependentArchive);
|
||||
out.close();
|
||||
touch(dataPath / blankMasterDependentArchive);
|
||||
} else {
|
||||
copyPlugin(getSourcePluginsPath(), blankArchive);
|
||||
|
||||
@@ -111,22 +108,19 @@ protected:
|
||||
}
|
||||
|
||||
// Create dummy archive files.
|
||||
out.open(dataPath / blankSuffixArchive);
|
||||
out.close();
|
||||
touch(dataPath / blankSuffixArchive);
|
||||
|
||||
auto nonAsciiArchivePath =
|
||||
dataPath /
|
||||
std::filesystem::u8path(u8"non\u00E1scii" +
|
||||
GetArchiveFileExtension(game_.GetType()));
|
||||
out.open(nonAsciiArchivePath);
|
||||
out.close();
|
||||
touch(dataPath / nonAsciiArchivePath);
|
||||
|
||||
auto nonAsciiPrefixArchivePath =
|
||||
dataPath /
|
||||
std::filesystem::u8path(u8"other non\u00E1scii2 - suffix" +
|
||||
GetArchiveFileExtension(game_.GetType()));
|
||||
out.open(nonAsciiPrefixArchivePath);
|
||||
out.close();
|
||||
touch(dataPath / nonAsciiPrefixArchivePath);
|
||||
|
||||
game_.GetCache().CacheArchivePaths({dataPath / "Blank - Main.ba2",
|
||||
dataPath / "Blank - Textures.ba2",
|
||||
|
||||
@@ -244,6 +244,12 @@ protected:
|
||||
return absolute("./Skyrim/Data");
|
||||
}
|
||||
|
||||
void touch(const std::filesystem::path& path) {
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
std::ofstream out(path);
|
||||
out.close();
|
||||
}
|
||||
|
||||
private:
|
||||
const std::filesystem::path rootTestPath;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user