From ce0baca28b39c17dcc1945ebe4b5bb70b898b1a7 Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:07:48 -0400 Subject: [PATCH 01/11] - First pass plugins --- .../Source/Common/System/AsciiString.cpp | 8 +- .../GameNetwork/GameSpy/MainMenuUtils.cpp | 8 +- .../GameSpy/Thread/GameResultsThread.cpp | 2 +- .../GameNetwork/GameSpy/Thread/PingThread.cpp | 2 +- Core/GameEngine/Source/GameNetwork/NAT.cpp | 2 +- .../Source/GameNetwork/NetworkUtil.cpp | 4 +- .../Source/Common/INI/INIWebpageURL.cpp | 2 +- .../GUI/GameWindowManagerScript.cpp | 2 +- GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 + .../GeneralsOnline/NGMP_interfaces.h | 2 + .../GameNetwork/GeneralsOnline/NetworkMesh.h | 10 +- .../GeneralsOnline/OnlineServices_Auth.h | 2 + .../OnlineServices_LobbyInterface.h | 1 + .../GeneralsOnline/PluginInterfaces.h | 90 +++++++++ .../Source/Common/INI/INIWebpageURL.cpp | 2 +- .../Common/System/SaveGame/GameState.cpp | 2 +- .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 2 +- .../GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp | 2 +- .../GUI/GameWindowManagerScript.cpp | 2 +- .../GeneralsOnline/NetworkMesh.cpp | 115 +++++++++-- .../GeneralsOnline/NextGenTransport.cpp | 27 +++ .../GeneralsOnline/OnlineServices_Auth.cpp | 35 ++++ .../GeneralsOnline/OnlineServices_Init.cpp | 8 + .../OnlineServices_LobbyInterface.cpp | 19 ++ .../GeneralsOnline/PluginInterfaces.cpp | 184 ++++++++++++++++++ 25 files changed, 492 insertions(+), 43 deletions(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h create mode 100644 GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp diff --git a/Core/GameEngine/Source/Common/System/AsciiString.cpp b/Core/GameEngine/Source/Common/System/AsciiString.cpp index 1f22650dc34..167d1d07cd7 100644 --- a/Core/GameEngine/Source/Common/System/AsciiString.cpp +++ b/Core/GameEngine/Source/Common/System/AsciiString.cpp @@ -70,7 +70,7 @@ inline char* skipNonSeps(char* p, const char* seps) //----------------------------------------------------------------------------- inline char* skipWhitespace(char* p) { - while (*p && isspace(*p)) + while (*p && isspace((unsigned char)*p)) ++p; return p; } @@ -78,7 +78,7 @@ inline char* skipWhitespace(char* p) //----------------------------------------------------------------------------- inline char* skipNonWhitespace(char* p) { - while (*p && !isspace(*p)) + while (*p && !isspace((unsigned char)*p)) ++p; return p; } @@ -330,7 +330,7 @@ void AsciiString::trimEnd() // Clip trailing white space from the string. const int len = strlen(peek()); int index = len; - while (index > 0 && isspace(getCharAt(index - 1))) + while (index > 0 && isspace((unsigned char)getCharAt(index - 1))) { --index; } @@ -378,7 +378,7 @@ void AsciiString::toLower() char* c = buf; while (c && *c) { - *c = tolower(*c); + *c = (char)tolower((unsigned char)*c); c++; } set(buf); diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp index 5bc1de83f46..3be5bee6c6c 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp @@ -649,15 +649,15 @@ static GHTTPBool overallStatsCallback( GHTTPRequest request, GHTTPResult result, message.nextToken(&totalLine, "\n"); message.nextToken(&winsLine, "\n"); message.nextToken(&lossesLine, "\n"); - while (totalLine.isNotEmpty() && !isdigit(totalLine.getCharAt(0))) + while (totalLine.isNotEmpty() && !isdigit((unsigned char)totalLine.getCharAt(0))) { totalLine = totalLine.str()+1; } - while (winsLine.isNotEmpty() && !isdigit(winsLine.getCharAt(0))) + while (winsLine.isNotEmpty() && !isdigit((unsigned char)winsLine.getCharAt(0))) { winsLine = winsLine.str()+1; } - while (lossesLine.isNotEmpty() && !isdigit(lossesLine.getCharAt(0))) + while (lossesLine.isNotEmpty() && !isdigit((unsigned char)lossesLine.getCharAt(0))) { lossesLine = lossesLine.str()+1; } @@ -877,7 +877,7 @@ void StartPatchCheck() #if defined(USE_TEST_ENV) || defined(USE_DEBUG_ON_LIVE_SERVER) bNeedsUpdate = false; #endif - + bNeedsUpdate = false; cantConnectBeforeOnline = !bSuccess; mustDownloadPatch = bNeedsUpdate; diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/Thread/GameResultsThread.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/Thread/GameResultsThread.cpp index 5af3d94fe06..573ba0f05ac 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/Thread/GameResultsThread.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/Thread/GameResultsThread.cpp @@ -224,7 +224,7 @@ void GameResultsThreadClass::Thread_Function() // resolve the hostname const char *hostnameBuffer = req.hostname.c_str(); UnsignedInt IP = 0xFFFFFFFF; - if (isdigit(hostnameBuffer[0])) + if (isdigit((unsigned char)hostnameBuffer[0])) { IP = inet_addr(hostnameBuffer); in_addr hostNode; diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/Thread/PingThread.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/Thread/PingThread.cpp index 8cd0d66a4ec..a42dded1445 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/Thread/PingThread.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/Thread/PingThread.cpp @@ -262,7 +262,7 @@ void PingThreadClass::Thread_Function() // resolve the hostname const char *hostnameBuffer = req.hostname.c_str(); UnsignedInt IP = 0xFFFFFFFF; - if (isdigit(hostnameBuffer[0])) + if (isdigit((unsigned char)hostnameBuffer[0])) { IP = inet_addr(hostnameBuffer); in_addr hostNode; diff --git a/Core/GameEngine/Source/GameNetwork/NAT.cpp b/Core/GameEngine/Source/GameNetwork/NAT.cpp index 28c5ccb3cd3..8715e958476 100644 --- a/Core/GameEngine/Source/GameNetwork/NAT.cpp +++ b/Core/GameEngine/Source/GameNetwork/NAT.cpp @@ -1167,7 +1167,7 @@ void NAT::sendMangledPortNumberToTarget(UnsignedShort mangledPort, GameSlot *tar void NAT::processGlobalMessage(Int slotNum, const char *options) { const char *ptr = options; // skip preceding whitespace. - while (isspace(*ptr)) { + while (isspace((unsigned char)*ptr)) { ++ptr; } DEBUG_LOG(("NAT::processGlobalMessage - got message from slot %d, message is \"%s\"", slotNum, ptr)); diff --git a/Core/GameEngine/Source/GameNetwork/NetworkUtil.cpp b/Core/GameEngine/Source/GameNetwork/NetworkUtil.cpp index b908c22b0f4..98e9f8966b1 100644 --- a/Core/GameEngine/Source/GameNetwork/NetworkUtil.cpp +++ b/Core/GameEngine/Source/GameNetwork/NetworkUtil.cpp @@ -80,7 +80,7 @@ void dumpBufferToLog(const void *vBuf, Int len, const char *fname, Int line) for (dumpindex2 = 0; dumpindex2 < numBytesThisLine; ++dumpindex2) { char c = buf[offset + dumpindex2]; - DEBUG_LOG_RAW(("%c", (isprint(c)?c:'.'))); + DEBUG_LOG_RAW(("%c", (isprint((unsigned char)c)?c:'.'))); } DEBUG_LOG_RAW(("\n")); } @@ -105,7 +105,7 @@ UnsignedInt ResolveIP(AsciiString host) } // String such as "127.0.0.1" - if (isdigit(host.getCharAt(0))) + if (isdigit((unsigned char)host.getCharAt(0))) { return ( ntohl(inet_addr(host.str())) ); } diff --git a/Generals/Code/GameEngine/Source/Common/INI/INIWebpageURL.cpp b/Generals/Code/GameEngine/Source/Common/INI/INIWebpageURL.cpp index 59388d9f854..13e88acd714 100644 --- a/Generals/Code/GameEngine/Source/Common/INI/INIWebpageURL.cpp +++ b/Generals/Code/GameEngine/Source/Common/INI/INIWebpageURL.cpp @@ -55,7 +55,7 @@ AsciiString encodeURL(AsciiString source) const char *ptr = source.str(); while (*ptr) { - if (isalnum(*ptr) || allowedChars.find(*ptr)) + if (isalnum((unsigned char)*ptr) || allowedChars.find(*ptr)) { target.concat(*ptr); } diff --git a/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp b/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp index 80f15c88c10..aa6efe67fee 100644 --- a/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp @@ -260,7 +260,7 @@ static void readUntilSemicolon( File *fp, char *buffer, int maxBufLen ) fp->read(buffer + i, 1); // make all whitespace characters spaces - if( isspace( buffer[ i ] ) ) + if( isspace( (unsigned char)buffer[ i ] ) ) { if( start == FALSE ) diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 5e6d97be8dc..87347a32e1d 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -1157,6 +1157,7 @@ set(GAMEENGINE_SRC Include/GameNetwork/GeneralsOnline/HTTP/HTTPManager.h Include/GameNetwork/GeneralsOnline/HTTP/HTTPRequest.h Include/GameNetwork/GeneralsOnline/NextGenTransport.h + Include/GameNetwork/GeneralsOnline/PluginInterfaces.h Include/GameNetwork/GeneralsOnline/Vendor/ValveNetworkingSockets/steam/isteamnetworkingmessages.h Include/GameNetwork/GeneralsOnline/Vendor/ValveNetworkingSockets/steam/isteamnetworkingsockets.h Include/GameNetwork/GeneralsOnline/Vendor/ValveNetworkingSockets/steam/isteamnetworkingutils.h @@ -1196,6 +1197,7 @@ set(GAMEENGINE_SRC Source/GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.cpp Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp Source/GameNetwork/GeneralsOnline/NetworkBitstream.cpp + Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp ) if(RTS_GAMEMEMORY_ENABLE) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_interfaces.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_interfaces.h index e63ce771a6e..509ad189ca1 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_interfaces.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NGMP_interfaces.h @@ -9,3 +9,5 @@ #include "GameNetwork/GeneralsOnline/OnlineServices_StatsInterface.h" #include "GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.h" #include "GameNetwork/GeneralsOnline/OnlineServices_SocialInterface.h" + +#include "GameNetwork/GeneralsOnline/PluginInterfaces.h" diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h index 2654c584e09..7a812794b84 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h @@ -43,6 +43,8 @@ class PlayerConnection int SendGamePacket(void* pBuffer, uint32_t totalDataSize); + void SendACPacket(const void* pData, uint32_t dataLen); + void UpdateLatencyHistogram(); bool IsIPV4(); @@ -91,6 +93,8 @@ class PlayerConnection int ComputeConnectionScore(); HSteamNetConnection m_hSteamConnection = k_HSteamNetConnection_Invalid; + + void LiteUpdateForAC(); }; struct LobbyMemberEntry; @@ -166,12 +170,10 @@ class NetworkMesh } - std::queue m_queueQueuedGamePackets; - - bool HasGamePacket(); - QueuedGamePacket RecvGamePacket(); int SendGamePacket(void* pBuffer, uint32_t totalDataSize, int64_t userID); + void SendACPacket(uint32_t userID, const void* pData, uint32_t dataLen); + void StartConnectionSignalling(int64_t remoteUserID, uint16_t preferredPort); void DisconnectUser(int64_t remoteUserID); void Disconnect(); diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Auth.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Auth.h index d8d757c9935..c0642314783 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Auth.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Auth.h @@ -19,6 +19,8 @@ class NGMP_OnlineServices_AuthInterface void GoToDetermineNetworkCaps(); + void SendMiddlewareToken(std::string strMWToken); + void BeginLogin(); void DoReAuth(); diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h index a712cbbd79c..0b04d50e524 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h @@ -27,6 +27,7 @@ struct LobbyMemberEntry : public NetworkMemberBase uint16_t m_SlotState = SlotState::SLOT_OPEN; std::string region; + std::string middlewareUserID; int latency = 0; bool IsHuman() const diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h new file mode 100644 index 00000000000..6a08fd51f50 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h @@ -0,0 +1,90 @@ +#pragma once + + +enum class EAnticheatActionType : int32_t +{ + NONE = 0, + KICK = 1 +}; + +enum class EAnticheatActionReason : int32_t +{ + Unknown = 0, + InternalError = 1, + InvalidMessage = 2, + AuthFailure = 3, + ACNotRunning = 4, + HeartbeatTimedOut = 5, + ClientViolation = 6, + BackendViolation = 7, + TempCooldown = 8, + TempBanned = 9, + PermaBanned = 10 +}; + + +static class AnticheatPlugInterface +{ +public: + static void AC_NetworkMessageArrived(uint32_t goUserID, void* pData, uint32_t dataLen); + + static bool IsPluginLoaded() + { + return g_hACPluginModule != nullptr; + } + + static void LoadPlugin(const char* szPluginName); + static void Authenticate(); + static void UnloadPlugin(); + static void Tick(); + + static bool RegisterPlayer(std::string mwUserID, uint32_t goUserID); + + static void BeginSession(); + static void EndSession(); + + // Callbacks from plugin + typedef void (*LoginCallback)(bool bSuccess); + typedef void (*LoggingFunc)(const char*); + typedef void (*FuncDefACPlayerActionRequiredCallbackFunc)(uint32_t, const char*, EAnticheatActionType, EAnticheatActionReason); + typedef void (*FuncDefSetACActionRequiredCallback)(FuncDefACPlayerActionRequiredCallbackFunc); + typedef void (*SendMessageViaTransportCallbackFunc)(uint32_t, const void*, uint32_t); + + // Func defs + typedef void (*FuncDefSetLoggingFunction)(LoggingFunc); + typedef int (*FuncDefInitialize)(void); + typedef bool (*FuncDefIsLoaded)(void); + + typedef void (*FuncDefSetSendMessageViaTransportCallback)(SendMessageViaTransportCallbackFunc); + typedef void (*FuncDefACMessageArrigedViaTransport)(uint32_t, void*, uint32_t); + typedef void (*FuncDefLogin)(const char* szGameToken, LoginCallback cb); + typedef bool (*FuncDefGetMiddlewareAuthToken)(char* buffer, size_t bufferSize); + typedef bool (*FuncDefIsLoggedIn)(void); + typedef void (*FuncDefBeginSession)(void); + typedef void (*FuncDefEndSession)(void); + typedef bool (*FuncDefRegisterPlayer)(const char* szMiddlewareUserID, uint32_t goUserID); + typedef void (*FuncDefTick)(void); + + struct AnticheatPluginFunctionPtrs + { + FuncDefSetLoggingFunction fnSetLoggingFunction = nullptr; + FuncDefInitialize fnInitialize = nullptr; + FuncDefIsLoaded fnIsLoaded = nullptr; + FuncDefSetACActionRequiredCallback fnSetACActionRequiredCallback = nullptr; + FuncDefSetSendMessageViaTransportCallback fnSetSendMessageViaTransportCallback = nullptr; + FuncDefACMessageArrigedViaTransport fnACMessageArrigedViaTransport = nullptr; + FuncDefLogin fnLogin = nullptr; + FuncDefGetMiddlewareAuthToken fnGetMiddlewareAuthToken = nullptr; + FuncDefIsLoggedIn fnIsLoggedIn = nullptr; + FuncDefBeginSession fnBeginSession = nullptr; + FuncDefEndSession fnEndSession = nullptr; + FuncDefRegisterPlayer fnRegisterPlayer = nullptr; + FuncDefTick fnTick = nullptr; + }; + static AnticheatPluginFunctionPtrs Functions; + + // Module + static HMODULE g_hACPluginModule; +}; + +extern HWND ApplicationHWnd; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/INI/INIWebpageURL.cpp b/GeneralsMD/Code/GameEngine/Source/Common/INI/INIWebpageURL.cpp index 17908288d94..e30fb5fbfc2 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/INI/INIWebpageURL.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/INI/INIWebpageURL.cpp @@ -55,7 +55,7 @@ AsciiString encodeURL(AsciiString source) const char *ptr = source.str(); while (*ptr) { - if (isalnum(*ptr) || allowedChars.find(*ptr)) + if (isalnum((unsigned char)*ptr) || allowedChars.find(*ptr)) { target.concat(*ptr); } diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp index 82883c2dbfb..60f6335c521 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/SaveGame/GameState.cpp @@ -442,7 +442,7 @@ AsciiString GameState::findNextSaveFilename( UnicodeString desc ) for (i = 0; i < desc.getLength(); ++i) { char c = (char)desc.getCharAt(i); - if (isalnum(c)) + if (isalnum((unsigned char)c)) adesc.concat(c); else adesc.concat('_'); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index bcd95eee9c4..0ede9bd6633 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -3542,7 +3542,7 @@ Bool handleGameSetupSlashCommands(UnicodeString uText) for (int i = 0; i < asciiVal.getLength(); ++i) { char thisChar = asciiVal.getCharAt(i); - if (!std::isdigit(thisChar)) + if (!std::isdigit((unsigned char)thisChar)) { bIsNumber = false; break; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp index ed9cf3dca96..49929b0e308 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLWelcomeMenu.cpp @@ -325,7 +325,7 @@ static const char* FindNextNumber( const char* pStart ) if( !pNum ) return pStart; //error - while( !isdigit(*pNum) ) + while( !isdigit((unsigned char)*pNum) ) ++pNum; //go to next number return pNum; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp index bf21260c6a2..d588e90cc31 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GameWindowManagerScript.cpp @@ -261,7 +261,7 @@ static void readUntilSemicolon( File *fp, char *buffer, int maxBufLen ) fp->read(buffer + i, 1); // make all whitespace characters spaces - if( isspace( buffer[ i ] ) ) + if( isspace( (unsigned char)buffer[ i ] ) ) { if( start == FALSE ) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index e6fa595230a..7706694fff1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -12,6 +12,7 @@ #include "../OnlineServices_Init.h" #include "ValveNetworkingSockets/steam/isteamnetworkingutils.h" #include "ValveNetworkingSockets/steam/steamnetworkingcustomsignaling.h" +#include "../PluginInterfaces.h" bool g_bForceRelay = false; UnsignedInt m_exeCRCOriginal = 0; @@ -733,24 +734,6 @@ void NetworkMesh::UpdateConnectivity(PlayerConnection* connection) }); } - -bool NetworkMesh::HasGamePacket() -{ - return !m_queueQueuedGamePackets.empty(); -} - -QueuedGamePacket NetworkMesh::RecvGamePacket() -{ - if (HasGamePacket()) - { - QueuedGamePacket frontPacket = m_queueQueuedGamePackets.front(); - m_queueQueuedGamePackets.pop(); - return frontPacket; - } - - return QueuedGamePacket(); -} - int NetworkMesh::SendGamePacket(void* pBuffer, uint32_t totalDataSize, int64_t user_id) { auto it = m_mapConnections.find(user_id); @@ -763,6 +746,14 @@ int NetworkMesh::SendGamePacket(void* pBuffer, uint32_t totalDataSize, int64_t u } +void NetworkMesh::SendACPacket(uint32_t userID, const void* pData, uint32_t dataLen) +{ + if (m_mapConnections.contains(userID)) + { + m_mapConnections[userID].SendACPacket(pData, dataLen); + } +} + void NetworkMesh::StartConnectionSignalling(int64_t remoteUserID, uint16_t preferredPort) { // if we already have a connection to this use, drop it, having a single-direction connection will break signalling @@ -973,8 +964,75 @@ void NetworkMesh::Tick() PlayerConnection& conn = kvPair.second; conn.UpdateLatencyHistogram(); } + + // the game transport isn't created until the game begins, but we want to transfer AC packets in the lobby first, so consider this a liteupdate + if (TheNGMPGame != nullptr && !TheNGMPGame->isGameInProgress()) + { + for (auto& kvPair : m_mapConnections) + { + kvPair.second.LiteUpdateForAC(); + } + } } +void PlayerConnection::LiteUpdateForAC() +{ + SteamNetworkingMessage_t* pMsg[255] = { nullptr }; + int numPackets = Recv(pMsg); + + if (numPackets <= 0) + return; + + if (numPackets > static_cast(std::size(pMsg))) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, + "Game Packet Recv: numPackets (%d) > pMsg capacity (%zu), clamping", + numPackets, std::size(pMsg)); + numPackets = static_cast(std::size(pMsg)); + } + + for (int iPacket = 0; iPacket < numPackets; ++iPacket) + { + SteamNetworkingMessage_t* msg = pMsg[iPacket]; + if (!msg) + { + return; + } + + const uint32_t numBytes = msg->m_cbSize; + + // is it an AC packet? + std::vector vecData; + vecData.resize(numBytes); + memcpy(vecData.data(), msg->GetData(), numBytes); + + BYTE b1 = (BYTE)vecData[0]; + BYTE b2 = (BYTE)vecData[1]; + BYTE b3 = (BYTE)vecData[2]; + if (b1 == 9 + && b2 == 1 + && b3 == 2) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Received AC message of size %u from user %lld", numBytes, static_cast(m_userID)); + + + // remove header + // TODO_AC: Optimize this + std::vector vecDataAC; + vecDataAC.resize(numBytes - 3); + memcpy(vecDataAC.data(), (char*)msg->GetData() + 3, numBytes - 3); + + AnticheatPlugInterface::AC_NetworkMessageArrived(m_userID, vecDataAC.data(), numBytes - 3); + } + else + { + // not an AC packet, we dont care + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Received NON AC message"); + } + + msg->Release(); + } +} PlayerConnection::PlayerConnection(int64_t userID, HSteamNetConnection hSteamConnection) { @@ -1037,6 +1095,25 @@ int PlayerConnection::SendGamePacket(void* pBuffer, uint32_t totalDataSize) } +void PlayerConnection::SendACPacket(const void* pData, uint32_t dataLen) +{ + std::vector vecData; + vecData.resize(dataLen + 3); + memcpy(vecData.data() + 3, pData, dataLen); + + vecData[0] = 9; + vecData[1] = 1; + vecData[2] = 2; + + NetworkLog(ELogVerbosity::LOG_DEBUG, "[AC PACKET] Sending AC msg of size %ld to user %ld\n", dataLen, m_userID); + EResult r = SteamNetworkingSockets()->SendMessageToConnection(m_hSteamConnection, vecData.data(), vecData.size(), k_nSteamNetworkingSend_Reliable | k_nSteamNetworkingSend_AutoRestartBrokenSession, nullptr); + + if (r != k_EResultOK) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Failed to send, err code was %d", r); + } +} + void PlayerConnection::UpdateLatencyHistogram() { int histogram_duration = 20000; @@ -1209,7 +1286,7 @@ void PlayerConnection::SetDisconnected(bool bWasError, NetworkMesh* pOwningMesh, UpdateState(m_State, pOwningMesh); // may erase *this from the map } - // Use saved stack values — do NOT touch any member after this point. + // Use saved stack values — do NOT touch any member after this point. NetworkLog(ELogVerbosity::LOG_RELEASE, "[STEAM CONNECTION] Setting connection %u to disconnected/invalid on user %lld", savedHandle, savedUserID); if (SteamNetworkingSockets()) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp index e8743a4da3d..4d8d5014737 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp @@ -6,6 +6,8 @@ #include "GameNetwork/GeneralsOnline/ngmp_include.h" #include "GameNetwork/GeneralsOnline/ngmp_interfaces.h" +#include "ValveNetworkingSockets/steam/steamnetworkingtypes.h" +#include "GameNetwork/GeneralsOnline/PluginInterfaces.h" #ifdef _INTERNAL // for occasional debugging... @@ -109,6 +111,31 @@ Bool NextGenTransport::doRecv(void) const uint32_t numBytes = msg->m_cbSize; + // is it an AC packet? + std::vector vecData; + vecData.resize(numBytes); + memcpy(vecData.data(), msg->GetData(), numBytes); + + BYTE b1 = (BYTE)vecData[0]; + BYTE b2 = (BYTE)vecData[1]; + BYTE b3 = (BYTE)vecData[2]; + if (b1 == 9 + && b2 == 1 + && b3 == 2) + { + NetworkLog(ELogVerbosity::LOG_RELEASE,"[AC PACKET] Received AC message of size %u from user %lld", numBytes, static_cast(kvPair.second.m_userID)); + + + // remove header + // TODO_AC: Optimize this + std::vector vecDataAC; + vecDataAC.resize(numBytes - 3); + memcpy(vecDataAC.data(), (char*)msg->GetData() + 3, numBytes - 3); + + AnticheatPlugInterface::AC_NetworkMessageArrived(kvPair.second.m_userID, vecDataAC.data(), numBytes - 3); + continue; + } + NetworkLog(ELogVerbosity::LOG_DEBUG, "[GAME PACKET] Received message of size %u from user %lld", numBytes, static_cast(kvPair.second.m_userID)); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp index 32ad81664c1..414322fb086 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -139,6 +139,37 @@ void NGMP_OnlineServices_AuthInterface::GoToDetermineNetworkCaps() }); } +void NGMP_OnlineServices_AuthInterface::SendMiddlewareToken(std::string strMWToken) +{ + // TODO_AC: Service should just return 200 if no middleware is loaded + std::string strLoginURI = NGMP_OnlineServicesManager::GetAPIEndpoint("ProvideMWToken"); + + // login + std::map mapHeaders; + + nlohmann::json j; + j["mw_token"] = strMWToken; + std::string strPostData = j.dump(); + + NGMP_OnlineServicesManager::GetInstance()->GetHTTPManager()->SendPOSTRequest(strLoginURI.c_str(), EIPProtocolVersion::DONT_CARE, mapHeaders, strPostData.c_str(), [=](bool bSuccess, int statusCode, std::string strBody, HTTPRequest* pReq) + { + if (statusCode >= 400 && statusCode < 500) + { + ClearGSMessageBoxes(); + GSMessageBoxOk(UnicodeString(L"Middleware Login Failed"), UnicodeString(L"Middleware Login Failed"), []() + { + TheShell->pop(); + }); + return; + } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] MW LOGIN: Logged in"); + } + + }, nullptr); +} + void NGMP_OnlineServices_AuthInterface::BeginLogin() { std::string strLoginURI = NGMP_OnlineServicesManager::GetAPIEndpoint("LoginWithToken"); @@ -383,6 +414,10 @@ void NGMP_OnlineServices_AuthInterface::OnLoginComplete(ELoginResult loginResult { if (loginResult == ELoginResult::Success) { + // TODO_AC: Consider chaining this + // login to AC + AnticheatPlugInterface::Authenticate(); + NGMP_OnlineServicesManager::GetInstance()->OnLogin(loginResult, szWSAddr, [=]() // wait for WS to connect { // move on to network capabilities section diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index 3530de6fcd1..9179cd1d355 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -829,6 +829,12 @@ void NGMP_OnlineServicesManager::Init() m_pHTTPManager = new HTTPManager(); m_pHTTPManager->Initialize(); +#if _DEBUG + AnticheatPlugInterface::LoadPlugin("F:\\gen\\eac_module\\build\\Debug\\plugin.dll"); +#else + AnticheatPlugInterface::LoadPlugin("plugin.dll"); +#endif + // TODO_NGMP: Better location // TODO_NGMP: Get all of this from the service int moneyVal = 100000; @@ -863,6 +869,8 @@ void NGMP_OnlineServicesManager::Init() void NGMP_OnlineServicesManager::Tick() { + AnticheatPlugInterface::Tick(); + // screenshots { // send screenshot diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index faa825d863f..7568c5955b0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -762,6 +762,8 @@ void NGMP_OnlineServices_LobbyInterface::ApplyLocalUserPropertiesToCurrentNetwor } } +// TODO_AC: Do this elsewhere +std::set g_AlreadyREgistered; void NGMP_OnlineServices_LobbyInterface::UpdateRoomDataCache(std::function fnCallback) { // refresh lobby @@ -901,6 +903,20 @@ void NGMP_OnlineServices_LobbyInterface::UpdateRoomDataCache(std::function fnCallback) { + // begin AC + AnticheatPlugInterface::BeginSession(); + // join the network mesh too if (m_pLobbyMesh == nullptr) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp new file mode 100644 index 00000000000..694385ce574 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -0,0 +1,184 @@ +#include "GameNetwork/GeneralsOnline/PluginInterfaces.h" +#include "../NGMP_include.h" +#include "../NetworkMesh.h" +#include "../OnlineServices_Init.h" +#include "../OnlineServices_Auth.h" + +#define AC_PLUGIN_LOAD_FUNCTION(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; \ + } + +void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) +{ + g_hACPluginModule = LoadLibraryA(szPluginName); + + if (!g_hACPluginModule) + { + DWORD err = GetLastError(); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Failed to load %s (%u)", szPluginName, err); + } + else + { + // set logger + AC_PLUGIN_LOAD_FUNCTION(SetLoggingFunction); + + Functions.fnSetLoggingFunction([](const char* szMsg) + { + //MessageBoxA(nullptr, szMsg, szMsg, MB_OK); + NetworkLog(ELogVerbosity::LOG_RELEASE, szMsg); + }); + + // Initialize AC + AC_PLUGIN_LOAD_FUNCTION(Initialize); + + int result = Functions.fnInitialize(); + NetworkLog(ELogVerbosity::LOG_RELEASE, "Initialize result = %d", result); + + // check loaded + AC_PLUGIN_LOAD_FUNCTION(IsLoaded); + +#if _DEBUG + SetWindowText(ApplicationHWnd, Functions.fnIsLoaded() ? "SECURED" : "INSECURE"); +#endif + + // set action required callback + AC_PLUGIN_LOAD_FUNCTION(SetACActionRequiredCallback); + + Functions.fnSetACActionRequiredCallback([](uint32_t userID, const char* szReason, EAnticheatActionType actionType, EAnticheatActionReason actionReason) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure."); + extern void PopBackToLobby(); + PopBackToLobby(); + }); + + // set transport callback + AC_PLUGIN_LOAD_FUNCTION(SetSendMessageViaTransportCallback); + 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 network message arrived callback + AC_PLUGIN_LOAD_FUNCTION(ACMessageArrigedViaTransport); + + // Login func + AC_PLUGIN_LOAD_FUNCTION(Login); + AC_PLUGIN_LOAD_FUNCTION(IsLoggedIn); + AC_PLUGIN_LOAD_FUNCTION(GetMiddlewareAuthToken); + + // Begin and end session funcs + AC_PLUGIN_LOAD_FUNCTION(BeginSession); + AC_PLUGIN_LOAD_FUNCTION(EndSession); + + // register player funcs + AC_PLUGIN_LOAD_FUNCTION(RegisterPlayer); + + // TODO_AC: Deregister player + } +} + +void AnticheatPlugInterface::AC_NetworkMessageArrived(uint32_t goUserID, void* pData, uint32_t dataLen) +{ + // TODO: Cache all of these getprocaddresses + if (IsPluginLoaded() && Functions.fnACMessageArrigedViaTransport != nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] fnOnMessageArrivedViaTransport"); + Functions.fnACMessageArrigedViaTransport(goUserID, pData, dataLen); + } +} + + +void AnticheatPlugInterface::Authenticate() +{ + if (IsPluginLoaded() && Functions.fnLogin != nullptr && Functions.fnIsLoggedIn) + { + NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); + if (pAuthInterface == nullptr) + { + return; + } + + Functions.fnLogin(pAuthInterface->GetAuthToken().c_str(), + [](bool bSuccess) + { + if (Functions.fnIsLoggedIn()) + { + char buf[4196]; + if (Functions.fnGetMiddlewareAuthToken(buf, sizeof(buf))) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Got MW token: %s", buf); + + // Now we can begin login + NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); + if (pAuthInterface == nullptr) + { + return; + } + + pAuthInterface->SendMiddlewareToken(std::string(buf)); + } + } + }); + } +} + +void AnticheatPlugInterface::BeginSession() +{ + if (IsPluginLoaded() && Functions.fnBeginSession != nullptr) + { + Functions.fnBeginSession; + } +} + +void AnticheatPlugInterface::EndSession() +{ + if (IsPluginLoaded() && Functions.fnEndSession != nullptr) + { + Functions.fnEndSession; + } +} + +AnticheatPlugInterface::AnticheatPluginFunctionPtrs AnticheatPlugInterface::Functions; + +HMODULE AnticheatPlugInterface::g_hACPluginModule = nullptr; + +bool AnticheatPlugInterface::RegisterPlayer(std::string mwUserID, uint32_t goUserID) +{ + if (IsPluginLoaded() && Functions.fnRegisterPlayer != nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "RegisterPlayer: %s to %" PRIu64, mwUserID.c_str(), goUserID); + + bool bReg = Functions.fnRegisterPlayer(mwUserID.c_str(), goUserID); + NetworkLog(ELogVerbosity::LOG_RELEASE, "RegisterPlayerFunc result: %d", bReg); + return bReg; + } + + return false; +} + + +void AnticheatPlugInterface::Tick() +{ + if (IsPluginLoaded() && Functions.fnTick != nullptr) + { + Functions.fnTick(); + } +} + +void AnticheatPlugInterface::UnloadPlugin() +{ + if (IsPluginLoaded()) + { + FreeLibrary(g_hACPluginModule); + g_hACPluginModule = nullptr; + } +} From ab827696b12c63de0244efab6dd4834d10acf75c Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:59:15 -0400 Subject: [PATCH 02/11] - Fix cb name --- .../Include/GameNetwork/GeneralsOnline/PluginInterfaces.h | 4 ++-- .../Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h index 6a08fd51f50..8a9c100fd66 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h @@ -56,7 +56,7 @@ static class AnticheatPlugInterface typedef bool (*FuncDefIsLoaded)(void); typedef void (*FuncDefSetSendMessageViaTransportCallback)(SendMessageViaTransportCallbackFunc); - typedef void (*FuncDefACMessageArrigedViaTransport)(uint32_t, void*, uint32_t); + typedef void (*FuncDefACMessageArrivedViaTransport)(uint32_t, void*, uint32_t); typedef void (*FuncDefLogin)(const char* szGameToken, LoginCallback cb); typedef bool (*FuncDefGetMiddlewareAuthToken)(char* buffer, size_t bufferSize); typedef bool (*FuncDefIsLoggedIn)(void); @@ -72,7 +72,7 @@ static class AnticheatPlugInterface FuncDefIsLoaded fnIsLoaded = nullptr; FuncDefSetACActionRequiredCallback fnSetACActionRequiredCallback = nullptr; FuncDefSetSendMessageViaTransportCallback fnSetSendMessageViaTransportCallback = nullptr; - FuncDefACMessageArrigedViaTransport fnACMessageArrigedViaTransport = nullptr; + FuncDefACMessageArrivedViaTransport fnACMessageArrivedViaTransport = nullptr; FuncDefLogin fnLogin = nullptr; FuncDefGetMiddlewareAuthToken fnGetMiddlewareAuthToken = nullptr; FuncDefIsLoggedIn fnIsLoggedIn = nullptr; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp index 694385ce574..59d4e1f6107 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -68,7 +68,7 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) }); // AC network message arrived callback - AC_PLUGIN_LOAD_FUNCTION(ACMessageArrigedViaTransport); + AC_PLUGIN_LOAD_FUNCTION(ACMessageArrivedViaTransport); // Login func AC_PLUGIN_LOAD_FUNCTION(Login); @@ -89,10 +89,10 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) void AnticheatPlugInterface::AC_NetworkMessageArrived(uint32_t goUserID, void* pData, uint32_t dataLen) { // TODO: Cache all of these getprocaddresses - if (IsPluginLoaded() && Functions.fnACMessageArrigedViaTransport != nullptr) + if (IsPluginLoaded() && Functions.fnACMessageArrivedViaTransport != nullptr) { NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] fnOnMessageArrivedViaTransport"); - Functions.fnACMessageArrigedViaTransport(goUserID, pData, dataLen); + Functions.fnACMessageArrivedViaTransport(goUserID, pData, dataLen); } } From e757f01c51e26f4c4b6b4fa741e7632603486f6d Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:21:24 -0400 Subject: [PATCH 03/11] - Plugin loading - Improvements to kick handling --- .../GameNetwork/GameSpy/MainMenuUtils.cpp | 34 ++++- .../GeneralsOnline/GeneralsOnline_Settings.h | 4 + .../GeneralsOnline/OnlineServices_Init.h | 4 +- .../OnlineServices_LobbyInterface.h | 3 +- .../GeneralsOnline/PluginInterfaces.h | 26 +++- .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 10 ++ .../GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp | 4 + .../GeneralsOnline_Settings.cpp | 20 +++ .../GeneralsOnline/OnlineServices_Init.cpp | 9 +- .../OnlineServices_LobbyInterface.cpp | 38 +++-- .../OnlineServices_RoomsInterface.cpp | 50 +++++++ .../GeneralsOnline/PluginInterfaces.cpp | 135 +++++++++++++++++- 12 files changed, 304 insertions(+), 33 deletions(-) diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp index 3be5bee6c6c..3a84de77d13 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp @@ -56,6 +56,7 @@ #include "../OnlineServices_Init.h" #include "Common/GameEngine.h" #include "Common/GlobalData.h" +#include "../PluginInterfaces.h" /////////////////////////////////////////////////////////////////////////////////////// @@ -865,13 +866,40 @@ void StartPatchCheck() // GENERALS ONLINE NGMP_OnlineServicesManager::CreateInstance(); - onlineCancelWindow = MessageBoxCancel(TheGameText->fetch("GUI:CheckingForPatches"), - TheGameText->fetch("GUI:CheckingForPatches"), CancelPatchCheckCallbackAndReopenDropdown); - // online services must be initialized // TODO_NGMP: Uninit this when leaving MP, waste of resources and cycles NGMP_OnlineServicesManager::GetInstance()->Init(); + // if we have an AC plugin loaded but the AC external process isnt running, show an error message + if (AnticheatPlugInterface::IsPluginLoaded()) + { + if (!AnticheatPlugInterface::IsExternalProcessRunning()) + { + MessageBoxOk(TheGameText->fetchOrSubstitute("GUI:ACErrorHeader", L"AntiCheat Error"), + TheGameText->fetchOrSubstitute("GUI:ACExternalProcessNotRunning", L"The AntiCheat external process is not running"), + CancelPatchCheckCallbackAndReopenDropdown); + + return; + } + } + else if (AnticheatPlugInterface::DidPluginFailToLoad()) // Did we have something to load but it failed? + { + std::string strPlugin = NGMP_OnlineServicesManager::Settings.GetAnticheatPlugin(); + std::string pluginPath = std::format("plugins/{}/{}.dll", strPlugin.c_str(), strPlugin.c_str()); + + UnicodeString strErrorMssage; + strErrorMssage.format(L"Failed to load the AntiCheat plugin from path: %hs. Please make sure the plugin is installed correctly.", pluginPath.c_str()); + + MessageBoxOk(TheGameText->fetchOrSubstitute("GUI:ACErrorHeader", L"AntiCheat Error"), + strErrorMssage, + CancelPatchCheckCallbackAndReopenDropdown); + + return; + } + + onlineCancelWindow = MessageBoxCancel(TheGameText->fetch("GUI:CheckingForPatches"), + TheGameText->fetch("GUI:CheckingForPatches"), CancelPatchCheckCallbackAndReopenDropdown); + NGMP_OnlineServicesManager::GetInstance()->StartVersionCheck([](bool bSuccess, bool bNeedsUpdate) { #if defined(USE_TEST_ENV) || defined(USE_DEBUG_ON_LIVE_SERVER) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h index 267cb6684bf..e0eebac3fab 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.h @@ -49,6 +49,8 @@ class GenOnlineSettings return m_Render_FramerateLimit_FPSVal; } + std::string GetAnticheatPlugin() const { 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; } bool Social_Notifications_FriendGoesOffline_Menus() { return m_Social_Notification_FriendGoesOffline_Menus; } @@ -136,6 +138,8 @@ class GenOnlineSettings bool m_Social_Notification_PlayerSendsRequest_Menus = true; bool m_Social_Notification_PlayerSendsRequest_Gameplay = true; + std::string m_Plugins_Anticheat = std::string(); + EHTTPVersion m_Network_HTTPVersion = EHTTPVersion::HTTP_VERSION_AUTO; bool m_Network_UseAlternativeEndpoint = false; }; diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Init.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Init.h index 1af0cb98124..579c22d6792 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Init.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Init.h @@ -90,7 +90,9 @@ enum EWebSocketMessageID SOCIAL_FRIEND_FRIEND_REQUEST_ACCEPTED_BY_TARGET = 36, SOCIAL_FRIENDS_LIST_DIRTY = 37, SOCIAL_CANT_ADD_FRIEND_LIST_FULL = 38, - PROBE_RESP = 39 + PROBE_RESP = 39, + AC_REGISTER_PLAYER = 40, + AC_DEREGISTER_PLAYER = 41 }; enum class EQoSRegions diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h index 0b04d50e524..423022c53a3 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h @@ -84,7 +84,8 @@ enum class EJoinLobbyResult JoinLobbyResult_Success, // The room was joined. JoinLobbyResult_FullRoom, // The room is full. JoinLobbyResult_BadPassword, // An incorrect password (or none) was given for a passworded room. - JoinLobbyResult_JoinFailed // Generic failure. + JoinLobbyResult_JoinFailed, // Generic failure. + JoinLobbyResult_AnticheatMismatch // Anticheat mismatch }; enum class ELobbyJoinability diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h index 8a9c100fd66..b43b70cb7fe 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h @@ -23,22 +23,31 @@ enum class EAnticheatActionReason : int32_t }; -static class AnticheatPlugInterface +class AnticheatPlugInterface { public: + static bool g_bPendingExitLobby; + static void AC_NetworkMessageArrived(uint32_t goUserID, void* pData, uint32_t dataLen); + static bool DidPluginFailToLoad() { return m_bPluginLoadFailed; } + static bool IsPluginLoaded() { - return g_hACPluginModule != nullptr; + return g_hACPluginModule != nullptr && !m_bPluginLoadFailed; } + static bool IsExternalProcessRunning(); + + static int GetAnticheatIdentifier(); + static void LoadPlugin(const char* szPluginName); static void Authenticate(); static void UnloadPlugin(); static void Tick(); static bool RegisterPlayer(std::string mwUserID, uint32_t goUserID); + static bool DeregisterPlayer(std::string mwUserID, uint32_t goUserID); static void BeginSession(); static void EndSession(); @@ -53,7 +62,9 @@ static class AnticheatPlugInterface // Func defs typedef void (*FuncDefSetLoggingFunction)(LoggingFunc); typedef int (*FuncDefInitialize)(void); - typedef bool (*FuncDefIsLoaded)(void); + typedef bool (*FuncDefIsExternalProcessRunning)(void); + + typedef int (*FuncDefGetAnticheatIdentifier)(void); typedef void (*FuncDefSetSendMessageViaTransportCallback)(SendMessageViaTransportCallbackFunc); typedef void (*FuncDefACMessageArrivedViaTransport)(uint32_t, void*, uint32_t); @@ -63,13 +74,17 @@ static class AnticheatPlugInterface typedef void (*FuncDefBeginSession)(void); typedef void (*FuncDefEndSession)(void); typedef bool (*FuncDefRegisterPlayer)(const char* szMiddlewareUserID, uint32_t goUserID); + typedef bool (*FuncDefDeregisterPlayer)(const char* szMiddlewareUserID, uint32_t goUserID); typedef void (*FuncDefTick)(void); + typedef void (*FuncDefShutdown)(void); + struct AnticheatPluginFunctionPtrs { FuncDefSetLoggingFunction fnSetLoggingFunction = nullptr; FuncDefInitialize fnInitialize = nullptr; - FuncDefIsLoaded fnIsLoaded = nullptr; + FuncDefIsExternalProcessRunning fnIsExternalProcessRunning = nullptr; + FuncDefGetAnticheatIdentifier fnGetAnticheatIdentifier = nullptr; FuncDefSetACActionRequiredCallback fnSetACActionRequiredCallback = nullptr; FuncDefSetSendMessageViaTransportCallback fnSetSendMessageViaTransportCallback = nullptr; FuncDefACMessageArrivedViaTransport fnACMessageArrivedViaTransport = nullptr; @@ -79,12 +94,15 @@ static class AnticheatPlugInterface FuncDefBeginSession fnBeginSession = nullptr; FuncDefEndSession fnEndSession = nullptr; FuncDefRegisterPlayer fnRegisterPlayer = nullptr; + FuncDefDeregisterPlayer fnDeregisterPlayer = nullptr; FuncDefTick fnTick = nullptr; + FuncDefShutdown fnShutdown = nullptr; }; static AnticheatPluginFunctionPtrs Functions; // Module static HMODULE g_hACPluginModule; + static bool m_bPluginLoadFailed; }; extern HWND ApplicationHWnd; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index 0ede9bd6633..6290b92a322 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -2343,6 +2343,16 @@ void WOLGameSetupMenuUpdate( WindowLayout * layout, void *userData) return; } + // TODO_AC: How do we handle this in quickmatch? + if (AnticheatPlugInterface::g_bPendingExitLobby) + { + AnticheatPlugInterface::g_bPendingExitLobby = false; + + GSMessageBoxOk(TheGameText->fetchOrSubstitute("GUI:ACErrorHeader", L"AntiCheat Error"), TheGameText->fetchOrSubstitute("GUI:ACLobbyIntegrityError", L"Lobby integrity could not be validated. Leaving Lobby.")); + + PopBackToLobby(); + } + if (NGMP_OnlineServicesManager::GetInstance() != nullptr) { NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp index 1eb76f6f974..c97aa7dae91 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLLobbyMenu.cpp @@ -1170,6 +1170,10 @@ void NGMP_WOLLobbyMenu_JoinLobbyCallback(EJoinLobbyResult result) s = TheGameText->fetch("GUI:JoinFailedRoomFull"); break; + case EJoinLobbyResult::JoinLobbyResult_AnticheatMismatch: + s = TheGameText->fetchOrSubstitute("GUI:JoinFailedAnticheatMismatch", L"You are running a different anticheat from this lobby host."); + break; + // NOTE: Commented out ones are no longer supported. Seems like these we GS concepts but not part of the game /* case PEERInviteOnlyRoom: // The room is invite only. diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp index 68f6756f6ba..36007c05b6d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/GeneralsOnline_Settings.cpp @@ -33,6 +33,9 @@ #define SETTINGS_KEY_NETWORK_HTTP_VERSION "http_version" #define SETTINGS_KEY_NETWORK_USE_ALTERNATIVE_ENDPOINT "use_alternative_endpoint" +#define SETTINGS_KEY_PLUGINS "plugins" +#define SETTINGS_KEY_PLUGINS_ANTICHEAT "anticheat" + #define SETTINGS_FILENAME_LEGACY "GeneralsOnline_settings.json" #define SETTINGS_FILENAME "settings.json" @@ -245,6 +248,16 @@ void GenOnlineSettings::Load(void) m_Social_Notification_PlayerSendsRequest_Gameplay = socialSettings[SETTINGS_KEY_SOCIAL_NOTIFICATIONS_PLAYER_SENDS_REQUEST_GAMEPLAY]; } } + + 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]; + } + } } } @@ -334,6 +347,13 @@ void GenOnlineSettings::Save() {SETTINGS_KEY_SOCIAL_NOTIFICATIONS_PLAYER_SENDS_REQUEST_GAMEPLAY, m_Social_Notification_PlayerSendsRequest_Gameplay}, } }, + + { + SETTINGS_KEY_PLUGINS, + { + {SETTINGS_KEY_PLUGINS_ANTICHEAT, m_Plugins_Anticheat} + } + }, }; std::string strData = root.dump(1); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index 9179cd1d355..4010a469af0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -346,6 +346,8 @@ void NGMP_OnlineServicesManager::Shutdown() NetworkLog(ELogVerbosity::LOG_RELEASE, "[NGMP] HTTPManager shutdown complete"); } + AnticheatPlugInterface::UnloadPlugin(); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[NGMP] OnlineServicesManager shutdown complete"); } @@ -829,10 +831,13 @@ 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()); + #if _DEBUG - AnticheatPlugInterface::LoadPlugin("F:\\gen\\eac_module\\build\\Debug\\plugin.dll"); + AnticheatPlugInterface::LoadPlugin(pluginPath.c_str()); #else - AnticheatPlugInterface::LoadPlugin("plugin.dll"); + AnticheatPlugInterface::LoadPlugin(pluginPath.c_str()); #endif // TODO_NGMP: Better location diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index 7568c5955b0..97e41c2f470 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -762,8 +762,6 @@ void NGMP_OnlineServices_LobbyInterface::ApplyLocalUserPropertiesToCurrentNetwor } } -// TODO_AC: Do this elsewhere -std::set g_AlreadyREgistered; void NGMP_OnlineServices_LobbyInterface::UpdateRoomDataCache(std::function fnCallback) { // refresh lobby @@ -905,19 +903,6 @@ void NGMP_OnlineServices_LobbyInterface::UpdateRoomDataCache(std::functionGetAndParseServiceConfig([=]() { m_CurrentLobby = LobbyEntry(); @@ -1314,6 +1316,7 @@ void NGMP_OnlineServices_LobbyInterface::CreateLobby(UnicodeString strLobbyName, j["exe_crc"] = TheGlobalData->m_exeCRC; j["ini_crc"] = TheGlobalData->m_iniCRC; j["max_cam_height"] = NGMP_OnlineServicesManager::Settings.Camera_GetMaxHeight_WhenLobbyHost(); + j["anticheat_id"] = AnticheatPlugInterface::GetAnticheatIdentifier(); std::string strPostData = j.dump(); @@ -1424,7 +1427,12 @@ void NGMP_OnlineServices_LobbyInterface::CreateLobby(UnicodeString strLobbyName, void NGMP_OnlineServices_LobbyInterface::OnJoinedOrCreatedLobby(bool bAlreadyUpdatedDetails, std::function fnCallback) { // begin AC + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Begin Session 0"); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Begin Session 0: %d", AnticheatPlugInterface::IsPluginLoaded()); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Begin Session 0: %d", AnticheatPlugInterface::Functions.fnBeginSession); + AnticheatPlugInterface::BeginSession(); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Begin Session End"); // join the network mesh too if (m_pLobbyMesh == nullptr) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp index 9cee01aee2b..09462c1eb73 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp @@ -251,6 +251,24 @@ class WebSocketMessage_NetworkStartSignalling : public WebSocketMessageBase NLOHMANN_DEFINE_TYPE_INTRUSIVE(WebSocketMessage_NetworkStartSignalling, msg_id, lobby_id, user_id, preferred_port) }; +class WebSocketMessage_ACRegisterPlayer : public WebSocketMessageBase +{ +public: + int64_t user_id; + std::string mwid; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(WebSocketMessage_ACRegisterPlayer, msg_id, user_id, mwid) +}; + +class WebSocketMessage_ACDeregisterPlayer : public WebSocketMessageBase +{ +public: + int64_t user_id; + std::string mwid; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(WebSocketMessage_ACDeregisterPlayer, msg_id, user_id, mwid) +}; + class WebSocketMessage_NetworkDisconnectPlayer : public WebSocketMessageBase { public: @@ -968,6 +986,38 @@ void WebSocket::Tick() } break; + case EWebSocketMessageID::AC_REGISTER_PLAYER: + { + WebSocketMessage_ACRegisterPlayer acData; + bool bParsed = JSONGetAsObject(jsonObject, &acData); + + if (bParsed) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Websocket AC_REGISTER_PLAYER for %lld and %s", acData.user_id, acData.mwid); + if (!AnticheatPlugInterface::RegisterPlayer(acData.mwid, acData.user_id)) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] AnticheatPlugInterface::RegisterPlayer failed"); + } + } + } + break; + + case EWebSocketMessageID::AC_DEREGISTER_PLAYER: + { + WebSocketMessage_ACDeregisterPlayer acData; + bool bParsed = JSONGetAsObject(jsonObject, &acData); + + if (bParsed) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Websocket AC_DEREGISTER_PLAYER for %lld and %s", acData.user_id, acData.mwid); + if (!AnticheatPlugInterface::DeregisterPlayer(acData.mwid, acData.user_id)) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] AnticheatPlugInterface::DeregisterPlayer failed"); + } + } + } + break; + case EWebSocketMessageID::NETWORK_CONNECTION_DISCONNECT_PLAYER: { WebSocketMessage_NetworkDisconnectPlayer disconnectPlayerData; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp index 59d4e1f6107..512ab422022 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -13,12 +13,38 @@ return; \ } +bool AnticheatPlugInterface::IsExternalProcessRunning() +{ + if (IsPluginLoaded()) + { + return Functions.fnIsExternalProcessRunning(); + } + + return false; +} + +int AnticheatPlugInterface::GetAnticheatIdentifier() +{ + if (IsPluginLoaded()) + { + return Functions.fnGetAnticheatIdentifier(); + } + + return 0; +} + void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Attempting to load plugin from %s", szPluginName); + + m_bPluginLoadFailed = false; g_hACPluginModule = LoadLibraryA(szPluginName); if (!g_hACPluginModule) { + g_hACPluginModule = nullptr; + m_bPluginLoadFailed = true; + DWORD err = GetLastError(); NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Failed to load %s (%u)", szPluginName, err); } @@ -40,10 +66,12 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) NetworkLog(ELogVerbosity::LOG_RELEASE, "Initialize result = %d", result); // check loaded - AC_PLUGIN_LOAD_FUNCTION(IsLoaded); + AC_PLUGIN_LOAD_FUNCTION(IsExternalProcessRunning); + + AC_PLUGIN_LOAD_FUNCTION(GetAnticheatIdentifier); #if _DEBUG - SetWindowText(ApplicationHWnd, Functions.fnIsLoaded() ? "SECURED" : "INSECURE"); + SetWindowText(ApplicationHWnd, Functions.fnIsExternalProcessRunning() ? "SECURED" : "INSECURE"); #endif // set action required callback @@ -51,9 +79,41 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) Functions.fnSetACActionRequiredCallback([](uint32_t userID, const char* szReason, EAnticheatActionType actionType, EAnticheatActionReason actionReason) { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure."); - extern void PopBackToLobby(); - PopBackToLobby(); + NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); + + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Action required."); + + 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); + + 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; + } + } + } }); // set transport callback @@ -81,11 +141,17 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) // register player funcs AC_PLUGIN_LOAD_FUNCTION(RegisterPlayer); + AC_PLUGIN_LOAD_FUNCTION(DeregisterPlayer); + + AC_PLUGIN_LOAD_FUNCTION(Tick); + AC_PLUGIN_LOAD_FUNCTION(Shutdown); // TODO_AC: Deregister player } } +bool AnticheatPlugInterface::g_bPendingExitLobby = false; + void AnticheatPlugInterface::AC_NetworkMessageArrived(uint32_t goUserID, void* pData, uint32_t dataLen) { // TODO: Cache all of these getprocaddresses @@ -110,6 +176,12 @@ void AnticheatPlugInterface::Authenticate() Functions.fnLogin(pAuthInterface->GetAuthToken().c_str(), [](bool bSuccess) { + if (!bSuccess) + { + // TODO_AC: Handle this, its a fatal error + return; + } + if (Functions.fnIsLoggedIn()) { char buf[4196]; @@ -127,15 +199,33 @@ void AnticheatPlugInterface::Authenticate() pAuthInterface->SendMiddlewareToken(std::string(buf)); } } + else + { + // TODO_AC: Handle this, its a fatal error + } + + }); } } +bool g_bSessionStarted = false; + void AnticheatPlugInterface::BeginSession() { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] BeginSession() called"); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] IsPluginLoaded=%d, fnBeginSession=%p", IsPluginLoaded(), Functions.fnBeginSession); + if (IsPluginLoaded() && Functions.fnBeginSession != nullptr) { - Functions.fnBeginSession; + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Calling plugin fnBeginSession()"); + Functions.fnBeginSession(); + g_bSessionStarted = true; + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Plugin fnBeginSession() completed"); + } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: Cannot call fnBeginSession - plugin not loaded or function pointer is null"); } } @@ -143,16 +233,23 @@ void AnticheatPlugInterface::EndSession() { if (IsPluginLoaded() && Functions.fnEndSession != nullptr) { - Functions.fnEndSession; + Functions.fnEndSession(); + g_bSessionStarted = false; } } AnticheatPlugInterface::AnticheatPluginFunctionPtrs AnticheatPlugInterface::Functions; HMODULE AnticheatPlugInterface::g_hACPluginModule = nullptr; +bool AnticheatPlugInterface::m_bPluginLoadFailed = false; bool AnticheatPlugInterface::RegisterPlayer(std::string mwUserID, uint32_t goUserID) { + if (!g_bSessionStarted) // TODO_AC: This is hacky, it's because on lobby join, the server can send AC_REGISTER_PLAYER before we join the lobby, so we didnt actually start the session yet. We should buffer these messages until session start or something instead of relying on this hacky global + { + AnticheatPlugInterface::BeginSession(); + } + if (IsPluginLoaded() && Functions.fnRegisterPlayer != nullptr) { NetworkLog(ELogVerbosity::LOG_RELEASE, "RegisterPlayer: %s to %" PRIu64, mwUserID.c_str(), goUserID); @@ -166,6 +263,20 @@ bool AnticheatPlugInterface::RegisterPlayer(std::string mwUserID, uint32_t goUse } +bool AnticheatPlugInterface::DeregisterPlayer(std::string mwUserID, uint32_t goUserID) +{ + if (IsPluginLoaded() && Functions.fnDeregisterPlayer != nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "DeregisterPlayer: %s to %" PRIu64, mwUserID.c_str(), goUserID); + + bool bReg = Functions.fnDeregisterPlayer(mwUserID.c_str(), goUserID); + NetworkLog(ELogVerbosity::LOG_RELEASE, "DeregisterPlayerFunc result: %d", bReg); + return bReg; + } + + return false; +} + void AnticheatPlugInterface::Tick() { if (IsPluginLoaded() && Functions.fnTick != nullptr) @@ -178,7 +289,17 @@ void AnticheatPlugInterface::UnloadPlugin() { if (IsPluginLoaded()) { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Starting Shutdown"); + if (Functions.fnShutdown != nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Shutdown in progress"); + Functions.fnShutdown(); + } + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Shutdown Complete"); + + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Unloading plugin"); FreeLibrary(g_hACPluginModule); g_hACPluginModule = nullptr; + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Unloaded plugin"); } } From 83ad5205fc7c8ec76f25231868027c2a5f528362 Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Sat, 25 Apr 2026 18:58:44 -0400 Subject: [PATCH 04/11] - AC token refresh on 45m timer --- .../GeneralsOnline/PluginInterfaces.h | 6 +++ .../GeneralsOnline/PluginInterfaces.cpp | 44 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h index b43b70cb7fe..6540b8d7868 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h @@ -46,6 +46,8 @@ class AnticheatPlugInterface static void UnloadPlugin(); static void Tick(); + static void RefreshToken(); + static bool RegisterPlayer(std::string mwUserID, uint32_t goUserID); static bool DeregisterPlayer(std::string mwUserID, uint32_t goUserID); @@ -69,6 +71,7 @@ class AnticheatPlugInterface typedef void (*FuncDefSetSendMessageViaTransportCallback)(SendMessageViaTransportCallbackFunc); typedef void (*FuncDefACMessageArrivedViaTransport)(uint32_t, void*, uint32_t); typedef void (*FuncDefLogin)(const char* szGameToken, LoginCallback cb); + typedef void (*FuncDefRefreshToken)(const char* szGameToken, LoginCallback cb); typedef bool (*FuncDefGetMiddlewareAuthToken)(char* buffer, size_t bufferSize); typedef bool (*FuncDefIsLoggedIn)(void); typedef void (*FuncDefBeginSession)(void); @@ -89,6 +92,7 @@ class AnticheatPlugInterface FuncDefSetSendMessageViaTransportCallback fnSetSendMessageViaTransportCallback = nullptr; FuncDefACMessageArrivedViaTransport fnACMessageArrivedViaTransport = nullptr; FuncDefLogin fnLogin = nullptr; + FuncDefRefreshToken fnRefreshToken = nullptr; FuncDefGetMiddlewareAuthToken fnGetMiddlewareAuthToken = nullptr; FuncDefIsLoggedIn fnIsLoggedIn = nullptr; FuncDefBeginSession fnBeginSession = nullptr; @@ -103,6 +107,8 @@ class AnticheatPlugInterface // Module static HMODULE g_hACPluginModule; static bool m_bPluginLoadFailed; + + static int64_t m_tokenCreationTime; }; extern HWND ApplicationHWnd; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp index 512ab422022..d3b721fa5b1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -130,8 +130,9 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) // AC network message arrived callback AC_PLUGIN_LOAD_FUNCTION(ACMessageArrivedViaTransport); - // Login func + // Login funcs AC_PLUGIN_LOAD_FUNCTION(Login); + AC_PLUGIN_LOAD_FUNCTION(RefreshToken); AC_PLUGIN_LOAD_FUNCTION(IsLoggedIn); AC_PLUGIN_LOAD_FUNCTION(GetMiddlewareAuthToken); @@ -182,6 +183,8 @@ void AnticheatPlugInterface::Authenticate() return; } + m_tokenCreationTime = std::chrono::duration_cast(std::chrono::utc_clock::now().time_since_epoch()).count(); + if (Functions.fnIsLoggedIn()) { char buf[4196]; @@ -243,6 +246,8 @@ AnticheatPlugInterface::AnticheatPluginFunctionPtrs AnticheatPlugInterface::Func HMODULE AnticheatPlugInterface::g_hACPluginModule = nullptr; bool AnticheatPlugInterface::m_bPluginLoadFailed = false; +int64_t AnticheatPlugInterface::m_tokenCreationTime = -1; + bool AnticheatPlugInterface::RegisterPlayer(std::string mwUserID, uint32_t goUserID) { if (!g_bSessionStarted) // TODO_AC: This is hacky, it's because on lobby join, the server can send AC_REGISTER_PLAYER before we join the lobby, so we didnt actually start the session yet. We should buffer these messages until session start or something instead of relying on this hacky global @@ -282,6 +287,43 @@ void AnticheatPlugInterface::Tick() if (IsPluginLoaded() && Functions.fnTick != nullptr) { Functions.fnTick(); + + // Do we need to refresh our token? + if (Functions.fnIsLoggedIn()) + { + int64_t now = std::chrono::duration_cast(std::chrono::utc_clock::now().time_since_epoch()).count(); + if (m_tokenCreationTime != -1 && now - m_tokenCreationTime >= 45 * 60 * 1000) // refresh every 45m, tokens last 60m, giving us a 15m buffer to refresh and retry if something goes wrong + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Token is about to expire, refreshing..."); + RefreshToken(); + } + } + } +} + +void AnticheatPlugInterface::RefreshToken() +{ + if (IsPluginLoaded() && Functions.fnRefreshToken != nullptr && Functions.fnIsLoggedIn) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Refreshing token"); + NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); + if (pAuthInterface == nullptr) + { + return; + } + + m_tokenCreationTime = std::chrono::duration_cast(std::chrono::utc_clock::now().time_since_epoch()).count(); + + Functions.fnRefreshToken(pAuthInterface->GetAuthToken().c_str(), + [](bool bSuccess) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Refreshed token: %d", bSuccess); + if (!bSuccess) + { + // TODO_AC: Handle this, its a fatal error + return; + } + }); } } From b131fb6245c03f7f781e54e9931b3c48e75d0d78 Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:03:56 -0400 Subject: [PATCH 05/11] - Hooked up AC integrity violation notifications --- .../GameNetwork/GeneralsOnline/PluginInterfaces.h | 4 ++++ .../GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp | 1 - .../Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp | 9 +++++++-- .../GameNetwork/GeneralsOnline/NextGenTransport.cpp | 1 + .../GeneralsOnline/OnlineServices_Auth.cpp | 1 - .../GeneralsOnline/OnlineServices_LobbyInterface.cpp | 2 -- .../OnlineServices_MatchmakingInterface.cpp | 1 + .../GameNetwork/GeneralsOnline/PluginInterfaces.cpp | 11 +++++++++-- 8 files changed, 22 insertions(+), 8 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h index 6540b8d7868..8c96db5f32f 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h @@ -61,6 +61,9 @@ class AnticheatPlugInterface typedef void (*FuncDefSetACActionRequiredCallback)(FuncDefACPlayerActionRequiredCallbackFunc); typedef void (*SendMessageViaTransportCallbackFunc)(uint32_t, const void*, uint32_t); + typedef void (*FuncDefCIntegrityViolationOccurredCallbackFunc)(const char*, int); + typedef void (*FuncDefSetACIntegrityViolationOccurredCallback)(FuncDefCIntegrityViolationOccurredCallbackFunc); + // Func defs typedef void (*FuncDefSetLoggingFunction)(LoggingFunc); typedef int (*FuncDefInitialize)(void); @@ -89,6 +92,7 @@ class AnticheatPlugInterface FuncDefIsExternalProcessRunning fnIsExternalProcessRunning = nullptr; FuncDefGetAnticheatIdentifier fnGetAnticheatIdentifier = nullptr; FuncDefSetACActionRequiredCallback fnSetACActionRequiredCallback = nullptr; + FuncDefSetACIntegrityViolationOccurredCallback fnSetACIntegrityViolationOccurredCallback = nullptr; FuncDefSetSendMessageViaTransportCallback fnSetSendMessageViaTransportCallback = nullptr; FuncDefACMessageArrivedViaTransport fnACMessageArrivedViaTransport = nullptr; FuncDefLogin fnLogin = nullptr; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index 6290b92a322..1c78a6c87a2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -2343,7 +2343,6 @@ void WOLGameSetupMenuUpdate( WindowLayout * layout, void *userData) return; } - // TODO_AC: How do we handle this in quickmatch? if (AnticheatPlugInterface::g_bPendingExitLobby) { AnticheatPlugInterface::g_bPendingExitLobby = false; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index 7706694fff1..380623b7ef5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -752,6 +752,10 @@ void NetworkMesh::SendACPacket(uint32_t userID, const void* pData, uint32_t data { m_mapConnections[userID].SendACPacket(pData, dataLen); } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Send Packet ERR 1"); + } } void NetworkMesh::StartConnectionSignalling(int64_t remoteUserID, uint16_t preferredPort) @@ -1002,6 +1006,7 @@ void PlayerConnection::LiteUpdateForAC() const uint32_t numBytes = msg->m_cbSize; // is it an AC packet? + // TODO_AC: Improve detection, just add a 'msg type' to the start of the packet std::vector vecData; vecData.resize(numBytes); memcpy(vecData.data(), msg->GetData(), numBytes); @@ -1027,7 +1032,7 @@ void PlayerConnection::LiteUpdateForAC() else { // not an AC packet, we dont care - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Received NON AC message"); + NetworkLog(ELogVerbosity::LOG_DEBUG, "[AC PACKET] Received NON AC message"); } msg->Release(); @@ -1105,7 +1110,7 @@ void PlayerConnection::SendACPacket(const void* pData, uint32_t dataLen) vecData[1] = 1; vecData[2] = 2; - NetworkLog(ELogVerbosity::LOG_DEBUG, "[AC PACKET] Sending AC msg of size %ld to user %ld\n", dataLen, m_userID); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Sending AC msg of size %ld to user %ld\n", dataLen, m_userID); EResult r = SteamNetworkingSockets()->SendMessageToConnection(m_hSteamConnection, vecData.data(), vecData.size(), k_nSteamNetworkingSend_Reliable | k_nSteamNetworkingSend_AutoRestartBrokenSession, nullptr); if (r != k_EResultOK) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp index 4d8d5014737..38d5a505287 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp @@ -116,6 +116,7 @@ Bool NextGenTransport::doRecv(void) vecData.resize(numBytes); memcpy(vecData.data(), msg->GetData(), numBytes); + // TODO_AC: Improve detection, just add a 'msg type' to the start of the packet BYTE b1 = (BYTE)vecData[0]; BYTE b2 = (BYTE)vecData[1]; BYTE b3 = (BYTE)vecData[2]; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp index 414322fb086..fe7a2e0dc69 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -141,7 +141,6 @@ void NGMP_OnlineServices_AuthInterface::GoToDetermineNetworkCaps() void NGMP_OnlineServices_AuthInterface::SendMiddlewareToken(std::string strMWToken) { - // TODO_AC: Service should just return 200 if no middleware is loaded std::string strLoginURI = NGMP_OnlineServicesManager::GetAPIEndpoint("ProvideMWToken"); // login diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index 97e41c2f470..b12aee927c1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -1029,7 +1029,6 @@ void NGMP_OnlineServices_LobbyInterface::JoinLobby(LobbyEntry lobbyInfo, std::st return; } - // TODO_AC: Safety, do we need this? AnticheatPlugInterface::EndSession(); m_bAttemptingToJoinLobby = true; @@ -1276,7 +1275,6 @@ struct CreateLobbyResponse void NGMP_OnlineServices_LobbyInterface::CreateLobby(UnicodeString strLobbyName, UnicodeString strInitialMapName, AsciiString strInitialMapPath, bool bIsOfficial, int initialMaxSize, bool bVanillaTeamsOnly, bool bTrackStats, uint32_t startingCash, bool bPassworded, std::string strPassword, bool bAllowObservers) { - // TODO_AC: Safety, do we need this? AnticheatPlugInterface::EndSession(); NGMP_OnlineServicesManager::GetInstance()->GetAndParseServiceConfig([=]() diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.cpp index 3e244ced40b..49d2167c317 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_MatchmakingInterface.cpp @@ -82,6 +82,7 @@ void NGMP_OnlineServices_MatchmakingInterface::StartMatchmaking(uint16_t playlis j["maps"] = vecSelectedMapIndexes; j["exe_crc"] = TheGlobalData->m_exeCRC; j["ini_crc"] = TheGlobalData->m_iniCRC; + j["anticheat_id"] = AnticheatPlugInterface::GetAnticheatIdentifier(); std::map mapHeaders; std::string strPostData = j.dump(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp index d3b721fa5b1..6441b6f9703 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -74,6 +74,15 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) SetWindowText(ApplicationHWnd, Functions.fnIsExternalProcessRunning() ? "SECURED" : "INSECURE"); #endif + // integrity callback + AC_PLUGIN_LOAD_FUNCTION(SetACIntegrityViolationOccurredCallback); + + 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; + }); + // set action required callback AC_PLUGIN_LOAD_FUNCTION(SetACActionRequiredCallback); @@ -146,8 +155,6 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) AC_PLUGIN_LOAD_FUNCTION(Tick); AC_PLUGIN_LOAD_FUNCTION(Shutdown); - - // TODO_AC: Deregister player } } From 67f6c993aa7a1a453cd27c5bf097bff7799b5427 Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:52:13 -0400 Subject: [PATCH 06/11] Load dxwrapper_go instead of dxwrapper.dll (signed vs unsigned) --- GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp index 6deec90ab90..53c46830693 100644 --- a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp +++ b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp @@ -322,7 +322,7 @@ bool DX8Wrapper::Init(void * hwnd, bool lite) if (!lite) { #if defined(GENERALS_ONLINE) - LoadLibrary("dxwrapper.dll"); + LoadLibrary("dxwrapper_go.dll"); D3D8Lib = LoadLibraryEx("D3D8.DLL", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); // dont load the local hooked d3d8 #else D3D8Lib = LoadLibrary("D3D8.DLL"); From 74b2f25d23f9655cf4b2b6db0b61fbac649a031b Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:26:06 -0400 Subject: [PATCH 07/11] version increment --- .../Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index 4010a469af0..8a4111fd2c4 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -1011,7 +1011,7 @@ void NGMP_OnlineServicesManager::InitSentry() sentry_options_set_dsn(options, "https://61750bebd112d279bcc286d617819269@o4509316925554688.ingest.us.sentry.io/4509316927586304"); sentry_options_set_database_path(options, strDumpPath.c_str()); - sentry_options_set_release(options, "generalsonline-client@032926_QFE5"); + sentry_options_set_release(options, "generalsonline-client@042826"); #if defined(USE_TEST_ENV) sentry_options_set_environment(options, "test"); From d90bae681772182115b238ef619a8e9ffd2297f8 Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Wed, 29 Apr 2026 17:38:49 -0400 Subject: [PATCH 08/11] QFE2 changes (safety mainly) --- .../GameNetwork/GameSpy/MainMenuUtils.cpp | 1 - .../GeneralsOnline/NetworkMesh.cpp | 90 +++++++++++++++---- .../GeneralsOnline/NextGenTransport.cpp | 39 +++++--- .../GeneralsOnline/OnlineServices_Init.cpp | 2 +- 4 files changed, 97 insertions(+), 35 deletions(-) diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp index 3a84de77d13..814c87b9e07 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/MainMenuUtils.cpp @@ -905,7 +905,6 @@ void StartPatchCheck() #if defined(USE_TEST_ENV) || defined(USE_DEBUG_ON_LIVE_SERVER) bNeedsUpdate = false; #endif - bNeedsUpdate = false; cantConnectBeforeOnline = !bSuccess; mustDownloadPatch = bNeedsUpdate; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index 380623b7ef5..2db7230c323 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -748,13 +748,25 @@ int NetworkMesh::SendGamePacket(void* pBuffer, uint32_t totalDataSize, int64_t u void NetworkMesh::SendACPacket(uint32_t userID, const void* pData, uint32_t dataLen) { + if (dataLen == 0) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Cannot send empty AC packet to user %u", userID); + return; + } + + if (pData == nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Cannot send AC packet with null data to user %u", userID); + return; + } + if (m_mapConnections.contains(userID)) { m_mapConnections[userID].SendACPacket(pData, dataLen); } else { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Send Packet ERR 1"); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Send Packet ERR - user %u not found in connections", userID); } } @@ -1011,30 +1023,40 @@ void PlayerConnection::LiteUpdateForAC() vecData.resize(numBytes); memcpy(vecData.data(), msg->GetData(), numBytes); - BYTE b1 = (BYTE)vecData[0]; - BYTE b2 = (BYTE)vecData[1]; - BYTE b3 = (BYTE)vecData[2]; - if (b1 == 9 - && b2 == 1 - && b3 == 2) + // Check minimum packet size for AC header + if (numBytes >= 3) { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Received AC message of size %u from user %lld", numBytes, static_cast(m_userID)); + BYTE b1 = (BYTE)vecData[0]; + BYTE b2 = (BYTE)vecData[1]; + BYTE b3 = (BYTE)vecData[2]; + if (b1 == 9 + && b2 == 1 + && b3 == 2) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Received AC message of size %u from user %lld", numBytes, static_cast(m_userID)); - // remove header - // TODO_AC: Optimize this - std::vector vecDataAC; - vecDataAC.resize(numBytes - 3); - memcpy(vecDataAC.data(), (char*)msg->GetData() + 3, numBytes - 3); + // remove header + // TODO_AC: Optimize this + std::vector vecDataAC; + vecDataAC.resize(numBytes - 3); + memcpy(vecDataAC.data(), (char*)msg->GetData() + 3, numBytes - 3); - AnticheatPlugInterface::AC_NetworkMessageArrived(m_userID, vecDataAC.data(), numBytes - 3); + AnticheatPlugInterface::AC_NetworkMessageArrived(m_userID, vecDataAC.data(), numBytes - 3); + msg->Release(); + continue; + } } - else + else if (numBytes > 0 && numBytes < 3) { - // not an AC packet, we dont care - NetworkLog(ELogVerbosity::LOG_DEBUG, "[AC PACKET] Received NON AC message"); + // Malformed AC packet - too small for header + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Dropping malformed AC packet - size %u is less than header size 3 from user %lld", numBytes, static_cast(m_userID)); + msg->Release(); + continue; } + // not an AC packet, we dont care + NetworkLog(ELogVerbosity::LOG_DEBUG, "[AC PACKET] Received NON AC message"); msg->Release(); } } @@ -1058,6 +1080,24 @@ PlayerConnection::PlayerConnection(int64_t userID, HSteamNetConnection hSteamCon int PlayerConnection::SendGamePacket(void* pBuffer, uint32_t totalDataSize) { + if (m_hSteamConnection == k_HSteamNetConnection_Invalid) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[GAME PACKET] Cannot send game packet - connection is invalid for user %lld", m_userID); + return (int)k_EResultFail; + } + + if (totalDataSize == 0) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[GAME PACKET] Cannot send empty game packet to user %lld", m_userID); + return (int)k_EResultFail; + } + + if (pBuffer == nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[GAME PACKET] Cannot send game packet with null buffer to user %lld", m_userID); + return (int)k_EResultFail; + } + int sendFlags = k_nSteamNetworkingSend_Reliable | k_nSteamNetworkingSend_AutoRestartBrokenSession; // default from last patch ServiceConfig& serviceConf = NGMP_OnlineServicesManager::GetInstance()->GetServiceConfig(); @@ -1102,6 +1142,18 @@ int PlayerConnection::SendGamePacket(void* pBuffer, uint32_t totalDataSize) void PlayerConnection::SendACPacket(const void* pData, uint32_t dataLen) { + if (m_hSteamConnection == k_HSteamNetConnection_Invalid) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Cannot send AC packet - connection is invalid for user %ld", m_userID); + return; + } + + if (dataLen > 0 && pData == nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Cannot send AC packet - data is null for user %ld", m_userID); + return; + } + std::vector vecData; vecData.resize(dataLen + 3); memcpy(vecData.data() + 3, pData, dataLen); @@ -1111,7 +1163,7 @@ void PlayerConnection::SendACPacket(const void* pData, uint32_t dataLen) vecData[2] = 2; NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Sending AC msg of size %ld to user %ld\n", dataLen, m_userID); - EResult r = SteamNetworkingSockets()->SendMessageToConnection(m_hSteamConnection, vecData.data(), vecData.size(), k_nSteamNetworkingSend_Reliable | k_nSteamNetworkingSend_AutoRestartBrokenSession, nullptr); + EResult r = SteamNetworkingSockets()->SendMessageToConnection(m_hSteamConnection, vecData.data(), vecData.size(), k_nSteamNetworkingSend_Reliable, nullptr); if (r != k_EResultOK) { @@ -1291,7 +1343,7 @@ void PlayerConnection::SetDisconnected(bool bWasError, NetworkMesh* pOwningMesh, UpdateState(m_State, pOwningMesh); // may erase *this from the map } - // Use saved stack values — do NOT touch any member after this point. + // Use saved stack values � do NOT touch any member after this point. NetworkLog(ELogVerbosity::LOG_RELEASE, "[STEAM CONNECTION] Setting connection %u to disconnected/invalid on user %lld", savedHandle, savedUserID); if (SteamNetworkingSockets()) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp index 38d5a505287..32f62ec4712 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp @@ -116,24 +116,35 @@ Bool NextGenTransport::doRecv(void) vecData.resize(numBytes); memcpy(vecData.data(), msg->GetData(), numBytes); - // TODO_AC: Improve detection, just add a 'msg type' to the start of the packet - BYTE b1 = (BYTE)vecData[0]; - BYTE b2 = (BYTE)vecData[1]; - BYTE b3 = (BYTE)vecData[2]; - if (b1 == 9 - && b2 == 1 - && b3 == 2) + // Check minimum packet size for AC header + if (numBytes >= 3) { - NetworkLog(ELogVerbosity::LOG_RELEASE,"[AC PACKET] Received AC message of size %u from user %lld", numBytes, static_cast(kvPair.second.m_userID)); + BYTE b1 = (BYTE)vecData[0]; + BYTE b2 = (BYTE)vecData[1]; + BYTE b3 = (BYTE)vecData[2]; + if (b1 == 9 + && b2 == 1 + && b3 == 2) + { + NetworkLog(ELogVerbosity::LOG_RELEASE,"[AC PACKET] Received AC message of size %u from user %lld", numBytes, static_cast(kvPair.second.m_userID)); - // remove header - // TODO_AC: Optimize this - std::vector vecDataAC; - vecDataAC.resize(numBytes - 3); - memcpy(vecDataAC.data(), (char*)msg->GetData() + 3, numBytes - 3); + // remove header + // TODO_AC: Optimize this + std::vector vecDataAC; + vecDataAC.resize(numBytes - 3); + memcpy(vecDataAC.data(), (char*)msg->GetData() + 3, numBytes - 3); - AnticheatPlugInterface::AC_NetworkMessageArrived(kvPair.second.m_userID, vecDataAC.data(), numBytes - 3); + AnticheatPlugInterface::AC_NetworkMessageArrived(kvPair.second.m_userID, vecDataAC.data(), numBytes - 3); + msg->Release(); + continue; + } + } + else if (numBytes > 0 && numBytes < 3) + { + // Malformed AC packet - too small for header + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC PACKET] Dropping malformed AC packet - size %u is less than header size 3 from user %lld", numBytes, static_cast(kvPair.second.m_userID)); + msg->Release(); continue; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index 8a4111fd2c4..f9b7d756f4b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -1011,7 +1011,7 @@ void NGMP_OnlineServicesManager::InitSentry() sentry_options_set_dsn(options, "https://61750bebd112d279bcc286d617819269@o4509316925554688.ingest.us.sentry.io/4509316927586304"); sentry_options_set_database_path(options, strDumpPath.c_str()); - sentry_options_set_release(options, "generalsonline-client@042826"); + sentry_options_set_release(options, "generalsonline-client@042826_QFE2_EAC"); #if defined(USE_TEST_ENV) sentry_options_set_environment(options, "test"); From e989d4bfe57a0874d6e00a6da779c12cf88f9cdd Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Fri, 1 May 2026 14:23:53 -0400 Subject: [PATCH 09/11] - Tick network before processing tear down requests --- .../Code/GameEngine/Source/Common/GameEngine.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 25d1ae1c8f8..c2105e16a54 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -975,6 +975,11 @@ void GameEngine::update() TheGameClient->UPDATE(); TheMessageStream->propagateMessages(); + if (TheNetwork != nullptr) + { + TheNetwork->UPDATE(); + } + if (g_bTearDownGeneralsOnlineRequested) // delayed tear down { g_bTearDownGeneralsOnlineRequested = false; @@ -983,12 +988,6 @@ void GameEngine::update() } - - if (TheNetwork != nullptr) - { - TheNetwork->UPDATE(); - } - if (NGMP_OnlineServicesManager::GetInstance() != nullptr) { NGMP_OnlineServicesManager::GetInstance()->Tick(); From 3492f6703fb08dd9814fe1e26b813a1c4011d6ca Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Sat, 2 May 2026 19:02:56 -0400 Subject: [PATCH 10/11] - Retry logic - Extra safety in networking and plugin impl --- .../GameNetwork/GeneralsOnline/NetworkMesh.h | 4 +- .../GeneralsOnline/NextGenTransport.h | 21 ++- .../GUICallbacks/Menus/WOLGameSetupMenu.cpp | 1 - .../GUICallbacks/Menus/WOLQuickMatchMenu.cpp | 1 - .../GeneralsOnline/NetworkMesh.cpp | 36 +++- .../GeneralsOnline/NextGenTransport.cpp | 161 +++++++++++++++--- .../GeneralsOnline/OnlineServices_Init.cpp | 2 +- .../GeneralsOnline/PluginInterfaces.cpp | 73 +++++--- 8 files changed, 241 insertions(+), 58 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h index 7a812794b84..8bbd6233a9c 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h @@ -2,7 +2,8 @@ #include "NGMP_include.h" #include -#include "ValveNetworkingSockets/steam/steamnetworkingsockets.h" +#include +#include "ValveNetworkingSockets/steam/steamnetworkingcustomsignaling.h" class NetRoom_ChatMessagePacket; @@ -200,6 +201,7 @@ class NetworkMesh private: std::map m_mapConnections; + mutable std::recursive_mutex m_mapConnectionsMutex; // Synchronizes access to m_mapConnections ISignalingClient* m_pSignaling = nullptr; diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NextGenTransport.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NextGenTransport.h index 89b4ab9268c..7c21c700928 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NextGenTransport.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NextGenTransport.h @@ -18,6 +18,13 @@ #pragma comment(lib, "ValveNetworkingSockets/webrtc-lite.lib") #pragma comment(lib, "Secur32.lib") +// Struct to track retry state for outgoing packets +struct OutgoingPacketState +{ + Int retryCount = 0; + static constexpr Int MAX_RETRIES = 3; +}; + // it to be a MemoryPoolObject (srj) class NextGenTransport : public Transport //: public MemoryPoolObject { @@ -40,6 +47,18 @@ class NextGenTransport : public Transport //: public MemoryPoolObject inline Bool allowBroadcasts(Bool val) override { return false; } + // Helper to clear a packet from the receive buffer (accounts for zero-length packets) + void clearInBufferSlot(int slotIndex) + { + if (slotIndex >= 0 && slotIndex < MAX_MESSAGES) + { + m_inBuffer[slotIndex].length = 0; + m_inBufferOccupied[slotIndex] = false; + } + } + private: - + OutgoingPacketState m_outPacketState[MAX_MESSAGES]; + // Track which incoming buffer slots are occupied (handles zero-length packets) + bool m_inBufferOccupied[MAX_MESSAGES]; }; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp index 1c78a6c87a2..d6677ba3974 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -1773,7 +1773,6 @@ void WOLGameSetupMenuInit( WindowLayout *layout, void *userData ) std::string strState = "Unknown"; EConnectionState connState = connection->GetState(); - std::string strConnectionType = connection->GetConnectionType(); switch (connState) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp index b58e6db9039..be7fbdc9534 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLQuickMatchMenu.cpp @@ -1247,7 +1247,6 @@ void WOLQuickMatchMenuInit( WindowLayout *layout, void *userData ) std::string strState = "Unknown"; EConnectionState connState = connection->GetState(); - std::string strConnectionType = connection->GetConnectionType(); switch (connState) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index 2db7230c323..05b41637636 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -13,6 +13,8 @@ #include "ValveNetworkingSockets/steam/isteamnetworkingutils.h" #include "ValveNetworkingSockets/steam/steamnetworkingcustomsignaling.h" #include "../PluginInterfaces.h" +#include "ValveNetworkingSockets/steam/isteamnetworkingsockets.h" +#include "ValveNetworkingSockets/steam/steamnetworkingsockets.h" bool g_bForceRelay = false; UnsignedInt m_exeCRCOriginal = 0; @@ -568,6 +570,7 @@ NetworkMesh::NetworkMesh() SteamNetworkingUtils()->SetGlobalConfigValueInt32(k_ESteamNetworkingConfig_LogLevel_P2PRendezvous, k_ESteamNetworkingSocketsDebugOutputType_Error); // try a shutdown + g_bNetworkMeshDestroying.store(true); GameNetworkingSockets_Kill(); NGMP_OnlineServicesManager* pOnlineServicesMgr = NGMP_OnlineServicesManager::GetInstance(); @@ -658,6 +661,7 @@ NetworkMesh::NetworkMesh() } SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged(OnSteamNetConnectionStatusChanged); + g_bNetworkMeshDestroying.store(false); ESteamNetworkingSocketsDebugOutputType logType = #if defined(_DEBUG) @@ -736,12 +740,22 @@ void NetworkMesh::UpdateConnectivity(PlayerConnection* connection) int NetworkMesh::SendGamePacket(void* pBuffer, uint32_t totalDataSize, int64_t user_id) { + if (!pBuffer || totalDataSize == 0) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[SendGamePacket] CRITICAL: Received null pBuffer or zero size from user %lld, size=%u", static_cast(user_id), totalDataSize); + return -3; // Invalid buffer + } + + // Thread safety: Lock connection map during access + std::lock_guard lock(m_mapConnectionsMutex); + auto it = m_mapConnections.find(user_id); if (it != m_mapConnections.end()) { return it->second.SendGamePacket(pBuffer, totalDataSize); } + NetworkLog(ELogVerbosity::LOG_RELEASE, "[SendGamePacket] Connection not found for user %lld", static_cast(user_id)); return -2; } @@ -760,6 +774,9 @@ void NetworkMesh::SendACPacket(uint32_t userID, const void* pData, uint32_t data return; } + // Thread safety: Lock connection map during access + std::lock_guard lock(m_mapConnectionsMutex); + if (m_mapConnections.contains(userID)) { m_mapConnections[userID].SendACPacket(pData, dataLen); @@ -772,6 +789,9 @@ void NetworkMesh::SendACPacket(uint32_t userID, const void* pData, uint32_t data void NetworkMesh::StartConnectionSignalling(int64_t remoteUserID, uint16_t preferredPort) { + // Thread safety: Lock connection map during access + std::lock_guard lock(m_mapConnectionsMutex); + // if we already have a connection to this use, drop it, having a single-direction connection will break signalling auto it = m_mapConnections.find(remoteUserID); if (it != m_mapConnections.end()) @@ -864,15 +884,20 @@ void NetworkMesh::StartConnectionSignalling(int64_t remoteUserID, uint16_t prefe } // create a local user type - m_mapConnections[remoteUserID] = PlayerConnection(remoteUserID, hSteamConnection); + { + std::lock_guard lock(m_mapConnectionsMutex); + m_mapConnections[remoteUserID] = PlayerConnection(remoteUserID, hSteamConnection); - // add attempt - ++m_mapConnections[remoteUserID].m_SignallingAttempts; + // add attempt + ++m_mapConnections[remoteUserID].m_SignallingAttempts; + } } void NetworkMesh::DisconnectUser(int64_t remoteUserID) { + std::lock_guard lock(m_mapConnectionsMutex); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[DC] Dumping all Steam connections"); for (auto& kvPair : m_mapConnections) { @@ -1012,7 +1037,10 @@ void PlayerConnection::LiteUpdateForAC() SteamNetworkingMessage_t* msg = pMsg[iPacket]; if (!msg) { - return; + // CRITICAL BUG FIX: Don't return early - continue loop to release remaining messages + // Skipping null entry but continue processing others + NetworkLog(ELogVerbosity::LOG_DEBUG, "[AC PACKET] Received null message at index %d", iPacket); + continue; } const uint32_t numBytes = msg->m_cbSize; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp index 32f62ec4712..cab47bab65b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp @@ -17,6 +17,11 @@ NextGenTransport::NextGenTransport() { + // Initialize statistics tracking + m_statisticsSlot = 0; + m_lastSecond = timeGetTime(); + m_useLatency = FALSE; + m_usePacketLoss = FALSE; } NextGenTransport::~NextGenTransport() @@ -45,6 +50,13 @@ void NextGenTransport::reset(void) std::memset(m_outgoingBytes, 0, sizeof(m_outgoingBytes)); std::memset(m_unknownPackets, 0, sizeof(m_unknownPackets)); std::memset(m_unknownBytes, 0, sizeof(m_unknownBytes)); + + // Clear retry state for all outgoing packets + for (int i = 0; i < MAX_MESSAGES; ++i) + { + m_outPacketState[i].retryCount = 0; + m_inBufferOccupied[i] = false; // Mark all incoming slots as empty + } } Bool NextGenTransport::update(void) @@ -68,6 +80,20 @@ Bool NextGenTransport::doRecv(void) bool bRet = FALSE; int numRead = 0; + // Statistics gathering - advance slot every second (same as UDPTransport) + UnsignedInt now = timeGetTime(); + if (m_lastSecond + 1000 < now) + { + m_lastSecond = now; + m_statisticsSlot = (m_statisticsSlot + 1) % MAX_TRANSPORT_STATISTICS_SECONDS; + m_outgoingPackets[m_statisticsSlot] = 0; + m_outgoingBytes[m_statisticsSlot] = 0; + m_incomingPackets[m_statisticsSlot] = 0; + m_incomingBytes[m_statisticsSlot] = 0; + m_unknownPackets[m_statisticsSlot] = 0; + m_unknownBytes[m_statisticsSlot] = 0; + } + TransportMessage incomingMessage{}; std::memset(&incomingMessage, 0, sizeof(incomingMessage)); @@ -214,9 +240,14 @@ Bool NextGenTransport::doRecv(void) #if defined(RTS_DEBUG) || defined(RTS_INTERNAL) if (m_usePacketLoss) { - if (TheGlobalData->m_packetLoss >= GameClientRandomValue(0, 100)) + // Drop packet if random value is below loss percentage + // E.g., if m_packetLoss = 50, drop ~50% of packets + if (TheGlobalData->m_packetLoss > GameClientRandomValue(0, 100)) { // Simulated packet loss + NetworkLog(ELogVerbosity::LOG_DEBUG, + "Game Packet Recv: Simulated packet loss (loss%%=%d)", + TheGlobalData->m_packetLoss); continue; } } @@ -226,8 +257,23 @@ Bool NextGenTransport::doRecv(void) if (!isGenerals) { - NetworkLog(ELogVerbosity::LOG_RELEASE, - "Game Packet Recv: Is NOT a generals packet"); + // Check if it's a CRC failure or magic number failure to help diagnose corruption + if (incomingMessage.header.magic != GENERALS_MAGIC_NUMBER) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, + "Game Packet Recv: BAD MAGIC NUMBER - Expected 0x%04X, got 0x%04X from user %lld. " + "Packet is corrupted or from wrong game version.", + GENERALS_MAGIC_NUMBER, incomingMessage.header.magic, + static_cast(kvPair.second.m_userID)); + } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, + "Game Packet Recv: CRC MISMATCH - Expected 0x%08X, got 0x%08X from user %lld. " + "Packet is corrupted during transmission or has invalid payload length (%u).", + incomingMessage.header.crc, 0, // We'd need to compute the CRC to compare + static_cast(kvPair.second.m_userID), incomingMessage.length); + } m_unknownPackets[m_statisticsSlot]++; m_unknownBytes[m_statisticsSlot] += numBytes; continue; @@ -238,10 +284,23 @@ Bool NextGenTransport::doRecv(void) // Store into first free slot in m_inBuffer bool stored = false; + int fullCount = 0; for (int i = 0; i < MAX_MESSAGES; ++i) { - if (m_inBuffer[i].length != 0) + // Check if slot is occupied using flag, not length + // (length could be 0 for legitimate empty packets) + // However, if the packet has been consumed (length cleared to 0 by outside code), + // clear the occupied flag too + if (m_inBuffer[i].length == 0 && m_inBufferOccupied[i]) + { + m_inBufferOccupied[i] = false; + } + + if (m_inBufferOccupied[i]) + { + fullCount++; continue; + } // Clear slot std::memset(&m_inBuffer[i], 0, sizeof(m_inBuffer[i])); @@ -258,8 +317,10 @@ Bool NextGenTransport::doRecv(void) if (payloadLen > dstCap) { NetworkLog(ELogVerbosity::LOG_RELEASE, - "Game Packet Recv: Truncating payload from %u to %zu bytes for inBuffer[%d]", - payloadLen, dstCap, i); + "Game Packet Recv: WARNING - Truncating payload from %u to %zu bytes for inBuffer[%d] from user %lld. " + "This indicates the incoming packet exceeds the buffer capacity and data will be lost. " + "Consider increasing MAX_MESSAGE_LEN or MAX_PACKET_SIZE.", + payloadLen, dstCap, i, static_cast(kvPair.second.m_userID)); } std::memcpy(m_inBuffer[i].data, @@ -270,17 +331,24 @@ Bool NextGenTransport::doRecv(void) } else { + // Zero-length packet - store with length=0 but mark as occupied m_inBuffer[i].length = 0; } + // Mark slot as occupied + m_inBufferOccupied[i] = true; stored = true; break; } if (!stored) { + // Buffer is full - log this as it indicates potential packet loss NetworkLog(ELogVerbosity::LOG_RELEASE, - "Game Packet Recv: m_inBuffer full, dropping packet"); + "Game Packet Recv: ERROR - m_inBuffer is FULL (%d/%d slots occupied), dropping packet from user %lld. " + "Incoming packets will be lost until buffer slots are freed. " + "Consider increasing MAX_MESSAGES (%d) to handle higher packet rates.", + fullCount, MAX_MESSAGES, static_cast(kvPair.second.m_userID), MAX_MESSAGES); } else { @@ -304,7 +372,10 @@ Bool NextGenTransport::doSend(void) for (int i = 0; i < MAX_MESSAGES; ++i) { if (m_outBuffer[i].length == 0) + { + m_outPacketState[i].retryCount = 0; // Reset retry counter when packet slot is cleared continue; + } NGMP_OnlineServicesManager* pOnlineServicesManager = NGMP_OnlineServicesManager::GetInstance(); if (pOnlineServicesManager == nullptr) @@ -346,6 +417,7 @@ Bool NextGenTransport::doSend(void) totalLen, sizeof(TransportMessageHeader) + static_cast(MAX_PACKET_SIZE)); m_outBuffer[i].length = 0; // drop this entry + m_outPacketState[i].retryCount = 0; retval = FALSE; continue; } @@ -355,38 +427,77 @@ Bool NextGenTransport::doSend(void) { NetworkLog(ELogVerbosity::LOG_RELEASE, "Game Packet Send: No network mesh"); - m_outBuffer[i].length = 0; + // Don't clear the packet - retry next frame retval = FALSE; continue; } + // CRITICAL FIX: Create a temporary buffer with ONLY header + data + // Do NOT send the entire TransportMessage struct which contains metadata + // (length, addr, port fields that corrupt the packet on the wire) + std::vector packetData; + packetData.resize(totalLen); + + // Copy header + std::memcpy(packetData.data(), + &m_outBuffer[i].header, + sizeof(TransportMessageHeader)); + + // Copy payload data + std::memcpy(packetData.data() + sizeof(TransportMessageHeader), + m_outBuffer[i].data, + m_outBuffer[i].length); + int sendResult = pMesh->SendGamePacket( - static_cast(&m_outBuffer[i]), + packetData.data(), // Send only header + data, NOT entire struct totalLen, pSlot->m_userID); - retval = (sendResult >= 0); + if (sendResult >= 0) + { + // Send successful + ++numSent; + m_outgoingPackets[m_statisticsSlot]++; + m_outgoingBytes[m_statisticsSlot] += + m_outBuffer[i].length + sizeof(TransportMessageHeader); + m_outBuffer[i].length = 0; // Remove from queue + m_outPacketState[i].retryCount = 0; + retval = TRUE; + } + else + { + // Send failed - implement retry logic for transient errors + m_outPacketState[i].retryCount++; + + if (m_outPacketState[i].retryCount < OutgoingPacketState::MAX_RETRIES) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, + "Game Packet Send: SendGamePacket failed (err=%d), retry %d/%d for packet to user %lld", + sendResult, m_outPacketState[i].retryCount, + OutgoingPacketState::MAX_RETRIES, pSlot->m_userID); + // Keep packet in queue for retry + retval = FALSE; + } + else + { + // Max retries exceeded - drop packet + NetworkLog(ELogVerbosity::LOG_RELEASE, + "Game Packet Send: Dropping packet after %d failed retries to user %lld", + m_outPacketState[i].retryCount, pSlot->m_userID); + m_outBuffer[i].length = 0; + m_outPacketState[i].retryCount = 0; + retval = FALSE; + } + } } else { NetworkLog(ELogVerbosity::LOG_RELEASE, - "Game Packet Send: No slot for addr %u", m_outBuffer[i].addr); - retval = FALSE; - } - - if (retval) - { - ++numSent; - m_outgoingPackets[m_statisticsSlot]++; - m_outgoingBytes[m_statisticsSlot] += - m_outBuffer[i].length + sizeof(TransportMessageHeader); - m_outBuffer[i].length = 0; // Remove from queue - } - else - { - // Keep the entry? For now, drop it to avoid infinite retry loops. + "Game Packet Send: No slot for addr %u, dropping packet", m_outBuffer[i].addr); m_outBuffer[i].length = 0; + m_outPacketState[i].retryCount = 0; + retval = FALSE; } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index f9b7d756f4b..a9d3d15d446 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -1011,7 +1011,7 @@ void NGMP_OnlineServicesManager::InitSentry() sentry_options_set_dsn(options, "https://61750bebd112d279bcc286d617819269@o4509316925554688.ingest.us.sentry.io/4509316927586304"); sentry_options_set_database_path(options, strDumpPath.c_str()); - sentry_options_set_release(options, "generalsonline-client@042826_QFE2_EAC"); + sentry_options_set_release(options, "generalsonline-client@042826_QFE3_EAC"); #if defined(USE_TEST_ENV) sentry_options_set_environment(options, "test"); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp index 6441b6f9703..4d24d4869cc 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -79,6 +79,11 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) Functions.fnSetACIntegrityViolationOccurredCallback([](const char* szReason, int violationType) { + if (szReason == nullptr) + { + szReason = "(null reason)"; + } + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, local AC integrity violation occured (%d): %s.", violationType, szReason); g_bPendingExitLobby = true; }); @@ -88,39 +93,43 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) Functions.fnSetACActionRequiredCallback([](uint32_t userID, const char* szReason, EAnticheatActionType actionType, EAnticheatActionReason actionReason) { + if (szReason == nullptr) + { + szReason = "(null reason)"; + } + NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Action required."); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Action required: %s", szReason); 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; + return; + } + + // 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 { - // If it's us, leave, if its someone else, d/c them - if (pAuthInterface->GetUserID() == userID) + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Disconnecting remote user, lobby isn't secure, action was requested against remote user %u.", userID); + + NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); + if (pMesh != nullptr) { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, action was requested against local user."); - g_bPendingExitLobby = true; + pMesh->DisconnectUser(userID); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Disconnected: %u.", userID); } - else + else // no mesh, just back out { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Disconnecting remote user, lobby isn't secure, action was requested against remote user %u.", userID); - - 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; - } + 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; } } }); @@ -129,11 +138,21 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) AC_PLUGIN_LOAD_FUNCTION(SetSendMessageViaTransportCallback); Functions.fnSetSendMessageViaTransportCallback([](uint32_t goUserID, const void* pData, uint32_t dataLen) { + if (pData == nullptr || dataLen == 0) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: SendMessageViaTransport received null/empty data"); + return; + } + NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); if (pMesh != nullptr) { pMesh->SendACPacket(goUserID, pData, dataLen); } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: Cannot send AC packet - NetworkMesh is null"); + } }); // AC network message arrived callback @@ -162,6 +181,12 @@ bool AnticheatPlugInterface::g_bPendingExitLobby = false; void AnticheatPlugInterface::AC_NetworkMessageArrived(uint32_t goUserID, void* pData, uint32_t dataLen) { + if (pData == nullptr || dataLen == 0) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: AC_NetworkMessageArrived received null/empty data"); + return; + } + // TODO: Cache all of these getprocaddresses if (IsPluginLoaded() && Functions.fnACMessageArrivedViaTransport != nullptr) { @@ -173,7 +198,7 @@ void AnticheatPlugInterface::AC_NetworkMessageArrived(uint32_t goUserID, void* p void AnticheatPlugInterface::Authenticate() { - if (IsPluginLoaded() && Functions.fnLogin != nullptr && Functions.fnIsLoggedIn) + if (IsPluginLoaded() && Functions.fnLogin != nullptr && Functions.fnIsLoggedIn != nullptr) { NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); if (pAuthInterface == nullptr) @@ -192,10 +217,10 @@ void AnticheatPlugInterface::Authenticate() m_tokenCreationTime = std::chrono::duration_cast(std::chrono::utc_clock::now().time_since_epoch()).count(); - if (Functions.fnIsLoggedIn()) + if (Functions.fnIsLoggedIn != nullptr && Functions.fnIsLoggedIn()) { char buf[4196]; - if (Functions.fnGetMiddlewareAuthToken(buf, sizeof(buf))) + if (Functions.fnGetMiddlewareAuthToken != nullptr && Functions.fnGetMiddlewareAuthToken(buf, sizeof(buf))) { NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Got MW token: %s", buf); @@ -296,7 +321,7 @@ void AnticheatPlugInterface::Tick() Functions.fnTick(); // Do we need to refresh our token? - if (Functions.fnIsLoggedIn()) + if (Functions.fnIsLoggedIn != nullptr && Functions.fnIsLoggedIn()) { int64_t now = std::chrono::duration_cast(std::chrono::utc_clock::now().time_since_epoch()).count(); if (m_tokenCreationTime != -1 && now - m_tokenCreationTime >= 45 * 60 * 1000) // refresh every 45m, tokens last 60m, giving us a 15m buffer to refresh and retry if something goes wrong @@ -310,7 +335,7 @@ void AnticheatPlugInterface::Tick() void AnticheatPlugInterface::RefreshToken() { - if (IsPluginLoaded() && Functions.fnRefreshToken != nullptr && Functions.fnIsLoggedIn) + if (IsPluginLoaded() && Functions.fnRefreshToken != nullptr && Functions.fnIsLoggedIn != nullptr) { NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Refreshing token"); NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); From 2ec0d99c1098e03feddc1c12c48f904542082268 Mon Sep 17 00:00:00 2001 From: x64-dev <202863051+x64-dev@users.noreply.github.com> Date: Thu, 7 May 2026 17:46:45 -0400 Subject: [PATCH 11/11] - Latest plugin code, AC msgs now go via service --- .../MilesAudioDevice/MilesAudioManager.cpp | 52 +++++++++--- .../GeneralsOnline/OnlineServices_Init.h | 4 +- .../GameEngine/Source/GameClient/InGameUI.cpp | 1 + .../GeneralsOnline/OnlineServices_Init.cpp | 12 ++- .../OnlineServices_RoomsInterface.cpp | 25 ++++++ .../GeneralsOnline/PluginInterfaces.cpp | 82 +++++++++++++++---- 6 files changed, 146 insertions(+), 30 deletions(-) diff --git a/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp b/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp index 2a80aa1df5e..221303576c6 100644 --- a/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp +++ b/Core/GameEngineDevice/Source/MilesAudioDevice/MilesAudioManager.cpp @@ -480,6 +480,13 @@ void MilesAudioManager::reset() void MilesAudioManager::update() { AudioManager::update(); + + // Check audio device is initialized before updating + if (m_digitalHandle == nullptr) + { + return; // Audio device not ready, skip update + } + setDeviceListenerPosition(); processRequestList(); processPlayingList(); @@ -1193,7 +1200,10 @@ void MilesAudioManager::freeAllMilesHandles() std::list::iterator it; for ( it = m_availableSamples.begin(); it != m_availableSamples.end(); /* empty */ ) { HSAMPLE sample = *it; - AIL_release_sample_handle(sample); + if (sample != nullptr) + { + AIL_release_sample_handle(sample); + } it = m_availableSamples.erase(it); } m_num2DSamples = 0; @@ -1201,7 +1211,10 @@ void MilesAudioManager::freeAllMilesHandles() std::list::iterator it3D; for ( it3D = m_available3DSamples.begin(); it3D != m_available3DSamples.end(); /* empty */ ) { H3DSAMPLE sample3D = *it3D; - AIL_release_3D_sample_handle(sample3D); + if (sample3D != nullptr) + { + AIL_release_3D_sample_handle(sample3D); + } it3D = m_available3DSamples.erase(it3D); } m_num3DSamples = 0; @@ -1440,9 +1453,13 @@ AsciiString MilesAudioManager::getMusicTrackName() const void MilesAudioManager::openDevice() { if (!TheGlobalData->m_audioOn) { + m_digitalHandle = nullptr; return; } + // Always clear handle at start - only set if initialization succeeds + m_digitalHandle = nullptr; + AIL_set_redist_directory("MSS\\"); AIL_startup(); Int retval = 0; @@ -1453,22 +1470,31 @@ void MilesAudioManager::openDevice() retval = AIL_quick_startup(audioSettings->m_useDigital, audioSettings->m_useMidi, audioSettings->m_outputRate, audioSettings->m_outputBits, audioSettings->m_outputChannels); - // Quick handles tells us where to store the various devices. For now, we're only interested in the digital handle. + if (!retval) { + // Initialization failed - ensure m_digitalHandle stays nullptr and audio is disabled + m_digitalHandle = nullptr; + setOn(false, AudioAffect_All); + return; // EXIT EARLY - don't continue with invalid device + } + + // Only get handles if initialization succeeded AIL_quick_handles(&m_digitalHandle, nullptr, nullptr); - if (retval) { - buildProviderList(); - } else { - // if we couldn't initialize any devices, turn sound off (fail silently) - setOn( false, AudioAffect_All ); + // If we still don't have a valid handle, disable audio + if (m_digitalHandle == nullptr) { + setOn(false, AudioAffect_All); + return; } + // Device initialized successfully - proceed with setup + buildProviderList(); selectProvider(TheAudio->getProviderIndex(m_pref3DProvider)); // Now that we're all done, update the cached variables so that everything is in sync. TheAudio->refreshCachedVariables(); if (!isValidProvider()) { + m_digitalHandle = nullptr; // Mark as invalid if provider check fails return; } @@ -1478,9 +1504,13 @@ void MilesAudioManager::openDevice() //------------------------------------------------------------------------------------------------- void MilesAudioManager::closeDevice() { - freeAllMilesHandles(); - unselectProvider(); - AIL_shutdown(); + if (m_digitalHandle != nullptr) + { + freeAllMilesHandles(); + unselectProvider(); + AIL_shutdown(); + m_digitalHandle = nullptr; + } } //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Init.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Init.h index 579c22d6792..1e7300a6c35 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Init.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Init.h @@ -59,7 +59,7 @@ enum EWebSocketMessageID NETWORK_ROOM_MARK_READY = 5, LOBBY_CURRENT_LOBBY_UPDATE = 6, NETWORK_ROOM_LOBBY_LIST_UPDATE = 7, - UNUSED_PLACEHOLDER = 8, // this was relay upgrade, was removed. We can re-use it later, but service needs this placeholder + ANTICHEAT_MESSAGE = 8, PLAYER_NAME_CHANGE = 9, LOBBY_ROOM_CHAT_FROM_CLIENT = 10, LOBBY_CHAT_FROM_SERVER = 11, @@ -157,6 +157,8 @@ class WebSocket void SendData_Signalling(int64_t targetUserID, std::vector vecPayload); void SendData_StartGame(); + void SendData_ACMessage(int64_t targetUserID, std::vector vecPayload); + void SendData_ChangeLobbyPassword(UnicodeString& strNewPassword); void SendData_RemoveLobbyPassword(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index a528002b462..c6d22660465 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -101,6 +101,7 @@ #include "GameNetwork/NetworkInterface.h" extern NetworkInterface * TheNetwork; #endif +#include "ValveNetworkingSockets/steam/isteamnetworkingsockets.h" // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index a9d3d15d446..e024dbe51d7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -1011,7 +1011,7 @@ void NGMP_OnlineServicesManager::InitSentry() sentry_options_set_dsn(options, "https://61750bebd112d279bcc286d617819269@o4509316925554688.ingest.us.sentry.io/4509316927586304"); sentry_options_set_database_path(options, strDumpPath.c_str()); - sentry_options_set_release(options, "generalsonline-client@042826_QFE3_EAC"); + sentry_options_set_release(options, "generalsonline-client@042826_QFE4_EAC"); #if defined(USE_TEST_ENV) sentry_options_set_environment(options, "test"); @@ -1190,6 +1190,16 @@ void WebSocket::SendData_StartGame() } +void WebSocket::SendData_ACMessage(int64_t targetUserID, std::vector vecPayload) +{ + nlohmann::json j; + j["msg_id"] = EWebSocketMessageID::ANTICHEAT_MESSAGE; + j["target_user_id"] = targetUserID; + j["payload"] = vecPayload; + std::string strBody = j.dump(); + Send(strBody.c_str()); +} + void WebSocket::SendData_SubscribeRealtimeUpdates() { nlohmann::json j; diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp index 09462c1eb73..e2581367815 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp @@ -343,6 +343,15 @@ class WebSocketMessage_NetworkSignal : public WebSocketMessageBase NLOHMANN_DEFINE_TYPE_INTRUSIVE(WebSocketMessage_NetworkSignal, target_user_id, payload) }; +class WebSocketMessage_AnticheatMessage : public WebSocketMessageBase +{ +public: + int64_t target_user_id = -1; + std::vector payload; + + NLOHMANN_DEFINE_TYPE_INTRUSIVE(WebSocketMessage_AnticheatMessage, target_user_id, payload) +}; + class WebSocketMessage_ServerProbe : public WebSocketMessageBase { public: @@ -1129,6 +1138,22 @@ void WebSocket::Tick() } break; + case EWebSocketMessageID::ANTICHEAT_MESSAGE: + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] GOT AC MSG FROM WEBSOCKET!"); + + WebSocketMessage_AnticheatMessage acMsg; + bool bParsed = JSONGetAsObject(jsonObject, &acMsg); + + if (bParsed) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] AC Msg Signal User: %lld!", acMsg.target_user_id); + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] AC Msg Signal Payload Size: %d!", (int)acMsg.payload.size()); + AnticheatPlugInterface::AC_NetworkMessageArrived(acMsg.target_user_id, acMsg.payload.data(), acMsg.payload.size()); + } + } + break; + case EWebSocketMessageID::LOBBY_CURRENT_LOBBY_UPDATE: { // re-get the room info as it is stale diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp index 4d24d4869cc..0cd3df16864 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -8,14 +8,15 @@ 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); \ + NetworkLog(ELogVerbosity::LOG_RELEASE, "Failed to find " #funcName " function"); \ FreeLibrary(g_hACPluginModule); \ + g_hACPluginModule = nullptr; \ return; \ } bool AnticheatPlugInterface::IsExternalProcessRunning() { - if (IsPluginLoaded()) + if (IsPluginLoaded() && Functions.fnIsExternalProcessRunning != nullptr) { return Functions.fnIsExternalProcessRunning(); } @@ -25,7 +26,7 @@ bool AnticheatPlugInterface::IsExternalProcessRunning() int AnticheatPlugInterface::GetAnticheatIdentifier() { - if (IsPluginLoaded()) + if (IsPluginLoaded() && Functions.fnGetAnticheatIdentifier != nullptr) { return Functions.fnGetAnticheatIdentifier(); } @@ -35,6 +36,13 @@ int AnticheatPlugInterface::GetAnticheatIdentifier() void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) { + if (szPluginName == nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: Plugin name is null"); + m_bPluginLoadFailed = true; + return; + } + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Attempting to load plugin from %s", szPluginName); m_bPluginLoadFailed = false; @@ -71,7 +79,10 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) AC_PLUGIN_LOAD_FUNCTION(GetAnticheatIdentifier); #if _DEBUG - SetWindowText(ApplicationHWnd, Functions.fnIsExternalProcessRunning() ? "SECURED" : "INSECURE"); + if (ApplicationHWnd != nullptr) + { + SetWindowText(ApplicationHWnd, Functions.fnIsExternalProcessRunning() ? "SECURED" : "INSECURE"); + } #endif // integrity callback @@ -111,7 +122,8 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) } // If it's us, leave, if its someone else, d/c them - if (pAuthInterface->GetUserID() == userID) + uint32_t localUserID = pAuthInterface->GetUserID(); + if (localUserID == userID) { NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Leaving lobby, lobby isn't secure, action was requested against local user."); g_bPendingExitLobby = true; @@ -144,14 +156,45 @@ void AnticheatPlugInterface::LoadPlugin(const char* szPluginName) return; } - NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); - if (pMesh != nullptr) + // prefer websocket if we have it, otherwise fall back to p2p mesh + bool bFallbackToP2P = false; + std::shared_ptr pWS = NGMP_OnlineServicesManager::GetWebSocket(); + if (pWS != nullptr) { - pMesh->SendACPacket(goUserID, pData, dataLen); + if (pWS->IsConnected()) + { + if (dataLen > 0) + { + std::vector vecPayload((uint8_t*)pData, (uint8_t*)pData + dataLen); + pWS->SendData_ACMessage(goUserID, vecPayload); + } + else + { + bFallbackToP2P = true; + } + } + else + { + bFallbackToP2P = true; + } } else { - NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: Cannot send AC packet - NetworkMesh is null"); + bFallbackToP2P = true; + } + + if (bFallbackToP2P) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] AC Packets - WebSocket unavailable, falling back to P2P"); + NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); + if (pMesh != nullptr) + { + pMesh->SendACPacket(goUserID, pData, dataLen); + } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: Cannot send AC packet - NetworkMesh is null"); + } } }); @@ -206,7 +249,8 @@ void AnticheatPlugInterface::Authenticate() return; } - Functions.fnLogin(pAuthInterface->GetAuthToken().c_str(), + std::string authToken = pAuthInterface->GetAuthToken(); + Functions.fnLogin(authToken.c_str(), [](bool bSuccess) { if (!bSuccess) @@ -226,12 +270,14 @@ void AnticheatPlugInterface::Authenticate() // Now we can begin login NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); - if (pAuthInterface == nullptr) + if (pAuthInterface != nullptr) { - return; + pAuthInterface->SendMiddlewareToken(std::string(buf)); + } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: Auth interface became null during login callback"); } - - pAuthInterface->SendMiddlewareToken(std::string(buf)); } } else @@ -289,7 +335,7 @@ bool AnticheatPlugInterface::RegisterPlayer(std::string mwUserID, uint32_t goUse if (IsPluginLoaded() && Functions.fnRegisterPlayer != nullptr) { - NetworkLog(ELogVerbosity::LOG_RELEASE, "RegisterPlayer: %s to %" PRIu64, mwUserID.c_str(), goUserID); + NetworkLog(ELogVerbosity::LOG_RELEASE, "RegisterPlayer: %s to %" PRIu32, mwUserID.c_str(), goUserID); bool bReg = Functions.fnRegisterPlayer(mwUserID.c_str(), goUserID); NetworkLog(ELogVerbosity::LOG_RELEASE, "RegisterPlayerFunc result: %d", bReg); @@ -304,7 +350,7 @@ bool AnticheatPlugInterface::DeregisterPlayer(std::string mwUserID, uint32_t goU { if (IsPluginLoaded() && Functions.fnDeregisterPlayer != nullptr) { - NetworkLog(ELogVerbosity::LOG_RELEASE, "DeregisterPlayer: %s to %" PRIu64, mwUserID.c_str(), goUserID); + NetworkLog(ELogVerbosity::LOG_RELEASE, "DeregisterPlayer: %s to %" PRIu32, mwUserID.c_str(), goUserID); bool bReg = Functions.fnDeregisterPlayer(mwUserID.c_str(), goUserID); NetworkLog(ELogVerbosity::LOG_RELEASE, "DeregisterPlayerFunc result: %d", bReg); @@ -346,12 +392,14 @@ void AnticheatPlugInterface::RefreshToken() m_tokenCreationTime = std::chrono::duration_cast(std::chrono::utc_clock::now().time_since_epoch()).count(); - Functions.fnRefreshToken(pAuthInterface->GetAuthToken().c_str(), + std::string authToken = pAuthInterface->GetAuthToken(); + Functions.fnRefreshToken(authToken.c_str(), [](bool bSuccess) { NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Refreshed token: %d", bSuccess); if (!bSuccess) { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: Token refresh failed"); // TODO_AC: Handle this, its a fatal error return; }