diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/src/game_settings.cpp b/src/game_settings.cpp index d679da1..cd4b7d0 100644 --- a/src/game_settings.cpp +++ b/src/game_settings.cpp @@ -1,305 +1,344 @@ #include "game_settings.h" namespace fs = std::filesystem; -namespace loot { - static constexpr float MORROWIND_MINIMUM_HEADER_VERSION = 1.2f; - static constexpr float OBLIVION_MINIMUM_HEADER_VERSION = 0.8f; - static constexpr float SKYRIM_FO3_MINIMUM_HEADER_VERSION = 0.94f; - static constexpr float SKYRIM_SE_MINIMUM_HEADER_VERSION = 1.7f; - static constexpr float FONV_MINIMUM_HEADER_VERSION = 1.32f; - static constexpr float FO4_MINIMUM_HEADER_VERSION = 0.95f; +namespace loot +{ +static constexpr float MORROWIND_MINIMUM_HEADER_VERSION = 1.2f; +static constexpr float OBLIVION_MINIMUM_HEADER_VERSION = 0.8f; +static constexpr float SKYRIM_FO3_MINIMUM_HEADER_VERSION = 0.94f; +static constexpr float SKYRIM_SE_MINIMUM_HEADER_VERSION = 1.7f; +static constexpr float FONV_MINIMUM_HEADER_VERSION = 1.32f; +static constexpr float FO4_MINIMUM_HEADER_VERSION = 0.95f; - GameType GetGameType(const GameId gameId) { - switch (gameId) { - case GameId::tes3: - return GameType::tes3; - case GameId::tes4: - case GameId::nehrim: - return GameType::tes4; - case GameId::tes5: - case GameId::enderal: - return GameType::tes5; - case GameId::tes5se: - case GameId::enderalse: - return GameType::tes5se; - case GameId::tes5vr: - return GameType::tes5vr; - case GameId::fo3: - return GameType::fo3; - case GameId::fonv: - return GameType::fonv; - case GameId::fo4: - return GameType::fo4; - case GameId::fo4vr: - return GameType::fo4vr; - default: - throw std::logic_error("Unrecognised game ID"); - } - } - - float GetMinimumHeaderVersion(const GameId gameId) { - switch (gameId) { - case GameId::tes3: - return MORROWIND_MINIMUM_HEADER_VERSION; - case GameId::tes4: - case GameId::nehrim: - return OBLIVION_MINIMUM_HEADER_VERSION; - case GameId::tes5: - case GameId::enderal: - return SKYRIM_FO3_MINIMUM_HEADER_VERSION; - case GameId::tes5se: - case GameId::tes5vr: - case GameId::enderalse: - return SKYRIM_SE_MINIMUM_HEADER_VERSION; - case GameId::fo3: - return SKYRIM_FO3_MINIMUM_HEADER_VERSION; - case GameId::fonv: - return FONV_MINIMUM_HEADER_VERSION; - case GameId::fo4: - case GameId::fo4vr: - return FO4_MINIMUM_HEADER_VERSION; - default: - throw std::logic_error("Unrecognised game ID"); - } - } - - std::string GetPluginsFolderName(GameId gameId) { - switch (gameId) { - case GameId::tes3: - return "Data Files"; - case GameId::tes4: - case GameId::nehrim: - case GameId::tes5: - case GameId::enderal: - case GameId::tes5se: - case GameId::enderalse: - case GameId::tes5vr: - case GameId::fo3: - case GameId::fonv: - case GameId::fo4: - case GameId::fo4vr: - return "Data"; - default: - throw std::logic_error("Unrecognised game ID"); - } - } - - std::string ToString(const GameId gameId) { - switch (gameId) { - case GameId::tes3: - return "Morrowind"; - case GameId::tes4: - return "Oblivion"; - case GameId::nehrim: - return "Nehrim"; - case GameId::tes5: - return "Skyrim"; - case GameId::enderal: - return "Enderal"; - case GameId::tes5se: - return "Skyrim Special Edition"; - case GameId::enderalse: - return "Enderal Special Edition"; - case GameId::tes5vr: - return "Skyrim VR"; - case GameId::fo3: - return "Fallout3"; - case GameId::fonv: - return "FalloutNV"; - case GameId::fo4: - return "Fallout4"; - case GameId::fo4vr: - return "Fallout4VR"; - default: - throw std::logic_error("Unrecognised game ID"); - } - } - - bool SupportsLightPlugins(const GameType gameType) { - return gameType == GameType::tes5se || gameType == GameType::tes5vr || - gameType == GameType::fo4 || gameType == GameType::fo4vr; - } - - std::string GetMasterFilename(const GameId gameId) { - switch (gameId) { - case GameId::tes3: - return "Morrowind.esm"; - case GameId::tes4: - return "Oblivion.esm"; - case GameId::nehrim: - return "Nehrim.esm"; - case GameId::tes5: - case GameId::tes5se: - case GameId::tes5vr: - case GameId::enderal: - case GameId::enderalse: - return "Skyrim.esm"; - case GameId::fo3: - return "Fallout3.esm"; - case GameId::fonv: - return "FalloutNV.esm"; - case GameId::fo4: - case GameId::fo4vr: - return "Fallout4.esm"; - default: - throw std::logic_error("Unrecognised game ID"); - } - } - - std::string GetGameName(const GameId gameId) { - switch (gameId) { - case GameId::tes3: - return "TES III: Morrowind"; - case GameId::tes4: - return "TES IV: Oblivion"; - case GameId::nehrim: - return "Nehrim - At Fate's Edge"; - case GameId::tes5: - return "TES V: Skyrim"; - case GameId::enderal: - return "Enderal: Forgotten Stories"; - case GameId::tes5se: - return "TES V: Skyrim Special Edition"; - case GameId::enderalse: - return "Enderal: Forgotten Stories (Special Edition)"; - case GameId::tes5vr: - return "TES V: Skyrim VR"; - case GameId::fo3: - return "Fallout 3"; - case GameId::fonv: - return "Fallout: New Vegas"; - case GameId::fo4: - return "Fallout 4"; - case GameId::fo4vr: - return "Fallout 4 VR"; - default: - throw std::logic_error("Unrecognised game ID"); - } - } - - std::string GetDefaultMasterlistRepositoryName(const GameId gameId) { - switch (gameId) { - case GameId::tes3: - return "morrowind"; - case GameId::tes4: - case GameId::nehrim: - return "oblivion"; - case GameId::tes5: - return "skyrim"; - case GameId::enderal: - case GameId::enderalse: - return "enderal"; - case GameId::tes5se: - return "skyrimse"; - case GameId::tes5vr: - return "skyrimvr"; - case GameId::fo3: - return "fallout3"; - case GameId::fonv: - return "falloutnv"; - case GameId::fo4: - return "fallout4"; - case GameId::fo4vr: - return "fallout4vr"; - default: - throw std::logic_error("Unrecognised game type"); - } - } - - std::string GetDefaultMasterlistUrl(const std::string& repositoryName) { - return std::string("https://raw.githubusercontent.com/loot/") + - repositoryName + "/" + DEFAULT_MASTERLIST_BRANCH + "/masterlist.yaml"; - } - - std::string GetDefaultMasterlistUrl(const GameId gameId) { - const auto repoName = GetDefaultMasterlistRepositoryName(gameId); - - return GetDefaultMasterlistUrl(repoName); - } - - GameSettings::GameSettings(const GameId gameId, const std::string& lootFolder) : - id_(gameId), - type_(GetGameType(gameId)), - name_(GetGameName(gameId)), - masterFile_(GetMasterFilename(gameId)), - minimumHeaderVersion_(GetMinimumHeaderVersion(gameId)), - lootFolderName_(lootFolder), - masterlistSource_(GetDefaultMasterlistUrl(gameId)) {} - - bool GameSettings::operator==(const GameSettings& rhs) const { - return name_ == rhs.Name() || lootFolderName_ == rhs.FolderName(); - } - - GameId GameSettings::Id() const { return id_; } - - GameType GameSettings::Type() const { return type_; } - - std::string GameSettings::Name() const { return name_; } - - std::string GameSettings::FolderName() const { return lootFolderName_; } - - std::string GameSettings::Master() const { return masterFile_; } - - float GameSettings::MinimumHeaderVersion() const { - return minimumHeaderVersion_; - } - - std::string GameSettings::MasterlistSource() const { return masterlistSource_; } - - std::filesystem::path GameSettings::GamePath() const { return gamePath_; } - - std::filesystem::path GameSettings::GameLocalPath() const { - return gameLocalPath_; - } - - std::filesystem::path GameSettings::DataPath() const { - return gamePath_ / GetPluginsFolderName(id_); - } - - GameSettings& GameSettings::SetName(const std::string& name) { - name_ = name; - return *this; - } - - GameSettings& GameSettings::SetMaster(const std::string& masterFile) { - masterFile_ = masterFile; - return *this; - } - - GameSettings& GameSettings::SetMinimumHeaderVersion( - float mininumHeaderVersion) { - minimumHeaderVersion_ = mininumHeaderVersion; - return *this; - } - - GameSettings& GameSettings::SetMasterlistSource(const std::string& source) { - masterlistSource_ = source; - return *this; - } - - GameSettings& GameSettings::SetGamePath(const std::filesystem::path& path) { - gamePath_ = path; - return *this; - } - - GameSettings& GameSettings::SetGameLocalPath( - const std::filesystem::path& path) { - gameLocalPath_ = path; - return *this; - } - - GameSettings& GameSettings::SetGameLocalFolder(const std::string& folderName) { - TCHAR path[MAX_PATH]; - - HRESULT res = ::SHGetFolderPath(nullptr, CSIDL_LOCAL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, path); - fs::path appData; - if (res == S_OK) { - appData = fs::path(path); - } - else { - appData = fs::path(""); - } - gameLocalPath_ = appData / fs::path(folderName); - return *this; - } +GameType GetGameType(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return GameType::tes3; + case GameId::tes4: + case GameId::nehrim: + return GameType::tes4; + case GameId::tes5: + case GameId::enderal: + return GameType::tes5; + case GameId::tes5se: + case GameId::enderalse: + return GameType::tes5se; + case GameId::tes5vr: + return GameType::tes5vr; + case GameId::fo3: + return GameType::fo3; + case GameId::fonv: + return GameType::fonv; + case GameId::fo4: + return GameType::fo4; + case GameId::fo4vr: + return GameType::fo4vr; + default: + throw std::logic_error("Unrecognised game ID"); + } } + +float GetMinimumHeaderVersion(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return MORROWIND_MINIMUM_HEADER_VERSION; + case GameId::tes4: + case GameId::nehrim: + return OBLIVION_MINIMUM_HEADER_VERSION; + case GameId::tes5: + case GameId::enderal: + return SKYRIM_FO3_MINIMUM_HEADER_VERSION; + case GameId::tes5se: + case GameId::tes5vr: + case GameId::enderalse: + return SKYRIM_SE_MINIMUM_HEADER_VERSION; + case GameId::fo3: + return SKYRIM_FO3_MINIMUM_HEADER_VERSION; + case GameId::fonv: + return FONV_MINIMUM_HEADER_VERSION; + case GameId::fo4: + case GameId::fo4vr: + return FO4_MINIMUM_HEADER_VERSION; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +std::string GetPluginsFolderName(GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return "Data Files"; + case GameId::tes4: + case GameId::nehrim: + case GameId::tes5: + case GameId::enderal: + case GameId::tes5se: + case GameId::enderalse: + case GameId::tes5vr: + case GameId::fo3: + case GameId::fonv: + case GameId::fo4: + case GameId::fo4vr: + return "Data"; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +std::string ToString(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return "Morrowind"; + case GameId::tes4: + return "Oblivion"; + case GameId::nehrim: + return "Nehrim"; + case GameId::tes5: + return "Skyrim"; + case GameId::enderal: + return "Enderal"; + case GameId::tes5se: + return "Skyrim Special Edition"; + case GameId::enderalse: + return "Enderal Special Edition"; + case GameId::tes5vr: + return "Skyrim VR"; + case GameId::fo3: + return "Fallout3"; + case GameId::fonv: + return "FalloutNV"; + case GameId::fo4: + return "Fallout4"; + case GameId::fo4vr: + return "Fallout4VR"; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +bool SupportsLightPlugins(const GameType gameType) +{ + return gameType == GameType::tes5se || gameType == GameType::tes5vr || + gameType == GameType::fo4 || gameType == GameType::fo4vr; +} + +std::string GetMasterFilename(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return "Morrowind.esm"; + case GameId::tes4: + return "Oblivion.esm"; + case GameId::nehrim: + return "Nehrim.esm"; + case GameId::tes5: + case GameId::tes5se: + case GameId::tes5vr: + case GameId::enderal: + case GameId::enderalse: + return "Skyrim.esm"; + case GameId::fo3: + return "Fallout3.esm"; + case GameId::fonv: + return "FalloutNV.esm"; + case GameId::fo4: + case GameId::fo4vr: + return "Fallout4.esm"; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +std::string GetGameName(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return "TES III: Morrowind"; + case GameId::tes4: + return "TES IV: Oblivion"; + case GameId::nehrim: + return "Nehrim - At Fate's Edge"; + case GameId::tes5: + return "TES V: Skyrim"; + case GameId::enderal: + return "Enderal: Forgotten Stories"; + case GameId::tes5se: + return "TES V: Skyrim Special Edition"; + case GameId::enderalse: + return "Enderal: Forgotten Stories (Special Edition)"; + case GameId::tes5vr: + return "TES V: Skyrim VR"; + case GameId::fo3: + return "Fallout 3"; + case GameId::fonv: + return "Fallout: New Vegas"; + case GameId::fo4: + return "Fallout 4"; + case GameId::fo4vr: + return "Fallout 4 VR"; + default: + throw std::logic_error("Unrecognised game ID"); + } +} + +std::string GetDefaultMasterlistRepositoryName(const GameId gameId) +{ + switch (gameId) { + case GameId::tes3: + return "morrowind"; + case GameId::tes4: + case GameId::nehrim: + return "oblivion"; + case GameId::tes5: + return "skyrim"; + case GameId::enderal: + case GameId::enderalse: + return "enderal"; + case GameId::tes5se: + return "skyrimse"; + case GameId::tes5vr: + return "skyrimvr"; + case GameId::fo3: + return "fallout3"; + case GameId::fonv: + return "falloutnv"; + case GameId::fo4: + return "fallout4"; + case GameId::fo4vr: + return "fallout4vr"; + default: + throw std::logic_error("Unrecognised game type"); + } +} + +std::string GetDefaultMasterlistUrl(const std::string& repositoryName) +{ + return std::string("https://raw.githubusercontent.com/loot/") + repositoryName + "/" + + DEFAULT_MASTERLIST_BRANCH + "/masterlist.yaml"; +} + +std::string GetDefaultMasterlistUrl(const GameId gameId) +{ + const auto repoName = GetDefaultMasterlistRepositoryName(gameId); + + return GetDefaultMasterlistUrl(repoName); +} + +GameSettings::GameSettings(const GameId gameId, const std::string& lootFolder) + : id_(gameId), type_(GetGameType(gameId)), name_(GetGameName(gameId)), + masterFile_(GetMasterFilename(gameId)), + minimumHeaderVersion_(GetMinimumHeaderVersion(gameId)), + lootFolderName_(lootFolder), masterlistSource_(GetDefaultMasterlistUrl(gameId)) +{} + +bool GameSettings::operator==(const GameSettings& rhs) const +{ + return name_ == rhs.Name() || lootFolderName_ == rhs.FolderName(); +} + +GameId GameSettings::Id() const +{ + return id_; +} + +GameType GameSettings::Type() const +{ + return type_; +} + +std::string GameSettings::Name() const +{ + return name_; +} + +std::string GameSettings::FolderName() const +{ + return lootFolderName_; +} + +std::string GameSettings::Master() const +{ + return masterFile_; +} + +float GameSettings::MinimumHeaderVersion() const +{ + return minimumHeaderVersion_; +} + +std::string GameSettings::MasterlistSource() const +{ + return masterlistSource_; +} + +std::filesystem::path GameSettings::GamePath() const +{ + return gamePath_; +} + +std::filesystem::path GameSettings::GameLocalPath() const +{ + return gameLocalPath_; +} + +std::filesystem::path GameSettings::DataPath() const +{ + return gamePath_ / GetPluginsFolderName(id_); +} + +GameSettings& GameSettings::SetName(const std::string& name) +{ + name_ = name; + return *this; +} + +GameSettings& GameSettings::SetMaster(const std::string& masterFile) +{ + masterFile_ = masterFile; + return *this; +} + +GameSettings& GameSettings::SetMinimumHeaderVersion(float mininumHeaderVersion) +{ + minimumHeaderVersion_ = mininumHeaderVersion; + return *this; +} + +GameSettings& GameSettings::SetMasterlistSource(const std::string& source) +{ + masterlistSource_ = source; + return *this; +} + +GameSettings& GameSettings::SetGamePath(const std::filesystem::path& path) +{ + gamePath_ = path; + return *this; +} + +GameSettings& GameSettings::SetGameLocalPath(const std::filesystem::path& path) +{ + gameLocalPath_ = path; + return *this; +} + +GameSettings& GameSettings::SetGameLocalFolder(const std::string& folderName) +{ + TCHAR path[MAX_PATH]; + + HRESULT res = ::SHGetFolderPath(nullptr, CSIDL_LOCAL_APPDATA, nullptr, + SHGFP_TYPE_CURRENT, path); + fs::path appData; + if (res == S_OK) { + appData = fs::path(path); + } else { + appData = fs::path(""); + } + gameLocalPath_ = appData / fs::path(folderName); + return *this; +} +} // namespace loot diff --git a/src/game_settings.h b/src/game_settings.h index 5ce314f..9c0d84d 100644 --- a/src/game_settings.h +++ b/src/game_settings.h @@ -9,88 +9,89 @@ #include "loot/enum/game_type.h" -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.21"; +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.21"; - enum struct GameId : uint8_t { - tes3, - tes4, - nehrim, - tes5, - enderal, - tes5se, - enderalse, - tes5vr, - fo3, - fonv, - fo4, - fo4vr - }; +enum struct GameId : uint8_t +{ + tes3, + tes4, + nehrim, + tes5, + enderal, + tes5se, + enderalse, + tes5vr, + fo3, + fonv, + fo4, + fo4vr +}; - GameType GetGameType(const GameId gameId); +GameType GetGameType(const GameId gameId); - float GetMinimumHeaderVersion(const GameId gameId); +float GetMinimumHeaderVersion(const GameId gameId); - std::string GetPluginsFolderName(GameId gamiId); +std::string GetPluginsFolderName(GameId gamiId); - std::string ToString(const GameId gameId); +std::string ToString(const GameId gameId); - bool SupportsLightPlugins(const GameType gameType); +bool SupportsLightPlugins(const GameType gameType); - std::string GetMasterFilename(const GameId gameId); +std::string GetMasterFilename(const GameId gameId); - std::string GetGameName(const GameId gameId); +std::string GetGameName(const GameId gameId); - std::string GetDefaultMasterlistRepositoryName(GameId gameId); +std::string GetDefaultMasterlistRepositoryName(GameId gameId); - std::string GetDefaultMasterlistUrl(const std::string& repositoryName); - std::string GetDefaultMasterlistUrl(const GameId gameId); +std::string GetDefaultMasterlistUrl(const std::string& repositoryName); +std::string GetDefaultMasterlistUrl(const GameId gameId); - class GameSettings { - public: - GameSettings() = default; - explicit GameSettings(const GameId gameId, - const std::string& lootFolder = ""); +class GameSettings +{ +public: + GameSettings() = default; + explicit GameSettings(const GameId gameId, const std::string& lootFolder = ""); - bool operator==( - const GameSettings& rhs) const; // Compares names and folder names. + bool operator==(const GameSettings& rhs) const; // Compares names and folder names. - GameId Id() const; - GameType Type() const; - std::string Name() const; //Returns the game's name, eg. "TES IV: Oblivion". - std::string FolderName() const; - std::string Master() const; - float MinimumHeaderVersion() const; - std::string MasterlistSource() const; - std::filesystem::path GamePath() const; - std::filesystem::path GameLocalPath() const; - std::filesystem::path DataPath() const; + GameId Id() const; + GameType Type() const; + std::string Name() const; // Returns the game's name, eg. "TES IV: Oblivion". + std::string FolderName() const; + std::string Master() const; + float MinimumHeaderVersion() const; + std::string MasterlistSource() const; + std::filesystem::path GamePath() const; + std::filesystem::path GameLocalPath() const; + std::filesystem::path DataPath() const; - GameSettings& SetName(const std::string& name); - GameSettings& SetMaster(const std::string& masterFile); - GameSettings& SetMinimumHeaderVersion(float minimumHeaderVersion); - 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); + GameSettings& SetName(const std::string& name); + GameSettings& SetMaster(const std::string& masterFile); + GameSettings& SetMinimumHeaderVersion(float minimumHeaderVersion); + 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); - private: - GameId id_{ GameId::tes4 }; - GameType type_{ GameType::tes4 }; - std::string name_; - std::string masterFile_; - float minimumHeaderVersion_{ 0.0f }; +private: + GameId id_{GameId::tes4}; + GameType type_{GameType::tes4}; + std::string name_; + std::string masterFile_; + float minimumHeaderVersion_{0.0f}; - std::string lootFolderName_; + std::string lootFolderName_; - std::string masterlistSource_; + std::string masterlistSource_; - std::filesystem::path gamePath_; //Path to the game's folder. - std::filesystem::path gameLocalPath_; - }; -} + std::filesystem::path gamePath_; // Path to the game's folder. + std::filesystem::path gameLocalPath_; +}; +} // namespace loot #endif \ No newline at end of file diff --git a/src/lootthread.cpp b/src/lootthread.cpp index bf850aa..f571a97 100644 --- a/src/lootthread.cpp +++ b/src/lootthread.cpp @@ -1,15 +1,15 @@ #pragma comment(lib, "winhttp.lib") -#include -#include -#include -#include -#include #include "lootthread.h" #include "game_settings.h" #include "version.h" +#include +#include +#include +#include +#include -//using namespace loot; +// using namespace loot; namespace fs = std::filesystem; using std::lock_guard; @@ -17,1123 +17,1092 @@ 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) - { - switch (type) - { - case loot::MessageType::say: return "info"; - case loot::MessageType::warn: return "warn"; - case loot::MessageType::error: return "error"; - default: return "unknown"; - } - } - - - LOOTWorker::LOOTWorker() - : m_GameId(loot::GameId::tes5) - , m_GameName("Skyrim") - , m_LogLevel(loot::LogLevel::info) - { - } - - std::string ToLower(std::string text) - { - std::transform( - text.begin(), text.end(), text.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - - return text; - } - - - void LOOTWorker::setGame(const std::string& gameName) - { - static std::map gameMap = { - {"morrowind", loot::GameId::tes3}, - {"oblivion", loot::GameId::tes4}, - {"fallout3", loot::GameId::fo3}, - {"fallout4", loot::GameId::fo4}, - {"fallout4vr", loot::GameId::fo4vr}, - {"falloutnv", loot::GameId::fonv}, - {"skyrim", loot::GameId::tes5}, - {"skyrimse", loot::GameId::tes5se}, - {"skyrimvr", loot::GameId::tes5vr}, - }; - - auto iter = gameMap.find(ToLower(gameName)); - - if (iter != gameMap.end()) { - m_GameName = gameName; - if (ToLower(gameName) == "skyrimse") { - m_GameName = "Skyrim Special Edition"; - } - m_GameId = iter->second; - } - else { - throw std::runtime_error("invalid game name \"" + gameName + "\""); - } - } - - void LOOTWorker::setGamePath(const std::string& gamePath) - { - m_GamePath = gamePath; - } - - void LOOTWorker::setOutput(const std::string& outputPath) - { - m_OutputPath = outputPath; - } - - void LOOTWorker::setUpdateMasterlist(bool update) - { - m_UpdateMasterlist = update; - } - - void LOOTWorker::setPluginListPath(const std::string& pluginListPath) - { - m_PluginListPath = pluginListPath; - } - - void LOOTWorker::setLanguageCode(const std::string& languageCode) - { - m_Language = languageCode; - } - - void LOOTWorker::setLogLevel(loot::LogLevel level) - { - m_LogLevel = level; - } - - fs::path GetLOOTAppData() { - TCHAR path[MAX_PATH]; - - HRESULT res = ::SHGetFolderPath(nullptr, CSIDL_LOCAL_APPDATA, nullptr, SHGFP_TYPE_CURRENT, path); - - if (res == S_OK) { - return fs::path(path) / "LOOT"; - } - else { - return fs::path(""); - } - } - - fs::path LOOTWorker::gamePath() const - { - return GetLOOTAppData() / "games" / m_GameSettings.FolderName(); - } - - fs::path LOOTWorker::masterlistPath() const - { - return gamePath() / "masterlist.yaml"; - } - - fs::path LOOTWorker::userlistPath() const - { - return gamePath() / "userlist.yaml"; - } - fs::path LOOTWorker::settingsPath() const - { - return GetLOOTAppData() / "settings.toml"; - } - - fs::path LOOTWorker::l10nPath() const - { - return GetLOOTAppData() / "resources" / "l10n"; - } - - fs::path LOOTWorker::dataPath() const - { - return m_GameSettings.DataPath(); - } - - void LOOTWorker::getSettings(const fs::path& file) { - lock_guard guard(mutex_); - // Don't use cpptoml::parse_file() as it just uses a std stream, - // which don't support UTF-8 paths on Windows. - std::ifstream in(file); - if (!in.is_open()) - throw std::runtime_error(file.string() + - " could not be opened for parsing"); - - const auto settings = toml::parse(in, file.string()); - const auto games = settings["games"]; - if (games.is_array_of_tables()) { - for (const auto& game : *games.as_array()) { - try { - if (!game.is_table()) { - throw std::runtime_error("games array element is not a table"); - } - auto gameTable = *game.as_table(); - - using loot::GameSettings; - using loot::GameId; - - auto id = gameTable["gameId"].value(); - if (!id) { - throw std::runtime_error( - "'gameId' and 'type' keys both missing from game settings table"); - } - const auto gameType = *id; - GameId gameId; - - if (gameType == "Morrowind") { - gameId = GameId::tes3; - } - else if (gameType == "Oblivion") { - // The Oblivion game type is shared between Oblivon and Nehrim. - gameId = IsNehrim(gameTable) ? GameId::nehrim : GameId::tes4; - } - else if (gameType == "Skyrim") { - // The Skyrim game type is shared between Skyrim and Enderal. - gameId = IsEnderal(gameTable) ? GameId::enderal : GameId::tes5; - } - else if (gameType == "SkyrimSE" || gameType == "Skyrim Special Edition") { - // The Skyrim SE game type is shared between Skyrim SE and Enderal SE. - gameId = IsEnderalSE(gameTable) ? GameId::enderalse : GameId::tes5se; - } - else if (gameType == "Skyrim VR") { - gameId = GameId::tes5vr; - } - else if (gameType == "Fallout3") { - gameId = GameId::fo3; - } - else if (gameType == "FalloutNV") { - gameId = GameId::fonv; - } - else if (gameType == "Fallout4") { - gameId = GameId::fo4; - } - else if (gameType == "Fallout4VR") { - gameId = GameId::fo4vr; - } - else { - throw std::runtime_error( - "invalid value for 'type' key in game settings table"); - } - - auto folder = gameTable["folder"].value(); - if (!folder) { - throw std::runtime_error("'folder' key missing from game settings table"); - } - - const auto type = gameTable["type"].value(); - - // SkyrimSE was a previous serialised value for GameType::tes5se, - // and the game folder name LOOT created for that game type. - if (type && *type == "SkyrimSE" && *folder == *type) { - folder = "Skyrim Special Edition"; - } - - GameSettings newSettings(gameId, folder.value()); - - - if (newSettings.Type() == m_GameSettings.Type()) { - - auto name = gameTable["name"].value(); - if (name) { - newSettings.SetName(*name); - } - - auto master = gameTable["master"].value(); - if (master) { - newSettings.SetMaster(*master); - } - - const auto minimumHeaderVersion = - gameTable["minimumHeaderVersion"].value(); - if (minimumHeaderVersion) { - newSettings.SetMinimumHeaderVersion((float)*minimumHeaderVersion); - } - - auto source = gameTable["masterlistSource"].value(); - if (source) { - newSettings.SetMasterlistSource(migrateMasterlistSource(*source)); - } - else { - auto url = gameTable["repo"].value(); - auto branch = gameTable["branch"].value(); - auto migratedSource = migrateMasterlistRepoSettings(newSettings.Id(), *url, *branch); - if (migratedSource.has_value()) { - newSettings.SetMasterlistSource(migratedSource.value()); - } - } - - auto path = gameTable["path"].value(); - if (path) { - newSettings.SetGamePath(std::filesystem::u8path(*path)); - } - - auto localPath = gameTable["local_path"].value(); - auto localFolder = gameTable["local_folder"].value(); - if (localPath && localFolder) { - throw std::runtime_error( - "Game settings have local_path and local_folder set, use only one."); - } - else if (localPath) { - newSettings.SetGameLocalPath(std::filesystem::u8path(*localPath)); - } - else if (localFolder) { - newSettings.SetGameLocalFolder(*localFolder); - } - - m_GameSettings = newSettings; - break; - } - } - catch (...) { - // Skip invalid games. - } - } - } - - if (m_Language.empty()) { - m_Language = settings["language"].value_or(loot::MessageContent::DEFAULT_LANGUAGE); - } - } - - std::optional LOOTWorker::GetLocalFolder(const toml::table& table) { - const auto localPath = table["local_path"].value(); - const auto localFolder = table["local_folder"].value(); - - if (localFolder.has_value()) { - return localFolder; - } - - if (localPath.has_value()) { - return std::filesystem::u8path(*localPath).filename().string(); - } - - return std::nullopt; - } - - - bool LOOTWorker::IsNehrim(const toml::table& table) { - const auto installPath = table["path"].value(); - - if (installPath.has_value() && !installPath.value().empty()) { - const auto path = std::filesystem::u8path(installPath.value()); - if (std::filesystem::exists(path)) { - return std::filesystem::exists(path / "NehrimLauncher.exe"); - } - } - - // Fall back to using heuristics based on the existing settings. - // Return true if any of these heuristics return a positive match. - const auto gameName = table["name"].value(); - const auto masterFilename = table["master"].value(); - const auto isBaseGameInstance = table["isBaseGameInstance"].value(); - const auto folder = table["folder"].value(); - - return - // Nehrim uses a different main master file from Oblivion. - (masterFilename.has_value() && - masterFilename.value() == - loot::GetMasterFilename(loot::GameId::nehrim)) || - // Game name probably includes "nehrim". - (gameName.has_value() && boost::icontains(gameName.value(), "nehrim")) || - // LOOT folder name probably includes "nehrim". - (folder.has_value() && boost::icontains(folder.value(), "nehrim")) || - // Between 0.18.1 and 0.19.0 inclusive, LOOT had an isBaseGameInstance - // game setting that was false for Nehrim, Enderal and Enderal SE. - (isBaseGameInstance.has_value() && !isBaseGameInstance.value()); - } - - bool LOOTWorker::IsEnderal(const toml::table& table, - const std::string& expectedLocalFolder) { - const auto installPath = table["path"].value(); - - if (installPath.has_value() && !installPath.value().empty()) { - const auto path = std::filesystem::u8path(installPath.value()); - if (std::filesystem::exists(path)) { - return std::filesystem::exists(path / "Enderal Launcher.exe"); - } - } - - // Fall back to using heuristics based on the existing settings. - // Return true if any of these heuristics return a positive match. - const auto gameName = table["name"].value(); - const auto isBaseGameInstance = table["isBaseGameInstance"].value(); - const auto localFolder = GetLocalFolder(table); - const auto folder = table["folder"].value(); - - return - // Enderal and Enderal SE use different local folders than their base - // games. - (localFolder.has_value() && localFolder.value() == expectedLocalFolder) || - // Game name probably includes "enderal". - (gameName.has_value() && boost::icontains(gameName.value(), "enderal")) || - // LOOT folder name probably includes "enderal". - (folder.has_value() && boost::icontains(folder.value(), "enderal")) || - // Between 0.18.1 and 0.19.0 inclusive, LOOT had an isBaseGameInstance - // game setting that was false for Nehrim, Enderal and Enderal SE. - (isBaseGameInstance.has_value() && !isBaseGameInstance.value()); - } - - bool LOOTWorker::IsEnderal(const toml::table& table) { return IsEnderal(table, "enderal"); } - - bool LOOTWorker::IsEnderalSE(const toml::table& table) { - return IsEnderal(table, "Enderal Special Edition"); - } - - std::string LOOTWorker::getOldDefaultRepoUrl(loot::GameId GameId) { - switch (GameId) { - case loot::GameId::tes3: - return "https://github.com/loot/morrowind.git"; - case loot::GameId::tes4: - return "https://github.com/loot/oblivion.git"; - case loot::GameId::tes5: - return "https://github.com/loot/skyrim.git"; - case loot::GameId::tes5se: - return "https://github.com/loot/skyrimse.git"; - case loot::GameId::tes5vr: - return "https://github.com/loot/skyrimvr.git"; - case loot::GameId::fo3: - return "https://github.com/loot/fallout3.git"; - case loot::GameId::fonv: - return "https://github.com/loot/falloutnv.git"; - case loot::GameId::fo4: - return "https://github.com/loot/fallout4.git"; - case loot::GameId::fo4vr: - return "https://github.com/loot/fallout4vr.git"; - default: - throw std::runtime_error( - "Unrecognised game type: " + - std::to_string( - static_cast>(GameId))); - } - } - - 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::GameId GameId, - 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 (GameId == loot::GameId::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 (GameId == loot::GameId::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::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, "\"", "\\\""); - } - - int LOOTWorker::run() - { - m_startTime = std::chrono::high_resolution_clock::now(); - - { - // Do some preliminary locale / UTF-8 support setup here, in case the settings file reading requires it. - //Boost.Locale initialisation: Specify location of language dictionaries. - boost::locale::generator gen; - gen.add_messages_path(l10nPath().string()); - gen.add_messages_domain("loot"); - - //Boost.Locale initialisation: Generate and imbue locales. - std::locale::global(gen("en.UTF-8")); - } - - loot::SetLoggingCallback([&](loot::LogLevel level, const char* message) { - log(level, message); - }); - - - try { - fs::path profile(m_PluginListPath); - profile = profile.parent_path(); - - m_GameSettings = loot::GameSettings(m_GameId, m_GamePath); - - std::unique_ptr gameHandle = CreateGameHandle( - m_GameSettings.Type(), m_GamePath, profile.string()); - - 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.Id() == loot::GameId::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)) - getSettings(settings); - - m_GameSettings.SetGamePath(m_GamePath); - - if (m_Language != loot::MessageContent::DEFAULT_LANGUAGE) { - log(loot::LogLevel::debug, "initialising language settings"); - log(loot::LogLevel::debug, "selected language: " + m_Language); - - //Boost.Locale initialisation: Generate and imbue locales. - boost::locale::generator gen; - std::locale::global(gen(m_Language + ".UTF-8")); - } - - 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()); - - 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; - - log(loot::LogLevel::error, - "Error downloading masterlist: " + converter.to_bytes(errorMessage)); - return FALSE; - } - } - - progress(Progress::LoadingLists); - - fs::path userlist = userlistPath(); - gameHandle->GetDatabase().LoadLists( - masterlistPath().string(), - fs::exists(userlist) ? userlistPath().string() : fs::path()); - - progress(Progress::ReadingPlugins); - gameHandle->LoadCurrentLoadOrderState(); - std::vector pluginsList; - for (auto plugin : gameHandle->GetLoadOrder()) { - std::filesystem::path pluginPath(plugin); - pluginsList.push_back(pluginPath); - } - - progress(Progress::SortingPlugins); - std::vector sortedPlugins = gameHandle->SortPlugins(pluginsList); - - progress(Progress::WritingLoadorder); - - std::ofstream outf(m_PluginListPath); - if (!outf) { - log(loot::LogLevel::error, "failed to open " + m_PluginListPath + " to rewrite it"); - return 1; - } - outf << "# This file was automatically generated by Mod Organizer." << std::endl; - for (const std::string& plugin : sortedPlugins) { - outf << plugin << std::endl; - } - outf.close(); - - progress(Progress::ParsingLootMessages); - std::ofstream(m_OutputPath) << createJsonReport(*gameHandle, sortedPlugins); - } - catch (std::system_error& e) { - log(loot::LogLevel::error, e.what()); - return 1; - } - catch (const std::exception& e) { - log(loot::LogLevel::error, e.what()); - return 1; - } - - progress(Progress::Done); - - return 0; - } - - void set(QJsonObject& o, const char* e, const QJsonValue& v) - { - if (v.isObject() && v.toObject().isEmpty()) { - return; - } - - if (v.isArray() && v.toArray().isEmpty()) { - return; - } - - if (v.isString() && v.toString().isEmpty()) { - return; - } - - o[e] = v; - } - - std::string LOOTWorker::createJsonReport( - loot::GameInterface& game, const std::vector& sortedPlugins) const - { - QJsonObject root; - - set(root, "messages", createMessages(game.GetDatabase().GetGeneralMessages(true))); - set(root, "plugins", createPlugins(game, sortedPlugins)); - - const auto end = std::chrono::high_resolution_clock::now(); - - set(root, "stats", QJsonObject{ - {"time", std::chrono::duration_cast(end - m_startTime).count()}, - {"lootcliVersion", LOOTCLI_VERSION_STRING}, - {"lootVersion", QString::fromStdString(loot::GetLiblootVersion())} - }); - - QJsonDocument doc(root); - return doc.toJson(QJsonDocument::Indented).toStdString(); - } - - template - QJsonArray createStringArray(const Container& c) - { - QJsonArray array; - - for (auto&& e : c) { - array.push_back(QString::fromStdString(e)); - } - - return array; - } - - QJsonArray LOOTWorker::createPlugins( - loot::GameInterface& game, - const std::vector& sortedPlugins) const - { - QJsonArray plugins; - - for (auto&& pluginName : sortedPlugins) { - - auto plugin = game.GetPlugin(pluginName); - - QJsonObject o; - o["name"] = QString::fromStdString(pluginName); - - if (auto metaData = game.GetDatabase().GetPluginMetadata(pluginName, true, true)) { - set(o, "incompatibilities", createIncompatibilities(game, metaData->GetIncompatibilities())); - set(o, "messages", createMessages(metaData->GetMessages())); - set(o, "dirty", createDirty(metaData->GetDirtyInfo())); - set(o, "clean", createClean(metaData->GetCleanInfo())); - } - - set(o, "missingMasters", createMissingMasters(game, pluginName)); - - if (plugin->LoadsArchive()) { - o["loadsArchive"] = true; - } - - if (plugin->IsMaster()) { - o["isMaster"] = true; - } - - if (plugin->IsLightPlugin()) { - o["isLightMaster"] = true; - } - - // don't add if the name is the only thing in there - if (o.size() > 1) { - plugins.push_back(o); - } - } - - return plugins; - } - - QJsonValue LOOTWorker::createMessages(const std::vector& list) const - { - QJsonArray messages; - - for (loot::Message m : list) { - auto simpleMessage = loot::SelectMessageContent(m.GetContent(), m_Language); - if (simpleMessage.has_value()) { - messages.push_back(QJsonObject{ - {"type", QString::fromStdString(toString(m.GetType()))}, - {"text", QString::fromStdString(simpleMessage.value().GetText())} - }); - } - } - - return messages; - } - - QJsonValue LOOTWorker::createDirty( - const std::vector& data) const - { - QJsonArray array; - - for (const auto& d : data) { - QJsonObject o{ - {"crc", static_cast(d.GetCRC())}, - {"itm", static_cast(d.GetITMCount())}, - {"deletedReferences", static_cast(d.GetDeletedReferenceCount())}, - {"deletedNavmesh", static_cast(d.GetDeletedNavmeshCount())}, - }; - - set(o, "cleaningUtility", QString::fromStdString(d.GetCleaningUtility())); - auto simpleMessage = loot::SelectMessageContent(loot::Message(loot::MessageType::say, d.GetDetail()).GetContent(), m_Language); - if (simpleMessage.has_value()) { - set(o, "info", QString::fromStdString(simpleMessage.value().GetText())); - } - else { - set(o, "info", QString::fromStdString("")); - } - - array.push_back(o); - } - - return array; - } - - QJsonValue LOOTWorker::createClean( - const std::vector& data) const - { - QJsonArray array; - - for (const auto& d : data) { - QJsonObject o{ - {"crc", static_cast(d.GetCRC())}, - }; - - set(o, "cleaningUtility", QString::fromStdString(d.GetCleaningUtility())); - auto simpleMessage = loot::SelectMessageContent(loot::Message(loot::MessageType::say, d.GetDetail()).GetContent(), m_Language); - if (simpleMessage.has_value()) { - set(o, "info", QString::fromStdString(simpleMessage.value().GetText())); - } - else { - set(o, "info", QString::fromStdString("")); - } - - array.push_back(o); - } - - return array; - } - - - QJsonValue LOOTWorker::createIncompatibilities( - loot::GameInterface& game, const std::vector& data) const - { - QJsonArray array; - - for (auto&& f : data) { - const auto n = static_cast(f.GetName()); - if (!game.GetPlugin(n)) { - continue; - } - - const auto name = QString::fromStdString(n); - const auto displayName = QString::fromStdString(f.GetDisplayName()); - - QJsonObject o{ - {"name", name} - }; - - if (displayName != name) { - set(o, "displayName", displayName); - } - - array.push_back(std::move(o)); - } - - return array; - } - - QJsonValue LOOTWorker::createMissingMasters( - loot::GameInterface& game, const std::string& pluginName) const - { - QJsonArray array; - - for (auto&& master : game.GetPlugin(pluginName)->GetMasters()) { - if (!game.GetPlugin(master)) { - array.push_back(QString::fromStdString(master)); - } - } - - return array; - } - - void LOOTWorker::progress(Progress p) - { - std::cout << "[progress] " << static_cast(p) << "\n"; - std::cout.flush(); - } - - std::string escapeNewlines(const std::string& s) - { - auto ss = boost::replace_all_copy(s, "\n", "\\n"); - boost::replace_all(ss, "\r", "\\r"); - return ss; - } - - void LOOTWorker::log(loot::LogLevel level, const std::string& message) const - { - if (level < m_LogLevel) { - return; - } - - const auto ll = fromLootLogLevel(level); - const auto levelName = logLevelToString(ll); - - std::cout << "[" << levelName << "] " << escapeNewlines(message) << "\n"; - std::cout.flush(); - } - - - loot::LogLevel toLootLogLevel(lootcli::LogLevels level) - { - using L = loot::LogLevel; - using LC = lootcli::LogLevels; - - switch (level) - { - case LC::Trace: return L::trace; - case LC::Debug: return L::debug; - case LC::Info: return L::info; - case LC::Warning: return L::warning; - case LC::Error: return L::error; - default: return L::info; - } - } - - lootcli::LogLevels fromLootLogLevel(loot::LogLevel level) - { - using L = loot::LogLevel; - using LC = lootcli::LogLevels; - - switch (level) - { - case L::trace: - return LC::Trace; - - case L::debug: - return LC::Debug; - - case L::info: - return LC::Info; - - case L::warning: - return LC::Warning; - - case L::error: // fall-through - case L::fatal: - return LC::Error; - - default: - return LC::Info; - } - } - -} // namespace +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) +{ + switch (type) { + case loot::MessageType::say: + return "info"; + case loot::MessageType::warn: + return "warn"; + case loot::MessageType::error: + return "error"; + default: + return "unknown"; + } +} + +LOOTWorker::LOOTWorker() + : m_GameId(loot::GameId::tes5), m_GameName("Skyrim"), + m_LogLevel(loot::LogLevel::info) +{} + +std::string ToLower(std::string text) +{ + std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + + return text; +} + +void LOOTWorker::setGame(const std::string& gameName) +{ + static std::map gameMap = { + {"morrowind", loot::GameId::tes3}, {"oblivion", loot::GameId::tes4}, + {"fallout3", loot::GameId::fo3}, {"fallout4", loot::GameId::fo4}, + {"fallout4vr", loot::GameId::fo4vr}, {"falloutnv", loot::GameId::fonv}, + {"skyrim", loot::GameId::tes5}, {"skyrimse", loot::GameId::tes5se}, + {"skyrimvr", loot::GameId::tes5vr}, + }; + + auto iter = gameMap.find(ToLower(gameName)); + + if (iter != gameMap.end()) { + m_GameName = gameName; + if (ToLower(gameName) == "skyrimse") { + m_GameName = "Skyrim Special Edition"; + } + m_GameId = iter->second; + } else { + throw std::runtime_error("invalid game name \"" + gameName + "\""); + } +} + +void LOOTWorker::setGamePath(const std::string& gamePath) +{ + m_GamePath = gamePath; +} + +void LOOTWorker::setOutput(const std::string& outputPath) +{ + m_OutputPath = outputPath; +} + +void LOOTWorker::setUpdateMasterlist(bool update) +{ + m_UpdateMasterlist = update; +} + +void LOOTWorker::setPluginListPath(const std::string& pluginListPath) +{ + m_PluginListPath = pluginListPath; +} + +void LOOTWorker::setLanguageCode(const std::string& languageCode) +{ + m_Language = languageCode; +} + +void LOOTWorker::setLogLevel(loot::LogLevel level) +{ + m_LogLevel = level; +} + +fs::path GetLOOTAppData() +{ + TCHAR path[MAX_PATH]; + + HRESULT res = ::SHGetFolderPath(nullptr, CSIDL_LOCAL_APPDATA, nullptr, + SHGFP_TYPE_CURRENT, path); + + if (res == S_OK) { + return fs::path(path) / "LOOT"; + } else { + return fs::path(""); + } +} + +fs::path LOOTWorker::gamePath() const +{ + return GetLOOTAppData() / "games" / m_GameSettings.FolderName(); +} + +fs::path LOOTWorker::masterlistPath() const +{ + return gamePath() / "masterlist.yaml"; +} + +fs::path LOOTWorker::userlistPath() const +{ + return gamePath() / "userlist.yaml"; +} +fs::path LOOTWorker::settingsPath() const +{ + return GetLOOTAppData() / "settings.toml"; +} + +fs::path LOOTWorker::l10nPath() const +{ + return GetLOOTAppData() / "resources" / "l10n"; +} + +fs::path LOOTWorker::dataPath() const +{ + return m_GameSettings.DataPath(); +} + +void LOOTWorker::getSettings(const fs::path& file) +{ + lock_guard guard(mutex_); + // Don't use cpptoml::parse_file() as it just uses a std stream, + // which don't support UTF-8 paths on Windows. + std::ifstream in(file); + if (!in.is_open()) + throw std::runtime_error(file.string() + " could not be opened for parsing"); + + const auto settings = toml::parse(in, file.string()); + const auto games = settings["games"]; + if (games.is_array_of_tables()) { + for (const auto& game : *games.as_array()) { + try { + if (!game.is_table()) { + throw std::runtime_error("games array element is not a table"); + } + auto gameTable = *game.as_table(); + + using loot::GameId; + using loot::GameSettings; + + auto id = gameTable["gameId"].value(); + if (!id) { + throw std::runtime_error( + "'gameId' and 'type' keys both missing from game settings table"); + } + const auto gameType = *id; + GameId gameId; + + if (gameType == "Morrowind") { + gameId = GameId::tes3; + } else if (gameType == "Oblivion") { + // The Oblivion game type is shared between Oblivon and Nehrim. + gameId = IsNehrim(gameTable) ? GameId::nehrim : GameId::tes4; + } else if (gameType == "Skyrim") { + // The Skyrim game type is shared between Skyrim and Enderal. + gameId = IsEnderal(gameTable) ? GameId::enderal : GameId::tes5; + } else if (gameType == "SkyrimSE" || gameType == "Skyrim Special Edition") { + // The Skyrim SE game type is shared between Skyrim SE and Enderal SE. + gameId = IsEnderalSE(gameTable) ? GameId::enderalse : GameId::tes5se; + } else if (gameType == "Skyrim VR") { + gameId = GameId::tes5vr; + } else if (gameType == "Fallout3") { + gameId = GameId::fo3; + } else if (gameType == "FalloutNV") { + gameId = GameId::fonv; + } else if (gameType == "Fallout4") { + gameId = GameId::fo4; + } else if (gameType == "Fallout4VR") { + gameId = GameId::fo4vr; + } else { + throw std::runtime_error( + "invalid value for 'type' key in game settings table"); + } + + auto folder = gameTable["folder"].value(); + if (!folder) { + throw std::runtime_error("'folder' key missing from game settings table"); + } + + const auto type = gameTable["type"].value(); + + // SkyrimSE was a previous serialised value for GameType::tes5se, + // and the game folder name LOOT created for that game type. + if (type && *type == "SkyrimSE" && *folder == *type) { + folder = "Skyrim Special Edition"; + } + + GameSettings newSettings(gameId, folder.value()); + + if (newSettings.Type() == m_GameSettings.Type()) { + + auto name = gameTable["name"].value(); + if (name) { + newSettings.SetName(*name); + } + + auto master = gameTable["master"].value(); + if (master) { + newSettings.SetMaster(*master); + } + + const auto minimumHeaderVersion = + gameTable["minimumHeaderVersion"].value(); + if (minimumHeaderVersion) { + newSettings.SetMinimumHeaderVersion((float)*minimumHeaderVersion); + } + + auto source = gameTable["masterlistSource"].value(); + if (source) { + newSettings.SetMasterlistSource(migrateMasterlistSource(*source)); + } else { + auto url = gameTable["repo"].value(); + auto branch = gameTable["branch"].value(); + auto migratedSource = + migrateMasterlistRepoSettings(newSettings.Id(), *url, *branch); + if (migratedSource.has_value()) { + newSettings.SetMasterlistSource(migratedSource.value()); + } + } + + auto path = gameTable["path"].value(); + if (path) { + newSettings.SetGamePath(std::filesystem::u8path(*path)); + } + + auto localPath = gameTable["local_path"].value(); + auto localFolder = gameTable["local_folder"].value(); + if (localPath && localFolder) { + throw std::runtime_error( + "Game settings have local_path and local_folder set, use only one."); + } else if (localPath) { + newSettings.SetGameLocalPath(std::filesystem::u8path(*localPath)); + } else if (localFolder) { + newSettings.SetGameLocalFolder(*localFolder); + } + + m_GameSettings = newSettings; + break; + } + } catch (...) { + // Skip invalid games. + } + } + } + + if (m_Language.empty()) { + m_Language = settings["language"].value_or(loot::MessageContent::DEFAULT_LANGUAGE); + } +} + +std::optional LOOTWorker::GetLocalFolder(const toml::table& table) +{ + const auto localPath = table["local_path"].value(); + const auto localFolder = table["local_folder"].value(); + + if (localFolder.has_value()) { + return localFolder; + } + + if (localPath.has_value()) { + return std::filesystem::u8path(*localPath).filename().string(); + } + + return std::nullopt; +} + +bool LOOTWorker::IsNehrim(const toml::table& table) +{ + const auto installPath = table["path"].value(); + + if (installPath.has_value() && !installPath.value().empty()) { + const auto path = std::filesystem::u8path(installPath.value()); + if (std::filesystem::exists(path)) { + return std::filesystem::exists(path / "NehrimLauncher.exe"); + } + } + + // Fall back to using heuristics based on the existing settings. + // Return true if any of these heuristics return a positive match. + const auto gameName = table["name"].value(); + const auto masterFilename = table["master"].value(); + const auto isBaseGameInstance = table["isBaseGameInstance"].value(); + const auto folder = table["folder"].value(); + + return + // Nehrim uses a different main master file from Oblivion. + (masterFilename.has_value() && + masterFilename.value() == loot::GetMasterFilename(loot::GameId::nehrim)) || + // Game name probably includes "nehrim". + (gameName.has_value() && boost::icontains(gameName.value(), "nehrim")) || + // LOOT folder name probably includes "nehrim". + (folder.has_value() && boost::icontains(folder.value(), "nehrim")) || + // Between 0.18.1 and 0.19.0 inclusive, LOOT had an isBaseGameInstance + // game setting that was false for Nehrim, Enderal and Enderal SE. + (isBaseGameInstance.has_value() && !isBaseGameInstance.value()); +} + +bool LOOTWorker::IsEnderal(const toml::table& table, + const std::string& expectedLocalFolder) +{ + const auto installPath = table["path"].value(); + + if (installPath.has_value() && !installPath.value().empty()) { + const auto path = std::filesystem::u8path(installPath.value()); + if (std::filesystem::exists(path)) { + return std::filesystem::exists(path / "Enderal Launcher.exe"); + } + } + + // Fall back to using heuristics based on the existing settings. + // Return true if any of these heuristics return a positive match. + const auto gameName = table["name"].value(); + const auto isBaseGameInstance = table["isBaseGameInstance"].value(); + const auto localFolder = GetLocalFolder(table); + const auto folder = table["folder"].value(); + + return + // Enderal and Enderal SE use different local folders than their base + // games. + (localFolder.has_value() && localFolder.value() == expectedLocalFolder) || + // Game name probably includes "enderal". + (gameName.has_value() && boost::icontains(gameName.value(), "enderal")) || + // LOOT folder name probably includes "enderal". + (folder.has_value() && boost::icontains(folder.value(), "enderal")) || + // Between 0.18.1 and 0.19.0 inclusive, LOOT had an isBaseGameInstance + // game setting that was false for Nehrim, Enderal and Enderal SE. + (isBaseGameInstance.has_value() && !isBaseGameInstance.value()); +} + +bool LOOTWorker::IsEnderal(const toml::table& table) +{ + return IsEnderal(table, "enderal"); +} + +bool LOOTWorker::IsEnderalSE(const toml::table& table) +{ + return IsEnderal(table, "Enderal Special Edition"); +} + +std::string LOOTWorker::getOldDefaultRepoUrl(loot::GameId GameId) +{ + switch (GameId) { + case loot::GameId::tes3: + return "https://github.com/loot/morrowind.git"; + case loot::GameId::tes4: + return "https://github.com/loot/oblivion.git"; + case loot::GameId::tes5: + return "https://github.com/loot/skyrim.git"; + case loot::GameId::tes5se: + return "https://github.com/loot/skyrimse.git"; + case loot::GameId::tes5vr: + return "https://github.com/loot/skyrimvr.git"; + case loot::GameId::fo3: + return "https://github.com/loot/fallout3.git"; + case loot::GameId::fonv: + return "https://github.com/loot/falloutnv.git"; + case loot::GameId::fo4: + return "https://github.com/loot/fallout4.git"; + case loot::GameId::fo4vr: + return "https://github.com/loot/fallout4vr.git"; + default: + throw std::runtime_error( + "Unrecognised game type: " + + std::to_string(static_cast>(GameId))); + } +} + +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::GameId GameId, 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 (GameId == loot::GameId::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 (GameId == loot::GameId::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::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, "\"", "\\\""); +} + +int LOOTWorker::run() +{ + m_startTime = std::chrono::high_resolution_clock::now(); + + { + // Do some preliminary locale / UTF-8 support setup here, in case the settings file + // reading requires it. + // Boost.Locale initialisation: Specify location of language dictionaries. + boost::locale::generator gen; + gen.add_messages_path(l10nPath().string()); + gen.add_messages_domain("loot"); + + // Boost.Locale initialisation: Generate and imbue locales. + std::locale::global(gen("en.UTF-8")); + } + + loot::SetLoggingCallback([&](loot::LogLevel level, const char* message) { + log(level, message); + }); + + try { + fs::path profile(m_PluginListPath); + profile = profile.parent_path(); + + m_GameSettings = loot::GameSettings(m_GameId, m_GamePath); + + std::unique_ptr gameHandle = + CreateGameHandle(m_GameSettings.Type(), m_GamePath, profile.string()); + + 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.Id() == loot::GameId::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)) + getSettings(settings); + + m_GameSettings.SetGamePath(m_GamePath); + + if (m_Language != loot::MessageContent::DEFAULT_LANGUAGE) { + log(loot::LogLevel::debug, "initialising language settings"); + log(loot::LogLevel::debug, "selected language: " + m_Language); + + // Boost.Locale initialisation: Generate and imbue locales. + boost::locale::generator gen; + std::locale::global(gen(m_Language + ".UTF-8")); + } + + 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()); + + 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; + + log(loot::LogLevel::error, + "Error downloading masterlist: " + converter.to_bytes(errorMessage)); + return FALSE; + } + } + + progress(Progress::LoadingLists); + + fs::path userlist = userlistPath(); + gameHandle->GetDatabase().LoadLists(masterlistPath().string(), + fs::exists(userlist) ? userlistPath().string() + : fs::path()); + + progress(Progress::ReadingPlugins); + gameHandle->LoadCurrentLoadOrderState(); + std::vector pluginsList; + for (auto plugin : gameHandle->GetLoadOrder()) { + std::filesystem::path pluginPath(plugin); + pluginsList.push_back(pluginPath); + } + + progress(Progress::SortingPlugins); + std::vector sortedPlugins = gameHandle->SortPlugins(pluginsList); + + progress(Progress::WritingLoadorder); + + std::ofstream outf(m_PluginListPath); + if (!outf) { + log(loot::LogLevel::error, + "failed to open " + m_PluginListPath + " to rewrite it"); + return 1; + } + outf << "# This file was automatically generated by Mod Organizer." << std::endl; + for (const std::string& plugin : sortedPlugins) { + outf << plugin << std::endl; + } + outf.close(); + + progress(Progress::ParsingLootMessages); + std::ofstream(m_OutputPath) << createJsonReport(*gameHandle, sortedPlugins); + } catch (std::system_error& e) { + log(loot::LogLevel::error, e.what()); + return 1; + } catch (const std::exception& e) { + log(loot::LogLevel::error, e.what()); + return 1; + } + + progress(Progress::Done); + + return 0; +} + +void set(QJsonObject& o, const char* e, const QJsonValue& v) +{ + if (v.isObject() && v.toObject().isEmpty()) { + return; + } + + if (v.isArray() && v.toArray().isEmpty()) { + return; + } + + if (v.isString() && v.toString().isEmpty()) { + return; + } + + o[e] = v; +} + +std::string +LOOTWorker::createJsonReport(loot::GameInterface& game, + const std::vector& sortedPlugins) const +{ + QJsonObject root; + + set(root, "messages", createMessages(game.GetDatabase().GetGeneralMessages(true))); + set(root, "plugins", createPlugins(game, sortedPlugins)); + + const auto end = std::chrono::high_resolution_clock::now(); + + set(root, "stats", + QJsonObject{{"time", std::chrono::duration_cast( + end - m_startTime) + .count()}, + {"lootcliVersion", LOOTCLI_VERSION_STRING}, + {"lootVersion", QString::fromStdString(loot::GetLiblootVersion())}}); + + QJsonDocument doc(root); + return doc.toJson(QJsonDocument::Indented).toStdString(); +} + +template +QJsonArray createStringArray(const Container& c) +{ + QJsonArray array; + + for (auto&& e : c) { + array.push_back(QString::fromStdString(e)); + } + + return array; +} + +QJsonArray +LOOTWorker::createPlugins(loot::GameInterface& game, + const std::vector& sortedPlugins) const +{ + QJsonArray plugins; + + for (auto&& pluginName : sortedPlugins) { + + auto plugin = game.GetPlugin(pluginName); + + QJsonObject o; + o["name"] = QString::fromStdString(pluginName); + + if (auto metaData = game.GetDatabase().GetPluginMetadata(pluginName, true, true)) { + set(o, "incompatibilities", + createIncompatibilities(game, metaData->GetIncompatibilities())); + set(o, "messages", createMessages(metaData->GetMessages())); + set(o, "dirty", createDirty(metaData->GetDirtyInfo())); + set(o, "clean", createClean(metaData->GetCleanInfo())); + } + + set(o, "missingMasters", createMissingMasters(game, pluginName)); + + if (plugin->LoadsArchive()) { + o["loadsArchive"] = true; + } + + if (plugin->IsMaster()) { + o["isMaster"] = true; + } + + if (plugin->IsLightPlugin()) { + o["isLightMaster"] = true; + } + + // don't add if the name is the only thing in there + if (o.size() > 1) { + plugins.push_back(o); + } + } + + return plugins; +} + +QJsonValue LOOTWorker::createMessages(const std::vector& list) const +{ + QJsonArray messages; + + for (loot::Message m : list) { + auto simpleMessage = loot::SelectMessageContent(m.GetContent(), m_Language); + if (simpleMessage.has_value()) { + messages.push_back(QJsonObject{ + {"type", QString::fromStdString(toString(m.GetType()))}, + {"text", QString::fromStdString(simpleMessage.value().GetText())}}); + } + } + + return messages; +} + +QJsonValue +LOOTWorker::createDirty(const std::vector& data) const +{ + QJsonArray array; + + for (const auto& d : data) { + QJsonObject o{ + {"crc", static_cast(d.GetCRC())}, + {"itm", static_cast(d.GetITMCount())}, + {"deletedReferences", static_cast(d.GetDeletedReferenceCount())}, + {"deletedNavmesh", static_cast(d.GetDeletedNavmeshCount())}, + }; + + set(o, "cleaningUtility", QString::fromStdString(d.GetCleaningUtility())); + auto simpleMessage = loot::SelectMessageContent( + loot::Message(loot::MessageType::say, d.GetDetail()).GetContent(), m_Language); + if (simpleMessage.has_value()) { + set(o, "info", QString::fromStdString(simpleMessage.value().GetText())); + } else { + set(o, "info", QString::fromStdString("")); + } + + array.push_back(o); + } + + return array; +} + +QJsonValue +LOOTWorker::createClean(const std::vector& data) const +{ + QJsonArray array; + + for (const auto& d : data) { + QJsonObject o{ + {"crc", static_cast(d.GetCRC())}, + }; + + set(o, "cleaningUtility", QString::fromStdString(d.GetCleaningUtility())); + auto simpleMessage = loot::SelectMessageContent( + loot::Message(loot::MessageType::say, d.GetDetail()).GetContent(), m_Language); + if (simpleMessage.has_value()) { + set(o, "info", QString::fromStdString(simpleMessage.value().GetText())); + } else { + set(o, "info", QString::fromStdString("")); + } + + array.push_back(o); + } + + return array; +} + +QJsonValue +LOOTWorker::createIncompatibilities(loot::GameInterface& game, + const std::vector& data) const +{ + QJsonArray array; + + for (auto&& f : data) { + const auto n = static_cast(f.GetName()); + if (!game.GetPlugin(n)) { + continue; + } + + const auto name = QString::fromStdString(n); + const auto displayName = QString::fromStdString(f.GetDisplayName()); + + QJsonObject o{{"name", name}}; + + if (displayName != name) { + set(o, "displayName", displayName); + } + + array.push_back(std::move(o)); + } + + return array; +} + +QJsonValue LOOTWorker::createMissingMasters(loot::GameInterface& game, + const std::string& pluginName) const +{ + QJsonArray array; + + for (auto&& master : game.GetPlugin(pluginName)->GetMasters()) { + if (!game.GetPlugin(master)) { + array.push_back(QString::fromStdString(master)); + } + } + + return array; +} + +void LOOTWorker::progress(Progress p) +{ + std::cout << "[progress] " << static_cast(p) << "\n"; + std::cout.flush(); +} + +std::string escapeNewlines(const std::string& s) +{ + auto ss = boost::replace_all_copy(s, "\n", "\\n"); + boost::replace_all(ss, "\r", "\\r"); + return ss; +} + +void LOOTWorker::log(loot::LogLevel level, const std::string& message) const +{ + if (level < m_LogLevel) { + return; + } + + const auto ll = fromLootLogLevel(level); + const auto levelName = logLevelToString(ll); + + std::cout << "[" << levelName << "] " << escapeNewlines(message) << "\n"; + std::cout.flush(); +} + +loot::LogLevel toLootLogLevel(lootcli::LogLevels level) +{ + using L = loot::LogLevel; + using LC = lootcli::LogLevels; + + switch (level) { + case LC::Trace: + return L::trace; + case LC::Debug: + return L::debug; + case LC::Info: + return L::info; + case LC::Warning: + return L::warning; + case LC::Error: + return L::error; + default: + return L::info; + } +} + +lootcli::LogLevels fromLootLogLevel(loot::LogLevel level) +{ + using L = loot::LogLevel; + using LC = lootcli::LogLevels; + + switch (level) { + case L::trace: + return LC::Trace; + + case L::debug: + return LC::Debug; + + case L::info: + return LC::Info; + + case L::warning: + return LC::Warning; + + case L::error: // fall-through + case L::fatal: + return LC::Error; + + default: + return LC::Info; + } +} + +} // namespace lootcli diff --git a/src/lootthread.h b/src/lootthread.h index e0968c1..0bf1e68 100644 --- a/src/lootthread.h +++ b/src/lootthread.h @@ -1,100 +1,102 @@ #ifndef LOOTTHREAD_H #define LOOTTHREAD_H -#include #include "game_settings.h" +#include -namespace loot { - class Game; +namespace loot +{ +class Game; } +namespace lootcli +{ -namespace lootcli { +loot::LogLevel toLootLogLevel(lootcli::LogLevels level); +lootcli::LogLevels fromLootLogLevel(loot::LogLevel level); - loot::LogLevel toLootLogLevel(lootcli::LogLevels level); - lootcli::LogLevels fromLootLogLevel(loot::LogLevel level); +class LOOTWorker +{ +public: + explicit LOOTWorker(); - class LOOTWorker - { - public: - explicit LOOTWorker(); + void setGame(const std::string& gameName); + void setGamePath(const std::string& gamePath); + void setOutput(const std::string& outputPath); + void setPluginListPath(const std::string& pluginListPath); + void + setLanguageCode(const std::string& language_code); // Will add this when I figure out + // how languages work on MO + void setLogLevel(loot::LogLevel level); - void setGame(const std::string& gameName); - void setGamePath(const std::string& gamePath); - void setOutput(const std::string& outputPath); - void setPluginListPath(const std::string& pluginListPath); - void setLanguageCode(const std::string& language_code); //Will add this when I figure out how languages work on MO - void setLogLevel(loot::LogLevel level); + void setUpdateMasterlist(bool update); - void setUpdateMasterlist(bool update); + int run(); - int run(); +private: + void progress(Progress p); + void log(loot::LogLevel level, const std::string& message) const; - 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::GameId gameType); + std::optional GetLocalFolder(const toml::table& table); + bool IsNehrim(const toml::table& table); + bool IsEnderal(const toml::table& table, const std::string& expectedLocalFolder); + bool IsEnderal(const toml::table& table); + bool IsEnderalSE(const toml::table& table); + 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::GameId gameType, + std::string url, + std::string branch); + std::string migrateMasterlistSource(const std::string& source); - DWORD GetFile(const WCHAR* szUrl, const CHAR* szFileName); - void getSettings(const std::filesystem::path& file); - std::string getOldDefaultRepoUrl(loot::GameId gameType); - std::optional GetLocalFolder(const toml::table& table); - bool IsNehrim(const toml::table& table); - bool IsEnderal(const toml::table& table, const std::string& expectedLocalFolder); - bool IsEnderal(const toml::table& table); - bool IsEnderalSE(const toml::table& table); - 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::GameId gameType, std::string url, std::string 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; + std::filesystem::path l10nPath() const; + std::filesystem::path dataPath() const; - std::filesystem::path gamePath() const; - std::filesystem::path masterlistPath() const; - std::filesystem::path settingsPath() const; - std::filesystem::path userlistPath() const; - std::filesystem::path l10nPath() const; - std::filesystem::path dataPath() const; +private: + // void handleErr(unsigned int resultCode, const char *description); + bool sort(loot::Game& game); + // const char *lootErrorString(unsigned int errorCode); + // template T resolveVariable(HMODULE lib, const char *name); + // template T resolveFunction(HMODULE lib, const char *name); - private: +private: + loot::GameId m_GameId; + std::string m_Language; + std::string m_GameName; + std::string m_GamePath; + std::string m_OutputPath; + std::string m_PluginListPath; + loot::LogLevel m_LogLevel; + bool m_UpdateMasterlist; + mutable std::recursive_mutex mutex_; + loot::GameSettings m_GameSettings; + std::chrono::high_resolution_clock::time_point m_startTime; - // void handleErr(unsigned int resultCode, const char *description); - bool sort(loot::Game& game); - //const char *lootErrorString(unsigned int errorCode); - //template T resolveVariable(HMODULE lib, const char *name); - //template T resolveFunction(HMODULE lib, const char *name); + std::string createJsonReport(loot::GameInterface& game, + const std::vector& sortedPlugins) const; - private: - loot::GameId m_GameId; - std::string m_Language; - std::string m_GameName; - std::string m_GamePath; - std::string m_OutputPath; - std::string m_PluginListPath; - loot::LogLevel m_LogLevel; - bool m_UpdateMasterlist; - mutable std::recursive_mutex mutex_; - loot::GameSettings m_GameSettings; - std::chrono::high_resolution_clock::time_point m_startTime; + QJsonArray createPlugins(loot::GameInterface& game, + const std::vector& sortedPlugins) const; - std::string createJsonReport( - loot::GameInterface& game, - const std::vector& sortedPlugins) const; + QJsonValue createMessages(const std::vector& list) const; + QJsonValue createDirty(const std::vector& data) const; + QJsonValue createClean(const std::vector& data) const; - QJsonArray createPlugins( - loot::GameInterface& game, - const std::vector& sortedPlugins) const; + QJsonValue createIncompatibilities(loot::GameInterface& game, + const std::vector& data) const; - QJsonValue createMessages(const std::vector& list) const; - QJsonValue createDirty(const std::vector& data) const; - QJsonValue createClean(const std::vector& data) const; + QJsonValue createMissingMasters(loot::GameInterface& game, + const std::string& pluginName) const; +}; - QJsonValue createIncompatibilities( - loot::GameInterface& game, const std::vector& data) const; +} // namespace lootcli - QJsonValue createMissingMasters( - loot::GameInterface& game, const std::string& pluginName) const; - }; - -} // namespace - -#endif // LOOTTHREAD_H +#endif // LOOTTHREAD_H diff --git a/src/main.cpp b/src/main.cpp index bf33707..0ded3e2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,48 +3,43 @@ using namespace std; - template T getParameter(const std::vector& arguments, const std::string& key) { - auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); - if ((iter != arguments.end()) - && ((iter + 1) != arguments.end())) { - return boost::lexical_cast(*(iter + 1)); - } - else { - throw std::runtime_error(std::string("argument missing " + key)); - } + auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); + if ((iter != arguments.end()) && ((iter + 1) != arguments.end())) { + return boost::lexical_cast(*(iter + 1)); + } else { + throw std::runtime_error(std::string("argument missing " + key)); + } } template <> -bool getParameter(const std::vector& arguments, const std::string& key) +bool getParameter(const std::vector& arguments, + const std::string& key) { - auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); - if (iter != arguments.end()) { - return true; - } - else { - return false; - } + auto iter = std::find(arguments.begin(), arguments.end(), std::string("--") + key); + if (iter != arguments.end()) { + return true; + } else { + return false; + } } template -T getOptionalParameter(const std::vector &arguments, const std::string &key, T def) +T getOptionalParameter(const std::vector& arguments, + const std::string& key, T def) { - try - { + try { return getParameter(arguments, key); - } - catch(std::runtime_error&) - { + } catch (std::runtime_error&) { return def; } } loot::LogLevel getLogLevel(const std::vector& arguments) { - const auto s = getOptionalParameter(arguments, "logLevel", ""); + const auto s = getOptionalParameter(arguments, "logLevel", ""); const auto level = lootcli::logLevelFromString(s); return lootcli::toLootLogLevel(level); @@ -57,23 +52,21 @@ int wWinMain(HINSTANCE, HINSTANCE, LPTSTR, int) std::vector arguments; int argc; - LPWSTR *argv = CommandLineToArgvW(GetCommandLineW(), &argc); + LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); - if (argv) - { - for (int i = 0; i < argc; ++i) - { - size_t num_converted; - std::vector arg(wcslen(argv[i]) * sizeof(wchar_t) + 1); + if (argv) { + for (int i = 0; i < argc; ++i) { + size_t num_converted; + std::vector arg(wcslen(argv[i]) * sizeof(wchar_t) + 1); - wcstombs_s(&num_converted, &(arg[0]), arg.size(), argv[i], arg.size() - 1); + wcstombs_s(&num_converted, &(arg[0]), arg.size(), argv[i], arg.size() - 1); - arguments.push_back(&(arg[0])); - } + arguments.push_back(&(arg[0])); } + } - // design rationale: this was designed to have the actual loot stuff run in a separate thread. That turned - // out to be unnecessary atm. + // design rationale: this was designed to have the actual loot stuff run in a separate + // thread. That turned out to be unnecessary atm. try { lootcli::LOOTWorker worker; @@ -91,7 +84,7 @@ int wWinMain(HINSTANCE, HINSTANCE, LPTSTR, int) } return worker.run(); - } catch (const std::exception &e) { + } catch (const std::exception& e) { std::cerr << "Error: " << e.what(); return 1; } diff --git a/src/pch.h b/src/pch.h index 7963f29..75cd3d1 100644 --- a/src/pch.h +++ b/src/pch.h @@ -1,25 +1,25 @@ -#pragma warning(disable: 4251) // neds to have dll-interface -#pragma warning(disable: 4355) // this used in initializer list -#pragma warning(disable: 4371) // layout may have changed -#pragma warning(disable: 4514) // unreferenced inline function removed -#pragma warning(disable: 4571) // catch semantics changed -#pragma warning(disable: 4619) // no warning X -#pragma warning(disable: 4623) // default constructor deleted -#pragma warning(disable: 4625) // copy constructor deleted -#pragma warning(disable: 4626) // copy assignment operator deleted -#pragma warning(disable: 4710) // function not inlined -#pragma warning(disable: 4820) // padding -#pragma warning(disable: 4866) // left-to-right evaluation order -#pragma warning(disable: 4868) // left-to-right evaluation order -#pragma warning(disable: 5026) // move constructor deleted -#pragma warning(disable: 5027) // move assignment operator deleted -#pragma warning(disable: 5045) // spectre mitigation +#pragma warning(disable : 4251) // neds to have dll-interface +#pragma warning(disable : 4355) // this used in initializer list +#pragma warning(disable : 4371) // layout may have changed +#pragma warning(disable : 4514) // unreferenced inline function removed +#pragma warning(disable : 4571) // catch semantics changed +#pragma warning(disable : 4619) // no warning X +#pragma warning(disable : 4623) // default constructor deleted +#pragma warning(disable : 4625) // copy constructor deleted +#pragma warning(disable : 4626) // copy assignment operator deleted +#pragma warning(disable : 4710) // function not inlined +#pragma warning(disable : 4820) // padding +#pragma warning(disable : 4866) // left-to-right evaluation order +#pragma warning(disable : 4868) // left-to-right evaluation order +#pragma warning(disable : 5026) // move constructor deleted +#pragma warning(disable : 5027) // move assignment operator deleted +#pragma warning(disable : 5045) // spectre mitigation #pragma warning(push, 3) -#pragma warning(disable: 4365) // signed/unsigned mismatch -#pragma warning(disable: 4774) // bad format string -#pragma warning(disable: 4946) // reinterpret_cast used between related classes -#pragma warning(disable: 4800) // implicit conversion +#pragma warning(disable : 4365) // signed/unsigned mismatch +#pragma warning(disable : 4774) // bad format string +#pragma warning(disable : 4946) // reinterpret_cast used between related classes +#pragma warning(disable : 4800) // implicit conversion // std #include @@ -42,23 +42,23 @@ #include // qt +#include +#include #include #include #include -#include -#include // boost #include -//#include -//#include +// #include +// #include #include #include -//#include -//#include -//#include -//#include -//#include +// #include +// #include +// #include +// #include +// #include // loot #include @@ -69,8 +69,8 @@ // windows #define WIN32_LEAN_AND_MEAN -#include #include +#include #include #include #include diff --git a/src/version.h b/src/version.h index 38e4a60..4787d74 100644 --- a/src/version.h +++ b/src/version.h @@ -1,4 +1,4 @@ #define LOOTCLI_VERSION_MAJOR 1 -#define LOOTCLI_VERSION_MINOR 4 +#define LOOTCLI_VERSION_MINOR 5 #define LOOTCLI_VERSION_MAINTENANCE 0 -#define LOOTCLI_VERSION_STRING "1.4.0" +#define LOOTCLI_VERSION_STRING "1.5.0"