diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h index b59bacccd96..7db2c420448 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/NetworkMesh.h @@ -69,6 +69,7 @@ class PlayerConnection } std::vector m_vecLatencyHistory; + std::vector m_vecQualityHistory; std::string GetStats(); std::string GetConnectionType(); @@ -85,7 +86,9 @@ class PlayerConnection int m_SignallingAttempts = 0; int GetLatency(); + int GetJitter(); float GetConnectionQuality(); + int ComputeConnectionScore(); HSteamNetConnection m_hSteamConnection = k_HSteamNetConnection_Invalid; }; 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 5fa5b071487..df44287abae 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/GUICallbacks/Menus/WOLGameSetupMenu.cpp @@ -73,6 +73,7 @@ NGMPGame* TheNGMPGame = NULL; void WOLDisplaySlotList( void ); +static void WOLRefreshConnectionIndicators( void ); extern std::list TheLobbyQueuedUTMs; @@ -499,7 +500,10 @@ static void playerTooltip(GameWindow *window, } bool bIsConnected = false; - int latency = -1; + int connectionScore = -1; + int connectionLatency = -1; + int connectionJitter = -1; + int connectionQualityPct = -1; std::string strConnectionType = ""; LobbyMemberEntry member = pLobbyInterface->GetRoomMemberFromID(slot->m_userID); @@ -521,7 +525,11 @@ static void playerTooltip(GameWindow *window, { bIsConnected = false; } - latency = pConnection->GetLatency(); + connectionScore = pConnection->ComputeConnectionScore(); + connectionLatency = pConnection->GetLatency(); + connectionJitter = pConnection->GetJitter(); + float rawQuality = pConnection->GetConnectionQuality(); + connectionQualityPct = (rawQuality >= 0.0f) ? static_cast(rawQuality * 100.0f) : -1; } } } @@ -534,13 +542,19 @@ static void playerTooltip(GameWindow *window, } else if (bIsConnected) { - playerInfo.format(L"\nConnection State: Connected (%hs)\nLatency: %d ms\nRegion: %hs\nWins: %d\nLosses: %d\nDisconnects: %d\nFavorite Army: %s", - strConnectionType.c_str(), latency, member.region.c_str(), totalWins, totalLosses, totalDiscons, favoriteSide.str()); + UnicodeString scoreStr, latencyStr, jitterStr, qualityStr; + if (connectionScore >= 0) scoreStr.format(L"%d%%", connectionScore); else scoreStr = L"Unknown"; + if (connectionLatency >= 0) latencyStr.format(L"%d ms", connectionLatency); else latencyStr = L"Unknown"; + if (connectionJitter >= 0) jitterStr.format(L"%d ms", connectionJitter); else jitterStr = L"Unknown"; + if (connectionQualityPct >= 0) qualityStr.format(L"%d%%", connectionQualityPct); else qualityStr = L"Unknown"; + playerInfo.format(L"\nConnection State: Connected (%hs)\nConnection Score: %s\nLatency: %s\nJitter: %s\nReliability: %s\nRegion: %hs\nWins: %d\nLosses: %d\nDisconnects: %d\nFavorite Army: %s", + strConnectionType.c_str(), scoreStr.str(), latencyStr.str(), jitterStr.str(), qualityStr.str(), + member.region.c_str(), totalWins, totalLosses, totalDiscons, favoriteSide.str()); } else { - playerInfo.format(L"\nConnection State: Connecting...\nLatency: %d ms\nRegion: %hs\nWins: %d\nLosses: %d\nDisconnects: %d\nFavorite Army: %s", - latency, member.region.c_str(), totalWins, totalLosses, totalDiscons, favoriteSide.str()); + playerInfo.format(L"\nConnection State: Connecting...\nRegion: %hs\nWins: %d\nLosses: %d\nDisconnects: %d\nFavorite Army: %s", + member.region.c_str(), totalWins, totalLosses, totalDiscons, favoriteSide.str()); } #else playerInfo.format(L"\nLatency: %d ms\nWins: %d\nLosses: %d\nDisconnects: %d\nFavorite Army: %s", @@ -1314,6 +1328,69 @@ void WOLDisplayGameOptions( void ) // ----------------------------------------------------------------------------------------- // The Bad munkee slot list displaying function //------------------------------------------------------------------------------------------------- +static void WOLRefreshConnectionIndicators( void ) +{ + NGMP_OnlineServices_LobbyInterface* pLobbyInterface = NGMP_OnlineServicesManager::GetInterface(); + NGMPGame* game = pLobbyInterface == nullptr ? nullptr : pLobbyInterface->GetCurrentGame(); + if (pLobbyInterface == nullptr || game == nullptr || !game->isInGame()) + return; + + NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); + static const Image* heroImage = TheMappedImageCollection->findImageByName("HeroReticle"); + + for (Int i = 0; i < MAX_SLOTS; ++i) + { + NGMPGameSlot* slot = game->getGameSpySlot(i); + if (slot == nullptr || !slot->isHuman()) + { + if (genericPingWindow[i]) + genericPingWindow[i]->winHide(TRUE); + continue; + } + + if (genericPingWindow[i] == nullptr) + continue; + + if (i == game->getLocalSlotNum()) + { + genericPingWindow[i]->winHide(TRUE); + continue; + } + + genericPingWindow[i]->winHide(FALSE); + + bool bIsConnected = false; + int connectionScore = -1; + + if (pMesh != nullptr) + { + PlayerConnection* pConnection = pMesh->GetConnectionForUser(slot->m_userID); + if (pConnection != nullptr) + { + bIsConnected = pConnection->GetState() == EConnectionState::CONNECTED_DIRECT; + connectionScore = pConnection->ComputeConnectionScore(); + } + } + + if (!bIsConnected || connectionScore < 0) + { + genericPingWindow[i]->winSetEnabledImage(0, heroImage); + } + else if (connectionScore >= 75) + { + genericPingWindow[i]->winSetEnabledImage(0, pingImages[0]); + } + else if (connectionScore >= 50) + { + genericPingWindow[i]->winSetEnabledImage(0, pingImages[1]); + } + else + { + genericPingWindow[i]->winSetEnabledImage(0, pingImages[2]); + } + } +} + void WOLDisplaySlotList( void ) { // TODO_NGMP @@ -1350,70 +1427,10 @@ void WOLDisplaySlotList( void ) { GadgetTextEntrySetTextColor(GadgetComboBoxGetEditBox(comboBoxPlayer[i]), nameColor); } - - bool bIsConnected = false; - int latency = -1; - std::string strConnectionType = ""; - - LobbyMemberEntry member = pLobbyInterface->GetRoomMemberFromID(slot->m_userID); - - if (NGMP_OnlineServicesManager::GetNetworkMesh() != nullptr) - { - PlayerConnection* pConnection = NGMP_OnlineServicesManager::GetNetworkMesh()->GetConnectionForUser(slot->m_userID); - - if (pConnection != nullptr) - { - strConnectionType = pConnection->GetConnectionType(); - if (pConnection->GetState() == EConnectionState::CONNECTED_DIRECT) - { - bIsConnected = true; - } - else - { - bIsConnected = false; - } - latency = pConnection->GetLatency(); - } - } - - if (genericPingWindow[i]) - { - genericPingWindow[i]->winHide(FALSE); - - genericPingWindow[i]->winSetEnabledImage(0, pingImages[0]); - - // not connected? show another icon - if (!bIsConnected && i != game->getLocalSlotNum()) - { - static const Image* image = TheMappedImageCollection->findImageByName("HeroReticle"); - genericPingWindow[i]->winSetEnabledImage(0, image); - } - else - { - if (latency > 0) - { - if (latency < 250) - { - genericPingWindow[i]->winSetEnabledImage(0, pingImages[0]); - } - else if (latency < 500) - { - genericPingWindow[i]->winSetEnabledImage(0, pingImages[1]); - } - else - { - genericPingWindow[i]->winSetEnabledImage(0, pingImages[2]); - } - } - } - } - } - else - { - if (genericPingWindow[i]) - genericPingWindow[i]->winHide(TRUE); } } + + WOLRefreshConnectionIndicators(); } //------------------------------------------------------------------------------------------------- @@ -2280,6 +2297,9 @@ static void fillPlayerInfo(const PeerResponse *resp, PlayerInfo *info) //------------------------------------------------------------------------------------------------- void WOLGameSetupMenuUpdate( WindowLayout * layout, void *userData) { + // Refresh only the fast-changing connection indicators each frame. + WOLRefreshConnectionIndicators(); + // need to exit? if (NGMP_OnlineServicesManager::GetInstance() != nullptr && NGMP_OnlineServicesManager::GetInstance()->IsPendingFullTeardown()) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp index d8be7650de8..cd7777ea7b5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/NetworkMesh.cpp @@ -1047,9 +1047,9 @@ void PlayerConnection::UpdateLatencyHistogram() // update latency history int currLatency = GetLatency(); #if defined(GENERALS_ONLINE_HIGH_FPS_SERVER) - const int connectionHistoryLength = histogram_duration /16; // ~10 sec worth of frames + const int connectionHistoryLength = histogram_duration / 16; // ~20 sec worth of frames at 60fps (default) #else - const int connectionHistoryLength = histogram_duration /33; // ~10 sec worth of frames + const int connectionHistoryLength = histogram_duration / 33; // ~20 sec worth of frames at 30fps (default) #endif if (m_vecLatencyHistory.size() >= connectionHistoryLength) @@ -1057,6 +1057,38 @@ void PlayerConnection::UpdateLatencyHistogram() m_vecLatencyHistory.erase(m_vecLatencyHistory.begin()); } m_vecLatencyHistory.push_back(currLatency); + + // Sample connection quality into rolling history. + // Prefer SNS local quality, then remote quality, then fall back to manual in/out packet rate ratio. + if (m_hSteamConnection != k_HSteamNetConnection_Invalid) + { + SteamNetConnectionRealTimeStatus_t status; + if (SteamNetworkingSockets()->GetConnectionRealTimeStatus(m_hSteamConnection, &status, 0, nullptr) == k_EResultOK) + { + float sample = -1.0f; + if (status.m_flConnectionQualityLocal >= 0.0f) + { + sample = status.m_flConnectionQualityLocal; + } + else if (status.m_flConnectionQualityRemote >= 0.0f) + { + sample = status.m_flConnectionQualityRemote; + } + else if (status.m_flOutPacketsPerSec > 0.0f) + { + sample = status.m_flInPacketsPerSec / status.m_flOutPacketsPerSec; + } + + if (sample > 1.0f) sample = 1.0f; + + if (sample >= 0.0f) + { + if (m_vecQualityHistory.size() >= connectionHistoryLength) + m_vecQualityHistory.erase(m_vecQualityHistory.begin()); + m_vecQualityHistory.push_back(sample); + } + } + } } bool PlayerConnection::IsIPV4() @@ -1181,20 +1213,96 @@ int PlayerConnection::GetLatency() return -1; } -float PlayerConnection::GetConnectionQuality() +int PlayerConnection::GetJitter() { - if (m_hSteamConnection != k_HSteamNetConnection_Invalid) + int sumDelta = 0; + int count = 0; + int prev = -1; + for (int sample : m_vecLatencyHistory) { - const int k_nLanes = 1; - SteamNetConnectionRealTimeStatus_t status; - SteamNetConnectionRealTimeLaneStatus_t laneStatus[k_nLanes]; - - EResult res = SteamNetworkingSockets()->GetConnectionRealTimeStatus(m_hSteamConnection, &status, k_nLanes, laneStatus); - if (res == k_EResultOK) + if (sample >= 0) + { + if (prev >= 0) + { + sumDelta += std::abs(sample - prev); + ++count; + } + prev = sample; + } + else { - return std::min(status.m_flConnectionQualityLocal, status.m_flConnectionQualityRemote); + prev = -1; // gap in valid data } } - return -1; + if (count < 10) + return -1; + + return sumDelta / count; +} + +float PlayerConnection::GetConnectionQuality() +{ + if (!m_vecQualityHistory.empty()) + { + float sum = 0.0f; + for (float r : m_vecQualityHistory) + sum += r; + return sum / static_cast(m_vecQualityHistory.size()); + } + + return -1.0f; +} + +int PlayerConnection::ComputeConnectionScore() +{ + const int latency = GetLatency(); + const int jitter = GetJitter(); + const float quality = GetConnectionQuality(); // packet delivery ratio [0..1] + + // Stability-first weighting + static constexpr float k_subScoreFloor = 0.01f; + static constexpr float k_latencyWeight = 0.22f; + static constexpr float k_jitterWeight = 0.38f; + static constexpr float k_reliabilityWeight = 0.40f; + + float weightedLogSum = 0.0f; + float activeWeightSum = 0.0f; + + if (latency >= 0) + { + // 10ms and below are treated as full score. Above that, roughly: + // 400ms -> composite 75, 800ms -> composite 50 when other metrics are perfect. + int effectiveLatency = (std::max)(latency - 10, 0); + float latFactor = std::clamp(1.0f - static_cast(effectiveLatency) / 1590.0f, 0.0f, 1.0f); + float latencyScore = (std::max)(std::powf(latFactor, 4.545f), k_subScoreFloor); + weightedLogSum += k_latencyWeight * std::logf(latencyScore); + activeWeightSum += k_latencyWeight; + } + + if (jitter >= 0) + { + // 50ms -> composite 75, 100ms -> composite 50 when other metrics are perfect. + float jitFactor = std::clamp(1.0f - static_cast(jitter) / 200.0f, 0.0f, 1.0f); + float jitterScore = (std::max)(std::powf(jitFactor, 2.632f), k_subScoreFloor); + weightedLogSum += k_jitterWeight * std::logf(jitterScore); + activeWeightSum += k_jitterWeight; + } + + if (quality >= 0.0f) + { + // 90% -> composite 75, 80% -> composite 50 when other metrics are perfect. + float relFactor = std::clamp(2.5f * quality - 1.5f, 0.0f, 1.0f); + float reliabilityScore = (std::max)(std::powf(relFactor, 2.5f), k_subScoreFloor); + weightedLogSum += k_reliabilityWeight * std::logf(reliabilityScore); + activeWeightSum += k_reliabilityWeight; + } + + if (activeWeightSum <= 0.0f) + { + return -1; + } + + float composite = std::expf(weightedLogSum / activeWeightSum); + return static_cast(std::round(composite * 100.0f)); }