Steam Session Ping 9999

Hi folks,
I put some efforts into it and came up with a solution. It was such a pain thought I had to share.
I based my thinking on Giuseppe Portelli’s article but I had to take a slightly different approach to make it work.
That’s because initial connection takes up to 2s to establish, giving wrong ping time measurements. So sending a 10 packets burst right away doesn’t work, they all come back with 1-2 seconds delay.
So what I do is repeatidly send ping packets to lobby servers during a given period every second. Which gives good results.

First I added an array to UNetDriver class to hold responses from servers:
in Engine\Source\Runtime\Engine\Classes\Engine\NetDriver.h :
add this struct definition

// RQ Start
struct FPingEntry
{
	uint8 OwnerId[8];
	double Ping;
};
// RQ End

Inside body of class UNetDriver, in public section, add

// RQ Start
	TArray<FPingEntry>			PingEntries;
// RQ End

Now, in Engine\Source\Runtime\Engine\Private\PacketHandlers\StatelessConnectHandlerComponent.cpp,
Find StatelessConnectHandlerComponent::IncomingConnectionless(FString Address, FBitReader& Packet) and modify it like this (my modifications are encompassed between RQ Start / RQ End) :

void StatelessConnectHandlerComponent::IncomingConnectionless(FString Address, FBitReader& Packet)
{
	bool bHandshakePacket = !!Packet.ReadBit() && !Packet.IsError();

// RQ Start
	bool bIsPingPacket = false;
	bool bIsPongPacket = false;
// RQ End

	LastChallengeSuccessAddress.Empty();

	if (bHandshakePacket)
	{
		uint8 SecretId = 0;
		float Timestamp = 1.f;
		uint8 Cookie[20];

		bHandshakePacket = ParseHandshakePacket(Packet, SecretId, Timestamp, Cookie);

		if (bHandshakePacket)
		{
			if (Handler->Mode == Handler::Mode::Server)
			{
				bool bInitialConnect = Timestamp == 0.f;

				if (bInitialConnect)
				{
					SendConnectChallenge(Address);
				}
				// Challenge response
				else if (Driver != nullptr)
				{
					bool bChallengeSuccess = false;
					float CookieDelta = Driver->Time - Timestamp;
					float SecretDelta = Timestamp - LastSecretUpdateTimestamp;
					bool bValidCookieLifetime = CookieDelta > 0.0 && (MAX_COOKIE_LIFETIME - CookieDelta) > 0.f;
					bool bValidSecretIdTimestamp = (SecretId == ActiveSecret) ? (SecretDelta >= 0.f) : (SecretDelta <= 0.f);

					if (bValidCookieLifetime && bValidSecretIdTimestamp)
					{
						// Regenerate cookie from packet info, and see if received cookie matches regenerated one
						uint8 RegenCookie[20];

						GenerateCookie(Address, SecretId, Timestamp, RegenCookie);

						bChallengeSuccess = FMemory::Memcmp(Cookie, RegenCookie, 20) == 0;
					}

					if (bChallengeSuccess)
					{
						LastChallengeSuccessAddress = Address;
					}
				}
			}
		}
		else
		{
			Packet.SetError();

#if !UE_BUILD_SHIPPING
			UE_LOG(LogHandshake, Log, TEXT("Error reading handshake packet."));
#endif
		}
	}
// RQ Start
	else if(!Packet.IsError())
	{
		// check header to see if it's a ping packet
		uint8 Header;
		Packet.SerializeBits(&Header, 7);
		if((Header<<1) == 74)
			bIsPingPacket = true;
		else if((Header<<1) == 54)
			bIsPongPacket = true;
	}

	if(bIsPingPacket)
	{
		// receiving a ping packet 
		if(Driver != NULL && Driver->IsNetResourceValid())
		{
			uint8 * InPacketDataPtr = Packet.GetData();

			// build an output packet with original data but PongHeader (must be even)
			FArrayWriter Writer;
			Writer.Add(54);

			// copy incoming data to out packet
			for(int32 i = 1; i < Packet.GetNumBytes(); i++)
				Writer.Add(InPacketDataPtr[i]);

			// pong this back to client
			Handler->SetRawSend(true);
			Driver->LowLevelSend(Address, Writer.GetData(), Writer.Num()*8);
			Handler->SetRawSend(false);
		}
	}
	else if(bIsPongPacket)
	{
		// receiving a pong packet 
		if(Driver != NULL)
		{
			// get OwnerId bytes out of packet
			uint8 OwnerId[8];
			Packet.Serialize(OwnerId, 8);

			// get time stamp out of packet
			double PacketTime;
			Packet.Serialize(&PacketTime, sizeof(double));

			// look for this OwnerId in Ping entries
			int32 EntryIdx = -1;
			int32 i;
			for(i = 0; i < Driver->PingEntries.Num(); i++)
			{
				int32 j = 0;
				while(j < 8 && OwnerId[j] == Driver->PingEntries[i].OwnerId[j])
					j++;

				if(j == 8)
					break;
			}

			if(i < Driver->PingEntries.Num())
			{
				// found, use this entry
				EntryIdx = i;
			}
			else
			{
				// not found, add a new entry
				if(Driver->PingEntries.Num() < 200)	  // put a limit 
				{
					EntryIdx = Driver->PingEntries.Num();
					FPingEntry NewEntry;
					for(int32 j = 0; j < 8; j++)
						NewEntry.OwnerId[j] = OwnerId[j];
					Driver->PingEntries.Add(NewEntry);
				}
			}

			// update ping entry value 
			if(EntryIdx >= 0)
				Driver->PingEntries[EntryIdx].Ping = FPlatformTime::Seconds() - PacketTime;
		}
	}
// RQ End
#if !UE_BUILD_SHIPPING
	else if (Packet.IsError())
	{
		UE_LOG(LogHandshake, Log, TEXT("Error reading handshake bit from packet."));
	}
#endif
}

It’s not over yet. Now in my game, I made a few functions to handle this:

#include "Networking.h"
#include "Sockets.h"
#include "SocketSubsystem.h"

...


// -----------------------------------------------------------------
void UServersPageWidget::PingServer(const FOnlineSessionSearchResult & Result)
{
	ISocketSubsystem* SocketSubsystem = ISocketSubsystem::Get();
	FString ConnectInfo;
	IOnlineSubsystem* OnlineSub = IOnlineSubsystem::Get();
	IOnlineSessionPtr Sessions = OnlineSub->GetSessionInterface();
	Sessions->GetResolvedConnectString(Result, GamePort, ConnectInfo);
	TSharedRef<FInternetAddr> Addr = SocketSubsystem->CreateInternetAddr();
	bool bIsValid;
	Addr->SetIp(*ConnectInfo, bIsValid);

	if (bIsValid)
	{
		// Creating client socket
		FSocket* Socket = SocketSubsystem->CreateSocket(FName("SteamClientSocket"), FString("PingSocket"), true);

		FArrayWriter Writer;
							
		// add a header to packet so we can recognize it on other side
		// could any value but LSB must be 0, 1 would mean "handshake packet"
		Writer.Add(74); 

		// write OwnerId data to packet
		const uint8 * OwnerIdPtr = Result.Session.OwningUserId->GetBytes();
		int32 OwningIdSize = Result.Session.OwningUserId->GetSize();
		for(int32 i = 0; i < OwningIdSize; i++)
			Writer.Add(OwnerIdPtr[i]);

		// write time stamp data to packet
		double TimeStamp = FPlatformTime::Seconds();
		uint8 * TimeStampPtr = (uint8*)&TimeStamp;
		for(int32 i = 0; i < sizeof(double); i++)
			Writer.Add(TimeStampPtr[i]);

		// add a trailing byte to ensure a non zero ending packet which would be considered an error when received
		Writer.Add(255);

		// Sending 10 ping packets
		int32 BytesSent;
		Socket->SendTo(Writer.GetData(), Writer.Num(), BytesSent, Addr.Get());

		SocketSubsystem->DestroySocket(Socket);
	}
}

// -----------------------------------------------------------------
double UServersPageWidget::GetPingByOwner(TSharedPtr<const FUniqueNetId> OwningUserId)
{
	UNetDriver * NetDriver = GetWorld()->GetNetDriver();
	if(OwningUserId.IsValid() && NetDriver != NULL)
	{
		const uint8 * OwnerIdPtr = OwningUserId->GetBytes();
		int32 OwningIdSize = OwningUserId->GetSize();
		for(int32 k = 0; k < NetDriver->PingEntries.Num(); k++)
		{
			int32 j = 0;
			while(j < OwningIdSize && j < 8 && OwnerIdPtr[j] == NetDriver->PingEntries[k].OwnerId[j])
				j++;
			
			if(j == 8)
				return NetDriver->PingEntries[k].Ping;
		}
	}

	return -1.0;
}

// -----------------------------------------------------------------
void UServersPageWidget::ClearPingArray()
{
	UNetDriver * NetDriver = GetWorld()->GetNetDriver();
	if(NetDriver != NULL)
		NetDriver->PingEntries.Empty();
}

PingServer(const FOnlineSessionSearchResult & Result) builds & sends a ping packet to server. You have to call that on all your search results on a regular time basis. I do it every second for at least 10 secs.

To get Ping values, call GetPingByOwner(TSharedPtr OwningUserId). result is in seconds, so you have to multilply it by 1000 to get a ms ping measurement.

When initiating a new search, I clear PingEntries in array by calling ClearPingArray().

Important notes :

  • Your game must be listening in order to receive Pong packets back from servers. So be sure to add ?Listen to your arguments or call UWorld::InitListen() (or something like that)
  • Your game might be reported as a potential threat by some AntiVirus software to user. Annoying.

I commented my code, so you might be able to get how it works. I assumed that Steam NetIds are 8 bytes long.

Some optimizations are possible.

Good luck