Переглянути джерело

Changes to get us closer to LAN Peer to Peer working.

Dominique Louis 3 тижнів тому
батько
коміт
9628f7070c

+ 9 - 1
MonoGame.Xna.Framework.Net/Net/AvailableNetworkSession.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Net;
 using System.Threading.Tasks;
 
 namespace Microsoft.Xna.Framework.Net
@@ -17,7 +18,8 @@ namespace Microsoft.Xna.Framework.Net
             int openPrivateGamerSlots,
             NetworkSessionType sessionType,
             IDictionary<string, object> sessionProperties,
-            string sessionId)
+            string sessionId,
+            IPEndPoint hostEndpoint = null)
         {
             SessionName = sessionName;
             HostGamertag = hostGamertag;
@@ -27,6 +29,7 @@ namespace Microsoft.Xna.Framework.Net
             SessionType = sessionType;
             SessionProperties = new Dictionary<string, object>(sessionProperties);
             SessionId = sessionId;
+            HostEndpoint = hostEndpoint;
         }
         /// <summary>
         /// Gets the unique session ID.
@@ -72,5 +75,10 @@ namespace Microsoft.Xna.Framework.Net
         /// Gets the quality of service information.
         /// </summary>
         public QualityOfService QualityOfService { get; internal set; } = new QualityOfService();
+
+    /// <summary>
+    /// Gets the host's network endpoint for SystemLink (LAN) sessions.
+    /// </summary>
+    public IPEndPoint HostEndpoint { get; }
     }
 }

+ 28 - 0
MonoGame.Xna.Framework.Net/Net/JoinAcceptedMessage.cs

@@ -0,0 +1,28 @@
+using System;
+using Microsoft.Xna.Framework.Net;
+
+namespace Microsoft.Xna.Framework.Net
+{
+    public class JoinAcceptedMessage : INetworkMessage
+    {
+        public byte MessageType => 3;
+        public string SessionId { get; set; }
+        public string HostGamerId { get; set; }
+        public string HostGamertag { get; set; }
+
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(MessageType);
+            writer.Write(SessionId);
+            writer.Write(HostGamerId);
+            writer.Write(HostGamertag);
+        }
+
+        public void Deserialize(PacketReader reader)
+        {
+            SessionId = reader.ReadString();
+            HostGamerId = reader.ReadString();
+            HostGamertag = reader.ReadString();
+        }
+    }
+}

+ 25 - 0
MonoGame.Xna.Framework.Net/Net/JoinRequestMessage.cs

@@ -0,0 +1,25 @@
+using System;
+using Microsoft.Xna.Framework.Net;
+
+namespace Microsoft.Xna.Framework.Net
+{
+    public class JoinRequestMessage : INetworkMessage
+    {
+        public byte MessageType => 2;
+        public string GamerId { get; set; }
+        public string Gamertag { get; set; }
+
+        public void Serialize(PacketWriter writer)
+        {
+            writer.Write(MessageType);
+            writer.Write(GamerId);
+            writer.Write(Gamertag);
+        }
+
+        public void Deserialize(PacketReader reader)
+        {
+            GamerId = reader.ReadString();
+            Gamertag = reader.ReadString();
+        }
+    }
+}

+ 6 - 0
MonoGame.Xna.Framework.Net/Net/NetworkMessageRegistry.cs

@@ -29,5 +29,11 @@ namespace Microsoft.Xna.Framework.Net
         {
             return registry.TryGetValue(typeId, out var ctor) ? ctor() : null;
         }
+
+        static NetworkMessageRegistry()
+        {
+            Register<JoinRequestMessage>(2);
+            Register<JoinAcceptedMessage>(3);
+        }
     }
 }

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

@@ -108,6 +108,19 @@ namespace Microsoft.Xna.Framework.Net
             // Start receive loop for SystemLink sessions
             if (sessionType == NetworkSessionType.SystemLink)
             {
+                // Bind immediately so our endpoint is stable and reachable
+                if (!networkTransport.IsBound)
+                {
+                    try
+                    {
+                        // Try binding to the well-known game port; fall back to ephemeral if taken
+                        (networkTransport as UdpTransport)?.Bind(31338);
+                    }
+                    catch
+                    {
+                        try { networkTransport.Bind(); } catch { }
+                    }
+                }
                 receiveTask = Task.Run(() => ReceiveLoopAsync(cancellationTokenSource.Token));
             }
         }
@@ -520,13 +533,25 @@ namespace Microsoft.Xna.Framework.Net
             }
         }
 
+        /// <summary>
+        /// Internally associates a remote gamer with an endpoint (used by SystemLink join/handshake).
+        /// </summary>
+        internal void RegisterGamerEndpoint(NetworkGamer gamer, IPEndPoint endpoint)
+        {
+            if (gamer == null || endpoint == null) return;
+            lock (lockObject)
+            {
+                gamerEndpoints[gamer.Id] = endpoint;
+            }
+        }
+
         public void AddLocalGamer(SignedInGamer gamer)
         {
             throw new NotImplementedException();
         }
 
 
-        private void AddGamer(NetworkGamer gamer)
+    private void AddGamer(NetworkGamer gamer)
         {
             lock (lockObject)
             {
@@ -589,6 +614,40 @@ namespace Microsoft.Xna.Framework.Net
 
         private void OnMessageReceived(MessageReceivedEventArgs e)
         {
+            if (e.Message is JoinRequestMessage joinRequest)
+            {
+                // Handle join request
+                var newGamer = new NetworkGamer(this, joinRequest.GamerId, isLocal: false, isHost: false, gamertag: joinRequest.Gamertag);
+                AddGamer(newGamer);
+                RegisterGamerEndpoint(newGamer, e.RemoteEndPoint);
+
+                // Send JoinAcceptedMessage back to the sender
+                var joinAccepted = new JoinAcceptedMessage
+                {
+                    SessionId = sessionId,
+                    HostGamerId = Host.Id,
+                    HostGamertag = Host.Gamertag
+                };
+                var writer = new PacketWriter();
+                joinAccepted.Serialize(writer);
+                networkTransport.Send(writer.GetData(), e.RemoteEndPoint);
+            }
+            else if (e.Message is PlayerMoveMessage moveMessage)
+            {
+                // Handle player movement
+                var gamer = gamers.FirstOrDefault(g => g.Id == moveMessage.PlayerId.ToString());
+                if (gamer != null)
+                {
+                    // Update gamer position (mock implementation)
+                    Debug.WriteLine($"Player {gamer.Gamertag} moved to ({moveMessage.X}, {moveMessage.Y}, {moveMessage.Z})");
+
+                    // Broadcast movement to all other gamers
+                    var writer = new PacketWriter();
+                    moveMessage.Serialize(writer);
+                    SendToAll(writer, SendDataOptions.Reliable, gamer);
+                }
+            }
+
             // Raise the MessageReceived event
             var handler = MessageReceived;
             if (handler != null)

+ 33 - 11
MonoGame.Xna.Framework.Net/Net/SystemLinkSessionManager.cs

@@ -10,7 +10,8 @@ namespace Microsoft.Xna.Framework.Net
 {
     internal static class SystemLinkSessionManager
     {
-        private const int BroadcastPort = 31337;
+    private const int BroadcastPort = 31337;
+    private const int GamePort = 31338; // Port for gameplay UDP traffic
         private static readonly List<AvailableNetworkSession> discoveredSessions = new List<AvailableNetworkSession>();
 
         public static Task AdvertiseSessionAsync(NetworkSession session, CancellationToken cancellationToken)
@@ -18,13 +19,14 @@ namespace Microsoft.Xna.Framework.Net
             // Periodically broadcast session info on LAN until session is full or ended
             return Task.Run(async () =>
             {
-                using (var udpClient = new UdpClient())
+        using (var udpClient = new UdpClient())
                 {
                     var endpoint = new IPEndPoint(IPAddress.Broadcast, BroadcastPort);
                     while (!cancellationToken.IsCancellationRequested && session.AllGamers.Count < session.MaxGamers && session.sessionState != NetworkSessionState.Ended)
                     {
                         var propertiesBytes = session.SerializeSessionPropertiesBinary();
-                        var header = $"SESSION:{session.sessionId}:{session.MaxGamers}:{session.PrivateGamerSlots}:{session.Host?.Gamertag ?? "Host"}:";
+            // Include gameplay port in the header so joiners know where to send join requests
+            var header = $"SESSION:{session.sessionId}:{session.MaxGamers}:{session.PrivateGamerSlots}:{session.Host?.Gamertag ?? "Host"}:{GamePort}:";
                         var headerBytes = Encoding.UTF8.GetBytes(header);
                         var message = new byte[headerBytes.Length + propertiesBytes.Length];
                         Buffer.BlockCopy(headerBytes, 0, message, 0, headerBytes.Length);
@@ -50,29 +52,30 @@ namespace Microsoft.Xna.Framework.Net
                     var buffer = result.Buffer;
 
                     // Find the header delimiter (the last colon of the header)
-                    int headerEnd = 0;
-                    int colonCount = 0;
+            int headerEnd = 0;
+            int colonCount = 0;
                     for (int i = 0; i < buffer.Length; i++)
                     {
                         if (buffer[i] == (byte)':')
                         {
                             colonCount++;
-                            if (colonCount == 5)
+                            if (colonCount == 6)
                             {
-                                headerEnd = i + 1; // header ends after 5th colon
+                                headerEnd = i + 1; // header ends after 6th colon (includes game port)
                                 break;
                             }
                         }
                     }
 
-                    if (colonCount == 5)
+            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 hostGamertag = parts[4];
+            var gamePort = int.Parse(parts[5]);
 
                         // Binary session properties start after headerEnd
                         var propertiesBytes = new byte[buffer.Length - headerEnd];
@@ -82,6 +85,7 @@ namespace Microsoft.Xna.Framework.Net
                         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,
@@ -90,7 +94,8 @@ namespace Microsoft.Xna.Framework.Net
                             openPrivateGamerSlots: privateSlots,
                             sessionType: NetworkSessionType.SystemLink,
                             sessionProperties: sessionProperties,
-                            sessionId: sessionId));
+                            sessionId: sessionId,
+                            hostEndpoint: hostEndpoint));
                     }
                 }
             }
@@ -99,7 +104,7 @@ namespace Microsoft.Xna.Framework.Net
 
         public static async Task<NetworkSession> JoinSessionAsync(AvailableNetworkSession availableSession, CancellationToken cancellationToken)
         {
-            // For demo: create a new session instance
+            // Minimal viable join: create a new client session and remember the host endpoint
             await Task.Delay(10, cancellationToken);
             var session = new NetworkSession(NetworkSessionType.SystemLink,
                 availableSession.OpenPublicGamerSlots + availableSession.CurrentGamerCount,
@@ -112,6 +117,23 @@ namespace Microsoft.Xna.Framework.Net
             foreach (var kvp in availableSession.SessionProperties)
                 session.SessionProperties[kvp.Key] = kvp.Value;
 
+            // Bind client transport on join so it can receive packets
+            if (!session.NetworkTransport.IsBound)
+            {
+                session.NetworkTransport.Bind();
+            }
+
+            // Create a synthetic remote host gamer and record endpoint so SendToAll can reach host
+            if (availableSession.HostEndpoint != null)
+            {
+                var hostGamer = new NetworkGamer(session, Guid.NewGuid().ToString(), isLocal: false, isHost: true, gamertag: availableSession.HostGamertag);
+                // Add to session
+                session.GetType().GetMethod("AddGamer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+                    ?.Invoke(session, new object[] { hostGamer });
+                // Map endpoint
+                session.RegisterGamerEndpoint(hostGamer, availableSession.HostEndpoint);
+            }
+
             return session;
         }
     }

+ 15 - 2
MonoGame.Xna.Framework.Net/Net/UdpTransport.cs

@@ -9,7 +9,7 @@ namespace Microsoft.Xna.Framework.Net
     /// </summary>
     public class UdpTransport : INetworkTransport
     {
-        private readonly UdpClient udpClient;
+    private readonly UdpClient udpClient;
         private bool isBound;
 
         /// <summary>
@@ -83,9 +83,22 @@ namespace Microsoft.Xna.Framework.Net
             }
         }
 
+        /// <summary>
+        /// Binds the transport to a specific local UDP port.
+        /// </summary>
+        /// <param name="port">Port to bind.</param>
+        public void Bind(int port)
+        {
+            if (!isBound)
+            {
+                udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, port));
+                isBound = true;
+            }
+        }
+
         /// <summary>
         /// Gets a value indicating whether the transport is bound to a local endpoint.
         /// </summary>
-        public bool IsBound => isBound;
+    public bool IsBound => isBound;
     }
 }

+ 3 - 5
MonoGame.Xna.Framework.Net/Tests/MonoGame.Xna.Framework.Net.Tests.csproj

@@ -6,13 +6,11 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\MonoGame.Xna.Framework.Net.csproj" />
     <PackageReference Include="NUnitLite" Version="3.13.2" />
     <PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
+    <PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.*" PrivateAssets="all"/>
   </ItemGroup>
 
-  <ItemGroup>
-    <ProjectReference Include="..\MonoGame.Xna.Framework.Net.csproj" />
-  </ItemGroup>
-
-</Project>
+</Project>

+ 65 - 2
MonoGame.Xna.Framework.Net/Tests/NetworkSessionTests.cs

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
 using Microsoft.Xna.Framework.Net;
 using System.Collections.Generic;
 using System;
+using System.Reflection;
 
 namespace Microsoft.Xna.Framework.Net.Tests
 {
@@ -41,8 +42,9 @@ namespace Microsoft.Xna.Framework.Net.Tests
             };
 
             // Simulate a gamer joining
-            var gamerType = typeof(NetworkGamer);
-            var gamer = (NetworkGamer)System.Runtime.Serialization.FormatterServices.GetUninitializedObject(gamerType);
+            var ctor = typeof(NetworkGamer).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, binder: null,
+                types: new[] { typeof(NetworkSession), typeof(string), typeof(bool), typeof(bool), typeof(string) }, modifiers: null);
+            var gamer = (NetworkGamer)ctor.Invoke(new object[] { session, Guid.NewGuid().ToString(), false, false, "TestGamer" });
             session.GetType().GetMethod("OnGamerJoined", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
                 .Invoke(session, new object[] { gamer });
 
@@ -134,5 +136,66 @@ namespace Microsoft.Xna.Framework.Net.Tests
             await hostSession.DisposeAsync();
             await clientSession.DisposeAsync();
         }
+
+        [Test]
+        public async Task HostProcessesJoinRequestAndResponds()
+        {
+            // Arrange: Create a host session
+            var hostSession = await NetworkSession.CreateAsync(NetworkSessionType.SystemLink, 1, 4, 0, new Dictionary<string, object>());
+            var joinRequest = new JoinRequestMessage
+            {
+                GamerId = "Player2",
+                Gamertag = "PlayerTwo"
+            };
+            var writer = new PacketWriter();
+            joinRequest.Serialize(writer);
+
+            // Act: Simulate receiving a join request
+            var remoteEndpoint = new System.Net.IPEndPoint(System.Net.IPAddress.Loopback, 12345);
+            hostSession.GetType().GetMethod("OnMessageReceived", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+                .Invoke(hostSession, new object[] { new MessageReceivedEventArgs(joinRequest, remoteEndpoint) });
+
+            // Assert: Verify the new player was added
+            bool playerFound = false;
+            foreach (var gamer in hostSession.AllGamers)
+            {
+                if (gamer.Gamertag == "PlayerTwo")
+                {
+                    playerFound = true;
+                    break;
+                }
+            }
+            Assert.IsTrue(playerFound);
+        }
+
+        [Test]
+        public async Task PlayerMoveMessageUpdatesPositionAndBroadcasts()
+        {
+            // Arrange: Create a host session and add a player
+            var hostSession = await NetworkSession.CreateAsync(NetworkSessionType.SystemLink, 1, 4, 0, new Dictionary<string, object>());
+            var ctor = typeof(NetworkGamer).GetConstructor(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic, null,
+                new[] { typeof(NetworkSession), typeof(string), typeof(bool), typeof(bool), typeof(string) }, null);
+            var player = (NetworkGamer)ctor.Invoke(new object[] { hostSession, "Player1", false, false, "PlayerOne" });
+            hostSession.GetType().GetMethod("AddGamer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+                .Invoke(hostSession, new object[] { player });
+
+            var moveMessage = new PlayerMoveMessage
+            {
+                PlayerId = int.Parse(player.Id),
+                X = 10.0f,
+                Y = 20.0f,
+                Z = 30.0f
+            };
+            var writer = new PacketWriter();
+            moveMessage.Serialize(writer);
+
+            // Act: Simulate receiving a movement message
+            hostSession.GetType().GetMethod("OnMessageReceived", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+                .Invoke(hostSession, new object[] { new MessageReceivedEventArgs(moveMessage, null) });
+
+            // Assert: Verify the movement was processed and broadcast
+            // (Mock implementation: Check debug output or internal state changes)
+            Assert.Pass();
+        }
     }
 }