NetworkSession.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. using System;
  2. using System.Net;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Linq;
  6. using System.Net.Sockets;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using Microsoft.Xna.Framework.GamerServices;
  10. namespace Microsoft.Xna.Framework.Net
  11. {
  12. /// <summary>
  13. /// Represents a network session for multiplayer gaming.
  14. /// </summary>
  15. public class NetworkSession : IDisposable, IAsyncDisposable
  16. {
  17. // Event for received messages
  18. public event EventHandler<MessageReceivedEventArgs> MessageReceived;
  19. private readonly List<NetworkGamer> gamers;
  20. private readonly GamerCollection gamerCollection;
  21. private readonly NetworkSessionType sessionType;
  22. private readonly int maxGamers;
  23. private readonly int privateGamerSlots;
  24. private readonly Dictionary<string, IPEndPoint> gamerEndpoints;
  25. private readonly object lockObject = new object();
  26. private INetworkTransport networkTransport;
  27. internal NetworkSessionState sessionState;
  28. private bool disposed;
  29. private bool isHost;
  30. internal string sessionId;
  31. private Task receiveTask;
  32. private CancellationTokenSource cancellationTokenSource;
  33. // Events
  34. public event EventHandler<GameStartedEventArgs> GameStarted;
  35. public event EventHandler<GameEndedEventArgs> GameEnded;
  36. public event EventHandler<GamerJoinedEventArgs> GamerJoined;
  37. public event EventHandler<GamerLeftEventArgs> GamerLeft;
  38. public event EventHandler<NetworkSessionEndedEventArgs> SessionEnded;
  39. // Static event for invite acceptance
  40. public static event EventHandler<InviteAcceptedEventArgs> InviteAccepted;
  41. /// <summary>
  42. /// Allows changing the network transport implementation.
  43. /// </summary>
  44. public INetworkTransport NetworkTransport
  45. {
  46. get => networkTransport;
  47. set => networkTransport = value ?? throw new ArgumentNullException(nameof(value));
  48. }
  49. /// <summary>
  50. /// Initializes a new NetworkSession.
  51. /// </summary>
  52. private NetworkSession(NetworkSessionType sessionType, int maxGamers, int privateGamerSlots, bool isHost)
  53. {
  54. // Register message types (can be moved to static constructor)
  55. NetworkMessageRegistry.Register<PlayerMoveMessage>(1);
  56. this.sessionType = sessionType;
  57. this.maxGamers = maxGamers;
  58. this.privateGamerSlots = privateGamerSlots;
  59. this.isHost = isHost;
  60. this.sessionId = Guid.NewGuid().ToString();
  61. this.sessionState = NetworkSessionState.Creating;
  62. gamers = new List<NetworkGamer>();
  63. gamerCollection = new GamerCollection(gamers);
  64. gamerEndpoints = new Dictionary<string, IPEndPoint>();
  65. networkTransport = new UdpTransport();
  66. cancellationTokenSource = new CancellationTokenSource();
  67. // Add local gamer
  68. var gamerGuid = Guid.NewGuid().ToString();
  69. var localGamer = new LocalNetworkGamer(this, gamerGuid, isHost, $"{SignedInGamer.Current?.Gamertag ?? "Player"}_{gamerGuid.Substring(0, 8)}");
  70. NetworkGamer.LocalGamer = localGamer;
  71. AddGamer(localGamer);
  72. // Start receive loop for SystemLink sessions
  73. if (sessionType == NetworkSessionType.SystemLink)
  74. {
  75. receiveTask = Task.Run(() => ReceiveLoopAsync(cancellationTokenSource.Token));
  76. }
  77. }
  78. // Internal constructor for SystemLink join
  79. internal NetworkSession(NetworkSessionType sessionType, int maxGamers, int privateGamerSlots, bool isHost, string sessionId)
  80. : this(sessionType, maxGamers, privateGamerSlots, isHost)
  81. {
  82. this.sessionId = sessionId;
  83. this.sessionState = NetworkSessionState.Lobby;
  84. }
  85. /// <summary>
  86. /// Gets all gamers in the session.
  87. /// </summary>
  88. public GamerCollection AllGamers => gamerCollection;
  89. /// <summary>
  90. /// Gets local gamers in the session.
  91. /// </summary>
  92. public LocalGamerCollection LocalGamers
  93. {
  94. get
  95. {
  96. var localGamers = gamers.Where(g => g.IsLocal).OfType<LocalNetworkGamer>().ToList();
  97. return new LocalGamerCollection(localGamers);
  98. }
  99. }
  100. /// <summary>
  101. /// Gets remote gamers in the session.
  102. /// </summary>
  103. public GamerCollection RemoteGamers
  104. {
  105. get
  106. {
  107. var remoteGamers = gamers.Where(g => !g.IsLocal).ToList();
  108. return new GamerCollection(remoteGamers);
  109. }
  110. }
  111. /// <summary>
  112. /// Gets the host gamer.
  113. /// </summary>
  114. public NetworkGamer Host => AllGamers.Host;
  115. /// <summary>
  116. /// Gets whether this machine is the host.
  117. /// </summary>
  118. public bool IsHost => isHost;
  119. /// <summary>
  120. /// Gets whether everyone is ready to start the game.
  121. /// </summary>
  122. public bool IsEveryoneReady => AllGamers.All(g => g.IsReady);
  123. /// <summary>
  124. /// Gets the maximum number of gamers.
  125. /// </summary>
  126. public int MaxGamers => maxGamers;
  127. /// <summary>
  128. /// Gets the number of private gamer slots.
  129. /// </summary>
  130. public int PrivateGamerSlots => privateGamerSlots;
  131. /// <summary>
  132. /// Gets the session type.
  133. /// </summary>
  134. public NetworkSessionType SessionType => sessionType;
  135. /// <summary>
  136. /// Gets the current session state.
  137. /// </summary>
  138. public NetworkSessionState SessionState => sessionState;
  139. /// <summary>
  140. /// Gets the bytes per second being sent.
  141. /// </summary>
  142. public int BytesPerSecondSent => 0; // Mock implementation
  143. /// <summary>
  144. /// Gets the bytes per second being received.
  145. /// </summary>
  146. public int BytesPerSecondReceived => 0; // Mock implementation
  147. /// <summary>
  148. /// Gets whether the session allows host migration.
  149. /// </summary>
  150. public bool AllowHostMigration { get; set; } = true;
  151. /// <summary>
  152. /// Gets whether the session allows gamers to join during gameplay.
  153. /// </summary>
  154. public bool AllowJoinInProgress { get; set; } = true;
  155. private IDictionary<string, object> sessionProperties = new Dictionary<string, object>();
  156. /// <summary>
  157. /// Gets or sets custom session properties.
  158. /// </summary>
  159. public IDictionary<string, object> SessionProperties
  160. {
  161. get => sessionProperties;
  162. set
  163. {
  164. sessionProperties = value ?? throw new ArgumentNullException(nameof(value));
  165. // Automatically broadcast changes if this machine is the host
  166. if (IsHost)
  167. {
  168. BroadcastSessionProperties();
  169. }
  170. }
  171. }
  172. // Simulation properties for testing network conditions
  173. private TimeSpan simulatedLatency = TimeSpan.Zero;
  174. private float simulatedPacketLoss = 0.0f;
  175. /// <summary>
  176. /// Gets or sets the simulated network latency for testing purposes.
  177. /// </summary>
  178. public TimeSpan SimulatedLatency
  179. {
  180. get => simulatedLatency;
  181. set => simulatedLatency = value;
  182. }
  183. /// <summary>
  184. /// Gets or sets the simulated packet loss percentage for testing purposes.
  185. /// </summary>
  186. public float SimulatedPacketLoss
  187. {
  188. get => simulatedPacketLoss;
  189. set => simulatedPacketLoss = Math.Max(0.0f, Math.Min(1.0f, value));
  190. }
  191. /// <summary>
  192. /// Cancels all ongoing async operations for this session.
  193. /// </summary>
  194. public void Cancel()
  195. {
  196. cancellationTokenSource?.Cancel();
  197. }
  198. /// <summary>
  199. /// Asynchronously creates a new network session.
  200. /// </summary>
  201. public static async Task<NetworkSession> CreateAsync(NetworkSessionType sessionType, int maxLocalGamers, int maxGamers, int privateGamerSlots, IDictionary<string, object> sessionProperties, CancellationToken cancellationToken = default)
  202. {
  203. if (maxLocalGamers < 1 || maxLocalGamers > 4)
  204. throw new ArgumentOutOfRangeException(nameof(maxLocalGamers));
  205. if (privateGamerSlots < 0 || privateGamerSlots > maxGamers)
  206. throw new ArgumentOutOfRangeException(nameof(privateGamerSlots));
  207. NetworkSession session = null;
  208. switch (sessionType)
  209. {
  210. case NetworkSessionType.Local:
  211. // Local session: in-memory only
  212. await Task.Delay(5, cancellationToken);
  213. session = new NetworkSession(sessionType, maxGamers, privateGamerSlots, true);
  214. session.sessionState = NetworkSessionState.Lobby;
  215. // Register in local session list for FindAsync
  216. LocalSessionRegistry.RegisterSession(session);
  217. break;
  218. case NetworkSessionType.SystemLink:
  219. // SystemLink: start UDP listener and broadcast session
  220. session = new NetworkSession(sessionType, maxGamers, privateGamerSlots, true);
  221. session.sessionState = NetworkSessionState.Lobby;
  222. _ = SystemLinkSessionManager.AdvertiseSessionAsync(session, cancellationToken); // Fire-and-forget
  223. break;
  224. default:
  225. // Not implemented
  226. throw new NotSupportedException($"SessionType {sessionType} not supported yet.");
  227. }
  228. return session;
  229. }
  230. /// <summary>
  231. /// Synchronous wrapper for CreateAsync (for XNA compatibility).
  232. /// </summary>
  233. public static NetworkSession Create(NetworkSessionType sessionType, int maxLocalGamers, int maxGamers, int privateGamerSlots, IDictionary<string, object> sessionProperties)
  234. {
  235. return CreateAsync(sessionType, maxLocalGamers, maxGamers, privateGamerSlots, sessionProperties).GetAwaiter().GetResult();
  236. }
  237. /// <summary>
  238. /// Asynchronously finds available network sessions.
  239. /// </summary>
  240. public static async Task<AvailableNetworkSessionCollection> FindAsync(NetworkSessionType sessionType, int maxLocalGamers, IDictionary<string, object> sessionProperties, CancellationToken cancellationToken = default)
  241. {
  242. switch (sessionType)
  243. {
  244. case NetworkSessionType.Local:
  245. await Task.Delay(5, cancellationToken);
  246. // Return sessions in local registry
  247. var localSessions = LocalSessionRegistry.FindSessions(maxLocalGamers).ToList();
  248. return new AvailableNetworkSessionCollection(localSessions);
  249. case NetworkSessionType.SystemLink:
  250. // Discover sessions via UDP broadcast
  251. var systemLinkSessions = (await SystemLinkSessionManager.DiscoverSessionsAsync(maxLocalGamers, cancellationToken)).ToList();
  252. return new AvailableNetworkSessionCollection(systemLinkSessions);
  253. default:
  254. throw new NotSupportedException($"SessionType {sessionType} not supported yet.");
  255. }
  256. }
  257. /// <summary>
  258. /// Synchronous wrapper for FindAsync (for XNA compatibility).
  259. /// </summary>
  260. public static AvailableNetworkSessionCollection Find(NetworkSessionType sessionType, int maxLocalGamers, IDictionary<string, object> sessionProperties)
  261. {
  262. return FindAsync(sessionType, maxLocalGamers, sessionProperties).GetAwaiter().GetResult();
  263. }
  264. /// <summary>
  265. /// Asynchronously joins an available network session.
  266. /// </summary>
  267. public static async Task<NetworkSession> JoinAsync(AvailableNetworkSession availableSession, CancellationToken cancellationToken = default)
  268. {
  269. switch (availableSession.SessionType)
  270. {
  271. case NetworkSessionType.Local:
  272. // Attach to local session
  273. var localSession = LocalSessionRegistry.GetSessionById(availableSession.SessionId);
  274. if (localSession == null)
  275. throw new NetworkSessionJoinException(NetworkSessionJoinError.SessionNotFound);
  276. // Add local gamer
  277. var newGamer = new LocalNetworkGamer(localSession, Guid.NewGuid().ToString(), false, SignedInGamer.Current?.Gamertag ?? "Player");
  278. localSession.AddGamer(newGamer);
  279. return localSession;
  280. case NetworkSessionType.SystemLink:
  281. // Connect to host via network
  282. var joinedSession = await SystemLinkSessionManager.JoinSessionAsync(availableSession, cancellationToken);
  283. return joinedSession;
  284. default:
  285. throw new NotSupportedException($"SessionType {availableSession.SessionType} not supported yet.");
  286. }
  287. }
  288. /// <summary>
  289. /// <summary>
  290. /// Creates a new network session synchronously with default properties.
  291. /// </summary>
  292. public static NetworkSession Create(NetworkSessionType sessionType, int maxLocalGamers, int maxGamers)
  293. {
  294. return CreateAsync(sessionType, maxLocalGamers, maxGamers, 0, new Dictionary<string, object>()).GetAwaiter().GetResult();
  295. }
  296. /// <summary>
  297. /// <summary>
  298. /// Finds available network sessions synchronously with default properties.
  299. /// </summary>
  300. public static AvailableNetworkSessionCollection Find(NetworkSessionType sessionType, int maxLocalGamers)
  301. {
  302. return FindAsync(sessionType, maxLocalGamers, new Dictionary<string, object>()).GetAwaiter().GetResult();
  303. }
  304. /// <summary>
  305. /// <summary>
  306. /// Joins an available network session synchronously.
  307. /// </summary>
  308. public static NetworkSession Join(AvailableNetworkSession availableSession)
  309. {
  310. return JoinAsync(availableSession).GetAwaiter().GetResult();
  311. }
  312. /// <summary>
  313. /// Updates the network session.
  314. /// </summary>
  315. public void Update()
  316. {
  317. if (disposed)
  318. return;
  319. // Process any pending network messages
  320. ProcessIncomingMessages();
  321. }
  322. /// <summary>
  323. /// Sends data to all gamers in the session.
  324. /// </summary>
  325. public void SendToAll(PacketWriter writer, SendDataOptions options)
  326. {
  327. SendToAll(writer, options, NetworkGamer.LocalGamer);
  328. }
  329. /// <summary>
  330. /// Sends data to all gamers except the specified sender.
  331. /// </summary>
  332. public void SendToAll(PacketWriter writer, SendDataOptions options, NetworkGamer sender)
  333. {
  334. if (writer == null)
  335. throw new ArgumentNullException(nameof(writer));
  336. byte[] data = writer.GetData();
  337. lock (lockObject)
  338. {
  339. foreach (var gamer in gamers)
  340. {
  341. if (gamer != sender && !gamer.IsLocal)
  342. {
  343. SendDataToGamer(gamer, data, options);
  344. }
  345. }
  346. }
  347. }
  348. /// <summary>
  349. /// Starts the game.
  350. /// </summary>
  351. public void StartGame()
  352. {
  353. if (sessionState == NetworkSessionState.Lobby)
  354. {
  355. sessionState = NetworkSessionState.Playing;
  356. OnGameStarted();
  357. }
  358. }
  359. /// <summary>
  360. /// Ends the game and returns to lobby.
  361. /// </summary>
  362. public void EndGame()
  363. {
  364. if (sessionState == NetworkSessionState.Playing)
  365. {
  366. sessionState = NetworkSessionState.Lobby;
  367. OnGameEnded();
  368. }
  369. }
  370. /// <summary>
  371. /// Notifies when a gamer's readiness changes.
  372. /// </summary>
  373. internal void NotifyReadinessChanged(NetworkGamer gamer)
  374. {
  375. // Send readiness update to other players
  376. if (IsHost)
  377. {
  378. var writer = new PacketWriter();
  379. writer.Write("ReadinessUpdate");
  380. writer.Write(gamer.Id);
  381. writer.Write(gamer.IsReady);
  382. SendToAll(writer, SendDataOptions.Reliable, gamer);
  383. }
  384. }
  385. /// <summary>
  386. /// Sends data to a specific gamer.
  387. /// </summary>
  388. internal void SendDataToGamer(NetworkGamer gamer, PacketWriter writer, SendDataOptions options)
  389. {
  390. SendDataToGamer(gamer, writer.GetData(), options);
  391. }
  392. internal void SendDataToGamer(NetworkGamer gamer, byte[] data, SendDataOptions options)
  393. {
  394. if (gamerEndpoints.TryGetValue(gamer.Id, out IPEndPoint endpoint))
  395. {
  396. try
  397. {
  398. networkTransport.Send(data, endpoint);
  399. }
  400. catch (Exception ex)
  401. {
  402. Debug.WriteLine($"Failed to send data to {gamer.Gamertag}: {ex.Message}");
  403. }
  404. }
  405. }
  406. private void AddGamer(NetworkGamer gamer)
  407. {
  408. lock (lockObject)
  409. {
  410. gamers.Add(gamer);
  411. }
  412. OnGamerJoined(gamer);
  413. }
  414. private void RemoveGamer(NetworkGamer gamer)
  415. {
  416. lock (lockObject)
  417. {
  418. gamers.Remove(gamer);
  419. gamerEndpoints.Remove(gamer.Id);
  420. }
  421. OnGamerLeft(gamer);
  422. }
  423. // Modern async receive loop for SystemLink
  424. private async Task ReceiveLoopAsync(CancellationToken cancellationToken)
  425. {
  426. using var udpClient = new UdpClient();
  427. udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 0)); // Use a dynamic port for tests
  428. while (!cancellationToken.IsCancellationRequested)
  429. {
  430. try
  431. {
  432. var result = await udpClient.ReceiveAsync();
  433. var data = result.Buffer;
  434. if (data.Length > 1)
  435. {
  436. var senderEndpoint = result.RemoteEndPoint;
  437. NetworkGamer senderGamer = null;
  438. lock (lockObject)
  439. {
  440. senderGamer = gamers.FirstOrDefault(g => gamerEndpoints.TryGetValue(g.Id, out var ep) && ep.Equals(senderEndpoint));
  441. }
  442. if (senderGamer != null)
  443. {
  444. senderGamer.EnqueueIncomingPacket(data, senderGamer);
  445. }
  446. var typeId = data[0];
  447. var reader = new PacketReader(data); // Use only the byte array
  448. var message = NetworkMessageRegistry.CreateMessage(typeId);
  449. message?.Deserialize(reader);
  450. OnMessageReceived(new MessageReceivedEventArgs(message, result.RemoteEndPoint));
  451. }
  452. }
  453. catch (ObjectDisposedException) { break; }
  454. catch (Exception ex)
  455. {
  456. Debug.WriteLine($"ReceiveLoop error: {ex.Message}");
  457. }
  458. }
  459. }
  460. private void OnMessageReceived(MessageReceivedEventArgs e)
  461. {
  462. // Raise the MessageReceived event
  463. var handler = MessageReceived;
  464. if (handler != null)
  465. handler(this, e);
  466. }
  467. private void OnGameStarted()
  468. {
  469. GameStarted?.Invoke(this, new GameStartedEventArgs());
  470. }
  471. private void OnGameEnded()
  472. {
  473. GameEnded?.Invoke(this, new GameEndedEventArgs());
  474. }
  475. private void OnGamerJoined(NetworkGamer gamer)
  476. {
  477. GamerJoined?.Invoke(this, new GamerJoinedEventArgs(gamer));
  478. }
  479. private void OnGamerLeft(NetworkGamer gamer)
  480. {
  481. GamerLeft?.Invoke(this, new GamerLeftEventArgs(gamer));
  482. }
  483. private void OnSessionEnded(NetworkSessionEndReason reason)
  484. {
  485. sessionState = NetworkSessionState.Ended;
  486. SessionEnded?.Invoke(this, new NetworkSessionEndedEventArgs(reason));
  487. }
  488. /// <summary>
  489. /// Disposes the network session.
  490. /// </summary>
  491. public void Dispose()
  492. {
  493. if (!disposed)
  494. {
  495. cancellationTokenSource?.Cancel();
  496. receiveTask?.Wait(1000); // Wait up to 1 second
  497. networkTransport?.Dispose();
  498. cancellationTokenSource?.Dispose();
  499. OnSessionEnded(NetworkSessionEndReason.ClientSignedOut);
  500. disposed = true;
  501. }
  502. }
  503. public async ValueTask DisposeAsync()
  504. {
  505. if (!disposed)
  506. {
  507. cancellationTokenSource?.Cancel();
  508. if (receiveTask != null)
  509. await receiveTask;
  510. if (networkTransport is IAsyncDisposable asyncTransport)
  511. await asyncTransport.DisposeAsync();
  512. else
  513. networkTransport?.Dispose();
  514. cancellationTokenSource?.Dispose();
  515. OnSessionEnded(NetworkSessionEndReason.ClientSignedOut);
  516. disposed = true;
  517. }
  518. }
  519. internal byte[] SerializeSessionPropertiesBinary()
  520. {
  521. using (var ms = new MemoryStream())
  522. using (var writer = new BinaryWriter(ms))
  523. {
  524. writer.Write(SessionProperties.Count);
  525. foreach (var kvp in SessionProperties)
  526. {
  527. writer.Write(kvp.Key);
  528. // Write type info and value
  529. if (kvp.Value is int i)
  530. {
  531. writer.Write((byte)1); // type marker
  532. writer.Write(i);
  533. }
  534. else if (kvp.Value is bool b)
  535. {
  536. writer.Write((byte)2);
  537. writer.Write(b);
  538. }
  539. else if (kvp.Value is string s)
  540. {
  541. writer.Write((byte)3);
  542. writer.Write(s ?? "");
  543. }
  544. // Add more types as needed
  545. else
  546. {
  547. writer.Write((byte)255); // unknown type
  548. writer.Write(kvp.Value?.ToString() ?? "");
  549. }
  550. }
  551. return ms.ToArray();
  552. }
  553. }
  554. internal void DeserializeSessionPropertiesBinary(byte[] data)
  555. {
  556. using (var ms = new MemoryStream(data))
  557. using (var reader = new BinaryReader(ms))
  558. {
  559. SessionProperties.Clear();
  560. int count = reader.ReadInt32();
  561. for (int i = 0; i < count; i++)
  562. {
  563. string key = reader.ReadString();
  564. byte type = reader.ReadByte();
  565. object value = null;
  566. switch (type)
  567. {
  568. case 1: value = reader.ReadInt32(); break;
  569. case 2: value = reader.ReadBoolean(); break;
  570. case 3: value = reader.ReadString(); break;
  571. default: value = reader.ReadString(); break;
  572. }
  573. SessionProperties[key] = value;
  574. }
  575. }
  576. }
  577. private void BroadcastSessionProperties()
  578. {
  579. var writer = new PacketWriter();
  580. writer.Write("SessionPropertiesUpdate");
  581. writer.Write(SerializeSessionPropertiesBinary());
  582. SendToAll(writer, SendDataOptions.Reliable);
  583. }
  584. private void ProcessIncomingMessages()
  585. {
  586. foreach (var gamer in gamers)
  587. {
  588. while (gamer.IsDataAvailable)
  589. {
  590. var reader = new PacketReader();
  591. gamer.ReceiveData(out reader, out var sender);
  592. var messageType = reader.ReadString();
  593. if (messageType == "SessionPropertiesUpdate")
  594. {
  595. var propertiesData = reader.ReadBytes();
  596. DeserializeSessionPropertiesBinary(propertiesData);
  597. }
  598. }
  599. }
  600. }
  601. }
  602. }