SystemLinkSessionManager.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Net;
  4. using System.Net.Sockets;
  5. using System.Text;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. namespace Microsoft.Xna.Framework.Net
  9. {
  10. internal static class SystemLinkSessionManager
  11. {
  12. private const int BroadcastPort = 31337;
  13. private const int GamePort = 31338; // Port for gameplay UDP traffic
  14. private static readonly List<AvailableNetworkSession> discoveredSessions = new List<AvailableNetworkSession>();
  15. public static Task AdvertiseSessionAsync(NetworkSession session, CancellationToken cancellationToken)
  16. {
  17. // Periodically broadcast session info on LAN until session is full or ended
  18. return Task.Run(async () =>
  19. {
  20. using (var udpClient = new UdpClient())
  21. {
  22. udpClient.EnableBroadcast = true;
  23. var broadcastEndpoint = new IPEndPoint(IPAddress.Broadcast, BroadcastPort);
  24. var localhostEndpoint = new IPEndPoint(IPAddress.Loopback, BroadcastPort);
  25. Console.WriteLine($"[BROADCAST] Starting session advertisement on port {BroadcastPort}");
  26. int broadcastCount = 0;
  27. while (!cancellationToken.IsCancellationRequested && session.AllGamers.Count < session.MaxGamers && session.sessionState != NetworkSessionState.Ended)
  28. {
  29. var propertiesBytes = session.SerializeSessionPropertiesBinary();
  30. // Include gameplay port in the header so joiners know where to send join requests
  31. var header = $"SESSION:{session.sessionId}:{session.MaxGamers}:{session.PrivateGamerSlots}:{session.Host?.Gamertag ?? "Host"}:{GamePort}:";
  32. var headerBytes = Encoding.UTF8.GetBytes(header);
  33. var message = new byte[headerBytes.Length + propertiesBytes.Length];
  34. Buffer.BlockCopy(headerBytes, 0, message, 0, headerBytes.Length);
  35. Buffer.BlockCopy(propertiesBytes, 0, message, headerBytes.Length, propertiesBytes.Length);
  36. // Send to broadcast address for LAN discovery
  37. await udpClient.SendAsync(message, message.Length, broadcastEndpoint);
  38. // ALSO send to localhost for same-machine testing
  39. try
  40. {
  41. await udpClient.SendAsync(message, message.Length, localhostEndpoint);
  42. Console.WriteLine($"[BROADCAST] Also sent to localhost (127.0.0.1:{BroadcastPort})");
  43. }
  44. catch (SocketException ex)
  45. {
  46. Console.WriteLine($"[BROADCAST] Localhost send failed: {ex.Message}");
  47. }
  48. broadcastCount++;
  49. Console.WriteLine($"[BROADCAST] Sent broadcast #{broadcastCount} - SessionID: {session.sessionId}, Gamers: {session.AllGamers.Count}/{session.MaxGamers}");
  50. await Task.Delay(750, cancellationToken); // Broadcast every 750ms for faster discovery
  51. }
  52. Console.WriteLine($"[BROADCAST] Stopped broadcasting. Reason: Cancelled={cancellationToken.IsCancellationRequested}, Full={session.AllGamers.Count >= session.MaxGamers}, Ended={session.sessionState == NetworkSessionState.Ended}");
  53. }
  54. }, cancellationToken);
  55. }
  56. public static async Task<IEnumerable<AvailableNetworkSession>> DiscoverSessionsAsync(int maxLocalGamers, CancellationToken cancellationToken)
  57. {
  58. Console.WriteLine($"[DISCOVERY] Starting session discovery on port {BroadcastPort}");
  59. // Use dictionary to deduplicate sessions by ID (in case we receive multiple broadcasts from same host)
  60. var sessionsDict = new Dictionary<string, AvailableNetworkSession>();
  61. try
  62. {
  63. using (var udpClient = new UdpClient())
  64. {
  65. // Enable port reuse for multiple instances on same machine
  66. udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
  67. udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, BroadcastPort));
  68. Console.WriteLine($"[DISCOVERY] Successfully bound to port {BroadcastPort}");
  69. udpClient.EnableBroadcast = true;
  70. // DON'T set ReceiveTimeout - it interferes with ReceiveAsync()
  71. // Phase 1: Listen for 1.5 seconds to catch at least 1 broadcast cycle (hosts broadcast every 1 second)
  72. // Reduced from 2.5s for faster discovery while still being reliable
  73. var startTime = DateTime.UtcNow;
  74. var endTime = startTime.AddSeconds(1.5);
  75. int receiveAttempts = 0;
  76. int packetsReceived = 0;
  77. while (DateTime.UtcNow < endTime && !cancellationToken.IsCancellationRequested)
  78. {
  79. try
  80. {
  81. receiveAttempts++;
  82. // Try to receive with reduced timeout for faster response
  83. var receiveTask = udpClient.ReceiveAsync();
  84. var timeoutTask = Task.Delay(100, cancellationToken);
  85. var completedTask = await Task.WhenAny(receiveTask, timeoutTask);
  86. if (completedTask == receiveTask)
  87. {
  88. var result = await receiveTask;
  89. var buffer = result.Buffer;
  90. packetsReceived++;
  91. Console.WriteLine($"[DISCOVERY] Received packet #{packetsReceived} from {result.RemoteEndPoint} ({buffer.Length} bytes)");
  92. // Find the header delimiter (the last colon of the header)
  93. int headerEnd = 0;
  94. int colonCount = 0;
  95. for (int i = 0; i < buffer.Length; i++)
  96. {
  97. if (buffer[i] == (byte)':')
  98. {
  99. colonCount++;
  100. if (colonCount == 6)
  101. {
  102. headerEnd = i + 1; // header ends after 6th colon (includes game port)
  103. break;
  104. }
  105. }
  106. }
  107. if (colonCount == 6)
  108. {
  109. var headerString = Encoding.UTF8.GetString(buffer, 0, headerEnd);
  110. Console.WriteLine($"[DISCOVERY] Parsed header: {headerString}");
  111. var parts = headerString.Split(':');
  112. var sessionId = parts[1];
  113. var maxGamers = int.Parse(parts[2]);
  114. var privateSlots = int.Parse(parts[3]);
  115. var hostGamertag = parts[4];
  116. var gamePort = int.Parse(parts[5]);
  117. // Binary session properties start after headerEnd
  118. var propertiesBytes = new byte[buffer.Length - headerEnd];
  119. Buffer.BlockCopy(buffer, headerEnd, propertiesBytes, 0, propertiesBytes.Length);
  120. var dummySession = new NetworkSession(NetworkSessionType.SystemLink, maxGamers, privateSlots, false, sessionId);
  121. dummySession.DeserializeSessionPropertiesBinary(propertiesBytes);
  122. var sessionProperties = dummySession.SessionProperties as Dictionary<string, object>;
  123. var hostEndpoint = new IPEndPoint(result.RemoteEndPoint.Address, gamePort);
  124. // Add to dictionary (will replace if we get multiple broadcasts from same session)
  125. sessionsDict[sessionId] = new AvailableNetworkSession(
  126. sessionName: "SystemLinkSession",
  127. hostGamertag: hostGamertag,
  128. currentGamerCount: 1,
  129. openPublicGamerSlots: maxGamers - 1,
  130. openPrivateGamerSlots: privateSlots,
  131. sessionType: NetworkSessionType.SystemLink,
  132. sessionProperties: sessionProperties,
  133. sessionId: sessionId,
  134. hostEndpoint: hostEndpoint);
  135. Console.WriteLine($"[DISCOVERY] Added session: {hostGamertag} ({sessionId})");
  136. }
  137. else
  138. {
  139. Console.WriteLine($"[DISCOVERY] Invalid packet - expected 6 colons, found {colonCount}");
  140. }
  141. }
  142. // If timeout occurs, continue listening until endTime
  143. }
  144. catch (SocketException ex)
  145. {
  146. Console.WriteLine($"[DISCOVERY] SocketException: {ex.Message}");
  147. // Socket timeout or other network error - continue listening
  148. }
  149. catch (ObjectDisposedException)
  150. {
  151. Console.WriteLine($"[DISCOVERY] Socket disposed");
  152. // Socket was disposed (shouldn't happen, but handle gracefully)
  153. break;
  154. }
  155. catch (Exception ex)
  156. {
  157. Console.WriteLine($"[DISCOVERY] Unexpected error: {ex.GetType().Name} - {ex.Message}");
  158. }
  159. }
  160. var elapsed = DateTime.UtcNow - startTime;
  161. Console.WriteLine($"[DISCOVERY] Completed after {elapsed.TotalSeconds:F2}s. Attempts: {receiveAttempts}, Packets: {packetsReceived}, Sessions: {sessionsDict.Count}");
  162. }
  163. }
  164. catch (Exception ex)
  165. {
  166. Console.WriteLine($"[DISCOVERY] Fatal error: {ex.GetType().Name} - {ex.Message}\n{ex.StackTrace}");
  167. }
  168. return sessionsDict.Values;
  169. }
  170. public static async Task<NetworkSession> JoinSessionAsync(AvailableNetworkSession availableSession, CancellationToken cancellationToken)
  171. {
  172. // Phase 1: Reliable join with timeout and retry
  173. const int MAX_RETRIES = 3;
  174. const int TIMEOUT_MS = 300; // Faster timeout for LAN (reduced from 500ms)
  175. Console.WriteLine($"[JOIN] Starting join process for session {availableSession.SessionId}");
  176. // Create client session in Joining state
  177. var session = new NetworkSession(NetworkSessionType.SystemLink,
  178. availableSession.OpenPublicGamerSlots + availableSession.CurrentGamerCount,
  179. availableSession.OpenPrivateGamerSlots,
  180. false,
  181. availableSession.SessionId);
  182. session.sessionState = NetworkSessionState.Joining; // Phase 1: Use new Joining state
  183. // Copy session properties from AvailableNetworkSession to NetworkSession
  184. foreach (var kvp in availableSession.SessionProperties)
  185. session.SessionProperties[kvp.Key] = kvp.Value;
  186. // Bind client transport on join so it can receive packets
  187. if (!session.NetworkTransport.IsBound)
  188. {
  189. session.NetworkTransport.Bind();
  190. }
  191. // Create a synthetic remote host gamer and record endpoint so SendToAll can reach host
  192. if (availableSession.HostEndpoint == null)
  193. {
  194. throw new NetworkSessionJoinException("Host endpoint is null", NetworkSessionJoinError.SessionNotFound);
  195. }
  196. var hostGamer = new NetworkGamer(session, Guid.NewGuid().ToString(), isLocal: false, isHost: true, gamertag: availableSession.HostGamertag);
  197. session.GetType().GetMethod("AddGamer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
  198. ?.Invoke(session, new object[] { hostGamer });
  199. session.RegisterGamerEndpoint(hostGamer, availableSession.HostEndpoint);
  200. // Phase 1: Send join request with retry logic
  201. for (int attempt = 0; attempt < MAX_RETRIES; attempt++)
  202. {
  203. Console.WriteLine($"[JOIN] Sending join request (attempt {attempt + 1}/{MAX_RETRIES})");
  204. // CRITICAL: Use the session's own local gamer, not the static NetworkGamer.LocalGamer
  205. // The static property can be stale when multiple sessions exist (e.g., testing on same machine)
  206. var localGamer = session.LocalGamers.FirstOrDefault();
  207. if (localGamer == null)
  208. throw new InvalidOperationException("No local gamer found in session");
  209. Console.WriteLine($"[JOIN] Sending as gamer: {localGamer.Gamertag} (ID: {localGamer.Id})");
  210. var joinRequest = new JoinRequestMessage
  211. {
  212. GamerId = localGamer.Id,
  213. Gamertag = localGamer.Gamertag,
  214. ProtocolVersion = JoinRequestMessage.CURRENT_PROTOCOL_VERSION
  215. };
  216. var writer = new PacketWriter();
  217. joinRequest.Serialize(writer);
  218. session.NetworkTransport.Send(writer.GetData(), availableSession.HostEndpoint);
  219. // Wait for response (NetworkSession.OnMessageReceived will update state to Lobby if accepted)
  220. var waitStart = DateTime.UtcNow;
  221. while ((DateTime.UtcNow - waitStart).TotalMilliseconds < TIMEOUT_MS)
  222. {
  223. if (session.sessionState == NetworkSessionState.Lobby)
  224. {
  225. Console.WriteLine($"[JOIN] Successfully joined session after {attempt + 1} attempt(s)");
  226. // Phase 1: Start connection monitoring for client
  227. session.GetType().GetField("connectionMonitor", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
  228. ?.GetValue(session)
  229. ?.GetType().GetMethod("StartMonitoring")
  230. ?.Invoke(session.GetType().GetField("connectionMonitor", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(session), new object[] { session });
  231. return session;
  232. }
  233. // Check if we received rejection (would still be in Joining state but we can check for it)
  234. // For now, just wait
  235. await Task.Delay(50, cancellationToken);
  236. }
  237. Console.WriteLine($"[JOIN] Attempt {attempt + 1} timed out");
  238. }
  239. // After MAX_RETRIES attempts, give up
  240. Console.WriteLine($"[JOIN] Failed to join session after {MAX_RETRIES} attempts");
  241. session.Dispose();
  242. throw new NetworkSessionJoinException(
  243. $"Failed to join session after {MAX_RETRIES} attempts. Host may be unreachable or session is full.",
  244. NetworkSessionJoinError.Timeout);
  245. }
  246. }
  247. }