Steam Session Ping 9999

Hi Nonlin, It seems to be an engine problem as I’ve tested it on example project and my own project with same results, and although it works perfectly locally once you add another PC on a different network both connected to steam you’ll get a ping return of 9999 which stops you testing your multiplayer game. (I’m sure your aware that just because it works locally it doesn’t mean it works online properly until tested, which means we’re kinda buggered until they fix problem which could be tomorrow or in a years time with answers we’re getting.)

1 Like

Everyone interested in this bug can go to new Unreal Issues tracker and vote for it.

Issue ID is UE-27444

Hi guys!

I have stumbled upon this issue with 9999 ping values while implementing Steam version of my upcoming game. I couldn’t find any clue searching online, so I decided to dig into code starting from Steam SDK and then analyzing how Unreal Engine Steam Online Subsystem is mapped into Steam APIs.
I’ve just written an article on my blog where I explain what I have found. I hope you can find it useful.

Ping 9999 Issue on Unreal Engine Steam Session Results

4.12.5
Still problem with steam, but…
When i search a game in LAN ping shows fine, then if i enter in host game and i call ping, it shows me 9999.

Hi, as I explained here, it’s not an issue of UE. It’s just Steam that doesn’t offer any ping information when searching for a lobby.

Any updates with this issue? I’m having same problem, all clients see is 9999 ping and only 1 player for every session.

In code of MyGameSession in your blog you use a variable SS, which should be socket subsystem. How do you get it?

Just updated blog post.
code to get Socket subsystem is:

ISocketSubsystem* SS = ISocketSubsystem::Get();

Thank you. I was doing some experiments and your code is of great help. Do you know how net driver could be accessed?

GetWorld()->GetNetDriver();

Bear in mind that if you want to modify UE Steam net driver, or create your own based on it, you must have a deep understanding of UE modules architecture.

I’ve written this post as a reminder. At moment I’m simply relying on Steam matchmaking result ordering. Maybe in future I will make some experiments and try to implement a custom ping handshaking for lobby searches.

Surely I’ll write some detailed post in future about UE modules/plugins and on UE multiplayer in general.

Thank you again, but I already tried that and it returns null.

I think a very quick and dirty way to ping host could be to send some message and then do a busy wait until something is received (checking with RecvFrom) or a timeout is hit.

I was thinking about sending a regular handshake packet (hence I was trying to access net driver), so that host responds without having to modify engine. However, this could not be a good idea since handshake could then continue if response comes after timeout, which is not desirable. So it could be worth to create a custom ping protocol.

net driver is null if you don’t have any client or server socket active in your map.
If you load a map by passing the ?listen param inside map URL, you will get a server listening socket active on your map. In this case net driver will be loaded and not null.

When you perform a session search, typically you are searching for sessions corresponding to game instances running a map in listening (server) mode, so that you can connect and join them. net driver is available first on server. On clients only after a connection to a server (session join and map travel).

In tests I did, I was able to send packets to a listening server and receive them on server without need to load net driver on client side. Just creating a socket on client did job.

If you want to make some experiments with net driver, do them on a server by opening a map in listening mode.

So I recently found something maybe useful to get ping right:
http://www.aclockworkberry.com/ping-9999-issue-on-unreal-engine-steam-session-results/

Hi, I’ve already posted that link above in this thread, where you can find a little discussion about it.

Oh sorry didn’t saw that… Will this actually work?

It should work, but it is not easy to implement well. Didn’t have time to implement it yet. That blog post is a remainder for future tests.

Are there any news on this issue?

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

Someone have news? I have created a multiplayer online by blueprints and i will not launch game for this ping 9999.

it is now 2018 and this is still happening how many updates does it take before we can get a correct ping ?

1 Like