Skip to content

Commit

Permalink
Add Starfield's My Games path as an additional data path
Browse files Browse the repository at this point in the history
Starfield will look for plugins and BA2 files there first before
checking under the game install path.
  • Loading branch information
Ortham committed Sep 7, 2023
1 parent 8c91e1b commit a0e9d3e
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 75 deletions.
36 changes: 30 additions & 6 deletions src/api/game/game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Expand Down Expand Up @@ -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_);
}

Expand Down
3 changes: 1 addition & 2 deletions src/tests/api/interface/game_interface_test.h
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
126 changes: 76 additions & 50 deletions src/tests/api/internals/game/game_test.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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));

const auto archivePaths = game.GetCache().GetArchivePaths();
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 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);

Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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);

Expand Down
6 changes: 1 addition & 5 deletions src/tests/api/internals/metadata/condition_evaluator_test.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,7 @@ class ConditionEvaluatorTest : public CommonGameTestFixture {
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());
Expand Down
18 changes: 6 additions & 12 deletions src/tests/api/internals/plugin_test.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ class PluginTest : public CommonGameTestFixture {
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
Expand Down Expand Up @@ -93,12 +92,10 @@ class PluginTest : public CommonGameTestFixture {
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);

Expand All @@ -111,22 +108,19 @@ class PluginTest : public CommonGameTestFixture {
}

// 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",
Expand Down
6 changes: 6 additions & 0 deletions src/tests/common_game_test_fixture.h
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ class CommonGameTestFixture : public ::testing::TestWithParam<GameType> {
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;

Expand Down

0 comments on commit a0e9d3e

Please sign in to comment.