3 using System.Collections.Generic;
7 using System.Threading;
11 internal sealed
class SteamP2PClientPeer : ClientPeer
13 private readonly SteamId hostSteamId;
14 private double timeout;
15 private double heartbeatTimer;
16 private double connectionStatusTimer;
18 private long sentBytes, receivedBytes;
20 private readonly List<IncomingInitializationMessage> incomingInitializationMessages =
new List<IncomingInitializationMessage>();
21 private readonly List<IReadMessage> incomingDataMessages =
new List<IReadMessage>();
23 public SteamP2PClientPeer(SteamP2PEndpoint endpoint, Callbacks callbacks) : base(endpoint, callbacks, Option<int>.
None())
25 ServerConnection =
null;
29 if (!(ServerEndpoint is SteamP2PEndpoint steamIdEndpoint))
31 throw new InvalidCastException(
"endPoint is not SteamId");
34 hostSteamId = steamIdEndpoint.SteamId;
37 public override void Start()
39 if (isActive) {
return; }
41 ContentPackageOrderReceived =
false;
43 steamAuthTicket = SteamManager.GetAuthSessionTicketForMultiplayer(ServerEndpoint);
46 if (steamAuthTicket ==
null)
48 throw new Exception(
"GetAuthSessionTicket returned null");
51 Steamworks.SteamNetworking.ResetActions();
52 Steamworks.SteamNetworking.OnP2PSessionRequest = OnIncomingConnection;
53 Steamworks.SteamNetworking.OnP2PConnectionFailed = OnConnectionFailed;
55 Steamworks.SteamNetworking.AllowP2PPacketRelay(
true);
57 ServerConnection =
new SteamP2PConnection(hostSteamId);
58 ServerConnection.SetAccountInfo(
new AccountInfo(hostSteamId));
60 var headers =
new PeerPacketHeaders
66 SendMsgInternal(headers,
null);
70 timeout = NetworkConnection.TimeoutThreshold;
72 connectionStatusTimer = 0.0;
77 private void OnIncomingConnection(Steamworks.SteamId steamId)
79 if (!isActive) {
return; }
81 if (steamId == hostSteamId.Value)
83 Steamworks.SteamNetworking.AcceptP2PSessionWithUser(steamId);
89 DebugConsole.ThrowError(
"Connection from incorrect SteamID was rejected: " +
90 $
"expected {hostSteamId}," +
91 $
"got {new SteamId(steamId)}");
95 private void OnConnectionFailed(Steamworks.SteamId steamId, Steamworks.P2PSessionError error)
97 if (!isActive) {
return; }
99 if (steamId != hostSteamId.Value) {
return; }
101 Close(PeerDisconnectPacket.SteamP2PError(error));
104 private void OnP2PData(ulong steamId,
byte[] data,
int dataLength)
106 if (!isActive) {
return; }
108 if (steamId != hostSteamId.Value) {
return; }
110 timeout = Screen.Selected == GameMain.GameScreen
111 ? NetworkConnection.TimeoutThresholdInGame
112 : NetworkConnection.TimeoutThreshold;
116 IReadMessage inc =
new ReadOnlyMessage(data,
false, 0, dataLength, ServerConnection);
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);
124 DebugConsole.ThrowError(errorMsg);
126 if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
131 private void ProcessP2PData(IReadMessage inc)
133 var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
135 if (!packetHeader.IsServerMessage()) {
return; }
137 if (packetHeader.IsConnectionInitializationStep())
139 if (!initialization.HasValue) {
return; }
141 var relayPacket = INetSerializableStruct.Read<SteamP2PInitializationRelayPacket>(inc);
143 SteamManager.JoinLobby(relayPacket.LobbyID,
false);
146 incomingInitializationMessages.Add(
new IncomingInitializationMessage
148 InitializationStep = initialization.Value,
149 Message = relayPacket.Message.GetReadMessageUncompressed()
153 else if (packetHeader.IsHeartbeatMessage())
157 else if (packetHeader.IsDisconnectMessage())
159 PeerDisconnectPacket packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
164 var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
165 incomingDataMessages.Add(packet.GetReadMessage(packetHeader.IsCompressed(), ServerConnection!));
169 public override void Update(
float deltaTime)
171 if (!isActive) {
return; }
173 if (GameMain.Client ==
null || !GameMain.Client.RoundStarting)
175 timeout -= deltaTime;
178 heartbeatTimer -= deltaTime;
184 connectionStatusTimer -= deltaTime;
185 if (connectionStatusTimer <= 0.0)
187 if (Steamworks.SteamNetworking.GetP2PSessionState(hostSteamId.Value) is { } state)
189 if (state.P2PSessionError != Steamworks.P2PSessionError.None)
191 Close(PeerDisconnectPacket.SteamP2PError(state.P2PSessionError));
196 Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
199 connectionStatusTimer = 1.0f;
203 for (
int i = 0; i < 100; i++)
205 if (!Steamworks.SteamNetworking.IsP2PPacketAvailable()) {
break; }
207 var packet = Steamworks.SteamNetworking.ReadP2PPacket();
208 if (packet is { SteamId: var steamId,
Data: var data })
210 OnP2PData(steamId, data, data.Length);
211 if (!isActive) {
return; }
212 receivedBytes += data.Length;
216 GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.ReceivedBytes, receivedBytes);
217 GameMain.Client?.NetStats?.AddValue(NetStats.NetStatType.SentBytes, sentBytes);
219 if (heartbeatTimer < 0.0)
221 var headers =
new PeerPacketHeaders
225 Initialization =
null
227 SendMsgInternal(headers,
null);
232 Close(PeerDisconnectPacket.WithReason(DisconnectReason.SteamP2PTimeOut));
238 if (incomingDataMessages.Count > 0)
240 void initializationError(
string errorMsg,
string analyticsTag)
242 GameAnalyticsManager.AddErrorEventOnce($
"SteamP2PClientPeer.OnInitializationComplete:{analyticsTag}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
243 DebugConsole.ThrowError(errorMsg);
244 Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
247 if (!ContentPackageOrderReceived)
250 errorMsg:
"Error during connection initialization: completed initialization before receiving content package order.",
251 analyticsTag:
"ContentPackageOrderNotReceived");
254 if (ServerContentPackages.Length == 0)
257 errorMsg:
"Error during connection initialization: list of content packages enabled on the server was empty when completing initialization.",
258 analyticsTag:
"NoContentPackages");
261 callbacks.OnInitializationComplete.Invoke();
266 foreach (var inc
in incomingInitializationMessages)
268 ReadConnectionInitializationStep(inc);
275 foreach (IReadMessage inc
in incomingDataMessages)
277 callbacks.OnMessageReceived.Invoke(inc);
281 incomingInitializationMessages.Clear();
282 incomingDataMessages.Clear();
285 public override void Send(IWriteMessage msg,
DeliveryMethod deliveryMethod,
bool compressPastThreshold =
true)
287 if (!isActive) {
return; }
289 byte[] bufAux = msg.PrepareForSending(compressPastThreshold, out
bool isCompressed, out _);
291 var headers =
new PeerPacketHeaders
295 Initialization =
null
297 var body =
new PeerPacketMessage
302 heartbeatTimer = 5.0;
305 void performSend() => SendMsgInternal(headers, body);
307 CoroutineManager.Invoke(() =>
309 if (GameMain.Client ==
null) { return; }
311 if (Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedLoss && deliveryMethod is
DeliveryMethod.Unreliable) { return; }
313 int count = Rand.Range(0.0f, 1.0f) < GameMain.Client.SimulatedDuplicatesChance ? 2 : 1;
314 for (
int i = 0; i < count; i++)
319 GameMain.Client.SimulatedMinimumLatency + Rand.Range(0.0f, GameMain.Client.SimulatedRandomLatency));
325 public override void SendPassword(
string password)
327 if (!isActive) {
return; }
331 var headers =
new PeerPacketHeaders
337 var body =
new ClientPeerPasswordPacket
339 Password = ServerSettings.SaltPassword(Encoding.UTF8.GetBytes(password), passwordSalt)
342 SendMsgInternal(headers, body);
345 public override void Close(PeerDisconnectPacket peerDisconnectPacket)
347 if (!isActive) {
return; }
349 SteamManager.LeaveLobby();
353 var headers =
new PeerPacketHeaders
357 Initialization =
null
359 SendMsgInternal(headers, peerDisconnectPacket);
363 Steamworks.SteamNetworking.ResetActions();
364 Steamworks.SteamNetworking.CloseP2PSessionWithUser(hostSteamId.Value);
366 if (steamAuthTicket.TryUnwrap(out var ticket)) { ticket.Cancel(); }
367 steamAuthTicket = Option.None;
369 callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
372 protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body)
374 IWriteMessage msgToSend =
new WriteOnlyMessage();
375 msgToSend.WriteNetSerializableStruct(headers);
376 body?.Write(msgToSend);
377 ForwardToSteamP2P(msgToSend, headers.DeliveryMethod);
380 private void ForwardToSteamP2P(IWriteMessage msg,
DeliveryMethod deliveryMethod)
382 heartbeatTimer = 5.0;
383 int length = msg.LengthBytes;
385 bool successSend = Steamworks.SteamNetworking.SendP2PPacket(hostSteamId.Value, msg.Buffer, length, 0, deliveryMethod.ToSteam());
388 if (successSend) {
return; }
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());
399 DebugConsole.AddWarning($
"Failed to send message to remote peer! ({length} bytes)");
404 public override void ForceTimeOut()