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..814c87b9e07 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" /////////////////////////////////////////////////////////////////////////////////////// @@ -649,15 +650,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; } @@ -865,19 +866,45 @@ 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) bNeedsUpdate = false; #endif - 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/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/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/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/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..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; @@ -43,6 +44,8 @@ class PlayerConnection int SendGamePacket(void* pBuffer, uint32_t totalDataSize); + void SendACPacket(const void* pData, uint32_t dataLen); + void UpdateLatencyHistogram(); bool IsIPV4(); @@ -91,6 +94,8 @@ class PlayerConnection int ComputeConnectionScore(); HSteamNetConnection m_hSteamConnection = k_HSteamNetConnection_Invalid; + + void LiteUpdateForAC(); }; struct LobbyMemberEntry; @@ -166,12 +171,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(); @@ -198,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/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_Init.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_Init.h index 1af0cb98124..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, @@ -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 @@ -155,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/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h index a712cbbd79c..423022c53a3 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 @@ -83,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 new file mode 100644 index 00000000000..8c96db5f32f --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h @@ -0,0 +1,118 @@ +#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 +}; + + +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 && !m_bPluginLoadFailed; + } + + static bool IsExternalProcessRunning(); + + static int GetAnticheatIdentifier(); + + static void LoadPlugin(const char* szPluginName); + static void Authenticate(); + 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); + + 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); + + typedef void (*FuncDefCIntegrityViolationOccurredCallbackFunc)(const char*, int); + typedef void (*FuncDefSetACIntegrityViolationOccurredCallback)(FuncDefCIntegrityViolationOccurredCallbackFunc); + + // Func defs + typedef void (*FuncDefSetLoggingFunction)(LoggingFunc); + typedef int (*FuncDefInitialize)(void); + typedef bool (*FuncDefIsExternalProcessRunning)(void); + + typedef int (*FuncDefGetAnticheatIdentifier)(void); + + 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); + 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; + FuncDefIsExternalProcessRunning fnIsExternalProcessRunning = nullptr; + FuncDefGetAnticheatIdentifier fnGetAnticheatIdentifier = nullptr; + FuncDefSetACActionRequiredCallback fnSetACActionRequiredCallback = nullptr; + FuncDefSetACIntegrityViolationOccurredCallback fnSetACIntegrityViolationOccurredCallback = nullptr; + FuncDefSetSendMessageViaTransportCallback fnSetSendMessageViaTransportCallback = nullptr; + FuncDefACMessageArrivedViaTransport fnACMessageArrivedViaTransport = nullptr; + FuncDefLogin fnLogin = nullptr; + FuncDefRefreshToken fnRefreshToken = nullptr; + FuncDefGetMiddlewareAuthToken fnGetMiddlewareAuthToken = nullptr; + FuncDefIsLoggedIn fnIsLoggedIn = nullptr; + 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; + + static int64_t m_tokenCreationTime; +}; + +extern HWND ApplicationHWnd; 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(); 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..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) { @@ -2343,6 +2342,15 @@ void WOLGameSetupMenuUpdate( WindowLayout * layout, void *userData) return; } + 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(); @@ -3542,7 +3550,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/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/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/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/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/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/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index e6fa595230a..05b41637636 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -12,6 +12,9 @@ #include "../OnlineServices_Init.h" #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; @@ -567,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(); @@ -657,6 +661,7 @@ NetworkMesh::NetworkMesh() } SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged(OnSteamNetConnectionStatusChanged); + g_bNetworkMeshDestroying.store(false); ESteamNetworkingSocketsDebugOutputType logType = #if defined(_DEBUG) @@ -733,38 +738,60 @@ void NetworkMesh::UpdateConnectivity(PlayerConnection* connection) }); } - -bool NetworkMesh::HasGamePacket() -{ - return !m_queueQueuedGamePackets.empty(); -} - -QueuedGamePacket NetworkMesh::RecvGamePacket() +int NetworkMesh::SendGamePacket(void* pBuffer, uint32_t totalDataSize, int64_t user_id) { - if (HasGamePacket()) + if (!pBuffer || totalDataSize == 0) { - QueuedGamePacket frontPacket = m_queueQueuedGamePackets.front(); - m_queueQueuedGamePackets.pop(); - return frontPacket; + 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 } - return QueuedGamePacket(); -} - -int NetworkMesh::SendGamePacket(void* pBuffer, uint32_t totalDataSize, int64_t user_id) -{ + // 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; } +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; + } + + // Thread safety: Lock connection map during access + std::lock_guard lock(m_mapConnectionsMutex); + + if (m_mapConnections.contains(userID)) + { + m_mapConnections[userID].SendACPacket(pData, dataLen); + } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Send Packet ERR - user %u not found in connections", userID); + } +} + 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()) @@ -857,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) { @@ -973,8 +1005,89 @@ 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) + { + // 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; + + // 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); + + // Check minimum packet size for AC header + if (numBytes >= 3) + { + 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); + 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(m_userID)); + msg->Release(); + continue; + } + + // not an AC packet, we dont care + NetworkLog(ELogVerbosity::LOG_DEBUG, "[AC PACKET] Received NON AC message"); + msg->Release(); + } +} PlayerConnection::PlayerConnection(int64_t userID, HSteamNetConnection hSteamConnection) { @@ -995,6 +1108,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(); @@ -1037,6 +1168,37 @@ 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); + + vecData[0] = 9; + vecData[1] = 1; + 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, 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 +1371,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..cab47bab65b 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... @@ -15,6 +17,11 @@ NextGenTransport::NextGenTransport() { + // Initialize statistics tracking + m_statisticsSlot = 0; + m_lastSecond = timeGetTime(); + m_useLatency = FALSE; + m_usePacketLoss = FALSE; } NextGenTransport::~NextGenTransport() @@ -43,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) @@ -66,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)); @@ -109,6 +137,43 @@ 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); + + // Check minimum packet size for AC header + if (numBytes >= 3) + { + 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); + 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; + } + NetworkLog(ELogVerbosity::LOG_DEBUG, "[GAME PACKET] Received message of size %u from user %lld", numBytes, static_cast(kvPair.second.m_userID)); @@ -175,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; } } @@ -187,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; @@ -199,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])); @@ -219,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, @@ -231,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 { @@ -265,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) @@ -307,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; } @@ -316,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_Auth.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp index 32ad81664c1..fe7a2e0dc69 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Auth.cpp @@ -139,6 +139,36 @@ void NGMP_OnlineServices_AuthInterface::GoToDetermineNetworkCaps() }); } +void NGMP_OnlineServices_AuthInterface::SendMiddlewareToken(std::string strMWToken) +{ + 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 +413,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..e024dbe51d7 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,6 +831,15 @@ 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(pluginPath.c_str()); +#else + AnticheatPlugInterface::LoadPlugin(pluginPath.c_str()); +#endif + // TODO_NGMP: Better location // TODO_NGMP: Get all of this from the service int moneyVal = 100000; @@ -863,6 +874,8 @@ void NGMP_OnlineServicesManager::Init() void NGMP_OnlineServicesManager::Tick() { + AnticheatPlugInterface::Tick(); + // screenshots { // send screenshot @@ -998,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_QFE4_EAC"); #if defined(USE_TEST_ENV) sentry_options_set_environment(options, "test"); @@ -1177,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_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index faa825d863f..b12aee927c1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -901,6 +901,7 @@ void NGMP_OnlineServices_LobbyInterface::UpdateRoomDataCache(std::functionGetAndParseServiceConfig([=]() { m_CurrentLobby = LobbyEntry(); @@ -1298,6 +1314,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(); @@ -1407,6 +1424,14 @@ 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_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/OnlineServices_RoomsInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_RoomsInterface.cpp index 9cee01aee2b..e2581367815 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: @@ -325,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: @@ -968,6 +995,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; @@ -1079,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 new file mode 100644 index 00000000000..0cd3df16864 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -0,0 +1,427 @@ +#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"); \ + FreeLibrary(g_hACPluginModule); \ + g_hACPluginModule = nullptr; \ + return; \ + } + +bool AnticheatPlugInterface::IsExternalProcessRunning() +{ + if (IsPluginLoaded() && Functions.fnIsExternalProcessRunning != nullptr) + { + return Functions.fnIsExternalProcessRunning(); + } + + return false; +} + +int AnticheatPlugInterface::GetAnticheatIdentifier() +{ + if (IsPluginLoaded() && Functions.fnGetAnticheatIdentifier != nullptr) + { + return Functions.fnGetAnticheatIdentifier(); + } + + return 0; +} + +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; + 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); + } + 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(IsExternalProcessRunning); + + AC_PLUGIN_LOAD_FUNCTION(GetAnticheatIdentifier); + +#if _DEBUG + if (ApplicationHWnd != nullptr) + { + SetWindowText(ApplicationHWnd, Functions.fnIsExternalProcessRunning() ? "SECURED" : "INSECURE"); + } +#endif + + // integrity callback + AC_PLUGIN_LOAD_FUNCTION(SetACIntegrityViolationOccurredCallback); + + 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; + }); + + // set action required callback + AC_PLUGIN_LOAD_FUNCTION(SetACActionRequiredCallback); + + 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: %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 + 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; + } + 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 + 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; + } + + // 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) + { + 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 + { + 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"); + } + } + }); + + // AC network message arrived callback + AC_PLUGIN_LOAD_FUNCTION(ACMessageArrivedViaTransport); + + // Login funcs + AC_PLUGIN_LOAD_FUNCTION(Login); + AC_PLUGIN_LOAD_FUNCTION(RefreshToken); + 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); + AC_PLUGIN_LOAD_FUNCTION(DeregisterPlayer); + + AC_PLUGIN_LOAD_FUNCTION(Tick); + AC_PLUGIN_LOAD_FUNCTION(Shutdown); + } +} + +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) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] fnOnMessageArrivedViaTransport"); + Functions.fnACMessageArrivedViaTransport(goUserID, pData, dataLen); + } +} + + +void AnticheatPlugInterface::Authenticate() +{ + if (IsPluginLoaded() && Functions.fnLogin != nullptr && Functions.fnIsLoggedIn != nullptr) + { + NGMP_OnlineServices_AuthInterface* pAuthInterface = NGMP_OnlineServicesManager::GetInterface(); + if (pAuthInterface == nullptr) + { + return; + } + + std::string authToken = pAuthInterface->GetAuthToken(); + Functions.fnLogin(authToken.c_str(), + [](bool bSuccess) + { + if (!bSuccess) + { + // TODO_AC: Handle this, its a fatal error + return; + } + + m_tokenCreationTime = std::chrono::duration_cast(std::chrono::utc_clock::now().time_since_epoch()).count(); + + if (Functions.fnIsLoggedIn != nullptr && Functions.fnIsLoggedIn()) + { + char buf[4196]; + if (Functions.fnGetMiddlewareAuthToken != nullptr && 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) + { + pAuthInterface->SendMiddlewareToken(std::string(buf)); + } + else + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] ERROR: Auth interface became null during login callback"); + } + } + } + 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) + { + 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"); + } +} + +void AnticheatPlugInterface::EndSession() +{ + if (IsPluginLoaded() && Functions.fnEndSession != nullptr) + { + Functions.fnEndSession(); + g_bSessionStarted = false; + } +} + +AnticheatPlugInterface::AnticheatPluginFunctionPtrs AnticheatPlugInterface::Functions; + +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 + { + AnticheatPlugInterface::BeginSession(); + } + + if (IsPluginLoaded() && Functions.fnRegisterPlayer != nullptr) + { + 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); + return bReg; + } + + return false; +} + + +bool AnticheatPlugInterface::DeregisterPlayer(std::string mwUserID, uint32_t goUserID) +{ + if (IsPluginLoaded() && Functions.fnDeregisterPlayer != nullptr) + { + 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); + return bReg; + } + + return false; +} + +void AnticheatPlugInterface::Tick() +{ + if (IsPluginLoaded() && Functions.fnTick != nullptr) + { + Functions.fnTick(); + + // Do we need to refresh our token? + 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 + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[AC] Token is about to expire, refreshing..."); + RefreshToken(); + } + } + } +} + +void AnticheatPlugInterface::RefreshToken() +{ + if (IsPluginLoaded() && Functions.fnRefreshToken != nullptr && Functions.fnIsLoggedIn != nullptr) + { + 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(); + + 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; + } + }); + } +} + +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"); + } +} 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");