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

Networking: Ensure players are synced across peers.

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

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

@@ -20,9 +20,10 @@ namespace Microsoft.Xna.Framework.Net
 
         public void Deserialize(PacketReader reader)
         {
+            // Reader is positioned after the type byte
             SessionId = reader.ReadString();
             HostGamerId = reader.ReadString();
             HostGamertag = reader.ReadString();
         }
     }
-}
+}

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

@@ -18,8 +18,9 @@ namespace Microsoft.Xna.Framework.Net
 
         public void Deserialize(PacketReader reader)
         {
+            // Reader is positioned after the type byte
             GamerId = reader.ReadString();
             Gamertag = reader.ReadString();
         }
     }
-}
+}

+ 13 - 12
MonoGame.Xna.Framework.Net/Net/NetworkGamer.cs

@@ -142,7 +142,7 @@ namespace Microsoft.Xna.Framework.Net
             return length;
         }
 
-		public int ReceiveData(byte[] data, int offset, out NetworkGamer sender)
+        public int ReceiveData(byte[] data, int offset, out NetworkGamer sender)
         {
             if (data == null)
                 throw new ArgumentNullException(nameof(data));
@@ -161,21 +161,22 @@ namespace Microsoft.Xna.Framework.Net
             int length = Math.Min(data.Length - offset, packet.Data.Length);
             Array.Copy(packet.Data, 0, data, offset, length);
             return length;
-		}
-
-		/// <summary>
-		/// Receives data from this gamer using PacketReader.
-		/// </summary>
-		/// <param name="data">Array to receive the data into.</param>
-		/// <param name="sender">The gamer who sent the data.</param>
-		/// <returns>The number of bytes received.</returns>
-		public int ReceiveData(PacketReader reader, out NetworkGamer sender)
+        }
+
+        /// <summary>
+        /// Receives data from this gamer using PacketReader.
+        /// </summary>
+        /// <param name="data">Array to receive the data into.</param>
+        /// <param name="sender">The gamer who sent the data.</param>
+        /// <returns>The number of bytes received.</returns>
+        public int ReceiveData(PacketReader reader, out NetworkGamer sender)
         {
             // Ensure the session is valid
             if (session == null)
                 throw new InvalidOperationException("Network session is not initialized.");
 
-            reader = null;
+            if (reader == null)
+                throw new ArgumentNullException(nameof(reader));
             sender = null;
 
             // Check if data is available
@@ -184,7 +185,7 @@ namespace Microsoft.Xna.Framework.Net
 
             var packet = incomingPackets.Dequeue();
             sender = packet.Sender;
-            reader = new PacketReader(packet.Data);
+            reader.Reset(packet.Data);
             return packet.Data.Length;
         }
 

+ 63 - 25
MonoGame.Xna.Framework.Net/Net/NetworkSession.cs

@@ -436,8 +436,9 @@ namespace Microsoft.Xna.Framework.Net
             if (disposed)
                 return;
 
-            // Process any pending network messages
-            ProcessIncomingMessages();
+            // Process any pending in-memory messages only for Local sessions
+            if (sessionType == NetworkSessionType.Local)
+                ProcessIncomingMessages();
         }
 
         /// <summary>
@@ -551,7 +552,7 @@ namespace Microsoft.Xna.Framework.Net
         }
 
 
-    private void AddGamer(NetworkGamer gamer)
+        private void AddGamer(NetworkGamer gamer)
         {
             lock (lockObject)
             {
@@ -584,7 +585,7 @@ namespace Microsoft.Xna.Framework.Net
                     }
 
                     var (data, senderEndpoint) = await networkTransport.ReceiveAsync();
-                    if (data.Length > 1)
+                    if (data.Length > 0)
                     {
                         NetworkGamer senderGamer = null;
                         lock (lockObject)
@@ -594,11 +595,13 @@ namespace Microsoft.Xna.Framework.Net
 
                         if (senderGamer != null)
                         {
-                            senderGamer.EnqueueIncomingPacket(data, senderGamer);
+                            // Deliver to the local gamer’s inbox with correct sender, matching XNA ReceiveData semantics
+                            NetworkGamer.LocalGamer?.EnqueueIncomingPacket(data, senderGamer);
                         }
 
-                        var typeId = data[0];
-                        var reader = new PacketReader(data); // Use only the byte array
+                        // Parse message: first byte is the type id, advance reader before Deserialize
+                        var reader = new PacketReader(data);
+                        var typeId = reader.ReadByte();
                         var message = NetworkMessageRegistry.CreateMessage(typeId);
                         message?.Deserialize(reader);
                         OnMessageReceived(new MessageReceivedEventArgs(message, senderEndpoint));
@@ -632,19 +635,47 @@ namespace Microsoft.Xna.Framework.Net
                 joinAccepted.Serialize(writer);
                 networkTransport.Send(writer.GetData(), e.RemoteEndPoint);
             }
+            else if (e.Message is JoinAcceptedMessage joinAccepted)
+            {
+                // Client receives confirmation from host; ensure host is present and mapped
+                if (!IsHost)
+                {
+                    var existingHost = gamers.FirstOrDefault(g => g.IsHost);
+                    if (existingHost != null && existingHost.Id != joinAccepted.HostGamerId)
+                    {
+                        // Remove synthetic host
+                        RemoveGamer(existingHost);
+                        existingHost = null;
+                    }
+
+                    var host = existingHost ?? new NetworkGamer(this, joinAccepted.HostGamerId, isLocal: false, isHost: true, gamertag: joinAccepted.HostGamertag);
+                    if (existingHost == null)
+                        AddGamer(host);
+                    RegisterGamerEndpoint(host, 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)
+                // Identify sender by endpoint mapping
+                NetworkGamer sourceGamer = null;
+                lock (lockObject)
+                {
+                    if (e.RemoteEndPoint != null)
+                        sourceGamer = gamers.FirstOrDefault(g => gamerEndpoints.TryGetValue(g.Id, out var ep) && ep.Equals(e.RemoteEndPoint));
+                }
+
+                if (sourceGamer != null)
                 {
-                    // Update gamer position (mock implementation)
-                    Debug.WriteLine($"Player {gamer.Gamertag} moved to ({moveMessage.X}, {moveMessage.Y}, {moveMessage.Z})");
+                    // Update position (mock) and broadcast to others
+                    Debug.WriteLine($"Player {sourceGamer.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);
+                    // Only the host rebroadcasts to others
+                    if (IsHost)
+                    {
+                        var writer = new PacketWriter();
+                        moveMessage.Serialize(writer);
+                        SendToAll(writer, SendDataOptions.Reliable, sourceGamer);
+                    }
                 }
             }
 
@@ -688,8 +719,9 @@ namespace Microsoft.Xna.Framework.Net
             if (!disposed)
             {
                 cancellationTokenSource?.Cancel();
-                receiveTask?.Wait(1000); // Wait up to 1 second
+                // Dispose transport first to unblock ReceiveAsync
                 networkTransport?.Dispose();
+                try { receiveTask?.Wait(1000); } catch { /* ignore */ }
                 cancellationTokenSource?.Dispose();
                 OnSessionEnded(NetworkSessionEndReason.ClientSignedOut);
                 disposed = true;
@@ -701,12 +733,15 @@ namespace Microsoft.Xna.Framework.Net
             if (!disposed)
             {
                 cancellationTokenSource?.Cancel();
-                if (receiveTask != null)
-                    await receiveTask;
+                // Dispose transport first to unblock any pending ReceiveAsync
                 if (networkTransport is IAsyncDisposable asyncTransport)
                     await asyncTransport.DisposeAsync();
                 else
                     networkTransport?.Dispose();
+                if (receiveTask != null)
+                {
+                    await Task.WhenAny(receiveTask, Task.Delay(1000));
+                }
                 cancellationTokenSource?.Dispose();
                 OnSessionEnded(NetworkSessionEndReason.ClientSignedOut);
                 disposed = true;
@@ -775,10 +810,13 @@ namespace Microsoft.Xna.Framework.Net
 
         private void BroadcastSessionProperties()
         {
-            var writer = new PacketWriter();
-            writer.Write("SessionPropertiesUpdate");
-            writer.Write(SerializeSessionPropertiesBinary());
-            SendToAll(writer, SendDataOptions.Reliable);
+            if (sessionType == NetworkSessionType.Local)
+            {
+                var writer = new PacketWriter();
+                writer.Write("SessionPropertiesUpdate");
+                writer.Write(SerializeSessionPropertiesBinary());
+                SendToAll(writer, SendDataOptions.Reliable);
+            }
         }
 
         private void ProcessIncomingMessages()
@@ -808,7 +846,7 @@ namespace Microsoft.Xna.Framework.Net
                 {
                     gamerJoined?.Invoke(this, new GamerJoinedEventArgs(gamer));
                 }
-			}
-		}
+            }
+        }
     }
 }

+ 14 - 1
MonoGame.Xna.Framework.Net/Net/PacketReader.cs

@@ -32,6 +32,19 @@ namespace Microsoft.Xna.Framework.Net
             reader = new BinaryReader(stream, Encoding.UTF8);
         }
 
+        /// <summary>
+        /// Replaces the internal buffer with the provided data and rewinds to the start.
+        /// This mirrors XNA's PacketReader.SetBuffer behavior for reuse.
+        /// </summary>
+        public void Reset(byte[] data)
+        {
+            if (data == null) throw new ArgumentNullException(nameof(data));
+            stream.SetLength(0);
+            stream.Position = 0;
+            stream.Write(data, 0, data.Length);
+            stream.Position = 0;
+        }
+
         /// <summary>
         /// Gets the length of the packet data.
         /// </summary>
@@ -238,4 +251,4 @@ namespace Microsoft.Xna.Framework.Net
             }
         }
     }
-}
+}

+ 3 - 2
MonoGame.Xna.Framework.Net/Net/PlayerMoveMessage.cs

@@ -1,6 +1,6 @@
 namespace Microsoft.Xna.Framework.Net
 {
-	public class PlayerMoveMessage : INetworkMessage
+    public class PlayerMoveMessage : INetworkMessage
     {
         public byte MessageType => 1;
         public int PlayerId { get; set; }
@@ -19,10 +19,11 @@ namespace Microsoft.Xna.Framework.Net
 
         public void Deserialize(PacketReader reader)
         {
+            // Reader is positioned after the type byte
             PlayerId = reader.ReadInt32();
             X = reader.ReadSingle();
             Y = reader.ReadSingle();
             Z = reader.ReadSingle();
         }
     }
-}
+}

+ 21 - 12
MonoGame.Xna.Framework.Net/Net/SystemLinkSessionManager.cs

@@ -10,8 +10,8 @@ namespace Microsoft.Xna.Framework.Net
 {
     internal static class SystemLinkSessionManager
     {
-    private const int BroadcastPort = 31337;
-    private const int GamePort = 31338; // Port for gameplay UDP traffic
+        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)
@@ -19,14 +19,15 @@ 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())
                 {
+                    udpClient.EnableBroadcast = true;
                     var endpoint = new IPEndPoint(IPAddress.Broadcast, BroadcastPort);
                     while (!cancellationToken.IsCancellationRequested && session.AllGamers.Count < session.MaxGamers && session.sessionState != NetworkSessionState.Ended)
                     {
                         var propertiesBytes = session.SerializeSessionPropertiesBinary();
-            // 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}:";
+                        // 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);
@@ -52,8 +53,8 @@ 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)':')
@@ -67,15 +68,15 @@ namespace Microsoft.Xna.Framework.Net
                         }
                     }
 
-            if (colonCount == 6)
+                    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]);
+                        var hostGamertag = parts[4];
+                        var gamePort = int.Parse(parts[5]);
 
                         // Binary session properties start after headerEnd
                         var propertiesBytes = new byte[buffer.Length - headerEnd];
@@ -127,11 +128,19 @@ namespace Microsoft.Xna.Framework.Net
             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);
+
+                // Proactively send a JoinRequest to the host so it can add this client
+                var joinRequest = new JoinRequestMessage
+                {
+                    GamerId = NetworkGamer.LocalGamer.Id,
+                    Gamertag = NetworkGamer.LocalGamer.Gamertag
+                };
+                var writer = new PacketWriter();
+                joinRequest.Serialize(writer);
+                session.NetworkTransport.Send(writer.GetData(), availableSession.HostEndpoint);
             }
 
             return session;