using System; using System.Net; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Microsoft.Xna.Framework.GamerServices; namespace Microsoft.Xna.Framework.Net { /// /// Represents a network session for multiplayer gaming. /// public class NetworkSession : IDisposable, IAsyncDisposable { // Event for received messages public event EventHandler MessageReceived; private readonly List gamers; private readonly GamerCollection gamerCollection; private readonly NetworkSessionType sessionType; private readonly int maxGamers; private readonly int privateGamerSlots; private readonly Dictionary gamerEndpoints; private readonly object lockObject = new object(); private INetworkTransport networkTransport; internal NetworkSessionState sessionState; private bool disposed; private bool isHost; internal string sessionId; private Task receiveTask; private CancellationTokenSource cancellationTokenSource; // Events public event EventHandler GameStarted; public event EventHandler GameEnded; public event EventHandler GamerJoined; public event EventHandler GamerLeft; public event EventHandler SessionEnded; // Static event for invite acceptance public static event EventHandler InviteAccepted; /// /// Allows changing the network transport implementation. /// public INetworkTransport NetworkTransport { get => networkTransport; set => networkTransport = value ?? throw new ArgumentNullException(nameof(value)); } /// /// Initializes a new NetworkSession. /// private NetworkSession(NetworkSessionType sessionType, int maxGamers, int privateGamerSlots, bool isHost) { // Register message types (can be moved to static constructor) NetworkMessageRegistry.Register(1); this.sessionType = sessionType; this.maxGamers = maxGamers; this.privateGamerSlots = privateGamerSlots; this.isHost = isHost; this.sessionId = Guid.NewGuid().ToString(); this.sessionState = NetworkSessionState.Creating; gamers = new List(); gamerCollection = new GamerCollection(gamers); gamerEndpoints = new Dictionary(); networkTransport = new UdpTransport(); cancellationTokenSource = new CancellationTokenSource(); // Add local gamer var gamerGuid = Guid.NewGuid().ToString(); var localGamer = new LocalNetworkGamer(this, gamerGuid, isHost, $"{SignedInGamer.Current?.Gamertag ?? "Player"}_{gamerGuid.Substring(0, 8)}"); NetworkGamer.LocalGamer = localGamer; AddGamer(localGamer); // Start receive loop for SystemLink sessions if (sessionType == NetworkSessionType.SystemLink) { receiveTask = Task.Run(() => ReceiveLoopAsync(cancellationTokenSource.Token)); } } // Internal constructor for SystemLink join internal NetworkSession(NetworkSessionType sessionType, int maxGamers, int privateGamerSlots, bool isHost, string sessionId) : this(sessionType, maxGamers, privateGamerSlots, isHost) { this.sessionId = sessionId; this.sessionState = NetworkSessionState.Lobby; } /// /// Gets all gamers in the session. /// public GamerCollection AllGamers => gamerCollection; /// /// Gets local gamers in the session. /// public LocalGamerCollection LocalGamers { get { var localGamers = gamers.Where(g => g.IsLocal).OfType().ToList(); return new LocalGamerCollection(localGamers); } } /// /// Gets remote gamers in the session. /// public GamerCollection RemoteGamers { get { var remoteGamers = gamers.Where(g => !g.IsLocal).ToList(); return new GamerCollection(remoteGamers); } } /// /// Gets the host gamer. /// public NetworkGamer Host => AllGamers.Host; /// /// Gets whether this machine is the host. /// public bool IsHost => isHost; /// /// Gets whether everyone is ready to start the game. /// public bool IsEveryoneReady => AllGamers.All(g => g.IsReady); /// /// Gets the maximum number of gamers. /// public int MaxGamers => maxGamers; /// /// Gets the number of private gamer slots. /// public int PrivateGamerSlots => privateGamerSlots; /// /// Gets the session type. /// public NetworkSessionType SessionType => sessionType; /// /// Gets the current session state. /// public NetworkSessionState SessionState => sessionState; /// /// Gets the bytes per second being sent. /// public int BytesPerSecondSent => 0; // Mock implementation /// /// Gets the bytes per second being received. /// public int BytesPerSecondReceived => 0; // Mock implementation /// /// Gets whether the session allows host migration. /// public bool AllowHostMigration { get; set; } = true; /// /// Gets whether the session allows gamers to join during gameplay. /// public bool AllowJoinInProgress { get; set; } = true; private IDictionary sessionProperties = new Dictionary(); /// /// Gets or sets custom session properties. /// public IDictionary SessionProperties { get => sessionProperties; set { sessionProperties = value ?? throw new ArgumentNullException(nameof(value)); // Automatically broadcast changes if this machine is the host if (IsHost) { BroadcastSessionProperties(); } } } // Simulation properties for testing network conditions private TimeSpan simulatedLatency = TimeSpan.Zero; private float simulatedPacketLoss = 0.0f; /// /// Gets or sets the simulated network latency for testing purposes. /// public TimeSpan SimulatedLatency { get => simulatedLatency; set => simulatedLatency = value; } /// /// Gets or sets the simulated packet loss percentage for testing purposes. /// public float SimulatedPacketLoss { get => simulatedPacketLoss; set => simulatedPacketLoss = Math.Max(0.0f, Math.Min(1.0f, value)); } /// /// Cancels all ongoing async operations for this session. /// public void Cancel() { cancellationTokenSource?.Cancel(); } /// /// Asynchronously creates a new network session. /// public static async Task CreateAsync(NetworkSessionType sessionType, int maxLocalGamers, int maxGamers, int privateGamerSlots, IDictionary sessionProperties, CancellationToken cancellationToken = default) { if (maxLocalGamers < 1 || maxLocalGamers > 4) throw new ArgumentOutOfRangeException(nameof(maxLocalGamers)); if (privateGamerSlots < 0 || privateGamerSlots > maxGamers) throw new ArgumentOutOfRangeException(nameof(privateGamerSlots)); NetworkSession session = null; switch (sessionType) { case NetworkSessionType.Local: // Local session: in-memory only await Task.Delay(5, cancellationToken); session = new NetworkSession(sessionType, maxGamers, privateGamerSlots, true); session.sessionState = NetworkSessionState.Lobby; // Register in local session list for FindAsync LocalSessionRegistry.RegisterSession(session); break; case NetworkSessionType.SystemLink: // 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 break; default: // Not implemented throw new NotSupportedException($"SessionType {sessionType} not supported yet."); } return session; } /// /// Synchronous wrapper for CreateAsync (for XNA compatibility). /// public static NetworkSession Create(NetworkSessionType sessionType, int maxLocalGamers, int maxGamers, int privateGamerSlots, IDictionary sessionProperties) { return CreateAsync(sessionType, maxLocalGamers, maxGamers, privateGamerSlots, sessionProperties).GetAwaiter().GetResult(); } /// /// Asynchronously finds available network sessions. /// public static async Task FindAsync(NetworkSessionType sessionType, int maxLocalGamers, IDictionary sessionProperties, CancellationToken cancellationToken = default) { switch (sessionType) { case NetworkSessionType.Local: await Task.Delay(5, cancellationToken); // Return sessions in local registry var localSessions = LocalSessionRegistry.FindSessions(maxLocalGamers).ToList(); return new AvailableNetworkSessionCollection(localSessions); case NetworkSessionType.SystemLink: // Discover sessions via UDP broadcast var systemLinkSessions = (await SystemLinkSessionManager.DiscoverSessionsAsync(maxLocalGamers, cancellationToken)).ToList(); return new AvailableNetworkSessionCollection(systemLinkSessions); default: throw new NotSupportedException($"SessionType {sessionType} not supported yet."); } } /// /// Synchronous wrapper for FindAsync (for XNA compatibility). /// public static AvailableNetworkSessionCollection Find(NetworkSessionType sessionType, int maxLocalGamers, IDictionary sessionProperties) { return FindAsync(sessionType, maxLocalGamers, sessionProperties).GetAwaiter().GetResult(); } /// /// Asynchronously joins an available network session. /// public static async Task JoinAsync(AvailableNetworkSession availableSession, CancellationToken cancellationToken = default) { switch (availableSession.SessionType) { case NetworkSessionType.Local: // Attach to local session var localSession = LocalSessionRegistry.GetSessionById(availableSession.SessionId); if (localSession == null) throw new NetworkSessionJoinException(NetworkSessionJoinError.SessionNotFound); // Add local gamer var newGamer = new LocalNetworkGamer(localSession, Guid.NewGuid().ToString(), false, SignedInGamer.Current?.Gamertag ?? "Player"); localSession.AddGamer(newGamer); return localSession; case NetworkSessionType.SystemLink: // Connect to host via network var joinedSession = await SystemLinkSessionManager.JoinSessionAsync(availableSession, cancellationToken); return joinedSession; default: throw new NotSupportedException($"SessionType {availableSession.SessionType} not supported yet."); } } /// /// /// Creates a new network session synchronously with default properties. /// public static NetworkSession Create(NetworkSessionType sessionType, int maxLocalGamers, int maxGamers) { return CreateAsync(sessionType, maxLocalGamers, maxGamers, 0, new Dictionary()).GetAwaiter().GetResult(); } /// /// /// Finds available network sessions synchronously with default properties. /// public static AvailableNetworkSessionCollection Find(NetworkSessionType sessionType, int maxLocalGamers) { return FindAsync(sessionType, maxLocalGamers, new Dictionary()).GetAwaiter().GetResult(); } /// /// /// Joins an available network session synchronously. /// public static NetworkSession Join(AvailableNetworkSession availableSession) { return JoinAsync(availableSession).GetAwaiter().GetResult(); } /// /// Updates the network session. /// public void Update() { if (disposed) return; // Process any pending network messages ProcessIncomingMessages(); } /// /// Sends data to all gamers in the session. /// public void SendToAll(PacketWriter writer, SendDataOptions options) { SendToAll(writer, options, NetworkGamer.LocalGamer); } /// /// Sends data to all gamers except the specified sender. /// public void SendToAll(PacketWriter writer, SendDataOptions options, NetworkGamer sender) { if (writer == null) throw new ArgumentNullException(nameof(writer)); byte[] data = writer.GetData(); lock (lockObject) { foreach (var gamer in gamers) { if (gamer != sender && !gamer.IsLocal) { SendDataToGamer(gamer, data, options); } } } } /// /// Starts the game. /// public void StartGame() { if (sessionState == NetworkSessionState.Lobby) { sessionState = NetworkSessionState.Playing; OnGameStarted(); } } /// /// Ends the game and returns to lobby. /// public void EndGame() { if (sessionState == NetworkSessionState.Playing) { sessionState = NetworkSessionState.Lobby; OnGameEnded(); } } /// /// Notifies when a gamer's readiness changes. /// internal void NotifyReadinessChanged(NetworkGamer gamer) { // Send readiness update to other players if (IsHost) { var writer = new PacketWriter(); writer.Write("ReadinessUpdate"); writer.Write(gamer.Id); writer.Write(gamer.IsReady); SendToAll(writer, SendDataOptions.Reliable, gamer); } } /// /// Sends data to a specific gamer. /// internal void SendDataToGamer(NetworkGamer gamer, PacketWriter writer, SendDataOptions options) { SendDataToGamer(gamer, writer.GetData(), options); } internal void SendDataToGamer(NetworkGamer gamer, byte[] data, SendDataOptions options) { if (gamerEndpoints.TryGetValue(gamer.Id, out IPEndPoint endpoint)) { try { networkTransport.Send(data, endpoint); } catch (Exception ex) { Debug.WriteLine($"Failed to send data to {gamer.Gamertag}: {ex.Message}"); } } } private void AddGamer(NetworkGamer gamer) { lock (lockObject) { gamers.Add(gamer); } OnGamerJoined(gamer); } private void RemoveGamer(NetworkGamer gamer) { lock (lockObject) { gamers.Remove(gamer); gamerEndpoints.Remove(gamer.Id); } OnGamerLeft(gamer); } // Modern async receive loop for SystemLink private async Task ReceiveLoopAsync(CancellationToken cancellationToken) { using var udpClient = new UdpClient(); udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 0)); // Use a dynamic port for tests while (!cancellationToken.IsCancellationRequested) { try { var result = await udpClient.ReceiveAsync(); var data = result.Buffer; if (data.Length > 1) { var senderEndpoint = result.RemoteEndPoint; NetworkGamer senderGamer = null; lock (lockObject) { senderGamer = gamers.FirstOrDefault(g => gamerEndpoints.TryGetValue(g.Id, out var ep) && ep.Equals(senderEndpoint)); } if (senderGamer != null) { senderGamer.EnqueueIncomingPacket(data, senderGamer); } var typeId = data[0]; var reader = new PacketReader(data); // Use only the byte array var message = NetworkMessageRegistry.CreateMessage(typeId); message?.Deserialize(reader); OnMessageReceived(new MessageReceivedEventArgs(message, result.RemoteEndPoint)); } } catch (ObjectDisposedException) { break; } catch (Exception ex) { Debug.WriteLine($"ReceiveLoop error: {ex.Message}"); } } } private void OnMessageReceived(MessageReceivedEventArgs e) { // Raise the MessageReceived event var handler = MessageReceived; if (handler != null) handler(this, e); } private void OnGameStarted() { GameStarted?.Invoke(this, new GameStartedEventArgs()); } private void OnGameEnded() { GameEnded?.Invoke(this, new GameEndedEventArgs()); } private void OnGamerJoined(NetworkGamer gamer) { GamerJoined?.Invoke(this, new GamerJoinedEventArgs(gamer)); } private void OnGamerLeft(NetworkGamer gamer) { GamerLeft?.Invoke(this, new GamerLeftEventArgs(gamer)); } private void OnSessionEnded(NetworkSessionEndReason reason) { sessionState = NetworkSessionState.Ended; SessionEnded?.Invoke(this, new NetworkSessionEndedEventArgs(reason)); } /// /// Disposes the network session. /// public void Dispose() { if (!disposed) { cancellationTokenSource?.Cancel(); receiveTask?.Wait(1000); // Wait up to 1 second networkTransport?.Dispose(); cancellationTokenSource?.Dispose(); OnSessionEnded(NetworkSessionEndReason.ClientSignedOut); disposed = true; } } public async ValueTask DisposeAsync() { if (!disposed) { cancellationTokenSource?.Cancel(); if (receiveTask != null) await receiveTask; if (networkTransport is IAsyncDisposable asyncTransport) await asyncTransport.DisposeAsync(); else networkTransport?.Dispose(); cancellationTokenSource?.Dispose(); OnSessionEnded(NetworkSessionEndReason.ClientSignedOut); disposed = true; } } internal byte[] SerializeSessionPropertiesBinary() { using (var ms = new MemoryStream()) using (var writer = new BinaryWriter(ms)) { writer.Write(SessionProperties.Count); foreach (var kvp in SessionProperties) { writer.Write(kvp.Key); // Write type info and value if (kvp.Value is int i) { writer.Write((byte)1); // type marker writer.Write(i); } else if (kvp.Value is bool b) { writer.Write((byte)2); writer.Write(b); } else if (kvp.Value is string s) { writer.Write((byte)3); writer.Write(s ?? ""); } // Add more types as needed else { writer.Write((byte)255); // unknown type writer.Write(kvp.Value?.ToString() ?? ""); } } return ms.ToArray(); } } internal void DeserializeSessionPropertiesBinary(byte[] data) { using (var ms = new MemoryStream(data)) using (var reader = new BinaryReader(ms)) { SessionProperties.Clear(); int count = reader.ReadInt32(); for (int i = 0; i < count; i++) { string key = reader.ReadString(); byte type = reader.ReadByte(); object value = null; switch (type) { case 1: value = reader.ReadInt32(); break; case 2: value = reader.ReadBoolean(); break; case 3: value = reader.ReadString(); break; default: value = reader.ReadString(); break; } SessionProperties[key] = value; } } } private void BroadcastSessionProperties() { var writer = new PacketWriter(); writer.Write("SessionPropertiesUpdate"); writer.Write(SerializeSessionPropertiesBinary()); SendToAll(writer, SendDataOptions.Reliable); } private void ProcessIncomingMessages() { foreach (var gamer in gamers) { while (gamer.IsDataAvailable) { var reader = new PacketReader(); gamer.ReceiveData(out reader, out var sender); var messageType = reader.ReadString(); if (messageType == "SessionPropertiesUpdate") { var propertiesData = reader.ReadBytes(); DeserializeSessionPropertiesBinary(propertiesData); } } } } } }