From 2b0eef7aecb951a5729bbd230e2ffd6c2f663f93 Mon Sep 17 00:00:00 2001 From: Jeremy Rimpo Date: Mon, 2 May 2022 02:03:55 -0500 Subject: [PATCH] libloot 0.18.x compatibility * On startup, migrate old LOOT data folders to new directory path * Incorporate GameSettings data changes * Update settings parser and migration code * Manually update/download masterlist.yaml to LOOT game directory TODO: Improve performace of masterlist updater --- src/game_settings.cpp | 46 ++-- src/game_settings.h | 34 ++- src/lootthread.cpp | 504 ++++++++++++++++++++++++++++++++++++++---- src/lootthread.h | 9 + 4 files changed, 495 insertions(+), 98 deletions(-) diff --git a/src/game_settings.cpp b/src/game_settings.cpp index a6dcfb5..1ef5d53 100644 --- a/src/game_settings.cpp +++ b/src/game_settings.cpp @@ -77,7 +77,7 @@ namespace loot { "1435828767_is1\\InstallLocation" }; lootFolderName_ = "Morrowind"; masterFile_ = "Morrowind.esm"; - mininumHeaderVersion_ = MORROWIND_MINIMUM_HEADER_VERSION; + minimumHeaderVersion_ = MORROWIND_MINIMUM_HEADER_VERSION; } else if (Type() == GameType::tes4) { name_ = "TES IV: Oblivion"; registryKeys_ = { "Software\\Bethesda Softworks\\Oblivion\\Installed Path", @@ -93,7 +93,7 @@ namespace loot { "1458058109_is1\\InstallLocation" }; lootFolderName_ = "Oblivion"; masterFile_ = "Oblivion.esm"; - mininumHeaderVersion_ = OBLIVION_MINIMUM_HEADER_VERSION; + minimumHeaderVersion_ = OBLIVION_MINIMUM_HEADER_VERSION; } else if (Type() == GameType::tes5) { name_ = "TES V: Skyrim"; registryKeys_ = { "Software\\Bethesda Softworks\\Skyrim\\Installed Path", @@ -101,7 +101,7 @@ namespace loot { "Steam App 72850\\InstallLocation" }; lootFolderName_ = "Skyrim"; masterFile_ = "Skyrim.esm"; - mininumHeaderVersion_ = SKYRIM_FO3_MINIMUM_HEADER_VERSION; + minimumHeaderVersion_ = SKYRIM_FO3_MINIMUM_HEADER_VERSION; } else if (Type() == GameType::tes5se) { name_ = "TES V: Skyrim Special Edition"; registryKeys_ = { @@ -110,7 +110,7 @@ namespace loot { "489830\\InstallLocation" }; lootFolderName_ = "Skyrim Special Edition"; masterFile_ = "Skyrim.esm"; - mininumHeaderVersion_ = SKYRIM_SE_MINIMUM_HEADER_VERSION; + minimumHeaderVersion_ = SKYRIM_SE_MINIMUM_HEADER_VERSION; } else if (Type() == GameType::tes5vr) { name_ = "TES V: Skyrim VR"; registryKeys_ = { "Software\\Bethesda Softworks\\Skyrim VR\\Installed Path", @@ -118,7 +118,7 @@ namespace loot { "Steam App 611670\\InstallLocation" }; lootFolderName_ = "Skyrim VR"; masterFile_ = "Skyrim.esm"; - mininumHeaderVersion_ = SKYRIM_SE_MINIMUM_HEADER_VERSION; + minimumHeaderVersion_ = SKYRIM_SE_MINIMUM_HEADER_VERSION; } else if (Type() == GameType::fo3) { name_ = "Fallout 3"; registryKeys_ = { "Software\\Bethesda Softworks\\Fallout3\\Installed Path", @@ -134,7 +134,7 @@ namespace loot { "1248282609_is1\\InstallLocation" }; lootFolderName_ = "Fallout3"; masterFile_ = "Fallout3.esm"; - mininumHeaderVersion_ = SKYRIM_FO3_MINIMUM_HEADER_VERSION; + minimumHeaderVersion_ = SKYRIM_FO3_MINIMUM_HEADER_VERSION; } else if (Type() == GameType::fonv) { name_ = "Fallout: New Vegas"; registryKeys_ = { "Software\\Bethesda Softworks\\FalloutNV\\Installed Path", @@ -150,7 +150,7 @@ namespace loot { "1454587428_is1\\InstallLocation" }; lootFolderName_ = "FalloutNV"; masterFile_ = "FalloutNV.esm"; - mininumHeaderVersion_ = FONV_MINIMUM_HEADER_VERSION; + minimumHeaderVersion_ = FONV_MINIMUM_HEADER_VERSION; } else if (Type() == GameType::fo4) { name_ = "Fallout 4"; registryKeys_ = { "Software\\Bethesda Softworks\\Fallout4\\Installed Path", @@ -158,7 +158,7 @@ namespace loot { "Steam App 377160\\InstallLocation" }; lootFolderName_ = "Fallout4"; masterFile_ = "Fallout4.esm"; - mininumHeaderVersion_ = FO4_MINIMUM_HEADER_VERSION; + minimumHeaderVersion_ = FO4_MINIMUM_HEADER_VERSION; } else if (Type() == GameType::fo4vr) { name_ = "Fallout 4 VR"; registryKeys_ = { @@ -167,7 +167,7 @@ namespace loot { "611660\\InstallLocation" }; lootFolderName_ = "Fallout4VR"; masterFile_ = "Fallout4.esm"; - mininumHeaderVersion_ = FO4_MINIMUM_HEADER_VERSION; + minimumHeaderVersion_ = FO4_MINIMUM_HEADER_VERSION; } if (!folder.empty()) { @@ -207,6 +207,8 @@ namespace loot { return gamePath_ / GetPluginsFolderName(type_); } + bool GameSettings::IsBaseGameInstance() const { return isBaseGameInstance_; } + GameSettings& GameSettings::SetName(const std::string& name) { name_ = name; return *this; @@ -219,7 +221,7 @@ namespace loot { GameSettings& GameSettings::SetMinimumHeaderVersion( float mininumHeaderVersion) { - mininumHeaderVersion_ = mininumHeaderVersion; + minimumHeaderVersion_ = mininumHeaderVersion; return *this; } @@ -260,26 +262,8 @@ namespace loot { return *this; } - void GameSettings::MigrateSettings() { - // If the masterlist repository is set to an official repository and the - // masterlist branch is an old default branch, update the masterlist - // branch to the current default. - if (boost::starts_with(repositoryURL_, "https://github.com/loot/") && - IsRepoBranchOldDefault()) { - SetRepoBranch(currentDefaultBranch); - } - - // If the game is Skyrim VR or Fallout 4 VR and the masterlist repository - // is the official Skyrim SE or Fallout 4 repository (respectively), - // switch to the newer VR-specific repositories. - if (Type() == GameType::tes5vr && - RepoURL() == GameSettings(GameType::tes5se).RepoURL()) { - SetRepoURL(GameSettings(GameType::tes5vr).RepoURL()); - } - - if (Type() == GameType::fo4vr && - RepoURL() == GameSettings(GameType::fo4).RepoURL()) { - SetRepoURL(GameSettings(GameType::fo4vr).RepoURL()); - } + GameSettings& GameSettings::SetIsBaseGameInstance(bool isInstance) { + isBaseGameInstance_ = isInstance; + return *this; } } diff --git a/src/game_settings.h b/src/game_settings.h index 78d8766..3c92da1 100644 --- a/src/game_settings.h +++ b/src/game_settings.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "loot/enum/game_type.h" @@ -12,14 +13,17 @@ namespace loot { constexpr inline std::string_view NEHRIM_STEAM_REGISTRY_KEY = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Steam App " "1014940\\InstallLocation"; + static constexpr const char* DEFAULT_MASTERLIST_BRANCH = "v0.18"; + + std::string GetPluginsFolderName(GameType gameType); + + std::string GetDefaultMasterlistUrl(std::string repoName); class GameSettings { public: - GameSettings(); + GameSettings() = default; explicit GameSettings(const GameType gameType, - const std::string& lootFolder = ""); - - bool IsRepoBranchOldDefault() const; + const std::string& lootFolder = ""); bool operator==( const GameSettings& rhs) const; // Compares names and folder names. @@ -30,40 +34,34 @@ namespace loot { std::string Master() const; float MinimumHeaderVersion() const; std::vector RegistryKeys() const; - std::string RepoURL() const; - std::string RepoBranch() const; + std::string MasterlistSource() const; std::filesystem::path GamePath() const; std::filesystem::path GameLocalPath() const; std::filesystem::path DataPath() const; + bool IsBaseGameInstance() const; GameSettings& SetName(const std::string& name); GameSettings& SetMaster(const std::string& masterFile); GameSettings& SetMinimumHeaderVersion(float minimumHeaderVersion); GameSettings& SetRegistryKeys(const std::vector& registry); - GameSettings& SetRepoURL(const std::string& repositoryURL); - GameSettings& SetRepoBranch(const std::string& repositoryBranch); + GameSettings& SetMasterlistSource(const std::string& source); GameSettings& SetGamePath(const std::filesystem::path& path); GameSettings& SetGameLocalPath(const std::filesystem::path& GameLocalPath); GameSettings& SetGameLocalFolder(const std::string& folderName); - - void MigrateSettings(); + GameSettings& SetIsBaseGameInstance(bool isInstance); private: - static const std::set oldDefaultBranches; - static const std::string currentDefaultBranch; - - GameType type_; + GameType type_{ GameType::tes4 }; std::string name_; std::string masterFile_; - float minimumHeaderVersion_; + float minimumHeaderVersion_{ 0.0f }; + bool isBaseGameInstance_{ true }; std::vector registryKeys_; - std::string pluginsFolderName_; std::string lootFolderName_; - std::string repositoryURL_; - std::string repositoryBranch_; + std::string masterlistSource_; std::filesystem::path gamePath_; //Path to the game's folder. std::filesystem::path gameLocalPath_; diff --git a/src/lootthread.cpp b/src/lootthread.cpp index 4bcb4de..6bfb368 100644 --- a/src/lootthread.cpp +++ b/src/lootthread.cpp @@ -1,3 +1,9 @@ +#pragma comment(lib, "winhttp.lib") + +#include +#include +#include +#include #include "lootthread.h" #include "game_settings.h" #include "version.h" @@ -10,6 +16,11 @@ using std::recursive_mutex; namespace lootcli { +static const std::set oldDefaultBranches( + { "master", "v0.7", "v0.8", "v0.10", "v0.13", "v0.14", "v0.15", "v0.17" }); +static const std::regex GITHUB_REPO_URL_REGEX = +std::regex(R"(^https://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$)", + std::regex::ECMAScript | std::regex::icase); std::string toString(loot::MessageType type) { @@ -110,14 +121,19 @@ fs::path GetLOOTAppData() { } } +fs::path LOOTWorker::gamePath() const +{ + return GetLOOTAppData() / "games" / m_GameSettings.FolderName(); +} + fs::path LOOTWorker::masterlistPath() const { - return GetLOOTAppData() / "games" / m_GameSettings.FolderName() / "masterlist.yaml"; + return gamePath() / "masterlist.yaml"; } fs::path LOOTWorker::userlistPath() const { - return GetLOOTAppData() / "games" / m_GameSettings.FolderName() / "userlist.yaml"; + return gamePath() / "userlist.yaml"; } fs::path LOOTWorker::settingsPath() const { @@ -166,11 +182,6 @@ void LOOTWorker::getSettings(const fs::path& file) { type = cpptoml::option( GameSettings(GameType::tes5se).FolderName()); folder = type; - - auto path = GetLOOTAppData() / "SkyrimSE"; - if (fs::exists(path)) { - fs::rename(path, GetLOOTAppData() / fs::path(*folder)); - } } if (*type == GameSettings(GameType::tes3).FolderName()) { @@ -202,6 +213,17 @@ void LOOTWorker::getSettings(const fs::path& file) { newSettings.SetName(*name); } + auto isBaseGameInstance = game->get_as("isBaseGameInstance"); + if (isBaseGameInstance) { + newSettings.SetIsBaseGameInstance(*isBaseGameInstance); + } + else if (newSettings.FolderName() == "Nehrim" || newSettings.FolderName() == "Enderal" || + newSettings.FolderName() == "Enderal Special Edition") { + // Migrate default settings for Nehrim, Enderal and Enderal SE, + // which are not base game instances. + newSettings.SetIsBaseGameInstance(false); + } + auto master = game->get_as("master"); if (master) { newSettings.SetMaster(*master); @@ -212,14 +234,18 @@ void LOOTWorker::getSettings(const fs::path& file) { newSettings.SetMinimumHeaderVersion((float)* minimumHeaderVersion); } - auto repo = game->get_as("repo"); - if (repo) { - newSettings.SetRepoURL(*repo); + auto source = game->get_as("masterlistSource"); + if (source) { + newSettings.SetMasterlistSource(migrateMasterlistSource(*source)); } - - auto branch = game->get_as("branch"); - if (branch) { - newSettings.SetRepoBranch(*branch); + else { + auto url = game->get_as("repo"); + auto branch = game->get_as("branch"); + auto migratedSource = + migrateMasterlistRepoSettings(newSettings.Type(), url, branch); + if (migratedSource.has_value()) { + newSettings.SetMasterlistSource(migratedSource.value()); + } } auto path = game->get_as("path"); @@ -251,8 +277,6 @@ void LOOTWorker::getSettings(const fs::path& file) { } } - newSettings.MigrateSettings(); - m_GameSettings = newSettings; break; } @@ -269,6 +293,346 @@ void LOOTWorker::getSettings(const fs::path& file) { } } +std::string LOOTWorker::getOldDefaultRepoUrl(loot::GameType gameType) { + switch (gameType) { + case loot::GameType::tes3: + return "https://github.com/loot/morrowind.git"; + case loot::GameType::tes4: + return "https://github.com/loot/oblivion.git"; + case loot::GameType::tes5: + return "https://github.com/loot/skyrim.git"; + case loot::GameType::tes5se: + return "https://github.com/loot/skyrimse.git"; + case loot::GameType::tes5vr: + return "https://github.com/loot/skyrimvr.git"; + case loot::GameType::fo3: + return "https://github.com/loot/fallout3.git"; + case loot::GameType::fonv: + return "https://github.com/loot/falloutnv.git"; + case loot::GameType::fo4: + return "https://github.com/loot/fallout4.git"; + case loot::GameType::fo4vr: + return "https://github.com/loot/fallout4vr.git"; + default: + throw std::runtime_error( + "Unrecognised game type: " + + std::to_string( + static_cast>(gameType))); + } +} + +bool LOOTWorker::isLocalPath(const std::string& location, const std::string& filename) { + if (boost::starts_with(location, "http://") || + boost::starts_with(location, "https://")) { + return false; + } + + // Could be a local path. Only return true if it points to a non-bare + // Git repository that currently has the given branch checked out and + // the given filename exists in the repo root. + auto locationPath = std::filesystem::u8path(location); + + auto filePath = locationPath / std::filesystem::u8path(filename); + + if (!std::filesystem::is_regular_file(filePath)) { + return false; + } + + auto headFilePath = locationPath / ".git" / "HEAD"; + + return std::filesystem::is_regular_file(headFilePath); +} + +bool LOOTWorker::isBranchCheckedOut(const std::filesystem::path& localGitRepo, + const std::string& branch) { + auto headFilePath = localGitRepo / ".git" / "HEAD"; + + std::ifstream in(headFilePath); + if (!in.is_open()) { + return false; + } + + std::string line; + std::getline(in, line); + in.close(); + + return line == "ref: refs/heads/" + branch; +} + +std::optional LOOTWorker::migrateMasterlistRepoSettings(loot::GameType gameType, + std::string url, + std::string branch) { + + if (oldDefaultBranches.count(branch) == 1) { + // Update to the latest masterlist branch. + log(loot::LogLevel::info, "Updating masterlist repository branch from " + branch + " to " + loot::DEFAULT_MASTERLIST_BRANCH); + branch = loot::DEFAULT_MASTERLIST_BRANCH; + } + + if (gameType == loot::GameType::tes5vr && + url == "https://github.com/loot/skyrimse.git") { + // Switch to the VR-specific repository (introduced for LOOT v0.17.0). + auto newUrl = "https://github.com/loot/skyrimvr.git"; + log(loot::LogLevel::info, "Updating masterlist repository URL from" + url + " to " + newUrl); + url = newUrl; + } + + if (gameType == loot::GameType::fo4vr && + url == "https://github.com/loot/fallout4.git") { + // Switch to the VR-specific repository (introduced for LOOT v0.17.0). + auto newUrl = "https://github.com/loot/fallout4vr.git"; + log(loot::LogLevel::info, "Updating masterlist repository URL from " + url + " to " + newUrl); + url = newUrl; + } + + auto filename = "masterlist.yaml"; + if (isLocalPath(url, filename)) { + auto localRepoPath = std::filesystem::u8path(url); + if (!isBranchCheckedOut(localRepoPath, branch)) { + log( + loot::LogLevel::warning, + "The URL " + url + " is a local Git repository path but the configured branch " + + branch + " is not checked out. LOOT will use the path as the masterlist " + "source, but there may be unexpected differences in the loaded " + "metadata if the " + branch +" branch is not manually checked out before the " + "next time the masterlist is updated." + ); + } + + return (localRepoPath / filename).string(); + } + + std::smatch regexMatches; + std::regex_match(url, regexMatches, GITHUB_REPO_URL_REGEX); + if (regexMatches.size() != 3) { + log( + loot::LogLevel::warning, + "Cannot migrate masterlist repository settings as the URL does not " + "point to a repository on GitHub."); + return std::nullopt; + } + + auto githubOwner = regexMatches.str(1); + auto githubRepo = regexMatches.str(2); + + return "https://raw.githubusercontent.com/" + githubOwner + "/" + githubRepo + + "/" + branch + "/masterlist.yaml"; +} + +std::optional LOOTWorker::migrateMasterlistRepoSettings( + loot::GameType gameType, + cpptoml::option url, + cpptoml::option branch) { + + if (!url && !branch) { + log(loot::LogLevel::debug, "Masterlist URL and branch are not configured."); + return std::nullopt; + } + + if (!url) { + // No url, it would be set to a game-type-dependent default. + url = getOldDefaultRepoUrl(gameType); + log(loot::LogLevel::warning, + "Found game branch config property but not repo, " + "defaulting repo to " + *url + " for migration."); + } + + if (!branch) { + // No branch, it would be set to the default, which was v0.17 in the last + // version of LOOT to have a branch config property. + branch = std::string("v0.17"); + log(loot::LogLevel::warning, + "Found repo config property but not branch, " + "defaulting branch to " + *branch + " for migration."); + } + + auto migratedSource = migrateMasterlistRepoSettings(gameType, *url, *branch); + if (migratedSource.has_value()) { + log( + loot::LogLevel::warning, + "Migrated masterlist repository URL " + *url + " and branch " + *branch + " to source " + migratedSource.value()); + } + else { + log( + loot::LogLevel::warning, + "Failed to migrate masterlist repository URL " + *url + " and branch " + *branch); + } + + return migratedSource; +} + +std::string LOOTWorker::migrateMasterlistSource(const std::string& source) { + static const std::vector officialMasterlistRepos = { "morrowind", + "oblivion", + "skyrim", + "skyrimse", + "skyrimvr", + "fallout3", + "falloutnv", + "fallout4", + "fallout4vr", + "enderal" }; + + for (const auto& repo : officialMasterlistRepos) { + for (const auto& branch : oldDefaultBranches) { + const auto url = "https://raw.githubusercontent.com/loot/" + repo + "/" + + branch + "/masterlist.yaml"; + + if (source == url) { + const auto newSource = loot::GetDefaultMasterlistUrl(repo); + + log(loot::LogLevel::info, + "Migrating masterlist source from " + source + " to " + newSource); + + return newSource; + } + } + } + + return source; +} + +DWORD LOOTWorker::GetFile(const WCHAR* szUrl, // Full URL + const CHAR* szFileName) // Local file name +{ + BYTE szTemp[25]; + DWORD dwSize = 0; + DWORD dwDownloaded = 0; + LPSTR pszOutBuffer; + BOOL bResults = FALSE; + HINTERNET hSession = NULL, + hConnect = NULL, + hRequest = NULL; + FILE* pFile; + std::wstring_convert> converter; + + URL_COMPONENTS urlComp; + DWORD dwUrlLen = 0; + + DWORD result = ERROR_SUCCESS; + + // Initialize the URL_COMPONENTS structure. + ZeroMemory(&urlComp, sizeof(urlComp)); + urlComp.dwStructSize = sizeof(urlComp); + + // Set required component lengths to non-zero + // so that they are cracked. + wchar_t szHostName[MAX_PATH] = L""; + wchar_t szURLPath[MAX_PATH * 4] = L""; + urlComp.lpszHostName = szHostName; + urlComp.lpszUrlPath = szURLPath; + urlComp.dwSchemeLength = (DWORD)-1; + urlComp.dwHostNameLength = (DWORD)-1; + urlComp.dwUrlPathLength = (DWORD)-1; + urlComp.dwExtraInfoLength = (DWORD)-1; + if (WinHttpCrackUrl(szUrl, (DWORD)wcslen(szUrl), 0, &urlComp)) { + // Use WinHttpOpen to obtain a session handle. + hSession = WinHttpOpen(L"lootcli/1.5.0", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, 0); + + // Specify an HTTP server. + if (hSession) + hConnect = WinHttpConnect(hSession, szHostName, + urlComp.nPort, 0); + + // Create an HTTP request handle. + if (hConnect) + hRequest = WinHttpOpenRequest(hConnect, L"GET", szURLPath, + NULL, WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + WINHTTP_FLAG_SECURE); + + // Send a request. + if (hRequest) + bResults = WinHttpSendRequest(hRequest, + WINHTTP_NO_ADDITIONAL_HEADERS, + 0, WINHTTP_NO_REQUEST_DATA, 0, + 0, 0); + + + // End the request. + if (bResults) + bResults = WinHttpReceiveResponse(hRequest, NULL); + + // Keep checking for data until there is nothing left. + if (bResults) + { + if (!(pFile = fopen(szFileName, "wb"))) + { + log(loot::LogLevel::debug, "File open failure"); + result = GetLastError(); + } + do + { + // Check for available data. + dwSize = 0; + if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) + { + log(loot::LogLevel::debug, "No data"); + result = GetLastError(); + break; + } + + // No more available data. + if (!dwSize) { + log(loot::LogLevel::debug, "No data"); + result = GetLastError(); + break; + } + + // Allocate space for the buffer. + pszOutBuffer = new char[dwSize + 1]; + if (!pszOutBuffer) + { + log(loot::LogLevel::debug, "Bad buffer"); + result = GetLastError(); + } + + // Read the Data. + ZeroMemory(pszOutBuffer, dwSize + 1); + + if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, + dwSize, &dwDownloaded)) + { + log(loot::LogLevel::debug, "Read data failure"); + result = GetLastError(); + } + else + { + fwrite(pszOutBuffer, sizeof(char), dwSize, pFile); + } + + // Free the memory allocated to the buffer. + delete[] pszOutBuffer; + + // This condition should never be reached since WinHttpQueryDataAvailable + // reported that there are bits to read. + if (!dwDownloaded) + break; + + } while (dwSize > 0); + } + else + { + log(loot::LogLevel::debug, "Response failure"); + result = GetLastError(); + } + + // Close any open handles. + if (hRequest) WinHttpCloseHandle(hRequest); + if (hConnect) WinHttpCloseHandle(hConnect); + if (hSession) WinHttpCloseHandle(hSession); + fflush(pFile); + fclose(pFile); + } else { + log(loot::LogLevel::debug, "URL parse failure: " + converter.to_bytes(szUrl)); + result = GetLastError(); + } + return result; +} + std::string escape(const std::string& s) { return boost::replace_all_copy(s, "\"", "\\\""); @@ -295,25 +659,51 @@ int LOOTWorker::run() try { - // ensure the loot directory exists - fs::path lootAppData = GetLOOTAppData(); - if (lootAppData.empty()) { - log(loot::LogLevel::error, "failed to create loot app data path"); - return 1; - } - - if (!fs::exists(lootAppData)) { - fs::create_directory(lootAppData); - } - fs::path profile(m_PluginListPath); profile = profile.parent_path(); - auto gameHandle = CreateGameHandle( - m_GameId, m_GamePath, profile.string()); - auto db = gameHandle->GetDatabase(); + std::unique_ptr gameHandle = CreateGameHandle( + m_GameId, m_GamePath, profile.string()); m_GameSettings = loot::GameSettings(m_GameId); + if (!GetLOOTAppData().empty()) { + // Make sure that the LOOT game path exists. + auto lootGamePath = gamePath(); + if (!fs::is_directory(lootGamePath)) { + if (fs::exists(lootGamePath)) { + throw loot::FileAccessError( + "Could not create LOOT folder for game, the path exists but is not " + "a directory"); + } + + std::vector legacyGamePaths{ GetLOOTAppData() / + fs::path(m_GameSettings.FolderName()) }; + + if (m_GameSettings.Type() == loot::GameType::tes5se) { + // LOOT v0.10.0 used SkyrimSE as its folder name for Skyrim SE, so + // migrate from that if it's present. + legacyGamePaths.insert(legacyGamePaths.begin(), + GetLOOTAppData() / "SkyrimSE"); + } + + for (const auto& legacyGamePath : legacyGamePaths) { + if (fs::is_directory(legacyGamePath)) { + log(loot::LogLevel::info, + "Found a folder for this game in the LOOT data folder, " + "assuming " + "that it's a legacy game folder and moving into the correct " + "subdirectory..."); + + fs::create_directories(lootGamePath.parent_path()); + fs::rename(legacyGamePath, lootGamePath); + break; + } + } + + fs::create_directories(lootGamePath); + } + } + fs::path settings = settingsPath(); if (fs::exists(settings)) @@ -330,33 +720,54 @@ int LOOTWorker::run() std::locale::global(gen(m_Language + ".UTF-8")); } - bool mlUpdated = false; - if (m_UpdateMasterlist) { + if (true) { progress(Progress::CheckingMasterlistExistence); if (!fs::exists(masterlistPath())) { fs::create_directories(masterlistPath().parent_path()); } progress(Progress::UpdatingMasterlist); + std::wstring_convert> converter; + std::wstring masterlistSource = converter.from_bytes(m_GameSettings.MasterlistSource()); - mlUpdated = loot::UpdateFile( - masterlistPath().string(), - m_GameSettings.RepoURL(), - m_GameSettings.RepoBranch() - ); + log(loot::LogLevel::info, + "Downloading latest masterlist file from " + m_GameSettings.MasterlistSource() + " to " + masterlistPath().string()); + DWORD result = GetFile(masterlistSource.c_str(), masterlistPath().string().c_str()); + if (result != ERROR_SUCCESS) { + LPVOID lpMsgBuf; + LPVOID lpDisplayBuf; + LPCWSTR lpszFunction = TEXT("GetFile"); + DWORD dw = result; + + FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + dw, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR)&lpMsgBuf, + 0, NULL); + + lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, + (lstrlen((LPCTSTR)lpMsgBuf) + lstrlen((LPCTSTR)lpszFunction) + 40) * sizeof(TCHAR)); + StringCchPrintf((LPTSTR)lpDisplayBuf, + LocalSize(lpDisplayBuf) / sizeof(TCHAR), + TEXT("%s failed with error %d: %s"), + lpszFunction, dw, lpMsgBuf); + + std::wstring errorMessage = (LPTSTR)lpDisplayBuf; - if (mlUpdated && !loot::IsLatestFile(masterlistPath().string(), m_GameSettings.RepoBranch())) { log(loot::LogLevel::error, - "the latest masterlist revision contains a syntax error, " - "LOOT is using the most recent valid revision instead. " - "Syntax errors are usually minor and fixed within hours."); + "Error downloading masterlist: " + converter.to_bytes(errorMessage)); + return FALSE; } } progress(Progress::LoadingLists); fs::path userlist = userlistPath(); - db.LoadLists( + gameHandle->GetDatabase().LoadLists( masterlistPath().string(), fs::exists(userlist) ? userlistPath().string() : fs::path()); @@ -384,13 +795,8 @@ int LOOTWorker::run() std::ofstream(m_OutputPath) << createJsonReport(*gameHandle, sortedPlugins); } catch(std::system_error& e) { - if (e.code().category() == loot::libgit2_category()) { - log(loot::LogLevel::error, "A firewall may be blocking LOOT."); - } - - log(loot::LogLevel::error, e.what()); - - return 1; + log(loot::LogLevel::error, e.what()); + return 1; } catch (const std::exception & e) { log(loot::LogLevel::error, e.what()); diff --git a/src/lootthread.h b/src/lootthread.h index c59dc32..f9ae2be 100644 --- a/src/lootthread.h +++ b/src/lootthread.h @@ -34,8 +34,17 @@ private: void progress(Progress p); void log(loot::LogLevel level, const std::string& message) const; + DWORD GetFile(const WCHAR* szUrl, const CHAR* szFileName); void getSettings(const std::filesystem::path& file); + std::string getOldDefaultRepoUrl(loot::GameType gameType); + bool isLocalPath(const std::string& location, const std::string& filename); + bool isBranchCheckedOut(const std::filesystem::path& localGitRepo, + const std::string& branch); + std::optional migrateMasterlistRepoSettings(loot::GameType gameType, std::string url, std::string branch); + std::optional migrateMasterlistRepoSettings(loot::GameType gameType, cpptoml::option url, cpptoml::option branch); + std::string migrateMasterlistSource(const std::string& source); + std::filesystem::path gamePath() const; std::filesystem::path masterlistPath() const; std::filesystem::path settingsPath() const; std::filesystem::path userlistPath() const;