Client LuaCsForBarotrauma
PingUtils.cs
1 using Barotrauma.Steam;
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
5 using System.Net;
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;
13 
14 namespace Barotrauma.Networking
15 {
16  static class PingUtils
17  {
18  private static readonly Dictionary<IPEndPoint, int> activePings = new Dictionary<IPEndPoint, int>();
19 
20  private static bool steamPingInfoReady;
21 
22  public static void QueryPingData()
23  {
24  steamPingInfoReady = false;
25  if (SteamManager.IsInitialized)
26  {
27  TaskPool.Add("WaitForPingDataAsync (serverlist)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), task =>
28  {
29  steamPingInfoReady = true;
30  });
31  }
32  }
33 
34  public static void GetServerPing(ServerInfo serverInfo, Action<ServerInfo> onPingDiscovered)
35  {
36  if (CoroutineManager.IsCoroutineRunning("ConnectToServer")) { return; }
37 
38  var endpointOption = serverInfo.Endpoints.FirstOrNone(e => e is not EosP2PEndpoint);
39  if (!endpointOption.TryUnwrap(out var endpoint)) { return; }
40 
41  switch (endpoint)
42  {
43  case LidgrenEndpoint { NetEndpoint: var endPoint }:
44  GetIPAddressPing(serverInfo, endPoint, onPingDiscovered);
45  break;
46  case SteamP2PEndpoint:
47  TaskPool.Add($"EstimateSteamLobbyPing ({endpoint.StringRepresentation})",
48  EstimateSteamLobbyPing(serverInfo),
49  t =>
50  {
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);
54  });
55  break;
56  }
57  }
58 
59  private readonly struct LobbyDataChangedEventHandler : IDisposable
60  {
61  private readonly Action<Lobby> action;
62 
63  public LobbyDataChangedEventHandler(Action<Lobby> action)
64  {
65  this.action = action;
66  Steamworks.SteamMatchmaking.OnLobbyDataChanged += action;
67  }
68 
69  public void Dispose()
70  {
71  Steamworks.SteamMatchmaking.OnLobbyDataChanged -= action;
72  }
73  }
74 
75  public static async Task<Lobby?> GetSteamLobbyForUser(SteamId steamId)
76  {
77  var steamFriend = new Steamworks.Friend(steamId.Value);
78  await steamFriend.RequestInfoAsync();
79 
80  var friendLobby = steamFriend.GameInfo?.Lobby;
81  if (!(friendLobby is { } lobby)) { return null; }
82 
83  bool waiting = true;
84  Lobby loadedLobby = default;
85 
86  void finishWaiting(Steamworks.Data.Lobby l)
87  {
88  loadedLobby = l;
89  waiting = false;
90  }
91 
92  using (new LobbyDataChangedEventHandler(finishWaiting))
93  {
94  lobby.Refresh();
95 
96  for (int i = 0;; i++)
97  {
98  if (!waiting) { break; }
99  if (i >= 100) { return null; }
100  }
101  }
102 
103  return loadedLobby;
104  }
105 
106  private enum SteamLobbyPingError
107  {
108  SteamPingUnsupported,
109  FailedToGetHostLocationData,
110  FailedToParseHostLocationData,
111  PingEstimationFailed
112  }
113 
114  private static async Task<Result<int, SteamLobbyPingError>> EstimateSteamLobbyPing(ServerInfo serverInfo)
115  {
116  while (!steamPingInfoReady)
117  {
118  if (!SteamManager.IsInitialized) { return Result.Failure(SteamLobbyPingError.SteamPingUnsupported); }
119  await Task.Delay(50);
120  }
121 
122  string pingLocationStr = "";
123 
124  if (serverInfo.MetadataSource.TryUnwrap(out SteamP2PServerProvider.DataSource src))
125  {
126  var lobby = src.Lobby;
127  pingLocationStr = lobby.GetData("steampinglocation");
128  if (pingLocationStr.IsNullOrEmpty()) { pingLocationStr = lobby.GetData("pinglocation"); }
129  }
130  else if (serverInfo.MetadataSource.TryUnwrap(out EosServerProvider.DataSource srcEos))
131  {
132  pingLocationStr = srcEos.SteamPingLocation;
133  }
134  else if (serverInfo.Endpoints.OfType<SteamP2PEndpoint>().FirstOrNone().TryUnwrap(out var steamP2PEndpoint))
135  {
136  var friendLobby = await GetSteamLobbyForUser(steamP2PEndpoint.SteamId);
137  pingLocationStr = friendLobby?.GetData("steampinglocation") ?? "";
138  }
139 
140  if (pingLocationStr.IsNullOrEmpty())
141  {
142  return Result.Failure(SteamLobbyPingError.FailedToGetHostLocationData);
143  }
144 
145  var pingLocation = NetPingLocation.TryParseFromString(pingLocationStr);
146 
147  if (pingLocation.HasValue)
148  {
149  int ping = Steamworks.SteamNetworkingUtils.EstimatePingTo(pingLocation.Value);
150  if (ping < 0) { return Result.Failure(SteamLobbyPingError.PingEstimationFailed); }
151  return Result.Success(ping);
152  }
153  else
154  {
155  return Result.Failure(SteamLobbyPingError.FailedToParseHostLocationData);
156  }
157  }
158 
159  private static void GetIPAddressPing(ServerInfo serverInfo, IPEndPoint endPoint, Action<ServerInfo> onPingDiscovered)
160  {
161  if (IPAddress.IsLoopback(endPoint.Address))
162  {
163  serverInfo.Ping = Option<int>.Some(0);
164  onPingDiscovered(serverInfo);
165  }
166  else
167  {
168  lock (activePings)
169  {
170  if (activePings.ContainsKey(endPoint)) { return; }
171  activePings.Add(endPoint, activePings.Any() ? activePings.Values.Max() + 1 : 0);
172  }
173  serverInfo.Ping = Option<int>.None();
174  TaskPool.Add($"PingServerAsync ({endPoint})", PingServerAsync(endPoint, 1000),
175  rtt =>
176  {
177  if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option<int>.None(); }
178  onPingDiscovered(serverInfo);
179  lock (activePings)
180  {
181  activePings.Remove(endPoint);
182  }
183  });
184  }
185  }
186 
187  private static async Task<Option<int>> PingServerAsync(IPEndPoint endPoint, int timeOut)
188  {
189  await Task.Yield();
190  bool shouldGo = false;
191  while (!shouldGo)
192  {
193  lock (activePings)
194  {
195  shouldGo = activePings.Count(kvp => kvp.Value < activePings[endPoint]) < 25;
196  }
197  await Task.Delay(25);
198  }
199 
200  if (endPoint?.Address == null) { return Option<int>.None(); }
201 
202  //don't attempt to ping if the address is IPv6 and it's not supported
203  if (endPoint.Address.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option<int>.None(); }
204 
205  Ping ping = new Ping();
206  byte[] buffer = new byte[32];
207  try
208  {
209  PingReply pingReply = await ping.SendPingAsync(endPoint.Address, timeOut, buffer, new PingOptions(128, true));
210 
211  return pingReply.Status switch
212  {
213  IPStatus.Success => Option<int>.Some((int)pingReply.RoundtripTime),
214  _ => Option<int>.None(),
215  };
216  }
217  catch (Exception ex)
218  {
219  GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + endPoint.Address, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message));
220 #if DEBUG
221  DebugConsole.NewMessage("Failed to ping a server (" + endPoint.Address + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red);
222 #endif
223 
224  return Option<int>.None();
225  }
226  }
227  }
228 }