diff --git a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp index ae432e692bf..987ef1041ae 100644 --- a/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp +++ b/Core/GameEngine/Source/GameNetwork/ConnectionManager.cpp @@ -1278,13 +1278,102 @@ void ConnectionManager::updateRunAhead(Int oldRunAhead, Int frameRate, Bool didS // TheSuperHackers @bugfix Mauller 21/08/2025 calculate the runahead so it always follows the latency // The runahead should always be rounded up to the next integer value to prevent variations in latency from causing stutter // The network slack pushes the runahead up to the next value when the latency is within the slack percentage of the current runahead - const Real runAheadSlackScale = 1.0f + ((Real)TheGlobalData->m_networkRunAheadSlack / 100.0f); - Int newRunAhead = ceilf((getMaximumLatency()) * runAheadSlackScale * (Real)minFps); + Real configuredSlack = (Real)TheGlobalData->m_networkRunAheadSlack / 100.0f; + + Real effectiveSlack = configuredSlack; + +#if defined(GENERALS_ONLINE) + if (TheNGMPGame != nullptr) + { + // dynamic slack based on jitter + NetworkMesh* pMesh = NGMP_OnlineServicesManager::GetNetworkMesh(); + if (pMesh != nullptr) + { + // 1) Current max latency (RTT) + int maxLatMs = pMesh->getMaximumLatency(); + + // 2) Compute worst-case jitter (max - min) across recent history + int jitterMs = 0; + + auto& connections = pMesh->GetAllConnections(); + for (auto& kvPair : connections) + { + PlayerConnection& conn = kvPair.second; + + if (conn.m_vecLatencyHistory.size() > 1) + { + int minL = INT_MAX; + int maxL = 0; + + for (int l : conn.m_vecLatencyHistory) + { + if (l < minL) minL = l; + if (l > maxL) maxL = l; + } + + int span = maxL - minL; + if (span > jitterMs) + { + jitterMs = span; + } + } + } + + // 3) Convert jitter to a 0–1+ ratio relative to latency + Real jitterRatio = 0.0f; + if (maxLatMs > 0) + { + jitterRatio = (Real)jitterMs / (Real)maxLatMs; + } + + // 4) Map jitter ratio into a slack range based on latency + // Lower latency: tighter slack + // Higher latency: allow more slack to hide jitter + Real minSlack = 0.20f; + Real maxSlack = 0.60f; + + if (maxLatMs > 300) + { + // At high RTT, allow more headroom + minSlack = 0.35f; + maxSlack = 0.90f; + } + else if (maxLatMs > 200) + { + // Medium-high latency (200–300 ms) + minSlack = 0.30f; + maxSlack = 0.80f; + } + + // Clamp jitterRatio to [0,1] when mapping + if (jitterRatio < 0.0f) jitterRatio = 0.0f; + if (jitterRatio > 1.0f) jitterRatio = 1.0f; + + Real dynamicSlack = minSlack + (maxSlack - minSlack) * jitterRatio; + + // ignore service slack and use our own dynamic slack + effectiveSlack = dynamicSlack; + } + } +#endif + + const Real runAheadSlackScale = 1.0f + effectiveSlack; + + Int newRunAhead = ceilf(getMaximumLatency() * runAheadSlackScale * (Real)minFps); // TheSuperHackers @info if the runahead goes below 3 logic frames it can start to introduce stutter // We also limit the upper range of the runahead to prevent it getting out of hand - newRunAhead = clamp(MIN_RUNAHEAD, newRunAhead, MAX_FRAMES_AHEAD / 2); + Int minRunAheadForClamp = MIN_RUNAHEAD; + +#if defined(GENERALS_ONLINE) + if (TheNGMPGame != nullptr) + { + minRunAheadForClamp = 3; + } +#endif + + newRunAhead = clamp(minRunAheadForClamp, newRunAhead, MAX_FRAMES_AHEAD / 2); NetRunAheadCommandMsg* msg = newInstance(NetRunAheadCommandMsg); msg->setPlayerID(m_localSlot);