Barotrauma Client Doc
SteamP2PClientPeer.cs
1 #nullable enable
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
5 using System.Text;
6 using Barotrauma.Steam;
7 using System.Threading;
8 
9 namespace Barotrauma.Networking
10 {
11  internal sealed class SteamP2PClientPeer : ClientPeer
12  {
13  private readonly SteamId hostSteamId;
14  private double timeout;
15  private double heartbeatTimer;
16  private double connectionStatusTimer;
17 
18  private long sentBytes, receivedBytes;
19 
20  private readonly List<IncomingInitializationMessage> incomingInitializationMessages = new List<IncomingInitializationMessage>();
21  private readonly List<IReadMessage> incomingDataMessages = new List<IReadMessage>();
22 
23  public SteamP2PClientPeer(SteamP2PEndpoint endpoint, Callbacks callbacks) : base(endpoint, callbacks, Option<int>.None())
24  {
25  ServerConnection = null;
26 
27  isActive = false;
28 
29  if (!(ServerEndpoint is SteamP2PEndpoint steamIdEndpoint))
30  {
31  throw new InvalidCastException("endPoint is not SteamId");
32  }
33 
34  hostSteamId = steamIdEndpoint.SteamId;
35  }
36 
37  public override void Start()
38  {
39  if (isActive) { return; }
40 
41  ContentPackageOrderReceived = false;
42 
43  steamAuthTicket = SteamManager.GetAuthSessionTicketForMultiplayer(ServerEndpoint);
44  //TODO: wait for GetAuthSessionTicketResponse_t
45 
46  if (steamAuthTicket == null)
47  {
48  throw new Exception("GetAuthSessionTicket returned null");
49  }
50 
51  Steamworks.SteamNetworking.ResetActions();
52  Steamworks.SteamNetworking.OnP2PSessionRequest = OnIncomingConnection;
53  Steamworks.SteamNetworking.OnP2PConnectionFailed = OnConnectionFailed;
54 
55  Steamworks.SteamNetworking.AllowP2PPacketRelay(true);
56 
57  ServerConnection = new SteamP2PConnection(hostSteamId);
58  ServerConnection.SetAccountInfo(new AccountInfo(hostSteamId));
59 
60  var headers = new PeerPacketHeaders
61  {
62  DeliveryMethod = DeliveryMethod.Reliable,
63  PacketHeader = PacketHeader.IsConnectionInitializationStep,
64  Initialization = ConnectionInitialization.ConnectionStarted
65  };
66  SendMsgInternal(headers, null);
67 
68  initializationStep = ConnectionInitialization.SteamTicketAndVersion;
69 
70  timeout = NetworkConnection.TimeoutThreshold;
71  heartbeatTimer = 1.0;
72  connectionStatusTimer = 0.0;
73 
74  isActive = true;
75  }
76 
77  private void OnIncomingConnection(Steamworks.SteamId steamId)
78  {
79  if (!isActive) { return; }
80 
81  if (steamId == hostSteamId.Value)
82  {
83  Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId);
84  }
85  else if (initializationStep != ConnectionInitialization.Password &&
86  initializationStep != ConnectionInitialization.ContentPackageOrder &&
87  initializationStep != ConnectionInitialization.Success)
88  {
89  DebugConsole.ThrowError("Connection from incorrect SteamID was rejected: " +
90  $"expected {hostSteamId}," +
91  $"got {new SteamId(steamId)}");
92  }
93  }
94 
95  private void OnConnectionFailed(Steamworks.SteamId steamId, Steamworks.P2PSessionError error)
96  {
97  if (!isActive) { return; }
98 
99  if (steamId != hostSteamId.Value) { return; }
100 
101  Close(PeerDisconnectPacket.SteamP2PError(error));
102  }
103 
104  private void OnP2PData(ulong steamId, byte[] data, int dataLength)
105  {
106  if (!isActive) { return; }
107 
108  if (steamId != hostSteamId.Value) { return; }
109 
110  timeout = Screen.Selected == GameMain.GameScreen
111  ? NetworkConnection.TimeoutThresholdInGame
112  : NetworkConnection.TimeoutThreshold;
113 
114  try
115  {
116  IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection);
117  ProcessP2PData(inc);
118  }
119  catch (Exception e)
120  {
121  string errorMsg = $"Client failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}";
122  GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnP2PData:ClientReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
123 #if DEBUG
124  DebugConsole.ThrowError(errorMsg);
125 #else
126  if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
127 #endif
128  }
129  }
130 
131  private void ProcessP2PData(IReadMessage inc)
132  {
133  var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
134 
135  if (!packetHeader.IsServerMessage()) { return; }
136 
137  if (packetHeader.IsConnectionInitializationStep())
138  {
139  if (!initialization.HasValue) { return; }
140 
141  var relayPacket = INetSerializableStruct.Read<SteamP2PInitializationRelayPacket>(inc);
142 
143  SteamManager.JoinLobby(relayPacket.LobbyID, false);
144  if (initializationStep != ConnectionInitialization.Success)
145  {
146  incomingInitializationMessages.Add(new IncomingInitializationMessage
147  {
148  InitializationStep = initialization.Value,
149  Message = relayPacket.Message.GetReadMessageUncompressed()
150  });
151  }
152  }
153  else if (packetHeader.IsHeartbeatMessage())
154  {
155  return; //TODO: implement heartbeats
156  }
157  else if (packetHeader.IsDisconnectMessage())
158  {
159  PeerDisconnectPacket packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
160  Close(packet);
161  }
162  else
163  {
164  var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
165  incomingDataMessages.Add(packet.GetReadMessage(packetHeader.IsCompressed(), ServerConnection!));
166  }
167  }
168 
169  public override void Update(float deltaTime)
170  {
171  if (!isActive) { return; }
172 
173  if (GameMain.Client == null || !GameMain.Client.RoundStarting)
174  {
175  timeout -= deltaTime;
176  }
177 
178  heartbeatTimer -= deltaTime;
179 
180  if (initializationStep != ConnectionInitialization.Password &&
181  initializationStep != ConnectionInitialization.ContentPackageOrder &&
182  initializationStep != ConnectionInitialization.Success)
183  {
184  connectionStatusTimer -= deltaTime;
185  if (connectionStatusTimer <= 0.0)
186  {
187  if (Steamworks.SteamNetworking.GetP2PSessionState(hostSteamId.Value) is { } state)
188  {
189  if (state.P2PSessionError != Steamworks.P2PSessionError.None)
190  {
191  Close(PeerDisconnectPacket.SteamP2PError(state.P2PSessionError));
192  }
193  }
194  else
195  {
196  Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
197  }
198 
199  connectionStatusTimer = 1.0f;
200  }
201  }
202 
203  for (int i = 0; i < 100; i++)
204  {
205  if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) { break; }
206 
207  var packet = Steamworks.SteamNetworking.ReadP2PPacket();
208  if (packet is { SteamId: var steamId, Data: var data })
209  {
210  OnP2PData(steamId, data, data.Length);
211  if (!isActive) { return; }
212  receivedBytes += data.Length;
213  }
214  }
215 
216  GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes);
217  GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes);
218 
219  if (heartbeatTimer < 0.0)
220  {
221  var headers = new PeerPacketHeaders
222  {
223  DeliveryMethod = DeliveryMethod.Unreliable,
224  PacketHeader = PacketHeader.IsHeartbeatMessage,
225  Initialization = null
226  };
227  SendMsgInternal(headers, null);
228  }
229 
230  if (timeout < 0.0)
231  {
232  Close(PeerDisconnectPacket.WithReason(DisconnectReason.SteamP2PTimeOut));
233  return;
234  }
235 
236  if (initializationStep != ConnectionInitialization.Success)
237  {
238  if (incomingDataMessages.Count > 0)
239  {
240  void initializationError(string errorMsg, string analyticsTag)
241  {
242  GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnInitializationComplete:{analyticsTag}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
243  DebugConsole.ThrowError(errorMsg);
244  Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
245  }
246 
247  if (!ContentPackageOrderReceived)
248  {
249  initializationError(
250  errorMsg: "Error during connection initialization: completed initialization before receiving content package order.",
251  analyticsTag: "ContentPackageOrderNotReceived");
252  return;
253  }
254  if (ServerContentPackages.Length == 0)
255  {
256  initializationError(
257  errorMsg: "Error during connection initialization: list of content packages enabled on the server was empty when completing initialization.",
258  analyticsTag: "NoContentPackages");
259  return;
260  }
261  callbacks.OnInitializationComplete.Invoke();
262  initializationStep = ConnectionInitialization.Success;
263  }
264  else
265  {
266  foreach (var inc in incomingInitializationMessages)
267  {
268  ReadConnectionInitializationStep(inc);
269  }
270  }
271  }
272 
273  if (initializationStep == ConnectionInitialization.Success)
274  {
275  foreach (IReadMessage inc in incomingDataMessages)
276  {
277  callbacks.OnMessageReceived.Invoke(inc);
278  }
279  }
280 
281  incomingInitializationMessages.Clear();
282  incomingDataMessages.Clear();
283  }
284 
285  public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true)
286  {
287  if (!isActive) { return; }
288 
289  byte[] bufAux = msg.PrepareForSending(compressPastThreshold, out bool isCompressed, out _);
290 
291  var headers = new PeerPacketHeaders
292  {
293  DeliveryMethod = deliveryMethod,
294  PacketHeader = isCompressed ? PacketHeader.IsCompressed : PacketHeader.None,
295  Initialization = null
296  };
297  var body = new PeerPacketMessage
298  {
299  Buffer = bufAux
300  };
301 
302  heartbeatTimer = 5.0;
303 
304  // Using an extra local method here to reduce chance of error whenever we need to change this
305  void performSend() => SendMsgInternal(headers, body);
306 #if DEBUG
307  CoroutineManager.Invoke(() =>
308  {
309  if (GameMain.Client == null) { return; }
310 
311  if (Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedLoss && deliveryMethod is DeliveryMethod.Unreliable) { return; }
312 
313  int count = Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedDuplicatesChance ? 2 : 1;
314  for (int i = 0; i < count; i++)
315  {
316  performSend();
317  }
318  },
319  GameMain.Client.SimulatedMinimumLatency + Rand.Range(0.0f, GameMain.Client.SimulatedRandomLatency));
320 #else
321  performSend();
322 #endif
323  }
324 
325  public override void SendPassword(string password)
326  {
327  if (!isActive) { return; }
328 
329  if (initializationStep != ConnectionInitialization.Password) { return; }
330 
331  var headers = new PeerPacketHeaders
332  {
333  DeliveryMethod = DeliveryMethod.Reliable,
334  PacketHeader = PacketHeader.IsConnectionInitializationStep,
335  Initialization = ConnectionInitialization.Password
336  };
337  var body = new ClientPeerPasswordPacket
338  {
339  Password = ServerSettings.SaltPassword(Encoding.UTF8.GetBytes(password), passwordSalt)
340  };
341 
342  SendMsgInternal(headers, body);
343  }
344 
345  public override void Close(PeerDisconnectPacket peerDisconnectPacket)
346  {
347  if (!isActive) { return; }
348 
349  SteamManager.LeaveLobby();
350 
351  isActive = false;
352 
353  var headers = new PeerPacketHeaders
354  {
355  DeliveryMethod = DeliveryMethod.Reliable,
356  PacketHeader = PacketHeader.IsDisconnectMessage,
357  Initialization = null
358  };
359  SendMsgInternal(headers, peerDisconnectPacket);
360 
361  Thread.Sleep(100);
362 
363  Steamworks.SteamNetworking.ResetActions();
364  Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId.Value);
365 
366  if (steamAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); }
367  steamAuthTicket = Option.None;
368 
369  callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
370  }
371 
372  protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body)
373  {
374  IWriteMessage msgToSend = new WriteOnlyMessage();
375  msgToSend.WriteNetSerializableStruct(headers);
376  body?.Write(msgToSend);
377  ForwardToSteamP2P(msgToSend, headers.DeliveryMethod);
378  }
379 
380  private void ForwardToSteamP2P(IWriteMessage msg, DeliveryMethod deliveryMethod)
381  {
382  heartbeatTimer = 5.0;
383  int length = msg.LengthBytes;
384 
385  bool successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, msg.Buffer, length, 0, deliveryMethod.ToSteam());
386  sentBytes += length;
387 
388  if (successSend) { return; }
389 
390  if (deliveryMethod is DeliveryMethod.Unreliable)
391  {
392  DebugConsole.Log($"WARNING: message couldn't be sent unreliably, forcing reliable send ({length} bytes)");
393  successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, msg.Buffer, length, 0, DeliveryMethod.Reliable.ToSteam());
394  sentBytes += length;
395  }
396 
397  if (!successSend)
398  {
399  DebugConsole.AddWarning($"Failed to send message to remote peer! ({length} bytes)");
400  }
401  }
402 
403 #if DEBUG
404  public override void ForceTimeOut()
405  {
406  timeout = 0.0f;
407  }
408 #endif
409  }
410 }