diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h index 1081d2e1486..fe3c7ac9c6c 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h @@ -218,6 +218,11 @@ class NetworkMesh void SendACPacket(uint32_t userID, const void* pData, uint32_t dataLen); void StartConnectionSignalling(const char* szMiddlewareID, int64_t remoteUserID, uint16_t preferredPort); + + // A joiner gets its TURN credentials after its mesh exists; signalling waits for them + void AwaitTurnCredentials(); + void SetTurnCredentials(const std::string& strUsername, const std::string& strToken); + void DisconnectUser(int64_t remoteUserID); void Disconnect(); @@ -272,6 +277,15 @@ class NetworkMesh std::string m_strTurnTokenString; std::string m_strTurnServerList; + struct PendingSignalling + { + std::string strMiddlewareID; + int64_t remoteUserID; + uint16_t preferredPort; + }; + bool m_bAwaitingTurnCredentials = false; + std::vector m_vecSignallingAwaitingTurn; + // k_nSteamNetworkingConfig_P2P_Transport_ICE_Enable_* for this lobby's connections. int m_iceEnable = 0; int m_iceImplementation = 2; diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h index df99b6e40aa..41a5345d336 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/PluginInterfaces.h @@ -1,5 +1,7 @@ #pragma once +#include + enum class EConnectionState : uint8_t { NOT_CONNECTED, @@ -55,7 +57,8 @@ enum class EAnticheatActionReason : int32_t class AnticheatPlugInterface { public: - static bool g_bPendingExitLobby; + // set from plugin threads + static std::atomic g_bPendingExitLobby; static void AC_NetworkMessageArrived(uint32_t goUserID, void* pData, uint32_t dataLen); @@ -207,7 +210,8 @@ extern HWND ApplicationHWnd; class AnticheatPlugInterface { public: - static bool g_bPendingExitLobby; + // set from plugin threads + static std::atomic g_bPendingExitLobby; static void AC_NetworkMessageArrived(uint32_t goUserID, void* pData, uint32_t dataLen) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index d267e5364d9..8d4c474857c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -112,7 +112,9 @@ void OnSteamNetConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t NGMP_OnlineServices_LobbyInterface* pJoinOrderLobby = NGMP_OnlineServicesManager::GetInterface(); const bool bWeJoinedLater = pJoinOrderLobby == nullptr || !pJoinOrderLobby->IsJoinOrderKnown() || pJoinOrderLobby->JoinedAfter(userID); const bool bPeerLeft = pJoinOrderLobby != nullptr && pJoinOrderLobby->IsJoinOrderKnown() && !pJoinOrderLobby->IsLobbyMember(userID); - bool bShouldRetry = serviceConf.retry_signalling && ((!bWeJoinedLater && !bPeerLeft) || signallingAttemptsBeforeDisconnect < numSignallingAttempts); + // a match can't leave its lobby, so keep repairing the link until the game drops the player + const bool bInMatch = TheGameLogic != nullptr && TheGameLogic->isInInternetGame(); + bool bShouldRetry = serviceConf.retry_signalling && (bInMatch || (!bWeJoinedLater && !bPeerLeft) || signallingAttemptsBeforeDisconnect < numSignallingAttempts); bool bWasError = pInfo->m_info.m_eState == k_ESteamNetworkingConnectionState_ProblemDetectedLocally || pInfo->m_info.m_eEndReason != k_ESteamNetConnectionEnd_App_Generic; plrConnection.SetDisconnected(bWasError, pMesh, bShouldRetry && bWasError); @@ -173,7 +175,12 @@ void OnSteamNetConnectionStatusChanged(SteamNetConnectionStatusChangedCallback_t NetworkLog(ELogVerbosity::LOG_RELEASE, "[STEAM NETWORKING][DISCONNECT HANDLER] Not retrying, handling disconnect as failure..."); NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); - if (pLobbyInterface != nullptr) + if (pLobbyInterface != nullptr && pLobbyInterface->IsHost()) + { + // the host keeps its lobby; the peer that can't connect is the one to go + NetworkLog(ELogVerbosity::LOG_RELEASE, "[STEAM NETWORKING][DISCONNECT HANDLER] Not leaving, we host this lobby; dropping user %lld only", userID); + } + else if (pLobbyInterface != nullptr) { NetworkLog(ELogVerbosity::LOG_RELEASE, "[STEAM NETWORKING][DISCONNECT HANDLER] Performing local removal for user %lld from lobby due to failure to connect\n", userID); @@ -871,6 +878,14 @@ void NetworkMesh::StartConnectionSignalling(const char* szMiddlewareID, int64_t } else { + // no relay without our TURN credentials + if (m_bAwaitingTurnCredentials) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[SIGNAL] Holding signalling with %lld until our TURN credentials arrive", remoteUserID); + m_vecSignallingAwaitingTurn.push_back({ szMiddlewareID != nullptr ? szMiddlewareID : "", remoteUserID, preferredPort }); + return; + } + // if we already have a connection to this use, drop it, having a single-direction connection will break signalling int previousAttempts = 0; auto it = m_mapConnections.find(remoteUserID); @@ -992,7 +1007,44 @@ void NetworkMesh::StartConnectionSignalling(const char* szMiddlewareID, int64_t m_mapConnections[remoteUserID].m_SignallingAttempts = previousAttempts + 1; } } - + +} + +void NetworkMesh::AwaitTurnCredentials() +{ + std::lock_guard lock(m_mapConnectionsMutex); + m_bAwaitingTurnCredentials = true; +} + +void NetworkMesh::SetTurnCredentials(const std::string& strUsername, const std::string& strToken) +{ + std::vector vecPending; + { + std::lock_guard lock(m_mapConnectionsMutex); + + m_strTurnUsername = strUsername; + m_strTurnToken = strToken; + m_strTurnUsernameString = std::format("{},{}", m_strTurnUsername.c_str(), m_strTurnUsername.c_str()); + m_strTurnTokenString = std::format("{},{}", m_strTurnToken.c_str(), m_strTurnToken.c_str()); + + // incoming connections use the listen socket's TURN settings + if (m_hListenSock != k_HSteamListenSocket_Invalid) + { + SteamNetworkingUtils()->SetConfigValue(k_ESteamNetworkingConfig_P2P_TURN_UserList, k_ESteamNetworkingConfig_ListenSocket, + (intptr_t)m_hListenSock, k_ESteamNetworkingConfig_String, m_strTurnUsernameString.c_str()); + SteamNetworkingUtils()->SetConfigValue(k_ESteamNetworkingConfig_P2P_TURN_PassList, k_ESteamNetworkingConfig_ListenSocket, + (intptr_t)m_hListenSock, k_ESteamNetworkingConfig_String, m_strTurnTokenString.c_str()); + } + + m_bAwaitingTurnCredentials = false; + vecPending.swap(m_vecSignallingAwaitingTurn); + } + + NetworkLog(ELogVerbosity::LOG_RELEASE, "[SIGNAL] Got TURN credentials, starting %d held signalling request(s)", (int)vecPending.size()); + for (const PendingSignalling& pending : vecPending) + { + StartConnectionSignalling(pending.strMiddlewareID.c_str(), pending.remoteUserID, pending.preferredPort); + } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp index 741f156d0c4..47b7bd8318e 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_Init.cpp @@ -82,21 +82,14 @@ void NGMP_OnlineServicesManager::GetAndParseServiceConfig(std::functionm_ServiceConfig = ServiceConfig(); + // keep the last good config (defaults if there never was one); resetting would turn off retry_signalling + NetworkLog(ELogVerbosity::LOG_RELEASE, "[NGMP] Failed to get service config, keeping the current one. Status code: %d", statusCode); } } catch (...) { - // It's OK to fail, we'll just use the sensible defaults - NetworkLog(ELogVerbosity::LOG_RELEASE, "[NGMP] Failed to get service config, using defaults. Exception."); - NGMP_OnlineServicesManager* pMgr = NGMP_OnlineServicesManager::GetInstance(); - if (pMgr != nullptr) - { - pMgr->m_ServiceConfig = ServiceConfig(); - } + NetworkLog(ELogVerbosity::LOG_RELEASE, "[NGMP] Failed to parse service config, keeping the current one."); } if (cbOnDone != nullptr) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index 01dfdfdcc59..106d2f9621b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -891,6 +891,13 @@ void NGMP_OnlineServices_LobbyInterface::UpdateRoomDataCache(std::functionisGameInProgress()) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[NGMP] Lobby lookup returned 404 during a match, keeping the match running"); + return; + } + LeaveCurrentLobby(); return; } @@ -1191,6 +1198,9 @@ void NGMP_OnlineServices_LobbyInterface::JoinLobby(LobbyEntry lobbyInfo, std::st m_pLobbyMesh = pNewMesh; } + // TURN credentials arrive with the join response + m_pLobbyMesh->AwaitTurnCredentials(); + // convert NGMP_OnlineServicesManager::GetInstance()->GetHTTPManager()->SendPUTRequest(strURI.c_str(), EIPProtocolVersion::DONT_CARE, mapHeaders, strPostData.c_str(), [=](bool bSuccess, int statusCode, std::string strBody, HTTPRequest* pReq) { @@ -1252,6 +1262,11 @@ void NGMP_OnlineServices_LobbyInterface::JoinLobby(LobbyEntry lobbyInfo, std::st } + if (m_pLobbyMesh != nullptr) + { + m_pLobbyMesh->SetTurnCredentials(m_strTURNUsername, m_strTURNToken); + } + // for safety if (TheNGMPGame != nullptr) { @@ -1548,6 +1563,11 @@ void NGMP_OnlineServices_LobbyInterface::CreateLobby(UnicodeString strLobbyName, m_strTURNToken = resp.turn_token; NetworkLog(ELogVerbosity::LOG_DEBUG, "Got TURN username: %s, token: %s", m_strTURNUsername.c_str(), m_strTURNToken.c_str()); + // a mesh kept from a failed join has stale credentials + if (m_pLobbyMesh != nullptr) + { + m_pLobbyMesh->SetTurnCredentials(m_strTURNUsername, m_strTURNToken); + } if (resp.result == ECreateLobbyResponseResult::SUCCEEDED) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp index 2fb69477c78..92c578d7f0b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp @@ -5,7 +5,7 @@ #include "../OnlineServices_Auth.h" #include "../OnlineServices_LobbyInterface.h" -bool AnticheatPlugInterface::g_bPendingExitLobby = false; +std::atomic AnticheatPlugInterface::g_bPendingExitLobby = false; #if defined(GENERALS_ONLINE_USE_PLUGINS_INTERFACE) @@ -475,6 +475,9 @@ void AnticheatPlugInterface::BeginSession() void AnticheatPlugInterface::EndSession() { + // a stale exit request would eject us from the next lobby + g_bPendingExitLobby = false; + #if defined(AC_ENABLED) if (IsPluginLoaded() && Functions.fnEndSession != nullptr) {