void CSteamNetworkConnectionBase::SNP_RecordReceivedPktNum( int64 nPktNum, SteamNetworkingMicroseconds usecNow, bool bScheduleAck )
{

	// Check if sender has already told us they don't need us to
	// account for packets this old anymore
	if ( unlikely( nPktNum < m_receiverState.m_nMinPktNumToSendAcks ) )
		return;

	// Fast path for the (hopefully) most common case of packets arriving in order
	if ( likely( nPktNum == m_statsEndToEnd.m_nMaxRecvPktNum+1 ) )
	{
		if ( bScheduleAck ) // fast path for all unreliable data (common when we are just being used for transport)
		{
			// Schedule ack of this packet (since we are the highest numbered
			// packet, that means reporting on everything)
			QueueFlushAllAcks( usecNow + k_usecMaxDataAckDelay );
		}
		return;
	}

	// At this point, ack invariants should be met
	m_receiverState.DebugCheckPackGapMap();

	// Latest time that this packet should be acked.
	// (We might already be scheduled to send and ack that would include this packet.)
	SteamNetworkingMicroseconds usecScheduleAck = bScheduleAck ? usecNow + k_usecMaxDataAckDelay : INT64_MAX;

	// Check if this introduced a gap since the last sequence packet we have received
	if ( nPktNum > m_statsEndToEnd.m_nMaxRecvPktNum )
	{

		// Protect against malicious sender!
		if ( len( m_receiverState.m_mapPacketGaps ) >= k_nMaxPacketGaps )
			return; // Nope, we will *not* actually mark the packet as received

		// Add a gap for the skipped packet(s).
		int64 nBegin = m_statsEndToEnd.m_nMaxRecvPktNum+1;
		std::pair<int64,SSNPPacketGap> x;
		x.first = nBegin;
		x.second.m_nEnd = nPktNum;
		x.second.m_usecWhenReceivedPktBefore = m_statsEndToEnd.m_usecTimeLastRecvSeq;
		x.second.m_usecWhenAckPrior = m_receiverState.m_mapPacketGaps.rbegin()->second.m_usecWhenAckPrior;

		// When should we nack this?
		x.second.m_usecWhenOKToNack = usecNow;
		if ( nPktNum < m_statsEndToEnd.m_nMaxRecvPktNum + 3 )
			x.second.m_usecWhenOKToNack += k_usecNackFlush;

		auto iter = m_receiverState.m_mapPacketGaps.insert( x ).first;

		SpewMsgGroup( m_connectionConfig.m_LogLevel_PacketGaps.Get(), "[%s] drop %d pkts [%lld-%lld)",
			GetDescription(),
			(int)( nPktNum - nBegin ),
			(long long)nBegin, (long long)nPktNum );

		// Remember that we need to send a NACK
		if ( m_receiverState.m_itPendingNack->first == INT64_MAX )
		{
			m_receiverState.m_itPendingNack = iter;
		}
		else
		{
			// Pending nacks should be for older packet, not newer
			Assert( m_receiverState.m_itPendingNack->first < nBegin );
		}

		// Back up if we we had a flush of everything scheduled
		if ( m_receiverState.m_itPendingAck->first == INT64_MAX && m_receiverState.m_itPendingAck->second.m_usecWhenAckPrior < INT64_MAX )
		{
			Assert( iter->second.m_usecWhenAckPrior == m_receiverState.m_itPendingAck->second.m_usecWhenAckPrior );
			m_receiverState.m_itPendingAck = iter;
		}

		// At this point, ack invariants should be met
		m_receiverState.DebugCheckPackGapMap();

		// Schedule ack of this packet (since we are the highest numbered
		// packet, that means reporting on everything) by the requested
		// time
		QueueFlushAllAcks( usecScheduleAck );
	}
	else
	{

		// Check if this filed a gap
		auto itGap = m_receiverState.m_mapPacketGaps.upper_bound( nPktNum );
		if ( itGap == m_receiverState.m_mapPacketGaps.end() )
		{
			AssertMsg( false, "[%s] Cannot locate gap, or processing packet %lld multiple times. %s | %s",
				GetDescription(), (long long)nPktNum,
				m_statsEndToEnd.RecvPktNumStateDebugString().c_str(), m_statsEndToEnd.HistoryRecvSeqNumDebugString(8).c_str() );
			return;
		}
		if ( itGap == m_receiverState.m_mapPacketGaps.begin() )
		{
			AssertMsg( false, "[%s] Cannot locate gap, or processing packet %lld multiple times. [%lld,%lld) %s | %s",
				GetDescription(), (long long)nPktNum, (long long)itGap->first, (long long)itGap->second.m_nEnd,
				m_statsEndToEnd.RecvPktNumStateDebugString().c_str(), m_statsEndToEnd.HistoryRecvSeqNumDebugString(8).c_str() );
			return;
		}
		--itGap;
		if ( itGap->first > nPktNum || itGap->second.m_nEnd <= nPktNum )
		{
			// We already received this packet.  But this should be impossible now,
			// we should be rejecting duplicate packet numbers earlier
			AssertMsg( false, "[%s] Packet gap bug.  %lld [%lld,%lld) %s | %s",
				GetDescription(), (long long)nPktNum, (long long)itGap->first, (long long)itGap->second.m_nEnd,
				m_statsEndToEnd.RecvPktNumStateDebugString().c_str(), m_statsEndToEnd.HistoryRecvSeqNumDebugString(8).c_str() );
			return;
		}

		// Packet is in a gap where we previously thought packets were lost.
		// (Packets arriving out of order.)

		// Last packet in gap?
		if ( itGap->second.m_nEnd-1 == nPktNum )
		{
			// Single-packet gap?
			if ( itGap->first == nPktNum )
			{
				// Were we waiting to ack/nack this?  Then move forward to the next gap, if any
				usecScheduleAck = std::min( usecScheduleAck, itGap->second.m_usecWhenAckPrior );
				if ( m_receiverState.m_itPendingAck == itGap )
					++m_receiverState.m_itPendingAck;
				if ( m_receiverState.m_itPendingNack == itGap )
					++m_receiverState.m_itPendingNack;

				// Save time when we needed to ack the packets before this gap
				SteamNetworkingMicroseconds usecWhenAckPrior = itGap->second.m_usecWhenAckPrior;

				// Gap is totally filled.  Erase, and move to the next one,
				// if any, so we can schedule ack below
				itGap = m_receiverState.m_mapPacketGaps.erase( itGap );

				// Were we scheduled to ack the packets before this?  If so, then
				// we still need to do that, only now when we send that ack, we will
				// ack the packets after this gap as well, since they will be included
				// in the same ack block.
				//
				// NOTE: This is based on what was scheduled to be acked before we got
				// this packet.  If we need to update the schedule to ack the current
				// packet, we will do that below.  However, usually if previous
				// packets were already scheduled to be acked, then that deadline time
				// will be sooner usecScheduleAck, so the code below will not actually
				// do anything.
				if ( usecWhenAckPrior < itGap->second.m_usecWhenAckPrior )
				{
					itGap->second.m_usecWhenAckPrior = usecWhenAckPrior;
				}
				else
				{
					// Otherwise, we might not have any acks scheduled.  In that
					// case, the invariant is that m_itPendingAck should point at the sentinel
					if ( m_receiverState.m_itPendingAck->second.m_usecWhenAckPrior == INT64_MAX )
					{
						m_receiverState.m_itPendingAck = m_receiverState.m_mapPacketGaps.end();
						--m_receiverState.m_itPendingAck;
						Assert( m_receiverState.m_itPendingAck->first == INT64_MAX );
					}
				}

				SpewVerboseGroup( m_connectionConfig.m_LogLevel_PacketGaps.Get(), "[%s] decode pkt %lld, single pkt gap filled", GetDescription(), (long long)nPktNum );

				// At this point, ack invariants should be met
				m_receiverState.DebugCheckPackGapMap();
			}
			else
			{
				// Shrink gap by one from the end
				--itGap->second.m_nEnd;
				Assert( itGap->first < itGap->second.m_nEnd );

				SpewVerboseGroup( m_connectionConfig.m_LogLevel_PacketGaps.Get(), "[%s] decode pkt %lld, last packet in gap, reduced to [%lld,%lld)", GetDescription(),
					(long long)nPktNum, (long long)itGap->first, (long long)itGap->second.m_nEnd );

				// Move to the next gap so we can schedule ack below
				++itGap;

				// At this point, ack invariants should be met
				m_receiverState.DebugCheckPackGapMap();
			}
		}
		else if ( itGap->first == nPktNum )
		{
			// First packet in multi-packet gap.
			// Shrink packet from the front
			// Cast away const to allow us to modify the key.
			// We know this won't break the map ordering
			++const_cast<int64&>( itGap->first );
			Assert( itGap->first < itGap->second.m_nEnd );
			itGap->second.m_usecWhenReceivedPktBefore = usecNow;

			SpewVerboseGroup( m_connectionConfig.m_LogLevel_PacketGaps.Get(), "[%s] decode pkt %lld, first packet in gap, reduced to [%lld,%lld)", GetDescription(),
				(long long)nPktNum, (long long)itGap->first, (long long)itGap->second.m_nEnd );

			// At this point, ack invariants should be met
			m_receiverState.DebugCheckPackGapMap();
		}
		else
		{
			// Packet is in the middle of the gap.  We'll need to fragment this gap
			// Protect against malicious sender!
			if ( len( m_receiverState.m_mapPacketGaps ) >= k_nMaxPacketGaps )
				return; // Nope, we will *not* actually mark the packet as received

			// Locate the next block so we can set the schedule time
			auto itNext = itGap;
			++itNext;

			// Start making a new gap to account for the upper end
			std::pair<int64,SSNPPacketGap> upper;
			upper.first = nPktNum+1;
			upper.second.m_nEnd = itGap->second.m_nEnd;
			upper.second.m_usecWhenReceivedPktBefore = usecNow;
			if ( itNext == m_receiverState.m_itPendingAck )
				upper.second.m_usecWhenAckPrior = INT64_MAX;
			else
				upper.second.m_usecWhenAckPrior = itNext->second.m_usecWhenAckPrior;
			upper.second.m_usecWhenOKToNack = itGap->second.m_usecWhenOKToNack;

			// Truncate the current gap
			itGap->second.m_nEnd = nPktNum;
			Assert( itGap->first < itGap->second.m_nEnd );

			SpewVerboseGroup( m_connectionConfig.m_LogLevel_PacketGaps.Get(), "[%s] decode pkt %lld, gap split [%lld,%lld) and [%lld,%lld)", GetDescription(),
				(long long)nPktNum, (long long)itGap->first, (long long)itGap->second.m_nEnd, upper.first, upper.second.m_nEnd );

			// Insert a new gap to account for the upper end, and
			// advance iterator to it, so that we can schedule ack below
			itGap = m_receiverState.m_mapPacketGaps.insert( upper ).first;

			// At this point, ack invariants should be met
			m_receiverState.DebugCheckPackGapMap();
		}

		Assert( itGap != m_receiverState.m_mapPacketGaps.end() );

		// Need to schedule ack (earlier than it is already scheduled)?
		if ( usecScheduleAck < itGap->second.m_usecWhenAckPrior )
		{

			// Earlier than the current thing being scheduled?
			if ( usecScheduleAck <= m_receiverState.m_itPendingAck->second.m_usecWhenAckPrior )
			{

				// We're next, set the time
				itGap->second.m_usecWhenAckPrior = usecScheduleAck;

				// Any schedules for lower-numbered packets are superseded
				// by this one.
				if ( m_receiverState.m_itPendingAck->first <= itGap->first )
				{
					while ( m_receiverState.m_itPendingAck != itGap )
					{
						m_receiverState.m_itPendingAck->second.m_usecWhenAckPrior = INT64_MAX;
						++m_receiverState.m_itPendingAck;
					}
				}
				else
				{
					// If our number is lower than the thing that was scheduled next,
					// then back up and re-schedule any blocks in between to be effectively
					// the same time as they would have been flushed before.
					SteamNetworkingMicroseconds usecOldSched = m_receiverState.m_itPendingAck->second.m_usecWhenAckPrior;
					while ( --m_receiverState.m_itPendingAck != itGap )
					{
						m_receiverState.m_itPendingAck->second.m_usecWhenAckPrior = usecOldSched;
					}
				}
			}
			else
			{
				// We're not the next thing that needs to be acked.
				
				if ( itGap->first < m_receiverState.m_itPendingAck->first )
				{
					// We're a lowered numbered packet,	so this request is subsumed by the
					// request to flush more packets at an earlier time,
					// and we don't need to do anything.

				}
				else
				{

					// We need to ack a bit earlier
					itGap->second.m_usecWhenAckPrior = usecScheduleAck;

					// Now the only way for our invariants to be violated is for lower
					// numbered blocks to have later scheduled times.
					Assert( itGap != m_receiverState.m_mapPacketGaps.begin() );
					while ( (--itGap)->second.m_usecWhenAckPrior > usecScheduleAck )
					{
						Assert( itGap != m_receiverState.m_mapPacketGaps.begin() );
						itGap->second.m_usecWhenAckPrior = usecScheduleAck;
					}
				}
			}

			// Make sure we didn't screw things up
			m_receiverState.DebugCheckPackGapMap();
		}

		// Make sure are scheduled to wake up
		if ( bScheduleAck )
			EnsureMinThinkTime( m_receiverState.TimeWhenFlushAcks() );
	}
}