diff --git a/src/connectdlg.cpp b/src/connectdlg.cpp index a83d499b7d..ff8810db80 100644 --- a/src/connectdlg.cpp +++ b/src/connectdlg.cpp @@ -36,7 +36,8 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR bServerListItemWasChosen ( false ), bListFilterWasActive ( false ), bShowAllMusicians ( true ), - bEnableIPv6 ( bNEnableIPv6 ) + bEnableIPv6 ( bNEnableIPv6 ), + iKeepPingAfterHideStartTimestamp ( 0 ) { setupUi ( this ); @@ -162,6 +163,8 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR // setup timers TimerInitialSort.setSingleShot ( true ); // only once after list request + TimerKeepPingAfterHide.setSingleShot ( true ); + #if defined( ANDROID ) || defined( Q_OS_IOS ) // for the Android and iOS version maximize the window setWindowState ( Qt::WindowMaximized ); @@ -197,10 +200,15 @@ CConnectDlg::CConnectDlg ( CClientSettings* pNSetP, const bool bNewShowCompleteR QObject::connect ( &TimerPing, &QTimer::timeout, this, &CConnectDlg::OnTimerPing ); QObject::connect ( &TimerReRequestServList, &QTimer::timeout, this, &CConnectDlg::OnTimerReRequestServList ); + + QObject::connect ( &TimerKeepPingAfterHide, &QTimer::timeout, this, &CConnectDlg::OnTimerKeepPingAfterHide ); } void CConnectDlg::showEvent ( QShowEvent* ) { + // Stop shutdown timer if dialog is shown again before it expires + TimerKeepPingAfterHide.stop(); + // load stored IP addresses in combo box cbxServerAddr->clear(); cbxServerAddr->clearEditText(); @@ -220,6 +228,9 @@ void CConnectDlg::showEvent ( QShowEvent* ) void CConnectDlg::RequestServerList() { + // Ensure shutdown timer is stopped when requesting new server list + TimerKeepPingAfterHide.stop(); + // reset flags bServerListReceived = false; bReducedServerListReceived = false; @@ -271,9 +282,15 @@ void CConnectDlg::RequestServerList() void CConnectDlg::hideEvent ( QHideEvent* ) { - // if window is closed, stop timers - TimerPing.stop(); + // Stop the regular server list request timer immediately TimerReRequestServList.stop(); + + iKeepPingAfterHideStartTimestamp = QDateTime::currentMSecsSinceEpoch(); + + // this will initiate the "after hide" phase where ping will continue, at the end of which pinging will stop + const int iRandomizedShutdownMs = + static_cast ( KEEP_PING_RUNNING_AFTER_HIDE_MS * ( 0.8f + QRandomGenerator::global()->generateDouble() * 0.4f ) ); + TimerKeepPingAfterHide.start ( iRandomizedShutdownMs ); } void CConnectDlg::OnDirectoryChanged ( int iTypeIdx ) @@ -295,6 +312,12 @@ void CConnectDlg::OnDirectoryChanged ( int iTypeIdx ) RequestServerList(); } +void CConnectDlg::OnTimerKeepPingAfterHide() +{ + // Shutdown timer expired - now stop all ping activities + TimerPing.stop(); +} + void CConnectDlg::OnTimerReRequestServList() { // if the server list is not yet received, retransmit the request for the @@ -451,9 +474,13 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorsetText ( LVC_CLIENTS_MAX_HIDDEN, QString().setNum ( vecServerInfo[iIdx].iMaxNumClients ) ); + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_PING_SALT, QRandomGenerator::global()->bounded ( 500 ) ); // random ping salt per server + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, 0 ); // store host address - pNewListViewItem->setData ( LVC_NAME, Qt::UserRole, CurHostAddress.toString() ); + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_HOST_ADDRESS, CurHostAddress.toString() ); + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_QHOST_ADDRESS_CACHE, QVariant() ); // cache QHostAddress, will be updated on first ping + pNewListViewItem->setData ( LVC_NAME, USER_ROLE_QHOST_PORT_CACHE, QVariant() ); // cache quint16 port number, will be updated on first ping // per default expand the list item (if not "show all servers") if ( bShowAllMusicians ) @@ -465,7 +492,10 @@ void CConnectDlg::SetServerList ( const CHostAddress& InetAddr, const CVectorbounded ( 50 ) + 500; + TimerPing.start ( iPingUpdateInterval ); } void CConnectDlg::SetConnClientsList ( const CHostAddress& InetAddr, const CVector& vecChanInfo ) @@ -728,7 +758,7 @@ void CConnectDlg::OnConnectClicked() QTreeWidgetItem* pCurSelTopListItem = GetParentListViewItem ( CurSelListItemList[0] ); // get host address from selected list view item as a string - strSelectedAddress = pCurSelTopListItem->data ( LVC_NAME, Qt::UserRole ).toString(); + strSelectedAddress = pCurSelTopListItem->data ( LVC_NAME, USER_ROLE_HOST_ADDRESS ).toString(); // store selected server name strSelectedServerName = pCurSelTopListItem->text ( LVC_NAME ); @@ -782,20 +812,95 @@ void CConnectDlg::OnTimerPing() // we need to ask for the server version only if we have not received it const bool bNeedVersion = pCurListViewItem->text ( LVC_VERSION ).isEmpty(); + // retrieve cached QHostAddress and port from UserData + QVariant varCachedIP = pCurListViewItem->data ( LVC_NAME, USER_ROLE_QHOST_ADDRESS_CACHE ); + QVariant varCachedPort = pCurListViewItem->data ( LVC_NAME, USER_ROLE_QHOST_PORT_CACHE ); + CHostAddress haServerAddress; - // try to parse host address string which is stored as user data - // in the server list item GUI control element - if ( NetworkUtil().ParseNetworkAddress ( pCurListViewItem->data ( LVC_NAME, Qt::UserRole ).toString(), haServerAddress, bEnableIPv6 ) ) + if ( varCachedIP.canConvert() && varCachedPort.canConvert() && !varCachedIP.isNull() && !varCachedPort.isNull() ) { - // if address is valid, send ping message using a new thread + // Use cached values + haServerAddress.InetAddr = varCachedIP.value(); + haServerAddress.iPort = varCachedPort.value(); + } + else + { + // Fallback: parse and cache if not present (should not happen in normal flow) + if ( !NetworkUtil().ParseNetworkAddress ( pCurListViewItem->data ( LVC_NAME, USER_ROLE_HOST_ADDRESS ).toString(), + haServerAddress, + bEnableIPv6 ) ) + { + continue; // Skip this server if parsing fails + } + + // Cache the parsed values for future use + pCurListViewItem->setData ( LVC_NAME, USER_ROLE_QHOST_ADDRESS_CACHE, QVariant::fromValue ( haServerAddress.InetAddr ) ); + pCurListViewItem->setData ( LVC_NAME, USER_ROLE_QHOST_PORT_CACHE, QVariant::fromValue ( haServerAddress.iPort ) ); + } + + // Get minimum ping time and last ping timestamp + const qint64 iCurrentTime = QDateTime::currentMSecsSinceEpoch(); + const int iMinPingTime = pCurListViewItem->text ( LVC_PING_MIN_HIDDEN ).toInt(); + const qint64 iLastPingTimestamp = pCurListViewItem->data ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP ).toLongLong(); + const int iServerSalt = pCurListViewItem->data ( LVC_NAME, USER_ROLE_PING_SALT ).toInt(); + const qint64 iTimeSinceLastPing = iCurrentTime - iLastPingTimestamp; + + // Calculate adaptive ping interval based on latency using linear formula: + int iPingInterval; + if ( iMinPingTime == 0 || iMinPingTime > 99999999 ) + { + // Never pinged or invalid - always ping to get initial measurement + iPingInterval = 0; + } + else + { + // Calculate adaptive ping interval: base multiplier from latency (15ms→1x, capped at fPingMaxMultiplier), + // combined with geographic (high-latency servers get 3-50% extra variance) and random factors (±15%) + // fPingMaxMultiplier is currently pretty low to keep it similar to the normal constant ping, should be increased in future versions (like + // 8-10) + const float fPingMaxMultiplier = 3.0f; + const float fGeoFactor = 1.0f + ( std::min ( 500, iMinPingTime ) / 100.0f ) * 0.2f; // high ping get more variance + const float fRandomFactor = ( 0.85f + QRandomGenerator::global()->generateDouble() * 0.3f ) * fGeoFactor; + const float fIntervalMultiplier = + std::min ( fPingMaxMultiplier, std::max ( 1.0f, 1.0f + ( iMinPingTime - 15 ) / 25.0f ) ) * fRandomFactor; + + iPingInterval = static_cast ( PING_UPDATE_TIME_SERVER_LIST_MS * fIntervalMultiplier ); + + iPingInterval += iServerSalt; + + // Add randomization to prevent any synchronized pings across servers with the same ping + const int iRandomOffsetMs = QRandomGenerator::global()->bounded ( 600 ) - 300; // -300ms to +300ms + iPingInterval += iRandomOffsetMs; + iPingInterval = std::max ( iPingInterval, 500 ); + + // during shutdown: randomly sent a ping for first 20 to 30 seconds only, can be removed in the future when this mode is used + // by the majority of clients + if ( TimerKeepPingAfterHide.isActive() ) + { + const qint64 iTimeSinceHide = iCurrentTime - iKeepPingAfterHideStartTimestamp; + if ( iTimeSinceHide < ( 20000 + QRandomGenerator::global()->bounded ( 10000 ) ) ) + { + iPingInterval = 2000 + QRandomGenerator::global()->bounded ( 1000 ); + } + } + } + + // Skip this server if not enough time has passed since last ping + if ( iTimeSinceLastPing < iPingInterval ) + { + continue; + } + + pCurListViewItem->setData ( LVC_NAME, USER_ROLE_LAST_PING_TIMESTAMP, qint64 ( iCurrentTime ) ); + + // if address is valid, send ping message using a new thread #if QT_VERSION >= QT_VERSION_CHECK( 6, 0, 0 ) - QFuture f = QtConcurrent::run ( &CConnectDlg::EmitCLServerListPingMes, this, haServerAddress, bNeedVersion ); - Q_UNUSED ( f ); + QFuture f = QtConcurrent::run ( &CConnectDlg::EmitCLServerListPingMes, this, haServerAddress, bNeedVersion ); + Q_UNUSED ( f ); #else - QtConcurrent::run ( this, &CConnectDlg::EmitCLServerListPingMes, haServerAddress, bNeedVersion ); + QtConcurrent::run ( this, &CConnectDlg::EmitCLServerListPingMes, haServerAddress, bNeedVersion ); #endif - } } } @@ -967,7 +1072,7 @@ QTreeWidgetItem* CConnectDlg::FindListViewItem ( const CHostAddress& InetAddr ) { // compare the received address with the user data string of the // host address by a string compare - if ( !lvwServers->topLevelItem ( iIdx )->data ( LVC_NAME, Qt::UserRole ).toString().compare ( InetAddr.toString() ) ) + if ( !lvwServers->topLevelItem ( iIdx )->data ( LVC_NAME, USER_ROLE_HOST_ADDRESS ).toString().compare ( InetAddr.toString() ) ) { return lvwServers->topLevelItem ( iIdx ); } diff --git a/src/connectdlg.h b/src/connectdlg.h index 62cb2dcfdf..21d2b58953 100644 --- a/src/connectdlg.h +++ b/src/connectdlg.h @@ -43,6 +43,11 @@ // transmitted until it is received #define SERV_LIST_REQ_UPDATE_TIME_MS 2000 // ms +// defines the time interval it will keep pinging servers after the dialog was hidden (randomized +/- 20%) +#define KEEP_PING_RUNNING_AFTER_HIDE_MS ( 1000 * 180 ) + +Q_DECLARE_METATYPE ( QHostAddress ) + /* Classes ********************************************************************/ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase { @@ -80,6 +85,16 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase LVC_COLUMNS // total number of columns }; + // those are the custom user data fields, all stored in the UserRole of the list view item (LVC_NAME) + enum EColumnPingNameUserRoles + { + USER_ROLE_HOST_ADDRESS = Qt::UserRole, // QString: CHostAddress as string + USER_ROLE_QHOST_ADDRESS_CACHE, // QHostAddress: cache QHostAddress, will be updated on first ping + USER_ROLE_QHOST_PORT_CACHE, // quint16: cache port number, will be updated on first ping + USER_ROLE_LAST_PING_TIMESTAMP, // qint64: timestamp of last ping measurement + USER_ROLE_PING_SALT // int: random ping salt per server + }; + virtual void showEvent ( QShowEvent* ); virtual void hideEvent ( QHideEvent* ); @@ -91,10 +106,12 @@ class CConnectDlg : public CBaseDlg, private Ui_CConnectDlgBase void RequestServerList(); void EmitCLServerListPingMes ( const CHostAddress& haServerAddress, const bool bNeedVersion ); void UpdateDirectoryComboBox(); - +# CClientSettings* pSettings; QTimer TimerPing; + QTimer TimerKeepPingAfterHide; + qint64 iKeepPingAfterHideStartTimestamp; // timestamp when keeping pings after hide started QTimer TimerReRequestServList; QTimer TimerInitialSort; CHostAddress haDirectoryAddress; @@ -118,6 +135,7 @@ public slots: void OnConnectClicked(); void OnDeleteServerAddrClicked(); void OnTimerPing(); + void OnTimerKeepPingAfterHide(); void OnTimerReRequestServList(); signals: