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;