Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class PlayerConnection
}

std::vector<int> m_vecLatencyHistory;
std::vector<float> m_vecQualityHistory;
std::string GetStats();

std::string GetConnectionType();
Expand All @@ -85,7 +86,9 @@ class PlayerConnection
int m_SignallingAttempts = 0;

int GetLatency();
int GetJitter();
float GetConnectionQuality();
int ComputeConnectionScore();

HSteamNetConnection m_hSteamConnection = k_HSteamNetConnection_Invalid;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
NGMPGame* TheNGMPGame = NULL;

void WOLDisplaySlotList( void );
static void WOLRefreshConnectionIndicators( void );


extern std::list<PeerResponse> TheLobbyQueuedUTMs;
Expand Down Expand Up @@ -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);
Expand All @@ -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<int>(rawQuality * 100.0f) : -1;
}
}
}
Expand All @@ -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",
Expand Down Expand Up @@ -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<NGMP_OnlineServices_LobbyInterface>();
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
Expand Down Expand Up @@ -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();
}

//-------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1047,16 +1047,48 @@ 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)
{
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()
Expand Down Expand Up @@ -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<float>(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<float>(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<float>(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<float>(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<int>(std::round(composite * 100.0f));
}
Loading