From 91f21934d785659d91d8d90670e5a902d86ef480 Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:27:26 +0200 Subject: [PATCH 1/6] fix(mesh): Give joiners their TURN credentials before they start signalling --- .../GameNetwork/GeneralsOnline/NetworkMesh.h | 14 ++++++ .../GeneralsOnline/NetworkMesh.cpp | 47 ++++++++++++++++++- .../OnlineServices_LobbyInterface.cpp | 13 +++++ 3 files changed, 73 insertions(+), 1 deletion(-) 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/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index d267e5364d9..c9533359a91 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -871,6 +871,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 +1000,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_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index 01dfdfdcc59..45855f79ddb 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp @@ -1191,6 +1191,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 +1255,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 +1556,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) { From ed8ce9f0ae3450fe4a821ae74f549df12c12ee67 Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:50:23 +0200 Subject: [PATCH 2/6] fix(lobby): Keep a running match's connections when a lobby lookup returns 404 --- .../GeneralsOnline/OnlineServices_LobbyInterface.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.cpp index 45855f79ddb..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; } From df875de350311edf4eb3d9e3d580340c458eee21 Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:50:30 +0200 Subject: [PATCH 3/6] fix(anticheat): Clear a pending lobby exit when its session ends --- .../Include/GameNetwork/GeneralsOnline/PluginInterfaces.h | 8 ++++++-- .../GameNetwork/GeneralsOnline/PluginInterfaces.cpp | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) 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/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) { From 3f1ebe4a71067fa3f1bd90a1d1f4a18a1c284728 Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:51:15 +0200 Subject: [PATCH 4/6] fix(mesh): Keep re-signalling a broken link for the whole match --- .../Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index c9533359a91..fbac81742c9 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); From dfe5b29d13f81d37f4e0b9a0d622b4b5d9112fa3 Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:51:48 +0200 Subject: [PATCH 5/6] fix(mesh): Never make the lobby host leave over a peer it can't reach --- .../Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index fbac81742c9..8d4c474857c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -175,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); From cc132f02f0b7b6048e93cabf798916e1de77c27c Mon Sep 17 00:00:00 2001 From: tintinhamans <5984296+tintinhamans@users.noreply.github.com> Date: Tue, 29 Sep 2026 22:52:05 +0200 Subject: [PATCH 6/6] fix(online): Keep the last good service config when a refresh fails --- .../GeneralsOnline/OnlineServices_Init.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) 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)