From 950d22eb6e832df03e458e727fb13f4ffb97f10e Mon Sep 17 00:00:00 2001 From: IbrahimAlzaidi Date: Thu, 30 Apr 2026 16:40:19 +0200 Subject: [PATCH] Fix AntiCheat plugin resolution and DLL load diagnostics Fix the AntiCheat startup path so old or incomplete settings can no longer produce the invalid DLL path `plugins/.dll`. Previously, `NGMP_OnlineServicesManager::Init()` built the plugin DLL path inline from `Settings.GetAnticheatPlugin()`: plugins/{plugin}/{plugin}.dll If `plugins.anticheat` was missing, empty, or invalid in `settings.json`, the settings object could return an empty string and startup attempted to load: plugins/.dll That made the real failure hard to diagnose and also relied on process current working directory behavior for DLL loading. This change makes AntiCheat startup deterministic, self-diagnosing, and safer: - Add `AnticheatPluginResolver` - Centralizes AntiCheat plugin path resolution. - Defaults missing/empty settings to `easyanticheat`. - Resolves approved candidates from the game executable directory. - Restricts candidates to the `/plugins` directory. - Records selected path, tried paths, defaulting state, and diagnostic notes. - Produces both detailed log text and user-facing failure text. - Normalize AntiCheat settings - Add `GenOnlineSettings::DEFAULT_ANTICHEAT_PLUGIN`. - Default `m_Plugins_Anticheat` to `easyanticheat`. - Make `GetAnticheatPlugin()` return the default as a final safety net. - Normalize the value after loading settings. - Normalize again before saving so empty values are not persisted. - Track whether the setting was healed/defaulted so diagnostics can explain old or missing config files accurately. - Use resolver during online services startup - Ensure settings are initialized before resolving the plugin. - Replace inline `std::format("plugins/{}/{}.dll", ...)` path construction. - Log the complete AntiCheat resolution result. - Show a user-visible AntiCheat error if no approved plugin file is found. - Fail closed by quitting instead of allowing online play without AntiCheat. - Improve DLL loading - Change `AnticheatPlugInterface::LoadPlugin()` to return `bool`. - Add an optional failure-reason output string. - Load the plugin from the resolved absolute path with `LoadLibraryExA`. - Use `LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR` and `LOAD_LIBRARY_SEARCH_DEFAULT_DIRS` for controlled dependency search. - Capture `GetLastError()` and convert it with `FormatMessageA`. - Report missing required exports as explicit load failures. - Reset function pointers on failure. - Track the last load path and Win32 load error. - Unload any already-loaded plugin module before reloading. The user now gets actionable failures instead of the old generic `plugins/.dll` load error. Missing files show the exact approved paths checked. DLL load failures show the resolved plugin path plus the formatted Windows error, which helps distinguish missing plugin files from missing dependency DLLs, architecture mismatches, quarantined files, or service/install issues. Verified with: cmake --preset win32 \ -DRTS_BUILD_GENERALS=OFF \ -DRTS_BUILD_ZEROHOUR=ON \ -DRTS_BUILD_CORE_TOOLS=OFF \ -DRTS_BUILD_ZEROHOUR_TOOLS=OFF \ -DRTS_BUILD_CORE_EXTRAS=OFF \ -DRTS_BUILD_ZEROHOUR_EXTRAS=OFF \ -DRTS_INSTALL_PREFIX_ZEROHOUR="C:/sys/Zero Hour Standalone" cmake --build build/win32 --config Release --target z_generals --parallel 8 Build completed successfully: [1107/1107] Linking CXX executable GeneralsMD\Release\GeneralsOnlineZH.exe --- GeneralsMD/Code/GameEngine/CMakeLists.txt | 4 +- .../GeneralsOnline/AnticheatPluginResolver.h | 32 ++ .../GeneralsOnline/GeneralsOnline_Settings.h | 45 ++- .../GeneralsOnline/PluginInterfaces.h | 5 +- .../AnticheatPluginResolver.cpp | 313 ++++++++++++++++++ .../GeneralsOnline_Settings.cpp | 55 ++- .../GeneralsOnline/OnlineServices_Init.cpp | 77 ++++- .../GeneralsOnline/PluginInterfaces.cpp | 280 ++++++++++------ 8 files changed, 694 insertions(+), 117 deletions(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/AnticheatPluginResolver.h create mode 100644 GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/AnticheatPluginResolver.cpp diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 87347a32e1d..de0d089ce71 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -1153,6 +1153,7 @@ set(GAMEENGINE_SRC Include/GameNetwork/GeneralsOnline/OnlineServices_StatsInterface.h Include/GameNetwork/GeneralsOnline/OnlineServices_SocialInterface.h Include/GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.h + Include/GameNetwork/GeneralsOnline/AnticheatPluginResolver.h Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h Include/GameNetwork/GeneralsOnline/HTTP/HTTPManager.h Include/GameNetwork/GeneralsOnline/HTTP/HTTPRequest.h @@ -1184,6 +1185,7 @@ set(GAMEENGINE_SRC Include/GameNetwork/GeneralsOnline/Vendor/libcurl/websockets.h Source/GameNetwork/GeneralsOnline/NGMP_Helpers.cpp Source/GameNetwork/GeneralsOnline/NGMPGame.cpp + Source/GameNetwork/GeneralsOnline/AnticheatPluginResolver.cpp Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp Source/GameNetwork/GeneralsOnline/HTTP/HTTPManager.cpp Source/GameNetwork/GeneralsOnline/HTTP/HTTPRequest.cpp @@ -1250,4 +1252,4 @@ target_precompile_headers(z_gameengine PRIVATE add_compile_definitions(_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR) -add_compile_definitions(GENERALS_ONLINE) \ No newline at end of file +add_compile_definitions(GENERALS_ONLINE) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/AnticheatPluginResolver.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/AnticheatPluginResolver.h new file mode 100644 index 00000000000..000be591ab4 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/AnticheatPluginResolver.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +struct AnticheatPluginResolutionResult +{ + bool found = false; + bool usedDefault = false; + bool settingsUsedDefault = false; + + std::string configuredValue; + std::string normalizedPluginName; + + std::filesystem::path selectedPath; + std::vector triedPaths; + std::vector notes; + + std::string ToLogString() const; + std::string ToUserFacingString() const; +}; + +class AnticheatPluginResolver final +{ +public: + static constexpr const char* DefaultPluginName = "easyanticheat"; + + static AnticheatPluginResolutionResult Resolve( + const std::string& configuredValue, + bool settingsUsedDefault = false); +}; diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h index e0eebac3fab..70bb8385353 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h @@ -1,5 +1,7 @@ -#pragma once -#include "libcurl/curl.h" +#pragma once +#include "libcurl/curl.h" + +#include enum EHTTPVersion { @@ -12,8 +14,10 @@ enum EHTTPVersion class GenOnlineSettings { -public: - GenOnlineSettings(); +public: + GenOnlineSettings(); + + static constexpr const char* DEFAULT_ANTICHEAT_PLUGIN = "easyanticheat"; float Camera_MoveSpeedRatio() const { return m_Camera_MoveSpeedRatio; } float Camera_GetMinHeight() const { return m_Camera_MinHeight; } @@ -49,7 +53,33 @@ class GenOnlineSettings return m_Render_FramerateLimit_FPSVal; } - std::string GetAnticheatPlugin() const { return m_Plugins_Anticheat; } + void EnsureInitialized() + { + if (!m_bInitialized) + { + Initialize(); + } + } + + bool IsInitialized() const + { + return m_bInitialized; + } + + bool WasAnticheatPluginDefaulted() const + { + return m_bAnticheatPluginDefaulted; + } + + std::string GetAnticheatPlugin() const + { + if (m_Plugins_Anticheat.empty()) + { + return DEFAULT_ANTICHEAT_PLUGIN; + } + + return m_Plugins_Anticheat; + } bool Social_Notifications_FriendComesOnline_Menus() { return m_Social_Notification_FriendComesOnline_Menus; } bool Social_Notifications_FriendComesOnline_Gameplay() { return m_Social_Notification_FriendComesOnline_Gameplay; } @@ -122,7 +152,8 @@ class GenOnlineSettings bool m_bInitialized = false; - bool m_bVerbose = false; + bool m_bVerbose = false; + bool m_bAnticheatPluginDefaulted = false; bool m_Render_DrawStatsOverlay = true; bool m_Render_LimitFramerate = true; @@ -138,7 +169,7 @@ class GenOnlineSettings bool m_Social_Notification_PlayerSendsRequest_Menus = true; bool m_Social_Notification_PlayerSendsRequest_Gameplay = true; - std::string m_Plugins_Anticheat = std::string(); + std::string m_Plugins_Anticheat = DEFAULT_ANTICHEAT_PLUGIN; EHTTPVersion m_Network_HTTPVersion = EHTTPVersion::HTTP_VERSION_AUTO; bool m_Network_UseAlternativeEndpoint = false; diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h index 8c96db5f32f..2f0248bd1b9 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h @@ -1,5 +1,6 @@ #pragma once +#include enum class EAnticheatActionType : int32_t { @@ -41,7 +42,7 @@ class AnticheatPlugInterface static int GetAnticheatIdentifier(); - static void LoadPlugin(const char* szPluginName); + static bool LoadPlugin(const char* szPluginPath, std::string* outFailureReason = nullptr); static void Authenticate(); static void UnloadPlugin(); static void Tick(); @@ -111,6 +112,8 @@ class AnticheatPlugInterface // Module static HMODULE g_hACPluginModule; static bool m_bPluginLoadFailed; + static DWORD m_lastLoadError; + static std::string m_lastLoadPath; static int64_t m_tokenCreationTime; }; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/AnticheatPluginResolver.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/AnticheatPluginResolver.cpp new file mode 100644 index 00000000000..bda8bf2975f --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/AnticheatPluginResolver.cpp @@ -0,0 +1,313 @@ +#include "GameNetwork/GeneralsOnline/AnticheatPluginResolver.h" + +#include + +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +namespace +{ + std::string Trim(std::string value) + { + auto notSpace = [](unsigned char c) + { + return !std::isspace(c); + }; + + value.erase(value.begin(), std::find_if(value.begin(), value.end(), notSpace)); + value.erase(std::find_if(value.rbegin(), value.rend(), notSpace).base(), value.end()); + + return value; + } + + std::string ToLowerAscii(std::string value) + { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) + { + return static_cast(std::tolower(c)); + }); + + return value; + } + + bool ContainsPathSeparator(const std::string& value) + { + return value.find('\\') != std::string::npos || value.find('/') != std::string::npos; + } + + bool EndsWithDll(const std::string& value) + { + const std::string lower = ToLowerAscii(value); + constexpr const char* suffix = ".dll"; + constexpr size_t suffixLen = 4; + + return lower.size() >= suffixLen && + lower.compare(lower.size() - suffixLen, suffixLen, suffix) == 0; + } + + bool IsSafePluginName(const std::string& value) + { + if (value.empty()) + { + return false; + } + + for (unsigned char c : value) + { + const bool ok = std::isalnum(c) || c == '_' || c == '-'; + if (!ok) + { + return false; + } + } + + return true; + } + + fs::path GetExecutableDirectory() + { + std::vector buffer(MAX_PATH); + + for (;;) + { + const DWORD len = GetModuleFileNameA(nullptr, buffer.data(), static_cast(buffer.size())); + if (len == 0) + { + return fs::current_path(); + } + + if (len < buffer.size() - 1) + { + return fs::path(buffer.data()).parent_path(); + } + + buffer.resize(buffer.size() * 2); + } + } + + bool IsInsideDirectoryLexical(const fs::path& candidate, const fs::path& root) + { + const std::string candidateText = ToLowerAscii(candidate.lexically_normal().string()); + std::string rootText = ToLowerAscii(root.lexically_normal().string()); + + while (!rootText.empty() && (rootText.back() == '\\' || rootText.back() == '/')) + { + rootText.pop_back(); + } + + if (candidateText == rootText) + { + return true; + } + + return candidateText.rfind(rootText + "\\", 0) == 0 || + candidateText.rfind(rootText + "/", 0) == 0; + } + + void AddCandidate( + AnticheatPluginResolutionResult& result, + std::set& seen, + const fs::path& pluginsDir, + const fs::path& candidate, + const char* reason) + { + const fs::path normalized = candidate.lexically_normal(); + + if (!IsInsideDirectoryLexical(normalized, pluginsDir)) + { + result.notes.push_back( + std::string("Rejected candidate outside plugins directory: ") + + normalized.string()); + return; + } + + const std::string key = ToLowerAscii(normalized.string()); + if (seen.insert(key).second) + { + result.triedPaths.push_back(normalized); + result.notes.push_back( + std::string("Candidate added: ") + + normalized.string() + + " (" + + reason + + ")"); + } + } +} + +std::string AnticheatPluginResolutionResult::ToLogString() const +{ + std::ostringstream out; + + out << "[AC] Plugin resolution\n"; + out << " configuredValue='" << configuredValue << "'\n"; + out << " normalizedPluginName='" << normalizedPluginName << "'\n"; + out << " usedDefault=" << (usedDefault ? "true" : "false") << "\n"; + out << " settingsUsedDefault=" << (settingsUsedDefault ? "true" : "false") << "\n"; + out << " found=" << (found ? "true" : "false") << "\n"; + + if (!selectedPath.empty()) + { + out << " selectedPath='" << selectedPath.string() << "'\n"; + } + + out << " triedPaths:\n"; + for (const fs::path& path : triedPaths) + { + out << " - " << path.string() << "\n"; + } + + out << " notes:\n"; + for (const std::string& note : notes) + { + out << " - " << note << "\n"; + } + + return out.str(); +} + +std::string AnticheatPluginResolutionResult::ToUserFacingString() const +{ + std::ostringstream out; + + out << "GeneralsOnline could not prepare the AntiCheat plugin.\n\n"; + out << "What is wrong:\n"; + + if (settingsUsedDefault) + { + out << "The AntiCheat plugin setting was missing, empty, or invalid, so it was healed to easyanticheat.\n\n"; + } + else if (configuredValue.empty()) + { + out << "The AntiCheat plugin setting is empty.\n\n"; + } + else + { + out << "The configured AntiCheat plugin value is: " << configuredValue << "\n\n"; + } + + out << "Expected plugin:\n"; + out << "plugins\\easyanticheat\\easyanticheat.dll\n\n"; + + if (!triedPaths.empty()) + { + out << "Paths checked:\n"; + for (const fs::path& path : triedPaths) + { + out << "- " << path.string() << "\n"; + } + } + + return out.str(); +} + +AnticheatPluginResolutionResult AnticheatPluginResolver::Resolve( + const std::string& configuredValue, + bool settingsUsedDefault) +{ + AnticheatPluginResolutionResult result; + result.configuredValue = configuredValue; + result.settingsUsedDefault = settingsUsedDefault; + result.usedDefault = settingsUsedDefault; + + const fs::path exeDir = GetExecutableDirectory(); + const fs::path pluginsDir = exeDir / "plugins"; + std::set seen; + + const std::string raw = Trim(configuredValue); + const std::string lower = ToLowerAscii(raw); + + if (lower.empty()) + { + result.usedDefault = true; + result.normalizedPluginName = DefaultPluginName; + result.notes.push_back("Configured AntiCheat plugin was empty; defaulting to easyanticheat."); + } + else if (settingsUsedDefault) + { + result.normalizedPluginName = DefaultPluginName; + result.notes.push_back("Settings healed the configured AntiCheat plugin value to easyanticheat."); + } + else if (ContainsPathSeparator(lower) || EndsWithDll(lower)) + { + const fs::path rawPath(raw); + + if (rawPath.is_absolute()) + { + AddCandidate(result, seen, pluginsDir, rawPath, "absolute configured path"); + } + else if (EndsWithDll(lower) && !ContainsPathSeparator(lower)) + { + const fs::path dllName = rawPath.filename(); + const fs::path stem = dllName.stem(); + + AddCandidate(result, seen, pluginsDir, pluginsDir / dllName, "dll filename under plugins"); + AddCandidate(result, seen, pluginsDir, pluginsDir / stem / dllName, "dll filename under plugin subdirectory"); + } + else + { + AddCandidate(result, seen, pluginsDir, exeDir / rawPath, "relative configured path"); + } + + result.normalizedPluginName = rawPath.stem().string(); + } + else if (IsSafePluginName(lower)) + { + result.normalizedPluginName = lower; + } + else + { + result.usedDefault = true; + result.normalizedPluginName = DefaultPluginName; + result.notes.push_back( + "Configured AntiCheat plugin contained unsupported characters; defaulting to easyanticheat."); + } + + if (!result.normalizedPluginName.empty() && IsSafePluginName(result.normalizedPluginName)) + { + AddCandidate( + result, + seen, + pluginsDir, + pluginsDir / result.normalizedPluginName / (result.normalizedPluginName + ".dll"), + "canonical plugin-name layout"); + + AddCandidate( + result, + seen, + pluginsDir, + pluginsDir / (result.normalizedPluginName + ".dll"), + "flat plugin-name layout"); + } + + if (result.normalizedPluginName != DefaultPluginName) + { + AddCandidate( + result, + seen, + pluginsDir, + pluginsDir / DefaultPluginName / (std::string(DefaultPluginName) + ".dll"), + "current production fallback"); + } + + for (const fs::path& candidate : result.triedPaths) + { + std::error_code ec; + + if (fs::exists(candidate, ec) && fs::is_regular_file(candidate, ec)) + { + result.found = true; + result.selectedPath = candidate; + result.notes.push_back("Selected existing AntiCheat plugin: " + candidate.string()); + return result; + } + } + + result.notes.push_back("No AntiCheat plugin file was found in any approved candidate path."); + return result; +} diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp index 36007c05b6d..5aadc0d2007 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp @@ -3,6 +3,9 @@ #include "../OnlineServices_LobbyInterface.h" #include "../OnlineServices_Init.h" +#include +#include + #define SETTINGS_KEY_CAMERA "camera" #define SETTINGS_KEY_CAMERA_MIN_HEIGHT "min_height" #define SETTINGS_KEY_CAMERA_MOVE_SPEED_RATIO "move_speed_ratio" @@ -40,6 +43,40 @@ #define SETTINGS_FILENAME_LEGACY "GeneralsOnline_settings.json" #define SETTINGS_FILENAME "settings.json" +namespace +{ + std::string TrimSettingsValue(std::string value) + { + auto notSpace = [](unsigned char c) + { + return !std::isspace(c); + }; + + value.erase(value.begin(), std::find_if(value.begin(), value.end(), notSpace)); + value.erase(std::find_if(value.rbegin(), value.rend(), notSpace).base(), value.end()); + + return value; + } + + std::string NormalizeAnticheatPluginSetting(std::string value, bool* outUsedDefault = nullptr) + { + value = TrimSettingsValue(value); + const bool usedDefault = value.empty(); + + if (outUsedDefault != nullptr) + { + *outUsedDefault = usedDefault; + } + + if (usedDefault) + { + return GenOnlineSettings::DEFAULT_ANTICHEAT_PLUGIN; + } + + return value; + } +} + GenOnlineSettings::GenOnlineSettings() { @@ -249,15 +286,27 @@ void GenOnlineSettings::Load(void) } } + bool anticheatSettingPresent = false; + bool anticheatSettingNormalizedToDefault = false; + if (jsonSettings.contains(SETTINGS_KEY_PLUGINS)) { auto pluginSettings = jsonSettings[SETTINGS_KEY_PLUGINS]; if (pluginSettings.contains(SETTINGS_KEY_PLUGINS_ANTICHEAT)) { - m_Plugins_Anticheat = pluginSettings[SETTINGS_KEY_PLUGINS_ANTICHEAT]; + anticheatSettingPresent = true; + const auto& anticheatSetting = pluginSettings[SETTINGS_KEY_PLUGINS_ANTICHEAT]; + m_Plugins_Anticheat = anticheatSetting.is_string() + ? anticheatSetting.get() + : std::string(); } } + + m_Plugins_Anticheat = NormalizeAnticheatPluginSetting( + m_Plugins_Anticheat, + &anticheatSettingNormalizedToDefault); + m_bAnticheatPluginDefaulted = !anticheatSettingPresent || anticheatSettingNormalizedToDefault; } } @@ -275,6 +324,8 @@ void GenOnlineSettings::Load(void) m_Render_FramerateLimit_FPSVal = 60; m_Render_DrawStatsOverlay = true; m_Chat_LifeSeconds = 30; + m_Plugins_Anticheat = GenOnlineSettings::DEFAULT_ANTICHEAT_PLUGIN; + m_bAnticheatPluginDefaulted = true; m_Social_Notification_FriendComesOnline_Menus = true; m_Social_Notification_FriendComesOnline_Gameplay = true; @@ -293,6 +344,8 @@ void GenOnlineSettings::Save() Initialize(); } + m_Plugins_Anticheat = NormalizeAnticheatPluginSetting(m_Plugins_Anticheat); + nlohmann::json root = { { SETTINGS_KEY_CAMERA, diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index f9b7d756f4b..d0d7083a5d7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -1,4 +1,5 @@ #include "GameNetwork/GeneralsOnline/NGMP_interfaces.h" +#include "GameNetwork/GeneralsOnline/AnticheatPluginResolver.h" #include "GameNetwork/GeneralsOnline/HTTP/HTTPManager.h" #include "../json.hpp" #include "GameClient/MessageBox.h" @@ -38,6 +39,16 @@ std::vector NGMP_OnlineServicesManager::m_vecGuardedSSData; bool NGMP_OnlineServicesManager::g_bAdvancedNetworkStats; +namespace +{ + UnicodeString ToUnicodeString(const std::string& value) + { + UnicodeString out; + out.translate(AsciiString(value.c_str())); + return out; + } +} + NetworkMesh* NGMP_OnlineServicesManager::GetNetworkMesh() { if (m_pOnlineServicesManager != nullptr) @@ -831,14 +842,66 @@ void NGMP_OnlineServicesManager::Init() m_pHTTPManager = new HTTPManager(); m_pHTTPManager->Initialize(); - std::string strPlugin = NGMP_OnlineServicesManager::Settings.GetAnticheatPlugin(); - std::string pluginPath = std::format("plugins/{}/{}.dll", strPlugin.c_str(), strPlugin.c_str()); + NGMP_OnlineServicesManager::Settings.EnsureInitialized(); -#if _DEBUG - AnticheatPlugInterface::LoadPlugin(pluginPath.c_str()); -#else - AnticheatPlugInterface::LoadPlugin(pluginPath.c_str()); -#endif + const std::string configuredPlugin = + NGMP_OnlineServicesManager::Settings.GetAnticheatPlugin(); + + const AnticheatPluginResolutionResult acResolution = + AnticheatPluginResolver::Resolve( + configuredPlugin, + NGMP_OnlineServicesManager::Settings.WasAnticheatPluginDefaulted()); + + NetworkLog(ELogVerbosity::LOG_RELEASE, "%s", acResolution.ToLogString().c_str()); + + if (!acResolution.found) + { + const std::string message = acResolution.ToUserFacingString(); + + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] %s", message.c_str()); + + ClearGSMessageBoxes(); + MessageBoxOk( + UnicodeString(L"AntiCheat Error"), + ToUnicodeString(message), + []() + { + TheGameEngine->setQuitting(TRUE); + }); + + return; + } + + std::string loadFailure; + if (!AnticheatPlugInterface::LoadPlugin(acResolution.selectedPath.string().c_str(), &loadFailure)) + { + std::string message; + message += "GeneralsOnline found the AntiCheat plugin file, but Windows could not load it.\n\n"; + message += "Plugin path:\n"; + message += acResolution.selectedPath.string(); + message += "\n\n"; + message += "Windows/load error:\n"; + message += loadFailure; + message += "\n\n"; + message += "Common causes:\n"; + message += "- Missing dependency DLL next to the plugin\n"; + message += "- Antivirus quarantined part of the plugin\n"; + message += "- 32-bit / 64-bit DLL mismatch\n"; + message += "- EasyAntiCheat service is not installed or not running\n"; + + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] %s", message.c_str()); + + ClearGSMessageBoxes(); + MessageBoxOk( + UnicodeString(L"AntiCheat Error"), + ToUnicodeString(message), + []() + { + TheGameEngine->setQuitting(TRUE); + }); + + return; + } // TODO_NGMP: Better location // TODO_NGMP: Get all of this from the service diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp index 6441b6f9703..83c5a796513 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -4,13 +4,53 @@ #include "../OnlineServices_Init.h" #include "../OnlineServices_Auth.h" +#include + +namespace +{ + std::string FormatWin32ErrorMessage(DWORD errorCode) + { + LPSTR messageBuffer = nullptr; + + const DWORD size = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + errorCode, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&messageBuffer), + 0, + nullptr); + + std::string message; + + if (size > 0 && messageBuffer != nullptr) + { + message = messageBuffer; + LocalFree(messageBuffer); + } + else + { + message = "Unknown Windows error"; + } + + while (!message.empty() && + (message.back() == '\r' || message.back() == '\n' || message.back() == ' ')) + { + message.pop_back(); + } + + return std::format("{} ({})", message, errorCode); + } +} + #define AC_PLUGIN_LOAD_FUNCTION(funcName) \ - AnticheatPlugInterface::Functions.fn##funcName = (FuncDef##funcName)GetProcAddress(g_hACPluginModule, #funcName); \ + AnticheatPlugInterface::Functions.fn##funcName = \ + (FuncDef##funcName)GetProcAddress(g_hACPluginModule, #funcName); \ if (!AnticheatPlugInterface::Functions.fn##funcName) \ { \ - NetworkLog(ELogVerbosity::LOG_RELEASE, "Failed to find " #funcName " function", MB_OK); \ - FreeLibrary(g_hACPluginModule); \ - return; \ + return Fail(std::format("Plugin loaded, but required export '{}' was not found.", #funcName)); \ } bool AnticheatPlugInterface::IsExternalProcessRunning() @@ -33,129 +73,167 @@ int AnticheatPlugInterface::GetAnticheatIdentifier() return 0; } -void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) +bool AnticheatPlugInterface::LoadPlugin(const char* szPluginPath, std::string* outFailureReason) { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Attempting to load plugin from %s", szPluginName); + auto Fail = [&](const std::string& reason) -> bool + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Plugin load failed: %s", reason.c_str()); - m_bPluginLoadFailed = false; - g_hACPluginModule = LoadLibraryA(szPluginName); + if (outFailureReason != nullptr) + { + *outFailureReason = reason; + } - if (!g_hACPluginModule) - { - g_hACPluginModule = nullptr; + if (g_hACPluginModule != nullptr) + { + FreeLibrary(g_hACPluginModule); + g_hACPluginModule = nullptr; + } + + Functions = AnticheatPluginFunctionPtrs{}; m_bPluginLoadFailed = true; - DWORD err = GetLastError(); - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Failed to load %s (%u)", szPluginName, err); + return false; + }; + + if (g_hACPluginModule != nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Existing plugin module is loaded; unloading before reload."); + UnloadPlugin(); } - else + + m_bPluginLoadFailed = false; + m_lastLoadError = ERROR_SUCCESS; + m_lastLoadPath.clear(); + Functions = AnticheatPluginFunctionPtrs{}; + + if (szPluginPath == nullptr || szPluginPath[0] == '\0') { - // set logger - AC_PLUGIN_LOAD_FUNCTION(SetLoggingFunction); + return Fail("Resolved AntiCheat plugin path was empty."); + } - Functions.fnSetLoggingFunction([](const char* szMsg) - { - //MessageBoxA(nullptr, szMsg, szMsg, MB_OK); - NetworkLog(ELogVerbosity::LOG_RELEASE, szMsg); - }); + m_lastLoadPath = szPluginPath; - // Initialize AC - AC_PLUGIN_LOAD_FUNCTION(Initialize); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Attempting to load plugin from absolute path: %s", szPluginPath); - int result = Functions.fnInitialize(); - NetworkLog(ELogVerbosity::LOG_RELEASE, "Initialize result = %d", result); + g_hACPluginModule = LoadLibraryExA( + szPluginPath, + nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); - // check loaded - AC_PLUGIN_LOAD_FUNCTION(IsExternalProcessRunning); + if (!g_hACPluginModule) + { + m_lastLoadError = GetLastError(); - AC_PLUGIN_LOAD_FUNCTION(GetAnticheatIdentifier); + return Fail( + std::format( + "Windows failed to load '{}'. GetLastError={}", + szPluginPath, + FormatWin32ErrorMessage(m_lastLoadError))); + } -#if _DEBUG - SetWindowText(ApplicationHWnd, Functions.fnIsExternalProcessRunning() ? "SECURED" : "INSECURE"); -#endif + AC_PLUGIN_LOAD_FUNCTION(SetLoggingFunction); - // integrity callback - AC_PLUGIN_LOAD_FUNCTION(SetACIntegrityViolationOccurredCallback); + Functions.fnSetLoggingFunction([](const char* szMsg) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "%s", szMsg); + }); - Functions.fnSetACIntegrityViolationOccurredCallback([](const char* szReason, int violationType) - { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, local AC integrity violation occured (%d): %s.", violationType, szReason); - g_bPendingExitLobby = true; - }); + AC_PLUGIN_LOAD_FUNCTION(Initialize); - // set action required callback - AC_PLUGIN_LOAD_FUNCTION(SetACActionRequiredCallback); + const int result = Functions.fnInitialize(); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Initialize result = %d", result); - Functions.fnSetACActionRequiredCallback([](uint32_t userID, const char* szReason, EAnticheatActionType actionType, EAnticheatActionReason actionReason) - { - NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); + AC_PLUGIN_LOAD_FUNCTION(IsExternalProcessRunning); + AC_PLUGIN_LOAD_FUNCTION(GetAnticheatIdentifier); - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Action required."); +#if _DEBUG + SetWindowText(ApplicationHWnd, Functions.fnIsExternalProcessRunning() ? "SECURED" : "INSECURE"); +#endif - if (pAuthInterface == nullptr) - { - // no auth interface? bail out - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, no auth interface."); - g_bPendingExitLobby = true; - } - else - { - // If it's us, leave, if its someone else, d/c them - if (pAuthInterface->GetUserID() == userID) - { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, action was requested against local user."); - g_bPendingExitLobby = true; - } - else - { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Disconnecting remote user, lobby isn't secure, action was requested against remote user %u.", userID); + AC_PLUGIN_LOAD_FUNCTION(SetACIntegrityViolationOccurredCallback); - NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); - if (pMesh != nullptr) - { - pMesh->DisconnectUser(userID); - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Disconnected: %u.", userID); - } - else // no mesh, just back out - { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, actionable player was remote, but no mesh exists to take action."); - g_bPendingExitLobby = true; - } - } - } - }); + Functions.fnSetACIntegrityViolationOccurredCallback([](const char* szReason, int violationType) + { + NetworkLog( + ELogVerbosity::LOG_RELEASE, + "[AC] Leaving lobby, local AC integrity violation occurred (%d): %s.", + violationType, + szReason); + + g_bPendingExitLobby = true; + }); + + AC_PLUGIN_LOAD_FUNCTION(SetACActionRequiredCallback); + + Functions.fnSetACActionRequiredCallback([]( + uint32_t userID, + const char* szReason, + EAnticheatActionType actionType, + EAnticheatActionReason actionReason) + { + NGMP_OnlineServices_AuthInterface* pAuthInterface = + NGMP_OnlineServicesManager::GetInterface(); + + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Action required."); - // set transport callback - AC_PLUGIN_LOAD_FUNCTION(SetSendMessageViaTransportCallback); - Functions.fnSetSendMessageViaTransportCallback([](uint32_t goUserID, const void* pData, uint32_t dataLen) + if (pAuthInterface == nullptr) { - NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); - if (pMesh != nullptr) - { - pMesh->SendACPacket(goUserID, pData, dataLen); - } - }); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, no auth interface."); + g_bPendingExitLobby = true; + return; + } - // AC network message arrived callback - AC_PLUGIN_LOAD_FUNCTION(ACMessageArrivedViaTransport); + if (pAuthInterface->GetUserID() == userID) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, action requested against local user."); + g_bPendingExitLobby = true; + return; + } - // Login funcs - AC_PLUGIN_LOAD_FUNCTION(Login); - AC_PLUGIN_LOAD_FUNCTION(RefreshToken); - AC_PLUGIN_LOAD_FUNCTION(IsLoggedIn); - AC_PLUGIN_LOAD_FUNCTION(GetMiddlewareAuthToken); + NetworkLog( + ELogVerbosity::LOG_RELEASE, + "[AC] Disconnecting remote user %u, action requested by AntiCheat.", + userID); - // Begin and end session funcs - AC_PLUGIN_LOAD_FUNCTION(BeginSession); - AC_PLUGIN_LOAD_FUNCTION(EndSession); + NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); + if (pMesh != nullptr) + { + pMesh->DisconnectUser(userID); + } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, no mesh exists for remote action."); + g_bPendingExitLobby = true; + } + }); - // register player funcs - AC_PLUGIN_LOAD_FUNCTION(RegisterPlayer); - AC_PLUGIN_LOAD_FUNCTION(DeregisterPlayer); + AC_PLUGIN_LOAD_FUNCTION(SetSendMessageViaTransportCallback); - AC_PLUGIN_LOAD_FUNCTION(Tick); - AC_PLUGIN_LOAD_FUNCTION(Shutdown); - } + Functions.fnSetSendMessageViaTransportCallback([](uint32_t goUserID, const void* pData, uint32_t dataLen) + { + NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); + if (pMesh != nullptr) + { + pMesh->SendACPacket(goUserID, pData, dataLen); + } + }); + + AC_PLUGIN_LOAD_FUNCTION(ACMessageArrivedViaTransport); + AC_PLUGIN_LOAD_FUNCTION(Login); + AC_PLUGIN_LOAD_FUNCTION(RefreshToken); + AC_PLUGIN_LOAD_FUNCTION(IsLoggedIn); + AC_PLUGIN_LOAD_FUNCTION(GetMiddlewareAuthToken); + AC_PLUGIN_LOAD_FUNCTION(BeginSession); + AC_PLUGIN_LOAD_FUNCTION(EndSession); + AC_PLUGIN_LOAD_FUNCTION(RegisterPlayer); + AC_PLUGIN_LOAD_FUNCTION(DeregisterPlayer); + AC_PLUGIN_LOAD_FUNCTION(Tick); + AC_PLUGIN_LOAD_FUNCTION(Shutdown); + + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Plugin loaded successfully: %s", szPluginPath); + + return true; } bool AnticheatPlugInterface::g_bPendingExitLobby = false; @@ -252,6 +330,8 @@ AnticheatPlugInterface::AnticheatPluginFunctionPtrs AnticheatPlugInterface::Func HMODULE AnticheatPlugInterface::g_hACPluginModule = nullptr; bool AnticheatPlugInterface::m_bPluginLoadFailed = false; +DWORD AnticheatPlugInterface::m_lastLoadError = ERROR_SUCCESS; +std::string AnticheatPlugInterface::m_lastLoadPath; int64_t AnticheatPlugInterface::m_tokenCreationTime = -1;