Browse Source

Try to make Find Sessions more reliable in the networking samples.

Dominique Louis 1 month ago
parent
commit
3c18749a68

+ 2 - 1
MonoGame.Xna.Framework.Net/Net/NetworkSession.cs

@@ -292,7 +292,8 @@ namespace Microsoft.Xna.Framework.Net
                     // SystemLink: start UDP listener and broadcast session
                     session = new NetworkSession(sessionType, maxGamers, privateGamerSlots, true);
                     session.sessionState = NetworkSessionState.Lobby;
-                    _ = SystemLinkSessionManager.AdvertiseSessionAsync(session, cancellationToken); // Fire-and-forget
+                    // Use the session's own cancellation token, not the CreateAsync parameter
+                    _ = SystemLinkSessionManager.AdvertiseSessionAsync(session, session.cancellationTokenSource.Token); // Fire-and-forget
                     break;
                 default:
                     // Not implemented

+ 142 - 50
MonoGame.Xna.Framework.Net/Net/SystemLinkSessionManager.cs

@@ -22,7 +22,12 @@ namespace Microsoft.Xna.Framework.Net
                 using (var udpClient = new UdpClient())
                 {
                     udpClient.EnableBroadcast = true;
-                    var endpoint = new IPEndPoint(IPAddress.Broadcast, BroadcastPort);
+                    var broadcastEndpoint = new IPEndPoint(IPAddress.Broadcast, BroadcastPort);
+                    var localhostEndpoint = new IPEndPoint(IPAddress.Loopback, BroadcastPort);
+
+                    Console.WriteLine($"[BROADCAST] Starting session advertisement on port {BroadcastPort}");
+                    int broadcastCount = 0;
+
                     while (!cancellationToken.IsCancellationRequested && session.AllGamers.Count < session.MaxGamers && session.sessionState != NetworkSessionState.Ended)
                     {
                         var propertiesBytes = session.SerializeSessionPropertiesBinary();
@@ -32,75 +37,162 @@ namespace Microsoft.Xna.Framework.Net
                         var message = new byte[headerBytes.Length + propertiesBytes.Length];
                         Buffer.BlockCopy(headerBytes, 0, message, 0, headerBytes.Length);
                         Buffer.BlockCopy(propertiesBytes, 0, message, headerBytes.Length, propertiesBytes.Length);
-                        await udpClient.SendAsync(message, message.Length, endpoint);
+
+                        // Send to broadcast address for LAN discovery
+                        await udpClient.SendAsync(message, message.Length, broadcastEndpoint);
+
+                        // ALSO send to localhost for same-machine testing
+                        try
+                        {
+                            await udpClient.SendAsync(message, message.Length, localhostEndpoint);
+                            Console.WriteLine($"[BROADCAST] Also sent to localhost (127.0.0.1:{BroadcastPort})");
+                        }
+                        catch (SocketException ex)
+                        {
+                            Console.WriteLine($"[BROADCAST] Localhost send failed: {ex.Message}");
+                        }
+
+                        broadcastCount++;
+                        Console.WriteLine($"[BROADCAST] Sent broadcast #{broadcastCount} - SessionID: {session.sessionId}, Gamers: {session.AllGamers.Count}/{session.MaxGamers}");
+
                         await Task.Delay(1000, cancellationToken); // Broadcast every second
                     }
+
+                    Console.WriteLine($"[BROADCAST] Stopped broadcasting. Reason: Cancelled={cancellationToken.IsCancellationRequested}, Full={session.AllGamers.Count >= session.MaxGamers}, Ended={session.sessionState == NetworkSessionState.Ended}");
                 }
             }, cancellationToken);
         }
 
         public static async Task<IEnumerable<AvailableNetworkSession>> DiscoverSessionsAsync(int maxLocalGamers, CancellationToken cancellationToken)
         {
-            var sessions = new List<AvailableNetworkSession>();
-            using (var udpClient = new UdpClient(BroadcastPort))
+            Console.WriteLine($"[DISCOVERY] Starting session discovery on port {BroadcastPort}");
+
+            // Use dictionary to deduplicate sessions by ID (in case we receive multiple broadcasts from same host)
+            var sessionsDict = new Dictionary<string, AvailableNetworkSession>();
+
+            try
             {
-                udpClient.EnableBroadcast = true;
-                var receiveTask = udpClient.ReceiveAsync();
-                var completedTask = await Task.WhenAny(receiveTask, Task.Delay(100, cancellationToken));
-                if (completedTask == receiveTask)
+                using (var udpClient = new UdpClient())
                 {
-                    var result = receiveTask.Result;
-                    var buffer = result.Buffer;
+                    // Enable port reuse for multiple instances on same machine
+                    udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
+                    udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, BroadcastPort));
+
+                    Console.WriteLine($"[DISCOVERY] Successfully bound to port {BroadcastPort}");
 
-                    // Find the header delimiter (the last colon of the header)
-                    int headerEnd = 0;
-                    int colonCount = 0;
-                    for (int i = 0; i < buffer.Length; i++)
+                    udpClient.EnableBroadcast = true;
+                    // DON'T set ReceiveTimeout - it interferes with ReceiveAsync()
+
+                    // Listen for 2 seconds to catch at least 2 broadcast cycles (hosts broadcast every 1 second)
+                    var startTime = DateTime.UtcNow;
+                    var endTime = startTime.AddSeconds(2);
+                    int receiveAttempts = 0;
+                    int packetsReceived = 0;
+
+                    while (DateTime.UtcNow < endTime && !cancellationToken.IsCancellationRequested)
                     {
-                        if (buffer[i] == (byte)':')
+                        try
                         {
-                            colonCount++;
-                            if (colonCount == 6)
+                            receiveAttempts++;
+                            // Try to receive with timeout
+                            var receiveTask = udpClient.ReceiveAsync();
+                            var timeoutTask = Task.Delay(150, cancellationToken);
+                            var completedTask = await Task.WhenAny(receiveTask, timeoutTask);
+
+                            if (completedTask == receiveTask)
                             {
-                                headerEnd = i + 1; // header ends after 6th colon (includes game port)
-                                break;
+                                var result = await receiveTask;
+                                var buffer = result.Buffer;
+                                packetsReceived++;
+
+                                Console.WriteLine($"[DISCOVERY] Received packet #{packetsReceived} from {result.RemoteEndPoint} ({buffer.Length} bytes)");
+
+                                // Find the header delimiter (the last colon of the header)
+                                int headerEnd = 0;
+                                int colonCount = 0;
+                                for (int i = 0; i < buffer.Length; i++)
+                                {
+                                    if (buffer[i] == (byte)':')
+                                    {
+                                        colonCount++;
+                                        if (colonCount == 6)
+                                        {
+                                            headerEnd = i + 1; // header ends after 6th colon (includes game port)
+                                            break;
+                                        }
+                                    }
+                                }
+
+                                if (colonCount == 6)
+                                {
+                                    var headerString = Encoding.UTF8.GetString(buffer, 0, headerEnd);
+                                    Console.WriteLine($"[DISCOVERY] Parsed header: {headerString}");
+
+                                    var parts = headerString.Split(':');
+                                    var sessionId = parts[1];
+                                    var maxGamers = int.Parse(parts[2]);
+                                    var privateSlots = int.Parse(parts[3]);
+                                    var hostGamertag = parts[4];
+                                    var gamePort = int.Parse(parts[5]);
+
+                                    // Binary session properties start after headerEnd
+                                    var propertiesBytes = new byte[buffer.Length - headerEnd];
+                                    Buffer.BlockCopy(buffer, headerEnd, propertiesBytes, 0, propertiesBytes.Length);
+
+                                    var dummySession = new NetworkSession(NetworkSessionType.SystemLink, maxGamers, privateSlots, false, sessionId);
+                                    dummySession.DeserializeSessionPropertiesBinary(propertiesBytes);
+                                    var sessionProperties = dummySession.SessionProperties as Dictionary<string, object>;
+
+                                    var hostEndpoint = new IPEndPoint(result.RemoteEndPoint.Address, gamePort);
+
+                                    // Add to dictionary (will replace if we get multiple broadcasts from same session)
+                                    sessionsDict[sessionId] = new AvailableNetworkSession(
+                                        sessionName: "SystemLinkSession",
+                                        hostGamertag: hostGamertag,
+                                        currentGamerCount: 1,
+                                        openPublicGamerSlots: maxGamers - 1,
+                                        openPrivateGamerSlots: privateSlots,
+                                        sessionType: NetworkSessionType.SystemLink,
+                                        sessionProperties: sessionProperties,
+                                        sessionId: sessionId,
+                                        hostEndpoint: hostEndpoint);
+
+                                    Console.WriteLine($"[DISCOVERY] Added session: {hostGamertag} ({sessionId})");
+                                }
+                                else
+                                {
+                                    Console.WriteLine($"[DISCOVERY] Invalid packet - expected 6 colons, found {colonCount}");
+                                }
                             }
+                            // If timeout occurs, continue listening until endTime
+                        }
+                        catch (SocketException ex)
+                        {
+                            Console.WriteLine($"[DISCOVERY] SocketException: {ex.Message}");
+                            // Socket timeout or other network error - continue listening
+                        }
+                        catch (ObjectDisposedException)
+                        {
+                            Console.WriteLine($"[DISCOVERY] Socket disposed");
+                            // Socket was disposed (shouldn't happen, but handle gracefully)
+                            break;
+                        }
+                        catch (Exception ex)
+                        {
+                            Console.WriteLine($"[DISCOVERY] Unexpected error: {ex.GetType().Name} - {ex.Message}");
                         }
                     }
 
-                    if (colonCount == 6)
-                    {
-                        var headerString = Encoding.UTF8.GetString(buffer, 0, headerEnd);
-                        var parts = headerString.Split(':');
-                        var sessionId = parts[1];
-                        var maxGamers = int.Parse(parts[2]);
-                        var privateSlots = int.Parse(parts[3]);
-                        var hostGamertag = parts[4];
-                        var gamePort = int.Parse(parts[5]);
-
-                        // Binary session properties start after headerEnd
-                        var propertiesBytes = new byte[buffer.Length - headerEnd];
-                        Buffer.BlockCopy(buffer, headerEnd, propertiesBytes, 0, propertiesBytes.Length);
-
-                        var dummySession = new NetworkSession(NetworkSessionType.SystemLink, maxGamers, privateSlots, false, sessionId);
-                        dummySession.DeserializeSessionPropertiesBinary(propertiesBytes);
-                        var sessionProperties = dummySession.SessionProperties as Dictionary<string, object>;
-
-                        var hostEndpoint = new IPEndPoint(result.RemoteEndPoint.Address, gamePort);
-                        sessions.Add(new AvailableNetworkSession(
-                            sessionName: "SystemLinkSession",
-                            hostGamertag: hostGamertag,
-                            currentGamerCount: 1,
-                            openPublicGamerSlots: maxGamers - 1,
-                            openPrivateGamerSlots: privateSlots,
-                            sessionType: NetworkSessionType.SystemLink,
-                            sessionProperties: sessionProperties,
-                            sessionId: sessionId,
-                            hostEndpoint: hostEndpoint));
-                    }
+                    var elapsed = DateTime.UtcNow - startTime;
+                    Console.WriteLine($"[DISCOVERY] Completed after {elapsed.TotalSeconds:F2}s. Attempts: {receiveAttempts}, Packets: {packetsReceived}, Sessions: {sessionsDict.Count}");
                 }
             }
-            return sessions;
+            catch (Exception ex)
+            {
+                Console.WriteLine($"[DISCOVERY] Fatal error: {ex.GetType().Name} - {ex.Message}\n{ex.StackTrace}");
+            }
+
+            return sessionsDict.Values;
         }
 
         public static async Task<NetworkSession> JoinSessionAsync(AvailableNetworkSession availableSession, CancellationToken cancellationToken)

+ 1 - 1
NetworkStateManagement/Core/Resources.resx

@@ -245,6 +245,6 @@ B button, Esc = cancel</value>
     <value>Single Player</value>
   </data>
   <data name="SystemLink" xml:space="preserve">
-    <value>System Link</value>
+    <value>LAN Session</value>
   </data>
 </root>

+ 9 - 4
Peer2Peer/Core/PeerToPeerGame.cs

@@ -174,14 +174,15 @@ namespace PeerToPeer
 		/// <summary>
 		/// Joins an existing network session.
 		/// </summary>
-		void JoinSession(NetworkSessionType type)
+		async void JoinSession(NetworkSessionType type)
 		{
 			DrawMessage($"Joining {type} session...");
 
 			try
 			{
-				// Search for sessions.
-				using (AvailableNetworkSessionCollection availableSessions = NetworkSession.Find(type, maxLocalGamers, null))
+				// Search for sessions asynchronously with cancellation support
+				var cancellationTokenSource = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(5)); // 5 second timeout
+				using (AvailableNetworkSessionCollection availableSessions = await NetworkSession.FindAsync(type, maxLocalGamers, null, cancellationTokenSource.Token))
 				{
 					if (availableSessions.Count == 0)
 					{
@@ -190,11 +191,15 @@ namespace PeerToPeer
 					}
 
 					// Join the first session we found.
-					networkSession = NetworkSession.Join(availableSessions[0]);
+					networkSession = await NetworkSession.JoinAsync(availableSessions[0], cancellationTokenSource.Token);
 
 					HookSessionEvents();
 				}
 			}
+			catch (System.OperationCanceledException)
+			{
+				errorMessage = "Session discovery timed out after 5 seconds.";
+			}
 			catch (Exception e)
 			{
 				errorMessage = e.Message;