diff --git a/src/API/APIController.cpp b/src/API/APIController.cpp index fcd2287..8b773ad 100644 --- a/src/API/APIController.cpp +++ b/src/API/APIController.cpp @@ -182,7 +182,7 @@ namespace API /* Do some rate limiting */ while (QueuedRequests.size() > 0) { - //DoHttpReq(QueuedRequests.front()); + //HttpGet(QueuedRequests.front()); /* Callback ? */ QueuedRequests.erase(QueuedRequests.begin()); } diff --git a/src/API/APIRequest.h b/src/API/APIRequest.h index 0077cec..705a6dc 100644 --- a/src/API/APIRequest.h +++ b/src/API/APIRequest.h @@ -9,12 +9,20 @@ #include "nlohmann/json.hpp" using json = nlohmann::json; +enum class ERequestType +{ + None, + Get, + Post +}; + struct APIRequest { - bool* IsComplete; - std::condition_variable* CV; - int Attempts; - std::string Query; + ERequestType Type; + bool* IsComplete; + std::condition_variable* CV; + int Attempts; + std::string Query; }; #endif diff --git a/src/API/CAPIClient.cpp b/src/API/CAPIClient.cpp index 29abbe2..70201a4 100644 --- a/src/API/CAPIClient.cpp +++ b/src/API/CAPIClient.cpp @@ -83,6 +83,7 @@ json CAPIClient::Get(std::string aEndpoint, std::string aParameters) // if not cached, push it into requests queue, so it can be done async APIRequest req{ + ERequestType::Get, &done, &cv, 0, @@ -108,6 +109,60 @@ json CAPIClient::Get(std::string aEndpoint, std::string aParameters) return cachedResponse != nullptr ? cachedResponse->Content : json{}; } +json CAPIClient::Post(std::string aEndpoint, std::string aParameters) +{ + std::string query = GetQuery(aEndpoint, aParameters); + + CachedResponse* cachedResponse = GetCachedResponse(query); + + if (cachedResponse != nullptr) + { + long long diff = Timestamp() - cachedResponse->Timestamp; + + if (diff < CacheLifetime && cachedResponse->Content != nullptr) + { + //LogDebug(("CAPIClient::" + BaseURL).c_str(), "Cached message %d seconds old. Reading from cache.", diff); + return cachedResponse->Content; + } + else + { + //LogDebug(("CAPIClient::" + BaseURL).c_str(), "Cached message %d seconds old. CacheLifetime %d. Queueing request.", diff, CacheLifetime); + } + } + + // Variables for synchronization + std::mutex mtx; + bool done = false; + std::condition_variable cv; + + // if not cached, push it into requests queue, so it can be done async + APIRequest req{ + ERequestType::Post, + &done, + &cv, + 0, + query + }; + + // Trigger the worker thread + { + const std::lock_guard lock(Mutex); + QueuedRequests.push_back(req); + IsSuspended = false; + ConVar.notify_all(); + } + + // Wait for the response + { + std::unique_lock lock(mtx); + cv.wait(lock, [&] { return done; }); + } + + cachedResponse = nullptr; // sanity + cachedResponse = GetCachedResponse(query); + + return cachedResponse != nullptr ? cachedResponse->Content : json{}; +} void CAPIClient::Download(std::filesystem::path aOutPath, std::string aEndpoint, std::string aParameters) { std::string query = GetQuery(aEndpoint, aParameters); @@ -251,8 +306,8 @@ void CAPIClient::ProcessRequests() APIRequest request = QueuedRequests.front(); - // DoHttpReq should set last request timestamp - APIResponse response = DoHttpReq(request); + // HttpGet should set last request timestamp + APIResponse response = HttpGet(request); // does the bucket get reduced on unsuccessful requests? we assume it does Bucket--; @@ -344,14 +399,23 @@ void CAPIClient::ProcessRequests() IsSuspended = true; } } -APIResponse CAPIClient::DoHttpReq(APIRequest aRequest) +APIResponse CAPIClient::HttpGet(APIRequest aRequest) { APIResponse response{ 0, nullptr }; - auto result = Client->Get(aRequest.Query); + httplib::Result result{}; + switch (aRequest.Type) + { + case ERequestType::Get: + result = Client->Get(aRequest.Query); + break; + case ERequestType::Post: + result = Client->Post(aRequest.Query); + break; + } if (!result) { diff --git a/src/API/CAPIClient.h b/src/API/CAPIClient.h index 3d5d6a4..2c61fae 100644 --- a/src/API/CAPIClient.h +++ b/src/API/CAPIClient.h @@ -38,6 +38,8 @@ class CAPIClient Returns the response string. */ json Get(std::string aEndpoint, std::string aParameters = ""); + + json Post(std::string aEndpoint, std::string aParameters = ""); /* Download: Downloads the remote resource to disk. @@ -87,7 +89,7 @@ class CAPIClient long long FileTimeOffset; void ProcessRequests(); - APIResponse DoHttpReq(APIRequest aRequest); + APIResponse HttpGet(APIRequest aRequest); }; #endif diff --git a/src/Consts.cpp b/src/Consts.cpp index 1739cf3..e9fdb70 100644 --- a/src/Consts.cpp +++ b/src/Consts.cpp @@ -34,6 +34,7 @@ const char* ICON_NEXUS_HOVER = "ICON_NEXUS_HOVER"; const char* ICON_GENERIC = "ICON_GENERIC"; const char* ICON_GENERIC_HOVER = "ICON_GENERIC_HOVER"; const char* ICON_NOTIFICATION = "ICON_NOTIFICATION"; +const char* ICON_WARNING = "ICON_WARNING"; const char* ICON_ADDONS = "ICON_ADDONS"; const char* ICON_OPTIONS = "ICON_OPTIONS"; const char* ICON_OPTIONS_HOVER = "ICON_OPTIONS_HOVER"; diff --git a/src/Consts.h b/src/Consts.h index 5368f7a..95b3ac7 100644 --- a/src/Consts.h +++ b/src/Consts.h @@ -35,6 +35,7 @@ extern const char* ICON_NEXUS_HOVER; extern const char* ICON_GENERIC; extern const char* ICON_GENERIC_HOVER; extern const char* ICON_NOTIFICATION; +extern const char* ICON_WARNING; extern const char* ICON_ADDONS; extern const char* ICON_OPTIONS; extern const char* ICON_OPTIONS_HOVER; diff --git a/src/DataLink/DataLink.cpp b/src/DataLink/DataLink.cpp index 195f87d..1e90161 100644 --- a/src/DataLink/DataLink.cpp +++ b/src/DataLink/DataLink.cpp @@ -16,7 +16,7 @@ namespace DataLink { void* ADDONAPI_ShareResource(const char* aIdentifier, size_t aResourceSize) { - ShareResource(aIdentifier, aResourceSize); + return ShareResource(aIdentifier, aResourceSize); } } diff --git a/src/Events/EventHandler.cpp b/src/Events/EventHandler.cpp index d60bf01..32148fe 100644 --- a/src/Events/EventHandler.cpp +++ b/src/Events/EventHandler.cpp @@ -106,7 +106,7 @@ namespace Events EventSubscriber sub{}; sub.Callback = aConsumeEventCallback; - for (auto& [path, addon] : Loader::Addons) + for (auto addon : Loader::Addons) { if (addon->Module == nullptr || addon->ModuleSize == 0 || diff --git a/src/GUI/Widgets/Addons/AddonItem.cpp b/src/GUI/Widgets/Addons/AddonItem.cpp index 40da19d..e083f2c 100644 --- a/src/GUI/Widgets/Addons/AddonItem.cpp +++ b/src/GUI/Widgets/Addons/AddonItem.cpp @@ -9,6 +9,7 @@ #include "Loader/AddonDefinition.h" #include "Loader/EAddonFlags.h" #include "Loader/Loader.h" +#include "Loader/Library.h" #include "Textures/TextureLoader.h" #include "Textures/Texture.h" @@ -40,7 +41,7 @@ namespace GUI aAddon->Definitions == nullptr || aAddon->State == EAddonState::NotLoadedDuplicate || aAddon->State == EAddonState::NotLoadedIncompatible || - aAddon->WillBeUninstalled) + aAddon->IsFlaggedForUninstall) { return; } @@ -120,22 +121,18 @@ namespace GUI { if (!aAddon->IsCheckingForUpdates) { - for (auto& it : Loader::Addons) + for (auto addon : Loader::Addons) { - if (it.second->Definitions == aAddon->Definitions) + if (addon->Definitions == aAddon->Definitions) { aAddon->IsCheckingForUpdates = true; - std::filesystem::path tmpPath = it.first.string(); - signed int tmpSig = aAddon->Definitions->Signature; - std::string tmpName = aAddon->Definitions->Name; - AddonVersion tmpVers = aAddon->Definitions->Version; - EUpdateProvider tmpProv = aAddon->Definitions->Provider; - std::string tmpLink = aAddon->Definitions->UpdateLink != nullptr ? aAddon->Definitions->UpdateLink : ""; - - std::thread([aAddon, tmpPath, tmpSig, tmpName, tmpVers, tmpProv, tmpLink]() + std::filesystem::path tmpPath = addon->Path.string(); + std::thread([aAddon, tmpPath]() { - if (Loader::UpdateAddon(tmpPath, tmpSig, tmpName, tmpVers, tmpProv, tmpLink)) + if (Loader::UpdateAddon(tmpPath, aAddon->Definitions->Signature, aAddon->Definitions->Name, + aAddon->Definitions->Version, aAddon->Definitions->Provider, + aAddon->Definitions->UpdateLink != nullptr ? aAddon->Definitions->UpdateLink : "")) { Loader::QueueAddon(ELoaderAction::Reload, tmpPath); } @@ -203,17 +200,20 @@ namespace GUI // just check if loaded, if it was not hot-reloadable it would be EAddonState::LoadedLOCKED if (aAddon->State == EAddonState::Loaded) { - if (ImGui::GW2::Button((Language.Translate("((000020))") + sig).c_str(), ImVec2(btnWidth * ImGui::GetFontSize(), btnHeight))) + if (ImGui::GW2::Button((aAddon->IsWaitingForUnload ? Language.Translate("((000078))") : Language.Translate("((000020))") + sig).c_str(), ImVec2(btnWidth * ImGui::GetFontSize(), btnHeight))) { - //LogDebug(CH_GUI, "Unload called: %s", it.second->Definitions->Name); - Loader::QueueAddon(ELoaderAction::Unload, aPath); + if (!aAddon->IsWaitingForUnload) + { + //LogDebug(CH_GUI, "Unload called: %s", it.second->Definitions->Name); + Loader::QueueAddon(ELoaderAction::Unload, aPath); + } } if (RequestedAddons.size() > 0) { ImGui::GW2::TooltipGeneric(Language.Translate("((000021))")); } } - else if (aAddon->State == EAddonState::LoadedLOCKED && aAddon->ShouldDisableNextLaunch == false) + else if (aAddon->State == EAddonState::LoadedLOCKED && aAddon->IsFlaggedForDisable == false) { std::string additionalInfo; @@ -225,11 +225,11 @@ namespace GUI if (ImGui::GW2::Button((Language.Translate("((000022))") + sig).c_str(), ImVec2(btnWidth * ImGui::GetFontSize(), btnHeight))) { - aAddon->ShouldDisableNextLaunch = true; + aAddon->IsFlaggedForDisable = true; } ImGui::GW2::TooltipGeneric(Language.Translate("((000023))"), additionalInfo.c_str()); } - else if (aAddon->State == EAddonState::LoadedLOCKED && aAddon->ShouldDisableNextLaunch == true) + else if (aAddon->State == EAddonState::LoadedLOCKED && aAddon->IsFlaggedForDisable == true) { std::string additionalInfo; @@ -241,7 +241,7 @@ namespace GUI if (ImGui::GW2::Button((Language.Translate("((000024))") + sig).c_str(), ImVec2(btnWidth * ImGui::GetFontSize(), btnHeight))) { - aAddon->ShouldDisableNextLaunch = false; + aAddon->IsFlaggedForDisable = false; } ImGui::GW2::TooltipGeneric(Language.Translate("((000025))"), additionalInfo.c_str()); } @@ -346,7 +346,7 @@ namespace GUI } else { - ToSComplianceWarning = TextureLoader::GetOrCreate(ICON_NOTIFICATION, RES_ICON_NOTIFICATION, NexusHandle); + ToSComplianceWarning = TextureLoader::GetOrCreate(ICON_WARNING, RES_ICON_WARNING, NexusHandle); } } @@ -359,12 +359,12 @@ namespace GUI { std::thread([aAddon]() { - Loader::InstallAddon(aAddon); + Loader::Library::InstallAddon(aAddon); aAddon->IsInstalling = false; }) .detach(); - Loader::AddonConfig[aAddon->Signature].IsLoaded = true; + //Loader::AddonConfig[aAddon->Signature].IsLoaded = true; } } } diff --git a/src/GUI/Widgets/Addons/CAddonsWindow.cpp b/src/GUI/Widgets/Addons/CAddonsWindow.cpp index 99b58a8..ebc09b7 100644 --- a/src/GUI/Widgets/Addons/CAddonsWindow.cpp +++ b/src/GUI/Widgets/Addons/CAddonsWindow.cpp @@ -10,6 +10,7 @@ #include "Renderer.h" #include "Loader/Loader.h" +#include "Loader/Library.h" #include "Loader/ArcDPS.h" #include "AddonItem.h" #include "Textures/TextureLoader.h" @@ -206,10 +207,10 @@ namespace GUI { const std::lock_guard lock(Loader::Mutex); { - for (auto& [path, addon] : Loader::Addons) + for (auto addon : Loader::Addons) { - if (path.filename() == "arcdps_integration64.dll") { continue; } - AddonItem(path, addon); + if (addon->Path.filename() == "arcdps_integration64.dll") { continue; } + AddonItem(addon->Path, addon); } } } @@ -236,26 +237,23 @@ namespace GUI checkedForUpdates = 0; queuedForCheck = 0; /* pre-iterate to get the count of how many need to be checked, else one call might finish before the count can be incremented */ - for (auto& [path, addon] : Loader::Addons) + for (auto addon : Loader::Addons) { if (nullptr == addon->Definitions) { continue; } queuedForCheck++; } - for (auto& [path, addon] : Loader::Addons) + for (auto addon : Loader::Addons) { if (nullptr == addon->Definitions) { continue; } - std::filesystem::path tmpPath = path.string(); - signed int tmpSig = addon->Definitions->Signature; - std::string tmpName = addon->Definitions->Name; - AddonVersion tmpVers = addon->Definitions->Version; - EUpdateProvider tmpProv = addon->Definitions->Provider; - std::string tmpLink = addon->Definitions->UpdateLink != nullptr ? addon->Definitions->UpdateLink : ""; + std::filesystem::path tmpPath = addon->Path.string(); - std::thread([tmpPath, tmpSig, tmpName, tmpVers, tmpProv, tmpLink]() + std::thread([tmpPath, addon]() { - if (Loader::UpdateAddon(tmpPath, tmpSig, tmpName, tmpVers, tmpProv, tmpLink)) + if (Loader::UpdateAddon(tmpPath, addon->Definitions->Signature, addon->Definitions->Name, + addon->Definitions->Version, addon->Definitions->Provider, + addon->Definitions->UpdateLink != nullptr ? addon->Definitions->UpdateLink : "")) { Loader::QueueAddon(ELoaderAction::Reload, tmpPath); } @@ -281,13 +279,13 @@ namespace GUI int downloadable = 0; const std::lock_guard lockLoader(Loader::Mutex); - if (Loader::AddonLibrary.size() != 0) + if (Loader::Library::Addons.size() != 0) { - for (auto& libAddon : Loader::AddonLibrary) + for (auto& libAddon : Loader::Library::Addons) { bool exists = false; { - for (auto& [path, addon] : Loader::Addons) + for (auto addon : Loader::Addons) { // if libAddon already exist in installed addons // or if arcdps is loaded another way and the libAddon is arc @@ -307,7 +305,7 @@ namespace GUI } } - if (Loader::AddonLibrary.size() == 0 || downloadable == 0) + if (Loader::Library::Addons.size() == 0 || downloadable == 0) { ImVec2 windowSize = ImGui::GetWindowSize(); ImVec2 textSize = ImGui::CalcTextSize(Language.Translate("((000037))")); diff --git a/src/GUI/Widgets/Debug/CDebugWindow.cpp b/src/GUI/Widgets/Debug/CDebugWindow.cpp index 959a538..7924e09 100644 --- a/src/GUI/Widgets/Debug/CDebugWindow.cpp +++ b/src/GUI/Widgets/Debug/CDebugWindow.cpp @@ -324,10 +324,13 @@ namespace GUI { if (ImGui::TreeNode("Tracked")) { - for (const auto& [path, addon] : Loader::Addons) + for (auto addon : Loader::Addons) { - if (ImGui::TreeNode(path.string().c_str())) + std::string cached = "Currently not installed: "; + if (ImGui::TreeNode(addon->Path.empty() ? (cached + std::to_string(addon->MatchSignature)).c_str() : addon->Path.string().c_str())) { + ImGui::Text("MatchSignature: %d", addon->MatchSignature); + std::string state = "State: "; switch (addon->State) { @@ -346,9 +349,9 @@ namespace GUI ImGui::TextDisabled("MD5: %s", MD5ToString(addon->MD5).c_str()); ImGui::TextDisabled("Definitions: %p", addon->Definitions); ImGui::Separator(); - ImGui::TextDisabled("ShouldDisableNextLaunch: %s", addon->ShouldDisableNextLaunch ? "true" : "false"); + ImGui::TextDisabled("IsFlaggedForDisable: %s", addon->IsFlaggedForDisable ? "true" : "false"); ImGui::TextDisabled("IsPausingUpdates: %s", addon->IsPausingUpdates ? "true" : "false"); - ImGui::TextDisabled("WillBeUninstalled: %s", addon->WillBeUninstalled ? "true" : "false"); + ImGui::TextDisabled("IsFlaggedForUninstall: %s", addon->IsFlaggedForUninstall ? "true" : "false"); ImGui::TextDisabled("IsDisabledUntilUpdate: %s", addon->IsDisabledUntilUpdate ? "true" : "false"); if (addon->Definitions != nullptr) diff --git a/src/GUI/Widgets/QuickAccess/QuickAccess.cpp b/src/GUI/Widgets/QuickAccess/QuickAccess.cpp index a37d694..dca6180 100644 --- a/src/GUI/Widgets/QuickAccess/QuickAccess.cpp +++ b/src/GUI/Widgets/QuickAccess/QuickAccess.cpp @@ -290,8 +290,6 @@ namespace GUI } } QuickAccess::Mutex.unlock(); - - NexusLink->QuickAccessIconsCount = GUI::QuickAccess::Registry.size(); } void RemoveShortcut(const char* aIdentifier) { @@ -302,8 +300,6 @@ namespace GUI Registry.erase(str); } QuickAccess::Mutex.unlock(); - - NexusLink->QuickAccessIconsCount = GUI::QuickAccess::Registry.size(); } void NotifyShortcut(const char* aIdentifier) { @@ -383,8 +379,6 @@ namespace GUI } QuickAccess::Mutex.unlock(); - NexusLink->QuickAccessIconsCount = GUI::QuickAccess::Registry.size(); - return refCounter; } } diff --git a/src/Hooks.cpp b/src/Hooks.cpp index f0419e7..71414ad 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -116,7 +116,7 @@ namespace Hooks NexusLink->FontBig = FontBig; NexusLink->FontUI = FontUI; - //NexusLink->QuickAccessIconsCount = GUI::QuickAccess::Registry.size(); // write this only when adding/removing icons + NexusLink->QuickAccessIconsCount = GUI::QuickAccess::Registry.size(); // write this only when adding/removing icons NexusLink->QuickAccessMode = (int)GUI::QuickAccess::Location; NexusLink->QuickAccessIsVertical = GUI::QuickAccess::VerticalLayout; } diff --git a/src/Loader/Addon.h b/src/Loader/Addon.h index 740c202..b81fb9b 100644 --- a/src/Loader/Addon.h +++ b/src/Loader/Addon.h @@ -3,6 +3,7 @@ #include #include +#include #include "EAddonState.h" #include "AddonDefinition.h" @@ -11,16 +12,23 @@ struct Addon { EAddonState State; + std::filesystem::path Path; + std::vector MD5; HMODULE Module; DWORD ModuleSize; - std::vector MD5; AddonDefinition* Definitions; - bool ShouldDisableNextLaunch; + /* Saved states */ bool IsPausingUpdates; - bool WillBeUninstalled; bool IsDisabledUntilUpdate; + + /* Runtime states */ + signed int MatchSignature; + //bool IsInitialLoad = true; bool IsCheckingForUpdates; + bool IsWaitingForUnload; + bool IsFlaggedForUninstall; + bool IsFlaggedForDisable; }; #endif \ No newline at end of file diff --git a/src/Loader/AddonDefinition.cpp b/src/Loader/AddonDefinition.cpp index c5a9840..e5d776f 100644 --- a/src/Loader/AddonDefinition.cpp +++ b/src/Loader/AddonDefinition.cpp @@ -78,3 +78,35 @@ bool AddonDefinition::HasFlag(EAddonFlags aAddonFlag) { return (bool)(Flags & aAddonFlag); } + +void AddonDefinition::Copy(AddonDefinition* aSrc, AddonDefinition** aDst) +{ + if (aSrc == nullptr) + { + *aDst = new AddonDefinition{}; + return; + } + + // Allocate new memory and copy data, copy strings + *aDst = new AddonDefinition(*aSrc); + (*aDst)->Name = _strdup(aSrc->Name); + (*aDst)->Author = _strdup(aSrc->Author); + (*aDst)->Description = _strdup(aSrc->Description); + (*aDst)->UpdateLink = aSrc->UpdateLink ? _strdup(aSrc->UpdateLink) : nullptr; +} + +void AddonDefinition::Free(AddonDefinition** aDefinitions) +{ + if (*aDefinitions == nullptr) { return; } + + free((char*)(*aDefinitions)->Name); + free((char*)(*aDefinitions)->Author); + free((char*)(*aDefinitions)->Description); + if ((*aDefinitions)->UpdateLink) + { + free((char*)(*aDefinitions)->UpdateLink); + } + delete* aDefinitions; + + *aDefinitions = nullptr; +} \ No newline at end of file diff --git a/src/Loader/AddonDefinition.h b/src/Loader/AddonDefinition.h index b183da2..47451ed 100644 --- a/src/Loader/AddonDefinition.h +++ b/src/Loader/AddonDefinition.h @@ -48,6 +48,9 @@ struct AddonDefinition /* internal */ bool HasMinimumRequirements(); bool HasFlag(EAddonFlags aAddonFlag); + + static void Copy(AddonDefinition* aSrc, AddonDefinition** aDst); + static void Free(AddonDefinition** aDefinitions); }; #endif \ No newline at end of file diff --git a/src/Loader/ELoaderAction.h b/src/Loader/ELoaderAction.h index 3426c51..baf2469 100644 --- a/src/Loader/ELoaderAction.h +++ b/src/Loader/ELoaderAction.h @@ -7,7 +7,9 @@ enum class ELoaderAction Load, Unload, Uninstall, - Reload + Reload, + FreeLibrary, + FreeLibraryThenLoad }; #endif \ No newline at end of file diff --git a/src/Loader/Library.cpp b/src/Loader/Library.cpp new file mode 100644 index 0000000..bfee7e6 --- /dev/null +++ b/src/Loader/Library.cpp @@ -0,0 +1,257 @@ +///---------------------------------------------------------------------------------------------------- +/// Copyright (c) Raidcore.GG - All rights reserved. +/// +/// Name : Library.cpp +/// Description : Handles installation of new addons and fetching them from the Raidcore API. +/// Authors : K. Bieniek +///---------------------------------------------------------------------------------------------------- + +#include "Library.h" + +#include "Loader.h" + +#include "core.h" +#include "Consts.h" +#include "Shared.h" +#include "Paths.h" + +#include "nlohmann/json.hpp" +using json = nlohmann::json; + +std::string extDll = ".dll"; + +namespace Loader +{ + namespace Library + { + std::mutex Mutex; + std::vector Addons; + + void Fetch() + { + json response = RaidcoreAPI->Get("/addonlibrary"); + + if (!response.is_null()) + { + const std::lock_guard lock(Mutex); + Addons.clear(); + + for (const auto& addon : response) + { + LibraryAddon* newAddon = new LibraryAddon{}; + newAddon->Signature = addon["id"]; + newAddon->Name = addon["name"]; + newAddon->Description = addon["description"]; + newAddon->Provider = GetProvider(addon["download"]); + newAddon->DownloadURL = addon["download"]; + if (addon.contains("tos_compliance") && !addon["tos_compliance"].is_null()) + { + newAddon->ToSComplianceNotice = addon["tos_compliance"]; + } + + Addons.push_back(newAddon); + } + + std::sort(Addons.begin(), Addons.end(), [](LibraryAddon* a, LibraryAddon* b) { + return a->Name < b->Name; + }); + } + else + { + LogWarning(CH_LOADER, "Error parsing API response for /addonlibrary."); + } + } + + void InstallAddon(LibraryAddon* aAddon) + { + aAddon->IsInstalling = true; + + std::filesystem::path installPath; + + /* this is all modified duplicate code from update */ + std::string baseUrl; + std::string endpoint; + + // override provider if none set, but a Raidcore ID is used + if (aAddon->Provider == EUpdateProvider::None && aAddon->Signature > 0) + { + aAddon->Provider = EUpdateProvider::Raidcore; + } + + /* setup baseUrl and endpoint */ + switch (aAddon->Provider) + { + case EUpdateProvider::None: return; + + case EUpdateProvider::Raidcore: + baseUrl = API_RAIDCORE; + endpoint = "/addons/" + std::to_string(aAddon->Signature); + + break; + + case EUpdateProvider::GitHub: + baseUrl = API_GITHUB; + if (aAddon->DownloadURL.empty()) + { + LogWarning(CH_LOADER, "Addon %s declares EUpdateProvider::GitHub but has no UpdateLink set.", aAddon->Name); + return; + } + + endpoint = "/repos" + GetEndpoint(aAddon->DownloadURL) + "/releases"; // "/releases/latest"; // fuck you Sognus + + break; + + case EUpdateProvider::Direct: + if (aAddon->DownloadURL.empty()) + { + LogWarning(CH_LOADER, "Addon %s declares EUpdateProvider::Direct but has no UpdateLink set.", aAddon->Name); + return; + } + + baseUrl = GetBaseURL(aAddon->DownloadURL); + endpoint = GetEndpoint(aAddon->DownloadURL); + + if (baseUrl.empty() || endpoint.empty()) + { + return; + } + + break; + } + + if (EUpdateProvider::Raidcore == aAddon->Provider) + { + LogWarning(CH_LOADER, "Downloading via Raidcore is not implemented yet, due to user-friendly names requiring an API request. If you see this tell the developers about it! Thank you!"); + return; + //RaidcoreAPI->Download(addonPath, endpoint + "/download"); // e.g. api.raidcore.gg/addons/17/download + } + else if (EUpdateProvider::GitHub == aAddon->Provider) + { + json response = GitHubAPI->Get(endpoint); + + if (response.is_null()) + { + LogWarning(CH_LOADER, "Error parsing API response."); + return; + } + + response = response[0]; // filthy hack to get "latest" + + if (response["tag_name"].is_null()) + { + LogWarning(CH_LOADER, "No tag_name set on %s%s", baseUrl.c_str(), endpoint.c_str()); + return; + } + + std::string endpointDownload; // e.g. github.com/RaidcoreGG/GW2-CommandersToolkit/releases/download/20220918-135925/squadmanager.dll + + if (response["assets"].is_null()) + { + LogWarning(CH_LOADER, "Release has no assets. Cannot check against version. (%s%s)", baseUrl.c_str(), endpoint.c_str()); + return; + } + + for (auto& asset : response["assets"]) + { + std::string assetName = asset["name"].get(); + + if (assetName.size() < 4) + { + continue; + } + + if (std::string_view(assetName).substr(assetName.size() - 4) == ".dll") + { + asset["browser_download_url"].get_to(endpointDownload); + } + } + + std::string downloadBaseUrl = GetBaseURL(endpointDownload); + endpointDownload = GetEndpoint(endpointDownload); + + httplib::Client downloadClient(downloadBaseUrl); + downloadClient.enable_server_certificate_verification(false); + downloadClient.set_follow_location(true); + + size_t lastSlashPos = endpointDownload.find_last_of('/'); + std::string filename = endpointDownload.substr(lastSlashPos + 1); + size_t dotDllPos = filename.find(extDll); + filename = filename.substr(0, filename.length() - extDll.length()); + + std::filesystem::path probe = Path::D_GW2_ADDONS / (filename + extDll); + + int i = 0; + while (std::filesystem::exists(probe)) + { + probe = Path::D_GW2_ADDONS / (filename + "_" + std::to_string(i) + extDll); + i++; + } + + installPath = probe; + + size_t bytesWritten = 0; + std::ofstream file(probe, std::ofstream::binary); + auto downloadResult = downloadClient.Get(endpointDownload, [&](const char* data, size_t data_length) { + file.write(data, data_length); + bytesWritten += data_length; + return true; + }); + file.close(); + + if (!downloadResult || downloadResult->status != 200 || bytesWritten == 0) + { + LogWarning(CH_LOADER, "Error fetching %s%s", downloadBaseUrl.c_str(), endpointDownload.c_str()); + return; + } + } + else if (EUpdateProvider::Direct == aAddon->Provider) + { + /* prepare client request */ + httplib::Client client(baseUrl); + client.enable_server_certificate_verification(false); + client.set_follow_location(true); + + size_t lastSlashPos = endpoint.find_last_of('/'); + std::string filename = endpoint.substr(lastSlashPos + 1); + size_t dotDllPos = filename.find(extDll); + if (dotDllPos != std::string::npos) + { + filename = filename.substr(0, filename.length() - extDll.length()); + } + else + { + filename = Normalize(aAddon->Name); + } + + std::filesystem::path probe = Path::D_GW2_ADDONS / (filename + extDll); + + int i = 0; + while (std::filesystem::exists(probe)) + { + probe = Path::D_GW2_ADDONS / (filename + "_" + std::to_string(i) + extDll); + i++; + } + + installPath = probe; + + size_t bytesWritten = 0; + std::ofstream fileUpdate(probe, std::ofstream::binary); + auto downloadResult = client.Get(endpoint, [&](const char* data, size_t data_length) { + fileUpdate.write(data, data_length); + bytesWritten += data_length; + return true; + }); + fileUpdate.close(); + + if (!downloadResult || downloadResult->status != 200 || bytesWritten == 0) + { + LogWarning(CH_LOADER, "Error fetching %s%s", baseUrl.c_str(), endpoint.c_str()); + return; + } + } + + LogInfo(CH_LOADER, "Successfully installed %s.", aAddon->Name.c_str()); + Loader::QueueAddon(ELoaderAction::Reload, installPath); + } + } +} diff --git a/src/Loader/Library.h b/src/Loader/Library.h new file mode 100644 index 0000000..b69bfb5 --- /dev/null +++ b/src/Loader/Library.h @@ -0,0 +1,44 @@ +///---------------------------------------------------------------------------------------------------- +/// Copyright (c) Raidcore.GG - All rights reserved. +/// +/// Name : Library.h +/// Description : Handles installation of new addons and fetching them from the Raidcore API. +/// Authors : K. Bieniek +///---------------------------------------------------------------------------------------------------- + +#ifndef LIBRARY_H +#define LIBRARY_H + +#include +#include + +#include "LibraryAddon.h" + +///---------------------------------------------------------------------------------------------------- +/// Loader Namespace +///---------------------------------------------------------------------------------------------------- +namespace Loader +{ + ///---------------------------------------------------------------------------------------------------- + /// Library Namespace + ///---------------------------------------------------------------------------------------------------- + namespace Library + { + extern std::mutex Mutex; + extern std::vector Addons; + + ///---------------------------------------------------------------------------------------------------- + /// Fetch: + /// Fetch AddonLibrary. + ///---------------------------------------------------------------------------------------------------- + void Fetch(); + + ///---------------------------------------------------------------------------------------------------- + /// InstallAddon: + /// Installs an addon and notifies the Loader. + ///---------------------------------------------------------------------------------------------------- + void InstallAddon(LibraryAddon* aAddon); + } +} + +#endif diff --git a/src/Loader/LibraryAddon.h b/src/Loader/LibraryAddon.h index 686f1ad..ea26282 100644 --- a/src/Loader/LibraryAddon.h +++ b/src/Loader/LibraryAddon.h @@ -7,13 +7,14 @@ struct LibraryAddon { - bool IsInstalling = false; - signed int Signature; + signed int Signature; std::string Name; std::string Description; - EUpdateProvider Provider; + EUpdateProvider Provider; std::string DownloadURL; std::string ToSComplianceNotice; + + bool IsInstalling = false; }; #endif \ No newline at end of file diff --git a/src/Loader/Loader.cpp b/src/Loader/Loader.cpp index 9273200..6ec2252 100644 --- a/src/Loader/Loader.cpp +++ b/src/Loader/Loader.cpp @@ -1,15 +1,19 @@ +///---------------------------------------------------------------------------------------------------- +/// Copyright (c) Raidcore.GG - All rights reserved. +/// +/// Name : Loader.cpp +/// Description : Handles addon hot-loading, updates etc. +/// Authors : K. Bieniek +///---------------------------------------------------------------------------------------------------- + #include "Loader.h" #include #include #include -#include #include -#include #include -#include "resource.h" - #include "core.h" #include "State.h" #include "Shared.h" @@ -35,6 +39,7 @@ #include "Localization/Localization.h" #include "ArcDPS.h" +#include "Library.h" #include "nlohmann/json.hpp" using json = nlohmann::json; @@ -46,19 +51,11 @@ using json = nlohmann::json; namespace Loader { std::mutex Mutex; - std::vector AddonLibrary; std::unordered_map< std::filesystem::path, ELoaderAction > QueuedAddons; - std::map< - signed int, - StoredAddon - > AddonConfig; - std::map< - std::filesystem::path, - Addon* - > Addons; + std::vector Addons; std::map ApiDefs; int DirectoryChangeCountdown = 0; @@ -75,6 +72,8 @@ namespace Loader std::string extOld = ".old"; std::string extUninstall = ".uninstall"; + std::vector WhitelistedAddons; /* List of addons that should be loaded on initial startup. */ + bool DisableVolatileUntilUpdate = false; void Initialize() @@ -83,21 +82,9 @@ namespace Loader { LoadAddonConfig(); - /* if addons were specified via param, only load those */ - if (RequestedAddons.size() > 0) - { - for (auto& cfgIt : AddonConfig) - { - cfgIt.second.IsLoaded = false; - } - for (signed int sig : RequestedAddons) - { - AddonConfig[sig].IsLoaded = true; - } - } - FSItemList = ILCreateFromPathA(Path::GetAddonDirectory(nullptr)); - if (FSItemList == 0) { + if (FSItemList == 0) + { LogCritical(CH_LOADER, "Loader disabled. Reason: ILCreateFromPathA(Path::D_GW2_ADDONS) returned 0."); return; } @@ -120,16 +107,10 @@ namespace Loader return; } - std::thread([]() - { - GetAddonLibrary(); - }) - .detach(); - std::thread([]() - { - ArcDPS::GetPluginLibrary(); - }) - .detach(); + std::thread lib(Library::Fetch); + lib.detach(); + std::thread arclib(ArcDPS::GetPluginLibrary); + arclib.detach(); LoaderThread = std::thread(ProcessChanges); LoaderThread.detach(); @@ -157,7 +138,7 @@ namespace Loader { while (Addons.size() != 0) { - UnloadAddon(Addons.begin()->first, true); + UnloadAddon(Addons.front()->Path); Addons.erase(Addons.begin()); } } @@ -168,34 +149,68 @@ namespace Loader { if (std::filesystem::exists(Path::F_ADDONCONFIG)) { + try { - try + std::ifstream file(Path::F_ADDONCONFIG); + + json cfg = json::parse(file); + for (json addonInfo : cfg) { - std::ifstream file(Path::F_ADDONCONFIG); + signed int signature = 0; + if (!addonInfo["Signature"].is_null()) { addonInfo["Signature"].get_to(signature); } - json cfg = json::parse(file); - for (json addonInfo : cfg) - { - signed int signature = 0; - StoredAddon storedAddonInfo{}; - if (!addonInfo["Signature"].is_null()) { addonInfo["Signature"].get_to(signature); } - if (!addonInfo["IsPausingUpdates"].is_null()) { addonInfo["IsPausingUpdates"].get_to(storedAddonInfo.IsPausingUpdates); } - if (!addonInfo["IsLoaded"].is_null()) { addonInfo["IsLoaded"].get_to(storedAddonInfo.IsLoaded); } - if (!addonInfo["IsDisabledUntilUpdate"].is_null()) { addonInfo["IsDisabledUntilUpdate"].get_to(storedAddonInfo.IsDisabledUntilUpdate); } + if (signature == 0) { continue; } - if (signature == 0) { continue; } + Addon* addon = FindAddonBySig(signature); - AddonConfig[signature] = storedAddonInfo; + if (!addon) + { + addon = new Addon{}; + addon->State = EAddonState::None; + Addons.push_back(addon); } - file.close(); - } - catch (json::parse_error& ex) - { - LogWarning(CH_KEYBINDS, "AddonConfig.json could not be parsed. Error: %s", ex.what()); + if (!addonInfo["IsPausingUpdates"].is_null()) { addonInfo["IsPausingUpdates"].get_to(addon->IsPausingUpdates); } + if (!addonInfo["IsDisabledUntilUpdate"].is_null()) { addonInfo["IsDisabledUntilUpdate"].get_to(addon->IsDisabledUntilUpdate); } + + // to match the actual addon to the saved states + addon->MatchSignature = signature; + + /* should load, indicates whether it was loaded last time */ + bool shouldLoad = false; + if (!addonInfo["IsLoaded"].is_null()) { addonInfo["IsLoaded"].get_to(shouldLoad); } + + if (shouldLoad) + { + auto it = std::find(WhitelistedAddons.begin(), WhitelistedAddons.end(), signature); + if (it == WhitelistedAddons.end()) + { + WhitelistedAddons.push_back(signature); + } + } } + + file.close(); + } + catch (json::parse_error& ex) + { + LogWarning(CH_KEYBINDS, "AddonConfig.json could not be parsed. Error: %s", ex.what()); } } + + /* if addons were specified via param, only load those */ + if (RequestedAddons.size() > 0) + { + WhitelistedAddons.clear(); + WhitelistedAddons = RequestedAddons; + } + + /* ensure arcdps integration will be loaded */ + auto hasArcIntegration = std::find(WhitelistedAddons.begin(), WhitelistedAddons.end(), 0xFED81763); + if (hasArcIntegration == WhitelistedAddons.end()) + { + WhitelistedAddons.push_back(0xFED81763); + } } void SaveAddonConfig() { @@ -203,103 +218,40 @@ namespace Loader { json cfg = json::array(); - std::vector foundAddons; + std::vector trackedSigs; - for (auto it : Addons) + for (auto addon : Addons) { - Addon* addon = it.second; - + // skip bridge if (!addon->Definitions) { continue; } - if (addon->Definitions->Signature == -19392669) { continue; } // skip bridge + if (addon->MatchSignature == 0xFED81763 || (addon->Definitions && addon->Definitions->Signature == 0xFED81763)) { continue; } + + auto tracked = std::find(trackedSigs.begin(), trackedSigs.end(), addon->Definitions->Signature); + if (tracked != trackedSigs.end()) { continue; } json addonInfo = { - {"Signature", addon->Definitions->Signature}, + {"Signature", addon->Definitions ? addon->Definitions->Signature : addon->MatchSignature}, {"IsLoaded", addon->State == EAddonState::Loaded || addon->State == EAddonState::LoadedLOCKED ? true : false}, {"IsPausingUpdates", addon->IsPausingUpdates}, {"IsDisabledUntilUpdate", addon->IsDisabledUntilUpdate} }; /* override loaded state, if it's supposed to disable next launch */ - if (addon->State == EAddonState::LoadedLOCKED && addon->ShouldDisableNextLaunch) + if (addon->State == EAddonState::LoadedLOCKED && addon->IsFlaggedForDisable) { addonInfo["IsLoaded"] = false; } - foundAddons.push_back(addon->Definitions->Signature); cfg.push_back(addonInfo); } - /* also keep tracking addons that are no longer there */ - for (auto& cfgIt : AddonConfig) - { - if (cfgIt.first == -19392669) { continue; } // skip bridge - - bool tracked = false; - for (size_t i = 0; i < foundAddons.size(); i++) - { - if (foundAddons[i] == cfgIt.first) - { - tracked = true; - break; - } - } - - if (!tracked) - { - json addonInfo = - { - {"Signature", cfgIt.first}, - {"IsLoaded", cfgIt.second.IsLoaded}, - {"IsPausingUpdates", cfgIt.second.IsPausingUpdates}, - {"IsDisabledUntilUpdate", cfgIt.second.IsDisabledUntilUpdate} - }; - - cfg.push_back(addonInfo); - } - } - std::ofstream file(Path::F_ADDONCONFIG); file << cfg.dump(1, '\t') << std::endl; file.close(); } } - void GetAddonLibrary() - { - json response = RaidcoreAPI->Get("/addonlibrary"); - - if (!response.is_null()) - { - const std::lock_guard lock(Mutex); - AddonLibrary.clear(); - - for (const auto& addon : response) - { - LibraryAddon* newAddon = new LibraryAddon{}; - newAddon->Signature = addon["id"]; - newAddon->Name = addon["name"]; - newAddon->Description = addon["description"]; - newAddon->Provider = GetProvider(addon["download"]); - newAddon->DownloadURL = addon["download"]; - if (addon.contains("tos_compliance") && !addon["tos_compliance"].is_null()) - { - newAddon->ToSComplianceNotice = addon["tos_compliance"]; - } - - AddonLibrary.push_back(newAddon); - } - - std::sort(AddonLibrary.begin(), AddonLibrary.end(), [](LibraryAddon* a, LibraryAddon* b) { - return a->Name < b->Name; - }); - } - else - { - LogWarning(CH_CORE, "Error parsing API response for /addonlibrary."); - } - } - UINT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg == WM_ADDONDIRUPDATE) @@ -347,37 +299,32 @@ namespace Loader UnloadAddon(it->first); break; case ELoaderAction::Uninstall: + UnloadAddon(it->first); UninstallAddon(it->first); break; case ELoaderAction::Reload: - ReloadAddon(it->first); + /* if it's already loaded, the unload call with unload, then load async (after done) + * if it's not already loaded, the unload call is skipped, and it's loaded instead */ + + UnloadAddon(it->first, true); + LoadAddon(it->first, true); + break; + case ELoaderAction::FreeLibrary: + FreeAddon(it->first); + break; + + // this can only be invoked via UnloadAddon(..., true) (aka Reload) + case ELoaderAction::FreeLibraryThenLoad: + FreeAddon(it->first); + LoadAddon(it->first, true); break; } - /* - if the action is not reload, then remove it after it was performed - else check if the addon exists and if it does check, if it is anything but NotLoaded - if it is, it was processed - */ - if (it->second != ELoaderAction::Reload) - { - QueuedAddons.erase(it); - } - else - { - auto addon = Addons.find(it->first); + QueuedAddons.erase(it); - if (addon != Addons.end()) - { - if (addon->second->State != EAddonState::NotLoaded) - { - QueuedAddons.erase(it); - } - } - } + SortAddons(); + ArcDPS::GetPlugins(); } - - ArcDPS::GetPlugins(); } void QueueAddon(ELoaderAction aAction, const std::filesystem::path& aPath) { @@ -426,7 +373,6 @@ namespace Loader int gameBuild = std::stoi(buildStr); int lastGameBuild = 0; - long long lastChecked = 0; if (!Settings::Settings.is_null()) { @@ -434,44 +380,10 @@ namespace Loader { Settings::Settings[OPT_LASTGAMEBUILD].get_to(lastGameBuild); } - - //if (!Settings::Settings[OPT_LASTCHECKEDGAMEBUILD].is_null()) - //{ - // Settings::Settings[OPT_LASTCHECKEDGAMEBUILD].get_to(lastChecked); - //} } if (gameBuild - lastGameBuild > 350 && lastGameBuild != 0) { - /* game updated */ - - /* check if today is tuesday (usually breaking patch) */ - /*std::time_t t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - tm local_tm = *localtime(&t); - int wday = local_tm.tm_wday; - - long long deltaTime = Timestamp() - lastChecked; - - long long secondsSinceMidnight = (local_tm.tm_hour * 60 * 60) + (local_tm.tm_min * 60) + (local_tm.tm_sec); - long long secondsSinceMidnightYesterday = (24 * 60 * 60) + secondsSinceMidnight; - long long aWholeEntireWeekInSeconds = 7 * 24 * 60 * 60; - - if (wday == 2 && deltaTime > secondsSinceMidnightYesterday) - { - DisableVolatileUntilUpdate = true; - LogWarning(CH_LOADER, "Game updated. Current Build %d. Old Build: %d. Disabling volatile addons until they update.", gameBuild, lastGameBuild); - } - else if (deltaTime > aWholeEntireWeekInSeconds) - { - DisableVolatileUntilUpdate = true; - LogWarning(CH_LOADER, "Game updated. Current Build %d. Old Build: %d. Disabling volatile addons until they update.", gameBuild, lastGameBuild); - } - else - { - DisableVolatileUntilUpdate = false; - LogWarning(CH_LOADER, "Game updated. But it's not a tuesday, so surely nothing broke."); - }*/ - DisableVolatileUntilUpdate = true; LogWarning(CH_LOADER, "Game updated. Current Build %d. Old Build: %d. Disabling volatile addons until they update.", gameBuild, lastGameBuild); @@ -483,7 +395,6 @@ namespace Loader } Settings::Settings[OPT_LASTGAMEBUILD] = gameBuild; - //Settings::Settings[OPT_LASTCHECKEDGAMEBUILD] = Timestamp(); Settings::Save(); } @@ -508,22 +419,23 @@ namespace Loader const std::lock_guard lock(Mutex); // check all tracked addons - for (auto& it : Addons) + for (Addon* addon : Addons) { // if addon no longer on disk - if (!std::filesystem::exists(it.first)) + if (!std::filesystem::exists(addon->Path)) { - QueueAddon(ELoaderAction::Unload, it.first); + QueueAddon(ELoaderAction::Unload, addon->Path); continue; } // get md5 of each file currently on disk and compare to tracked md5 // also check if an update is available (e.g. "addon.dll" + ".update" -> "addon.dll.update" exists) - std::vector md5 = MD5FromFile(it.first); - std::filesystem::path updatePath = it.first.string() + extUpdate; - if ((it.second->MD5.empty() || it.second->MD5 != md5) || std::filesystem::exists(updatePath)) + std::vector md5 = MD5FromFile(addon->Path); + std::filesystem::path updatePath = addon->Path.string() + extUpdate; + if ((addon->MD5.empty() || addon->MD5 != md5) || std::filesystem::exists(updatePath)) { - QueueAddon(ELoaderAction::Reload, it.first); + UpdateSwapAddon(addon->Path); + QueueAddon(ELoaderAction::Reload, addon->Path); } } @@ -538,7 +450,8 @@ namespace Loader } // if already tracked - if (Addons.find(path) != Addons.end()) + Addon* exists = FindAddonByPath(path); + if (exists) { continue; } @@ -577,6 +490,19 @@ namespace Loader return; } } + + if (path.extension() == extOld) + { + try + { + std::filesystem::remove(path); + } + catch (std::filesystem::filesystem_error fErr) + { + LogDebug(CH_LOADER, "%s", fErr.what()); + return; + } + } } } @@ -589,17 +515,13 @@ namespace Loader std::string path = aPath.string(); std::string strFile = aPath.filename().string(); - bool firstLoad; + /* used to indicate whether the addon already existed or was newly allocated and has to be merged (possibly) with the config-created one */ + bool allocNew = false; - Addon* addon; + Addon* addon = FindAddonByPath(aPath); - auto it = Addons.find(aPath); - - if (it != Addons.end()) + if (addon) { - firstLoad = false; - addon = it->second; - if (addon->State == EAddonState::Loaded || addon->State == EAddonState::LoadedLOCKED) { //LogWarning(CH_LOADER, "Cancelled loading \"%s\". Already loaded.", strFile.c_str()); @@ -608,18 +530,10 @@ namespace Loader } else { - if (std::filesystem::exists(Path::F_ADDONCONFIG) && !aIsReload) - { - firstLoad = true; - } - else - { - // migration - firstLoad = false; - } + allocNew = true; addon = new Addon{}; addon->State = EAddonState::None; - Addons.insert({ aPath, addon }); + addon->Path = aPath; } UpdateSwapAddon(aPath); @@ -633,23 +547,37 @@ namespace Loader { LogWarning(CH_LOADER, "Failed LoadLibrary on \"%s\". Incompatible. Last Error: %u", strFile.c_str(), GetLastError()); addon->State = EAddonState::NotLoadedIncompatible; + + if (allocNew) + { + Addons.push_back(addon); // track this anyway + } + return; } /* doesn't have GetAddonDef */ if (FindFunction(addon->Module, &getAddonDef, "GetAddonDef") == false) { - //typedef void* (*get_init_addr)(char*, ImGuiContext*, void*, HMODULE, void*, void*); + /* if it is an arc plugin, tell arc to load it */ void* exp_get_init_addr = nullptr; if (FindFunction(addon->Module, &exp_get_init_addr, "get_init_addr") == true) { ArcDPS::Add(addon->Module); } - - LogWarning(CH_LOADER, "\"%s\" is not a Nexus-compatible library. Incompatible.", strFile.c_str()); + else + { + LogWarning(CH_LOADER, "\"%s\" is not a Nexus-compatible library. Incompatible.", strFile.c_str()); + } FreeLibrary(addon->Module); addon->State = EAddonState::NotLoadedIncompatible; addon->Module = nullptr; + + if (allocNew) + { + Addons.push_back(addon); // track this anyway + } + return; } @@ -658,89 +586,134 @@ namespace Loader /* addon defs are nullptr */ if (tmpDefs == nullptr) { - LogWarning(CH_LOADER, "\"%s\" is Nexus-compatible but returned a nullptr. Incompatible.", strFile.c_str()); + LogWarning(CH_LOADER, "\"%s\" is exporting \"GetAddonDef\" but returned a nullptr. Incompatible.", strFile.c_str()); FreeLibrary(addon->Module); addon->State = EAddonState::NotLoadedIncompatible; addon->Module = nullptr; + + if (allocNew) + { + Addons.push_back(addon); // track this anyway + } + return; } - // free old and clone new to show in list - FreeAddonDefs(&addon->Definitions); - CopyAddonDefs(tmpDefs, &addon->Definitions); - - // should load - bool shouldLoad = false; + /* free old (if exists) and clone new to show in list */ + AddonDefinition::Free(&addon->Definitions); + AddonDefinition::Copy(tmpDefs, &addon->Definitions); /* get stored info about addon and apply to addon */ - if (firstLoad) + if (allocNew) { - StoredAddon* addonInfo = nullptr; - auto cfgIt = AddonConfig.find(tmpDefs->Signature); - if (cfgIt != AddonConfig.end()) + Addon* stored = FindAddonByMatchSig(addon->Definitions->Signature); + + // stored exists and is "unclaimed" + if (stored && stored->State == EAddonState::None) { - addonInfo = &cfgIt->second; - addon->IsDisabledUntilUpdate = addonInfo->IsDisabledUntilUpdate; - addon->IsPausingUpdates = addonInfo->IsPausingUpdates; - if (addonInfo->IsLoaded) + /* we have some settings/info stored, we merge and delete the new alloc */ + Addon* alloc = addon; + addon = stored; + addon->Definitions = alloc->Definitions; + addon->Path = aPath; + addon->Module = alloc->Module; + addon->State = alloc->State; + addon->MD5 = alloc->MD5; + + delete alloc; + } + else + { + addon->MatchSignature = addon->Definitions->Signature; + if (allocNew) { - shouldLoad = true; + Addons.push_back(addon); // track this anyway } } } - else - { - shouldLoad = true; + + /* check if duplicate signature */ + auto duplicate = std::find_if(Addons.begin(), Addons.end(), [addon](Addon* cmpAddon) { + // check if it has definitions and if the signature is the same + return cmpAddon->Path != addon->Path && + cmpAddon->Definitions && + cmpAddon->Definitions->Signature == addon->Definitions->Signature; + }); + if (duplicate != Addons.end()) { + LogWarning(CH_LOADER, "\"%s\" or another addon with this signature (%d) is already loaded. Duplicate.", strFile.c_str(), addon->Definitions->Signature); + FreeLibrary(addon->Module); + AddonDefinition::Free(&addon->Definitions); + addon->State = EAddonState::NotLoadedDuplicate; + addon->Module = nullptr; + return; } - /* set disabled until update state if game has updated and addon is volatile and this is the intial load, - * subsequent (user-invoked) loads are on them */ - if (!aIsReload && DisableVolatileUntilUpdate && firstLoad && tmpDefs->HasFlag(EAddonFlags::IsVolatile)) + bool isInitialLoad = addon->State == EAddonState::None; + + // if not on whitelist and its the initial load (aka not manually invoked) + auto it = std::find(WhitelistedAddons.begin(), WhitelistedAddons.end(), addon->Definitions->Signature); + bool shouldLoad = it != WhitelistedAddons.end() || !isInitialLoad; + + // if pausing updates, but wasn't set to be disabled until update + bool shouldCheckForUpdate = !(addon->IsPausingUpdates && !addon->IsDisabledUntilUpdate); + + /* set DUU state if game has updated and addon is volatile and this is the intial load */ + if (isInitialLoad && addon->Definitions->HasFlag(EAddonFlags::IsVolatile) && DisableVolatileUntilUpdate) { addon->IsDisabledUntilUpdate = true; SaveAddonConfig(); // save the DUU state } + else if (!isInitialLoad && addon->IsDisabledUntilUpdate) // reset DUU state if loading manually + { + addon->IsDisabledUntilUpdate = false; + SaveAddonConfig(); // save the DUU state + } + + /* predeclare locked helper for later */ + bool locked = addon->Definitions->Unload == nullptr || addon->Definitions->HasFlag(EAddonFlags::DisableHotloading); /* don't update when reloading; check when: it's waiting to re-enable but wasn't manually invoked, it's not pausing updates atm */ - if (!aIsReload && ((addon->IsDisabledUntilUpdate && firstLoad) || !addon->IsPausingUpdates)) + if (!aIsReload && ((addon->IsDisabledUntilUpdate && isInitialLoad) || !addon->IsPausingUpdates)) { std::filesystem::path tmpPath = aPath.string(); - signed int tmpSig = tmpDefs->Signature; - std::string tmpName = tmpDefs->Name; - AddonVersion tmpVers = tmpDefs->Version; - bool tmpLocked = tmpDefs->Unload == nullptr || tmpDefs->HasFlag(EAddonFlags::DisableHotloading); - EUpdateProvider tmpProv = tmpDefs->Provider; - std::string tmpLink = tmpDefs->UpdateLink != nullptr ? tmpDefs->UpdateLink : ""; - - std::thread([tmpPath, tmpSig, tmpName, tmpVers, tmpLocked, tmpProv, tmpLink, addon, shouldLoad]() + std::thread([tmpPath, addon, locked, shouldLoad]() { - if (UpdateAddon(tmpPath, tmpSig, tmpName, tmpVers, tmpProv, tmpLink)) + if (UpdateAddon(tmpPath, addon->Definitions->Signature, addon->Definitions->Name, + addon->Definitions->Version, addon->Definitions->Provider, + addon->Definitions->UpdateLink != nullptr ? addon->Definitions->UpdateLink : "")) { LogInfo(CH_LOADER, "Update available for \"%s\".", tmpPath.string().c_str()); if (addon->IsDisabledUntilUpdate) { - addon->IsDisabledUntilUpdate = false; // reset state, because it updated - const std::lock_guard lock(Mutex); // mutex because we're async/threading - SaveAddonConfig(); // save the DUU state + // reset state, because it updated + addon->IsDisabledUntilUpdate = false; + + // mutex because we're async/threading + { + const std::lock_guard lock(Mutex); + SaveAddonConfig(); // save the DUU state + } } QueueAddon(ELoaderAction::Reload, tmpPath); } - else if (tmpLocked && shouldLoad && !addon->IsDisabledUntilUpdate) // if addon is locked and not DUU + else if (locked && shouldLoad && !addon->IsDisabledUntilUpdate) // if addon is locked and not DUU { + // the lock state is checked because if it will be locked it means it was unloaded, prior to checking for an update QueueAddon(ELoaderAction::Reload, tmpPath); } else if (addon->IsDisabledUntilUpdate && DisableVolatileUntilUpdate) // if addon is DUP and the global state is too { - std::string msg = tmpName + " "; + // show message that addon was disabled due to game update + std::string msg = addon->Definitions->Name; + msg.append(" "); msg.append(Language.Translate("((000073))")); GUI::Alerts::Notify(msg.c_str()); } }) .detach(); - /* if it will be locked, explicitly set it to NotLoaded, this prevents it from being loaded, so it can check for an update - * other addons continue and will be loaded. */ - if (tmpDefs->Unload == nullptr || tmpDefs->HasFlag(EAddonFlags::DisableHotloading)) + /* if will be locked, explicitly unload so the update can invoke a reload */ + if (locked) { FreeLibrary(addon->Module); addon->State = EAddonState::NotLoaded; @@ -749,19 +722,10 @@ namespace Loader } } - /* if someone wants to do shenanigans and inject a different integration module */ - if (tmpDefs->Signature == 0xFED81763 && aPath != Path::F_ARCDPSINTEGRATION) - { - FreeLibrary(addon->Module); - addon->State = EAddonState::NotLoadedIncompatible; - addon->Module = nullptr; - return; - } - /* don't load addons that weren't requested or loaded last time (ignore arcdps integration) */ - if (firstLoad && !shouldLoad && tmpDefs->Signature != 0xFED81763) + if (!shouldLoad) { - LogInfo(CH_LOADER, "\"%s\" was not requested via start parameter or last state was disabled. Skipped.", strFile.c_str(), tmpDefs->Signature); + //LogInfo(CH_LOADER, "\"%s\" was not requested via start parameter or last state was disabled. Skipped.", strFile.c_str(), addon->Definitions->Signature); FreeLibrary(addon->Module); addon->State = EAddonState::NotLoaded; addon->Module = nullptr; @@ -769,7 +733,7 @@ namespace Loader } /* doesn't fulfill min reqs */ - if (!tmpDefs->HasMinimumRequirements()) + if (!addon->Definitions->HasMinimumRequirements()) { LogWarning(CH_LOADER, "\"%s\" does not fulfill minimum requirements. At least define Name, Version, Author, Description as well as the Load function. Incompatible.", strFile.c_str()); FreeLibrary(addon->Module); @@ -778,24 +742,17 @@ namespace Loader return; } - /* check if duplicate signature */ - for (auto& it : Addons) + /* (effectively duplicate check) if someone wants to do shenanigans and inject a different integration module */ + if (addon->Definitions->Signature == 0xFED81763 && aPath != Path::F_ARCDPSINTEGRATION) { - // if defs defined && not the same path && signature the same though (another && it.second->Definitions in the mix because could still be null during load) - if (it.first != aPath && - it.second->Definitions && - it.second->Definitions->Signature == tmpDefs->Signature && - (it.second->State == EAddonState::Loaded || it.second->State == EAddonState::LoadedLOCKED)) - { - LogWarning(CH_LOADER, "\"%s\" or another addon with this signature (%d) is already loaded. Added to blacklist.", strFile.c_str(), tmpDefs->Signature); - FreeLibrary(addon->Module); - addon->State = EAddonState::NotLoadedDuplicate; - addon->Module = nullptr; - return; - } + LogWarning(CH_LOADER, "\"%s\" declares signature 0xFED81763 but is not the actual Nexus ArcDPS Integration. Either this was in error or an attempt to tamper with Nexus files. Incompatible.", strFile.c_str()); + FreeLibrary(addon->Module); + addon->State = EAddonState::NotLoadedIncompatible; + addon->Module = nullptr; + return; } - AddonAPI* api = GetAddonAPI(tmpDefs->APIVersion); // will be nullptr if doesn't exist or APIVersion = 0 + AddonAPI* api = GetAddonAPI(addon->Definitions->APIVersion); // will be nullptr if doesn't exist or APIVersion = 0 // if the api doesn't exist and there was one requested if (api == nullptr && addon->Definitions->APIVersion != 0) @@ -819,25 +776,15 @@ namespace Loader Events::Raise(EV_ADDON_LOADED, &addon->Definitions->Signature); Events::Raise(EV_MUMBLE_IDENTITY_UPDATED, MumbleIdentity); - bool locked = addon->Definitions->Unload == nullptr || addon->Definitions->HasFlag(EAddonFlags::DisableHotloading); + SortAddons(); + addon->State = locked ? EAddonState::LoadedLOCKED : EAddonState::Loaded; SaveAddonConfig(); - std::string apiVerStr; - if (addon->Definitions->APIVersion == 0) - { - apiVerStr = "(No API was requested.)"; - } - else - { - apiVerStr.append("(API Version "); - apiVerStr.append(std::to_string(addon->Definitions->APIVersion)); - apiVerStr.append(" was requested.)"); - } - LogInfo(CH_LOADER, u8"Loaded addon: %s (Signature %d) [%p - %p] %s Took %uµs.", - strFile.c_str(), addon->Definitions->Signature, addon->Module, - ((PBYTE)addon->Module) + moduleInfo.SizeOfImage, - apiVerStr.c_str(), time / std::chrono::microseconds(1) + LogInfo(CH_LOADER, u8"Loaded addon: %s (Signature %d) [%p - %p] (API Version %d was requested.) Took %uµs.", + strFile.c_str(), addon->Definitions->Signature, + addon->Module, ((PBYTE)addon->Module) + moduleInfo.SizeOfImage, + addon->Definitions->APIVersion, time / std::chrono::microseconds(1) ); /* if arcdps */ @@ -853,142 +800,164 @@ namespace Loader ArcDPS::InitializeBridge(addon->Module); } } - void UnloadAddon(const std::filesystem::path& aPath, bool aIsShutdown) + void UnloadAddon(const std::filesystem::path& aPath, bool aDoReload) { std::string path = aPath.string(); std::string strFile = aPath.filename().string(); - auto it = Addons.find(aPath); + Addon* addon = FindAddonByPath(aPath); - if (it == Addons.end()) + if (!addon || !(addon->State == EAddonState::Loaded || addon->State == EAddonState::LoadedLOCKED)) { - return; - } - - Addon* addon = it->second; - - if (addon->State == EAddonState::NotLoaded || - addon->State == EAddonState::NotLoadedDuplicate || - addon->State == EAddonState::NotLoadedIncompatible || - addon->State == EAddonState::NotLoadedIncompatibleAPI) - { - //LogWarning(CH_LOADER, "Cancelled unload of \"%s\". EAddonState = %d.", strFile.c_str(), addon->State); - return; - } - - std::chrono::steady_clock::time_point start_time; - std::chrono::steady_clock::time_point end_time; - std::chrono::steady_clock::duration time; - - if (addon->Definitions) - { - if (addon->State == EAddonState::Loaded) - { - start_time = std::chrono::high_resolution_clock::now(); - addon->Definitions->Unload(); - end_time = std::chrono::high_resolution_clock::now(); - time = end_time - start_time; - Events::Raise(EV_ADDON_UNLOADED, &addon->Definitions->Signature); - } - else if (addon->State == EAddonState::LoadedLOCKED && aIsShutdown) + if (!std::filesystem::exists(aPath)) { - if (addon->Definitions->Unload) + auto it = std::find(Addons.begin(), Addons.end(), addon); + if (it != Addons.end()) { - /* If it's a shutdown and Unload is defined, let the addon run its shutdown routine to save settings etc, but do not freelib */ - start_time = std::chrono::high_resolution_clock::now(); - addon->Definitions->Unload(); - end_time = std::chrono::high_resolution_clock::now(); - time = end_time - start_time; - Events::Raise(EV_ADDON_UNLOADED, &addon->Definitions->Signature); + if (addon->Definitions) + { + AddonDefinition::Free(&addon->Definitions); + } + Addons.erase(it); } } + //LogWarning(CH_LOADER, "Cancelled unload of \"%s\". EAddonState = %d.", strFile.c_str(), addon->State); + return; } - if (addon->Module) + bool isShutdown = State::Nexus == ENexusState::SHUTTING_DOWN; + + if (addon->Definitions) { - if (addon->State == EAddonState::Loaded || - (addon->State == EAddonState::LoadedLOCKED && aIsShutdown)) + // Either normal unload + // or shutting down anyway, addon has Unload defined, so it can save settings etc + if ((addon->State == EAddonState::Loaded) || (addon->State == EAddonState::LoadedLOCKED && isShutdown)) { - if (addon->ModuleSize > 0) - { - /* Verify all APIs don't have any unreleased references to the addons address space */ - void* startAddress = addon->Module; - void* endAddress = ((PBYTE)addon->Module) + addon->ModuleSize; - - int leftoverRefs = 0; - leftoverRefs += Events::Verify(startAddress, endAddress); - leftoverRefs += GUI::Verify(startAddress, endAddress); - leftoverRefs += GUI::QuickAccess::Verify(startAddress, endAddress); - leftoverRefs += Keybinds::Verify(startAddress, endAddress); - leftoverRefs += WndProc::Verify(startAddress, endAddress); - - if (leftoverRefs > 0) + addon->IsWaitingForUnload = true; + std::thread unloadTask([addon, aPath, isShutdown, aDoReload]() { - LogWarning(CH_LOADER, "Removed %d unreleased references from \"%s\". Make sure your addon releases all references during Addon::Unload().", leftoverRefs, strFile.c_str()); - } - } - } - - if (addon->State == EAddonState::Loaded) - { - int freeCalls = 0; + std::chrono::steady_clock::time_point start_time = std::chrono::high_resolution_clock::now(); + if (addon->Definitions->Unload) + { + addon->Definitions->Unload(); + } + std::chrono::steady_clock::time_point end_time = std::chrono::high_resolution_clock::now(); + std::chrono::steady_clock::duration time = end_time - start_time; - while (FreeLibrary(addon->Module)) - { - freeCalls++; - } + if (addon->Module && addon->ModuleSize > 0) + { + /* Verify all APIs don't have any unreleased references to the addons address space */ + void* startAddress = addon->Module; + void* endAddress = ((PBYTE)addon->Module) + addon->ModuleSize; + + int leftoverRefs = 0; + leftoverRefs += Events::Verify(startAddress, endAddress); + leftoverRefs += GUI::Verify(startAddress, endAddress); + leftoverRefs += GUI::QuickAccess::Verify(startAddress, endAddress); + leftoverRefs += Keybinds::Verify(startAddress, endAddress); + leftoverRefs += WndProc::Verify(startAddress, endAddress); + + if (leftoverRefs > 0) + { + LogWarning(CH_LOADER, "Removed %d unreleased references from \"%s\". Make sure your addon releases all references during Addon::Unload().", leftoverRefs, aPath.filename().string().c_str()); + } + } - if (freeCalls == 0) - { - LogWarning(CH_LOADER, "Couldn't unload \"%s\". FreeLibrary() call failed.", strFile.c_str()); - return; - } + addon->IsWaitingForUnload = false; + LogInfo(CH_LOADER, u8"Unloaded addon: %s (Took %uµs.)", aPath.filename().string().c_str(), time / std::chrono::microseconds(1)); - //LogDebug(CH_LOADER, "Called FreeLibrary() %d times on \"%s\".", freeCalls, strFile.c_str()); + if (!isShutdown) + { + Events::Raise(EV_ADDON_UNLOADED, &addon->Definitions->Signature); + + const std::lock_guard lock(Mutex); + if (aDoReload) + { + Loader::QueueAddon(ELoaderAction::FreeLibraryThenLoad, aPath); + } + else + { + Loader::QueueAddon(ELoaderAction::FreeLibrary, aPath); + } + } + }); + unloadTask.detach(); } } - if (addon->State == EAddonState::Loaded) + if (!isShutdown) { - addon->Module = nullptr; - addon->ModuleSize = 0; + SaveAddonConfig(); + } + } + + void FreeAddon(const std::filesystem::path& aPath) + { + std::string path = aPath.string(); + std::string strFile = aPath.filename().string(); - addon->State = EAddonState::NotLoaded; + Addon* addon = FindAddonByPath(aPath); - if (!std::filesystem::exists(aPath)) + if (!addon) + { + return; + } + + int freeCalls = 0; + + while (FreeLibrary(addon->Module)) + { + freeCalls++; + + if (freeCalls >= 10) { - Addons.erase(aPath); + LogWarning(CH_LOADER, "Aborting unload of \"%s\". Called FreeLibrary() 10 times.", strFile.c_str()); + break; } - - LogInfo(CH_LOADER, u8"Unloaded addon: %s (Took %uµs.)", strFile.c_str(), time / std::chrono::microseconds(1)); } - else if (addon->State == EAddonState::LoadedLOCKED && aIsShutdown) + + if (freeCalls == 0) { - LogInfo(CH_LOADER, u8"Unloaded addon on shutdown without freeing module due to locked state: %s (Took %uµs.)", strFile.c_str(), time / std::chrono::microseconds(1)); + LogWarning(CH_LOADER, "Couldn't unload \"%s\". FreeLibrary() call failed.", strFile.c_str()); + return; } - if (!aIsShutdown) + addon->Module = nullptr; + addon->ModuleSize = 0; + + addon->State = EAddonState::NotLoaded; + + if (!std::filesystem::exists(aPath)) { - SaveAddonConfig(); + auto it = std::find(Addons.begin(), Addons.end(), addon); + if (it != Addons.end()) + { + if (addon->Definitions) + { + AddonDefinition::Free(&addon->Definitions); + } + Addons.erase(it); + } } + + //LogDebug(CH_LOADER, "Called FreeLibrary() %d times on \"%s\".", freeCalls, strFile.c_str()); } + void UninstallAddon(const std::filesystem::path& aPath) { - UnloadAddon(aPath); - /* check both LoadedLOCKED, but also Loaded as a sanity check */ - auto it = Addons.find(aPath); + Addon* addon = FindAddonByPath(aPath); /* if it's still loaded due to being locked (or for some obscure other reason) try to move addon.dll to addon.dll.uninstall, so it will be deleted on next restart */ - if (it != Addons.end()) + if (addon) { - if (it->second->State == EAddonState::Loaded || it->second->State == EAddonState::LoadedLOCKED) + if (addon->State == EAddonState::Loaded || addon->State == EAddonState::LoadedLOCKED || addon->IsWaitingForUnload) { try { std::filesystem::rename(aPath, aPath.string() + extUninstall); - it->second->WillBeUninstalled = true; + addon->IsFlaggedForUninstall = true; LogWarning(CH_LOADER, "Addon is stilled loaded, it will be uninstalled the next time the game is restarted: %s", aPath.string().c_str()); } catch (std::filesystem::filesystem_error fErr) @@ -1007,43 +976,17 @@ namespace Loader try { std::filesystem::remove(aPath.string().c_str()); - Addons.erase(aPath); - LogInfo(CH_LOADER, "Uninstalled addon: %s", aPath.string().c_str()); - } - catch (std::filesystem::filesystem_error fErr) - { - LogDebug(CH_LOADER, "%s", fErr.what()); - return; - } - } - } - void ReloadAddon(const std::filesystem::path& aPath) - { - UnloadAddon(aPath); - LoadAddon(aPath, true); - } - void UpdateSwapAddon(const std::filesystem::path& aPath) - { - /* setup paths */ - std::filesystem::path pathOld = aPath.string() + extOld; - std::filesystem::path pathUpdate = aPath.string() + extUpdate; - - if (std::filesystem::exists(pathUpdate)) - { - try - { - if (std::filesystem::exists(pathOld)) - { - std::filesystem::remove(pathOld); - } - - std::filesystem::rename(aPath, pathOld); - std::filesystem::rename(pathUpdate, aPath); - if (std::filesystem::exists(pathOld)) + auto it = std::find(Addons.begin(), Addons.end(), addon); + if (it != Addons.end()) { - std::filesystem::remove(pathOld); + if (addon && addon->Definitions) + { + AddonDefinition::Free(&addon->Definitions); + } + Addons.erase(it); } + LogInfo(CH_LOADER, "Uninstalled addon: %s", aPath.string().c_str()); } catch (std::filesystem::filesystem_error fErr) { @@ -1052,21 +995,20 @@ namespace Loader } } } + bool UpdateAddon(const std::filesystem::path& aPath, signed int aSignature, std::string aName, AddonVersion aVersion, EUpdateProvider aProvider, std::string aUpdateLink) { /* setup paths */ std::filesystem::path pathOld = aPath.string() + extOld; std::filesystem::path pathUpdate = aPath.string() + extUpdate; - auto it = Addons.find(aPath); + Addon* addon = FindAddonByPath(aPath); - if (it == Addons.end()) + if (!addon) { return false; } - Addon* addon = (*it).second; - /* cleanup old files */ try { @@ -1081,6 +1023,7 @@ namespace Loader { if (addon->MD5 != MD5FromFile(pathUpdate)) { + UpdateSwapAddon(aPath); return true; } @@ -1164,7 +1107,7 @@ namespace Loader LogInfo(CH_LOADER, "%s is outdated: API replied with Version %s but installed is Version %s", aName.c_str(), remoteVersion.ToString().c_str(), aVersion.ToString().c_str()); RaidcoreAPI->Download(pathUpdate, endpoint + "/download"); // e.g. api.raidcore.gg/addons/17/download - + LogInfo(CH_LOADER, "Successfully updated %s.", aName.c_str()); wasUpdated = true; } @@ -1178,7 +1121,7 @@ namespace Loader LogWarning(CH_LOADER, "Error parsing API response."); return false; } - + response = response[0]; // filthy hack to get "latest" if (response["tag_name"].is_null()) @@ -1357,182 +1300,39 @@ namespace Loader return wasUpdated; } - void InstallAddon(LibraryAddon* aAddon) + bool UpdateSwapAddon(const std::filesystem::path& aPath) { - aAddon->IsInstalling = true; - - /* this is all modified duplicate code from update */ - std::string baseUrl; - std::string endpoint; - - // override provider if none set, but a Raidcore ID is used - if (aAddon->Provider == EUpdateProvider::None && aAddon->Signature > 0) - { - aAddon->Provider = EUpdateProvider::Raidcore; - } - - /* setup baseUrl and endpoint */ - switch (aAddon->Provider) - { - case EUpdateProvider::None: return; - - case EUpdateProvider::Raidcore: - baseUrl = API_RAIDCORE; - endpoint = "/addons/" + std::to_string(aAddon->Signature); - - break; - - case EUpdateProvider::GitHub: - baseUrl = API_GITHUB; - if (aAddon->DownloadURL.empty()) - { - LogWarning(CH_LOADER, "Addon %s declares EUpdateProvider::GitHub but has no UpdateLink set.", aAddon->Name); - return; - } - - endpoint = "/repos" + GetEndpoint(aAddon->DownloadURL) + "/releases"; // "/releases/latest"; // fuck you Sognus - - break; - - case EUpdateProvider::Direct: - if (aAddon->DownloadURL.empty()) - { - LogWarning(CH_LOADER, "Addon %s declares EUpdateProvider::Direct but has no UpdateLink set.", aAddon->Name); - return; - } - - baseUrl = GetBaseURL(aAddon->DownloadURL); - endpoint = GetEndpoint(aAddon->DownloadURL); - - if (baseUrl.empty() || endpoint.empty()) - { - return; - } - - break; - } + /* setup paths */ + std::filesystem::path pathOld = aPath.string() + extOld; + std::filesystem::path pathUpdate = aPath.string() + extUpdate; - if (EUpdateProvider::Raidcore == aAddon->Provider) - { - LogWarning(CH_LOADER, "Downloading via Raidcore is not implemented yet, due to user-friendly names requiring an API request. If you see this tell the developers about it! Thank you!"); - return; - //RaidcoreAPI->Download(addonPath, endpoint + "/download"); // e.g. api.raidcore.gg/addons/17/download - } - else if (EUpdateProvider::GitHub == aAddon->Provider) + if (std::filesystem::exists(pathUpdate)) { - json response = GitHubAPI->Get(endpoint); - - if (response.is_null()) - { - LogWarning(CH_LOADER, "Error parsing API response."); - return; - } - - response = response[0]; // filthy hack to get "latest" - - if (response["tag_name"].is_null()) - { - LogWarning(CH_LOADER, "No tag_name set on %s%s", baseUrl.c_str(), endpoint.c_str()); - return; - } - - std::string endpointDownload; // e.g. github.com/RaidcoreGG/GW2-CommandersToolkit/releases/download/20220918-135925/squadmanager.dll - - if (response["assets"].is_null()) - { - LogWarning(CH_LOADER, "Release has no assets. Cannot check against version. (%s%s)", baseUrl.c_str(), endpoint.c_str()); - return; - } - - for (auto& asset : response["assets"]) + try { - std::string assetName = asset["name"].get(); - - if (assetName.size() < 4) + if (std::filesystem::exists(pathOld)) { - continue; + std::filesystem::remove(pathOld); } - if (std::string_view(assetName).substr(assetName.size() - 4) == ".dll") + std::filesystem::rename(aPath, pathOld); + std::filesystem::rename(pathUpdate, aPath); + + if (std::filesystem::exists(pathOld)) { - asset["browser_download_url"].get_to(endpointDownload); + std::filesystem::remove(pathOld); } - } - - std::string downloadBaseUrl = GetBaseURL(endpointDownload); - endpointDownload = GetEndpoint(endpointDownload); - - httplib::Client downloadClient(downloadBaseUrl); - downloadClient.enable_server_certificate_verification(false); - downloadClient.set_follow_location(true); - - size_t lastSlashPos = endpointDownload.find_last_of('/'); - std::string filename = endpointDownload.substr(lastSlashPos + 1); - size_t dotDllPos = filename.find(extDll); - filename = filename.substr(0, filename.length() - extDll.length()); - std::filesystem::path probe = Path::D_GW2_ADDONS / (filename + extDll); - - int i = 0; - while (std::filesystem::exists(probe)) - { - probe = Path::D_GW2_ADDONS / (filename + "_" + std::to_string(i) + extDll); - i++; - } - - size_t bytesWritten = 0; - std::ofstream file(probe, std::ofstream::binary); - auto downloadResult = downloadClient.Get(endpointDownload, [&](const char* data, size_t data_length) { - file.write(data, data_length); - bytesWritten += data_length; return true; - }); - file.close(); - - if (!downloadResult || downloadResult->status != 200 || bytesWritten == 0) - { - LogWarning(CH_LOADER, "Error fetching %s%s", downloadBaseUrl.c_str(), endpointDownload.c_str()); - return; - } - } - else if (EUpdateProvider::Direct == aAddon->Provider) - { - /* prepare client request */ - httplib::Client client(baseUrl); - client.enable_server_certificate_verification(false); - - size_t lastSlashPos = endpoint.find_last_of('/'); - std::string filename = endpoint.substr(lastSlashPos + 1); - size_t dotDllPos = filename.find(extDll); - filename = filename.substr(0, filename.length() - extDll.length()); - - std::filesystem::path probe = Path::D_GW2_ADDONS / (filename + extDll); - - int i = 0; - while (std::filesystem::exists(probe)) - { - probe = Path::D_GW2_ADDONS / (filename + "_" + std::to_string(i) + extDll); - i++; } - - size_t bytesWritten = 0; - std::ofstream fileUpdate(probe, std::ofstream::binary); - auto downloadResult = client.Get(endpoint, [&](const char* data, size_t data_length) { - fileUpdate.write(data, data_length); - bytesWritten += data_length; - return true; - }); - fileUpdate.close(); - - if (!downloadResult || downloadResult->status != 200 || bytesWritten == 0) + catch (std::filesystem::filesystem_error fErr) { - LogWarning(CH_LOADER, "Error fetching %s%s", baseUrl.c_str(), endpoint.c_str()); - return; + LogDebug(CH_LOADER, "%s", fErr.what()); + return false; } } - LogInfo(CH_LOADER, "Successfully installed %s.", aAddon->Name.c_str()); - NotifyChanges(); + return false; } AddonAPI* GetAddonAPI(int aVersion) @@ -1730,42 +1530,13 @@ namespace Loader return sizeof(AddonAPI1); case 2: return sizeof(AddonAPI2); + case 3: + return sizeof(AddonAPI3); } return 0; } - void CopyAddonDefs(AddonDefinition* aDefinitions, AddonDefinition** aOutDefinitions) - { - if (aDefinitions == nullptr) - { - *aOutDefinitions = new AddonDefinition{}; - return; - } - - // Allocate new memory and copy data, copy strings - *aOutDefinitions = new AddonDefinition(*aDefinitions); - (*aOutDefinitions)->Name = _strdup(aDefinitions->Name); - (*aOutDefinitions)->Author = _strdup(aDefinitions->Author); - (*aOutDefinitions)->Description = _strdup(aDefinitions->Description); - (*aOutDefinitions)->UpdateLink = aDefinitions->UpdateLink ? _strdup(aDefinitions->UpdateLink) : nullptr; - } - void FreeAddonDefs(AddonDefinition** aDefinitions) - { - if (*aDefinitions == nullptr) { return; } - - free((char*)(*aDefinitions)->Name); - free((char*)(*aDefinitions)->Author); - free((char*)(*aDefinitions)->Description); - if ((*aDefinitions)->UpdateLink) - { - free((char*)(*aDefinitions)->UpdateLink); - } - delete *aDefinitions; - - *aDefinitions = nullptr; - } - std::string GetOwner(void* aAddress) { if (aAddress == nullptr) @@ -1775,7 +1546,7 @@ namespace Loader //const std::lock_guard lock(Mutex); { - for (auto& [path, addon] : Addons) + for (auto& addon : Addons) { if (!addon->Module) { continue; } @@ -1799,4 +1570,67 @@ namespace Loader return "(null)"; } + + Addon* FindAddonBySig(signed int aSignature) + { + auto it = std::find_if(Addons.begin(), Addons.end(), [aSignature](Addon* addon) { + // check if it has definitions and if the signature is the same + return addon->Definitions && addon->Definitions->Signature == aSignature; + }); + + if (it != Addons.end()) { + return *it; + } + + return nullptr; + } + Addon* FindAddonByPath(const std::filesystem::path& aPath) + { + auto it = std::find_if(Addons.begin(), Addons.end(), [aPath](Addon* addon) { + // check if it has definitions and if the signature is the same + return addon->Path == aPath; + }); + + if (it != Addons.end()) { + return *it; + } + + return nullptr; + } + Addon* FindAddonByMatchSig(signed int aMatchSignature) + { + auto it = std::find_if(Addons.begin(), Addons.end(), [aMatchSignature](Addon* addon) { + // check if it has definitions and if the signature is the same + return addon->MatchSignature == aMatchSignature; + }); + + if (it != Addons.end()) { + return *it; + } + + return nullptr; + } + + void SortAddons() + { + std::sort(Addons.begin(), Addons.end(), [](Addon* lhs, Addon* rhs) + { + if (lhs->Definitions && rhs->Definitions) + { + std::string lname = lhs->Definitions->Name; + std::transform(lname.begin(), lname.end(), lname.begin(), ::tolower); + std::string rname = rhs->Definitions->Name; + std::transform(rname.begin(), rname.end(), rname.begin(), ::tolower); + + return lname < rname; + } + + std::string lpath = lhs->Path.string(); + std::transform(lpath.begin(), lpath.end(), lpath.begin(), ::tolower); + std::string rpath = rhs->Path.string(); + std::transform(rpath.begin(), rpath.end(), rpath.begin(), ::tolower); + + return lpath < rpath; + }); + } } diff --git a/src/Loader/Loader.h b/src/Loader/Loader.h index 18ed2c9..16cbe08 100644 --- a/src/Loader/Loader.h +++ b/src/Loader/Loader.h @@ -1,3 +1,11 @@ +///---------------------------------------------------------------------------------------------------- +/// Copyright (c) Raidcore.GG - All rights reserved. +/// +/// Name : Loader.h +/// Description : Handles addon hot-loading, updates etc. +/// Authors : K. Bieniek +///---------------------------------------------------------------------------------------------------- + #ifndef LOADER_H #define LOADER_H @@ -10,27 +18,17 @@ #include #include "ELoaderAction.h" -#include "StoredAddon.h" -#include "LibraryAddon.h" #include "Addon.h" #include "AddonAPI.h" namespace Loader { extern std::mutex Mutex; - extern std::vector AddonLibrary; extern std::unordered_map< std::filesystem::path, ELoaderAction > QueuedAddons; /* To be loaded or unloaded addons */ - extern std::map< - signed int, - StoredAddon - > AddonConfig; - extern std::map< - std::filesystem::path, - Addon* - > Addons; /* Addons and their corresponding paths */ + extern std::vector Addons; extern std::map ApiDefs; /* Addon API definitions, created on demand */ extern int DirectoryChangeCountdown; @@ -42,59 +40,140 @@ namespace Loader extern PIDLIST_ABSOLUTE FSItemList; extern ULONG FSNotifierID; - /* Registers the addon directory update notifications and loads all addons. */ + ///---------------------------------------------------------------------------------------------------- + /// Initialize: + /// Registers the addon directory update notifications and loads all addons. + ///---------------------------------------------------------------------------------------------------- void Initialize(); - /* Deregisters the directory updates and unloads all addons. */ + + ///---------------------------------------------------------------------------------------------------- + /// Shutdown: + /// Deregisters the directory updates and calls unload on all addons. + ///---------------------------------------------------------------------------------------------------- void Shutdown(); - /* Load AddonConfig. */ + ///---------------------------------------------------------------------------------------------------- + /// LoadAddonConfig: + /// Load AddonConfig. + ///---------------------------------------------------------------------------------------------------- void LoadAddonConfig(); - /* Save AddonConfig. */ - void SaveAddonConfig(); - /* Fetch AddonLibrary. */ - void GetAddonLibrary(); + ///---------------------------------------------------------------------------------------------------- + /// SaveAddonConfig: + /// Save AddonConfig. + ///---------------------------------------------------------------------------------------------------- + void SaveAddonConfig(); - /* Returns 0 if message was processed. */ + ///---------------------------------------------------------------------------------------------------- + /// WndProc: + /// Returns 0 if message was processed. + ///---------------------------------------------------------------------------------------------------- UINT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); - /* Processes all currently queued addons. */ + ///---------------------------------------------------------------------------------------------------- + /// ProcessQueue: + /// Processes all currently queued addons. + ///---------------------------------------------------------------------------------------------------- void ProcessQueue(); - /* Pushes an item to the queue. */ + + ///---------------------------------------------------------------------------------------------------- + /// QueueAddon: + /// Pushes an item to the queue. + ///---------------------------------------------------------------------------------------------------- void QueueAddon(ELoaderAction aAction, const std::filesystem::path& aPath); - /* Notifies that something in the addon directory changed. */ + ///---------------------------------------------------------------------------------------------------- + /// NotifyChanges: + /// Notifies that something in the addon directory changed. + ///---------------------------------------------------------------------------------------------------- void NotifyChanges(); - /* Detects and processes any changes to addons. */ + + ///---------------------------------------------------------------------------------------------------- + /// ProcessChanges: + /// Detects and processes any changes to addons. + ///---------------------------------------------------------------------------------------------------- void ProcessChanges(); - /* Loads an addon. */ + ///---------------------------------------------------------------------------------------------------- + /// LoadAddon: + /// Loads an addon. + ///---------------------------------------------------------------------------------------------------- void LoadAddon(const std::filesystem::path& aPath, bool aIsReload = false); - /* Unloads an addon. */ - void UnloadAddon(const std::filesystem::path& aPath, bool aIsShutdown = false); - /* Unloads, then uninstalls an addon. */ + + ///---------------------------------------------------------------------------------------------------- + /// UnloadAddon: + /// Unloads an addon and performs a reload as soon as the addon returns, if requested. + ///---------------------------------------------------------------------------------------------------- + void UnloadAddon(const std::filesystem::path& aPath, bool aDoReload = false); + + ///---------------------------------------------------------------------------------------------------- + /// FreeAddon: + /// Calls FreeLibrary on the specified addon. + /// This function should not be invoked manually, but through Addon::Unload + Queue(Free). + ///---------------------------------------------------------------------------------------------------- + void FreeAddon(const std::filesystem::path& aPath); + + ///---------------------------------------------------------------------------------------------------- + /// UninstallAddon: + /// Uninstalls an addon, or moves it to addon.dll.uninstall to be cleaned up by the loader later. + /// This function should not be invoked manually, but through Unload + FollowUpAction::Uninstall. + ///---------------------------------------------------------------------------------------------------- void UninstallAddon(const std::filesystem::path& aPath); - /* Unloads, then loads an addon. */ - void ReloadAddon(const std::filesystem::path& aPath); - /* Swaps addon.dll with addon.dll.update. */ - void UpdateSwapAddon(const std::filesystem::path& aPath); - /* Updates an addon. */ + + ///---------------------------------------------------------------------------------------------------- + /// UpdateAddon: + /// Returns true if the addon updated. + ///---------------------------------------------------------------------------------------------------- bool UpdateAddon(const std::filesystem::path& aPath, signed int aSignature, std::string aName, AddonVersion aVersion, EUpdateProvider aProvider, std::string aUpdateLink); - /* Installs an addon. */ - void InstallAddon(LibraryAddon* aAddon); - /* Gets or creates a pointer to the provided version, or nullptr if no such version exists. */ + ///---------------------------------------------------------------------------------------------------- + /// UpdateSwapAddon: + /// Swaps addon.dll with addon.dll.update. + /// Returns true if there was an update dll. + ///---------------------------------------------------------------------------------------------------- + bool UpdateSwapAddon(const std::filesystem::path& aPath); + + ///---------------------------------------------------------------------------------------------------- + /// GetAddonAPI: + /// Gets or creates a pointer to the provided version, or nullptr if no such version exists. + ///---------------------------------------------------------------------------------------------------- AddonAPI* GetAddonAPI(int aVersion); - /* Returns the size of the provided version. */ - long GetAddonAPISize(int aVersion); - /* HELPER: Copies the addon definitions. */ - void CopyAddonDefs(AddonDefinition* aDefinitions, AddonDefinition** aOutDefinitions); - /* HELPER: Frees the addon definitions. */ - void FreeAddonDefs(AddonDefinition** aDefinitions); + ///---------------------------------------------------------------------------------------------------- + /// GetAddonAPISize: + /// Returns the size of the provided API version. + ///---------------------------------------------------------------------------------------------------- + long GetAddonAPISize(int aVersion); - /* HELPER: Returns the name of the owning addon. Similar to ::Verify. */ + ///---------------------------------------------------------------------------------------------------- + /// GetOwner: + /// Returns the name of the addon owning the provided address. + ///---------------------------------------------------------------------------------------------------- std::string GetOwner(void* aAddress); + + ///---------------------------------------------------------------------------------------------------- + /// FindAddonBySig: + /// Returns the addon with a matching signature or nullptr. + ///---------------------------------------------------------------------------------------------------- + Addon* FindAddonBySig(signed int aSignature); + + ///---------------------------------------------------------------------------------------------------- + /// FindAddonByPath: + /// Returns the addon with a matching path or nullptr. + ///---------------------------------------------------------------------------------------------------- + Addon* FindAddonByPath(const std::filesystem::path& aPath); + + ///---------------------------------------------------------------------------------------------------- + /// FindAddonByMatchSig: + /// Returns the addon with a matching mock signature or nullptr. + ///---------------------------------------------------------------------------------------------------- + Addon* FindAddonByMatchSig(signed int aMatchSignature); + + ///---------------------------------------------------------------------------------------------------- + /// SortAddons: + /// Sorts addons by name but prioritizes DUU state. + ///---------------------------------------------------------------------------------------------------- + void SortAddons(); } #endif diff --git a/src/Loader/StoredAddon.h b/src/Loader/StoredAddon.h deleted file mode 100644 index e3b707d..0000000 --- a/src/Loader/StoredAddon.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef STOREDADDON_H -#define STOREDADDON_H - -struct StoredAddon -{ - bool IsPausingUpdates; - bool IsLoaded; - bool IsDisabledUntilUpdate; -}; - -#endif diff --git a/src/Localization/Localization.h b/src/Localization/Localization.h index 40ce5a9..bce1405 100644 --- a/src/Localization/Localization.h +++ b/src/Localization/Localization.h @@ -14,6 +14,9 @@ #include #include +///---------------------------------------------------------------------------------------------------- +/// Localization Namespace +///---------------------------------------------------------------------------------------------------- namespace Localization { ///---------------------------------------------------------------------------------------------------- diff --git a/src/Main.cpp b/src/Main.cpp index d660d11..4881816 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -79,9 +79,43 @@ namespace Main Language.BuildLocaleAtlas(); //Paradigm::Initialize(); - std::thread([]() + std::thread update(SelfUpdate); + update.detach(); + + std::thread([]() { + try { - SelfUpdate(); + char computerName[MAX_COMPUTERNAME_LENGTH + 1]; + DWORD size = sizeof(computerName); + + if (!GetComputerName(computerName, &size)) { + return; + } + + httplib::Client client("https://www.google-analytics.com"); + + // Construct the JSON payload + json payload = { + {"client_id", computerName}, + {"user_id", computerName}, + {"events", json::array({ + { + {"name", "login"}, + {"params", json::object()} + } + })} + }; + + // Construct the URL query string + std::string query = "/mp/collect?measurement_id=G-DZHKTJ4DWS&api_secret=NbLP7I_0TkK7Amyf1iZBTw"; + + // Make a POST request with JSON payload + auto response = client.Post(query.c_str(), payload.dump(), "application/json"); + } + catch (...) + { + LogDebug(CH_CORE, "Beep Boop."); + } }) .detach(); diff --git a/src/Nexus.rc b/src/Nexus.rc index 0b42290..412f3f5 100644 --- a/src/Nexus.rc +++ b/src/Nexus.rc @@ -132,6 +132,8 @@ RES_TEX_ADDONITEM PNG "Resources\\AddonsWindow\\AddonItem.png" RES_TEX_ADDONS_TITLEBAR PNG "Resources\\AddonsWindow\\TitleBar.png" RES_TEX_ADDONS_TITLEBAR_HOVER PNG "Resources\\AddonsWindow\\TitleBar_Hover.png" +RES_ICON_WARNING PNG "Resources\\AddonsWindow\\TosWarning.png" + /* Generic */ RES_TEX_TABBTN PNG "Resources\\TabBtn.png" RES_TEX_TABBTN_HOVER PNG "Resources\\TabBtn_Hover.png" diff --git a/src/Nexus.vcxproj b/src/Nexus.vcxproj index 378a364..f7b7048 100644 --- a/src/Nexus.vcxproj +++ b/src/Nexus.vcxproj @@ -21,6 +21,7 @@ + @@ -134,10 +135,10 @@ + - @@ -329,7 +330,12 @@ + + + + + diff --git a/src/Nexus.vcxproj.filters b/src/Nexus.vcxproj.filters index f600ab1..8afb6d6 100644 --- a/src/Nexus.vcxproj.filters +++ b/src/Nexus.vcxproj.filters @@ -321,6 +321,9 @@ Loader + + Loader + @@ -1055,9 +1058,6 @@ Loader\Structs - - Loader\Structs - Localization @@ -1073,6 +1073,9 @@ Loader + + Loader + @@ -1089,6 +1092,21 @@ Resource Files\Locales + + Resource Files\Locales + + + Resource Files\Locales + + + Resource Files\Locales + + + Resource Files\Locales + + + Resource Files\Locales + diff --git a/src/Resources/AddonsWindow/TosWarning.png b/src/Resources/AddonsWindow/TosWarning.png new file mode 100644 index 0000000..09c9448 Binary files /dev/null and b/src/Resources/AddonsWindow/TosWarning.png differ diff --git a/src/Resources/Locales b/src/Resources/Locales index 112991b..0fd89d3 160000 --- a/src/Resources/Locales +++ b/src/Resources/Locales @@ -1 +1 @@ -Subproject commit 112991b6caaa180245410193f9ee984578f4bdc9 +Subproject commit 0fd89d34b3c8ec4b36a934bbb3e7eacd996c6bf0 diff --git a/src/core.cpp b/src/core.cpp index 9ddfe4a..8c3daf7 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -262,6 +262,27 @@ std::string GetQuery(const std::string& aEndpoint, const std::string& aParameter return rQuery; } +std::string Normalize(const std::string& aString) +{ + std::string ret; + + for (size_t i = 0; i < aString.length(); i++) + { + // alphanumeric + if ((aString[i] >= 48 && aString[i] <= 57) || + (aString[i] >= 65 && aString[i] <= 90) || + (aString[i] >= 97 && aString[i] <= 122)) + { + ret += aString[i]; + } + else if (aString[i] == 32) + { + ret.append("_"); + } + } + + return ret; +} namespace Base64 { diff --git a/src/core.h b/src/core.h index a403402..7cd9c30 100644 --- a/src/core.h +++ b/src/core.h @@ -31,6 +31,7 @@ EUpdateProvider GetProvider(const std::string& aUrl); std::string GetBaseURL(const std::string& aUrl); std::string GetEndpoint(const std::string& aUrl); std::string GetQuery(const std::string& aEndpoint, const std::string& aParameters); +std::string Normalize(const std::string& aString); namespace Base64 { diff --git a/src/resource.h b/src/resource.h index 4e24a4d..148e27e 100644 --- a/src/resource.h +++ b/src/resource.h @@ -41,6 +41,8 @@ #define RES_TEX_ADDONS_TITLEBAR 403 #define RES_TEX_ADDONS_TITLEBAR_HOVER 404 +#define RES_ICON_WARNING 405 + /* Generic */ #define RES_TEX_TABBTN 506 #define RES_TEX_TABBTN_HOVER 507