3 using System.Collections.Generic;
6 using System.Net.NetworkInformation;
7 using System.Net.Sockets;
8 using System.Threading.Tasks;
10 using Steamworks.Data;
11 using Color = Microsoft.Xna.Framework.Color;
12 using Socket = System.Net.Sockets.Socket;
16 static class PingUtils
18 private static readonly Dictionary<IPEndPoint, int> activePings =
new Dictionary<IPEndPoint, int>();
20 private static bool steamPingInfoReady;
22 public static void QueryPingData()
24 steamPingInfoReady =
false;
25 if (SteamManager.IsInitialized)
27 TaskPool.Add(
"WaitForPingDataAsync (serverlist)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), task =>
29 steamPingInfoReady = true;
34 public static void GetServerPing(ServerInfo serverInfo, Action<ServerInfo> onPingDiscovered)
36 if (CoroutineManager.IsCoroutineRunning(
"ConnectToServer")) {
return; }
38 var endpointOption = serverInfo.Endpoints.FirstOrNone(e => e is not
EosP2PEndpoint);
39 if (!endpointOption.TryUnwrap(out var endpoint)) {
return; }
43 case LidgrenEndpoint { NetEndpoint: var endPoint }:
44 GetIPAddressPing(serverInfo, endPoint, onPingDiscovered);
46 case SteamP2PEndpoint:
47 TaskPool.Add($
"EstimateSteamLobbyPing ({endpoint.StringRepresentation})",
48 EstimateSteamLobbyPing(serverInfo),
51 if (!t.TryGetResult(out Result<int, SteamLobbyPingError> ping)) { return; }
52 serverInfo.Ping = ping.TryUnwrapSuccess(out var ms) ? Option.Some(ms) : Option.None;
53 onPingDiscovered(serverInfo);
59 private readonly
struct LobbyDataChangedEventHandler : IDisposable
61 private readonly Action<Lobby> action;
63 public LobbyDataChangedEventHandler(Action<Lobby> action)
66 Steamworks.SteamMatchmaking.OnLobbyDataChanged += action;
71 Steamworks.SteamMatchmaking.OnLobbyDataChanged -= action;
75 public static async Task<Lobby?> GetSteamLobbyForUser(SteamId steamId)
77 var steamFriend =
new Steamworks.Friend(steamId.Value);
78 await steamFriend.RequestInfoAsync();
80 var friendLobby = steamFriend.GameInfo?.Lobby;
81 if (!(friendLobby is { } lobby)) {
return null; }
84 Lobby loadedLobby =
default;
86 void finishWaiting(Steamworks.Data.Lobby l)
92 using (
new LobbyDataChangedEventHandler(finishWaiting))
98 if (!waiting) {
break; }
99 if (i >= 100) {
return null; }
106 private enum SteamLobbyPingError
108 SteamPingUnsupported,
109 FailedToGetHostLocationData,
110 FailedToParseHostLocationData,
114 private static async Task<Result<int, SteamLobbyPingError>> EstimateSteamLobbyPing(ServerInfo serverInfo)
116 while (!steamPingInfoReady)
118 if (!SteamManager.IsInitialized) {
return Result.Failure(SteamLobbyPingError.SteamPingUnsupported); }
119 await Task.Delay(50);
122 string pingLocationStr =
"";
124 if (serverInfo.MetadataSource.TryUnwrap(out SteamP2PServerProvider.DataSource src))
126 var lobby = src.Lobby;
127 pingLocationStr = lobby.GetData(
"steampinglocation");
128 if (pingLocationStr.IsNullOrEmpty()) { pingLocationStr = lobby.GetData(
"pinglocation"); }
132 pingLocationStr = srcEos.SteamPingLocation;
134 else if (serverInfo.Endpoints.OfType<SteamP2PEndpoint>().FirstOrNone().TryUnwrap(out var steamP2PEndpoint))
136 var friendLobby = await GetSteamLobbyForUser(steamP2PEndpoint.SteamId);
137 pingLocationStr = friendLobby?.GetData(
"steampinglocation") ??
"";
140 if (pingLocationStr.IsNullOrEmpty())
142 return Result.Failure(SteamLobbyPingError.FailedToGetHostLocationData);
145 var pingLocation = NetPingLocation.TryParseFromString(pingLocationStr);
147 if (pingLocation.HasValue)
149 int ping = Steamworks.SteamNetworkingUtils.EstimatePingTo(pingLocation.Value);
150 if (ping < 0) {
return Result.Failure(SteamLobbyPingError.PingEstimationFailed); }
151 return Result.Success(ping);
155 return Result.Failure(SteamLobbyPingError.FailedToParseHostLocationData);
159 private static void GetIPAddressPing(ServerInfo serverInfo, IPEndPoint endPoint, Action<ServerInfo> onPingDiscovered)
161 if (IPAddress.IsLoopback(endPoint.Address))
163 serverInfo.Ping = Option<int>.Some(0);
164 onPingDiscovered(serverInfo);
170 if (activePings.ContainsKey(endPoint)) {
return; }
171 activePings.Add(endPoint, activePings.Any() ? activePings.Values.Max() + 1 : 0);
173 serverInfo.Ping = Option<int>.None();
174 TaskPool.Add($
"PingServerAsync ({endPoint})", PingServerAsync(endPoint, 1000),
177 if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option<int>.None(); }
178 onPingDiscovered(serverInfo);
181 activePings.Remove(endPoint);
187 private static async Task<Option<int>> PingServerAsync(IPEndPoint endPoint,
int timeOut)
190 bool shouldGo =
false;
195 shouldGo = activePings.Count(kvp => kvp.Value < activePings[endPoint]) < 25;
197 await Task.Delay(25);
200 if (endPoint?.Address ==
null) {
return Option<int>.None(); }
203 if (endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) {
return Option<int>.None(); }
205 Ping ping =
new Ping();
206 byte[] buffer =
new byte[32];
209 PingReply pingReply = await ping.SendPingAsync(endPoint.Address, timeOut, buffer,
new PingOptions(128,
true));
211 return pingReply.Status
switch
213 IPStatus.Success => Option<int>.Some((
int)pingReply.RoundtripTime),
214 _ => Option<int>.None(),
219 GameAnalyticsManager.AddErrorEventOnce(
"ServerListScreen.PingServer:PingException" + endPoint.Address, GameAnalyticsManager.ErrorSeverity.Warning,
"Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message));
221 DebugConsole.NewMessage(
"Failed to ping a server (" + endPoint.Address +
") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red);
224 return Option<int>.None();