From a68b5f6c99b33e88dd91bbefb93c703c8be71ee5 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sat, 22 Jun 2019 15:02:49 +0300 Subject: [PATCH 001/109] Start work on version 3 tunnel support --- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 117 +++++++++++- .../Domain/Multiplayer/CnCNet/Constants.cs | 11 ++ .../Domain/Multiplayer/CnCNet/IPUtilities.cs | 43 +++++ .../Multiplayer/CnCNet/V3TunnelConnection.cs | 167 ++++++++++++++++++ .../Multiplayer/CnCNet/V3TunnelHandler.cs | 104 +++++++++++ 5 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/IPUtilities.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelHandler.cs diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index edb45d85e..6d80d5df1 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -4,6 +4,8 @@ using System.Globalization; using System.Net; using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Threading; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -12,7 +14,8 @@ namespace DTAClient.Domain.Multiplayer.CnCNet /// public class CnCNetTunnel { - private const int REQUEST_TIMEOUT = 10000; // In milliseconds + private const int VERSION_3_PING_PACKET_SEND_SIZE = 800; + private const int VERSION_3_PING_PACKET_RECEIVE_SIZE = 12; private const int PING_TIMEOUT = 1000; public CnCNetTunnel() { } @@ -69,6 +72,20 @@ public static CnCNetTunnel Parse(string str) } } + private string _ipAddress; + public string Address + { + get => _ipAddress; + set + { + _ipAddress = value; + if (IPAddress.TryParse(_ipAddress, out IPAddress address)) + IPAddress = address; + } + } + + public IPAddress IPAddress { get; private set; } + public string Address { get; private set; } public int Port { get; private set; } public string Country { get; private set; } @@ -85,12 +102,17 @@ public static CnCNetTunnel Parse(string str) public double Distance { get; private set; } public int PingInMs { get; set; } = -1; + public event EventHandler Pinged; + /// /// Gets a list of player ports to use from a specific tunnel server. /// /// A list of player ports to use. public List GetPlayerPortInfo(int playerCount) { + if (Version != Constants.TUNNEL_VERSION_2) + throw new InvalidOperationException("GetPlayerPortInfo only works with version 2 tunnels."); + try { Logger.Log($"Contacting tunnel at {Address}:{Port}"); @@ -98,12 +120,12 @@ public List GetPlayerPortInfo(int playerCount) string addressString = $"http://{Address}:{Port}/request?clients={playerCount}"; Logger.Log($"Downloading from {addressString}"); - using (var client = new ExtendedWebClient(REQUEST_TIMEOUT)) + using (var client = new ExtendedWebClient(Constants.TUNNEL_CONNECTION_TIMEOUT)) { string data = client.DownloadString(addressString); - data = data.Replace("[", String.Empty); - data = data.Replace("]", String.Empty); + data = data.Replace("[", string.Empty); + data = data.Replace("]", string.Empty); string[] portIDs = data.Split(','); List playerPorts = new List(); @@ -141,5 +163,92 @@ public void UpdatePing() } } } + + public void PingAsync() + { + Thread thread = new Thread(new ThreadStart(Ping)); + thread.Start(); + } + + public void Ping() + { + if (Version == Constants.TUNNEL_VERSION_2) + { + PingV2(); + } + else if (Version == Constants.TUNNEL_VERSION_3) + { + PingV3(); + } + } + + private void PingV2() + { + int pingInMs = -1; + Ping p = new Ping(); + try + { + PingReply reply = p.Send(IPAddress.Parse(Address), PING_TIMEOUT); + if (reply.Status == IPStatus.Success) + { + if (reply.RoundtripTime > 0) + pingInMs = Convert.ToInt32(reply.RoundtripTime); + } + } + catch { } + + if (pingInMs > -1) + PingInMs = pingInMs; + + Pinged?.Invoke(this, EventArgs.Empty); + } + + private void PingV3() + { + using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) + { + socket.SendTimeout = PING_TIMEOUT; + socket.ReceiveTimeout = PING_TIMEOUT; + + try + { + byte[] buffer = new byte[VERSION_3_PING_PACKET_SEND_SIZE]; + EndPoint ep = new IPEndPoint(IPAddress, Port); + long ticks = DateTime.Now.Ticks; + socket.SendTo(buffer, ep); + + buffer = new byte[VERSION_3_PING_PACKET_RECEIVE_SIZE]; + socket.ReceiveFrom(buffer, ref ep); + + ticks = DateTime.Now.Ticks - ticks; + PingInMs = new TimeSpan(ticks).Milliseconds; + } + catch (SocketException ex) + { + Logger.Log($"Failed to ping V3 tunnel {Name} ({Address}:{Port}). Message: {ex.Message}"); + + PingInMs = -1; + } + } + + Pinged?.Invoke(this, EventArgs.Empty); + } + + /// + /// Returns a bool that tells if the tunnel server has passed + /// initial connection checks and is available for online games. + /// + public bool IsAvailable() + { + return true; // temporary hack until all v3 tunnels support pinging + + if (Version == Constants.TUNNEL_VERSION_2) + return true; + + if (Version == Constants.TUNNEL_VERSION_3) + return PingInMs > -1; + + return false; + } } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs new file mode 100644 index 000000000..178041d73 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs @@ -0,0 +1,11 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet +{ + class Constants + { + internal const int TUNNEL_CONNECTION_TIMEOUT = 10000; // In milliseconds + internal const int TUNNEL_RECEIVE_TIMEOUT = 30000; + + internal const int TUNNEL_VERSION_2 = 2; + internal const int TUNNEL_VERSION_3 = 3; + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/IPUtilities.cs b/DXMainClient/Domain/Multiplayer/CnCNet/IPUtilities.cs new file mode 100644 index 000000000..d43918fb8 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/IPUtilities.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace DTAClient.Domain.Multiplayer.CnCNet +{ + class IPUtilities + { + public static IPEndPoint GetPublicEndPoint(IPAddress serverIP, int destPort, int gamePort) + { + // Code by FunkyFr3sh + + using (var udpClient = new UdpClient()) + { + udpClient.ExclusiveAddressUse = false; + udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + + udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, gamePort)); + + IAsyncResult iAsyncResult = udpClient.BeginReceive(null, null); + udpClient.Send(new byte[1], 1, new IPEndPoint(serverIP, destPort)); + iAsyncResult.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(750), false); + if (iAsyncResult.IsCompleted) + { + IPEndPoint remote = null; + byte[] data = udpClient.EndReceive(iAsyncResult, ref remote); + if (remote.Address.Equals(serverIP) && remote.Port == destPort && data.Length == 8) + { + byte[] ip = new byte[4]; + Array.Copy(data, 4, ip, 0, 4); + return new IPEndPoint(new IPAddress(ip), BitConverter.ToInt32(data, 0)); + } + } + } + + throw new Exception("No response from server"); + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs new file mode 100644 index 000000000..e123c19ac --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -0,0 +1,167 @@ +using Rampastring.Tools; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace DTAClient.Domain.Multiplayer.CnCNet +{ + public enum ConnectionState + { + NotConnected = 0, + WaitingForPassword = 1, + WaitingForVerification = 2, + Connected = 3 + } + + /// + /// Handles connections to version 3 CnCNet tunnel servers. + /// + class V3TunnelConnection + { + private const int PASSWORD_REQUEST_SIZE = 512; + private const int PASSWORD_MESSAGE_SIZE = 12; + + public V3TunnelConnection(CnCNetTunnel tunnel, uint senderId) + { + this.tunnel = tunnel; + SenderId = senderId; + } + + public event EventHandler Connected; + public event EventHandler ConnectionFailed; + + public delegate void MessageDelegate(byte[] data, uint senderId); + public event MessageDelegate MessageReceived; + + public uint SenderId { get; set; } + + public ConnectionState State { get; private set; } + + private bool aborted = false; + public bool Aborted + { + get { lock (locker) return aborted; } + private set { lock (locker) aborted = value; } + } + + private CnCNetTunnel tunnel; + private Socket tunnelSocket; + private EndPoint tunnelEndPoint; + + private readonly object locker = new object(); + + public void ConnectAsync() + { + Thread thread = new Thread(new ThreadStart(DoConnect)); + thread.Start(); + } + + private void DoConnect() + { + Logger.Log($"Attempting to establish connection to V3 tunnel server " + + $"{tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); + + tunnelSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + tunnelSocket.SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; + + try + { + byte[] buffer = new byte[PASSWORD_REQUEST_SIZE]; + WriteSenderIdToBuffer(buffer); + tunnelEndPoint = new IPEndPoint(tunnel.IPAddress, tunnel.Port); + tunnelSocket.SendTo(buffer, tunnelEndPoint); + State = ConnectionState.WaitingForPassword; + Logger.Log("Sent ID, waiting for password."); + + buffer = new byte[PASSWORD_MESSAGE_SIZE]; + tunnelSocket.ReceiveFrom(buffer, ref tunnelEndPoint); + + byte[] password = new byte[4]; + Array.Copy(buffer, 8, password, 0, password.Length); + Logger.Log("Password received, sending it back for verification."); + + // Echo back the password + // <4 bytes of anything> + buffer = new byte[PASSWORD_MESSAGE_SIZE]; + WriteSenderIdToBuffer(buffer); + Array.Copy(password, 0, buffer, 8, password.Length); + tunnelSocket.SendTo(buffer, tunnelEndPoint); + State = ConnectionState.Connected; + + Logger.Log("Connection to tunnel server established. Entering receive loop."); + Connected?.Invoke(this, EventArgs.Empty); + } + catch (SocketException ex) + { + Logger.Log($"Failed to establish connection to tunnel server. Message: " + ex.Message); + tunnelSocket.Close(); + ConnectionFailed?.Invoke(this, EventArgs.Empty); + return; + } + + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; + ReceiveLoop(); + } + + private void WriteSenderIdToBuffer(byte[] buffer) => + Array.Copy(BitConverter.GetBytes(SenderId), buffer, sizeof(uint)); + + private void ReceiveLoop() + { + while (true) + { + if (Aborted) + { + DoClose(); + Logger.Log("Exiting receive loop."); + return; + } + + byte[] buffer = new byte[1024]; + int size = tunnelSocket.ReceiveFrom(buffer, ref tunnelEndPoint); + + if (size < 8) + { + Logger.Log("Invalid data packet from tunnel server"); + continue; + } + + byte[] data = new byte[size - 8]; + Array.Copy(buffer, 8, data, 0, data.Length); + uint senderId = BitConverter.ToUInt32(buffer, 0); + + MessageReceived?.Invoke(data, senderId); + } + } + + public void CloseConnection() + { + Logger.Log("Closing connection to the tunnel server."); + Aborted = true; + } + + private void DoClose() + { + if (tunnelSocket != null) + { + tunnelSocket.Close(); + tunnelSocket = null; + State = ConnectionState.NotConnected; + } + + Logger.Log("Connection to tunnel server closed."); + } + + public void SendData(byte[] data, uint receiverId) + { + byte[] packet = new byte[data.Length + 8]; // 8 = sizeof(uint) * 2 + WriteSenderIdToBuffer(packet); + Array.Copy(BitConverter.GetBytes(receiverId), 0, packet, 4, sizeof(uint)); + Array.Copy(data, 0, packet, 8, data.Length); + + tunnelSocket.SendTo(packet, tunnelEndPoint); + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelHandler.cs new file mode 100644 index 000000000..0bf7cd2e8 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelHandler.cs @@ -0,0 +1,104 @@ +using Rampastring.Tools; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace DTAClient.Domain.Multiplayer.CnCNet +{ + class V3TunnelHandler + { + public string TunnelCacheFilePath { get; set; } + + private const string CNCNET_TUNNEL_LIST_URL = "http://cncnet.org/master-list"; + + /// + /// Downloads and parses the list of CnCNet tunnels. + /// + /// A list of tunnel servers. + private List RefreshTunnels() + { + List returnValue = new List(); + + WebClient client = new WebClient(); + + byte[] data; + + Logger.Log("Fetching tunnel server info."); + + try + { + data = client.DownloadData(CNCNET_TUNNEL_LIST_URL); + } + catch (WebException ex) + { + Logger.Log("Error when downloading tunnel server info: " + ex.Message); + Logger.Log("Retrying."); + try + { + data = client.DownloadData(CNCNET_TUNNEL_LIST_URL); + } + catch (WebException) + { + if (!File.Exists(TunnelCacheFilePath)) + { + Logger.Log("Tunnel cache file doesn't exist!"); + return returnValue; + } + else + { + Logger.Log("Fetching tunnel server list failed. Using cached tunnel data."); + data = File.ReadAllBytes(TunnelCacheFilePath); + } + } + } + + string convertedData = Encoding.Default.GetString(data); + + string[] serverList = convertedData.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string serverInfo in serverList) + { + try + { + CnCNetTunnel tunnel = CnCNetTunnel.Parse(serverInfo); + + if (tunnel == null) + continue; + + if (tunnel.RequiresPassword) + continue; + + if (tunnel.Version != Constants.TUNNEL_VERSION_2 && + tunnel.Version != Constants.TUNNEL_VERSION_3) + continue; + + returnValue.Add(tunnel); + } + catch + { + } + } + + if (returnValue.Count > 0) + { + try + { + if (File.Exists(TunnelCacheFilePath)) + File.Delete(TunnelCacheFilePath); + Directory.CreateDirectory(Path.GetDirectoryName(TunnelCacheFilePath)); + File.WriteAllBytes(TunnelCacheFilePath, data); + } + catch (Exception ex) + { + Logger.Log("Refreshing tunnel cache file failed! Returned error: " + ex.Message); + } + } + + return returnValue; + } + } +} From 8cd23182e09e359d520a88b3b3238e1d9006a506 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sun, 23 Jun 2019 14:56:54 +0300 Subject: [PATCH 002/109] Update TunnelHandler, add TunneledPlayerConnection --- .../Multiplayer/CnCNet/TunnelHandler.cs | 17 ++- .../CnCNet/TunneledPlayerConnection.cs | 102 +++++++++++++++++ .../Multiplayer/CnCNet/V3TunnelHandler.cs | 104 ------------------ 3 files changed, 113 insertions(+), 110 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs delete mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelHandler.cs diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 59b3d2f49..1a6ffa7dc 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -31,11 +31,15 @@ public class TunnelHandler : GameComponent private const int SUPPORTED_TUNNEL_VERSION = 2; - public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) : base(wm.Game) + private const string CNCNET_TUNNEL_LIST_URL = "http://cncnet.org/master-list"; + + public TunnelHandler(WindowManager wm, CnCNetManager connectionManager, string cacheFilePath) : base(wm.Game) { this.wm = wm; this.connectionManager = connectionManager; + TunnelCacheFilePath = cacheFilePath; + wm.Game.Components.Add(this); Enabled = false; @@ -161,17 +165,17 @@ private List RefreshTunnels() try { - data = client.DownloadData(MainClientConstants.CNCNET_TUNNEL_LIST_URL); + data = client.DownloadData(CNCNET_TUNNEL_LIST_URL); } - catch (Exception ex) + catch (WebException ex) { Logger.Log("Error when downloading tunnel server info: " + ex.Message); Logger.Log("Retrying."); try { - data = client.DownloadData(MainClientConstants.CNCNET_TUNNEL_LIST_URL); + data = client.DownloadData(CNCNET_TUNNEL_LIST_URL); } - catch + catch (WebException) { if (!tunnelCacheFile.Exists) { @@ -203,7 +207,8 @@ private List RefreshTunnels() if (tunnel.RequiresPassword) continue; - if (tunnel.Version != SUPPORTED_TUNNEL_VERSION) + if (tunnel.Version != Constants.TUNNEL_VERSION_2 && + tunnel.Version != Constants.TUNNEL_VERSION_3) continue; returnValue.Add(tunnel); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs new file mode 100644 index 000000000..5719f91fc --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace DTAClient.Domain.Multiplayer.CnCNet +{ + /// + /// Captures packets sent by an UDP client (the game) to a specific address + /// and allows forwarding messages back to it. + /// + public class TunneledPlayerConnection + { + private const int Timeout = 60000; + + public TunneledPlayerConnection(ulong playerId) + { + PlayerID = playerId; + } + + public delegate void PacketReceivedEventHandler(byte[] data); + public event PacketReceivedEventHandler PacketReceived; + + public int PortNumber { get; private set; } + public ulong PlayerID { get; private set; } + + private bool _aborted; + + private bool Aborted + { + get { lock (locker) return _aborted; } + set { lock (locker) _aborted = value; } + } + + private Socket socket; + private EndPoint endPoint; + + private readonly object locker = new object(); + + + public void Stop() + { + Aborted = true; + } + + /// + /// Creates a socket and sets the connection's port number. + /// + public void CreateSocket() + { + socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + endPoint = new IPEndPoint(IPAddress.Loopback, 0); + socket.Bind(endPoint); + + PortNumber = ((IPEndPoint)(socket.LocalEndPoint)).Port; + } + + public void Start() + { + Thread thread = new Thread(new ThreadStart(Run)); + thread.Start(); + } + + private void Run() + { + socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket.ReceiveTimeout = Timeout; + byte[] buffer = new byte[1024]; + + try + { + while (true) + { + if (Aborted) + break; + + int received = socket.ReceiveFrom(buffer, ref endPoint); + + byte[] data = new byte[received]; + Array.Copy(buffer, data, received); + Array.Clear(buffer, 0, received); + PacketReceived?.Invoke(data); + } + } + catch (SocketException) + { + // Timeout + } + + socket.Close(); + } + + public void SendPacket(byte[] packet) + { + socket.SendTo(packet, endPoint); + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelHandler.cs deleted file mode 100644 index 0bf7cd2e8..000000000 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelHandler.cs +++ /dev/null @@ -1,104 +0,0 @@ -using Rampastring.Tools; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; - -namespace DTAClient.Domain.Multiplayer.CnCNet -{ - class V3TunnelHandler - { - public string TunnelCacheFilePath { get; set; } - - private const string CNCNET_TUNNEL_LIST_URL = "http://cncnet.org/master-list"; - - /// - /// Downloads and parses the list of CnCNet tunnels. - /// - /// A list of tunnel servers. - private List RefreshTunnels() - { - List returnValue = new List(); - - WebClient client = new WebClient(); - - byte[] data; - - Logger.Log("Fetching tunnel server info."); - - try - { - data = client.DownloadData(CNCNET_TUNNEL_LIST_URL); - } - catch (WebException ex) - { - Logger.Log("Error when downloading tunnel server info: " + ex.Message); - Logger.Log("Retrying."); - try - { - data = client.DownloadData(CNCNET_TUNNEL_LIST_URL); - } - catch (WebException) - { - if (!File.Exists(TunnelCacheFilePath)) - { - Logger.Log("Tunnel cache file doesn't exist!"); - return returnValue; - } - else - { - Logger.Log("Fetching tunnel server list failed. Using cached tunnel data."); - data = File.ReadAllBytes(TunnelCacheFilePath); - } - } - } - - string convertedData = Encoding.Default.GetString(data); - - string[] serverList = convertedData.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); - - foreach (string serverInfo in serverList) - { - try - { - CnCNetTunnel tunnel = CnCNetTunnel.Parse(serverInfo); - - if (tunnel == null) - continue; - - if (tunnel.RequiresPassword) - continue; - - if (tunnel.Version != Constants.TUNNEL_VERSION_2 && - tunnel.Version != Constants.TUNNEL_VERSION_3) - continue; - - returnValue.Add(tunnel); - } - catch - { - } - } - - if (returnValue.Count > 0) - { - try - { - if (File.Exists(TunnelCacheFilePath)) - File.Delete(TunnelCacheFilePath); - Directory.CreateDirectory(Path.GetDirectoryName(TunnelCacheFilePath)); - File.WriteAllBytes(TunnelCacheFilePath, data); - } - catch (Exception ex) - { - Logger.Log("Refreshing tunnel cache file failed! Returned error: " + ex.Message); - } - } - - return returnValue; - } - } -} From f92f33f5f85aefb4d3ee21f2fc34101703079fb9 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sun, 23 Jun 2019 23:02:23 +0300 Subject: [PATCH 003/109] Add GameTunnelHandler --- .../Multiplayer/CnCNet/GameTunnelHandler.cs | 107 ++++++++++++++++++ .../CnCNet/TunneledPlayerConnection.cs | 9 +- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 50 ++++---- 3 files changed, 141 insertions(+), 25 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs new file mode 100644 index 000000000..1e2a8346f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DTAClient.Domain.Multiplayer.CnCNet +{ + class GameTunnelHandler + { + public GameTunnelHandler() + { + } + + public event EventHandler Connected; + public event EventHandler ConnectionFailed; + + private CnCNetTunnel tunnel; + private uint senderId; + + private V3TunnelConnection tunnelConnection; + private Dictionary playerConnections; + + public void SetUp(CnCNetTunnel tunnel, uint ourSenderId) + { + this.tunnel = tunnel; + this.senderId = ourSenderId; + + tunnelConnection = new V3TunnelConnection(tunnel, senderId); + tunnelConnection.Connected += TunnelConnection_Connected; + tunnelConnection.ConnectionFailed += TunnelConnection_ConnectionFailed; + tunnelConnection.ConnectionCut += TunnelConnection_ConnectionCut; + tunnelConnection.MessageReceived += TunnelConnection_MessageReceived; + } + + public int[] CreatePlayerConnections(List playerIds) + { + int[] ports = new int[playerIds.Count]; + playerConnections = new Dictionary(); + + for (int i = 0; i < playerIds.Count; i++) + { + var playerConnection = new TunneledPlayerConnection(playerIds[i]); + playerConnection.CreateSocket(); + ports[i] = playerConnection.PortNumber; + playerConnections.Add(playerIds[i], playerConnection); + playerConnection.PacketReceived += PlayerConnection_PacketReceived; + playerConnection.Start(); + } + + return ports; + } + + public void Clear() + { + foreach (var connection in playerConnections) + { + connection.Value.Stop(); + connection.Value.PacketReceived -= PlayerConnection_PacketReceived; + } + + playerConnections.Clear(); + ClearTunnelConnection(); + } + + private void PlayerConnection_PacketReceived(TunneledPlayerConnection sender, byte[] data) + { + tunnelConnection.SendData(data, sender.PlayerID); + } + + private void TunnelConnection_MessageReceived(byte[] data, uint senderId) + { + if (playerConnections.TryGetValue(senderId, out TunneledPlayerConnection connection)) + connection.SendPacket(data); + } + + private void TunnelConnection_Connected(object sender, EventArgs e) + { + Connected?.Invoke(this, EventArgs.Empty); + ClearTunnelConnection(); + } + + private void TunnelConnection_ConnectionFailed(object sender, EventArgs e) + { + ConnectionFailed?.Invoke(this, EventArgs.Empty); + ClearTunnelConnection(); + } + + private void TunnelConnection_ConnectionCut(object sender, EventArgs e) + { + ClearTunnelConnection(); + } + + private void ClearTunnelConnection() + { + if (tunnelConnection == null) + return; + + tunnelConnection.CloseConnection(); + tunnelConnection.Connected -= TunnelConnection_Connected; + tunnelConnection.ConnectionFailed -= TunnelConnection_ConnectionFailed; + tunnelConnection.ConnectionCut -= TunnelConnection_ConnectionCut; + tunnelConnection.MessageReceived -= TunnelConnection_MessageReceived; + tunnelConnection = null; + } + } +} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index 5719f91fc..9f3e7506c 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -17,19 +17,18 @@ public class TunneledPlayerConnection { private const int Timeout = 60000; - public TunneledPlayerConnection(ulong playerId) + public TunneledPlayerConnection(uint playerId) { PlayerID = playerId; } - public delegate void PacketReceivedEventHandler(byte[] data); + public delegate void PacketReceivedEventHandler(TunneledPlayerConnection sender, byte[] data); public event PacketReceivedEventHandler PacketReceived; public int PortNumber { get; private set; } - public ulong PlayerID { get; private set; } + public uint PlayerID { get; } private bool _aborted; - private bool Aborted { get { lock (locker) return _aborted; } @@ -83,7 +82,7 @@ private void Run() byte[] data = new byte[received]; Array.Copy(buffer, data, received); Array.Clear(buffer, 0, received); - PacketReceived?.Invoke(data); + PacketReceived?.Invoke(this, data); } } catch (SocketException) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index e123c19ac..e27fa6a6f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -30,6 +30,7 @@ public V3TunnelConnection(CnCNetTunnel tunnel, uint senderId) public event EventHandler Connected; public event EventHandler ConnectionFailed; + public event EventHandler ConnectionCut; public delegate void MessageDelegate(byte[] data, uint senderId); public event MessageDelegate MessageReceived; @@ -110,29 +111,38 @@ private void WriteSenderIdToBuffer(byte[] buffer) => private void ReceiveLoop() { - while (true) + try { - if (Aborted) - { - DoClose(); - Logger.Log("Exiting receive loop."); - return; - } - - byte[] buffer = new byte[1024]; - int size = tunnelSocket.ReceiveFrom(buffer, ref tunnelEndPoint); - - if (size < 8) + while (true) { - Logger.Log("Invalid data packet from tunnel server"); - continue; + if (Aborted) + { + DoClose(); + Logger.Log("Exiting receive loop."); + return; + } + + byte[] buffer = new byte[1024]; + int size = tunnelSocket.ReceiveFrom(buffer, ref tunnelEndPoint); + + if (size < 8) + { + Logger.Log("Invalid data packet from tunnel server"); + continue; + } + + byte[] data = new byte[size - 8]; + Array.Copy(buffer, 8, data, 0, data.Length); + uint senderId = BitConverter.ToUInt32(buffer, 0); + + MessageReceived?.Invoke(data, senderId); } - - byte[] data = new byte[size - 8]; - Array.Copy(buffer, 8, data, 0, data.Length); - uint senderId = BitConverter.ToUInt32(buffer, 0); - - MessageReceived?.Invoke(data, senderId); + } + catch (SocketException ex) + { + Logger.Log("Socket exception in V3 tunnel receive loop: " + ex.Message); + DoClose(); + ConnectionCut?.Invoke(this, EventArgs.Empty); } } From 0592c6c85d7637c896676ef81c5c06065865e648 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sun, 23 Jun 2019 23:44:02 +0300 Subject: [PATCH 004/109] Implement starting games with V3 tunnels, testing TODO --- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 4 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 260 ++++++++++++++++-- .../Multiplayer/CnCNet/GameTunnelHandler.cs | 11 +- 3 files changed, 246 insertions(+), 29 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 0e5448bb0..e3250be5b 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -907,7 +907,7 @@ private void _JoinGame(HostedCnCNetGame hg, string password) } else { - gameLobby.SetUp(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded); + gameLobby.SetUp(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded, false); gameChannel.UserAdded += GameChannel_UserAdded; gameChannel.InvalidPasswordEntered += GameChannel_InvalidPasswordEntered_NewGame; gameChannel.InviteOnlyErrorOnJoin += GameChannel_InviteOnlyErrorOnJoin; @@ -1019,7 +1019,7 @@ private void Gcw_GameCreated(object sender, GameCreationEventArgs e) Channel gameChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, password); connectionManager.AddChannel(gameChannel); - gameLobby.SetUp(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword); + gameLobby.SetUp(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword, false); gameChannel.UserAdded += GameChannel_UserAdded; //gameChannel.MessageAdded += GameChannel_MessageAdded; connectionManager.SendCustomMessage(new QueuedMessage("JOIN " + channelName + " " + password, diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index bb6371b9f..1dcaa7197 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -31,8 +31,12 @@ public class CnCNetGameLobby : MultiplayerGameLobby private const double GAME_BROADCAST_ACCELERATION = 10.0; private const double INITIAL_GAME_BROADCAST_DELAY = 10.0; + private const double MAX_TIME_FOR_GAME_LAUNCH = 20.0; + private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; + #region Commands + private const string MAP_SHARING_FAIL_MESSAGE = "MAPFAIL"; private const string MAP_SHARING_DOWNLOAD_REQUEST = "MAPOK"; private const string MAP_SHARING_UPLOAD_REQUEST = "MAPREQ"; @@ -40,6 +44,18 @@ public class CnCNetGameLobby : MultiplayerGameLobby private const string CHEAT_DETECTED_MESSAGE = "CD"; private const string DICE_ROLL_MESSAGE = "DR"; private const string CHANGE_TUNNEL_SERVER_MESSAGE = "CHTNL"; + private const string GAME_START_MESSAGE = "START"; + private const string GAME_START_MESSAGE_V3 = "STARTV3"; + private const string TUNNEL_CONNECTION_OK_MESSAGE = "TNLOK"; + private const string TUNNEL_CONNECTION_FAIL_MESSAGE = "TNLFAIL"; + + #endregion + + #region Priorities + + private const int PRIORITY_START_GAME = 10; + + #endregion public CnCNetGameLobby( WindowManager windowManager, @@ -66,7 +82,10 @@ DiscordHandler discordHandler new StringCommandHandler("PO", ApplyPlayerOptions), new StringCommandHandler(PlayerExtraOptions.CNCNET_MESSAGE_KEY, ApplyPlayerExtraOptions), new StringCommandHandler("GO", ApplyGameOptions), - new StringCommandHandler("START", NonHostLaunchGame), + new StringCommandHandler(GAME_START_MESSAGE, NonHostLaunchGame), + new StringCommandHandler(GAME_START_MESSAGE_V3, HandleGameStartV3TunnelMessage), + new NoParamCommandHandler(TUNNEL_CONNECTION_OK_MESSAGE, HandleTunnelConnected), + new NoParamCommandHandler(TUNNEL_CONNECTION_FAIL_MESSAGE, HandleTunnelFail), new NotificationHandler("AISPECS", HandleNotification, AISpectatorsNotification), new NotificationHandler("GETREADY", HandleNotification, GetReadyNotification), new NotificationHandler("INSFSPLRS", HandleNotification, InsufficientPlayersNotification), @@ -126,12 +145,19 @@ DiscordHandler discordHandler private IRCColor chatColor; private XNATimerControl gameBroadcastTimer; + private XNATimerControl gameStartTimer; private int playerLimit; private bool closed = false; private bool isCustomPassword = false; + private bool isP2P = false; + + private List tunnelPlayerIds = new List(); + private bool[] isPlayerConnectedToTunnel; + private GameTunnelHandler gameTunnelHandler; + private bool isStartingGame; private string gameFilesHash; @@ -168,6 +194,10 @@ public override void Initialize() IniNameOverride = nameof(CnCNetGameLobby); base.Initialize(); + gameTunnelHandler = new GameTunnelHandler(); + gameTunnelHandler.Connected += GameTunnelHandler_Connected; + gameTunnelHandler.ConnectionFailed += GameTunnelHandler_ConnectionFailed; + btnChangeTunnel = FindChild(nameof(btnChangeTunnel)); btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick; @@ -177,6 +207,12 @@ public override void Initialize() gameBroadcastTimer.Enabled = false; gameBroadcastTimer.TimeElapsed += GameBroadcastTimer_TimeElapsed; + gameStartTimer = new XNATimerControl(WindowManager); + gameStartTimer.AutoReset = false; + gameStartTimer.Interval = TimeSpan.FromSeconds(MAX_TIME_FOR_GAME_LAUNCH); + gameStartTimer.Enabled = false; + gameStartTimer.TimeElapsed += GameStartTimer_TimeElapsed; + tunnelSelectionWindow = new TunnelSelectionWindow(WindowManager, tunnelHandler); tunnelSelectionWindow.Initialize(); tunnelSelectionWindow.DrawOrder = 1; @@ -194,12 +230,18 @@ public override void Initialize() globalContextMenu = new GlobalContextMenu(WindowManager, connectionManager, cncnetUserData, pmWindow); AddChild(globalContextMenu); + AddChild(gameStartTimer); MultiplayerNameRightClicked += MultiplayerName_RightClick; PostInitialize(); } + private void GameStartTimer_TimeElapsed(object sender, EventArgs e) + { + AbortGameStart(); + } + private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClickedEventArgs args) { globalContextMenu.Show(new GlobalContextMenuData() @@ -214,7 +256,7 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGame(); public void SetUp(Channel channel, bool isHost, int playerLimit, - CnCNetTunnel tunnel, string hostName, bool isCustomPassword) + CnCNetTunnel tunnel, string hostName, bool isCustomPassword, bool isP2P) { this.channel = channel; channel.MessageAdded += Channel_MessageAdded; @@ -229,6 +271,7 @@ public void SetUp(Channel channel, bool isHost, int playerLimit, this.hostName = hostName; this.playerLimit = playerLimit; this.isCustomPassword = isCustomPassword; + this.isP2P = isP2P; if (isHost) { @@ -555,6 +598,8 @@ private void Channel_UserAdded(object sender, ChannelUserEventArgs e) private void RemovePlayer(string playerName) { + AbortGameStart(); + PlayerInfo pInfo = Players.Find(p => p.Name == playerName); if (pInfo != null) @@ -632,37 +677,27 @@ protected override void HostLaunchGame() { if (Players.Count > 1) { - AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); - - List playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(Players.Count); + if (isP2P) + throw new NotImplementedException("Peer-to-peer is not implemented yet."); - if (playerPorts.Count < Players.Count) + if (tunnel.Version == Constants.TUNNEL_VERSION_2) { - ShowTunnelSelectionWindow(("An error occured while contacting " + - "the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); - AddNotice(("An error occured while contacting the specified CnCNet " + - "tunnel server. Please try using a different tunnel server").L10N("Client:Main:ConnectTunnelError2") + " ", ERROR_MESSAGE_COLOR); - return; + StartGame_V2Tunnel(); } - - StringBuilder sb = new StringBuilder("START "); - sb.Append(UniqueGameID); - for (int pId = 0; pId < Players.Count; pId++) + else if (tunnel.Version == Constants.TUNNEL_VERSION_3) { - Players[pId].Port = playerPorts[pId]; - sb.Append(";"); - sb.Append(Players[pId].Name); - sb.Append(";"); - sb.Append("0.0.0.0:"); - sb.Append(playerPorts[pId]); + StartGame_V3Tunnel(); } - channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 10); - } - else - { - Logger.Log("One player MP -- starting!"); + else + { + throw new InvalidOperationException("Unknown tunnel server version!"); + } + + return; } + Logger.Log("One player MP -- starting!"); + Players.ForEach(pInfo => pInfo.IsInGame = true); CopyPlayerDataToUI(); @@ -671,6 +706,174 @@ protected override void HostLaunchGame() StartGame(); } + private void StartGame_V2Tunnel() + { + AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); + + List playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(Players.Count); + + if (playerPorts.Count < Players.Count) + { + ShowTunnelSelectionWindow(("An error occured while contacting " + + "the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); + AddNotice(("An error occured while contacting the specified CnCNet " + + "tunnel server. Please try using a different tunnel server " + + "(accessible by typing /CHANGETUNNEL in the chat box).").L10N("Client:Main:ConnectTunnelError2"), ERROR_MESSAGE_COLOR); + return; + } + + StringBuilder sb = new StringBuilder(GAME_START_MESSAGE + " "); + sb.Append(UniqueGameID); + for (int pId = 0; pId < Players.Count; pId++) + { + Players[pId].Port = playerPorts[pId]; + sb.Append(";"); + sb.Append(Players[pId].Name); + sb.Append(";"); + sb.Append("0.0.0.0:"); + sb.Append(playerPorts[pId]); + } + channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + + Players.ForEach(pInfo => pInfo.IsInGame = true); + + StartGame(); + } + + private void StartGame_V3Tunnel() + { + AddNotice("Contacting tunnel server.."); + btnLaunchGame.InputEnabled = false; + + Random random = new Random(); + uint randomNumber = (uint)random.Next(0, int.MaxValue - (MAX_PLAYER_COUNT / 2)) * (uint)random.Next(1, 3); + + StringBuilder sb = new StringBuilder(GAME_START_MESSAGE_V3 + " "); + sb.Append(UniqueGameID); + tunnelPlayerIds.Clear(); + for (int i = 0; i < Players.Count; i++) + { + uint id = randomNumber + (uint)i; + sb.Append(";"); + sb.Append(id); + tunnelPlayerIds.Add(id); + } + channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + isStartingGame = true; + + ContactTunnel(); + } + + private void HandleGameStartV3TunnelMessage(string sender, string message) + { + if (sender != hostName) + return; + + string[] parts = message.Split(';'); + + if (parts.Length != Players.Count + 1) + return; + + UniqueGameID = Conversions.IntFromString(parts[0], -1); + if (UniqueGameID < 0) + return; + + tunnelPlayerIds.Clear(); + + for (int i = 1; i < parts.Length; i++) + { + if (!uint.TryParse(parts[i], out uint id)) + return; + + tunnelPlayerIds.Add(id); + } + + isStartingGame = true; + ContactTunnel(); + } + + private void ContactTunnel() + { + isPlayerConnectedToTunnel = new bool[Players.Count]; + gameTunnelHandler.SetUp(tunnel, + tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); + gameTunnelHandler.ConnectToTunnel(); + // Abort starting the game if not everyone + // replies within the timer's limit + gameStartTimer.Start(); + } + + private void GameTunnelHandler_Connected(object sender, EventArgs e) + { + isPlayerConnectedToTunnel[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)] = true; + channel.SendCTCPMessage(TUNNEL_CONNECTION_OK_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + } + + private void GameTunnelHandler_ConnectionFailed(object sender, EventArgs e) + { + channel.SendCTCPMessage(TUNNEL_CONNECTION_FAIL_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); + HandleTunnelFail(ProgramConstants.PLAYERNAME); + } + + private void HandleTunnelFail(string playerName) + { + Logger.Log(playerName + " failed to connect to tunnel - aborting game launch."); + AddNotice(playerName + " failed to connect to the tunnel server. Please " + + "retry or pick another tunnel server by type /CHANGETUNNEL to the chat input box."); + AbortGameStart(); + } + + private void HandleTunnelConnected(string playerName) + { + if (!isStartingGame) + return; + + int index = Players.FindIndex(p => p.Name == playerName); + if (index == -1) + { + Logger.Log("HandleTunnelConnected: Couldn't find player " + playerName + "!"); + AbortGameStart(); + return; + } + + isPlayerConnectedToTunnel[index] = true; + + if (isPlayerConnectedToTunnel.All(b => b)) + { + Logger.Log("All players are connected to the tunnel, starting game!"); + AddNotice("All players have connected to the tunnel..."); + + // Remove our own ID from the list + List ids = new List(tunnelPlayerIds); + ids.Remove(tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); + int[] ports = gameTunnelHandler.CreatePlayerConnections(ids); + for (int i = 0; i < ports.Length; i++) + { + Players[i].Port = ports[i]; + } + btnLaunchGame.InputEnabled = true; + StartGame(); + } + } + + private void AbortGameStart() + { + btnLaunchGame.InputEnabled = true; + gameTunnelHandler.Clear(); + isStartingGame = false; + } + + protected override string GetIPAddressForPlayer(PlayerInfo player) + { + if (isP2P) + return player.IPAddress; + + if (tunnel.Version == Constants.TUNNEL_VERSION_3) + return "127.0.0.1"; + + return base.GetIPAddressForPlayer(player); + } + protected override void RequestPlayerOptions(int side, int color, int start, int team) { byte[] value = new byte[] @@ -1245,6 +1448,9 @@ protected override void GameProcessExited() /// private void NonHostLaunchGame(string sender, string message) { + if (tunnel.Version != Constants.TUNNEL_VERSION_2) + return; + if (sender != hostName) return; @@ -1293,6 +1499,8 @@ protected override void StartGame() { AddNotice("Starting game...".L10N("Client:Main:StartingGame")); + isStartingGame = false; + FileHashCalculator fhc = new FileHashCalculator(); fhc.CalculateHashes(GameModeMaps.GameModes); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs index 1e2a8346f..51625a7fb 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs @@ -19,7 +19,8 @@ public GameTunnelHandler() private uint senderId; private V3TunnelConnection tunnelConnection; - private Dictionary playerConnections; + private Dictionary playerConnections = + new Dictionary(); public void SetUp(CnCNetTunnel tunnel, uint ourSenderId) { @@ -33,6 +34,14 @@ public void SetUp(CnCNetTunnel tunnel, uint ourSenderId) tunnelConnection.MessageReceived += TunnelConnection_MessageReceived; } + public void ConnectToTunnel() + { + if (tunnelConnection == null) + throw new InvalidOperationException("GameTunnelHandler: Call SetUp before calling ConnectToTunnel."); + + tunnelConnection.ConnectAsync(); + } + public int[] CreatePlayerConnections(List playerIds) { int[] ports = new int[playerIds.Count]; From 5e00f3adfcb6e5d4ae95ec7cd6fa4421c9dbe59f Mon Sep 17 00:00:00 2001 From: Rampastring Date: Mon, 24 Jun 2019 23:26:03 +0300 Subject: [PATCH 005/109] Fix CTCP string command handler not properly checking that the message matches the command --- .../GameLobby/CommandHandlers/StringCommandHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/StringCommandHandler.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/StringCommandHandler.cs index e1403ab15..b59f16828 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/StringCommandHandler.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/StringCommandHandler.cs @@ -16,7 +16,7 @@ public override bool Handle(string sender, string message) if (message.Length < CommandName.Length + 1) return false; - if (message.StartsWith(CommandName)) + if (message.StartsWith(CommandName + " ")) { string parameters = message.Substring(CommandName.Length + 1); From 3156a68ac25b83fd06e3fdb3bdf19109063714c6 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Mon, 24 Jun 2019 23:27:28 +0300 Subject: [PATCH 006/109] Fix bugs related to starting games with v3 tunnel connections, send the tunnel port alongside the tunnel address to non-host players --- ClientCore/ProgramConstants.cs | 2 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 30 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index feb207172..45f5d88b1 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -32,7 +32,7 @@ public static class ProgramConstants public const string QRES_EXECUTABLE = "qres.dat"; - public const string CNCNET_PROTOCOL_REVISION = "R10"; + public const string CNCNET_PROTOCOL_REVISION = "R11"; public const string LAN_PROTOCOL_REVISION = "RL7"; public const int LAN_PORT = 1234; public const int LAN_INGAME_PORT = 1234; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 1dcaa7197..856c05127 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -210,7 +210,6 @@ public override void Initialize() gameStartTimer = new XNATimerControl(WindowManager); gameStartTimer.AutoReset = false; gameStartTimer.Interval = TimeSpan.FromSeconds(MAX_TIME_FOR_GAME_LAUNCH); - gameStartTimer.Enabled = false; gameStartTimer.TimeElapsed += GameStartTimer_TimeElapsed; tunnelSelectionWindow = new TunnelSelectionWindow(WindowManager, tunnelHandler); @@ -239,6 +238,22 @@ public override void Initialize() private void GameStartTimer_TimeElapsed(object sender, EventArgs e) { + string playerString = ""; + + for (int i = 0; i < Players.Count; i++) + { + if (!isPlayerConnectedToTunnel[i]) + { + if (playerString == "") + playerString = Players[i].Name; + else + playerString += ", " + Players[i].Name; + } + } + + AddNotice($"Some players ({playerString}) failed to connect within the time limit. " + + $"Aborting game launch."); + AbortGameStart(); } @@ -742,7 +757,6 @@ private void StartGame_V2Tunnel() private void StartGame_V3Tunnel() { - AddNotice("Contacting tunnel server.."); btnLaunchGame.InputEnabled = false; Random random = new Random(); @@ -794,6 +808,7 @@ private void HandleGameStartV3TunnelMessage(string sender, string message) private void ContactTunnel() { + AddNotice("Contacting tunnel server.."); isPlayerConnectedToTunnel = new bool[Players.Count]; gameTunnelHandler.SetUp(tunnel, tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); @@ -804,12 +819,22 @@ private void ContactTunnel() } private void GameTunnelHandler_Connected(object sender, EventArgs e) + { + AddCallback(new Action(GameTunnelHandler_Connected_Callback), null); + } + + private void GameTunnelHandler_Connected_Callback() { isPlayerConnectedToTunnel[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)] = true; channel.SendCTCPMessage(TUNNEL_CONNECTION_OK_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); } private void GameTunnelHandler_ConnectionFailed(object sender, EventArgs e) + { + AddCallback(new Action(GameTunnelHandler_ConnectionFailed_Callback), null); + } + + private void GameTunnelHandler_ConnectionFailed_Callback() { channel.SendCTCPMessage(TUNNEL_CONNECTION_FAIL_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); HandleTunnelFail(ProgramConstants.PLAYERNAME); @@ -860,6 +885,7 @@ private void AbortGameStart() { btnLaunchGame.InputEnabled = true; gameTunnelHandler.Clear(); + gameStartTimer.Pause(); isStartingGame = false; } From 82eecd148f37bd81d05eaaa77e4b14bd94937708 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Mon, 24 Jun 2019 23:29:55 +0300 Subject: [PATCH 007/109] Improve format of "total time played" and "average game length" in the statistics window --- DXMainClient/DXGUI/Generic/StatisticsWindow.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/DXMainClient/DXGUI/Generic/StatisticsWindow.cs b/DXMainClient/DXGUI/Generic/StatisticsWindow.cs index 88abe541d..d41835426 100644 --- a/DXMainClient/DXGUI/Generic/StatisticsWindow.cs +++ b/DXMainClient/DXGUI/Generic/StatisticsWindow.cs @@ -922,7 +922,7 @@ private void SetTotalStatistics() if (gamesStarted > 0) { - lblAverageGameLengthValue.Text = TimeSpan.FromSeconds((int)timePlayed.TotalSeconds / gamesStarted).ToString(); + lblAverageGameLengthValue.Text = TimeSpanToString(TimeSpan.FromSeconds((int)timePlayed.TotalSeconds / gamesStarted)); } else lblAverageGameLengthValue.Text = "-"; @@ -951,7 +951,7 @@ private void SetTotalStatistics() else lblKillLossRatioValue.Text = "-"; - lblTotalTimePlayedValue.Text = timePlayed.ToString(); + lblTotalTimePlayedValue.Text = TimeSpanToString(timePlayed); lblTotalKillsValue.Text = totalKills.ToString(); lblTotalLossesValue.Text = totalLosses.ToString(); lblTotalScoreValue.Text = totalScore.ToString(); @@ -965,6 +965,13 @@ private void SetTotalStatistics() lblAverageAILevelValue.Text = "Hard".L10N("Client:Main:HardAI"); } + private string TimeSpanToString(TimeSpan timeSpan) + { + return timeSpan.Days > 0 ? + $"{timeSpan.Days} d {timeSpan.Hours} h {timeSpan.Minutes} m {timeSpan.Seconds} s" : + $"{timeSpan.Hours} h {timeSpan.Minutes} m {timeSpan.Seconds} s"; + } + private PlayerStatistics FindLocalPlayer(MatchStatistics ms) { int pCount = ms.GetPlayerCount(); From 96aec4956c087b66084f23c5f583aa41f77450de Mon Sep 17 00:00:00 2001 From: Rampastring Date: Tue, 25 Jun 2019 20:31:49 +0300 Subject: [PATCH 008/109] Fix a bunch of issues related to starting games using v3 tunnels --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 12 +++- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 3 + .../Multiplayer/CnCNet/GameTunnelHandler.cs | 56 ++++++++++--------- .../CnCNet/TunneledPlayerConnection.cs | 15 +++-- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 8 ++- 5 files changed, 61 insertions(+), 33 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 856c05127..92ec4b084 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -871,11 +871,14 @@ private void HandleTunnelConnected(string playerName) // Remove our own ID from the list List ids = new List(tunnelPlayerIds); ids.Remove(tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); + List players = new List(Players); + players.RemoveAt(Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)); int[] ports = gameTunnelHandler.CreatePlayerConnections(ids); for (int i = 0; i < ports.Length; i++) { - Players[i].Port = ports[i]; + players[i].Port = ports[i]; } + gameStartTimer.Pause(); btnLaunchGame.InputEnabled = true; StartGame(); } @@ -1544,8 +1547,11 @@ protected override void WriteSpawnIniAdditions(IniFile iniFile) { base.WriteSpawnIniAdditions(iniFile); - iniFile.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); - iniFile.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); + if (!isP2P && tunnel.Version == Constants.TUNNEL_VERSION_2) + { + iniFile.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); + iniFile.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); + } iniFile.SetIntValue("Settings", "GameID", UniqueGameID); iniFile.SetBooleanValue("Settings", "Host", IsHost); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 6d80d5df1..704b3a98f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -172,6 +172,9 @@ public void PingAsync() public void Ping() { + PingV2(); // temporary hack until all v3 tunnels support pinging + return; + if (Version == Constants.TUNNEL_VERSION_2) { PingV2(); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs index 51625a7fb..50c9766cb 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs @@ -22,6 +22,8 @@ public GameTunnelHandler() private Dictionary playerConnections = new Dictionary(); + private readonly object locker = new object(); + public void SetUp(CnCNetTunnel tunnel, uint ourSenderId) { this.tunnel = tunnel; @@ -62,55 +64,59 @@ public int[] CreatePlayerConnections(List playerIds) public void Clear() { - foreach (var connection in playerConnections) + lock (locker) { - connection.Value.Stop(); - connection.Value.PacketReceived -= PlayerConnection_PacketReceived; + foreach (var connection in playerConnections) + { + connection.Value.Stop(); + connection.Value.PacketReceived -= PlayerConnection_PacketReceived; + } + + playerConnections.Clear(); + + if (tunnelConnection == null) + return; + + tunnelConnection.CloseConnection(); + tunnelConnection.Connected -= TunnelConnection_Connected; + tunnelConnection.ConnectionFailed -= TunnelConnection_ConnectionFailed; + tunnelConnection.ConnectionCut -= TunnelConnection_ConnectionCut; + tunnelConnection.MessageReceived -= TunnelConnection_MessageReceived; + tunnelConnection = null; } - - playerConnections.Clear(); - ClearTunnelConnection(); } private void PlayerConnection_PacketReceived(TunneledPlayerConnection sender, byte[] data) { - tunnelConnection.SendData(data, sender.PlayerID); + lock (locker) + { + tunnelConnection.SendData(data, sender.PlayerID); + } } private void TunnelConnection_MessageReceived(byte[] data, uint senderId) { - if (playerConnections.TryGetValue(senderId, out TunneledPlayerConnection connection)) - connection.SendPacket(data); + lock (locker) + { + if (playerConnections.TryGetValue(senderId, out TunneledPlayerConnection connection)) + connection.SendPacket(data); + } } private void TunnelConnection_Connected(object sender, EventArgs e) { Connected?.Invoke(this, EventArgs.Empty); - ClearTunnelConnection(); } private void TunnelConnection_ConnectionFailed(object sender, EventArgs e) { ConnectionFailed?.Invoke(this, EventArgs.Empty); - ClearTunnelConnection(); + Clear(); } private void TunnelConnection_ConnectionCut(object sender, EventArgs e) { - ClearTunnelConnection(); - } - - private void ClearTunnelConnection() - { - if (tunnelConnection == null) - return; - - tunnelConnection.CloseConnection(); - tunnelConnection.Connected -= TunnelConnection_Connected; - tunnelConnection.ConnectionFailed -= TunnelConnection_ConnectionFailed; - tunnelConnection.ConnectionCut -= TunnelConnection_ConnectionCut; - tunnelConnection.MessageReceived -= TunnelConnection_MessageReceived; - tunnelConnection = null; + Clear(); } } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index 9f3e7506c..1129d9c2b 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -66,7 +66,6 @@ public void Start() private void Run() { - socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); socket.ReceiveTimeout = Timeout; byte[] buffer = new byte[1024]; @@ -89,13 +88,21 @@ private void Run() { // Timeout } - - socket.Close(); + + lock (locker) + { + _aborted = true; + socket.Close(); + } } public void SendPacket(byte[] packet) { - socket.SendTo(packet, endPoint); + lock (locker) + { + if (!_aborted) + socket.SendTo(packet, endPoint); + } } } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index e27fa6a6f..218de9968 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -154,6 +154,8 @@ public void CloseConnection() private void DoClose() { + Aborted = true; + if (tunnelSocket != null) { tunnelSocket.Close(); @@ -171,7 +173,11 @@ public void SendData(byte[] data, uint receiverId) Array.Copy(BitConverter.GetBytes(receiverId), 0, packet, 4, sizeof(uint)); Array.Copy(data, 0, packet, 8, data.Length); - tunnelSocket.SendTo(packet, tunnelEndPoint); + lock (locker) + { + if (!aborted) + tunnelSocket.SendTo(packet, tunnelEndPoint); + } } } } From 7f4ef8356401e6022a3e835f0b06e8c88192ab5d Mon Sep 17 00:00:00 2001 From: Rampastring Date: Tue, 25 Jun 2019 21:23:48 +0300 Subject: [PATCH 009/109] Fix more v3 tunnel issues --- DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs | 6 +++++- DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 92ec4b084..0e9458800 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -24,6 +24,8 @@ namespace DTAClient.DXGUI.Multiplayer.GameLobby { public class CnCNetGameLobby : MultiplayerGameLobby { + private const int INGAME_PORT = 1234; + private const int HUMAN_PLAYER_OPTIONS_LENGTH = 3; private const int AI_PLAYER_OPTIONS_LENGTH = 2; @@ -872,7 +874,9 @@ private void HandleTunnelConnected(string playerName) List ids = new List(tunnelPlayerIds); ids.Remove(tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); List players = new List(Players); - players.RemoveAt(Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)); + int myIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); + Players[myIndex].Port = INGAME_PORT; + players.RemoveAt(myIndex); int[] ports = gameTunnelHandler.CreatePlayerConnections(ids); for (int i = 0; i < ports.Length; i++) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs index 50c9766cb..121ef2b5e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs @@ -90,7 +90,8 @@ private void PlayerConnection_PacketReceived(TunneledPlayerConnection sender, by { lock (locker) { - tunnelConnection.SendData(data, sender.PlayerID); + if (tunnelConnection != null) + tunnelConnection.SendData(data, sender.PlayerID); } } From 496f54681d9f2d9b699fb5f256acf66d3a1dc88c Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 14 Aug 2022 16:03:51 +0200 Subject: [PATCH 010/109] Rebase fix --- .../DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs | 12 ++++++------ .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 1 - .../Domain/Multiplayer/CnCNet/TunnelHandler.cs | 4 +--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 0e9458800..fbe9ec9c6 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -697,11 +697,11 @@ protected override void HostLaunchGame() if (isP2P) throw new NotImplementedException("Peer-to-peer is not implemented yet."); - if (tunnel.Version == Constants.TUNNEL_VERSION_2) + if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) { StartGame_V2Tunnel(); } - else if (tunnel.Version == Constants.TUNNEL_VERSION_3) + else if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) { StartGame_V3Tunnel(); } @@ -812,7 +812,7 @@ private void ContactTunnel() { AddNotice("Contacting tunnel server.."); isPlayerConnectedToTunnel = new bool[Players.Count]; - gameTunnelHandler.SetUp(tunnel, + gameTunnelHandler.SetUp(tunnelHandler.CurrentTunnel, tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); gameTunnelHandler.ConnectToTunnel(); // Abort starting the game if not everyone @@ -901,7 +901,7 @@ protected override string GetIPAddressForPlayer(PlayerInfo player) if (isP2P) return player.IPAddress; - if (tunnel.Version == Constants.TUNNEL_VERSION_3) + if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) return "127.0.0.1"; return base.GetIPAddressForPlayer(player); @@ -1481,7 +1481,7 @@ protected override void GameProcessExited() /// private void NonHostLaunchGame(string sender, string message) { - if (tunnel.Version != Constants.TUNNEL_VERSION_2) + if (tunnelHandler.CurrentTunnel.Version != Constants.TUNNEL_VERSION_2) return; if (sender != hostName) @@ -1551,7 +1551,7 @@ protected override void WriteSpawnIniAdditions(IniFile iniFile) { base.WriteSpawnIniAdditions(iniFile); - if (!isP2P && tunnel.Version == Constants.TUNNEL_VERSION_2) + if (!isP2P && tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) { iniFile.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); iniFile.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 704b3a98f..237b8e5d6 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -86,7 +86,6 @@ public string Address public IPAddress IPAddress { get; private set; } - public string Address { get; private set; } public int Port { get; private set; } public string Country { get; private set; } public string CountryCode { get; private set; } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 1a6ffa7dc..2adb4be9f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -33,13 +33,11 @@ public class TunnelHandler : GameComponent private const string CNCNET_TUNNEL_LIST_URL = "http://cncnet.org/master-list"; - public TunnelHandler(WindowManager wm, CnCNetManager connectionManager, string cacheFilePath) : base(wm.Game) + public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) : base(wm.Game) { this.wm = wm; this.connectionManager = connectionManager; - TunnelCacheFilePath = cacheFilePath; - wm.Game.Components.Add(this); Enabled = false; From 8e43728b5af15df578707eac725f5a1f8f525250 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 14 Aug 2022 19:34:23 +0200 Subject: [PATCH 011/109] Rebase cleanup, remove V3 tunnel login logic --- .../CnCNet/CnCNetGameLoadingLobby.cs | 4 +- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 2 +- .../CnCNet/PasswordRequestWindow.cs | 6 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 11 +- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 142 ++++-------------- .../Domain/Multiplayer/CnCNet/Constants.cs | 4 +- .../Multiplayer/CnCNet/GameTunnelHandler.cs | 20 +-- .../Multiplayer/CnCNet/HostedCnCNetGame.cs | 12 +- .../Multiplayer/CnCNet/TunnelHandler.cs | 32 ++-- .../CnCNet/TunneledPlayerConnection.cs | 16 +- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 63 +------- 11 files changed, 78 insertions(+), 234 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index ab9a68a6f..f34bfd801 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -22,7 +22,7 @@ namespace DTAClient.DXGUI.Multiplayer.CnCNet /// /// A game lobby for loading saved CnCNet games. /// - public class CnCNetGameLoadingLobby : GameLoadingLobbyBase + internal sealed class CnCNetGameLoadingLobby : GameLoadingLobbyBase { private const double GAME_BROADCAST_INTERVAL = 20.0; private const double INITIAL_GAME_BROADCAST_DELAY = 10.0; @@ -722,4 +722,4 @@ protected override void UpdateDiscordPresence(bool resetTimer = false) channel.UIName, IsHost, resetTimer); } } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index e3250be5b..2ca63e973 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -30,7 +30,7 @@ namespace DTAClient.DXGUI.Multiplayer.CnCNet using UserChannelPair = Tuple; using InvitationIndex = Dictionary, WeakReference>; - internal class CnCNetLobby : XNAWindow, ISwitchable + internal sealed class CnCNetLobby : XNAWindow, ISwitchable { public event EventHandler UpdateCheck; diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/PasswordRequestWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/PasswordRequestWindow.cs index 946a7bf37..76ebac053 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PasswordRequestWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PasswordRequestWindow.cs @@ -81,8 +81,8 @@ private void PasswordRequestWindow_EnabledChanged(object sender, EventArgs e) if (!privateMessagingWindow.Enabled) return; pmWindowWasEnabled = true; privateMessagingWindow.Disable(); - } - else if(pmWindowWasEnabled) + } + else if (pmWindowWasEnabled) { privateMessagingWindow.Enable(); } @@ -111,7 +111,7 @@ public void SetHostedGame(HostedCnCNetGame hostedGame) } } - public class PasswordEventArgs : EventArgs + internal sealed class PasswordEventArgs : EventArgs { public PasswordEventArgs(string password, HostedCnCNetGame hostedGame) { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index fbe9ec9c6..6ad2d3526 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -22,7 +22,7 @@ namespace DTAClient.DXGUI.Multiplayer.GameLobby { - public class CnCNetGameLobby : MultiplayerGameLobby + internal sealed class CnCNetGameLobby : MultiplayerGameLobby { private const int INGAME_PORT = 1234; @@ -250,7 +250,7 @@ private void GameStartTimer_TimeElapsed(object sender, EventArgs e) playerString = Players[i].Name; else playerString += ", " + Players[i].Name; - } + } } AddNotice($"Some players ({playerString}) failed to connect within the time limit. " + @@ -697,6 +697,8 @@ protected override void HostLaunchGame() if (isP2P) throw new NotImplementedException("Peer-to-peer is not implemented yet."); + AddNotice("Contacting tunnel server...".L10N("UI:Main:ConnectingTunnel")); + if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) { StartGame_V2Tunnel(); @@ -725,8 +727,6 @@ protected override void HostLaunchGame() private void StartGame_V2Tunnel() { - AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); - List playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(Players.Count); if (playerPorts.Count < Players.Count) @@ -810,9 +810,8 @@ private void HandleGameStartV3TunnelMessage(string sender, string message) private void ContactTunnel() { - AddNotice("Contacting tunnel server.."); isPlayerConnectedToTunnel = new bool[Players.Count]; - gameTunnelHandler.SetUp(tunnelHandler.CurrentTunnel, + gameTunnelHandler.SetUp(tunnelHandler.CurrentTunnel, tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); gameTunnelHandler.ConnectToTunnel(); // Abort starting the game if not everyone diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 237b8e5d6..a8c664df7 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -3,23 +3,19 @@ using System.Collections.Generic; using System.Globalization; using System.Net; -using System.Net.NetworkInformation; using System.Net.Sockets; -using System.Threading; namespace DTAClient.Domain.Multiplayer.CnCNet { /// /// A CnCNet tunnel server. /// - public class CnCNetTunnel + internal sealed class CnCNetTunnel { - private const int VERSION_3_PING_PACKET_SEND_SIZE = 800; - private const int VERSION_3_PING_PACKET_RECEIVE_SIZE = 12; + private const int PING_PACKET_SEND_SIZE = 50; + private const int PING_PACKET_RECEIVE_SIZE = 12; private const int PING_TIMEOUT = 1000; - public CnCNetTunnel() { } - /// /// Parses a formatted string that contains the tunnel server's /// information into a CnCNetTunnel instance. @@ -36,27 +32,26 @@ public static CnCNetTunnel Parse(string str) string[] parts = str.Split(';'); string address = parts[0]; - string[] detailedAddress = address.Split(new char[] { ':' }); - + string[] detailedAddress = address.Split(':'); + int version = int.Parse(parts[10]); + tunnel.Address = detailedAddress[0]; tunnel.Port = int.Parse(detailedAddress[1]); tunnel.Country = parts[1]; tunnel.CountryCode = parts[2]; - tunnel.Name = parts[3]; + tunnel.Name = parts[3] + " V" + version; tunnel.RequiresPassword = parts[4] != "0"; - tunnel.Clients = int.Parse(parts[5]); - tunnel.MaxClients = int.Parse(parts[6]); - int status = int.Parse(parts[7]); + tunnel.Clients = int.Parse(parts[5], CultureInfo.InvariantCulture); + tunnel.MaxClients = int.Parse(parts[6], CultureInfo.InvariantCulture); + int status = int.Parse(parts[7], CultureInfo.InvariantCulture); tunnel.Official = status == 2; if (!tunnel.Official) tunnel.Recommended = status == 1; - CultureInfo cultureInfo = CultureInfo.InvariantCulture; - - tunnel.Latitude = double.Parse(parts[8], cultureInfo); - tunnel.Longitude = double.Parse(parts[9], cultureInfo); - tunnel.Version = int.Parse(parts[10]); - tunnel.Distance = double.Parse(parts[11], cultureInfo); + tunnel.Latitude = double.Parse(parts[8], CultureInfo.InvariantCulture); + tunnel.Longitude = double.Parse(parts[9], CultureInfo.InvariantCulture); + tunnel.Version = version; + tunnel.Distance = double.Parse(parts[11], CultureInfo.InvariantCulture); return tunnel; } @@ -148,109 +143,32 @@ public List GetPlayerPortInfo(int playerCount) public void UpdatePing() { - using (Ping p = new Ping()) - { - try - { - PingReply reply = p.Send(IPAddress.Parse(Address), PING_TIMEOUT); - if (reply.Status == IPStatus.Success) - PingInMs = Convert.ToInt32(reply.RoundtripTime); - } - catch (PingException ex) - { - Logger.Log($"Caught an exception when pinging {Name} tunnel server: {ex.Message}"); - } - } - } - - public void PingAsync() - { - Thread thread = new Thread(new ThreadStart(Ping)); - thread.Start(); - } + using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - public void Ping() - { - PingV2(); // temporary hack until all v3 tunnels support pinging - return; + socket.SendTimeout = PING_TIMEOUT; + socket.ReceiveTimeout = PING_TIMEOUT; - if (Version == Constants.TUNNEL_VERSION_2) - { - PingV2(); - } - else if (Version == Constants.TUNNEL_VERSION_3) - { - PingV3(); - } - } - - private void PingV2() - { - int pingInMs = -1; - Ping p = new Ping(); try { - PingReply reply = p.Send(IPAddress.Parse(Address), PING_TIMEOUT); - if (reply.Status == IPStatus.Success) - { - if (reply.RoundtripTime > 0) - pingInMs = Convert.ToInt32(reply.RoundtripTime); - } - } - catch { } - - if (pingInMs > -1) - PingInMs = pingInMs; + byte[] buffer = new byte[PING_PACKET_SEND_SIZE]; + EndPoint ep = new IPEndPoint(IPAddress, Port); + long ticks = DateTime.Now.Ticks; + socket.SendTo(buffer, ep); - Pinged?.Invoke(this, EventArgs.Empty); - } + buffer = new byte[PING_PACKET_RECEIVE_SIZE]; + socket.ReceiveFrom(buffer, ref ep); - private void PingV3() - { - using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)) + ticks = DateTime.Now.Ticks - ticks; + PingInMs = new TimeSpan(ticks).Milliseconds; + } + catch (SocketException ex) { - socket.SendTimeout = PING_TIMEOUT; - socket.ReceiveTimeout = PING_TIMEOUT; - - try - { - byte[] buffer = new byte[VERSION_3_PING_PACKET_SEND_SIZE]; - EndPoint ep = new IPEndPoint(IPAddress, Port); - long ticks = DateTime.Now.Ticks; - socket.SendTo(buffer, ep); - - buffer = new byte[VERSION_3_PING_PACKET_RECEIVE_SIZE]; - socket.ReceiveFrom(buffer, ref ep); - - ticks = DateTime.Now.Ticks - ticks; - PingInMs = new TimeSpan(ticks).Milliseconds; - } - catch (SocketException ex) - { - Logger.Log($"Failed to ping V3 tunnel {Name} ({Address}:{Port}). Message: {ex.Message}"); + Logger.Log($"Failed to ping tunnel {Name} ({Address}:{Port}). Message: {ex.Message}"); - PingInMs = -1; - } + PingInMs = -1; } Pinged?.Invoke(this, EventArgs.Empty); } - - /// - /// Returns a bool that tells if the tunnel server has passed - /// initial connection checks and is available for online games. - /// - public bool IsAvailable() - { - return true; // temporary hack until all v3 tunnels support pinging - - if (Version == Constants.TUNNEL_VERSION_2) - return true; - - if (Version == Constants.TUNNEL_VERSION_3) - return PingInMs > -1; - - return false; - } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs index 178041d73..c38002d12 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs @@ -1,6 +1,6 @@ namespace DTAClient.Domain.Multiplayer.CnCNet { - class Constants + internal static class Constants { internal const int TUNNEL_CONNECTION_TIMEOUT = 10000; // In milliseconds internal const int TUNNEL_RECEIVE_TIMEOUT = 30000; @@ -8,4 +8,4 @@ class Constants internal const int TUNNEL_VERSION_2 = 2; internal const int TUNNEL_VERSION_3 = 3; } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs index 121ef2b5e..edbe279ae 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs @@ -1,33 +1,23 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace DTAClient.Domain.Multiplayer.CnCNet { - class GameTunnelHandler + internal sealed class GameTunnelHandler { - public GameTunnelHandler() - { - } - public event EventHandler Connected; public event EventHandler ConnectionFailed; - private CnCNetTunnel tunnel; private uint senderId; private V3TunnelConnection tunnelConnection; - private Dictionary playerConnections = - new Dictionary(); + private Dictionary playerConnections = new(); - private readonly object locker = new object(); + private readonly object locker = new(); public void SetUp(CnCNetTunnel tunnel, uint ourSenderId) { - this.tunnel = tunnel; - this.senderId = ourSenderId; + senderId = ourSenderId; tunnelConnection = new V3TunnelConnection(tunnel, senderId); tunnelConnection.Connected += TunnelConnection_Connected; @@ -120,4 +110,4 @@ private void TunnelConnection_ConnectionCut(object sender, EventArgs e) Clear(); } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs b/DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs index f97aff68d..cdace2f6a 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs @@ -2,14 +2,10 @@ namespace DTAClient.Domain.Multiplayer.CnCNet { - public class HostedCnCNetGame : GenericHostedGame + internal sealed class HostedCnCNetGame : GenericHostedGame { - public HostedCnCNetGame() { } - - public HostedCnCNetGame(string channelName, string revision, string gamever, int maxPlayers, - string roomName, bool passworded, - bool tunneled, - string[] players, string adminName, string mapName, string gameMode) + public HostedCnCNetGame(string channelName, string revision, string gamever, int maxPlayers, string roomName, + bool passworded, bool tunneled, string[] players, string adminName, string mapName, string gameMode) { ChannelName = channelName; Revision = revision; @@ -38,4 +34,4 @@ public override bool Equals(GenericHostedGame other) ? string.Equals(hostedCnCNetGame.ChannelName, ChannelName, StringComparison.InvariantCultureIgnoreCase) : base.Equals(other); } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 2adb4be9f..f32475bc9 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -13,7 +13,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet { - public class TunnelHandler : GameComponent + internal sealed class TunnelHandler : GameComponent { /// /// Determines the time between pinging the current tunnel (if it's set). @@ -29,14 +29,9 @@ public class TunnelHandler : GameComponent /// private const uint CYCLES_PER_TUNNEL_LIST_REFRESH = 6; - private const int SUPPORTED_TUNNEL_VERSION = 2; - - private const string CNCNET_TUNNEL_LIST_URL = "http://cncnet.org/master-list"; - public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) : base(wm.Game) { this.wm = wm; - this.connectionManager = connectionManager; wm.Game.Components.Add(this); @@ -47,18 +42,17 @@ public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) : base(w connectionManager.ConnectionLost += ConnectionManager_ConnectionLost; } - public List Tunnels { get; private set; } = new List(); - public CnCNetTunnel CurrentTunnel { get; set; } = null; + public List Tunnels { get; private set; } = new(); + public CnCNetTunnel CurrentTunnel { get; set; } public event EventHandler TunnelsRefreshed; public event EventHandler CurrentTunnelPinged; public event Action TunnelPinged; - private WindowManager wm; - private CnCNetManager connectionManager; + private readonly WindowManager wm; private TimeSpan timeSinceTunnelRefresh = TimeSpan.MaxValue; - private uint skipCount = 0; + private uint skipCount; private void DoTunnelPinged(int index) { @@ -163,7 +157,7 @@ private List RefreshTunnels() try { - data = client.DownloadData(CNCNET_TUNNEL_LIST_URL); + data = client.DownloadData(MainClientConstants.CNCNET_TUNNEL_LIST_URL); } catch (WebException ex) { @@ -171,7 +165,7 @@ private List RefreshTunnels() Logger.Log("Retrying."); try { - data = client.DownloadData(CNCNET_TUNNEL_LIST_URL); + data = client.DownloadData(MainClientConstants.CNCNET_TUNNEL_LIST_URL); } catch (WebException) { @@ -180,17 +174,15 @@ private List RefreshTunnels() Logger.Log("Tunnel cache file doesn't exist!"); return returnValue; } - else - { - Logger.Log("Fetching tunnel server list failed. Using cached tunnel data."); - data = File.ReadAllBytes(tunnelCacheFile.FullName); - } + + Logger.Log("Fetching tunnel server list failed. Using cached tunnel data."); + data = File.ReadAllBytes(tunnelCacheFile.FullName); } } string convertedData = Encoding.Default.GetString(data); - string[] serverList = convertedData.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); + string[] serverList = convertedData.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); // skip first header item ("address;country;countrycode;name;password;clients;maxclients;official;latitude;longitude;version;distance") foreach (string serverInfo in serverList.Skip(1)) @@ -263,4 +255,4 @@ public override void Update(GameTime gameTime) base.Update(gameTime); } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index 1129d9c2b..cacbb47c5 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -1,11 +1,7 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Sockets; -using System.Text; using System.Threading; -using System.Threading.Tasks; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -13,7 +9,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet /// Captures packets sent by an UDP client (the game) to a specific address /// and allows forwarding messages back to it. /// - public class TunneledPlayerConnection + internal sealed class TunneledPlayerConnection { private const int Timeout = 60000; @@ -38,9 +34,9 @@ private bool Aborted private Socket socket; private EndPoint endPoint; - private readonly object locker = new object(); + private readonly object locker = new object(); + - public void Stop() { Aborted = true; @@ -55,12 +51,12 @@ public void CreateSocket() endPoint = new IPEndPoint(IPAddress.Loopback, 0); socket.Bind(endPoint); - PortNumber = ((IPEndPoint)(socket.LocalEndPoint)).Port; + PortNumber = ((IPEndPoint)socket.LocalEndPoint).Port; } public void Start() { - Thread thread = new Thread(new ThreadStart(Run)); + Thread thread = new Thread(Run); thread.Start(); } @@ -105,4 +101,4 @@ public void SendPacket(byte[] packet) } } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index 218de9968..e4315716b 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -6,22 +6,11 @@ namespace DTAClient.Domain.Multiplayer.CnCNet { - public enum ConnectionState - { - NotConnected = 0, - WaitingForPassword = 1, - WaitingForVerification = 2, - Connected = 3 - } - /// /// Handles connections to version 3 CnCNet tunnel servers. /// - class V3TunnelConnection + internal sealed class V3TunnelConnection { - private const int PASSWORD_REQUEST_SIZE = 512; - private const int PASSWORD_MESSAGE_SIZE = 12; - public V3TunnelConnection(CnCNetTunnel tunnel, uint senderId) { this.tunnel = tunnel; @@ -37,9 +26,7 @@ public V3TunnelConnection(CnCNetTunnel tunnel, uint senderId) public uint SenderId { get; set; } - public ConnectionState State { get; private set; } - - private bool aborted = false; + private bool aborted; public bool Aborted { get { lock (locker) return aborted; } @@ -50,11 +37,11 @@ public bool Aborted private Socket tunnelSocket; private EndPoint tunnelEndPoint; - private readonly object locker = new object(); + private readonly object locker = new(); public void ConnectAsync() { - Thread thread = new Thread(new ThreadStart(DoConnect)); + Thread thread = new Thread(DoConnect); thread.Start(); } @@ -65,44 +52,11 @@ private void DoConnect() tunnelSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); tunnelSocket.SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; - tunnelSocket.ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; - try - { - byte[] buffer = new byte[PASSWORD_REQUEST_SIZE]; - WriteSenderIdToBuffer(buffer); - tunnelEndPoint = new IPEndPoint(tunnel.IPAddress, tunnel.Port); - tunnelSocket.SendTo(buffer, tunnelEndPoint); - State = ConnectionState.WaitingForPassword; - Logger.Log("Sent ID, waiting for password."); - - buffer = new byte[PASSWORD_MESSAGE_SIZE]; - tunnelSocket.ReceiveFrom(buffer, ref tunnelEndPoint); - - byte[] password = new byte[4]; - Array.Copy(buffer, 8, password, 0, password.Length); - Logger.Log("Password received, sending it back for verification."); - - // Echo back the password - // <4 bytes of anything> - buffer = new byte[PASSWORD_MESSAGE_SIZE]; - WriteSenderIdToBuffer(buffer); - Array.Copy(password, 0, buffer, 8, password.Length); - tunnelSocket.SendTo(buffer, tunnelEndPoint); - State = ConnectionState.Connected; - - Logger.Log("Connection to tunnel server established. Entering receive loop."); - Connected?.Invoke(this, EventArgs.Empty); - } - catch (SocketException ex) - { - Logger.Log($"Failed to establish connection to tunnel server. Message: " + ex.Message); - tunnelSocket.Close(); - ConnectionFailed?.Invoke(this, EventArgs.Empty); - return; - } + Logger.Log("Connection to tunnel server established. Entering receive loop."); + Connected?.Invoke(this, EventArgs.Empty); - tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; ReceiveLoop(); } @@ -160,7 +114,6 @@ private void DoClose() { tunnelSocket.Close(); tunnelSocket = null; - State = ConnectionState.NotConnected; } Logger.Log("Connection to tunnel server closed."); @@ -180,4 +133,4 @@ public void SendData(byte[] data, uint receiverId) } } } -} +} \ No newline at end of file From abab4fa4a52543c59aadc2cfdb9526024550afbf Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 15 Aug 2022 12:07:44 +0200 Subject: [PATCH 012/109] Fix V3 tunnel player port assignment --- ClientCore/ProgramConstants.cs | 1 - .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 20 +++++++-- .../Domain/Multiplayer/CnCNet/IPUtilities.cs | 43 ------------------- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 18 +++++++- DXMainClient/Properties/launchSettings.json | 4 +- 5 files changed, 37 insertions(+), 49 deletions(-) delete mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/IPUtilities.cs diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index 45f5d88b1..8464a16c4 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -34,7 +34,6 @@ public static class ProgramConstants public const string CNCNET_PROTOCOL_REVISION = "R11"; public const string LAN_PROTOCOL_REVISION = "RL7"; - public const int LAN_PORT = 1234; public const int LAN_INGAME_PORT = 1234; public const int LAN_LOBBY_PORT = 1232; public const int LAN_GAME_LOBBY_PORT = 1233; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 6ad2d3526..72a9b3933 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -16,6 +16,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; +using System.Net.NetworkInformation; using System.Text; using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Extensions; @@ -24,8 +26,6 @@ namespace DTAClient.DXGUI.Multiplayer.GameLobby { internal sealed class CnCNetGameLobby : MultiplayerGameLobby { - private const int INGAME_PORT = 1234; - private const int HUMAN_PLAYER_OPTIONS_LENGTH = 3; private const int AI_PLAYER_OPTIONS_LENGTH = 2; @@ -874,7 +874,7 @@ private void HandleTunnelConnected(string playerName) ids.Remove(tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); List players = new List(Players); int myIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); - Players[myIndex].Port = INGAME_PORT; + Players[myIndex].Port = GetFreePort(Players.Select(q => q.Port)); players.RemoveAt(myIndex); int[] ports = gameTunnelHandler.CreatePlayerConnections(ids); for (int i = 0; i < ports.Length; i++) @@ -887,6 +887,20 @@ private void HandleTunnelConnected(string playerName) } } + private static int GetFreePort(IEnumerable playerPorts) + { + IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); + int[] usedPorts = endPoints.Select(q => q.Port).Concat(playerPorts).ToArray(); + int selectedPort = 0; + + while (selectedPort == 0 || usedPorts.Contains(selectedPort)) + { + selectedPort = new Random().Next(1, 65535); + } + + return selectedPort; + } + private void AbortGameStart() { btnLaunchGame.InputEnabled = true; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/IPUtilities.cs b/DXMainClient/Domain/Multiplayer/CnCNet/IPUtilities.cs deleted file mode 100644 index d43918fb8..000000000 --- a/DXMainClient/Domain/Multiplayer/CnCNet/IPUtilities.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading.Tasks; - -namespace DTAClient.Domain.Multiplayer.CnCNet -{ - class IPUtilities - { - public static IPEndPoint GetPublicEndPoint(IPAddress serverIP, int destPort, int gamePort) - { - // Code by FunkyFr3sh - - using (var udpClient = new UdpClient()) - { - udpClient.ExclusiveAddressUse = false; - udpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - - udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, gamePort)); - - IAsyncResult iAsyncResult = udpClient.BeginReceive(null, null); - udpClient.Send(new byte[1], 1, new IPEndPoint(serverIP, destPort)); - iAsyncResult.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(750), false); - if (iAsyncResult.IsCompleted) - { - IPEndPoint remote = null; - byte[] data = udpClient.EndReceive(iAsyncResult, ref remote); - if (remote.Address.Equals(serverIP) && remote.Port == destPort && data.Length == 8) - { - byte[] ip = new byte[4]; - Array.Copy(data, 4, ip, 0, 4); - return new IPEndPoint(new IPAddress(ip), BitConverter.ToInt32(data, 0)); - } - } - } - - throw new Exception("No response from server"); - } - } -} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index e4315716b..2bae0df2d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -52,6 +52,23 @@ private void DoConnect() tunnelSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); tunnelSocket.SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; + + try + { + byte[] buffer = new byte[50]; + WriteSenderIdToBuffer(buffer); + tunnelEndPoint = new IPEndPoint(tunnel.IPAddress, tunnel.Port); + tunnelSocket.SendTo(buffer, tunnelEndPoint); + } + catch (SocketException ex) + { + Logger.Log($"Failed to establish connection to tunnel server. Message: " + ex.Message); + tunnelSocket.Close(); + ConnectionFailed?.Invoke(this, EventArgs.Empty); + return; + } + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; Logger.Log("Connection to tunnel server established. Entering receive loop."); @@ -78,7 +95,6 @@ private void ReceiveLoop() byte[] buffer = new byte[1024]; int size = tunnelSocket.ReceiveFrom(buffer, ref tunnelEndPoint); - if (size < 8) { Logger.Log("Invalid data packet from tunnel server"); diff --git a/DXMainClient/Properties/launchSettings.json b/DXMainClient/Properties/launchSettings.json index 030ba9b5b..770993e99 100644 --- a/DXMainClient/Properties/launchSettings.json +++ b/DXMainClient/Properties/launchSettings.json @@ -1,10 +1,12 @@ { "profiles": { "DXMainClient": { - "commandName": "Project" + "commandName": "Project", + "commandLineArgs": "-MULTIPLEINSTANCE" }, "WSL": { "commandName": "WSL2", + "commandLineArgs": "-MULTIPLEINSTANCE", "distributionName": "" } } From 94c416badf2095e9e8ff5827c9b800ff537d8b5b Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 15 Aug 2022 15:32:55 +0200 Subject: [PATCH 013/109] Logging --- .../CnCNet/CnCNetGameLoadingLobby.cs | 1 + .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 6 ++--- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 6 +++++ .../Multiplayer/GameLobby/GameLobbyBase.cs | 4 +++- .../CnCNet/TunneledPlayerConnection.cs | 23 +++++++++++++++++-- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 20 ++++++++++++++++ 6 files changed, 54 insertions(+), 6 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index f34bfd801..e9e71434f 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -637,6 +637,7 @@ protected override void HostStartGame() protected override void WriteSpawnIniAdditions(IniFile spawnIni) { + Logger.Log($"Tunnel_V3 Writing tunnel to spawner {tunnelHandler.CurrentTunnel.Address}:{tunnelHandler.CurrentTunnel.Port}."); spawnIni.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); spawnIni.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index 64935edbd..d5bf785f0 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -304,6 +304,7 @@ protected void LoadGame() return; spawnIni.SetIntValue("Settings", "Port", localPlayer.Port); + Logger.Log($"Tunnel_V3 Writing local player {localPlayer.Name} address to spawner {localPlayer.IPAddress}:{localPlayer.Port}."); for (int i = 1; i < Players.Count; i++) { @@ -319,6 +320,7 @@ protected void LoadGame() spawnIni.SetStringValue("Other" + i, "Ip", otherPlayer.IPAddress); spawnIni.SetIntValue("Other" + i, "Port", otherPlayer.Port); + Logger.Log($"Tunnel_V3 Writing other player {otherPlayer.Name} address to spawner {otherPlayer.IPAddress}:{otherPlayer.Port}."); } WriteSpawnIniAdditions(spawnIni); @@ -475,8 +477,6 @@ protected void CopyPlayerDataToUI() } } - protected virtual string GetIPAddressForPlayer(PlayerInfo pInfo) => "0.0.0.0"; - private void DdSavedGame_SelectedIndexChanged(object sender, EventArgs e) { if (!IsHost) @@ -523,4 +523,4 @@ public override void Draw(GameTime gameTime) public abstract string GetSwitchName(); } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 72a9b3933..965c5ece9 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -782,6 +782,8 @@ private void StartGame_V3Tunnel() private void HandleGameStartV3TunnelMessage(string sender, string message) { + Logger.Log($"Tunnel_V3 received STARTV3 from {sender}: {message}."); + if (sender != hostName) return; @@ -851,6 +853,8 @@ private void HandleTunnelFail(string playerName) private void HandleTunnelConnected(string playerName) { + Logger.Log($"Tunnel_V3 received TNLOK {playerName}."); + if (!isStartingGame) return; @@ -875,10 +879,12 @@ private void HandleTunnelConnected(string playerName) List players = new List(Players); int myIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); Players[myIndex].Port = GetFreePort(Players.Select(q => q.Port)); + Logger.Log($"Tunnel_V3 set own player connection to {Players[myIndex].IPAddress}:{Players[myIndex].Port}."); players.RemoveAt(myIndex); int[] ports = gameTunnelHandler.CreatePlayerConnections(ids); for (int i = 0; i < ports.Length; i++) { + Logger.Log($"Tunnel_V3 set player {players[i].Name} connection to {players[i].IPAddress}:{players[i].Port}."); players[i].Port = ports[i]; } gameStartTimer.Pause(); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index f8fb4d7dd..b1caaea61 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1371,12 +1371,14 @@ private PlayerHouseInfo[] WriteSpawnIni() continue; string sectionName = "Other" + otherId; + string playerAddress = GetIPAddressForPlayer(pInfo); + Logger.Log($"Tunnel_V3 Writing player {pInfo.Name} address to spawner {playerAddress}:{pInfo.Port}."); spawnIni.SetStringValue(sectionName, "Name", pInfo.Name); spawnIni.SetIntValue(sectionName, "Side", pHouseInfo.InternalSideIndex); spawnIni.SetBooleanValue(sectionName, "IsSpectator", pHouseInfo.IsSpectator); spawnIni.SetIntValue(sectionName, "Color", pHouseInfo.ColorIndex); - spawnIni.SetStringValue(sectionName, "Ip", GetIPAddressForPlayer(pInfo)); + spawnIni.SetStringValue(sectionName, "Ip", playerAddress); spawnIni.SetIntValue(sectionName, "Port", pInfo.Port); otherId++; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index cacbb47c5..f0b7fefc9 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -2,6 +2,7 @@ using System.Net; using System.Net.Sockets; using System.Threading; +using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -34,8 +35,7 @@ private bool Aborted private Socket socket; private EndPoint endPoint; - private readonly object locker = new object(); - + private readonly object locker = new(); public void Stop() { @@ -52,6 +52,7 @@ public void CreateSocket() socket.Bind(endPoint); PortNumber = ((IPEndPoint)socket.LocalEndPoint).Port; + Logger.Log($"Tunnel_V3 Created local game connection for clientId {PlayerID} {socket.LocalEndPoint} ({PortNumber})."); } public void Start() @@ -70,9 +71,18 @@ private void Run() while (true) { if (Aborted) + { + Logger.Log($"Tunnel_V3 abort listening for game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); break; + } +#if DEBUG + Logger.Log($"Tunnel_V3 listening for game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); +#endif int received = socket.ReceiveFrom(buffer, ref endPoint); +#if DEBUG + Logger.Log($"Tunnel_V3 received game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); +#endif byte[] data = new byte[received]; Array.Copy(buffer, data, received); @@ -97,7 +107,16 @@ public void SendPacket(byte[] packet) lock (locker) { if (!_aborted) + { +#if DEBUG + Logger.Log($"Tunnel_V3 sending game data for {PlayerID} from {socket.LocalEndPoint} to {socket.RemoteEndPoint}."); +#endif socket.SendTo(packet, endPoint); + } + else + { + Logger.Log($"Tunnel_V3 abort sending game data for {PlayerID} from {socket.LocalEndPoint} to {socket.RemoteEndPoint}."); + } } } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index 2bae0df2d..6966f12c9 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -60,6 +60,8 @@ private void DoConnect() WriteSenderIdToBuffer(buffer); tunnelEndPoint = new IPEndPoint(tunnel.IPAddress, tunnel.Port); tunnelSocket.SendTo(buffer, tunnelEndPoint); + + Logger.Log($"Tunnel_V3 Connection to tunnel server established. Entering receive loop using clientId {SenderId} for tunnel address {tunnelSocket.LocalEndPoint}."); Connected?.Invoke(this, EventArgs.Empty); } catch (SocketException ex) { @@ -92,6 +94,10 @@ private void ReceiveLoop() Logger.Log("Exiting receive loop."); return; } +#if DEBUG + + Logger.Log($"Tunnel_V3 Listening for server using {tunnelSocket.LocalEndPoint} to {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint}."); +#endif byte[] buffer = new byte[1024]; int size = tunnelSocket.ReceiveFrom(buffer, ref tunnelEndPoint); @@ -104,6 +110,10 @@ private void ReceiveLoop() byte[] data = new byte[size - 8]; Array.Copy(buffer, 8, data, 0, data.Length); uint senderId = BitConverter.ToUInt32(buffer, 0); +#if DEBUG + + Logger.Log($"Tunnel_V3 Received data from server on {tunnelSocket.LocalEndPoint} from {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint} from clientId {senderId}."); +#endif MessageReceived?.Invoke(data, senderId); } @@ -145,7 +155,17 @@ public void SendData(byte[] data, uint receiverId) lock (locker) { if (!aborted) + { +#if DEBUG + Logger.Log($"Tunnel_V3 sending data using {tunnelSocket.LocalEndPoint} to server {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint} SenderId {SenderId} receiverId {receiverId}."); + +#endif tunnelSocket.SendTo(packet, tunnelEndPoint); + } + else + { + Logger.Log($"Tunnel_V3 abort sending data using {tunnelSocket.LocalEndPoint} to server {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint} SenderId {SenderId} receiverId {receiverId}."); + } } } } From 480ec20e7aa441882379712c1f8ef2078908ecf2 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 15 Aug 2022 23:33:03 +0200 Subject: [PATCH 014/109] Parse IPv6 & IPv4 tunnel addresses from master server, support IPv6 & IPv4 tunnel connections, network code optimizations --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 6 +- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 46 +++-- .../Multiplayer/CnCNet/GameTunnelHandler.cs | 58 ++++-- .../Multiplayer/CnCNet/TunnelHandler.cs | 54 ++++-- .../CnCNet/TunneledPlayerConnection.cs | 151 +++++++++++----- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 171 +++++++++++++----- 6 files changed, 340 insertions(+), 146 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 965c5ece9..7e755a7cd 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -878,8 +878,6 @@ private void HandleTunnelConnected(string playerName) ids.Remove(tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); List players = new List(Players); int myIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); - Players[myIndex].Port = GetFreePort(Players.Select(q => q.Port)); - Logger.Log($"Tunnel_V3 set own player connection to {Players[myIndex].IPAddress}:{Players[myIndex].Port}."); players.RemoveAt(myIndex); int[] ports = gameTunnelHandler.CreatePlayerConnections(ids); for (int i = 0; i < ports.Length; i++) @@ -887,6 +885,10 @@ private void HandleTunnelConnected(string playerName) Logger.Log($"Tunnel_V3 set player {players[i].Name} connection to {players[i].IPAddress}:{players[i].Port}."); players[i].Port = ports[i]; } + + Players.Single(p => p.Name == ProgramConstants.PLAYERNAME).Port = GetFreePort(ports); + Logger.Log($"Tunnel_V3 set own player connection to {Players[myIndex].IPAddress}:{Players[myIndex].Port}."); + gameStartTimer.Pause(); btnLaunchGame.InputEnabled = true; StartGame(); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index a8c664df7..0455f6bae 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -1,9 +1,16 @@ using Rampastring.Tools; using System; +#if !NETFRAMEWORK +using System.Buffers; +#endif using System.Collections.Generic; using System.Globalization; using System.Net; using System.Net.Sockets; +#if !NETFRAMEWORK +using System.Threading; +#endif +using System.Threading.Tasks; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -30,13 +37,16 @@ public static CnCNetTunnel Parse(string str) { var tunnel = new CnCNetTunnel(); string[] parts = str.Split(';'); - string address = parts[0]; - string[] detailedAddress = address.Split(':'); int version = int.Parse(parts[10]); - tunnel.Address = detailedAddress[0]; - tunnel.Port = int.Parse(detailedAddress[1]); +#if NETFRAMEWORK + tunnel.Address = address.Substring(0, address.LastIndexOf(':')); + tunnel.Port = int.Parse(address.Substring(address.LastIndexOf(':') + 1)); +#else + tunnel.Address = address[..address.LastIndexOf(':')]; + tunnel.Port = int.Parse(address[(address.LastIndexOf(':') + 1)..]); +#endif tunnel.Country = parts[1]; tunnel.CountryCode = parts[2]; tunnel.Name = parts[3] + " V" + version; @@ -96,8 +106,6 @@ public string Address public double Distance { get; private set; } public int PingInMs { get; set; } = -1; - public event EventHandler Pinged; - /// /// Gets a list of player ports to use from a specific tunnel server. /// @@ -141,22 +149,34 @@ public List GetPlayerPortInfo(int playerCount) return new List(); } - public void UpdatePing() + public async Task UpdatePingAsync() { - using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); socket.SendTimeout = PING_TIMEOUT; socket.ReceiveTimeout = PING_TIMEOUT; try { - byte[] buffer = new byte[PING_PACKET_SEND_SIZE]; EndPoint ep = new IPEndPoint(IPAddress, Port); long ticks = DateTime.Now.Ticks; - socket.SendTo(buffer, ep); +#if NETFRAMEWORK + byte[] buffer1 = new byte[PING_PACKET_SEND_SIZE]; + var buffer = new ArraySegment(buffer1); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(PING_PACKET_SEND_SIZE); + Memory buffer = memoryOwner.Memory[..PING_PACKET_SEND_SIZE]; +#endif + + await socket.SendToAsync(buffer, SocketFlags.None, ep); - buffer = new byte[PING_PACKET_RECEIVE_SIZE]; - socket.ReceiveFrom(buffer, ref ep); +#if NETFRAMEWORK + buffer = new ArraySegment(buffer1, 0, PING_PACKET_RECEIVE_SIZE); +#else + buffer = buffer[..PING_PACKET_RECEIVE_SIZE]; +#endif + + await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep); ticks = DateTime.Now.Ticks - ticks; PingInMs = new TimeSpan(ticks).Milliseconds; @@ -167,8 +187,6 @@ public void UpdatePing() PingInMs = -1; } - - Pinged?.Invoke(this, EventArgs.Empty); } } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs index edbe279ae..2e69127c3 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -8,22 +10,17 @@ internal sealed class GameTunnelHandler public event EventHandler Connected; public event EventHandler ConnectionFailed; - private uint senderId; - private V3TunnelConnection tunnelConnection; private Dictionary playerConnections = new(); - private readonly object locker = new(); + private readonly SemaphoreSlim locker = new(1, 1); public void SetUp(CnCNetTunnel tunnel, uint ourSenderId) { - senderId = ourSenderId; - - tunnelConnection = new V3TunnelConnection(tunnel, senderId); + tunnelConnection = new V3TunnelConnection(tunnel, this, ourSenderId); tunnelConnection.Connected += TunnelConnection_Connected; tunnelConnection.ConnectionFailed += TunnelConnection_ConnectionFailed; tunnelConnection.ConnectionCut += TunnelConnection_ConnectionCut; - tunnelConnection.MessageReceived += TunnelConnection_MessageReceived; } public void ConnectToTunnel() @@ -41,12 +38,11 @@ public int[] CreatePlayerConnections(List playerIds) for (int i = 0; i < playerIds.Count; i++) { - var playerConnection = new TunneledPlayerConnection(playerIds[i]); + var playerConnection = new TunneledPlayerConnection(playerIds[i], this); playerConnection.CreateSocket(); ports[i] = playerConnection.PortNumber; playerConnections.Add(playerIds[i], playerConnection); - playerConnection.PacketReceived += PlayerConnection_PacketReceived; - playerConnection.Start(); + playerConnection.StartAsync(); } return ports; @@ -54,12 +50,13 @@ public int[] CreatePlayerConnections(List playerIds) public void Clear() { - lock (locker) + locker.Wait(); + + try { foreach (var connection in playerConnections) { connection.Value.Stop(); - connection.Value.PacketReceived -= PlayerConnection_PacketReceived; } playerConnections.Clear(); @@ -71,26 +68,49 @@ public void Clear() tunnelConnection.Connected -= TunnelConnection_Connected; tunnelConnection.ConnectionFailed -= TunnelConnection_ConnectionFailed; tunnelConnection.ConnectionCut -= TunnelConnection_ConnectionCut; - tunnelConnection.MessageReceived -= TunnelConnection_MessageReceived; tunnelConnection = null; } + finally + { + locker.Release(); + } } - private void PlayerConnection_PacketReceived(TunneledPlayerConnection sender, byte[] data) +#if NETFRAMEWORK + public async Task PlayerConnection_PacketReceivedAsync(TunneledPlayerConnection sender, byte[] data) +#else + public async Task PlayerConnection_PacketReceivedAsync(TunneledPlayerConnection sender, ReadOnlyMemory data) +#endif { - lock (locker) + await locker.WaitAsync(); + + try { if (tunnelConnection != null) - tunnelConnection.SendData(data, sender.PlayerID); + await tunnelConnection.SendDataAsync(data, sender.PlayerID); + } + finally + { + locker.Release(); } } - private void TunnelConnection_MessageReceived(byte[] data, uint senderId) +#if NETFRAMEWORK + public async Task TunnelConnection_MessageReceivedAsync(byte[] data, uint senderId) +#else + public async Task TunnelConnection_MessageReceivedAsync(ReadOnlyMemory data, uint senderId) +#endif { - lock (locker) + await locker.WaitAsync(); + + try { if (playerConnections.TryGetValue(senderId, out TunneledPlayerConnection connection)) - connection.SendPacket(data); + await connection.SendPacketAsync(data); + } + finally + { + locker.Release(); } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index f32475bc9..1fcea5899 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -72,13 +72,17 @@ private void DoCurrentTunnelPinged() private void ConnectionManager_Disconnected(object sender, EventArgs e) => Enabled = false; - private void RefreshTunnelsAsync() + private async Task RefreshTunnelsAsync() { - Task.Factory.StartNew(() => + try { - List tunnels = RefreshTunnels(); + List tunnels = await DoRefreshTunnelsAsync(); wm.AddCallback(new Action>(HandleRefreshedTunnels), tunnels); - }); + } + catch (Exception ex) + { + PreStartup.LogException(ex); + } } private void HandleRefreshedTunnels(List tunnels) @@ -88,12 +92,10 @@ private void HandleRefreshedTunnels(List tunnels) TunnelsRefreshed?.Invoke(this, EventArgs.Empty); - Task[] pingTasks = new Task[Tunnels.Count]; - for (int i = 0; i < Tunnels.Count; i++) { if (UserINISettings.Instance.PingUnofficialCnCNetTunnels || Tunnels[i].Official || Tunnels[i].Recommended) - pingTasks[i] = PingListTunnelAsync(i); + PingListTunnelAsync(i); } if (CurrentTunnel != null) @@ -114,20 +116,24 @@ private void HandleRefreshedTunnels(List tunnels) } } - private Task PingListTunnelAsync(int index) + private async Task PingListTunnelAsync(int index) { - return Task.Factory.StartNew(() => + try { - Tunnels[index].UpdatePing(); + await Tunnels[index].UpdatePingAsync(); DoTunnelPinged(index); - }); + } + catch (Exception ex) + { + PreStartup.LogException(ex); + } } - private Task PingCurrentTunnelAsync(bool checkTunnelList = false) + private async Task PingCurrentTunnelAsync(bool checkTunnelList = false) { - return Task.Factory.StartNew(() => + try { - CurrentTunnel.UpdatePing(); + await CurrentTunnel.UpdatePingAsync(); DoCurrentTunnelPinged(); if (checkTunnelList) @@ -136,14 +142,18 @@ private Task PingCurrentTunnelAsync(bool checkTunnelList = false) if (tunnelIndex > -1) DoTunnelPinged(tunnelIndex); } - }); + } + catch (Exception ex) + { + PreStartup.LogException(ex); + } } /// /// Downloads and parses the list of CnCNet tunnels. /// /// A list of tunnel servers. - private List RefreshTunnels() + private async Task> DoRefreshTunnelsAsync() { FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); @@ -157,7 +167,7 @@ private List RefreshTunnels() try { - data = client.DownloadData(MainClientConstants.CNCNET_TUNNEL_LIST_URL); + data = await client.DownloadDataTaskAsync(MainClientConstants.CNCNET_TUNNEL_LIST_URL); } catch (WebException ex) { @@ -165,7 +175,7 @@ private List RefreshTunnels() Logger.Log("Retrying."); try { - data = client.DownloadData(MainClientConstants.CNCNET_TUNNEL_LIST_URL); + data = await client.DownloadDataTaskAsync(MainClientConstants.CNCNET_TUNNEL_LIST_URL); } catch (WebException) { @@ -176,7 +186,11 @@ private List RefreshTunnels() } Logger.Log("Fetching tunnel server list failed. Using cached tunnel data."); +#if NETFRAMEWORK data = File.ReadAllBytes(tunnelCacheFile.FullName); +#else + data = await File.ReadAllBytesAsync(tunnelCacheFile.FullName); +#endif } } @@ -221,7 +235,11 @@ private List RefreshTunnels() if (!clientDirectoryInfo.Exists) clientDirectoryInfo.Create(); +#if NETFRAMEWORK File.WriteAllBytes(tunnelCacheFile.FullName, data); +#else + await File.WriteAllBytesAsync(tunnelCacheFile.FullName, data); +#endif } catch (Exception ex) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index f0b7fefc9..0ad6ff889 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -1,7 +1,9 @@ using System; +using System.Buffers; using System.Net; using System.Net.Sockets; using System.Threading; +using System.Threading.Tasks; using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet @@ -14,28 +16,52 @@ internal sealed class TunneledPlayerConnection { private const int Timeout = 60000; - public TunneledPlayerConnection(uint playerId) + private GameTunnelHandler gameTunnelHandler; + + public TunneledPlayerConnection(uint playerId, GameTunnelHandler gameTunnelHandler) { PlayerID = playerId; + this.gameTunnelHandler = gameTunnelHandler; } - public delegate void PacketReceivedEventHandler(TunneledPlayerConnection sender, byte[] data); - public event PacketReceivedEventHandler PacketReceived; - public int PortNumber { get; private set; } public uint PlayerID { get; } - private bool _aborted; - private bool Aborted + private bool aborted; + public bool Aborted { - get { lock (locker) return _aborted; } - set { lock (locker) _aborted = value; } + get + { + locker.Wait(); + + try + { + return aborted; + } + finally + { + locker.Release(); + } + } + private set + { + locker.Wait(); + + try + { + aborted = value; + } + finally + { + locker.Release(); + } + } } private Socket socket; private EndPoint endPoint; - private readonly object locker = new(); + private readonly SemaphoreSlim locker = new(1, 1); public void Stop() { @@ -55,69 +81,108 @@ public void CreateSocket() Logger.Log($"Tunnel_V3 Created local game connection for clientId {PlayerID} {socket.LocalEndPoint} ({PortNumber})."); } - public void Start() - { - Thread thread = new Thread(Run); - thread.Start(); - } - - private void Run() + public async Task StartAsync() { - socket.ReceiveTimeout = Timeout; - byte[] buffer = new byte[1024]; - try { - while (true) +#if NETFRAMEWORK + byte[] buffer1 = new byte[1024]; + var buffer = new ArraySegment(buffer1); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); + Memory buffer = memoryOwner.Memory[..1024]; +#endif + + socket.ReceiveTimeout = Timeout; + + try { - if (Aborted) + while (true) { - Logger.Log($"Tunnel_V3 abort listening for game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); - break; - } + if (Aborted) + { + Logger.Log($"Tunnel_V3 abort listening for game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); + break; + } #if DEBUG - Logger.Log($"Tunnel_V3 listening for game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); + Logger.Log($"Tunnel_V3 listening for game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); #endif - int received = socket.ReceiveFrom(buffer, ref endPoint); + + SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, endPoint); + #if DEBUG - Logger.Log($"Tunnel_V3 received game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); + Logger.Log($"Tunnel_V3 received game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); +#endif + +#if NETFRAMEWORK + byte[] data = new byte[socketReceiveFromResult.ReceivedBytes]; + Array.Copy(buffer1, data, socketReceiveFromResult.ReceivedBytes); + Array.Clear(buffer1, 0, socketReceiveFromResult.ReceivedBytes); +#else + + Memory data = buffer[..socketReceiveFromResult.ReceivedBytes]; #endif - byte[] data = new byte[received]; - Array.Copy(buffer, data, received); - Array.Clear(buffer, 0, received); - PacketReceived?.Invoke(this, data); + await gameTunnelHandler.PlayerConnection_PacketReceivedAsync(this, data); + } } - } - catch (SocketException) - { - // Timeout - } + catch (SocketException) + { + // Timeout + } + + await locker.WaitAsync(); - lock (locker) + try + { + aborted = true; + socket.Close(); + } + finally + { + locker.Release(); + } + } + catch (Exception ex) { - _aborted = true; - socket.Close(); + PreStartup.LogException(ex); } } - public void SendPacket(byte[] packet) +#if NETFRAMEWORK + public async Task SendPacketAsync(byte[] packet) + { + var buffer = new ArraySegment(packet); + +#else + public async Task SendPacketAsync(ReadOnlyMemory packet) { - lock (locker) +#endif + await locker.WaitAsync(); + + try { - if (!_aborted) + if (!aborted) { #if DEBUG Logger.Log($"Tunnel_V3 sending game data for {PlayerID} from {socket.LocalEndPoint} to {socket.RemoteEndPoint}."); #endif - socket.SendTo(packet, endPoint); +#if NETFRAMEWORK + await socket.SendToAsync(buffer, SocketFlags.None, endPoint); +#else + await socket.SendToAsync(packet, SocketFlags.None, endPoint); +#endif } else { Logger.Log($"Tunnel_V3 abort sending game data for {PlayerID} from {socket.LocalEndPoint} to {socket.RemoteEndPoint}."); } } + finally + { + locker.Release(); + } } } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index 6966f12c9..88b4df677 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -1,8 +1,13 @@ using Rampastring.Tools; using System; +#if !NETFRAMEWORK +using System.Buffers; +#endif using System.Net; using System.Net.Sockets; using System.Threading; +using System.Threading.Tasks; +using Exception = System.Exception; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -11,9 +16,10 @@ namespace DTAClient.Domain.Multiplayer.CnCNet /// internal sealed class V3TunnelConnection { - public V3TunnelConnection(CnCNetTunnel tunnel, uint senderId) + public V3TunnelConnection(CnCNetTunnel tunnel, GameTunnelHandler gameTunnelHandler, uint senderId) { this.tunnel = tunnel; + this.gameTunnelHandler = gameTunnelHandler; SenderId = senderId; } @@ -21,68 +27,101 @@ public V3TunnelConnection(CnCNetTunnel tunnel, uint senderId) public event EventHandler ConnectionFailed; public event EventHandler ConnectionCut; - public delegate void MessageDelegate(byte[] data, uint senderId); - public event MessageDelegate MessageReceived; - public uint SenderId { get; set; } private bool aborted; public bool Aborted { - get { lock (locker) return aborted; } - private set { lock (locker) aborted = value; } + get + { + locker.Wait(); + + try + { + return aborted; + } + finally + { + locker.Release(); + } + } + private set + { + locker.Wait(); + + try + { + aborted = value; + } + finally + { + locker.Release(); + } + } } - private CnCNetTunnel tunnel; + private readonly CnCNetTunnel tunnel; + private readonly GameTunnelHandler gameTunnelHandler; private Socket tunnelSocket; private EndPoint tunnelEndPoint; - private readonly object locker = new(); - - public void ConnectAsync() - { - Thread thread = new Thread(DoConnect); - thread.Start(); - } + private readonly SemaphoreSlim locker = new(1, 1); - private void DoConnect() + public async Task ConnectAsync() { - Logger.Log($"Attempting to establish connection to V3 tunnel server " + - $"{tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); - - tunnelSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - tunnelSocket.SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; - tunnelSocket.ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; - try { - byte[] buffer = new byte[50]; - WriteSenderIdToBuffer(buffer); + Logger.Log($"Attempting to establish connection to V3 tunnel server " + + $"{tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); + tunnelEndPoint = new IPEndPoint(tunnel.IPAddress, tunnel.Port); - tunnelSocket.SendTo(buffer, tunnelEndPoint); + tunnelSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); + tunnelSocket.SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; - Logger.Log($"Tunnel_V3 Connection to tunnel server established. Entering receive loop using clientId {SenderId} for tunnel address {tunnelSocket.LocalEndPoint}."); Connected?.Invoke(this, EventArgs.Empty); - } - catch (SocketException ex) - { - Logger.Log($"Failed to establish connection to tunnel server. Message: " + ex.Message); - tunnelSocket.Close(); - ConnectionFailed?.Invoke(this, EventArgs.Empty); - return; - } + try + { +#if NETFRAMEWORK + byte[] buffer1 = new byte[50]; + WriteSenderIdToBuffer(buffer1); + var buffer = new ArraySegment(buffer1); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); + Memory buffer = memoryOwner.Memory[..50]; + if (!BitConverter.TryWriteBytes(buffer.Span[..4], SenderId)) throw new Exception(); +#endif - tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; + await tunnelSocket.SendToAsync(buffer, SocketFlags.None, tunnelEndPoint); - Logger.Log("Connection to tunnel server established. Entering receive loop."); - Connected?.Invoke(this, EventArgs.Empty); + Logger.Log($"Tunnel_V3 Connection to tunnel server established. Entering receive loop using clientId {SenderId} for tunnel address {tunnelSocket.LocalEndPoint}."); Connected?.Invoke(this, EventArgs.Empty); + } + catch (SocketException ex) + { + Logger.Log($"Failed to establish connection to tunnel server. Message: " + ex.Message); + tunnelSocket.Close(); + ConnectionFailed?.Invoke(this, EventArgs.Empty); + return; + } - ReceiveLoop(); + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; + + Logger.Log("Connection to tunnel server established. Entering receive loop."); + Connected?.Invoke(this, EventArgs.Empty); + + await ReceiveLoopAsync(); + } + catch (Exception ex) + { + PreStartup.LogException(ex); + } } +#if NETFRAMEWORK private void WriteSenderIdToBuffer(byte[] buffer) => Array.Copy(BitConverter.GetBytes(SenderId), buffer, sizeof(uint)); +#endif - private void ReceiveLoop() + private async Task ReceiveLoopAsync() { try { @@ -99,23 +138,35 @@ private void ReceiveLoop() Logger.Log($"Tunnel_V3 Listening for server using {tunnelSocket.LocalEndPoint} to {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint}."); #endif - byte[] buffer = new byte[1024]; - int size = tunnelSocket.ReceiveFrom(buffer, ref tunnelEndPoint); - if (size < 8) +#if NETFRAMEWORK + byte[] buffer1 = new byte[1024]; + var buffer = new ArraySegment(buffer1); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); + Memory buffer = memoryOwner.Memory[..1024]; +#endif + + SocketReceiveFromResult socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync(buffer, SocketFlags.None, tunnelEndPoint); + + if (socketReceiveFromResult.ReceivedBytes < 8) { Logger.Log("Invalid data packet from tunnel server"); continue; } - byte[] data = new byte[size - 8]; - Array.Copy(buffer, 8, data, 0, data.Length); - uint senderId = BitConverter.ToUInt32(buffer, 0); +#if NETFRAMEWORK + byte[] data = new byte[socketReceiveFromResult.ReceivedBytes - 8]; + Array.Copy(buffer1, 8, data, 0, data.Length); + uint senderId = BitConverter.ToUInt32(buffer1, 0); +#else + Memory data = buffer[8..socketReceiveFromResult.ReceivedBytes]; + uint senderId = BitConverter.ToUInt32(buffer[..4].Span); +#endif #if DEBUG Logger.Log($"Tunnel_V3 Received data from server on {tunnelSocket.LocalEndPoint} from {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint} from clientId {senderId}."); #endif - - MessageReceived?.Invoke(data, senderId); + await gameTunnelHandler.TunnelConnection_MessageReceivedAsync(data, senderId); } } catch (SocketException ex) @@ -145,14 +196,26 @@ private void DoClose() Logger.Log("Connection to tunnel server closed."); } - public void SendData(byte[] data, uint receiverId) +#if NETFRAMEWORK + public async Task SendDataAsync(byte[] data, uint receiverId) { byte[] packet = new byte[data.Length + 8]; // 8 = sizeof(uint) * 2 WriteSenderIdToBuffer(packet); Array.Copy(BitConverter.GetBytes(receiverId), 0, packet, 4, sizeof(uint)); Array.Copy(data, 0, packet, 8, data.Length); +#else + public async Task SendDataAsync(ReadOnlyMemory data, uint receiverId) + { + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(data.Length + 8); + Memory packet = memoryOwner.Memory[..(data.Length + 8)]; + if (!BitConverter.TryWriteBytes(packet.Span[..4], SenderId)) throw new Exception(); + if (!BitConverter.TryWriteBytes(packet.Span[5..8], receiverId)) throw new Exception(); + data.CopyTo(packet[8..]); +#endif + + await locker.WaitAsync().ConfigureAwait(false); - lock (locker) + try { if (!aborted) { @@ -160,13 +223,21 @@ public void SendData(byte[] data, uint receiverId) Logger.Log($"Tunnel_V3 sending data using {tunnelSocket.LocalEndPoint} to server {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint} SenderId {SenderId} receiverId {receiverId}."); #endif - tunnelSocket.SendTo(packet, tunnelEndPoint); +#if NETFRAMEWORK + await tunnelSocket.SendToAsync(new ArraySegment(packet), SocketFlags.None, tunnelEndPoint); +#else + await tunnelSocket.SendToAsync(packet, SocketFlags.None, tunnelEndPoint); +#endif } else { Logger.Log($"Tunnel_V3 abort sending data using {tunnelSocket.LocalEndPoint} to server {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint} SenderId {SenderId} receiverId {receiverId}."); } } + finally + { + locker.Release(); + } } } } \ No newline at end of file From 0a09e711c922cc0065ad9af755d5734532ddbb22 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 16 Aug 2022 22:39:13 +0200 Subject: [PATCH 015/109] V3 tunnel fixes --- ClientCore/SavedGameManager.cs | 2 +- .../DXGUI/Generic/CampaignSelector.cs | 2 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 1 - .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 4 +-- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 31 +++---------------- .../Multiplayer/GameLobby/GameLobbyBase.cs | 1 - .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 2 +- .../Multiplayer/CnCNet/GameTunnelHandler.cs | 29 +++++++++++++++-- .../CnCNet/TunneledPlayerConnection.cs | 31 +++++-------------- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 24 ++------------ 10 files changed, 45 insertions(+), 82 deletions(-) diff --git a/ClientCore/SavedGameManager.cs b/ClientCore/SavedGameManager.cs index f32d63533..89506a3a7 100644 --- a/ClientCore/SavedGameManager.cs +++ b/ClientCore/SavedGameManager.cs @@ -79,7 +79,7 @@ public static bool InitSavedGames() { Logger.Log("Writing spawn.ini for saved game."); SafePath.DeleteFileIfExists(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, "spawnSG.ini"); - File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawn.ini"), SafePath.CombineFilePath(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, "spawnSG.ini")); + File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS), SafePath.CombineFilePath(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, "spawnSG.ini")); } catch (Exception ex) { diff --git a/DXMainClient/DXGUI/Generic/CampaignSelector.cs b/DXMainClient/DXGUI/Generic/CampaignSelector.cs index 9aae7c6bf..3e182e284 100644 --- a/DXMainClient/DXGUI/Generic/CampaignSelector.cs +++ b/DXMainClient/DXGUI/Generic/CampaignSelector.cs @@ -270,7 +270,7 @@ private void LaunchMission(Mission mission) bool copyMapsToSpawnmapINI = ClientConfiguration.Instance.CopyMissionsToSpawnmapINI; Logger.Log("About to write spawn.ini."); - using var spawnStreamWriter = new StreamWriter(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawn.ini")); + using var spawnStreamWriter = new StreamWriter(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS)); spawnStreamWriter.WriteLine("; Generated by DTA Client"); spawnStreamWriter.WriteLine("[Settings]"); if (copyMapsToSpawnmapINI) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index e9e71434f..f34bfd801 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -637,7 +637,6 @@ protected override void HostStartGame() protected override void WriteSpawnIniAdditions(IniFile spawnIni) { - Logger.Log($"Tunnel_V3 Writing tunnel to spawner {tunnelHandler.CurrentTunnel.Address}:{tunnelHandler.CurrentTunnel.Port}."); spawnIni.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); spawnIni.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index d5bf785f0..a2cdba568 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -284,7 +284,7 @@ protected virtual void NotAllPresentNotification() => protected void LoadGame() { - FileInfo spawnFileInfo = SafePath.GetFile(ProgramConstants.GamePath, "spawn.ini"); + FileInfo spawnFileInfo = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS); spawnFileInfo.Delete(); @@ -304,7 +304,6 @@ protected void LoadGame() return; spawnIni.SetIntValue("Settings", "Port", localPlayer.Port); - Logger.Log($"Tunnel_V3 Writing local player {localPlayer.Name} address to spawner {localPlayer.IPAddress}:{localPlayer.Port}."); for (int i = 1; i < Players.Count; i++) { @@ -320,7 +319,6 @@ protected void LoadGame() spawnIni.SetStringValue("Other" + i, "Ip", otherPlayer.IPAddress); spawnIni.SetIntValue("Other" + i, "Port", otherPlayer.Port); - Logger.Log($"Tunnel_V3 Writing other player {otherPlayer.Name} address to spawner {otherPlayer.IPAddress}:{otherPlayer.Port}."); } WriteSpawnIniAdditions(spawnIni); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 7e755a7cd..0890c4683 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -16,8 +16,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; -using System.Net.NetworkInformation; using System.Text; using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Extensions; @@ -782,8 +780,6 @@ private void StartGame_V3Tunnel() private void HandleGameStartV3TunnelMessage(string sender, string message) { - Logger.Log($"Tunnel_V3 received STARTV3 from {sender}: {message}."); - if (sender != hostName) return; @@ -853,8 +849,6 @@ private void HandleTunnelFail(string playerName) private void HandleTunnelConnected(string playerName) { - Logger.Log($"Tunnel_V3 received TNLOK {playerName}."); - if (!isStartingGame) return; @@ -879,36 +873,19 @@ private void HandleTunnelConnected(string playerName) List players = new List(Players); int myIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); players.RemoveAt(myIndex); - int[] ports = gameTunnelHandler.CreatePlayerConnections(ids); - for (int i = 0; i < ports.Length; i++) + Tuple ports = gameTunnelHandler.CreatePlayerConnections(ids); + for (int i = 0; i < ports.Item1.Length; i++) { - Logger.Log($"Tunnel_V3 set player {players[i].Name} connection to {players[i].IPAddress}:{players[i].Port}."); - players[i].Port = ports[i]; + players[i].Port = ports.Item1[i]; } - Players.Single(p => p.Name == ProgramConstants.PLAYERNAME).Port = GetFreePort(ports); - Logger.Log($"Tunnel_V3 set own player connection to {Players[myIndex].IPAddress}:{Players[myIndex].Port}."); - + Players.Single(p => p.Name == ProgramConstants.PLAYERNAME).Port = ports.Item2; gameStartTimer.Pause(); btnLaunchGame.InputEnabled = true; StartGame(); } } - private static int GetFreePort(IEnumerable playerPorts) - { - IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); - int[] usedPorts = endPoints.Select(q => q.Port).Concat(playerPorts).ToArray(); - int selectedPort = 0; - - while (selectedPort == 0 || usedPorts.Contains(selectedPort)) - { - selectedPort = new Random().Next(1, 65535); - } - - return selectedPort; - } - private void AbortGameStart() { btnLaunchGame.InputEnabled = true; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index b1caaea61..dac240794 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1373,7 +1373,6 @@ private PlayerHouseInfo[] WriteSpawnIni() string sectionName = "Other" + otherId; string playerAddress = GetIPAddressForPlayer(pInfo); - Logger.Log($"Tunnel_V3 Writing player {pInfo.Name} address to spawner {playerAddress}:{pInfo.Port}."); spawnIni.SetStringValue(sectionName, "Name", pInfo.Name); spawnIni.SetIntValue(sectionName, "Side", pHouseInfo.InternalSideIndex); spawnIni.SetBooleanValue(sectionName, "IsSpectator", pHouseInfo.IsSpectator); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 0455f6bae..491c1c49c 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -159,7 +159,6 @@ public async Task UpdatePingAsync() try { EndPoint ep = new IPEndPoint(IPAddress, Port); - long ticks = DateTime.Now.Ticks; #if NETFRAMEWORK byte[] buffer1 = new byte[PING_PACKET_SEND_SIZE]; var buffer = new ArraySegment(buffer1); @@ -168,6 +167,7 @@ public async Task UpdatePingAsync() Memory buffer = memoryOwner.Memory[..PING_PACKET_SEND_SIZE]; #endif + long ticks = DateTime.Now.Ticks; await socket.SendToAsync(buffer, SocketFlags.None, ep); #if NETFRAMEWORK diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs index 2e69127c3..95f219e26 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; using System.Threading; using System.Threading.Tasks; @@ -31,7 +34,7 @@ public void ConnectToTunnel() tunnelConnection.ConnectAsync(); } - public int[] CreatePlayerConnections(List playerIds) + public Tuple CreatePlayerConnections(List playerIds) { int[] ports = new int[playerIds.Count]; playerConnections = new Dictionary(); @@ -42,10 +45,30 @@ public int[] CreatePlayerConnections(List playerIds) playerConnection.CreateSocket(); ports[i] = playerConnection.PortNumber; playerConnections.Add(playerIds[i], playerConnection); - playerConnection.StartAsync(); } - return ports; + int gamePort = GetFreePort(ports); + + foreach (KeyValuePair playerConnection in playerConnections) + { + playerConnection.Value.StartAsync(gamePort); + } + + return new Tuple(ports, gamePort); + } + + private static int GetFreePort(int[] playerPorts) + { + IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); + int[] usedPorts = endPoints.Select(q => q.Port).ToArray().Concat(playerPorts).ToArray(); + int selectedPort = 0; + + while (selectedPort == 0 || usedPorts.Contains(selectedPort)) + { + selectedPort = new Random().Next(1, 65535); + } + + return selectedPort; } public void Clear() diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index 0ad6ff889..7916765a6 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -1,5 +1,7 @@ using System; +#if !NETFRAMEWORK using System.Buffers; +#endif using System.Net; using System.Net.Sockets; using System.Threading; @@ -60,6 +62,7 @@ private set private Socket socket; private EndPoint endPoint; + private EndPoint remoteEndPoint; private readonly SemaphoreSlim locker = new(1, 1); @@ -78,13 +81,13 @@ public void CreateSocket() socket.Bind(endPoint); PortNumber = ((IPEndPoint)socket.LocalEndPoint).Port; - Logger.Log($"Tunnel_V3 Created local game connection for clientId {PlayerID} {socket.LocalEndPoint} ({PortNumber})."); } - public async Task StartAsync() + public async Task StartAsync(int gamePort) { try { + remoteEndPoint = new IPEndPoint(IPAddress.Loopback, gamePort); #if NETFRAMEWORK byte[] buffer1 = new byte[1024]; var buffer = new ArraySegment(buffer1); @@ -100,20 +103,9 @@ public async Task StartAsync() while (true) { if (Aborted) - { - Logger.Log($"Tunnel_V3 abort listening for game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); break; - } - -#if DEBUG - Logger.Log($"Tunnel_V3 listening for game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); -#endif - - SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, endPoint); -#if DEBUG - Logger.Log($"Tunnel_V3 received game data for {PlayerID} on {socket.LocalEndPoint} from {socket.RemoteEndPoint}."); -#endif + SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint); #if NETFRAMEWORK byte[] data = new byte[socketReceiveFromResult.ReceivedBytes]; @@ -165,19 +157,12 @@ public async Task SendPacketAsync(ReadOnlyMemory packet) { if (!aborted) { -#if DEBUG - Logger.Log($"Tunnel_V3 sending game data for {PlayerID} from {socket.LocalEndPoint} to {socket.RemoteEndPoint}."); -#endif #if NETFRAMEWORK - await socket.SendToAsync(buffer, SocketFlags.None, endPoint); + await socket.SendToAsync(buffer, SocketFlags.None, remoteEndPoint); #else - await socket.SendToAsync(packet, SocketFlags.None, endPoint); + await socket.SendToAsync(packet, SocketFlags.None, remoteEndPoint); #endif } - else - { - Logger.Log($"Tunnel_V3 abort sending game data for {PlayerID} from {socket.LocalEndPoint} to {socket.RemoteEndPoint}."); - } } finally { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index 88b4df677..0bb0a2744 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -7,7 +7,6 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using Exception = System.Exception; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -93,7 +92,8 @@ public async Task ConnectAsync() await tunnelSocket.SendToAsync(buffer, SocketFlags.None, tunnelEndPoint); - Logger.Log($"Tunnel_V3 Connection to tunnel server established. Entering receive loop using clientId {SenderId} for tunnel address {tunnelSocket.LocalEndPoint}."); Connected?.Invoke(this, EventArgs.Empty); + Logger.Log($"Connection to tunnel server established."); + Connected?.Invoke(this, EventArgs.Empty); } catch (SocketException ex) { @@ -105,9 +105,6 @@ public async Task ConnectAsync() tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; - Logger.Log("Connection to tunnel server established. Entering receive loop."); - Connected?.Invoke(this, EventArgs.Empty); - await ReceiveLoopAsync(); } catch (Exception ex) @@ -133,10 +130,6 @@ private async Task ReceiveLoopAsync() Logger.Log("Exiting receive loop."); return; } -#if DEBUG - - Logger.Log($"Tunnel_V3 Listening for server using {tunnelSocket.LocalEndPoint} to {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint}."); -#endif #if NETFRAMEWORK byte[] buffer1 = new byte[1024]; @@ -162,10 +155,7 @@ private async Task ReceiveLoopAsync() Memory data = buffer[8..socketReceiveFromResult.ReceivedBytes]; uint senderId = BitConverter.ToUInt32(buffer[..4].Span); #endif -#if DEBUG - Logger.Log($"Tunnel_V3 Received data from server on {tunnelSocket.LocalEndPoint} from {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint} from clientId {senderId}."); -#endif await gameTunnelHandler.TunnelConnection_MessageReceivedAsync(data, senderId); } } @@ -209,7 +199,7 @@ public async Task SendDataAsync(ReadOnlyMemory data, uint receiverId) using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(data.Length + 8); Memory packet = memoryOwner.Memory[..(data.Length + 8)]; if (!BitConverter.TryWriteBytes(packet.Span[..4], SenderId)) throw new Exception(); - if (!BitConverter.TryWriteBytes(packet.Span[5..8], receiverId)) throw new Exception(); + if (!BitConverter.TryWriteBytes(packet.Span[4..8], receiverId)) throw new Exception(); data.CopyTo(packet[8..]); #endif @@ -219,20 +209,12 @@ public async Task SendDataAsync(ReadOnlyMemory data, uint receiverId) { if (!aborted) { -#if DEBUG - Logger.Log($"Tunnel_V3 sending data using {tunnelSocket.LocalEndPoint} to server {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint} SenderId {SenderId} receiverId {receiverId}."); - -#endif #if NETFRAMEWORK await tunnelSocket.SendToAsync(new ArraySegment(packet), SocketFlags.None, tunnelEndPoint); #else await tunnelSocket.SendToAsync(packet, SocketFlags.None, tunnelEndPoint); #endif } - else - { - Logger.Log($"Tunnel_V3 abort sending data using {tunnelSocket.LocalEndPoint} to server {tunnelSocket.RemoteEndPoint} tunnelEndPoint {tunnelEndPoint} SenderId {SenderId} receiverId {receiverId}."); - } } finally { From bc2304a3c4a404caf48b9b331c61a780e33a9d52 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 17 Aug 2022 20:05:32 +0200 Subject: [PATCH 016/109] Cleanup --- .../Multiplayer/GameLobby/GameLobbyBase.cs | 3 +-- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 2 +- .../CnCNet/TunneledPlayerConnection.cs | 13 +++---------- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 17 ++++++----------- 4 files changed, 11 insertions(+), 24 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index dac240794..f8fb4d7dd 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1371,13 +1371,12 @@ private PlayerHouseInfo[] WriteSpawnIni() continue; string sectionName = "Other" + otherId; - string playerAddress = GetIPAddressForPlayer(pInfo); spawnIni.SetStringValue(sectionName, "Name", pInfo.Name); spawnIni.SetIntValue(sectionName, "Side", pHouseInfo.InternalSideIndex); spawnIni.SetBooleanValue(sectionName, "IsSpectator", pHouseInfo.IsSpectator); spawnIni.SetIntValue(sectionName, "Color", pHouseInfo.ColorIndex); - spawnIni.SetStringValue(sectionName, "Ip", playerAddress); + spawnIni.SetStringValue(sectionName, "Ip", GetIPAddressForPlayer(pInfo)); spawnIni.SetIntValue(sectionName, "Port", pInfo.Port); otherId++; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 491c1c49c..3d6da5979 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -81,7 +81,7 @@ public static CnCNetTunnel Parse(string str) public string Address { get => _ipAddress; - set + private set { _ipAddress = value; if (IPAddress.TryParse(_ipAddress, out IPAddress address)) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index 7916765a6..f2d368d8c 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -6,7 +6,6 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -18,7 +17,7 @@ internal sealed class TunneledPlayerConnection { private const int Timeout = 60000; - private GameTunnelHandler gameTunnelHandler; + private readonly GameTunnelHandler gameTunnelHandler; public TunneledPlayerConnection(uint playerId, GameTunnelHandler gameTunnelHandler) { @@ -143,9 +142,9 @@ public async Task StartAsync(int gamePort) } #if NETFRAMEWORK - public async Task SendPacketAsync(byte[] packet) + public async Task SendPacketAsync(byte[] buffer) { - var buffer = new ArraySegment(packet); + var packet = new ArraySegment(buffer); #else public async Task SendPacketAsync(ReadOnlyMemory packet) @@ -156,13 +155,7 @@ public async Task SendPacketAsync(ReadOnlyMemory packet) try { if (!aborted) - { -#if NETFRAMEWORK - await socket.SendToAsync(buffer, SocketFlags.None, remoteEndPoint); -#else await socket.SendToAsync(packet, SocketFlags.None, remoteEndPoint); -#endif - } } finally { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index 0bb0a2744..726e7f56f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -26,7 +26,7 @@ public V3TunnelConnection(CnCNetTunnel tunnel, GameTunnelHandler gameTunnelHandl public event EventHandler ConnectionFailed; public event EventHandler ConnectionCut; - public uint SenderId { get; set; } + public uint SenderId { get; } private bool aborted; public bool Aborted @@ -189,10 +189,11 @@ private void DoClose() #if NETFRAMEWORK public async Task SendDataAsync(byte[] data, uint receiverId) { - byte[] packet = new byte[data.Length + 8]; // 8 = sizeof(uint) * 2 - WriteSenderIdToBuffer(packet); - Array.Copy(BitConverter.GetBytes(receiverId), 0, packet, 4, sizeof(uint)); - Array.Copy(data, 0, packet, 8, data.Length); + byte[] buffer = new byte[data.Length + 8]; // 8 = sizeof(uint) * 2 + WriteSenderIdToBuffer(buffer); + Array.Copy(BitConverter.GetBytes(receiverId), 0, buffer, 4, sizeof(uint)); + Array.Copy(data, 0, buffer, 8, data.Length); + var packet = new ArraySegment(buffer); #else public async Task SendDataAsync(ReadOnlyMemory data, uint receiverId) { @@ -208,13 +209,7 @@ public async Task SendDataAsync(ReadOnlyMemory data, uint receiverId) try { if (!aborted) - { -#if NETFRAMEWORK - await tunnelSocket.SendToAsync(new ArraySegment(packet), SocketFlags.None, tunnelEndPoint); -#else await tunnelSocket.SendToAsync(packet, SocketFlags.None, tunnelEndPoint); -#endif - } } finally { From 511ac5e604fb62a7b3323310e5076d0c5c8a5cb2 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 19 Aug 2022 16:57:35 +0200 Subject: [PATCH 017/109] Ignore socket error when game not loaded yet --- .../Multiplayer/CnCNet/TunneledPlayerConnection.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index f2d368d8c..b32fbd7b6 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -1,6 +1,8 @@ using System; #if !NETFRAMEWORK using System.Buffers; +#else +using ClientCore; #endif using System.Net; using System.Net.Sockets; @@ -16,6 +18,9 @@ namespace DTAClient.Domain.Multiplayer.CnCNet internal sealed class TunneledPlayerConnection { private const int Timeout = 60000; + private const uint IOC_IN = 0x80000000; + private const uint IOC_VENDOR = 0x18000000; + private const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; private readonly GameTunnelHandler gameTunnelHandler; @@ -77,6 +82,15 @@ public void CreateSocket() { socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); endPoint = new IPEndPoint(IPAddress.Loopback, 0); + + // Disable ICMP port not reachable exceptions, happens when the game is still loading and has not yet opened the socket. +#if !NETFRAMEWORK + if (OperatingSystem.IsWindows()) +#else + if (!ProgramConstants.ISMONO) +#endif + socket.IOControl(unchecked((int)SIO_UDP_CONNRESET), new byte[] { 0 }, null); + socket.Bind(endPoint); PortNumber = ((IPEndPoint)socket.LocalEndPoint).Port; From 8451250d21b8b15e252868f1ddf69d63ee161ef0 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 19 Aug 2022 17:01:07 +0200 Subject: [PATCH 018/109] Use game DualMode socket --- .../Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index b32fbd7b6..2e8e37c33 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -80,7 +80,7 @@ public void Stop() /// public void CreateSocket() { - socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket = new Socket(SocketType.Dgram, ProtocolType.Udp); endPoint = new IPEndPoint(IPAddress.Loopback, 0); // Disable ICMP port not reachable exceptions, happens when the game is still loading and has not yet opened the socket. From fa939d274931a664fd03c7d0a00ae81ad8e033af Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 21 Aug 2022 23:33:46 +0200 Subject: [PATCH 019/109] Fix tunnel selection window --- .../Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs | 5 +---- .../DXGUI/Multiplayer/CnCNet/TunnelListBox.cs | 9 ++++----- .../Multiplayer/CnCNet/TunnelSelectionWindow.cs | 12 ++++++------ .../DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs | 5 +---- 4 files changed, 12 insertions(+), 19 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index f34bfd801..1b595b3cf 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -381,10 +381,7 @@ protected override void NotAllPresentNotification() } private void ShowTunnelSelectionWindow(string description) - { - tunnelSelectionWindow.Open(description, - tunnelHandler.CurrentTunnel?.Address); - } + => tunnelSelectionWindow.Open(description, tunnelHandler.CurrentTunnel); private void TunnelSelectionWindow_TunnelSelected(object sender, TunnelEventArgs e) { diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs index 1e1a0ba46..e2b8fdac1 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs @@ -47,19 +47,18 @@ public TunnelListBox(WindowManager windowManager, TunnelHandler tunnelHandler) : private bool isManuallySelectedTunnel; private string manuallySelectedTunnelAddress; - /// /// Selects a tunnel from the list with the given address. /// - /// The address of the tunnel server to select. - public void SelectTunnel(string address) + /// The tunnel server to select. + public void SelectTunnel(CnCNetTunnel cnCNetTunnel) { - int index = tunnelHandler.Tunnels.FindIndex(t => t.Address == address); + int index = tunnelHandler.Tunnels.FindIndex(t => t == cnCNetTunnel); if (index > -1) { SelectedIndex = index; isManuallySelectedTunnel = true; - manuallySelectedTunnelAddress = address; + manuallySelectedTunnelAddress = cnCNetTunnel.Address; } } diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs index b445eed62..108ccc38b 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs @@ -97,14 +97,14 @@ private void LbTunnelList_SelectedIndexChanged(object sender, EventArgs e) => /// with the given address. /// /// The window description. - /// The address of the tunnel server to select. - public void Open(string description, string tunnelAddress = null) + /// The tunnel server to select. + public void Open(string description, CnCNetTunnel cnCNetTunnel) { lblDescription.Text = description; - originalTunnelAddress = tunnelAddress; + originalTunnelAddress = cnCNetTunnel.Address; - if (!string.IsNullOrWhiteSpace(tunnelAddress)) - lbTunnelList.SelectTunnel(tunnelAddress); + if (cnCNetTunnel is not null) + lbTunnelList.SelectTunnel(cnCNetTunnel); else lbTunnelList.SelectedIndex = -1; @@ -130,4 +130,4 @@ public TunnelEventArgs(CnCNetTunnel tunnel) public CnCNetTunnel Tunnel { get; } } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 0890c4683..04f41263f 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -389,10 +389,7 @@ private void PrintTunnelServerInformation(string s) } private void ShowTunnelSelectionWindow(string description) - { - tunnelSelectionWindow.Open(description, - tunnelHandler.CurrentTunnel?.Address); - } + => tunnelSelectionWindow.Open(description, tunnelHandler.CurrentTunnel); private void TunnelSelectionWindow_TunnelSelected(object sender, TunnelEventArgs e) { From a4256447e525130045fa97f31d743aafc7d9b101 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 20 Aug 2022 23:50:30 +0200 Subject: [PATCH 020/109] Replace obsolete WebClient with HttpClient, IRC & LAN IPv6 support with new Socket API --- .../DXGUI/Generic/GameInProgressWindow.cs | 2 +- DXMainClient/DXGUI/Generic/LoadingScreen.cs | 4 +- DXMainClient/DXGUI/Generic/MainMenu.cs | 53 +- DXMainClient/DXGUI/Generic/TopBar.cs | 18 +- DXMainClient/DXGUI/Generic/UpdateWindow.cs | 13 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 482 +++--- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 827 ++++++---- .../Multiplayer/CnCNet/GlobalContextMenu.cs | 71 +- .../CnCNet/PrivateMessagingWindow.cs | 72 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 165 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 1466 ++++++++++------- .../Multiplayer/GameLobby/GameLobbyBase.cs | 377 +++-- .../Multiplayer/GameLobby/LANGameLobby.cs | 858 ++++++---- .../GameLobby/MultiplayerGameLobby.cs | 817 +++++---- .../Multiplayer/GameLobby/SkirmishLobby.cs | 60 +- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 515 +++--- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 430 +++-- .../CnCNet/CnCNetPlayerCountTask.cs | 55 +- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 52 +- .../Multiplayer/CnCNet/ExtendedWebClient.cs | 25 - .../Domain/Multiplayer/CnCNet/MapSharer.cs | 368 ++--- .../Multiplayer/CnCNet/TunnelHandler.cs | 52 +- .../CnCNet/TunneledPlayerConnection.cs | 8 +- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 13 +- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 194 +-- DXMainClient/Domain/Multiplayer/MapLoader.cs | 12 - DXMainClient/Online/Channel.cs | 43 +- DXMainClient/Online/CnCNetManager.cs | 255 ++- DXMainClient/Online/Connection.cs | 710 ++++---- DXMainClient/Online/IConnectionManager.cs | 18 +- DXMainClient/PreStartup.cs | 43 +- DXMainClient/Startup.cs | 10 +- 32 files changed, 4552 insertions(+), 3536 deletions(-) delete mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/ExtendedWebClient.cs diff --git a/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs b/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs index dad31dc45..ec7ad9e8e 100644 --- a/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs @@ -129,7 +129,7 @@ private void SharedUILogic_GameProcessStarted() private void SharedUILogic_GameProcessExited() { - AddCallback(new Action(HandleGameProcessExited), null); + AddCallback(HandleGameProcessExited); } private void HandleGameProcessExited() diff --git a/DXMainClient/DXGUI/Generic/LoadingScreen.cs b/DXMainClient/DXGUI/Generic/LoadingScreen.cs index fe8bc84ce..e612faf12 100644 --- a/DXMainClient/DXGUI/Generic/LoadingScreen.cs +++ b/DXMainClient/DXGUI/Generic/LoadingScreen.cs @@ -30,8 +30,6 @@ MapLoader mapLoader this.mapLoader = mapLoader; } - private static readonly object locker = new object(); - private MapLoader mapLoader; private PrivateMessagingPanel privateMessagingPanel; @@ -120,4 +118,4 @@ public override void Update(GameTime gameTime) } } } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index f928ac78d..528dd4290 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -20,6 +20,7 @@ using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using ClientUpdater; using DTAClient.Domain.Multiplayer; @@ -185,7 +186,7 @@ public override void Initialize() btnLan.IdleTexture = AssetLoader.LoadTexture("MainMenu/lan.png"); btnLan.HoverTexture = AssetLoader.LoadTexture("MainMenu/lan_c.png"); btnLan.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); - btnLan.LeftClick += BtnLan_LeftClick; + btnLan.LeftClick += (_, _) => BtnLan_LeftClickAsync(); btnOptions = new XNAClientButton(WindowManager); btnOptions.Name = nameof(btnOptions); @@ -500,7 +501,7 @@ private void FirstRunMessageBox_NoClicked(XNAMessageBox messageBox) private void SharedUILogic_GameProcessStarted() => MusicOff(); - private void WindowManager_GameClosing(object sender, EventArgs e) => Clean(); + private void WindowManager_GameClosing(object sender, EventArgs e) => CleanAsync(); private void SkirmishLobby_Exited(object sender, EventArgs e) { @@ -533,17 +534,24 @@ private void CnCNetInfoController_CnCNetGameCountUpdated(object sender, PlayerCo /// /// Attemps to "clean" the client session in a nice way if the user closes the game. /// - private void Clean() + private async Task CleanAsync() { - Updater.FileIdentifiersUpdated -= Updater_FileIdentifiersUpdated; + try + { + Updater.FileIdentifiersUpdated -= Updater_FileIdentifiersUpdated; - if (cncnetPlayerCountCancellationSource != null) cncnetPlayerCountCancellationSource.Cancel(); - topBar.Clean(); - if (UpdateInProgress) - Updater.StopUpdate(); + if (cncnetPlayerCountCancellationSource != null) cncnetPlayerCountCancellationSource.Cancel(); + topBar.Clean(); + if (UpdateInProgress) + Updater.StopUpdate(); - if (connectionManager.IsConnected) - connectionManager.Disconnect(); + if (connectionManager.IsConnected) + await connectionManager.DisconnectAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } /// @@ -834,17 +842,24 @@ private void BtnNewCampaign_LeftClick(object sender, EventArgs e) private void BtnLoadGame_LeftClick(object sender, EventArgs e) => innerPanel.Show(innerPanel.GameLoadingWindow); - private void BtnLan_LeftClick(object sender, EventArgs e) + private async Task BtnLan_LeftClickAsync() { - lanLobby.Open(); + try + { + await lanLobby.OpenAsync(); - if (UserINISettings.Instance.StopMusicOnMenu) - MusicOff(); + if (UserINISettings.Instance.StopMusicOnMenu) + MusicOff(); - if (connectionManager.IsConnected) - connectionManager.Disconnect(); + if (connectionManager.IsConnected) + await connectionManager.DisconnectAsync(); - topBar.SetLanMode(true); + topBar.SetLanMode(true); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void BtnCnCNet_LeftClick(object sender, EventArgs e) => topBar.SwitchToSecondary(); @@ -879,7 +894,7 @@ private void BtnExit_LeftClick(object sender, EventArgs e) } private void SharedUILogic_GameProcessExited() => - AddCallback(new Action(HandleGameProcessExited), null); + AddCallback(HandleGameProcessExited); private void HandleGameProcessExited() { @@ -984,7 +999,7 @@ private void FadeMusicExit() if (MediaPlayer.Volume > step) { MediaPlayer.Volume -= step; - AddCallback(new Action(FadeMusicExit), null); + AddCallback(FadeMusicExit); } else { diff --git a/DXMainClient/DXGUI/Generic/TopBar.cs b/DXMainClient/DXGUI/Generic/TopBar.cs index 639e7457b..89a039aa6 100644 --- a/DXMainClient/DXGUI/Generic/TopBar.cs +++ b/DXMainClient/DXGUI/Generic/TopBar.cs @@ -9,6 +9,7 @@ using ClientGUI; using ClientCore; using System.Threading; +using System.Threading.Tasks; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.Online.EventArguments; using DTAConfig; @@ -172,7 +173,7 @@ public override void Initialize() btnLogout.FontIndex = 1; btnLogout.Text = "Log Out".L10N("Client:Main:TopBarLogOut"); btnLogout.AllowClick = false; - btnLogout.LeftClick += BtnLogout_LeftClick; + btnLogout.LeftClick += (_, _) => BtnLogout_LeftClickAsync(); btnOptions = new XNAClientButton(WindowManager); btnOptions.Name = "btnOptions"; @@ -288,11 +289,18 @@ private void ConnectionEvent(string text) downTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS - EVENT_DOWN_TIME_WAIT_SECONDS); } - private void BtnLogout_LeftClick(object sender, EventArgs e) + private async Task BtnLogout_LeftClickAsync() { - connectionManager.Disconnect(); - LogoutEvent?.Invoke(this, null); - SwitchToPrimary(); + try + { + await connectionManager.DisconnectAsync(); + LogoutEvent?.Invoke(this, null); + SwitchToPrimary(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void ConnectionManager_Connected(object sender, EventArgs e) diff --git a/DXMainClient/DXGUI/Generic/UpdateWindow.cs b/DXMainClient/DXGUI/Generic/UpdateWindow.cs index 4448b43c0..44b03a8dd 100644 --- a/DXMainClient/DXGUI/Generic/UpdateWindow.cs +++ b/DXMainClient/DXGUI/Generic/UpdateWindow.cs @@ -155,13 +155,13 @@ private void Updater_FileIdentifiersUpdated() if (Updater.VersionState == VersionState.UNKNOWN) { XNAMessageBox.Show(WindowManager, "Force Update Failure".L10N("Client:Main:ForceUpdateFailureTitle"), "Checking for updates failed.".L10N("Client:Main:ForceUpdateFailureText")); - AddCallback(new Action(CloseWindow), null); + AddCallback(CloseWindow); return; } else if (Updater.VersionState == VersionState.OUTDATED && Updater.ManualUpdateRequired) { UpdateCancelled?.Invoke(this, EventArgs.Empty); - AddCallback(new Action(CloseWindow), null); + AddCallback(CloseWindow); return; } @@ -172,8 +172,7 @@ private void Updater_FileIdentifiersUpdated() private void Updater_LocalFileCheckProgressChanged(int checkedFileCount, int totalFileCount) { - AddCallback(new Action(UpdateFileProgress), - (checkedFileCount * 100 / totalFileCount)); + AddCallback(() => UpdateFileProgress(checkedFileCount * 100 / totalFileCount)); } private void UpdateFileProgress(int value) @@ -239,7 +238,7 @@ private void HandleUpdateProgressChange() private void Updater_OnFileDownloadCompleted(string archiveName) { - AddCallback(new FileDownloadCompletedDelegate(HandleFileDownloadCompleted), archiveName); + AddCallback(() => HandleFileDownloadCompleted(archiveName)); } private void HandleFileDownloadCompleted(string archiveName) @@ -249,7 +248,7 @@ private void HandleFileDownloadCompleted(string archiveName) private void Updater_OnUpdateCompleted() { - AddCallback(new Action(HandleUpdateCompleted), null); + AddCallback(HandleUpdateCompleted); } private void HandleUpdateCompleted() @@ -262,7 +261,7 @@ private void HandleUpdateCompleted() private void Updater_OnUpdateFailed(Exception ex) { - AddCallback(new Action(HandleUpdateFailed), ex.Message); + AddCallback(() => HandleUpdateFailed(ex.Message)); } private void HandleUpdateFailed(string updateFailureErrorMessage) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index 1b595b3cf..c7b859f9e 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; namespace DTAClient.DXGUI.Multiplayer.CnCNet { @@ -56,15 +57,15 @@ DiscordHandler discordHandler ctcpCommandHandlers = new CommandHandlerBase[] { - new NoParamCommandHandler(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, HandleNotAllPresentNotification), - new NoParamCommandHandler(GET_READY_CTCP_COMMAND, HandleGetReadyNotification), - new StringCommandHandler(FILE_HASH_CTCP_COMMAND, HandleFileHashCommand), - new StringCommandHandler(INVALID_FILE_HASH_CTCP_COMMAND, HandleCheaterNotification), + new NoParamCommandHandler(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, sender => HandleNotAllPresentNotificationAsync(sender)), + new NoParamCommandHandler(GET_READY_CTCP_COMMAND, sender => HandleGetReadyNotificationAsync(sender)), + new StringCommandHandler(FILE_HASH_CTCP_COMMAND, (sender, fileHash) => HandleFileHashCommandAsync(sender, fileHash)), + new StringCommandHandler(INVALID_FILE_HASH_CTCP_COMMAND, (sender, cheaterName) => HandleCheaterNotificationAsync(sender, cheaterName)), new IntCommandHandler(TUNNEL_PING_CTCP_COMMAND, HandleTunnelPing), - new StringCommandHandler(OPTIONS_CTCP_COMMAND, HandleOptionsMessage), + new StringCommandHandler(OPTIONS_CTCP_COMMAND, (sender, data) => HandleOptionsMessageAsync(sender, data)), new NoParamCommandHandler(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, HandleInvalidSaveIndexCommand), new StringCommandHandler(START_GAME_CTCP_COMMAND, HandleStartGameCommand), - new IntCommandHandler(PLAYER_READY_CTCP_COMMAND, HandlePlayerReadyRequest), + new IntCommandHandler(PLAYER_READY_CTCP_COMMAND, (sender, readyStatus) => HandlePlayerReadyRequestAsync(sender, readyStatus)), new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, HandleTunnelServerChangeMessage) }; } @@ -100,6 +101,10 @@ DiscordHandler discordHandler private TopBar topBar; + private EventHandler channel_UserLeftFunc; + private EventHandler channel_UserQuitIRCFunc; + private EventHandler channel_UserAddedFunc; + public override void Initialize() { dp = new DarkeningPanel(WindowManager); @@ -123,7 +128,7 @@ public override void Initialize() DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow); tunnelSelectionWindow.CenterOnParent(); tunnelSelectionWindow.Disable(); - tunnelSelectionWindow.TunnelSelected += TunnelSelectionWindow_TunnelSelected; + tunnelSelectionWindow.TunnelSelected += (_, e) => TunnelSelectionWindow_TunnelSelectedAsync(e); btnChangeTunnel = new XNAClientButton(WindowManager); btnChangeTunnel.Name = nameof(btnChangeTunnel); @@ -144,11 +149,11 @@ public override void Initialize() private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:"); - private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGame(); + private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGameAsync(); - private void ConnectionManager_Disconnected(object sender, EventArgs e) => Clear(); + private void ConnectionManager_Disconnected(object sender, EventArgs e) => ClearAsync(); - private void ConnectionManager_ConnectionLost(object sender, ConnectionLostEventArgs e) => Clear(); + private void ConnectionManager_ConnectionLost(object sender, ConnectionLostEventArgs e) => ClearAsync(); /// /// Sets up events and information before joining the channel. @@ -159,10 +164,14 @@ public void SetUp(bool isHost, CnCNetTunnel tunnel, Channel channel, this.channel = channel; this.hostName = hostName; + channel_UserLeftFunc = (_, args) => Channel_UserLeftAsync(args); + channel_UserQuitIRCFunc = (_, args) => Channel_UserQuitIRCAsync(args); + channel_UserAddedFunc = (_, args) => Channel_UserAddedAsync(args); + channel.MessageAdded += Channel_MessageAdded; - channel.UserAdded += Channel_UserAdded; - channel.UserLeft += Channel_UserLeft; - channel.UserQuitIRC += Channel_UserQuitIRC; + channel.UserAdded += channel_UserAddedFunc; + channel.UserLeft += channel_UserLeftFunc; + channel.UserQuitIRC += channel_UserQuitIRCFunc; channel.CTCPReceived += Channel_CTCPReceived; tunnelHandler.CurrentTunnel = tunnel; @@ -181,36 +190,43 @@ private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e) /// /// Clears event subscriptions and leaves the channel. /// - public void Clear() + public async Task ClearAsync() { - gameBroadcastTimer.Enabled = false; - - if (channel != null) + try { - // TODO leave channel only if we've joined the channel - channel.Leave(); + gameBroadcastTimer.Enabled = false; - channel.MessageAdded -= Channel_MessageAdded; - channel.UserAdded -= Channel_UserAdded; - channel.UserLeft -= Channel_UserLeft; - channel.UserQuitIRC -= Channel_UserQuitIRC; - channel.CTCPReceived -= Channel_CTCPReceived; + if (channel != null) + { + // TODO leave channel only if we've joined the channel + await channel.LeaveAsync(); - connectionManager.RemoveChannel(channel); - } + channel.MessageAdded -= Channel_MessageAdded; + channel.UserAdded -= channel_UserAddedFunc; + channel.UserLeft -= channel_UserLeftFunc; + channel.UserQuitIRC -= channel_UserQuitIRCFunc; + channel.CTCPReceived -= Channel_CTCPReceived; - if (Enabled) - { - Enabled = false; - Visible = false; + connectionManager.RemoveChannel(channel); + } - base.LeaveGame(); - } + if (Enabled) + { + Enabled = false; + Visible = false; - tunnelHandler.CurrentTunnel = null; - tunnelHandler.CurrentTunnelPinged -= TunnelHandler_CurrentTunnelPinged; + await base.LeaveGameAsync(); + } - topBar.RemovePrimarySwitchable(this); + tunnelHandler.CurrentTunnel = null; + tunnelHandler.CurrentTunnelPinged -= TunnelHandler_CurrentTunnelPinged; + + topBar.RemovePrimarySwitchable(this); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e) @@ -227,19 +243,19 @@ private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e) /// /// Called when the local user has joined the game channel. /// - public void OnJoined() + public async Task OnJoinedAsync() { FileHashCalculator fhc = new FileHashCalculator(); fhc.CalculateHashes(gameModes); if (IsHost) { - connectionManager.SendCustomMessage(new QueuedMessage( + await connectionManager.SendCustomMessageAsync(new QueuedMessage( string.Format("MODE {0} +klnNs {1} {2}", channel.ChannelName, channel.Password, SGPlayers.Count), QueuedMessageType.SYSTEM_MESSAGE, 50)); - connectionManager.SendCustomMessage(new QueuedMessage( + await connectionManager.SendCustomMessageAsync(new QueuedMessage( string.Format("TOPIC {0} :{1}", channel.ChannelName, ProgramConstants.CNCNET_PROTOCOL_REVISION + ";" + localGame.ToLower()), QueuedMessageType.SYSTEM_MESSAGE, 50)); @@ -252,9 +268,9 @@ public void OnJoined() } else { - channel.SendCTCPMessage(FILE_HASH_CTCP_COMMAND + " " + fhc.GetCompleteHash(), QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(FILE_HASH_CTCP_COMMAND + " " + fhc.GetCompleteHash(), QueuedMessageType.SYSTEM_MESSAGE, 10); - channel.SendCTCPMessage(TUNNEL_PING_CTCP_COMMAND + " " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(TUNNEL_PING_CTCP_COMMAND + " " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); if (tunnelHandler.CurrentTunnel.PingInMs < 0) AddNotice(string.Format("{0} - unknown ping to tunnel server.".L10N("Client:Main:PlayerUnknownPing"), ProgramConstants.PLAYERNAME)); @@ -268,7 +284,7 @@ public void OnJoined() UpdateDiscordPresence(true); } - private void Channel_UserAdded(object sender, ChannelUserEventArgs e) + private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) { PlayerInfo pInfo = new PlayerInfo(); pInfo.Name = e.User.IRCUser.Name; @@ -277,24 +293,24 @@ private void Channel_UserAdded(object sender, ChannelUserEventArgs e) sndJoinSound.Play(); - BroadcastOptions(); + await BroadcastOptionsAsync(); CopyPlayerDataToUI(); UpdateDiscordPresence(); } - private void Channel_UserLeft(object sender, UserNameEventArgs e) + private async Task Channel_UserLeftAsync(UserNameEventArgs e) { - RemovePlayer(e.UserName); + await RemovePlayerAsync(e.UserName); UpdateDiscordPresence(); } - private void Channel_UserQuitIRC(object sender, UserNameEventArgs e) + private async Task Channel_UserQuitIRCAsync(UserNameEventArgs e) { - RemovePlayer(e.UserName); + await RemovePlayerAsync(e.UserName); UpdateDiscordPresence(); } - private void RemovePlayer(string playerName) + private async Task RemovePlayerAsync(string playerName) { int index = Players.FindIndex(p => p.Name == playerName); @@ -312,7 +328,7 @@ private void RemovePlayer(string playerName) connectionManager.MainChannel.AddMessage(new ChatMessage( Color.Yellow, "The game host left the game!".L10N("Client:Main:HostLeft"))); - Clear(); + await ClearAsync(); } } @@ -326,7 +342,7 @@ private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) protected override void AddNotice(string message, Color color) => channel.AddMessage(new ChatMessage(color, message)); - protected override void BroadcastOptions() + protected override async Task BroadcastOptionsAsync() { if (!IsHost) return; @@ -346,36 +362,36 @@ protected override void BroadcastOptions() } message.Remove(message.Length - 1, 1); - channel.SendCTCPMessage(message.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 10); + await channel.SendCTCPMessageAsync(message.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 10); } - protected override void SendChatMessage(string message) + protected override Task SendChatMessageAsync(string message) { sndMessageSound.Play(); - channel.SendChatMessage(message, chatColor); + return channel.SendChatMessageAsync(message, chatColor); } - protected override void RequestReadyStatus() => - channel.SendCTCPMessage(PLAYER_READY_CTCP_COMMAND + " 1", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 10); + protected override Task RequestReadyStatusAsync() => + channel.SendCTCPMessageAsync(PLAYER_READY_CTCP_COMMAND + " 1", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 10); - protected override void GetReadyNotification() + protected override async Task GetReadyNotificationAsync() { - base.GetReadyNotification(); + await base.GetReadyNotificationAsync(); topBar.SwitchToPrimary(); if (IsHost) - channel.SendCTCPMessage(GET_READY_CTCP_COMMAND, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); + await channel.SendCTCPMessageAsync(GET_READY_CTCP_COMMAND, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); } - protected override void NotAllPresentNotification() + protected override async Task NotAllPresentNotificationAsync() { - base.NotAllPresentNotification(); + await base.NotAllPresentNotificationAsync(); if (IsHost) { - channel.SendCTCPMessage(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, + await channel.SendCTCPMessageAsync(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } } @@ -383,58 +399,93 @@ protected override void NotAllPresentNotification() private void ShowTunnelSelectionWindow(string description) => tunnelSelectionWindow.Open(description, tunnelHandler.CurrentTunnel); - private void TunnelSelectionWindow_TunnelSelected(object sender, TunnelEventArgs e) + private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) { - channel.SendCTCPMessage($"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", - QueuedMessageType.SYSTEM_MESSAGE, 10); - HandleTunnelServerChange(e.Tunnel); + try + { + await channel.SendCTCPMessageAsync($"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", + QueuedMessageType.SYSTEM_MESSAGE, 10); + HandleTunnelServerChange(e.Tunnel); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } #region CTCP Handlers - private void HandleGetReadyNotification(string sender) + private async Task HandleGetReadyNotificationAsync(string sender) { - if (sender != hostName) - return; + try + { + if (sender != hostName) + return; - GetReadyNotification(); + await GetReadyNotificationAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void HandleNotAllPresentNotification(string sender) + private async Task HandleNotAllPresentNotificationAsync(string sender) { - if (sender != hostName) - return; + try + { + if (sender != hostName) + return; - NotAllPresentNotification(); + await NotAllPresentNotificationAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void HandleFileHashCommand(string sender, string fileHash) + private async Task HandleFileHashCommandAsync(string sender, string fileHash) { - if (!IsHost) - return; - - if (fileHash != gameFilesHash) + try { - PlayerInfo pInfo = Players.Find(p => p.Name == sender); - - if (pInfo == null) + if (!IsHost) return; - pInfo.Verified = true; + if (fileHash != gameFilesHash) + { + PlayerInfo pInfo = Players.Find(p => p.Name == sender); + + if (pInfo == null) + return; + + pInfo.Verified = true; - HandleCheaterNotification(hostName, sender); // This is kinda hacky + await HandleCheaterNotificationAsync(hostName, sender); // This is kinda hacky + } + } + catch (Exception ex) + { + PreStartup.HandleException(ex); } } - private void HandleCheaterNotification(string sender, string cheaterName) + private async Task HandleCheaterNotificationAsync(string sender, string cheaterName) { - if (sender != hostName) - return; + try + { + if (sender != hostName) + return; - AddNotice(string.Format("{0} - modified files detected! They could be cheating!".L10N("Client:Main:PlayerCheating"), cheaterName), Color.Red); + AddNotice(string.Format("{0} - modified files detected! They could be cheating!".L10N("Client:Main:PlayerCheating"), cheaterName), Color.Red); - if (IsHost) - channel.SendCTCPMessage(INVALID_FILE_HASH_CTCP_COMMAND + " " + cheaterName, QueuedMessageType.SYSTEM_MESSAGE, 0); + if (IsHost) + await channel.SendCTCPMessageAsync(INVALID_FILE_HASH_CTCP_COMMAND + " " + cheaterName, QueuedMessageType.SYSTEM_MESSAGE, 0); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void HandleTunnelPing(string sender, int pingInMs) @@ -448,52 +499,59 @@ private void HandleTunnelPing(string sender, int pingInMs) /// /// Handles an options broadcast sent by the game host. /// - private void HandleOptionsMessage(string sender, string data) + private async Task HandleOptionsMessageAsync(string sender, string data) { - if (sender != hostName) - return; + try + { + if (sender != hostName) + return; - string[] parts = data.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + string[] parts = data.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length < 1) - return; + if (parts.Length < 1) + return; - int sgIndex = Conversions.IntFromString(parts[0], -1); + int sgIndex = Conversions.IntFromString(parts[0], -1); - if (sgIndex < 0) - return; + if (sgIndex < 0) + return; - if (sgIndex >= ddSavedGame.Items.Count) - { - AddNotice("The game host has selected an invalid saved game index!".L10N("Client:Main:HostInvalidIndex") + " " + sgIndex); - channel.SendCTCPMessage(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, QueuedMessageType.SYSTEM_MESSAGE, 10); - return; - } + if (sgIndex >= ddSavedGame.Items.Count) + { + AddNotice("The game host has selected an invalid saved game index!".L10N("Client:Main:HostInvalidIndex") + " " + sgIndex); + await channel.SendCTCPMessageAsync(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, QueuedMessageType.SYSTEM_MESSAGE, 10); + return; + } - ddSavedGame.SelectedIndex = sgIndex; + ddSavedGame.SelectedIndex = sgIndex; - Players.Clear(); + Players.Clear(); - for (int i = 1; i < parts.Length; i++) - { - string[] playerAndReadyStatus = parts[i].Split(':'); - if (playerAndReadyStatus.Length < 2) - return; + for (int i = 1; i < parts.Length; i++) + { + string[] playerAndReadyStatus = parts[i].Split(':'); + if (playerAndReadyStatus.Length < 2) + return; - string playerName = playerAndReadyStatus[0]; - int readyStatus = Conversions.IntFromString(playerAndReadyStatus[1], -1); + string playerName = playerAndReadyStatus[0]; + int readyStatus = Conversions.IntFromString(playerAndReadyStatus[1], -1); - if (string.IsNullOrEmpty(playerName) || readyStatus == -1) - return; + if (string.IsNullOrEmpty(playerName) || readyStatus == -1) + return; - PlayerInfo pInfo = new PlayerInfo(); - pInfo.Name = playerName; - pInfo.Ready = Convert.ToBoolean(readyStatus); + PlayerInfo pInfo = new PlayerInfo(); + pInfo.Name = playerName; + pInfo.Ready = Convert.ToBoolean(readyStatus); - Players.Add(pInfo); - } + Players.Add(pInfo); + } - CopyPlayerDataToUI(); + CopyPlayerDataToUI(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void HandleInvalidSaveIndexCommand(string sender) @@ -547,19 +605,26 @@ private void HandleStartGameCommand(string sender, string data) LoadGame(); } - private void HandlePlayerReadyRequest(string sender, int readyStatus) + private async Task HandlePlayerReadyRequestAsync(string sender, int readyStatus) { - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + try + { + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo == null) - return; + if (pInfo == null) + return; - pInfo.Ready = Convert.ToBoolean(readyStatus); + pInfo.Ready = Convert.ToBoolean(readyStatus); - CopyPlayerDataToUI(); + CopyPlayerDataToUI(); - if (IsHost) - BroadcastOptions(); + if (IsHost) + await BroadcastOptionsAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void HandleTunnelServerChangeMessage(string sender, string tunnelAddressAndPort) @@ -594,42 +659,49 @@ private void HandleTunnelServerChange(CnCNetTunnel tunnel) { tunnelHandler.CurrentTunnel = tunnel; AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("Client:Main:HostChangeTunnel"), tunnel.Name)); - //UpdatePing(); } #endregion - protected override void HostStartGame() + protected override async Task HostStartGameAsync() { - AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); - List playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(SGPlayers.Count); - - if (playerPorts.Count < Players.Count) + try { - ShowTunnelSelectionWindow(("An error occured while contacting the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); - AddNotice(("An error occured while contacting the specified CnCNet " + - "tunnel server. Please try using a different tunnel server").L10N("Client:Main:ConnectTunnelError2") + " ", Color.Yellow); - return; + AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); + List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(SGPlayers.Count); + + if (playerPorts.Count < Players.Count) + { + ShowTunnelSelectionWindow(("An error occured while contacting " + + "the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); + AddNotice(("An error occured while contacting the specified CnCNet " + + "tunnel server. Please try using a different tunnel server").L10N("Client:Main:ConnectTunnelError2"), Color.Yellow); + return; + } + + StringBuilder sb = new StringBuilder(START_GAME_CTCP_COMMAND + " "); + for (int pId = 0; pId < Players.Count; pId++) + { + Players[pId].Port = playerPorts[pId]; + sb.Append(Players[pId].Name); + sb.Append(";"); + sb.Append("0.0.0.0:"); + sb.Append(playerPorts[pId]); + sb.Append(";"); + } + sb.Remove(sb.Length - 1, 1); + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 9); + + AddNotice("Starting game...".L10N("Client:Main:StartingGame")); + + started = true; + + LoadGame(); } - - StringBuilder sb = new StringBuilder(START_GAME_CTCP_COMMAND + " "); - for (int pId = 0; pId < Players.Count; pId++) + catch (Exception ex) { - Players[pId].Port = playerPorts[pId]; - sb.Append(Players[pId].Name); - sb.Append(";"); - sb.Append("0.0.0.0:"); - sb.Append(playerPorts[pId]); - sb.Append(";"); + PreStartup.HandleException(ex); } - sb.Remove(sb.Length - 1, 1); - channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 9); - - AddNotice("Starting game...".L10N("Client:Main:StartingGame")); - - started = true; - - LoadGame(); } protected override void WriteSpawnIniAdditions(IniFile spawnIni) @@ -640,14 +712,21 @@ protected override void WriteSpawnIniAdditions(IniFile spawnIni) base.WriteSpawnIniAdditions(spawnIni); } - protected override void HandleGameProcessExited() + protected override async Task HandleGameProcessExitedAsync() { - base.HandleGameProcessExited(); + try + { + await base.HandleGameProcessExitedAsync(); - Clear(); + await ClearAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void LeaveGame() => Clear(); + protected override Task LeaveGameAsync() => ClearAsync(); public void ChangeChatColor(IRCColor chatColor) { @@ -655,50 +734,57 @@ public void ChangeChatColor(IRCColor chatColor) tbChatInput.TextColor = chatColor.XnaColor; } - private void BroadcastGame() + private async Task BroadcastGameAsync() { - Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); + try + { + Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); - if (broadcastChannel == null) - return; + if (broadcastChannel == null) + return; - StringBuilder sb = new StringBuilder("GAME "); - sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); - sb.Append(";"); - sb.Append(ProgramConstants.GAME_VERSION); - sb.Append(";"); - sb.Append(SGPlayers.Count); - sb.Append(";"); - sb.Append(channel.ChannelName); - sb.Append(";"); - sb.Append(channel.UIName); - sb.Append(";"); - if (started || Players.Count == SGPlayers.Count) - sb.Append("1"); - else - sb.Append("0"); - sb.Append("0"); // IsCustomPassword - sb.Append("0"); // Closed - sb.Append("1"); // IsLoadedGame - sb.Append("0"); // IsLadder - sb.Append(";"); - foreach (SavedGamePlayer sgPlayer in SGPlayers) + StringBuilder sb = new StringBuilder("GAME "); + sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); + sb.Append(";"); + sb.Append(ProgramConstants.GAME_VERSION); + sb.Append(";"); + sb.Append(SGPlayers.Count); + sb.Append(";"); + sb.Append(channel.ChannelName); + sb.Append(";"); + sb.Append(channel.UIName); + sb.Append(";"); + if (started || Players.Count == SGPlayers.Count) + sb.Append("1"); + else + sb.Append("0"); + sb.Append("0"); // IsCustomPassword + sb.Append("0"); // Closed + sb.Append("1"); // IsLoadedGame + sb.Append("0"); // IsLadder + sb.Append(";"); + foreach (SavedGamePlayer sgPlayer in SGPlayers) + { + sb.Append(sgPlayer.Name); + sb.Append(","); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(";"); + sb.Append((string)lblMapNameValue.Tag); + sb.Append(";"); + sb.Append((string)lblGameModeValue.Tag); + sb.Append(";"); + sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); + sb.Append(";"); + sb.Append(0); // LoadedGameId + + await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); + } + catch (Exception ex) { - sb.Append(sgPlayer.Name); - sb.Append(","); + PreStartup.HandleException(ex); } - - sb.Remove(sb.Length - 1, 1); - sb.Append(";"); - sb.Append((string)lblMapNameValue.Tag); - sb.Append(";"); - sb.Append((string)lblGameModeValue.Tag); - sb.Append(";"); - sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); - sb.Append(";"); - sb.Append(0); // LoadedGameId - - broadcastChannel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); } public override string GetSwitchName() => "Load Game".L10N("Client:Main:LoadGame"); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 2ca63e973..1f7ee26c5 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -18,6 +18,7 @@ using System.Linq; using System.Reflection; using System.Threading; +using System.Threading.Tasks; using ClientCore.Enums; using DTAConfig; using ClientCore.Extensions; @@ -37,8 +38,7 @@ internal sealed class CnCNetLobby : XNAWindow, ISwitchable public CnCNetLobby(WindowManager windowManager, CnCNetManager connectionManager, CnCNetGameLobby gameLobby, CnCNetGameLoadingLobby gameLoadingLobby, TopBar topBar, PrivateMessagingWindow pmWindow, TunnelHandler tunnelHandler, - GameCollection gameCollection, CnCNetUserData cncnetUserData, - OptionsWindow optionsWindow, MapLoader mapLoader) + GameCollection gameCollection, CnCNetUserData cncnetUserData, MapLoader mapLoader) : base(windowManager) { this.connectionManager = connectionManager; @@ -49,23 +49,20 @@ public CnCNetLobby(WindowManager windowManager, CnCNetManager connectionManager, this.pmWindow = pmWindow; this.gameCollection = gameCollection; this.cncnetUserData = cncnetUserData; - this.optionsWindow = optionsWindow; this.mapLoader = mapLoader; ctcpCommandHandlers = new CommandHandlerBase[] { - new StringCommandHandler(ProgramConstants.GAME_INVITE_CTCP_COMMAND, HandleGameInviteCommand), + new StringCommandHandler(ProgramConstants.GAME_INVITE_CTCP_COMMAND, (sender, argumentsString) => HandleGameInviteCommandAsync(sender, argumentsString)), new NoParamCommandHandler(ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND, HandleGameInvitationFailedNotification) }; topBar.LogoutEvent += LogoutEvent; } - private MapLoader mapLoader; - - private CnCNetManager connectionManager; - private CnCNetUserData cncnetUserData; - private readonly OptionsWindow optionsWindow; + private readonly MapLoader mapLoader; + private readonly CnCNetManager connectionManager; + private readonly CnCNetUserData cncnetUserData; private PlayerListBox lbPlayerList; private ChatListBox lbChatMessages; @@ -96,50 +93,53 @@ public CnCNetLobby(WindowManager windowManager, CnCNetManager connectionManager, private Channel currentChatChannel; - private GameCollection gameCollection; - - private Color cAdminNameColor; + private readonly GameCollection gameCollection; private Texture2D unknownGameIcon; - private Texture2D adminGameIcon; private EnhancedSoundEffect sndGameCreated; private EnhancedSoundEffect sndGameInviteReceived; - private IRCColor[] chatColors; + private readonly CnCNetGameLobby gameLobby; + private readonly CnCNetGameLoadingLobby gameLoadingLobby; - private CnCNetGameLobby gameLobby; - private CnCNetGameLoadingLobby gameLoadingLobby; - - private TunnelHandler tunnelHandler; + private readonly TunnelHandler tunnelHandler; private CnCNetLoginWindow loginWindow; - private TopBar topBar; + private readonly TopBar topBar; - private PrivateMessagingWindow pmWindow; + private readonly PrivateMessagingWindow pmWindow; private PasswordRequestWindow passwordRequestWindow; - private bool isInGameRoom = false; - private bool updateDenied = false; + private bool isInGameRoom; + private bool updateDenied; private string localGameID; private CnCNetGame localGame; - private List followedGames = new List(); + private readonly List followedGames = new List(); - private bool isJoiningGame = false; + private bool isJoiningGame; private HostedCnCNetGame gameOfLastJoinAttempt; private CancellationTokenSource gameCheckCancellation; - private CommandHandlerBase[] ctcpCommandHandlers; + private readonly CommandHandlerBase[] ctcpCommandHandlers; private InvitationIndex invitationIndex; private GameFiltersPanel panelGameFilters; + private EventHandler gameChannel_UserAddedFunc; + private EventHandler gameChannel_InvalidPasswordEntered_LoadedGameFunc; + private EventHandler gameLoadingChannel_UserAddedFunc; + private EventHandler gameChannel_InvalidPasswordEntered_NewGameFunc; + private EventHandler gameChannel_InviteOnlyErrorOnJoinFunc; + private EventHandler gameChannel_ChannelFullFunc; + private EventHandler gameChannel_TargetChangeTooFastFunc; + private void GameList_ClientRectangleUpdated(object sender, EventArgs e) { panelGameFilters.ClientRectangle = lbGameList.ClientRectangle; @@ -181,8 +181,8 @@ public override void Initialize() btnLogout.Name = nameof(btnLogout); btnLogout.ClientRectangle = new Rectangle(Width - 145, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); - btnLogout.Text = "Log Out".L10N("Client:Main:LogOut"); - btnLogout.LeftClick += BtnLogout_LeftClick; + btnLogout.Text = "Log Out".L10N("Client:Main:ButtonLogOut"); + btnLogout.LeftClick += (_, _) => BtnLogout_LeftClickAsync(); var gameListRectangle = new Rectangle( btnNewGame.X, 41, @@ -215,7 +215,7 @@ public override void Initialize() lbPlayerList.RightClick += LbPlayerList_RightClick; globalContextMenu = new GlobalContextMenu(WindowManager, connectionManager, cncnetUserData, pmWindow); - globalContextMenu.JoinEvent += (sender, args) => JoinUser(args.IrcUser, connectionManager.MainChannel); + globalContextMenu.JoinEvent += (_, args) => JoinUserAsync(args.IrcUser, connectionManager.MainChannel); lbChatMessages = new ChatListBox(WindowManager); lbChatMessages.Name = nameof(lbChatMessages); @@ -224,7 +224,7 @@ public override void Initialize() lbChatMessages.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbChatMessages.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); lbChatMessages.LineHeight = 16; - lbChatMessages.LeftClick += (sender, args) => lbGameList.SelectedIndex = -1; + lbChatMessages.LeftClick += (_, _) => lbGameList.SelectedIndex = -1; lbChatMessages.RightClick += LbChatMessages_RightClick; tbChatInput = new XNAChatTextBox(WindowManager); @@ -235,7 +235,7 @@ public override void Initialize() tbChatInput.Suggestion = "Type here to chat...".L10N("Client:Main:ChatHere"); tbChatInput.Enabled = false; tbChatInput.MaximumTextLength = 200; - tbChatInput.EnterPressed += TbChatInput_EnterPressed; + tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync(); lblColor = new XNALabel(WindowManager); lblColor.Name = nameof(lblColor); @@ -248,8 +248,6 @@ public override void Initialize() ddColor.ClientRectangle = new Rectangle(lblColor.X + 95, 12, 150, 21); - chatColors = connectionManager.GetIRCColors(); - foreach (IRCColor color in connectionManager.GetIRCColors()) { if (!color.Selectable) @@ -276,7 +274,7 @@ public override void Initialize() ddCurrentChannel.ClientRectangle = new Rectangle( lbChatMessages.Right - 200, ddColor.Y, 200, 21); - ddCurrentChannel.SelectedIndexChanged += DdCurrentChannel_SelectedIndexChanged; + ddCurrentChannel.SelectedIndexChanged += (_, _) => DdCurrentChannel_SelectedIndexChangedAsync(); ddCurrentChannel.AllowDropDown = false; lblCurrentChannel = new XNALabel(WindowManager); @@ -355,13 +353,19 @@ public override void Initialize() AddChild(btnGameSortAlpha); AddChild(btnGameFilterOptions); - panelGameFilters.VisibleChanged += GameFiltersPanel_VisibleChanged; CnCNetPlayerCountTask.CnCNetGameCountUpdated += OnCnCNetGameCountUpdated; - UpdateOnlineCount(CnCNetPlayerCountTask.PlayerCount); - pmWindow.SetJoinUserAction(JoinUser); + gameChannel_UserAddedFunc = (sender, e) => GameChannel_UserAddedAsync(sender, e); + gameChannel_InvalidPasswordEntered_LoadedGameFunc = (sender, _) => GameChannel_InvalidPasswordEntered_LoadedGameAsync(sender); + gameLoadingChannel_UserAddedFunc = (sender, e) => GameLoadingChannel_UserAddedAsync(sender, e); + gameChannel_InvalidPasswordEntered_NewGameFunc = (sender, _) => GameChannel_InvalidPasswordEntered_NewGameAsync(sender); + gameChannel_InviteOnlyErrorOnJoinFunc = (sender, _) => GameChannel_InviteOnlyErrorOnJoinAsync(sender); + gameChannel_ChannelFullFunc = (sender, _) => GameChannel_ChannelFullAsync(sender); + gameChannel_TargetChangeTooFastFunc = (sender, e) => GameChannel_TargetChangeTooFastAsync(sender, e); + + pmWindow.SetJoinUserAction((user, messageView) => JoinUserAsync(user, messageView)); base.Initialize(); @@ -527,16 +531,12 @@ private void PostUIInit() sndGameCreated = new EnhancedSoundEffect("gamecreated.wav"); sndGameInviteReceived = new EnhancedSoundEffect("pm.wav"); - cAdminNameColor = AssetLoader.GetColorFromString(ClientConfiguration.Instance.AdminNameColor); - var assembly = Assembly.GetAssembly(typeof(GameCollection)); using Stream unknownIconStream = assembly.GetManifestResourceStream("ClientCore.Resources.unknownicon.png"); - using Stream cncnetIconStream = assembly.GetManifestResourceStream("ClientCore.Resources.cncneticon.png"); unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream)); - adminGameIcon = AssetLoader.TextureFromImage(Image.Load(cncnetIconStream)); - connectionManager.WelcomeMessageReceived += ConnectionManager_WelcomeMessageReceived; + connectionManager.WelcomeMessageReceived += (_, _) => ConnectionManager_WelcomeMessageReceivedAsync(); connectionManager.Disconnected += ConnectionManager_Disconnected; connectionManager.PrivateCTCPReceived += ConnectionManager_PrivateCTCPReceived; @@ -550,8 +550,8 @@ private void PostUIInit() gameCreationPanel.AddChild(gcw); gameCreationPanel.Tag = gcw; gcw.Cancelled += Gcw_Cancelled; - gcw.GameCreated += Gcw_GameCreated; - gcw.LoadedGameCreated += Gcw_LoadedGameCreated; + gcw.GameCreated += (_, e) => Gcw_GameCreatedAsync(e); + gcw.LoadedGameCreated += (_, e) => Gcw_LoadedGameCreatedAsync(e); gameCreationPanel.Hide(); @@ -559,7 +559,7 @@ private void PostUIInit() string.Format("*** DTA CnCNet Client version {0} ***".L10N("Client:Main:CnCNetClientVersionMessage"), Assembly.GetAssembly(typeof(CnCNetLobby)).GetName().Version), lbChatMessages.FontIndex))); - connectionManager.BannedFromChannel += ConnectionManager_BannedFromChannel; + connectionManager.BannedFromChannel += (_, e) => ConnectionManager_BannedFromChannelAsync(e); loginWindow = new CnCNetLoginWindow(WindowManager); loginWindow.Connect += LoginWindow_Connect; @@ -584,79 +584,93 @@ private void PostUIInit() gameLobby.GameLeft += GameLobby_GameLeft; gameLoadingLobby.GameLeft += GameLoadingLobby_GameLeft; - UserINISettings.Instance.SettingsSaved += Instance_SettingsSaved; + UserINISettings.Instance.SettingsSaved += (_, _) => Instance_SettingsSavedAsync(); - GameProcessLogic.GameProcessStarted += SharedUILogic_GameProcessStarted; - GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited; + GameProcessLogic.GameProcessStarted += () => SharedUILogic_GameProcessStartedAsync(); + GameProcessLogic.GameProcessExited += () => SharedUILogic_GameProcessExitedAsync(); } /// /// Displays a message when the IRC server has informed that the local user /// has been banned from a channel that they're attempting to join. /// - private void ConnectionManager_BannedFromChannel(object sender, ChannelEventArgs e) + private async Task ConnectionManager_BannedFromChannelAsync(ChannelEventArgs e) { - var game = lbGameList.HostedGames.Find(hg => ((HostedCnCNetGame)hg).ChannelName == e.ChannelName); - - if (game == null) + try { - var chatChannel = connectionManager.FindChannel(e.ChannelName); - chatChannel?.AddMessage(new ChatMessage(Color.White, string.Format( - "Cannot join chat channel {0}, you're banned!".L10N("Client:Main:PlayerBannedByChannel"), chatChannel.UIName))); - return; - } + var game = lbGameList.HostedGames.Find(hg => ((HostedCnCNetGame)hg).ChannelName == e.ChannelName); - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format( - "Cannot join game {0}, you've been banned by the game host!".L10N("Client:Main:PlayerBannedByHost"), game.RoomName))); + if (game == null) + { + var chatChannel = connectionManager.FindChannel(e.ChannelName); + chatChannel?.AddMessage(new ChatMessage(Color.White, string.Format( + "Cannot join chat channel {0}, you're banned!".L10N("Client:Main:PlayerBannedByChannel"), chatChannel.UIName))); + return; + } - isJoiningGame = false; - if (gameOfLastJoinAttempt != null) + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format( + "Cannot join game {0}, you've been banned by the game host!".L10N("Client:Main:PlayerBannedByHost"), game.RoomName))); + + isJoiningGame = false; + if (gameOfLastJoinAttempt != null) + { + if (gameOfLastJoinAttempt.IsLoadedGame) + await gameLoadingLobby.ClearAsync(); + else + await gameLobby.ClearAsync(); + } + } + catch (Exception ex) { - if (gameOfLastJoinAttempt.IsLoadedGame) - gameLoadingLobby.Clear(); - else - gameLobby.Clear(); + PreStartup.HandleException(ex); } } - private void SharedUILogic_GameProcessStarted() + private Task SharedUILogic_GameProcessStartedAsync() { - connectionManager.SendCustomMessage(new QueuedMessage("AWAY " + (char)58 + "In-game", + return connectionManager.SendCustomMessageAsync(new QueuedMessage("AWAY " + (char)58 + "In-game", QueuedMessageType.SYSTEM_MESSAGE, 0)); } - private void SharedUILogic_GameProcessExited() + private Task SharedUILogic_GameProcessExitedAsync() { - connectionManager.SendCustomMessage(new QueuedMessage("AWAY", - QueuedMessageType.SYSTEM_MESSAGE, 0)); + return connectionManager.SendCustomMessageAsync(new QueuedMessage("AWAY", + QueuedMessageType.SYSTEM_MESSAGE, 0)); } - private void Instance_SettingsSaved(object sender, EventArgs e) + private async Task Instance_SettingsSavedAsync() { - if (!connectionManager.IsConnected) - return; - - foreach (CnCNetGame game in gameCollection.GameList) + try { - if (!game.Supported) - continue; - - if (game.InternalName.ToUpper() == localGameID) - continue; + if (!connectionManager.IsConnected) + return; - if (followedGames.Contains(game.InternalName) && - !UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) + foreach (CnCNetGame game in gameCollection.GameList) { - connectionManager.FindChannel(game.GameBroadcastChannel).Leave(); - followedGames.Remove(game.InternalName); - } - else if (!followedGames.Contains(game.InternalName) && - UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) - { - connectionManager.FindChannel(game.GameBroadcastChannel).Join(); - followedGames.Add(game.InternalName); + if (!game.Supported) + continue; + + if (game.InternalName.ToUpper() == localGameID) + continue; + + if (followedGames.Contains(game.InternalName) && + !UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) + { + await connectionManager.FindChannel(game.GameBroadcastChannel).LeaveAsync(); + followedGames.Remove(game.InternalName); + } + else if (!followedGames.Contains(game.InternalName) && + UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) + { + await connectionManager.FindChannel(game.GameBroadcastChannel).JoinAsync(); + followedGames.Add(game.InternalName); + } } } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void LbPlayerList_RightClick(object sender, EventArgs e) @@ -760,11 +774,11 @@ private void SetLogOutButtonText() btnLogout.Text = "Log Out".L10N("Client:Main:LogOut"); } - private void BtnJoinGame_LeftClick(object sender, EventArgs e) => JoinSelectedGame(); + private void BtnJoinGame_LeftClick(object sender, EventArgs e) => JoinSelectedGameAsync(); - private void LbGameList_DoubleLeftClick(object sender, EventArgs e) => JoinSelectedGame(); + private void LbGameList_DoubleLeftClick(object sender, EventArgs e) => JoinSelectedGameAsync(); - private void PasswordRequestWindow_PasswordEntered(object sender, PasswordEventArgs e) => _JoinGame(e.HostedGame, e.Password); + private void PasswordRequestWindow_PasswordEntered(object sender, PasswordEventArgs e) => _JoinGameAsync(e.HostedGame, e.Password); private string GetJoinGameErrorBase() { @@ -812,16 +826,23 @@ private string GetJoinGameError(HostedCnCNetGame hg) return GetJoinGameErrorBase(); } - private void JoinSelectedGame() + private async Task JoinSelectedGameAsync() { - var listedGame = (HostedCnCNetGame)lbGameList.SelectedItem?.Tag; - if (listedGame == null) - return; - var hostedGameIndex = lbGameList.HostedGames.IndexOf(listedGame); - JoinGameByIndex(hostedGameIndex, string.Empty); + try + { + var listedGame = (HostedCnCNetGame)lbGameList.SelectedItem?.Tag; + if (listedGame == null) + return; + var hostedGameIndex = lbGameList.HostedGames.IndexOf(listedGame); + await JoinGameByIndexAsync(hostedGameIndex, string.Empty); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private bool JoinGameByIndex(int gameIndex, string password) + private async Task JoinGameByIndexAsync(int gameIndex, string password) { string error = GetJoinGameErrorByIndex(gameIndex); if (!string.IsNullOrEmpty(error)) @@ -830,7 +851,7 @@ private bool JoinGameByIndex(int gameIndex, string password) return false; } - return JoinGame((HostedCnCNetGame)lbGameList.HostedGames[gameIndex], password, connectionManager.MainChannel); + return await JoinGameAsync((HostedCnCNetGame)lbGameList.HostedGames[gameIndex], password, connectionManager.MainChannel); } /// @@ -840,7 +861,7 @@ private bool JoinGameByIndex(int gameIndex, string password) /// The password to join with. /// The message view/list to write error messages to. /// - private bool JoinGame(HostedCnCNetGame hg, string password, IMessageView messageView) + private async Task JoinGameAsync(HostedCnCNetGame hg, string password, IMessageView messageView) { string error = GetJoinGameError(hg); if (!string.IsNullOrEmpty(error)) @@ -883,65 +904,85 @@ private bool JoinGame(HostedCnCNetGame hg, string password, IMessageView message } } - _JoinGame(hg, password); + await _JoinGameAsync(hg, password); return true; } - private void _JoinGame(HostedCnCNetGame hg, string password) + private async Task _JoinGameAsync(HostedCnCNetGame hg, string password) { - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, - string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName))); - isJoiningGame = true; - gameOfLastJoinAttempt = hg; + try + { + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName))); + isJoiningGame = true; + gameOfLastJoinAttempt = hg; - Channel gameChannel = connectionManager.CreateChannel(hg.RoomName, hg.ChannelName, false, true, password); - connectionManager.AddChannel(gameChannel); + Channel gameChannel = connectionManager.CreateChannel(hg.RoomName, hg.ChannelName, false, true, password); + connectionManager.AddChannel(gameChannel); - if (hg.IsLoadedGame) - { - gameLoadingLobby.SetUp(false, hg.TunnelServer, gameChannel, hg.HostName); - gameChannel.UserAdded += GameLoadingChannel_UserAdded; - //gameChannel.MessageAdded += GameLoadingChannel_MessageAdded; - gameChannel.InvalidPasswordEntered += GameChannel_InvalidPasswordEntered_LoadedGame; + if (hg.IsLoadedGame) + { + gameLoadingLobby.SetUp(false, hg.TunnelServer, gameChannel, hg.HostName); + gameChannel.UserAdded += gameLoadingChannel_UserAddedFunc; + gameChannel.InvalidPasswordEntered += gameChannel_InvalidPasswordEntered_LoadedGameFunc; + } + else + { + await gameLobby.SetUpAsync(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded, false); + gameChannel.UserAdded += gameChannel_UserAddedFunc; + gameChannel.InvalidPasswordEntered += gameChannel_InvalidPasswordEntered_NewGameFunc; + gameChannel.InviteOnlyErrorOnJoin += gameChannel_InviteOnlyErrorOnJoinFunc; + gameChannel.ChannelFull += gameChannel_ChannelFullFunc; + gameChannel.TargetChangeTooFast += gameChannel_TargetChangeTooFastFunc; + } + + await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + hg.ChannelName + " " + password, + QueuedMessageType.INSTANT_MESSAGE, 0)); } - else + catch (Exception ex) { - gameLobby.SetUp(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded, false); - gameChannel.UserAdded += GameChannel_UserAdded; - gameChannel.InvalidPasswordEntered += GameChannel_InvalidPasswordEntered_NewGame; - gameChannel.InviteOnlyErrorOnJoin += GameChannel_InviteOnlyErrorOnJoin; - gameChannel.ChannelFull += GameChannel_ChannelFull; - gameChannel.TargetChangeTooFast += GameChannel_TargetChangeTooFast; + PreStartup.HandleException(ex); } - - connectionManager.SendCustomMessage(new QueuedMessage("JOIN " + hg.ChannelName + " " + password, - QueuedMessageType.INSTANT_MESSAGE, 0)); } - private void GameChannel_TargetChangeTooFast(object sender, MessageEventArgs e) + private async Task GameChannel_TargetChangeTooFastAsync(object sender, MessageEventArgs e) { - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, e.Message)); - ClearGameJoinAttempt((Channel)sender); + try + { + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, e.Message)); + await ClearGameJoinAttemptAsync((Channel)sender); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void GameChannel_ChannelFull(object sender, EventArgs e) => + private Task GameChannel_ChannelFullAsync(object sender) => // We'd do the exact same things here, so we can just call the method below - GameChannel_InviteOnlyErrorOnJoin(sender, e); + GameChannel_InviteOnlyErrorOnJoinAsync(sender); - private void GameChannel_InviteOnlyErrorOnJoin(object sender, EventArgs e) + private async Task GameChannel_InviteOnlyErrorOnJoinAsync(object sender) { - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "The selected game is locked!".L10N("Client:Main:GameLocked"))); - var channel = (Channel)sender; + try + { + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "The selected game is locked!".L10N("Client:Main:GameLocked"))); + var channel = (Channel)sender; + + var game = FindGameByChannelName(channel.ChannelName); + if (game != null) + { + game.Locked = true; + SortAndRefreshHostedGames(); + } - var game = FindGameByChannelName(channel.ChannelName); - if (game != null) + await ClearGameJoinAttemptAsync((Channel)sender); + } + catch (Exception ex) { - game.Locked = true; - SortAndRefreshHostedGames(); + PreStartup.HandleException(ex); } - - ClearGameJoinAttempt((Channel)sender); } private HostedCnCNetGame FindGameByChannelName(string channelName) @@ -953,38 +994,45 @@ private HostedCnCNetGame FindGameByChannelName(string channelName) return (HostedCnCNetGame)game; } - private void GameChannel_InvalidPasswordEntered_NewGame(object sender, EventArgs e) + private async Task GameChannel_InvalidPasswordEntered_NewGameAsync(object sender) { - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "Incorrect password!".L10N("Client:Main:PasswordWrong"))); - ClearGameJoinAttempt((Channel)sender); + try + { + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "Incorrect password!".L10N("Client:Main:PasswordWrong"))); + await ClearGameJoinAttemptAsync((Channel)sender); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void GameChannel_UserAdded(object sender, Online.ChannelUserEventArgs e) + private async Task GameChannel_UserAddedAsync(object sender, Online.ChannelUserEventArgs e) { Channel gameChannel = (Channel)sender; if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) { ClearGameChannelEvents(gameChannel); - gameLobby.OnJoined(); + await gameLobby.OnJoinedAsync(); isInGameRoom = true; SetLogOutButtonText(); } } - private void ClearGameJoinAttempt(Channel channel) + private async Task ClearGameJoinAttemptAsync(Channel channel) { ClearGameChannelEvents(channel); - gameLobby.Clear(); + await gameLobby.ClearAsync(); } private void ClearGameChannelEvents(Channel channel) { - channel.UserAdded -= GameChannel_UserAdded; - channel.InvalidPasswordEntered -= GameChannel_InvalidPasswordEntered_NewGame; - channel.InviteOnlyErrorOnJoin -= GameChannel_InviteOnlyErrorOnJoin; - channel.ChannelFull -= GameChannel_ChannelFull; - channel.TargetChangeTooFast -= GameChannel_TargetChangeTooFast; + channel.UserAdded -= gameChannel_UserAddedFunc; + channel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_NewGameFunc; + channel.InviteOnlyErrorOnJoin -= gameChannel_InviteOnlyErrorOnJoinFunc; + channel.ChannelFull -= gameChannel_ChannelFullFunc; + channel.TargetChangeTooFast -= gameChannel_TargetChangeTooFastFunc; isJoiningGame = false; } @@ -1002,78 +1050,92 @@ private void BtnNewGame_LeftClick(object sender, EventArgs e) gcw.Refresh(); } - private void Gcw_GameCreated(object sender, GameCreationEventArgs e) + private async Task Gcw_GameCreatedAsync(GameCreationEventArgs e) { - if (gameLobby.Enabled || gameLoadingLobby.Enabled) - return; - - string channelName = RandomizeChannelName(); - string password = e.Password; - bool isCustomPassword = true; - if (string.IsNullOrEmpty(password)) + try { - password = Rampastring.Tools.Utilities.CalculateSHA1ForString( - channelName + e.GameRoomName).Substring(0, 10); - isCustomPassword = false; - } + if (gameLobby.Enabled || gameLoadingLobby.Enabled) + return; - Channel gameChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, password); - connectionManager.AddChannel(gameChannel); - gameLobby.SetUp(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword, false); - gameChannel.UserAdded += GameChannel_UserAdded; - //gameChannel.MessageAdded += GameChannel_MessageAdded; - connectionManager.SendCustomMessage(new QueuedMessage("JOIN " + channelName + " " + password, - QueuedMessageType.INSTANT_MESSAGE, 0)); - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, - string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); + string channelName = RandomizeChannelName(); + string password = e.Password; + bool isCustomPassword = true; + if (string.IsNullOrEmpty(password)) + { + password = Rampastring.Tools.Utilities.CalculateSHA1ForString( + channelName + e.GameRoomName).Substring(0, 10); + isCustomPassword = false; + } - gameCreationPanel.Hide(); + Channel gameChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, password); + connectionManager.AddChannel(gameChannel); + await gameLobby.SetUpAsync(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword, false); + gameChannel.UserAdded += gameChannel_UserAddedFunc; + //gameChannel.MessageAdded += GameChannel_MessageAdded; + await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + channelName + " " + password, + QueuedMessageType.INSTANT_MESSAGE, 0)); + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); - // update the friends window so it can enable the Invite option - pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); + gameCreationPanel.Hide(); + + // update the friends window so it can enable the Invite option + pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void Gcw_LoadedGameCreated(object sender, GameCreationEventArgs e) + private async Task Gcw_LoadedGameCreatedAsync(GameCreationEventArgs e) { - if (gameLobby.Enabled || gameLoadingLobby.Enabled) - return; + try + { + if (gameLobby.Enabled || gameLoadingLobby.Enabled) + return; - string channelName = RandomizeChannelName(); + string channelName = RandomizeChannelName(); - Channel gameLoadingChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, e.Password); - connectionManager.AddChannel(gameLoadingChannel); - gameLoadingLobby.SetUp(true, e.Tunnel, gameLoadingChannel, ProgramConstants.PLAYERNAME); - gameLoadingChannel.UserAdded += GameLoadingChannel_UserAdded; - connectionManager.SendCustomMessage(new QueuedMessage("JOIN " + channelName + " " + e.Password, - QueuedMessageType.INSTANT_MESSAGE, 0)); - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, - string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); + Channel gameLoadingChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, e.Password); + connectionManager.AddChannel(gameLoadingChannel); + gameLoadingLobby.SetUp(true, e.Tunnel, gameLoadingChannel, ProgramConstants.PLAYERNAME); + gameLoadingChannel.UserAdded += gameLoadingChannel_UserAddedFunc; + await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + channelName + " " + e.Password, + QueuedMessageType.INSTANT_MESSAGE, 0)); + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); - gameCreationPanel.Hide(); + gameCreationPanel.Hide(); - // update the friends window so it can enable the Invite option - pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); + // update the friends window so it can enable the Invite option + pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void GameChannel_InvalidPasswordEntered_LoadedGame(object sender, EventArgs e) + private async Task GameChannel_InvalidPasswordEntered_LoadedGameAsync(object sender) { var channel = (Channel)sender; - channel.UserAdded -= GameLoadingChannel_UserAdded; - channel.InvalidPasswordEntered -= GameChannel_InvalidPasswordEntered_LoadedGame; - gameLoadingLobby.Clear(); + channel.UserAdded -= gameLoadingChannel_UserAddedFunc; + channel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; + await gameLoadingLobby.ClearAsync(); isJoiningGame = false; } - private void GameLoadingChannel_UserAdded(object sender, ChannelUserEventArgs e) + private async Task GameLoadingChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) { Channel gameLoadingChannel = (Channel)sender; if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) { - gameLoadingChannel.UserAdded -= GameLoadingChannel_UserAdded; - gameLoadingChannel.InvalidPasswordEntered -= GameChannel_InvalidPasswordEntered_LoadedGame; + gameLoadingChannel.UserAdded -= gameLoadingChannel_UserAddedFunc; + gameLoadingChannel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; - gameLoadingLobby.OnJoined(); + await gameLoadingLobby.OnJoinedAsync(); isInGameRoom = true; isJoiningGame = false; } @@ -1096,16 +1158,23 @@ private string RandomizeChannelName() private void Gcw_Cancelled(object sender, EventArgs e) => gameCreationPanel.Hide(); - private void TbChatInput_EnterPressed(object sender, EventArgs e) + private async Task TbChatInput_EnterPressedAsync() { - if (string.IsNullOrEmpty(tbChatInput.Text)) - return; + try + { + if (string.IsNullOrEmpty(tbChatInput.Text)) + return; - IRCColor selectedColor = (IRCColor)ddColor.SelectedItem.Tag; + IRCColor selectedColor = (IRCColor)ddColor.SelectedItem.Tag; - currentChatChannel.SendChatMessage(tbChatInput.Text, selectedColor); + await currentChatChannel.SendChatMessageAsync(tbChatInput.Text, selectedColor); - tbChatInput.Text = string.Empty; + tbChatInput.Text = string.Empty; + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void SetChatColor() @@ -1148,40 +1217,47 @@ private void ConnectionManager_Disconnected(object sender, EventArgs e) gameCheckCancellation.Cancel(); } - private void ConnectionManager_WelcomeMessageReceived(object sender, EventArgs e) + private async Task ConnectionManager_WelcomeMessageReceivedAsync() { - btnNewGame.AllowClick = true; - btnJoinGame.AllowClick = true; - ddCurrentChannel.AllowDropDown = true; - tbChatInput.Enabled = true; + try + { + btnNewGame.AllowClick = true; + btnJoinGame.AllowClick = true; + ddCurrentChannel.AllowDropDown = true; + tbChatInput.Enabled = true; - Channel cncnetChannel = connectionManager.FindChannel("#cncnet"); - cncnetChannel.Join(); + Channel cncnetChannel = connectionManager.FindChannel("#cncnet"); + await cncnetChannel.JoinAsync(); - string localGameChatChannelName = gameCollection.GetGameChatChannelNameFromIdentifier(localGameID); - connectionManager.FindChannel(localGameChatChannelName).Join(); + string localGameChatChannelName = gameCollection.GetGameChatChannelNameFromIdentifier(localGameID); + await connectionManager.FindChannel(localGameChatChannelName).JoinAsync(); - string localGameBroadcastChannel = gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGameID); - connectionManager.FindChannel(localGameBroadcastChannel).Join(); + string localGameBroadcastChannel = gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGameID); + await connectionManager.FindChannel(localGameBroadcastChannel).JoinAsync(); - foreach (CnCNetGame game in gameCollection.GameList) - { - if (!game.Supported) - continue; - - if (game.InternalName.ToUpper() != localGameID) + foreach (CnCNetGame game in gameCollection.GameList) { - if (UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) + if (!game.Supported) + continue; + + if (game.InternalName.ToUpper() != localGameID) { - connectionManager.FindChannel(game.GameBroadcastChannel).Join(); - followedGames.Add(game.InternalName); + if (UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) + { + await connectionManager.FindChannel(game.GameBroadcastChannel).JoinAsync(); + followedGames.Add(game.InternalName); + } } } - } - gameCheckCancellation = new CancellationTokenSource(); - CnCNetGameCheck gameCheck = new CnCNetGameCheck(); - gameCheck.InitializeService(gameCheckCancellation); + gameCheckCancellation = new CancellationTokenSource(); + CnCNetGameCheck gameCheck = new CnCNetGameCheck(); + gameCheck.InitializeService(gameCheckCancellation); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void ConnectionManager_PrivateCTCPReceived(object sender, PrivateCTCPEventArgs e) @@ -1195,95 +1271,109 @@ private void ConnectionManager_PrivateCTCPReceived(object sender, PrivateCTCPEve Logger.Log("Unhandled private CTCP command: " + e.Message + " from " + e.Sender); } - private void HandleGameInviteCommand(string sender, string argumentsString) + private async Task HandleGameInviteCommandAsync(string sender, string argumentsString) { - // arguments are semicolon-delimited - var arguments = argumentsString.Split(';'); + try + { + // arguments are semicolon-delimited + var arguments = argumentsString.Split(';'); - // we expect to be given a channel name, a (human-friendly) game name and optionally a password - if (arguments.Length < 2 || arguments.Length > 3) - return; + // we expect to be given a channel name, a (human-friendly) game name and optionally a password + if (arguments.Length < 2 || arguments.Length > 3) + return; - string channelName = arguments[0]; - string gameName = arguments[1]; - string password = (arguments.Length == 3) ? arguments[2] : string.Empty; + string channelName = arguments[0]; + string gameName = arguments[1]; + string password = (arguments.Length == 3) ? arguments[2] : string.Empty; - if (!CanReceiveInvitationMessagesFrom(sender)) - return; + if (!CanReceiveInvitationMessagesFrom(sender)) + return; - var gameIndex = lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName); + var gameIndex = lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName); - // also enforce user preference on whether to accept invitations from non-friends - // this is kept separate from CanReceiveInvitationMessagesFrom() as we still - // want to let the host know that we couldn't receive the invitation - if (!string.IsNullOrEmpty(GetJoinGameErrorByIndex(gameIndex)) || - (UserINISettings.Instance.AllowGameInvitesFromFriendsOnly && - !cncnetUserData.IsFriend(sender))) - { - // let the host know that we can't accept - // note this is not reached for the rejection case - connectionManager.SendCustomMessage(new QueuedMessage("PRIVMSG " + sender + " :\u0001" + - ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND + "\u0001", - QueuedMessageType.CHAT_MESSAGE, 0)); + // also enforce user preference on whether to accept invitations from non-friends + // this is kept separate from CanReceiveInvitationMessagesFrom() as we still + // want to let the host know that we couldn't receive the invitation + if (!string.IsNullOrEmpty(GetJoinGameErrorByIndex(gameIndex)) || + (UserINISettings.Instance.AllowGameInvitesFromFriendsOnly && + !cncnetUserData.IsFriend(sender))) + { + // let the host know that we can't accept + // note this is not reached for the rejection case + await connectionManager.SendCustomMessageAsync(new QueuedMessage("PRIVMSG " + sender + " :\u0001" + + ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND + "\u0001", + QueuedMessageType.CHAT_MESSAGE, 0)); - return; - } + return; + } - // if there's already an outstanding invitation from this user/channel combination, - // we don't want to display another - // we won't bother telling the host though, since their old invitation is still - // available to us - var invitationIdentity = new UserChannelPair(sender, channelName); + // if there's already an outstanding invitation from this user/channel combination, + // we don't want to display another + // we won't bother telling the host though, since their old invitation is still + // available to us + var invitationIdentity = new UserChannelPair(sender, channelName); - if (invitationIndex.ContainsKey(invitationIdentity)) - { - return; - } + if (invitationIndex.ContainsKey(invitationIdentity)) + { + return; + } - var gameInviteChoiceBox = new ChoiceNotificationBox(WindowManager); + var gameInviteChoiceBox = new ChoiceNotificationBox(WindowManager); - WindowManager.AddAndInitializeControl(gameInviteChoiceBox); + WindowManager.AddAndInitializeControl(gameInviteChoiceBox); - // show the invitation at top left; it will remain until it is acted upon or the target game is closed - gameInviteChoiceBox.Show( - "GAME INVITATION".L10N("Client:Main:GameInviteTitle"), - GetUserTexture(sender), - sender, - string.Format("Join {0}?".L10N("Client:Main:GameInviteText"), gameName), - "Yes".L10N("Client:Main:ButtonYes"), "No".L10N("Client:Main:ButtonNo"), 0); + // show the invitation at top left; it will remain until it is acted upon or the target game is closed + gameInviteChoiceBox.Show( + "GAME INVITATION".L10N("Client:Main:GameInviteTitle"), + GetUserTexture(sender), + sender, + string.Format("Join {0}?".L10N("Client:Main:GameInviteText"), gameName), + "Yes".L10N("Client:Main:ButtonYes"), "No".L10N("Client:Main:ButtonNo"), 0); - // add the invitation to the index so we can remove it if the target game is closed - // also lets us silently ignore new invitations from the same person while this one is still outstanding - invitationIndex[invitationIdentity] = - new WeakReference(gameInviteChoiceBox); + // add the invitation to the index so we can remove it if the target game is closed + // also lets us silently ignore new invitations from the same person while this one is still outstanding + invitationIndex[invitationIdentity] = + new WeakReference(gameInviteChoiceBox); - gameInviteChoiceBox.AffirmativeClickedAction = delegate (ChoiceNotificationBox choiceBox) - { - // if we're currently in a game lobby, first leave that channel - if (isInGameRoom) + gameInviteChoiceBox.AffirmativeClickedAction = async _ => { - gameLobby.LeaveGameLobby(); - } + try + { + // if we're currently in a game lobby, first leave that channel + if (isInGameRoom) + { + await gameLobby.LeaveGameLobbyAsync(); + } + + // JoinGameByIndex does bounds checking so we're safe to pass -1 if the game doesn't exist + if (!await JoinGameByIndexAsync(lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName), password)) + { + XNAMessageBox.Show(WindowManager, + "Failed to join".L10N("Client:Main:JoinFailedTitle"), + string.Format("Unable to join {0}'s game. The game may be locked or closed.".L10N("Client:Main:JoinFailedText"), sender)); + } + + // clean up the index as this invitation no longer exists + invitationIndex.Remove(invitationIdentity); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + }; - // JoinGameByIndex does bounds checking so we're safe to pass -1 if the game doesn't exist - if (!JoinGameByIndex(lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName), password)) + gameInviteChoiceBox.NegativeClickedAction = delegate (ChoiceNotificationBox choiceBox) { - XNAMessageBox.Show(WindowManager, - "Failed to join".L10N("Client:Main:JoinFailedTitle"), - string.Format("Unable to join {0}'s game. The game may be locked or closed.".L10N("Client:Main:JoinFailedText"), sender)); - } + // clean up the index as this invitation no longer exists + invitationIndex.Remove(invitationIdentity); + }; - // clean up the index as this invitation no longer exists - invitationIndex.Remove(invitationIdentity); - }; - - gameInviteChoiceBox.NegativeClickedAction = delegate (ChoiceNotificationBox choiceBox) + sndGameInviteReceived.Play(); + } + catch (Exception ex) { - // clean up the index as this invitation no longer exists - invitationIndex.Remove(invitationIdentity); - }; - - sndGameInviteReceived.Play(); + PreStartup.HandleException(ex); + } } private void HandleGameInvitationFailedNotification(string sender) @@ -1300,53 +1390,60 @@ private void HandleGameInvitationFailedNotification(string sender) } } - private void DdCurrentChannel_SelectedIndexChanged(object sender, EventArgs e) + private async Task DdCurrentChannel_SelectedIndexChangedAsync() { - if (currentChatChannel != null) + try { - currentChatChannel.UserAdded -= RefreshPlayerList; - currentChatChannel.UserLeft -= RefreshPlayerList; - currentChatChannel.UserQuitIRC -= RefreshPlayerList; - currentChatChannel.UserKicked -= RefreshPlayerList; - currentChatChannel.UserListReceived -= RefreshPlayerList; - currentChatChannel.MessageAdded -= CurrentChatChannel_MessageAdded; - currentChatChannel.UserGameIndexUpdated -= CurrentChatChannel_UserGameIndexUpdated; - - if (currentChatChannel.ChannelName != "#cncnet" && - currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) + if (currentChatChannel != null) { - // Remove the assigned channels from the users so we don't have ghost users on the PM user list - currentChatChannel.Users.DoForAllUsers(user => + currentChatChannel.UserAdded -= RefreshPlayerList; + currentChatChannel.UserLeft -= RefreshPlayerList; + currentChatChannel.UserQuitIRC -= RefreshPlayerList; + currentChatChannel.UserKicked -= RefreshPlayerList; + currentChatChannel.UserListReceived -= RefreshPlayerList; + currentChatChannel.MessageAdded -= CurrentChatChannel_MessageAdded; + currentChatChannel.UserGameIndexUpdated -= CurrentChatChannel_UserGameIndexUpdated; + + if (currentChatChannel.ChannelName != "#cncnet" && + currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) { - connectionManager.RemoveChannelFromUser(user.IRCUser.Name, currentChatChannel.ChannelName); - }); + // Remove the assigned channels from the users so we don't have ghost users on the PM user list + currentChatChannel.Users.DoForAllUsers(user => + { + connectionManager.RemoveChannelFromUser(user.IRCUser.Name, currentChatChannel.ChannelName); + }); - currentChatChannel.Leave(); + await currentChatChannel.LeaveAsync(); + } } - } - currentChatChannel = (Channel)ddCurrentChannel.SelectedItem.Tag; - currentChatChannel.UserAdded += RefreshPlayerList; - currentChatChannel.UserLeft += RefreshPlayerList; - currentChatChannel.UserQuitIRC += RefreshPlayerList; - currentChatChannel.UserKicked += RefreshPlayerList; - currentChatChannel.UserListReceived += RefreshPlayerList; - currentChatChannel.MessageAdded += CurrentChatChannel_MessageAdded; - currentChatChannel.UserGameIndexUpdated += CurrentChatChannel_UserGameIndexUpdated; - connectionManager.SetMainChannel(currentChatChannel); + currentChatChannel = (Channel)ddCurrentChannel.SelectedItem.Tag; + currentChatChannel.UserAdded += RefreshPlayerList; + currentChatChannel.UserLeft += RefreshPlayerList; + currentChatChannel.UserQuitIRC += RefreshPlayerList; + currentChatChannel.UserKicked += RefreshPlayerList; + currentChatChannel.UserListReceived += RefreshPlayerList; + currentChatChannel.MessageAdded += CurrentChatChannel_MessageAdded; + currentChatChannel.UserGameIndexUpdated += CurrentChatChannel_UserGameIndexUpdated; + connectionManager.SetMainChannel(currentChatChannel); - lbPlayerList.TopIndex = 0; + lbPlayerList.TopIndex = 0; - lbChatMessages.TopIndex = 0; - lbChatMessages.Clear(); - currentChatChannel.Messages.ForEach(msg => AddMessageToChat(msg)); + lbChatMessages.TopIndex = 0; + lbChatMessages.Clear(); + currentChatChannel.Messages.ForEach(msg => AddMessageToChat(msg)); - RefreshPlayerList(this, EventArgs.Empty); + RefreshPlayerList(this, EventArgs.Empty); - if (currentChatChannel.ChannelName != "#cncnet" && - currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) + if (currentChatChannel.ChannelName != "#cncnet" && + currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) + { + await currentChatChannel.JoinAsync(); + } + } + catch (Exception ex) { - currentChatChannel.Join(); + PreStartup.HandleException(ex); } } @@ -1563,21 +1660,28 @@ private void UpdateMessageBox_YesClicked(XNAMessageBox messageBox) => private void UpdateMessageBox_NoClicked(XNAMessageBox messageBox) => updateDenied = true; - private void BtnLogout_LeftClick(object sender, EventArgs e) + private async Task BtnLogout_LeftClickAsync() { - if (isInGameRoom) + try { + if (isInGameRoom) + { + topBar.SwitchToPrimary(); + return; + } + + if (connectionManager.IsConnected && + !UserINISettings.Instance.PersistentMode) + { + await connectionManager.DisconnectAsync(); + } + topBar.SwitchToPrimary(); - return; } - - if (connectionManager.IsConnected && - !UserINISettings.Instance.PersistentMode) + catch (Exception ex) { - connectionManager.Disconnect(); + PreStartup.HandleException(ex); } - - topBar.SwitchToPrimary(); } public void SwitchOn() @@ -1655,14 +1759,10 @@ private void DismissInvalidInvitations() private void DismissInvitation(UserChannelPair invitationIdentity) { - if (invitationIndex.ContainsKey(invitationIdentity)) + if (invitationIndex.TryGetValue(invitationIdentity, out WeakReference _)) { - var invitationNotification = invitationIndex[invitationIdentity].Target as ChoiceNotificationBox; - - if (invitationNotification != null) - { + if (invitationIndex[invitationIdentity].Target is ChoiceNotificationBox invitationNotification) WindowManager.RemoveControl(invitationNotification); - } invitationIndex.Remove(invitationIdentity); } @@ -1684,22 +1784,29 @@ private HostedCnCNetGame GetHostedGameForUser(IRCUser user) /// /// The user to join. /// The message view/list to write error messages to. - private void JoinUser(IRCUser user, IMessageView messageView) + private async Task JoinUserAsync(IRCUser user, IMessageView messageView) { - if (user == null) + try { - // can happen if a user is selected while offline - messageView.AddMessage(new ChatMessage(Color.White, "User is not currently available!".L10N("Client:Main:UserNotAvailable"))); - return; + if (user == null) + { + // can happen if a user is selected while offline + messageView.AddMessage(new ChatMessage(Color.White, "User is not currently available!".L10N("Client:Main:UserNotAvailable"))); + return; + } + var game = GetHostedGameForUser(user); + if (game == null) + { + messageView.AddMessage(new ChatMessage(Color.White, string.Format("{0} is not in a game!".L10N("Client:Main:UserNotInGame"), user.Name))); + return; + } + + await JoinGameAsync(game, string.Empty, messageView); } - var game = GetHostedGameForUser(user); - if (game == null) + catch (Exception ex) { - messageView.AddMessage(new ChatMessage(Color.White, string.Format("{0} is not in a game!".L10N("Client:Main:UserNotInGame"), user.Name))); - return; + PreStartup.HandleException(ex); } - - JoinGame(game, string.Empty, messageView); } } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs index 1f8d533f8..37d6b7d42 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading.Tasks; using ClientCore; using ClientCore.Extensions; using ClientGUI; @@ -72,12 +73,12 @@ public override void Initialize() toggleIgnoreItem = new XNAContextMenuItem() { Text = BLOCK, - SelectAction = () => GetIrcUserIdent(cncnetUserData.ToggleIgnoreUser) + SelectAction = () => GetIrcUserIdentAsync(cncnetUserData.ToggleIgnoreUser) }; invitePlayerItem = new XNAContextMenuItem() { Text = INVITE, - SelectAction = Invite + SelectAction = () => InviteAsync() }; joinPlayerItem = new XNAContextMenuItem() { @@ -104,24 +105,31 @@ public override void Initialize() AddItem(openLinkItem); } - private void Invite() + private async Task InviteAsync() { - // note it's assumed that if the channel name is specified, the game name must be also - if (string.IsNullOrEmpty(contextMenuData.inviteChannelName) || ProgramConstants.IsInGame) + try { - return; - } + // note it's assumed that if the channel name is specified, the game name must be also + if (string.IsNullOrEmpty(contextMenuData.inviteChannelName) || ProgramConstants.IsInGame) + { + return; + } + + string messageBody = ProgramConstants.GAME_INVITE_CTCP_COMMAND + " " + contextMenuData.inviteChannelName + ";" + contextMenuData.inviteGameName; - string messageBody = ProgramConstants.GAME_INVITE_CTCP_COMMAND + " " + contextMenuData.inviteChannelName + ";" + contextMenuData.inviteGameName; + if (!string.IsNullOrEmpty(contextMenuData.inviteChannelPassword)) + { + messageBody += ";" + contextMenuData.inviteChannelPassword; + } - if (!string.IsNullOrEmpty(contextMenuData.inviteChannelPassword)) + await connectionManager.SendCustomMessageAsync(new QueuedMessage( + "PRIVMSG " + GetIrcUser().Name + " :\u0001" + messageBody + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0 + )); + } + catch (Exception ex) { - messageBody += ";" + contextMenuData.inviteChannelPassword; + PreStartup.HandleException(ex); } - - connectionManager.SendCustomMessage(new QueuedMessage( - "PRIVMSG " + GetIrcUser().Name + " :\u0001" + messageBody + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0 - )); } private void UpdateButtons() @@ -185,25 +193,32 @@ private void CopyLink(string link) } } - private void GetIrcUserIdent(Action callback) + private async Task GetIrcUserIdentAsync(Action callback) { - var ircUser = GetIrcUser(); - - if (!string.IsNullOrEmpty(ircUser.Ident)) + try { - callback.Invoke(ircUser.Ident); - return; - } + var ircUser = GetIrcUser(); + + if (!string.IsNullOrEmpty(ircUser.Ident)) + { + callback.Invoke(ircUser.Ident); + return; + } + + void WhoIsReply(object sender, WhoEventArgs whoEventargs) + { + ircUser.Ident = whoEventargs.Ident; + callback.Invoke(whoEventargs.Ident); + connectionManager.WhoReplyReceived -= WhoIsReply; + } - void WhoIsReply(object sender, WhoEventArgs whoEventargs) + connectionManager.WhoReplyReceived += WhoIsReply; + await connectionManager.SendWhoIsMessageAsync(ircUser.Name); + } + catch (Exception ex) { - ircUser.Ident = whoEventargs.Ident; - callback.Invoke(whoEventargs.Ident); - connectionManager.WhoReplyReceived -= WhoIsReply; + PreStartup.HandleException(ex); } - - connectionManager.WhoReplyReceived += WhoIsReply; - connectionManager.SendWhoIsMessage(ircUser.Name); } private IRCUser GetIrcUser() diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs index 5c460ce6f..6d3681c30 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs @@ -12,6 +12,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using ClientCore.Enums; using ClientCore.Extensions; using SixLabors.ImageSharp; @@ -180,7 +181,7 @@ public override void Initialize() tbMessageInput.Name = nameof(tbMessageInput); tbMessageInput.ClientRectangle = new Rectangle(lbMessages.X, lbMessages.Bottom + 6, lbMessages.Width, 19); - tbMessageInput.EnterPressed += TbMessageInput_EnterPressed; + tbMessageInput.EnterPressed += (_, _) => TbMessageInput_EnterPressedAsync(); tbMessageInput.MaximumTextLength = 200; tbMessageInput.Enabled = false; @@ -515,52 +516,59 @@ private void ShowNotification(IRCUser ircUser, string message) private int FindItemIndexForName(string userName) => lbUserList.Items.FindIndex(MatchItemForName(userName)); - private void TbMessageInput_EnterPressed(object sender, EventArgs e) + private async Task TbMessageInput_EnterPressedAsync() { - if (string.IsNullOrEmpty(tbMessageInput.Text)) - return; + try + { + if (string.IsNullOrEmpty(tbMessageInput.Text)) + return; - if (lbUserList.SelectedItem == null) - return; + if (lbUserList.SelectedItem == null) + return; - string userName = lbUserList.SelectedItem.Text; + string userName = lbUserList.SelectedItem.Text; - connectionManager.SendCustomMessage(new QueuedMessage("PRIVMSG " + userName + " :" + tbMessageInput.Text, - QueuedMessageType.CHAT_MESSAGE, 0)); + await connectionManager.SendCustomMessageAsync(new QueuedMessage("PRIVMSG " + userName + " :" + tbMessageInput.Text, + QueuedMessageType.CHAT_MESSAGE, 0)); - PrivateMessageUser pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == userName); - if (pmUser == null) - { - IRCUser iu = connectionManager.UserList.Find(u => u.Name == userName); - - if (iu == null) + PrivateMessageUser pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == userName); + if (pmUser == null) { - Logger.Log("Null IRCUser in private messaging?"); - return; + IRCUser iu = connectionManager.UserList.Find(u => u.Name == userName); + + if (iu == null) + { + Logger.Log("Null IRCUser in private messaging?"); + return; + } + + pmUser = new PrivateMessageUser(iu); + privateMessageUsers.Add(pmUser); } - pmUser = new PrivateMessageUser(iu); - privateMessageUsers.Add(pmUser); - } + ChatMessage sentMessage = new ChatMessage(ProgramConstants.PLAYERNAME, + personalMessageColor, DateTime.Now, tbMessageInput.Text); - ChatMessage sentMessage = new ChatMessage(ProgramConstants.PLAYERNAME, - personalMessageColor, DateTime.Now, tbMessageInput.Text); + pmUser.Messages.Add(sentMessage); - pmUser.Messages.Add(sentMessage); + lbMessages.AddMessage(sentMessage); + if (sndMessageSound != null) + sndMessageSound.Play(); - lbMessages.AddMessage(sentMessage); - if (sndMessageSound != null) - sndMessageSound.Play(); + lastConversationPartner = userName; - lastConversationPartner = userName; + if (tabControl.SelectedTab != MESSAGES_INDEX) + { + tabControl.SelectedTab = MESSAGES_INDEX; + lbUserList.SelectedIndex = FindItemIndexForName(userName); + } - if (tabControl.SelectedTab != MESSAGES_INDEX) + tbMessageInput.Text = string.Empty; + } + catch (Exception ex) { - tabControl.SelectedTab = MESSAGES_INDEX; - lbUserList.SelectedIndex = FindItemIndexForName(userName); + PreStartup.HandleException(ex); } - - tbMessageInput.Text = string.Empty; } private void LbUserList_SelectedIndexChanged(object sender, EventArgs e) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index a2cdba568..498d80aed 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -11,6 +11,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; namespace DTAClient.DXGUI.Multiplayer { @@ -65,12 +66,10 @@ public GameLoadingLobbyBase(WindowManager windowManager, DiscordHandler discordH private List MPColors = new List(); - private string loadedGameID; - - private bool isSettingUp = false; + private bool isSettingUp; private FileSystemWatcher fsw; - private int uniqueGameId = 0; + private int uniqueGameId; private DateTime gameLoadTime; public override void Initialize() @@ -146,7 +145,7 @@ public override void Initialize() ddSavedGame.ClientRectangle = new Rectangle(lblSavedGameTime.X, panelPlayers.Bottom - 21, Width - lblSavedGameTime.X - 12, 21); - ddSavedGame.SelectedIndexChanged += DdSavedGame_SelectedIndexChanged; + ddSavedGame.SelectedIndexChanged += (_, _) => DdSavedGame_SelectedIndexChangedAsync(); lbChatMessages = new ChatListBox(WindowManager); lbChatMessages.Name = nameof(lbChatMessages); @@ -161,14 +160,14 @@ public override void Initialize() tbChatInput.ClientRectangle = new Rectangle(lbChatMessages.X, lbChatMessages.Bottom + 3, lbChatMessages.Width, 19); tbChatInput.MaximumTextLength = 200; - tbChatInput.EnterPressed += TbChatInput_EnterPressed; + tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync(); btnLoadGame = new XNAClientButton(WindowManager); btnLoadGame.Name = nameof(btnLoadGame); btnLoadGame.ClientRectangle = new Rectangle(lbChatMessages.X, tbChatInput.Bottom + 6, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLoadGame.Text = "Load Game".L10N("Client:Main:LoadGame"); - btnLoadGame.LeftClick += BtnLoadGame_LeftClick; + btnLoadGame.LeftClick += (_, _) => BtnLoadGame_LeftClickAsync(); btnLeaveGame = new XNAClientButton(WindowManager); btnLeaveGame.Name = nameof(btnLeaveGame); @@ -219,16 +218,25 @@ public override void Initialize() /// protected void ResetDiscordPresence() => discordHandler.UpdatePresence(); - private void BtnLeaveGame_LeftClick(object sender, EventArgs e) => LeaveGame(); + private void BtnLeaveGame_LeftClick(object sender, EventArgs e) => LeaveGameAsync(); - protected virtual void LeaveGame() + protected virtual Task LeaveGameAsync() { - GameLeft?.Invoke(this, EventArgs.Empty); - ResetDiscordPresence(); + try + { + GameLeft?.Invoke(this, EventArgs.Empty); + ResetDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return Task.CompletedTask; } private void fsw_Created(object sender, FileSystemEventArgs e) => - AddCallback(new Action(HandleFSWEvent), e); + AddCallback(() => HandleFSWEvent(e)); private void HandleFSWEvent(FileSystemEventArgs e) { @@ -240,32 +248,39 @@ private void HandleFSWEvent(FileSystemEventArgs e) } } - private void BtnLoadGame_LeftClick(object sender, EventArgs e) + private async Task BtnLoadGame_LeftClickAsync() { - if (!IsHost) + try { - RequestReadyStatus(); - return; - } + if (!IsHost) + { + await RequestReadyStatusAsync(); + return; + } - if (Players.Find(p => !p.Ready) != null) - { - GetReadyNotification(); - return; - } + if (Players.Find(p => !p.Ready) != null) + { + await GetReadyNotificationAsync(); + return; + } - if (Players.Count != SGPlayers.Count) + if (Players.Count != SGPlayers.Count) + { + await NotAllPresentNotificationAsync(); + return; + } + } + catch (Exception ex) { - NotAllPresentNotification(); - return; + PreStartup.HandleException(ex); } - HostStartGame(); + await HostStartGameAsync(); } - protected abstract void RequestReadyStatus(); + protected abstract Task RequestReadyStatusAsync(); - protected virtual void GetReadyNotification() + protected virtual Task GetReadyNotificationAsync() { AddNotice("The game host wants to load the game but cannot because not all players are ready!".L10N("Client:Main:GetReadyPlease")); @@ -275,12 +290,16 @@ protected virtual void GetReadyNotification() WindowManager.FlashWindow(); #endif + return Task.CompletedTask; } - protected virtual void NotAllPresentNotification() => + protected virtual Task NotAllPresentNotificationAsync() + { AddNotice("You cannot load the game before all players are present.".L10N("Client:Main:NotAllPresent")); + return Task.CompletedTask; + } - protected abstract void HostStartGame(); + protected abstract Task HostStartGameAsync(); protected void LoadGame() { @@ -342,31 +361,38 @@ protected void LoadGame() UpdateDiscordPresence(true); } - private void SharedUILogic_GameProcessExited() => - AddCallback(new Action(HandleGameProcessExited), null); + private void SharedUILogic_GameProcessExited() => AddCallback(HandleGameProcessExitedAsync); - protected virtual void HandleGameProcessExited() + protected virtual Task HandleGameProcessExitedAsync() { - fsw.EnableRaisingEvents = false; + try + { + fsw.EnableRaisingEvents = false; - GameProcessLogic.GameProcessExited -= SharedUILogic_GameProcessExited; + GameProcessLogic.GameProcessExited -= SharedUILogic_GameProcessExited; - var matchStatistics = StatisticsManager.Instance.GetMatchWithGameID(uniqueGameId); + var matchStatistics = StatisticsManager.Instance.GetMatchWithGameID(uniqueGameId); - if (matchStatistics != null) - { - int oldLength = matchStatistics.LengthInSeconds; - int newLength = matchStatistics.LengthInSeconds + - (int)(DateTime.Now - gameLoadTime).TotalSeconds; + if (matchStatistics != null) + { + int newLength = matchStatistics.LengthInSeconds + + (int)(DateTime.Now - gameLoadTime).TotalSeconds; - matchStatistics.ParseStatistics(ProgramConstants.GamePath, - ClientConfiguration.Instance.LocalGame, true); + matchStatistics.ParseStatistics(ProgramConstants.GamePath, + ClientConfiguration.Instance.LocalGame, true); - matchStatistics.LengthInSeconds = newLength; + matchStatistics.LengthInSeconds = newLength; - StatisticsManager.Instance.SaveDatabase(); + StatisticsManager.Instance.SaveDatabase(); + } + UpdateDiscordPresence(true); } - UpdateDiscordPresence(true); + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return Task.CompletedTask; } protected virtual void WriteSpawnIniAdditions(IniFile spawnIni) @@ -399,7 +425,6 @@ public void Refresh(bool isHost) IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "Saved Games", "spawnSG.ini")); - loadedGameID = spawnSGIni.GetStringValue("Settings", "GameID", "0"); lblMapNameValue.Tag = spawnSGIni.GetStringValue("Settings", "UIMapName", string.Empty); lblMapNameValue.Text = ((string)lblGameModeValue.Tag).L10N($"INI:Maps:{spawnSGIni.GetStringValue("Settings", "MapID", string.Empty)}:Description"); lblGameModeValue.Tag = spawnSGIni.GetStringValue("Settings", "UIGameMode", string.Empty); @@ -475,37 +500,51 @@ protected void CopyPlayerDataToUI() } } - private void DdSavedGame_SelectedIndexChanged(object sender, EventArgs e) + private async Task DdSavedGame_SelectedIndexChangedAsync() { - if (!IsHost) - return; + try + { + if (!IsHost) + return; - for (int i = 1; i < Players.Count; i++) - Players[i].Ready = false; + for (int i = 1; i < Players.Count; i++) + Players[i].Ready = false; - CopyPlayerDataToUI(); + CopyPlayerDataToUI(); - if (!isSettingUp) - BroadcastOptions(); - UpdateDiscordPresence(); + if (!isSettingUp) + await BroadcastOptionsAsync(); + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void TbChatInput_EnterPressed(object sender, EventArgs e) + private async Task TbChatInput_EnterPressedAsync() { - if (string.IsNullOrEmpty(tbChatInput.Text)) - return; + try + { + if (string.IsNullOrEmpty(tbChatInput.Text)) + return; - SendChatMessage(tbChatInput.Text); - tbChatInput.Text = string.Empty; + await SendChatMessageAsync(tbChatInput.Text); + tbChatInput.Text = string.Empty; + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } /// /// Override in a derived class to broadcast player ready statuses and the selected /// saved game to players. /// - protected abstract void BroadcastOptions(); + protected abstract Task BroadcastOptionsAsync(); - protected abstract void SendChatMessage(string message); + protected abstract Task SendChatMessageAsync(string message); public override void Draw(GameTime gameTime) { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 04f41263f..8894ad087 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -17,6 +17,7 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Extensions; @@ -77,35 +78,35 @@ DiscordHandler discordHandler ctcpCommandHandlers = new CommandHandlerBase[] { - new IntCommandHandler("OR", HandleOptionsRequest), - new IntCommandHandler("R", HandleReadyRequest), + new IntCommandHandler("OR", (playerName, options) => HandleOptionsRequestAsync(playerName, options)), + new IntCommandHandler("R", (playerName, options) => HandleReadyRequestAsync(playerName, options)), new StringCommandHandler("PO", ApplyPlayerOptions), new StringCommandHandler(PlayerExtraOptions.CNCNET_MESSAGE_KEY, ApplyPlayerExtraOptions), - new StringCommandHandler("GO", ApplyGameOptions), - new StringCommandHandler(GAME_START_MESSAGE, NonHostLaunchGame), + new StringCommandHandler("GO", (sender, message) => ApplyGameOptionsAsync(sender, message)), + new StringCommandHandler(GAME_START_MESSAGE, (sender, message) => NonHostLaunchGameAsync(sender, message)), new StringCommandHandler(GAME_START_MESSAGE_V3, HandleGameStartV3TunnelMessage), - new NoParamCommandHandler(TUNNEL_CONNECTION_OK_MESSAGE, HandleTunnelConnected), + new NoParamCommandHandler(TUNNEL_CONNECTION_OK_MESSAGE, playerName => HandleTunnelConnectedAsync(playerName)), new NoParamCommandHandler(TUNNEL_CONNECTION_FAIL_MESSAGE, HandleTunnelFail), - new NotificationHandler("AISPECS", HandleNotification, AISpectatorsNotification), - new NotificationHandler("GETREADY", HandleNotification, GetReadyNotification), - new NotificationHandler("INSFSPLRS", HandleNotification, InsufficientPlayersNotification), - new NotificationHandler("TMPLRS", HandleNotification, TooManyPlayersNotification), - new NotificationHandler("CLRS", HandleNotification, SharedColorsNotification), - new NotificationHandler("SLOC", HandleNotification, SharedStartingLocationNotification), - new NotificationHandler("LCKGME", HandleNotification, LockGameNotification), - new IntNotificationHandler("NVRFY", HandleIntNotification, NotVerifiedNotification), - new IntNotificationHandler("INGM", HandleIntNotification, StillInGameNotification), + new NotificationHandler("AISPECS", HandleNotification, () => AISpectatorsNotificationAsync()), + new NotificationHandler("GETREADY", HandleNotification, () => GetReadyNotificationAsync()), + new NotificationHandler("INSFSPLRS", HandleNotification, () => InsufficientPlayersNotificationAsync()), + new NotificationHandler("TMPLRS", HandleNotification, () => TooManyPlayersNotificationAsync()), + new NotificationHandler("CLRS", HandleNotification, () => SharedColorsNotificationAsync()), + new NotificationHandler("SLOC", HandleNotification, () => SharedStartingLocationNotificationAsync()), + new NotificationHandler("LCKGME", HandleNotification, () => LockGameNotificationAsync()), + new IntNotificationHandler("NVRFY", HandleIntNotification, (playerIndex) => NotVerifiedNotificationAsync(playerIndex)), + new IntNotificationHandler("INGM", HandleIntNotification, (playerIndex) => StillInGameNotificationAsync(playerIndex)), new StringCommandHandler(MAP_SHARING_UPLOAD_REQUEST, HandleMapUploadRequest), new StringCommandHandler(MAP_SHARING_FAIL_MESSAGE, HandleMapTransferFailMessage), new StringCommandHandler(MAP_SHARING_DOWNLOAD_REQUEST, HandleMapDownloadRequest), new NoParamCommandHandler(MAP_SHARING_DISABLED_MESSAGE, HandleMapSharingBlockedMessage), new NoParamCommandHandler("RETURN", ReturnNotification), new IntCommandHandler("TNLPNG", HandleTunnelPing), - new StringCommandHandler("FHSH", FileHashNotification), + new StringCommandHandler("FHSH", (sender, filesHash) => FileHashNotificationAsync(sender, filesHash)), new StringCommandHandler("MM", CheaterNotification), new StringCommandHandler(DICE_ROLL_MESSAGE, HandleDiceRollResult), new NoParamCommandHandler(CHEAT_DETECTED_MESSAGE, HandleCheatDetectedMessage), - new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, HandleTunnelServerChangeMessage) + new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, (sender, tunnelAddressAndPort) => HandleTunnelServerChangeMessageAsync(sender, tunnelAddressAndPort)) }; MapSharer.MapDownloadFailed += MapSharer_MapDownloadFailed; @@ -149,10 +150,10 @@ DiscordHandler discordHandler private int playerLimit; - private bool closed = false; + private bool closed; - private bool isCustomPassword = false; - private bool isP2P = false; + private bool isCustomPassword; + private bool isP2P; private List tunnelPlayerIds = new List(); private bool[] isPlayerConnectedToTunnel; @@ -178,11 +179,13 @@ DiscordHandler discordHandler /// private string lastMapName; - /// - /// The game mode of the latest selected map. - /// Used for map sharing. - /// - private string lastGameMode; + private EventHandler channel_UserAddedFunc; + private EventHandler channel_UserQuitIRCFunc; + private EventHandler channel_UserLeftFunc; + private EventHandler channel_UserKickedFunc; + private EventHandler channel_UserListReceivedFunc; + private EventHandler connectionManager_ConnectionLostFunc; + private EventHandler connectionManager_DisconnectedFunc; /// /// Set to true if host has selected invalid tunnel server. @@ -219,7 +222,7 @@ public override void Initialize() DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow); tunnelSelectionWindow.CenterOnParent(); tunnelSelectionWindow.Disable(); - tunnelSelectionWindow.TunnelSelected += TunnelSelectionWindow_TunnelSelected; + tunnelSelectionWindow.TunnelSelected += (_, e) => TunnelSelectionWindow_TunnelSelectedAsync(e); mapSharingConfirmationPanel = new MapSharingConfirmationPanel(WindowManager); MapPreviewBox.AddChild(mapSharingConfirmationPanel); @@ -233,6 +236,14 @@ public override void Initialize() MultiplayerNameRightClicked += MultiplayerName_RightClick; + channel_UserAddedFunc = (_, e) => Channel_UserAddedAsync(e); + channel_UserQuitIRCFunc = (_, e) => Channel_UserQuitIRCAsync(e); + channel_UserLeftFunc = (_, e) => Channel_UserLeftAsync(e); + channel_UserKickedFunc = (_, e) => Channel_UserKickedAsync(e); + channel_UserListReceivedFunc = (_, _) => Channel_UserListReceivedAsync(); + connectionManager_ConnectionLostFunc = (sender, e) => ConnectionManager_ConnectionLostAsync(sender, e); + connectionManager_DisconnectedFunc = (sender, e) => ConnectionManager_DisconnectedAsync(sender, e); + PostInitialize(); } @@ -259,7 +270,7 @@ private void GameStartTimer_TimeElapsed(object sender, EventArgs e) private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClickedEventArgs args) { - globalContextMenu.Show(new GlobalContextMenuData() + globalContextMenu.Show(new GlobalContextMenuData { PlayerName = args.PlayerName, PreventJoinGame = true @@ -268,20 +279,20 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")); - private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGame(); + private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGameAsync(); - public void SetUp(Channel channel, bool isHost, int playerLimit, + public async Task SetUpAsync(Channel channel, bool isHost, int playerLimit, CnCNetTunnel tunnel, string hostName, bool isCustomPassword, bool isP2P) { this.channel = channel; channel.MessageAdded += Channel_MessageAdded; channel.CTCPReceived += Channel_CTCPReceived; - channel.UserKicked += Channel_UserKicked; - channel.UserQuitIRC += Channel_UserQuitIRC; - channel.UserLeft += Channel_UserLeft; - channel.UserAdded += Channel_UserAdded; + channel.UserKicked += channel_UserKickedFunc; + channel.UserQuitIRC += channel_UserQuitIRCFunc; + channel.UserLeft += channel_UserLeftFunc; + channel.UserAdded += channel_UserAddedFunc; channel.UserNameChanged += Channel_UserNameChanged; - channel.UserListReceived += Channel_UserListReceived; + channel.UserListReceived += channel_UserListReceivedFunc; this.hostName = hostName; this.playerLimit = playerLimit; @@ -291,7 +302,7 @@ public void SetUp(Channel channel, bool isHost, int playerLimit, if (isHost) { RandomSeed = new Random().Next(); - RefreshMapSelectionUI(); + await RefreshMapSelectionUIAsync(); btnChangeTunnel.Enable(); } else @@ -304,15 +315,15 @@ public void SetUp(Channel channel, bool isHost, int playerLimit, tunnelHandler.CurrentTunnel = tunnel; tunnelHandler.CurrentTunnelPinged += TunnelHandler_CurrentTunnelPinged; - connectionManager.ConnectionLost += ConnectionManager_ConnectionLost; - connectionManager.Disconnected += ConnectionManager_Disconnected; + connectionManager.ConnectionLost += connectionManager_ConnectionLostFunc; + connectionManager.Disconnected += connectionManager_DisconnectedFunc; Refresh(isHost); } - private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e) => UpdatePing(); + private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e) => UpdatePingAsync(); - public void OnJoined() + public async Task OnJoinedAsync() { FileHashCalculator fhc = new FileHashCalculator(); fhc.CalculateHashes(GameModeMaps.GameModes); @@ -321,12 +332,12 @@ public void OnJoined() if (IsHost) { - connectionManager.SendCustomMessage(new QueuedMessage( + await connectionManager.SendCustomMessageAsync(new QueuedMessage( string.Format("MODE {0} +klnNs {1} {2}", channel.ChannelName, channel.Password, playerLimit), QueuedMessageType.SYSTEM_MESSAGE, 50)); - connectionManager.SendCustomMessage(new QueuedMessage( + await connectionManager.SendCustomMessageAsync(new QueuedMessage( string.Format("TOPIC {0} :{1}", channel.ChannelName, ProgramConstants.CNCNET_PROTOCOL_REVISION + ";" + localGame.ToLower()), QueuedMessageType.SYSTEM_MESSAGE, 50)); @@ -337,29 +348,36 @@ public void OnJoined() } else { - channel.SendCTCPMessage("FHSH " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync("FHSH " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); } TopBar.AddPrimarySwitchable(this); TopBar.SwitchToPrimary(); WindowManager.SelectedControl = tbChatInput; ResetAutoReadyCheckbox(); - UpdatePing(); + await UpdatePingAsync(); UpdateDiscordPresence(true); } - private void UpdatePing() + private async Task UpdatePingAsync() { - if (tunnelHandler.CurrentTunnel == null) - return; + try + { + if (tunnelHandler.CurrentTunnel == null) + return; - channel.SendCTCPMessage("TNLPNG " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync("TNLPNG " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); - PlayerInfo pInfo = Players.Find(p => p.Name.Equals(ProgramConstants.PLAYERNAME)); - if (pInfo != null) + PlayerInfo pInfo = Players.Find(p => p.Name.Equals(ProgramConstants.PLAYERNAME)); + if (pInfo != null) + { + pInfo.Ping = tunnelHandler.CurrentTunnel.PingInMs; + UpdatePlayerPingIndicator(pInfo); + } + } + catch (Exception ex) { - pInfo.Ping = tunnelHandler.CurrentTunnel.PingInMs; - UpdatePlayerPingIndicator(pInfo); + PreStartup.HandleException(ex); } } @@ -391,11 +409,18 @@ private void PrintTunnelServerInformation(string s) private void ShowTunnelSelectionWindow(string description) => tunnelSelectionWindow.Open(description, tunnelHandler.CurrentTunnel); - private void TunnelSelectionWindow_TunnelSelected(object sender, TunnelEventArgs e) + private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) { - channel.SendCTCPMessage($"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", - QueuedMessageType.SYSTEM_MESSAGE, 10); - HandleTunnelServerChange(e.Tunnel); + try + { + await channel.SendCTCPMessageAsync($"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", + QueuedMessageType.SYSTEM_MESSAGE, 10); + await HandleTunnelServerChangeAsync(e.Tunnel); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } public void ChangeChatColor(IRCColor chatColor) @@ -404,20 +429,20 @@ public void ChangeChatColor(IRCColor chatColor) tbChatInput.TextColor = chatColor.XnaColor; } - public override void Clear() + public override async Task ClearAsync() { - base.Clear(); + await base.ClearAsync(); if (channel != null) { channel.MessageAdded -= Channel_MessageAdded; channel.CTCPReceived -= Channel_CTCPReceived; - channel.UserKicked -= Channel_UserKicked; - channel.UserQuitIRC -= Channel_UserQuitIRC; - channel.UserLeft -= Channel_UserLeft; - channel.UserAdded -= Channel_UserAdded; + channel.UserKicked -= channel_UserKickedFunc; + channel.UserQuitIRC -= channel_UserQuitIRCFunc; + channel.UserLeft -= channel_UserLeftFunc; + channel.UserAdded -= channel_UserAddedFunc; channel.UserNameChanged -= Channel_UserNameChanged; - channel.UserListReceived -= Channel_UserListReceived; + channel.UserListReceived -= channel_UserListReceivedFunc; if (!IsHost) { @@ -428,8 +453,8 @@ public override void Clear() } Disable(); - connectionManager.ConnectionLost -= ConnectionManager_ConnectionLost; - connectionManager.Disconnected -= ConnectionManager_Disconnected; + connectionManager.ConnectionLost -= connectionManager_ConnectionLostFunc; + connectionManager.Disconnected -= connectionManager_DisconnectedFunc; gameBroadcastTimer.Enabled = false; closed = false; @@ -445,26 +470,41 @@ public override void Clear() ResetDiscordPresence(); } - public void LeaveGameLobby() + public async Task LeaveGameLobbyAsync() { - if (IsHost) + try { - closed = true; - BroadcastGame(); - } + if (IsHost) + { + closed = true; + await BroadcastGameAsync(); + } - Clear(); - channel.Leave(); + await ClearAsync(); + await channel.LeaveAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void ConnectionManager_Disconnected(object sender, EventArgs e) => HandleConnectionLoss(); + private Task ConnectionManager_DisconnectedAsync(object sender, EventArgs e) => HandleConnectionLossAsync(); - private void ConnectionManager_ConnectionLost(object sender, ConnectionLostEventArgs e) => HandleConnectionLoss(); + private Task ConnectionManager_ConnectionLostAsync(object sender, ConnectionLostEventArgs e) => HandleConnectionLossAsync(); - private void HandleConnectionLoss() + private async Task HandleConnectionLossAsync() { - Clear(); - Disable(); + try + { + await ClearAsync(); + Disable(); + + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) @@ -480,7 +520,7 @@ private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) } } - protected override void BtnLeaveGame_LeftClick(object sender, EventArgs e) => LeaveGameLobby(); + protected override Task BtnLeaveGame_LeftClickAsync(object sender, EventArgs e) => LeaveGameLobbyAsync(); protected override void UpdateDiscordPresence(bool resetTimer = false) { @@ -501,41 +541,59 @@ protected override void UpdateDiscordPresence(bool resetTimer = false) channel.UIName, IsHost, isCustomPassword, Locked, resetTimer); } - private void Channel_UserQuitIRC(object sender, UserNameEventArgs e) + private async Task Channel_UserQuitIRCAsync(UserNameEventArgs e) { - RemovePlayer(e.UserName); + try + { + await RemovePlayerAsync(e.UserName); - if (e.UserName == hostName) + if (e.UserName == hostName) + { + connectionManager.MainChannel.AddMessage(new ChatMessage( + ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); + await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + } + else + { + UpdateDiscordPresence(); + } + } + catch (Exception ex) { - connectionManager.MainChannel.AddMessage(new ChatMessage( - ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); - BtnLeaveGame_LeftClick(this, EventArgs.Empty); + PreStartup.HandleException(ex); } - else - UpdateDiscordPresence(); } - private void Channel_UserLeft(object sender, UserNameEventArgs e) + private async Task Channel_UserLeftAsync(UserNameEventArgs e) { - RemovePlayer(e.UserName); + try + { + await RemovePlayerAsync(e.UserName); - if (e.UserName == hostName) + if (e.UserName == hostName) + { + connectionManager.MainChannel.AddMessage(new ChatMessage( + ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); + await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + } + else + { + UpdateDiscordPresence(); + } + } + catch (Exception ex) { - connectionManager.MainChannel.AddMessage(new ChatMessage( - ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); - BtnLeaveGame_LeftClick(this, EventArgs.Empty); + PreStartup.HandleException(ex); } - else - UpdateDiscordPresence(); } - private void Channel_UserKicked(object sender, UserNameEventArgs e) + private async Task Channel_UserKickedAsync(UserNameEventArgs e) { if (e.UserName == ProgramConstants.PLAYERNAME) { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); - Clear(); + await ClearAsync(); this.Visible = false; this.Enabled = false; return; @@ -552,63 +610,77 @@ private void Channel_UserKicked(object sender, UserNameEventArgs e) } } - private void Channel_UserListReceived(object sender, EventArgs e) + private async Task Channel_UserListReceivedAsync() { - if (!IsHost) + try { - if (channel.Users.Find(hostName) == null) + if (!IsHost) { - connectionManager.MainChannel.AddMessage(new ChatMessage( - ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); - BtnLeaveGame_LeftClick(this, EventArgs.Empty); + if (channel.Users.Find(hostName) == null) + { + connectionManager.MainChannel.AddMessage(new ChatMessage( + ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); + await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + } } + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); } - UpdateDiscordPresence(); } - private void Channel_UserAdded(object sender, ChannelUserEventArgs e) + private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) { - PlayerInfo pInfo = new PlayerInfo(e.User.IRCUser.Name); - Players.Add(pInfo); + try + { + PlayerInfo pInfo = new PlayerInfo(e.User.IRCUser.Name); + Players.Add(pInfo); - if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT && AIPlayers.Count > 0) - AIPlayers.RemoveAt(AIPlayers.Count - 1); + if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT && AIPlayers.Count > 0) + AIPlayers.RemoveAt(AIPlayers.Count - 1); - sndJoinSound.Play(); + sndJoinSound.Play(); #if WINFORMS - WindowManager.FlashWindow(); + WindowManager.FlashWindow(); #endif - if (!IsHost) - { - CopyPlayerDataToUI(); - return; - } + if (!IsHost) + { + CopyPlayerDataToUI(); + return; + } - if (e.User.IRCUser.Name != ProgramConstants.PLAYERNAME) - { - // Changing the map applies forced settings (co-op sides etc.) to the - // new player, and it also sends an options broadcast message - //CopyPlayerDataToUI(); This is also called by ChangeMap() - ChangeMap(GameModeMap); - BroadcastPlayerOptions(); - BroadcastPlayerExtraOptions(); - UpdateDiscordPresence(); - } - else - { - Players[0].Ready = true; - CopyPlayerDataToUI(); - } + if (e.User.IRCUser.Name != ProgramConstants.PLAYERNAME) + { + // Changing the map applies forced settings (co-op sides etc.) to the + // new player, and it also sends an options broadcast message + //CopyPlayerDataToUI(); This is also called by ChangeMap() + await ChangeMapAsync(GameModeMap); + await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerExtraOptionsAsync(); + UpdateDiscordPresence(); + } + else + { + Players[0].Ready = true; + CopyPlayerDataToUI(); + } - if (Players.Count >= playerLimit) + if (Players.Count >= playerLimit) + { + AddNotice("Player limit reached. The game room has been locked.".L10N("Client:Main:GameRoomNumberLimitReached")); + await LockGameAsync(); + } + } + catch (Exception ex) { - AddNotice("Player limit reached. The game room has been locked.".L10N("Client:Main:GameRoomNumberLimitReached")); - LockGame(); + PreStartup.HandleException(ex); } } - private void RemovePlayer(string playerName) + private async Task RemovePlayerAsync(string playerName) { AbortGameStart(); @@ -622,15 +694,13 @@ private void RemovePlayer(string playerName) // This might not be necessary if (IsHost) - BroadcastPlayerOptions(); + await BroadcastPlayerOptionsAsync(); } sndLeaveSound.Play(); if (IsHost && Locked && !ProgramConstants.IsInGame) - { - UnlockGame(true); - } + await UnlockGameAsync(true); } private void Channel_ChannelModesChanged(object sender, ChannelModeEventArgs e) @@ -685,44 +755,51 @@ private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) /// /// Starts the game for the game host. /// - protected override void HostLaunchGame() + protected override async Task HostLaunchGameAsync() { - if (Players.Count > 1) + try { - if (isP2P) - throw new NotImplementedException("Peer-to-peer is not implemented yet."); + if (Players.Count > 1) + { + if (isP2P) + throw new NotImplementedException("Peer-to-peer is not implemented yet."); - AddNotice("Contacting tunnel server...".L10N("UI:Main:ConnectingTunnel")); + AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); - if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) - { - StartGame_V2Tunnel(); - } - else if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) - { - StartGame_V3Tunnel(); - } - else - { - throw new InvalidOperationException("Unknown tunnel server version!"); - } + if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) + { + await StartGame_V2TunnelAsync(); + } + else if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) + { + await StartGame_V3TunnelAsync(); + } + else + { + throw new InvalidOperationException("Unknown tunnel server version!"); + } - return; - } + return; + } - Logger.Log("One player MP -- starting!"); + Logger.Log("One player MP -- starting!"); - Players.ForEach(pInfo => pInfo.IsInGame = true); - CopyPlayerDataToUI(); + Players.ForEach(pInfo => pInfo.IsInGame = true); + CopyPlayerDataToUI(); - cncnetUserData.AddRecentPlayers(Players.Select(p => p.Name), channel.UIName); + cncnetUserData.AddRecentPlayers(Players.Select(p => p.Name), channel.UIName); - StartGame(); + await StartGameAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void StartGame_V2Tunnel() + private async Task StartGame_V2TunnelAsync() { - List playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(Players.Count); + List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(Players.Count); if (playerPorts.Count < Players.Count) { @@ -745,14 +822,14 @@ private void StartGame_V2Tunnel() sb.Append("0.0.0.0:"); sb.Append(playerPorts[pId]); } - channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); Players.ForEach(pInfo => pInfo.IsInGame = true); - StartGame(); + await StartGameAsync(); } - private void StartGame_V3Tunnel() + private async Task StartGame_V3TunnelAsync() { btnLaunchGame.InputEnabled = false; @@ -769,7 +846,7 @@ private void StartGame_V3Tunnel() sb.Append(id); tunnelPlayerIds.Add(id); } - channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); isStartingGame = true; ContactTunnel(); @@ -816,24 +893,38 @@ private void ContactTunnel() private void GameTunnelHandler_Connected(object sender, EventArgs e) { - AddCallback(new Action(GameTunnelHandler_Connected_Callback), null); + AddCallback(GameTunnelHandler_Connected_CallbackAsync); } - private void GameTunnelHandler_Connected_Callback() + private async Task GameTunnelHandler_Connected_CallbackAsync() { - isPlayerConnectedToTunnel[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)] = true; - channel.SendCTCPMessage(TUNNEL_CONNECTION_OK_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + try + { + isPlayerConnectedToTunnel[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)] = true; + await channel.SendCTCPMessageAsync(TUNNEL_CONNECTION_OK_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void GameTunnelHandler_ConnectionFailed(object sender, EventArgs e) { - AddCallback(new Action(GameTunnelHandler_ConnectionFailed_Callback), null); + AddCallback(GameTunnelHandler_ConnectionFailed_CallbackAsync); } - private void GameTunnelHandler_ConnectionFailed_Callback() + private async Task GameTunnelHandler_ConnectionFailed_CallbackAsync() { - channel.SendCTCPMessage(TUNNEL_CONNECTION_FAIL_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); - HandleTunnelFail(ProgramConstants.PLAYERNAME); + try + { + await channel.SendCTCPMessageAsync(TUNNEL_CONNECTION_FAIL_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); + HandleTunnelFail(ProgramConstants.PLAYERNAME); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void HandleTunnelFail(string playerName) @@ -844,7 +935,7 @@ private void HandleTunnelFail(string playerName) AbortGameStart(); } - private void HandleTunnelConnected(string playerName) + private async Task HandleTunnelConnectedAsync(string playerName) { if (!isStartingGame) return; @@ -879,7 +970,7 @@ private void HandleTunnelConnected(string playerName) Players.Single(p => p.Name == ProgramConstants.PLAYERNAME).Port = ports.Item2; gameStartTimer.Pause(); btnLaunchGame.InputEnabled = true; - StartGame(); + await StartGameAsync(); } } @@ -902,10 +993,9 @@ protected override string GetIPAddressForPlayer(PlayerInfo player) return base.GetIPAddressForPlayer(player); } - protected override void RequestPlayerOptions(int side, int color, int start, int team) + protected override Task RequestPlayerOptionsAsync(int side, int color, int start, int team) { - byte[] value = new byte[] - { + byte[] value = { (byte)side, (byte)color, (byte)start, @@ -914,12 +1004,12 @@ protected override void RequestPlayerOptions(int side, int color, int start, int int intValue = BitConverter.ToInt32(value, 0); - channel.SendCTCPMessage( + return channel.SendCTCPMessageAsync( string.Format("OR {0}", intValue), QueuedMessageType.GAME_SETTINGS_MESSAGE, 6); } - protected override void RequestReadyStatus() + protected override async Task RequestReadyStatusAsync() { if (Map == null || GameMode == null) { @@ -927,7 +1017,7 @@ protected override void RequestReadyStatus() "you will be unable to participate in the match.").L10N("Client:Main:HostMustReplaceMap")); if (chkAutoReady.Checked) - channel.SendCTCPMessage("R 0", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); + await channel.SendCTCPMessageAsync("R 0", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); return; } @@ -940,7 +1030,7 @@ protected override void RequestReadyStatus() else if (!pInfo.Ready) readyState = 1; - channel.SendCTCPMessage($"R {readyState}", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); + await channel.SendCTCPMessageAsync($"R {readyState}", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); } protected override void AddNotice(string message, Color color) => channel.AddMessage(new ChatMessage(color, message)); @@ -948,92 +1038,107 @@ protected override void RequestReadyStatus() /// /// Handles player option requests received from non-host players. /// - private void HandleOptionsRequest(string playerName, int options) + private async Task HandleOptionsRequestAsync(string playerName, int options) { - if (!IsHost) - return; + try + { + if (!IsHost) + return; - if (ProgramConstants.IsInGame) - return; + if (ProgramConstants.IsInGame) + return; - PlayerInfo pInfo = Players.Find(p => p.Name == playerName); + PlayerInfo pInfo = Players.Find(p => p.Name == playerName); - if (pInfo == null) - return; + if (pInfo == null) + return; - byte[] bytes = BitConverter.GetBytes(options); + byte[] bytes = BitConverter.GetBytes(options); - int side = bytes[0]; - int color = bytes[1]; - int start = bytes[2]; - int team = bytes[3]; + int side = bytes[0]; + int color = bytes[1]; + int start = bytes[2]; + int team = bytes[3]; - if (side < 0 || side > SideCount + RandomSelectorCount) - return; + if (side < 0 || side > SideCount + RandomSelectorCount) + return; - if (color < 0 || color > MPColors.Count) - return; + if (color < 0 || color > MPColors.Count) + return; - var disallowedSides = GetDisallowedSides(); + var disallowedSides = GetDisallowedSides(); - if (side > 0 && side <= SideCount && disallowedSides[side - 1]) - return; + if (side > 0 && side <= SideCount && disallowedSides[side - 1]) + return; - if (Map.CoopInfo != null) - { - if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) + if (Map.CoopInfo != null) + { + if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) + return; + + if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) + return; + } + + if (start < 0 || start > Map.MaxPlayers) return; - if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) + if (team < 0 || team > 4) return; - } - if (start < 0 || start > Map.MaxPlayers) - return; + if (side != pInfo.SideId + || start != pInfo.StartingLocation + || team != pInfo.TeamId) + { + ClearReadyStatuses(); + } - if (team < 0 || team > 4) - return; + pInfo.SideId = side; + pInfo.ColorId = color; + pInfo.StartingLocation = start; + pInfo.TeamId = team; - if (side != pInfo.SideId - || start != pInfo.StartingLocation - || team != pInfo.TeamId) + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + } + catch (Exception ex) { - ClearReadyStatuses(); + PreStartup.HandleException(ex); } - - pInfo.SideId = side; - pInfo.ColorId = color; - pInfo.StartingLocation = start; - pInfo.TeamId = team; - - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); } /// /// Handles "I'm ready" messages received from non-host players. /// - private void HandleReadyRequest(string playerName, int readyStatus) + private async Task HandleReadyRequestAsync(string playerName, int readyStatus) { - if (!IsHost) - return; + try + { + if (!IsHost) + return; - PlayerInfo pInfo = Players.Find(p => p.Name == playerName); + PlayerInfo pInfo = Players.Find(p => p.Name == playerName); - if (pInfo == null) - return; + if (pInfo == null) + return; - pInfo.Ready = readyStatus > 0; - pInfo.AutoReady = readyStatus > 1; + pInfo.Ready = readyStatus > 0; + pInfo.AutoReady = readyStatus > 1; - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } /// /// Broadcasts player options to non-host players. /// - protected override void BroadcastPlayerOptions() + protected override Task BroadcastPlayerOptionsAsync() { // Broadcast player options StringBuilder sb = new StringBuilder("PO "); @@ -1069,23 +1174,32 @@ protected override void BroadcastPlayerOptions() } } - channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.GAME_PLAYERS_MESSAGE, 11); + return channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_PLAYERS_MESSAGE, 11); } - protected override void PlayerExtraOptions_OptionsChanged(object sender, EventArgs e) + protected override async Task PlayerExtraOptions_OptionsChangedAsync(object sender, EventArgs e) { - base.PlayerExtraOptions_OptionsChanged(sender, e); - BroadcastPlayerExtraOptions(); + try + { + await base.PlayerExtraOptions_OptionsChangedAsync(sender, e); + await BroadcastPlayerExtraOptionsAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + await Task.CompletedTask; } - protected override void BroadcastPlayerExtraOptions() + protected override async Task BroadcastPlayerExtraOptionsAsync() { if (!IsHost) return; var playerExtraOptions = GetPlayerExtraOptions(); - channel.SendCTCPMessage(playerExtraOptions.ToCncnetMessage(), QueuedMessageType.GAME_PLAYERS_EXTRA_MESSAGE, 11, true); + await channel.SendCTCPMessageAsync(playerExtraOptions.ToCncnetMessage(), QueuedMessageType.GAME_PLAYERS_EXTRA_MESSAGE, 11, true); } /// @@ -1193,9 +1307,9 @@ private void ApplyPlayerOptions(string sender, string message) /// Broadcasts game options to non-host players /// when the host has changed an option. /// - protected override void OnGameOptionChanged() + protected override async Task OnGameOptionChangedAsync() { - base.OnGameOptionChanged(); + await base.OnGameOptionChangedAsync(); if (!IsHost) return; @@ -1235,180 +1349,186 @@ protected override void OnGameOptionChanged() sb.Append(Convert.ToInt32(RemoveStartingLocations)); sb.Append(Map.UntranslatedName); - channel.SendCTCPMessage(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); } /// /// Handles game option messages received from the game host. /// - private void ApplyGameOptions(string sender, string message) + private async Task ApplyGameOptionsAsync(string sender, string message) { - if (sender != hostName) - return; - - string[] parts = message.Split(';'); - - int checkBoxIntegerCount = (CheckBoxes.Count / 32) + 1; - - int partIndex = checkBoxIntegerCount + DropDowns.Count; - - if (parts.Length < partIndex + 6) + try { - AddNotice(("The game host has sent an invalid game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); - return; - } - - string mapOfficial = parts[partIndex]; - bool isMapOfficial = Conversions.BooleanFromString(mapOfficial, true); + if (sender != hostName) + return; - string mapSHA1 = parts[partIndex + 1]; + string[] parts = message.Split(';'); - string gameMode = parts[partIndex + 2]; + int checkBoxIntegerCount = (CheckBoxes.Count / 32) + 1; - int frameSendRate = Conversions.IntFromString(parts[partIndex + 3], FrameSendRate); - if (frameSendRate != FrameSendRate) - { - FrameSendRate = frameSendRate; - AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); - } + int partIndex = checkBoxIntegerCount + DropDowns.Count; - int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead); - if (maxAhead != MaxAhead) - { - MaxAhead = maxAhead; - AddNotice(string.Format("The game host has changed MaxAhead to {0}".L10N("Client:Main:HostChangeMaxAhead"), maxAhead)); - } + if (parts.Length < partIndex + 6) + { + AddNotice(("The game host has sent an invalid game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); + return; + } - int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion); - if (protocolVersion != ProtocolVersion) - { - ProtocolVersion = protocolVersion; - AddNotice(string.Format("The game host has changed ProtocolVersion to {0}".L10N("Client:Main:HostChangeProtocolVersion"), protocolVersion)); - } + string mapOfficial = parts[partIndex]; + bool isMapOfficial = Conversions.BooleanFromString(mapOfficial, true); - string mapName = parts[partIndex + 8]; - GameModeMap currentGameModeMap = GameModeMap; + string mapSHA1 = parts[partIndex + 1]; - lastGameMode = gameMode; - lastMapSHA1 = mapSHA1; - lastMapName = mapName; + string gameMode = parts[partIndex + 2]; - GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); - if (GameModeMap == null) - { - ChangeMap(null); + int frameSendRate = Conversions.IntFromString(parts[partIndex + 3], FrameSendRate); + if (frameSendRate != FrameSendRate) + { + FrameSendRate = frameSendRate; + AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); + } - if (!isMapOfficial) - RequestMap(mapSHA1); - else - ShowOfficialMapMissingMessage(mapSHA1); - } - else if (GameModeMap != currentGameModeMap) - { - ChangeMap(GameModeMap); - } + int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead); + if (maxAhead != MaxAhead) + { + MaxAhead = maxAhead; + AddNotice(string.Format("The game host has changed MaxAhead to {0}".L10N("Client:Main:HostChangeMaxAhead"), maxAhead)); + } - // By changing the game options after changing the map, we know which - // game options were changed by the map and which were changed by the game host + int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion); + if (protocolVersion != ProtocolVersion) + { + ProtocolVersion = protocolVersion; + AddNotice(string.Format("The game host has changed ProtocolVersion to {0}".L10N("Client:Main:HostChangeProtocolVersion"), protocolVersion)); + } - // If the map doesn't exist on the local installation, it's impossible - // to know which options were set by the host and which were set by the - // map, so we'll just assume that the host has set all the options. - // Very few (if any) custom maps force options, so it'll be correct nearly always + string mapName = parts[partIndex + 8]; + GameModeMap currentGameModeMap = GameModeMap; - for (int i = 0; i < checkBoxIntegerCount; i++) - { - if (parts.Length <= i) - return; + lastMapSHA1 = mapSHA1; + lastMapName = mapName; - int checkBoxStatusInt; - bool success = int.TryParse(parts[i], out checkBoxStatusInt); + GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); + if (GameModeMap == null) + { + await ChangeMapAsync(null); - if (!success) + if (!isMapOfficial) + await RequestMapAsync(); + else + await ShowOfficialMapMissingMessageAsync(mapSHA1); + } + else if (GameModeMap != currentGameModeMap) { - AddNotice(("Failed to parse check box options sent by game host!" + - "The game host's game version might be different from yours.").L10N("Client:Main:HostCheckBoxParseError"), Color.Red); - return; + await ChangeMapAsync(GameModeMap); } - byte[] byteArray = BitConverter.GetBytes(checkBoxStatusInt); - bool[] boolArray = Conversions.BytesIntoBoolArray(byteArray); + // By changing the game options after changing the map, we know which + // game options were changed by the map and which were changed by the game host - for (int optionIndex = 0; optionIndex < boolArray.Length; optionIndex++) - { - int gameOptionIndex = i * 32 + optionIndex; + // If the map doesn't exist on the local installation, it's impossible + // to know which options were set by the host and which were set by the + // map, so we'll just assume that the host has set all the options. + // Very few (if any) custom maps force options, so it'll be correct nearly always - if (gameOptionIndex >= CheckBoxes.Count) - break; + for (int i = 0; i < checkBoxIntegerCount; i++) + { + if (parts.Length <= i) + return; - GameLobbyCheckBox checkBox = CheckBoxes[gameOptionIndex]; + int checkBoxStatusInt; + bool success = int.TryParse(parts[i], out checkBoxStatusInt); - if (checkBox.Checked != boolArray[optionIndex]) + if (!success) { - if (boolArray[optionIndex]) - AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), checkBox.Text)); - else - AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), checkBox.Text)); + AddNotice(("Failed to parse check box options sent by game host!" + + "The game host's game version might be different from yours.").L10N("Client:Main:HostCheckBoxParseError"), Color.Red); + return; } - CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex]; + byte[] byteArray = BitConverter.GetBytes(checkBoxStatusInt); + bool[] boolArray = Conversions.BytesIntoBoolArray(byteArray); + + for (int optionIndex = 0; optionIndex < boolArray.Length; optionIndex++) + { + int gameOptionIndex = i * 32 + optionIndex; + + if (gameOptionIndex >= CheckBoxes.Count) + break; + + GameLobbyCheckBox checkBox = CheckBoxes[gameOptionIndex]; + + if (checkBox.Checked != boolArray[optionIndex]) + { + if (boolArray[optionIndex]) + AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), checkBox.Text)); + else + AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), checkBox.Text)); + } + + CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex]; + } } - } - for (int i = checkBoxIntegerCount; i < DropDowns.Count + checkBoxIntegerCount; i++) - { - if (parts.Length <= i) + for (int i = checkBoxIntegerCount; i < DropDowns.Count + checkBoxIntegerCount; i++) { - AddNotice(("The game host has sent an invalid game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); - return; - } + if (parts.Length <= i) + { + AddNotice(("The game host has sent an invalid game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); + return; + } - int ddSelectedIndex; - bool success = int.TryParse(parts[i], out ddSelectedIndex); + int ddSelectedIndex; + bool success = int.TryParse(parts[i], out ddSelectedIndex); - if (!success) - { - AddNotice(("Failed to parse drop down options sent by game host (2)! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostDropDownParseError"), Color.Red); - return; - } + if (!success) + { + AddNotice(("Failed to parse drop down options sent by game host (2)! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalidTheSecondTime"), Color.Red); + return; + } - GameLobbyDropDown dd = DropDowns[i - checkBoxIntegerCount]; + GameLobbyDropDown dd = DropDowns[i - checkBoxIntegerCount]; - if (ddSelectedIndex < -1 || ddSelectedIndex >= dd.Items.Count) - continue; + if (ddSelectedIndex < -1 || ddSelectedIndex >= dd.Items.Count) + continue; - if (dd.SelectedIndex != ddSelectedIndex) - { - string ddName = dd.OptionName; - if (dd.OptionName == null) - ddName = dd.Name; + if (dd.SelectedIndex != ddSelectedIndex) + { + string ddName = dd.OptionName; + if (dd.OptionName == null) + ddName = dd.Name; + + AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); + } - AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); + DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex; } - DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex; - } + int randomSeed; + bool parseSuccess = int.TryParse(parts[partIndex + 6], out randomSeed); + + if (!parseSuccess) + { + AddNotice(("Failed to parse random seed from game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostRandomSeedError"), Color.Red); + } - int randomSeed; - bool parseSuccess = int.TryParse(parts[partIndex + 6], out randomSeed); + bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString(parts[partIndex + 7], + Convert.ToInt32(RemoveStartingLocations))); + SetRandomStartingLocations(removeStartingLocations); - if (!parseSuccess) + RandomSeed = randomSeed; + } + catch (Exception ex) { - AddNotice(("Failed to parse random seed from game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostRandomSeedError"), Color.Red); + PreStartup.HandleException(ex); } - - bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString(parts[partIndex + 7], - Convert.ToInt32(RemoveStartingLocations))); - SetRandomStartingLocations(removeStartingLocations); - - RandomSeed = randomSeed; } - private void RequestMap(string mapSHA1) + private async Task RequestMapAsync() { if (UserINISettings.Instance.EnableMapSharing) { @@ -1420,16 +1540,16 @@ private void RequestMap(string mapSHA1) AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + " " + ("Because you've disabled map sharing, it cannot be transferred. The game host needs " + "to change the map or you will be unable to participate in the match.").L10N("Client:Main:MapSharingDisabledNotice")); - channel.SendCTCPMessage(MAP_SHARING_DISABLED_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(MAP_SHARING_DISABLED_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, 9); } } - private void ShowOfficialMapMissingMessage(string sha1) + private Task ShowOfficialMapMissingMessageAsync(string sha1) { AddNotice(("The game host has selected an official map that doesn't exist on your installation. " + "This could mean that the game host has modified game files, or is running a different game version. " + "They need to change the map or you will be unable to participate in the match.").L10N("Client:Main:OfficialMapNotExist")); - channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + " " + sha1, QueuedMessageType.SYSTEM_MESSAGE, 9); + return channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + sha1, QueuedMessageType.SYSTEM_MESSAGE, 9); } private void MapSharingConfirmationPanel_MapDownloadConfirmed(object sender, EventArgs e) @@ -1440,90 +1560,105 @@ private void MapSharingConfirmationPanel_MapDownloadConfirmed(object sender, Eve MapSharer.DownloadMap(lastMapSHA1, localGame, lastMapName); } - protected override void ChangeMap(GameModeMap gameModeMap) + protected override Task ChangeMapAsync(GameModeMap gameModeMap) { mapSharingConfirmationPanel.Disable(); - base.ChangeMap(gameModeMap); + return base.ChangeMapAsync(gameModeMap); } /// /// Signals other players that the local player has returned from the game, /// and unlocks the game as well as generates a new random seed as the game host. /// - protected override void GameProcessExited() + protected override async Task GameProcessExitedAsync() { - base.GameProcessExited(); + try + { + await base.GameProcessExitedAsync(); - channel.SendCTCPMessage("RETURN", QueuedMessageType.SYSTEM_MESSAGE, 20); - ReturnNotification(ProgramConstants.PLAYERNAME); + await channel.SendCTCPMessageAsync("RETURN", QueuedMessageType.SYSTEM_MESSAGE, 20); + ReturnNotification(ProgramConstants.PLAYERNAME); - if (IsHost) + if (IsHost) + { + RandomSeed = new Random().Next(); + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerExtraOptionsAsync(); + + if (Players.Count < playerLimit) + await UnlockGameAsync(true); + } + } + catch (Exception ex) { - RandomSeed = new Random().Next(); - OnGameOptionChanged(); - ClearReadyStatuses(); - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); - BroadcastPlayerExtraOptions(); - - if (Players.Count < playerLimit) - UnlockGame(true); + PreStartup.HandleException(ex); } } /// /// Handles the "START" (game start) command sent by the game host. /// - private void NonHostLaunchGame(string sender, string message) + private async Task NonHostLaunchGameAsync(string sender, string message) { - if (tunnelHandler.CurrentTunnel.Version != Constants.TUNNEL_VERSION_2) - return; + try + { + if (tunnelHandler.CurrentTunnel.Version != Constants.TUNNEL_VERSION_2) + return; - if (sender != hostName) - return; + if (sender != hostName) + return; - string[] parts = message.Split(';'); + string[] parts = message.Split(';'); - if (parts.Length < 1) - return; + if (parts.Length < 1) + return; - UniqueGameID = Conversions.IntFromString(parts[0], -1); - if (UniqueGameID < 0) - return; + UniqueGameID = Conversions.IntFromString(parts[0], -1); + if (UniqueGameID < 0) + return; - var recentPlayers = new List(); + var recentPlayers = new List(); - for (int i = 1; i < parts.Length; i += 2) - { - if (parts.Length <= i + 1) - return; + for (int i = 1; i < parts.Length; i += 2) + { + if (parts.Length <= i + 1) + return; - string pName = parts[i]; - string[] ipAndPort = parts[i + 1].Split(':'); + string pName = parts[i]; + string[] ipAndPort = parts[i + 1].Split(':'); - if (ipAndPort.Length < 2) - return; + if (ipAndPort.Length < 2) + return; - int port; - bool success = int.TryParse(ipAndPort[1], out port); + int port; + bool success = int.TryParse(ipAndPort[1], out port); - if (!success) - return; + if (!success) + return; - PlayerInfo pInfo = Players.Find(p => p.Name == pName); + PlayerInfo pInfo = Players.Find(p => p.Name == pName); - if (pInfo == null) - return; + if (pInfo == null) + return; - pInfo.Port = port; - recentPlayers.Add(pName); - } - cncnetUserData.AddRecentPlayers(recentPlayers, channel.UIName); + pInfo.Port = port; + recentPlayers.Add(pName); + } + cncnetUserData.AddRecentPlayers(recentPlayers, channel.UIName); - StartGame(); + await StartGameAsync(); + + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void StartGame() + protected override async Task StartGameAsync() { AddNotice("Starting game...".L10N("Client:Main:StartingGame")); @@ -1535,11 +1670,11 @@ protected override void StartGame() if (gameFilesHash != fhc.GetCompleteHash()) { Logger.Log("Game files modified during client session!"); - channel.SendCTCPMessage(CHEAT_DETECTED_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CHEAT_DETECTED_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); HandleCheatDetectedMessage(ProgramConstants.PLAYERNAME); } - base.StartGame(); + await base.StartGameAsync(); } protected override void WriteSpawnIniAdditions(IniFile iniFile) @@ -1563,7 +1698,7 @@ protected override void WriteSpawnIniAdditions(IniFile iniFile) iniFile.SetIntValue("Settings", "Port", localPlayer.Port); } - protected override void SendChatMessage(string message) => channel.SendChatMessage(message, chatColor); + protected override Task SendChatMessageAsync(string message) => channel.SendChatMessageAsync(message, chatColor); #region Notifications @@ -1583,80 +1718,143 @@ private void HandleIntNotification(string sender, int parameter, Action han handler(parameter); } - protected override void GetReadyNotification() + protected override async Task GetReadyNotificationAsync() { - base.GetReadyNotification(); + try + { + await base.GetReadyNotificationAsync(); #if WINFORMS WindowManager.FlashWindow(); #endif - TopBar.SwitchToPrimary(); + TopBar.SwitchToPrimary(); - if (IsHost) - channel.SendCTCPMessage("GETREADY", QueuedMessageType.GAME_GET_READY_MESSAGE, 0); + if (IsHost) + await channel.SendCTCPMessageAsync("GETREADY", QueuedMessageType.GAME_GET_READY_MESSAGE, 0); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void AISpectatorsNotification() + protected override async Task AISpectatorsNotificationAsync() { - base.AISpectatorsNotification(); + try + { + await base.AISpectatorsNotificationAsync(); - if (IsHost) - channel.SendCTCPMessage("AISPECS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + if (IsHost) + await channel.SendCTCPMessageAsync("AISPECS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void InsufficientPlayersNotification() + protected override async Task InsufficientPlayersNotificationAsync() { - base.InsufficientPlayersNotification(); + try + { + await base.InsufficientPlayersNotificationAsync(); - if (IsHost) - channel.SendCTCPMessage("INSFSPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + if (IsHost) + await channel.SendCTCPMessageAsync("INSFSPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void TooManyPlayersNotification() + protected override async Task TooManyPlayersNotificationAsync() { - base.TooManyPlayersNotification(); + try + { + await base.TooManyPlayersNotificationAsync(); - if (IsHost) - channel.SendCTCPMessage("TMPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + if (IsHost) + await channel.SendCTCPMessageAsync("TMPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void SharedColorsNotification() + protected override async Task SharedColorsNotificationAsync() { - base.SharedColorsNotification(); + try + { + await base.SharedColorsNotificationAsync(); - if (IsHost) - channel.SendCTCPMessage("CLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + if (IsHost) + await channel.SendCTCPMessageAsync("CLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void SharedStartingLocationNotification() + protected override async Task SharedStartingLocationNotificationAsync() { - base.SharedStartingLocationNotification(); + try + { + await base.SharedStartingLocationNotificationAsync(); - if (IsHost) - channel.SendCTCPMessage("SLOC", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + if (IsHost) + await channel.SendCTCPMessageAsync("SLOC", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void LockGameNotification() + protected override async Task LockGameNotificationAsync() { - base.LockGameNotification(); + try + { + await base.LockGameNotificationAsync(); - if (IsHost) - channel.SendCTCPMessage("LCKGME", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + if (IsHost) + await channel.SendCTCPMessageAsync("LCKGME", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void NotVerifiedNotification(int playerIndex) + protected override async Task NotVerifiedNotificationAsync(int playerIndex) { - base.NotVerifiedNotification(playerIndex); + try + { + await base.NotVerifiedNotificationAsync(playerIndex); - if (IsHost) - channel.SendCTCPMessage("NVRFY " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + if (IsHost) + await channel.SendCTCPMessageAsync("NVRFY " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void StillInGameNotification(int playerIndex) + protected override async Task StillInGameNotificationAsync(int playerIndex) { - base.StillInGameNotification(playerIndex); + try + { + await base.StillInGameNotificationAsync(playerIndex); - if (IsHost) - channel.SendCTCPMessage("INGM " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + if (IsHost) + await channel.SendCTCPMessageAsync("INGM " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void ReturnNotification(string sender) @@ -1682,21 +1880,30 @@ private void HandleTunnelPing(string sender, int ping) } } - private void FileHashNotification(string sender, string filesHash) + private async Task FileHashNotificationAsync(string sender, string filesHash) { - if (!IsHost) - return; + try + { + if (!IsHost) + return; - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo != null) - pInfo.Verified = true; - CopyPlayerDataToUI(); + if (pInfo != null) + pInfo.Verified = true; + + CopyPlayerDataToUI(); + + if (filesHash != gameFilesHash) + { + await channel.SendCTCPMessageAsync("MM " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10); + CheaterNotification(ProgramConstants.PLAYERNAME, sender); + } - if (filesHash != gameFilesHash) + } + catch (Exception ex) { - channel.SendCTCPMessage("MM " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10); - CheaterNotification(ProgramConstants.PLAYERNAME, sender); + PreStartup.HandleException(ex); } } @@ -1708,28 +1915,28 @@ private void CheaterNotification(string sender, string cheaterName) AddNotice(string.Format("Player {0} has different files compared to the game host. Either {0} or the game host could be cheating.".L10N("Client:Main:DifferentFileCheating"), cheaterName), Color.Red); } - protected override void BroadcastDiceRoll(int dieSides, int[] results) + protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) { string resultString = string.Join(",", results); - channel.SendCTCPMessage($"{DICE_ROLL_MESSAGE} {dieSides},{resultString}", QueuedMessageType.CHAT_MESSAGE, 0); + await channel.SendCTCPMessageAsync($"{DICE_ROLL_MESSAGE} {dieSides},{resultString}", QueuedMessageType.CHAT_MESSAGE, 0); PrintDiceRollResult(ProgramConstants.PLAYERNAME, dieSides, results); } #endregion - protected override void HandleLockGameButtonClick() + protected override async Task HandleLockGameButtonClickAsync() { if (!Locked) { AddNotice("You've locked the game room.".L10N("Client:Main:RoomLockedByYou")); - LockGame(); + await LockGameAsync(); } else { if (Players.Count < playerLimit) { AddNotice("You've unlocked the game room.".L10N("Client:Main:RoomUnockedByYou")); - UnlockGame(false); + await UnlockGameAsync(false); } else AddNotice(string.Format( @@ -1737,20 +1944,20 @@ protected override void HandleLockGameButtonClick() } } - protected override void LockGame() + protected override async Task LockGameAsync() { - connectionManager.SendCustomMessage(new QueuedMessage( - string.Format("MODE {0} +i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); + await connectionManager.SendCustomMessageAsync(new QueuedMessage( + string.Format("MODE {0} +i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); Locked = true; btnLockGame.Text = "Unlock Game".L10N("Client:Main:UnlockGame"); AccelerateGameBroadcasting(); } - protected override void UnlockGame(bool announce) + protected override async Task UnlockGameAsync(bool announce) { - connectionManager.SendCustomMessage(new QueuedMessage( - string.Format("MODE {0} -i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); + await connectionManager.SendCustomMessageAsync(new QueuedMessage( + string.Format("MODE {0} -i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); Locked = false; if (announce) @@ -1759,7 +1966,7 @@ protected override void UnlockGame(bool announce) AccelerateGameBroadcasting(); } - protected override void KickPlayer(int playerIndex) + protected override async Task KickPlayerAsync(int playerIndex) { if (playerIndex >= Players.Count) return; @@ -1767,10 +1974,10 @@ protected override void KickPlayer(int playerIndex) var pInfo = Players[playerIndex]; AddNotice(string.Format("Kicking {0} from the game...".L10N("Client:Main:KickPlayer"), pInfo.Name)); - channel.SendKickMessage(pInfo.Name, 8); + await channel.SendKickMessageAsync(pInfo.Name, 8); } - protected override void BanPlayer(int playerIndex) + protected override async Task BanPlayerAsync(int playerIndex) { if (playerIndex >= Players.Count) return; @@ -1782,49 +1989,56 @@ protected override void BanPlayer(int playerIndex) if (user != null) { AddNotice(string.Format("Banning and kicking {0} from the game...".L10N("Client:Main:BanAndKickPlayer"), pInfo.Name)); - channel.SendBanMessage(user.Hostname, 8); - channel.SendKickMessage(user.Name, 8); + await channel.SendBanMessageAsync(user.Hostname, 8); + await channel.SendKickMessageAsync(user.Name, 8); } } private void HandleCheatDetectedMessage(string sender) => AddNotice(string.Format("{0} has modified game files during the client session. They are likely attempting to cheat!".L10N("Client:Main:PlayerModifyFileCheat"), sender), Color.Red); - private void HandleTunnelServerChangeMessage(string sender, string tunnelAddressAndPort) + private async Task HandleTunnelServerChangeMessageAsync(string sender, string tunnelAddressAndPort) { - if (sender != hostName) - return; + try + { + if (sender != hostName) + return; - string[] split = tunnelAddressAndPort.Split(':'); - string tunnelAddress = split[0]; - int tunnelPort = int.Parse(split[1]); + string[] split = tunnelAddressAndPort.Split(':'); + string tunnelAddress = split[0]; + int tunnelPort = int.Parse(split[1]); - CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); - if (tunnel == null) - { - tunnelErrorMode = true; - AddNotice(("The game host has selected an invalid tunnel server! " + - "The game host needs to change the server or you will be unable " + - "to participate in the match.").L10N("Client:Main:HostInvalidTunnel"), - Color.Yellow); + CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); + if (tunnel == null) + { + tunnelErrorMode = true; + AddNotice(("The game host has selected an invalid tunnel server! " + + "The game host needs to change the server or you will be unable " + + "to participate in the match.").L10N("Client:Main:HostInvalidTunnel"), + Color.Yellow); + UpdateLaunchGameButtonStatus(); + return; + } + + tunnelErrorMode = false; + await HandleTunnelServerChangeAsync(tunnel); UpdateLaunchGameButtonStatus(); - return; } - - tunnelErrorMode = false; - HandleTunnelServerChange(tunnel); - UpdateLaunchGameButtonStatus(); + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } /// /// Changes the tunnel server used for the game. /// /// The new tunnel server to use. - private void HandleTunnelServerChange(CnCNetTunnel tunnel) + private Task HandleTunnelServerChangeAsync(CnCNetTunnel tunnel) { tunnelHandler.CurrentTunnel = tunnel; AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("Client:Main:HostChangeTunnel"), tunnel.Name)); - UpdatePing(); + return UpdatePingAsync(); } protected override bool UpdateLaunchGameButtonStatus() @@ -1836,96 +2050,125 @@ protected override bool UpdateLaunchGameButtonStatus() #region CnCNet map sharing private void MapSharer_MapDownloadFailed(object sender, SHA1EventArgs e) - => WindowManager.AddCallback(new Action(MapSharer_HandleMapDownloadFailed), e); + => WindowManager.AddCallback(MapSharer_HandleMapDownloadFailedAsync, e); - private void MapSharer_HandleMapDownloadFailed(SHA1EventArgs e) + private async Task MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) { - // If the host has already uploaded the map, we shouldn't request them to re-upload it - if (hostUploadedMaps.Contains(e.SHA1)) + try { - AddNotice("Download of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:DownloadCustomMapFailed")); - mapSharingConfirmationPanel.SetFailedStatus(); + // If the host has already uploaded the map, we shouldn't request them to re-upload it + if (hostUploadedMaps.Contains(e.SHA1)) + { + AddNotice("Download of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:DownloadCustomMapFailed")); + mapSharingConfirmationPanel.SetFailedStatus(); - channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - return; + await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + return; + } + + if (chatCommandDownloadedMaps.Contains(e.SHA1)) + { + // Notify the user that their chat command map download failed. + // Do not notify other users with a CTCP message as this is irrelevant to them. + AddNotice("Downloading map via chat command has failed. Check the map ID and try again.".L10N("Client:Main:DownloadMapCommandFailedGeneric")); + mapSharingConfirmationPanel.SetFailedStatus(); + return; + } + + AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("Client:Main:RequestHostUploadMapToDB")); + + await channel.SendCTCPMessageAsync(MAP_SHARING_UPLOAD_REQUEST + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } - else if (chatCommandDownloadedMaps.Contains(e.SHA1)) + catch (Exception ex) { - // Notify the user that their chat command map download failed. - // Do not notify other users with a CTCP message as this is irrelevant to them. - AddNotice("Downloading map via chat command has failed. Check the map ID and try again.".L10N("Client:Main:DownloadMapCommandFailedGeneric")); - mapSharingConfirmationPanel.SetFailedStatus(); - return; + PreStartup.HandleException(ex); } - - AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("Client:Main:RequestHostUploadMapToDB")); - - channel.SendCTCPMessage(MAP_SHARING_UPLOAD_REQUEST + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } private void MapSharer_MapDownloadComplete(object sender, SHA1EventArgs e) => - WindowManager.AddCallback(new Action(MapSharer_HandleMapDownloadComplete), e); + WindowManager.AddCallback(MapSharer_HandleMapDownloadCompleteAsync, e); - private void MapSharer_HandleMapDownloadComplete(SHA1EventArgs e) + private async Task MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) { - string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName); - Logger.Log("Map " + mapFileName + " downloaded, parsing."); - string mapPath = "Maps/Custom/" + mapFileName; - Map map = MapLoader.LoadCustomMap(mapPath, out string returnMessage); - if (map != null) + try { - AddNotice(returnMessage); - if (lastMapSHA1 == e.SHA1) + string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName); + Logger.Log("Map " + mapFileName + " downloaded, parsing."); + string mapPath = "Maps/Custom/" + mapFileName; + Map map = MapLoader.LoadCustomMap(mapPath, out string returnMessage); + if (map != null) { - GameModeMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == lastMapSHA1); - ChangeMap(GameModeMap); + AddNotice(returnMessage); + if (lastMapSHA1 == e.SHA1) + { + GameModeMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == lastMapSHA1); + await ChangeMapAsync(GameModeMap); + } + } + else if (chatCommandDownloadedMaps.Contains(e.SHA1)) + { + // Somehow the user has managed to download an already existing sha1 hash. + // This special case prevents user confusion from the file successfully downloading but showing an error anyway. + AddNotice(returnMessage, Color.Yellow); + AddNotice("Map was downloaded, but a duplicate is already loaded from a different filename. This may cause strange behavior.".L10N("Client:Main:DownloadMapCommandDuplicateMapFileLoaded"), + Color.Yellow); + } + else + { + AddNotice(returnMessage, Color.Red); + AddNotice("Transfer of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:MapTransferFailed")); + mapSharingConfirmationPanel.SetFailedStatus(); + await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } } - else if (chatCommandDownloadedMaps.Contains(e.SHA1)) - { - // Somehow the user has managed to download an already existing sha1 hash. - // This special case prevents user confusion from the file successfully downloading but showing an error anyway. - AddNotice(returnMessage, Color.Yellow); - AddNotice("Map was downloaded, but a duplicate is already loaded from a different filename. This may cause strange behavior.".L10N("Client:Main:DownloadMapCommandDuplicateMapFileLoaded"), - Color.Yellow); - } - else + catch (Exception ex) { - AddNotice(returnMessage, Color.Red); - AddNotice("Transfer of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:MapTransferFailed")); - mapSharingConfirmationPanel.SetFailedStatus(); - channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + PreStartup.HandleException(ex); } } private void MapSharer_MapUploadFailed(object sender, MapEventArgs e) => - WindowManager.AddCallback(new Action(MapSharer_HandleMapUploadFailed), e); + WindowManager.AddCallback(MapSharer_HandleMapUploadFailedAsync, e); - private void MapSharer_HandleMapUploadFailed(MapEventArgs e) + private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) { - Map map = e.Map; + try + { + Map map = e.Map; - hostUploadedMaps.Add(map.SHA1); + hostUploadedMaps.Add(map.SHA1); - AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("Client:Main:UpdateMapToDBFailed"), map.Name)); - if (map == Map) + AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("Client:Main:UpdateMapToDBFailed"), map.Name)); + if (map == Map) + { + AddNotice("You need to change the map or some players won't be able to participate in this match.".L10N("Client:Main:YouMustReplaceMap")); + await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } + } + catch (Exception ex) { - AddNotice("You need to change the map or some players won't be able to participate in this match.".L10N("Client:Main:YouMustReplaceMap")); - channel.SendCTCPMessage(MAP_SHARING_FAIL_MESSAGE + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + PreStartup.HandleException(ex); } } private void MapSharer_MapUploadComplete(object sender, MapEventArgs e) => - WindowManager.AddCallback(new Action(MapSharer_HandleMapUploadComplete), e); + WindowManager.AddCallback(MapSharer_HandleMapUploadCompleteAsync, e); - private void MapSharer_HandleMapUploadComplete(MapEventArgs e) + private async Task MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) { - hostUploadedMaps.Add(e.Map.SHA1); + try + { + hostUploadedMaps.Add(e.Map.SHA1); - AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); - if (e.Map == Map) + AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); + if (e.Map == Map) + { + await channel.SendCTCPMessageAsync(MAP_SHARING_DOWNLOAD_REQUEST + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } + } + catch (Exception ex) { - channel.SendCTCPMessage(MAP_SHARING_DOWNLOAD_REQUEST + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + PreStartup.HandleException(ex); } } @@ -2117,60 +2360,67 @@ private void DownloadMapByIdCommand(string parameters) private void AccelerateGameBroadcasting() => gameBroadcastTimer.Accelerate(TimeSpan.FromSeconds(GAME_BROADCAST_ACCELERATION)); - private void BroadcastGame() + private async Task BroadcastGameAsync() { - Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); + try + { + Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); - if (broadcastChannel == null) - return; + if (broadcastChannel == null) + return; - if (ProgramConstants.IsInGame && broadcastChannel.Users.Count > 500) - return; + if (ProgramConstants.IsInGame && broadcastChannel.Users.Count > 500) + return; - if (GameMode == null || Map == null) - return; + if (GameMode == null || Map == null) + return; - StringBuilder sb = new StringBuilder("GAME "); - sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); - sb.Append(";"); - sb.Append(ProgramConstants.GAME_VERSION); - sb.Append(";"); - sb.Append(playerLimit); - sb.Append(";"); - sb.Append(channel.ChannelName); - sb.Append(";"); - sb.Append(channel.UIName); - sb.Append(";"); - if (Locked) - sb.Append("1"); - else - sb.Append("0"); - sb.Append(Convert.ToInt32(isCustomPassword)); - sb.Append(Convert.ToInt32(closed)); - sb.Append("0"); // IsLoadedGame - sb.Append("0"); // IsLadder - sb.Append(";"); - foreach (PlayerInfo pInfo in Players) - { - sb.Append(pInfo.Name); - sb.Append(","); - } + StringBuilder sb = new StringBuilder("GAME "); + sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); + sb.Append(";"); + sb.Append(ProgramConstants.GAME_VERSION); + sb.Append(";"); + sb.Append(playerLimit); + sb.Append(";"); + sb.Append(channel.ChannelName); + sb.Append(";"); + sb.Append(channel.UIName); + sb.Append(";"); + if (Locked) + sb.Append("1"); + else + sb.Append("0"); + sb.Append(Convert.ToInt32(isCustomPassword)); + sb.Append(Convert.ToInt32(closed)); + sb.Append("0"); // IsLoadedGame + sb.Append("0"); // IsLadder + sb.Append(";"); + foreach (PlayerInfo pInfo in Players) + { + sb.Append(pInfo.Name); + sb.Append(","); + } - sb.Remove(sb.Length - 1, 1); - sb.Append(";"); - sb.Append(Map.UntranslatedName); - sb.Append(";"); - sb.Append(GameMode.UntranslatedUIName); - sb.Append(";"); - sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); - sb.Append(";"); - sb.Append(0); // LoadedGameId + sb.Remove(sb.Length - 1, 1); + sb.Append(";"); + sb.Append(Map.UntranslatedName); + sb.Append(";"); + sb.Append(GameMode.UntranslatedUIName); + sb.Append(";"); + sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); + sb.Append(";"); + sb.Append(0); // LoadedGameId - broadcastChannel.SendCTCPMessage(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); + await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } #endregion public override string GetSwitchName() => "Game Lobby".L10N("Client:Main:GameLobby"); } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index f8fb4d7dd..3bbfb4a5c 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using ClientCore.Enums; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.Online.EventArguments; @@ -178,6 +179,8 @@ protected GameModeMap GameModeMap private LoadOrSaveGameOptionPresetWindow loadOrSaveGameOptionPresetWindow; + private XNAMultiColumnListBox.SelectedIndexChangedEventHandler lbGameModeMapList_SelectedIndexChangedFunc; + public override void Initialize() { Name = _iniSectionName; @@ -205,10 +208,10 @@ public override void Initialize() PlayerOptionsPanel = FindChild(nameof(PlayerOptionsPanel)); btnLeaveGame = FindChild(nameof(btnLeaveGame)); - btnLeaveGame.LeftClick += BtnLeaveGame_LeftClick; + btnLeaveGame.LeftClick += (sender, e) => BtnLeaveGame_LeftClickAsync(sender, e); btnLaunchGame = FindChild(nameof(btnLaunchGame)); - btnLaunchGame.LeftClick += BtnLaunchGame_LeftClick; + btnLaunchGame.LeftClick += (sender, e) => BtnLaunchGame_LeftClickAsync(sender, e); btnLaunchGame.InitStarDisplay(RankTextures); MapPreviewBox = FindChild("MapPreviewBox"); @@ -221,7 +224,7 @@ public override void Initialize() lblMapSize = FindChild(nameof(lblMapSize)); lbGameModeMapList = FindChild("lbMapList"); // lbMapList for backwards compatibility - lbGameModeMapList.SelectedIndexChanged += LbGameModeMapList_SelectedIndexChanged; + lbGameModeMapList.SelectedIndexChanged += lbGameModeMapList_SelectedIndexChangedFunc; lbGameModeMapList.RightClick += LbGameModeMapList_RightClick; lbGameModeMapList.AllowKeyboardInput = true; //!isMultiplayer @@ -260,7 +263,7 @@ public override void Initialize() lbGameModeMapList.AddColumn("MAP NAME".L10N("Client:Main:MapNameHeader"), lbGameModeMapList.Width - RankTextures[1].Width - 3); ddGameModeMapFilter = FindChild("ddGameMode"); // ddGameMode for backwards compatibility - ddGameModeMapFilter.SelectedIndexChanged += DdGameModeMapFilter_SelectedIndexChanged; + ddGameModeMapFilter.SelectedIndexChanged += (_, _) => DdGameModeMapFilter_SelectedIndexChangedAsync(); ddGameModeMapFilter.AddItem(CreateGameFilterItem(FavoriteMapsLabel, new GameModeMapFilter(GetFavoriteGameModeMaps))); foreach (GameMode gm in GameModeMaps.GameModes) @@ -276,8 +279,10 @@ public override void Initialize() btnPickRandomMap = FindChild(nameof(btnPickRandomMap)); btnPickRandomMap.LeftClick += BtnPickRandomMap_LeftClick; - CheckBoxes.ForEach(chk => chk.CheckedChanged += ChkBox_CheckedChanged); - DropDowns.ForEach(dd => dd.SelectedIndexChanged += Dropdown_SelectedIndexChanged); + CheckBoxes.ForEach(chk => chk.CheckedChanged += (sender, _) => ChkBox_CheckedChangedAsync(sender)); + DropDowns.ForEach(dd => dd.SelectedIndexChanged += (sender, _) => Dropdown_SelectedIndexChangedAsync(sender)); + + lbGameModeMapList_SelectedIndexChangedFunc = (_, _) => LbGameModeMapList_SelectedIndexChangedAsync(); InitializeGameOptionPresetUI(); } @@ -396,52 +401,75 @@ protected void HandleGameOptionPresetSaveCommand(string presetName) AddNotice(error); } - protected void HandleGameOptionPresetLoadCommand(GameOptionPresetEventArgs e) => HandleGameOptionPresetLoadCommand(e.PresetName); + protected void HandleGameOptionPresetLoadCommand(GameOptionPresetEventArgs e) => HandleGameOptionPresetLoadCommandAsync(e.PresetName); - protected void HandleGameOptionPresetLoadCommand(string presetName) + protected async Task HandleGameOptionPresetLoadCommandAsync(string presetName) { - if (LoadGameOptionPreset(presetName)) - AddNotice("Game option preset loaded succesfully.".L10N("Client:Main:PresetLoaded")); - else - AddNotice(string.Format("Preset {0} not found!".L10N("Client:Main:PresetNotFound"), presetName)); + try + { + if (await LoadGameOptionPresetAsync(presetName)) + AddNotice("Game option preset loaded succesfully.".L10N("Client:Main:PresetLoaded")); + else + AddNotice(string.Format("Preset {0} not found!".L10N("Client:Main:PresetNotFound"), presetName)); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } protected void AddNotice(string message) => AddNotice(message, Color.White); protected abstract void AddNotice(string message, Color color); - private void BtnPickRandomMap_LeftClick(object sender, EventArgs e) => PickRandomMap(); + private void BtnPickRandomMap_LeftClick(object sender, EventArgs e) => PickRandomMapAsync(); private void TbMapSearch_InputReceived(object sender, EventArgs e) => ListMaps(); - private void Dropdown_SelectedIndexChanged(object sender, EventArgs e) + private async Task Dropdown_SelectedIndexChangedAsync(object sender) { - if (disableGameOptionUpdateBroadcast) - return; + try + { + if (disableGameOptionUpdateBroadcast) + return; - var dd = (GameLobbyDropDown)sender; - dd.HostSelectedIndex = dd.SelectedIndex; - OnGameOptionChanged(); + var dd = (GameLobbyDropDown)sender; + dd.HostSelectedIndex = dd.SelectedIndex; + await OnGameOptionChangedAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void ChkBox_CheckedChanged(object sender, EventArgs e) + private async Task ChkBox_CheckedChangedAsync(object sender) { - if (disableGameOptionUpdateBroadcast) - return; + try + { + if (disableGameOptionUpdateBroadcast) + return; - var checkBox = (GameLobbyCheckBox)sender; - checkBox.HostChecked = checkBox.Checked; - OnGameOptionChanged(); + var checkBox = (GameLobbyCheckBox)sender; + checkBox.HostChecked = checkBox.Checked; + await OnGameOptionChangedAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected virtual void OnGameOptionChanged() + protected virtual Task OnGameOptionChangedAsync() { CheckDisallowedSides(); btnLaunchGame.SetRank(GetRank()); + + return Task.CompletedTask; } - protected void DdGameModeMapFilter_SelectedIndexChanged(object sender, EventArgs e) + protected async Task DdGameModeMapFilter_SelectedIndexChangedAsync() { gameModeMapFilter = ddGameModeMapFilter.SelectedItem.Tag as GameModeMapFilter; @@ -453,7 +481,7 @@ protected void DdGameModeMapFilter_SelectedIndexChanged(object sender, EventArgs if (lbGameModeMapList.SelectedIndex == -1) lbGameModeMapList.SelectedIndex = 0; // Select default GameModeMap else - ChangeMap(GameModeMap); + await ChangeMapAsync(GameModeMap); } protected void BtnPlayerExtraOptions_LeftClick(object sender, EventArgs e) @@ -515,7 +543,7 @@ private List GetSortedGameModeMaps() protected void ListMaps() { - lbGameModeMapList.SelectedIndexChanged -= LbGameModeMapList_SelectedIndexChanged; + lbGameModeMapList.SelectedIndexChanged -= lbGameModeMapList_SelectedIndexChangedFunc; lbGameModeMapList.ClearItems(); lbGameModeMapList.SetTopIndex(0); @@ -584,7 +612,7 @@ protected void ListMaps() lbGameModeMapList.TopIndex++; } - lbGameModeMapList.SelectedIndexChanged += LbGameModeMapList_SelectedIndexChanged; + lbGameModeMapList.SelectedIndexChanged += lbGameModeMapList_SelectedIndexChangedFunc; } protected abstract int GetDefaultMapRankIndex(GameModeMap gameModeMap); @@ -616,7 +644,7 @@ private void DeleteMapConfirmation() var messageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, "Delete Confirmation".L10N("Client:Main:DeleteMapConfirmTitle"), string.Format("Are you sure you wish to delete the custom map {0}?".L10N("Client:Main:DeleteMapConfirmText"), Map.Name)); - messageBox.YesClickedAction = DeleteSelectedMap; + messageBox.YesClickedAction = _ => DeleteSelectedMapAsync(); } private void MapPreviewBox_ToggleFavorite(object sender, EventArgs e) => @@ -641,7 +669,7 @@ protected void RefreshForFavoriteMapRemoved() lbGameModeMapList.SelectedIndex = 0; // the map was removed while viewing favorites } - private void DeleteSelectedMap(XNAMessageBox messageBox) + private async Task DeleteSelectedMapAsync() { try { @@ -660,7 +688,7 @@ private void DeleteSelectedMap(XNAMessageBox messageBox) } ListMaps(); - ChangeMap(GameModeMap); + await ChangeMapAsync(GameModeMap); } catch (IOException ex) { @@ -668,21 +696,33 @@ private void DeleteSelectedMap(XNAMessageBox messageBox) XNAMessageBox.Show(WindowManager, "Deleting Map Failed".L10N("Client:Main:DeleteMapFailedTitle"), "Deleting map failed! Reason:".L10N("Client:Main:DeleteMapFailedText") + " " + ex.Message); } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void LbGameModeMapList_SelectedIndexChanged(object sender, EventArgs e) + private async Task LbGameModeMapList_SelectedIndexChangedAsync() { - if (lbGameModeMapList.SelectedIndex < 0 || lbGameModeMapList.SelectedIndex >= lbGameModeMapList.ItemCount) + try { - ChangeMap(null); - return; - } + if (lbGameModeMapList.SelectedIndex < 0 || lbGameModeMapList.SelectedIndex >= lbGameModeMapList.ItemCount) + { + await ChangeMapAsync(GameModeMap); + return; + } + + XNAListBoxItem item = lbGameModeMapList.GetItem(1, lbGameModeMapList.SelectedIndex); - XNAListBoxItem item = lbGameModeMapList.GetItem(1, lbGameModeMapList.SelectedIndex); + GameModeMap = (GameModeMap)item.Tag; - GameModeMap = (GameModeMap)item.Tag; + await ChangeMapAsync(GameModeMap); - ChangeMap(GameModeMap); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void LbGameModeMapList_HoveredIndexChanged(object sender, EventArgs e) @@ -701,23 +741,30 @@ private void LbGameModeMapList_HoveredIndexChanged(object sender, EventArgs e) mapListTooltip.Text = string.Empty; } - private void PickRandomMap() + private async Task PickRandomMapAsync() { - int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) - + AIPlayers.Count; - List maps = GetMapList(totalPlayerCount); - if (maps.Count < 1) - return; + try + { + int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + + AIPlayers.Count; + List maps = GetMapList(totalPlayerCount); + if (maps.Count < 1) + return; - int random = new Random().Next(0, maps.Count); - GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode == GameMode && gmm.Map == maps[random]); + int random = new Random().Next(0, maps.Count); + GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode == GameMode && gmm.Map == maps[random]); - Logger.Log("PickRandomMap: Rolled " + random + " out of " + maps.Count + ". Picked map: " + Map.Name); + Logger.Log("PickRandomMap: Rolled " + random + " out of " + maps.Count + ". Picked map: " + Map.Name); - ChangeMap(GameModeMap); - tbMapSearch.Text = string.Empty; - tbMapSearch.OnSelectedChanged(); - ListMaps(); + await ChangeMapAsync(GameModeMap); + tbMapSearch.Text = string.Empty; + tbMapSearch.OnSelectedChanged(); + ListMaps(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private List GetMapList(int playerCount) @@ -725,15 +772,15 @@ private List GetMapList(int playerCount) List mapList = (GameMode?.Maps.Where(x => x.MaxPlayers == playerCount) ?? Array.Empty()).ToList(); if (mapList.Count < 1 && playerCount <= MAX_PLAYER_COUNT) return GetMapList(playerCount + 1); - else - return mapList; + + return mapList; } /// /// Refreshes the map selection UI to match the currently selected map /// and game mode. /// - protected void RefreshMapSelectionUI() + protected async Task RefreshMapSelectionUIAsync() { if (GameMode == null) return; @@ -744,7 +791,7 @@ protected void RefreshMapSelectionUI() return; if (ddGameModeMapFilter.SelectedIndex == gameModeMapFilterIndex) - DdGameModeMapFilter_SelectedIndexChanged(this, EventArgs.Empty); + await DdGameModeMapFilter_SelectedIndexChangedAsync(); ddGameModeMapFilter.SelectedIndex = gameModeMapFilterIndex; } @@ -804,7 +851,7 @@ protected void InitPlayerOptionDropdowns() ddPlayerName.AddItem(String.Empty); ProgramConstants.AI_PLAYER_NAMES.ForEach(ddPlayerName.AddItem); ddPlayerName.AllowDropDown = true; - ddPlayerName.SelectedIndexChanged += CopyPlayerDataFromUI; + ddPlayerName.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); ddPlayerName.RightClick += MultiplayerName_RightClick; ddPlayerName.Tag = true; @@ -823,7 +870,7 @@ protected void InitPlayerOptionDropdowns() AddSideToDropDown(ddPlayerSide, sideName); ddPlayerSide.AllowDropDown = false; - ddPlayerSide.SelectedIndexChanged += CopyPlayerDataFromUI; + ddPlayerSide.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); ddPlayerSide.Tag = true; var ddPlayerColor = new XNAClientDropDown(WindowManager); @@ -835,7 +882,7 @@ protected void InitPlayerOptionDropdowns() foreach (MultiplayerColor mpColor in MPColors) ddPlayerColor.AddItem(mpColor.Name, mpColor.XnaColor); ddPlayerColor.AllowDropDown = false; - ddPlayerColor.SelectedIndexChanged += CopyPlayerDataFromUI; + ddPlayerColor.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); ddPlayerColor.Tag = false; var ddPlayerTeam = new XNAClientDropDown(WindowManager); @@ -846,7 +893,7 @@ protected void InitPlayerOptionDropdowns() ddPlayerTeam.AddItem("-"); ProgramConstants.TEAMS.ForEach(ddPlayerTeam.AddItem); ddPlayerTeam.AllowDropDown = false; - ddPlayerTeam.SelectedIndexChanged += CopyPlayerDataFromUI; + ddPlayerTeam.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); ddPlayerTeam.Tag = true; var ddPlayerStart = new XNAClientDropDown(WindowManager); @@ -857,7 +904,7 @@ protected void InitPlayerOptionDropdowns() for (int j = 1; j < 9; j++) ddPlayerStart.AddItem(j.ToString()); ddPlayerStart.AllowDropDown = false; - ddPlayerStart.SelectedIndexChanged += CopyPlayerDataFromUI; + ddPlayerStart.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); ddPlayerStart.Visible = false; ddPlayerStart.Enabled = false; ddPlayerStart.Tag = true; @@ -901,7 +948,7 @@ protected void InitPlayerOptionDropdowns() { PlayerExtraOptionsPanel = FindChild(nameof(PlayerExtraOptionsPanel)); PlayerExtraOptionsPanel.Disable(); - PlayerExtraOptionsPanel.OptionsChanged += PlayerExtraOptions_OptionsChanged; + PlayerExtraOptionsPanel.OptionsChanged += (sender, e) => PlayerExtraOptions_OptionsChangedAsync(sender, e); btnPlayerExtraOptionsOpen.LeftClick += BtnPlayerExtraOptions_LeftClick; } @@ -920,24 +967,33 @@ private XNALabel GeneratePlayerOptionCaption(string name, string text, int x, in return label; } - protected virtual void PlayerExtraOptions_OptionsChanged(object sender, EventArgs e) + protected virtual Task PlayerExtraOptions_OptionsChangedAsync(object sender, EventArgs e) { - var playerExtraOptions = GetPlayerExtraOptions(); + try + { + var playerExtraOptions = GetPlayerExtraOptions(); - for (int i = 0; i < ddPlayerSides.Length; i++) - EnablePlayerOptionDropDown(ddPlayerSides[i], i, !playerExtraOptions.IsForceRandomSides); + for (int i = 0; i < ddPlayerSides.Length; i++) + EnablePlayerOptionDropDown(ddPlayerSides[i], i, !playerExtraOptions.IsForceRandomSides); - for (int i = 0; i < ddPlayerTeams.Length; i++) - EnablePlayerOptionDropDown(ddPlayerTeams[i], i, !playerExtraOptions.IsForceRandomTeams); + for (int i = 0; i < ddPlayerTeams.Length; i++) + EnablePlayerOptionDropDown(ddPlayerTeams[i], i, !playerExtraOptions.IsForceRandomTeams); - for (int i = 0; i < ddPlayerColors.Length; i++) - EnablePlayerOptionDropDown(ddPlayerColors[i], i, !playerExtraOptions.IsForceRandomColors); + for (int i = 0; i < ddPlayerColors.Length; i++) + EnablePlayerOptionDropDown(ddPlayerColors[i], i, !playerExtraOptions.IsForceRandomColors); - for (int i = 0; i < ddPlayerStarts.Length; i++) - EnablePlayerOptionDropDown(ddPlayerStarts[i], i, !playerExtraOptions.IsForceRandomStarts); + for (int i = 0; i < ddPlayerStarts.Length; i++) + EnablePlayerOptionDropDown(ddPlayerStarts[i], i, !playerExtraOptions.IsForceRandomStarts); - UpdateMapPreviewBoxEnabledStatus(); - RefreshBtnPlayerExtraOptionsOpenTexture(); + UpdateMapPreviewBoxEnabledStatus(); + RefreshBtnPlayerExtraOptionsOpenTexture(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return Task.CompletedTask; } private void EnablePlayerOptionDropDown(XNAClientDropDown clientDropDown, int playerIndex, bool enable) @@ -1001,9 +1057,9 @@ private void GetRandomSelectors(List selectorNames, List selector } } - protected abstract void BtnLaunchGame_LeftClick(object sender, EventArgs e); + protected abstract Task BtnLaunchGame_LeftClickAsync(object sender, EventArgs e); - protected abstract void BtnLeaveGame_LeftClick(object sender, EventArgs e); + protected abstract Task BtnLeaveGame_LeftClickAsync(object sender, EventArgs e); /// /// Updates Discord Rich Presence with actual information. @@ -1772,7 +1828,7 @@ private void ManipulateStartingLocations(IniFile mapIni, PlayerHouseInfo[] house /// Writes spawn.ini, writes the map file, initializes statistics and /// starts the game process. /// - protected virtual void StartGame() + protected virtual Task StartGameAsync() { PlayerHouseInfo[] houseInfos = WriteSpawnIni(); InitializeMatchStatistics(houseInfos); @@ -1782,102 +1838,121 @@ protected virtual void StartGame() GameProcessLogic.StartGameProcess(WindowManager); UpdateDiscordPresence(true); + + return Task.CompletedTask; } - private void GameProcessExited_Callback() => AddCallback(new Action(GameProcessExited), null); + private void GameProcessExited_Callback() => AddCallback(GameProcessExitedAsync); - protected virtual void GameProcessExited() + protected virtual Task GameProcessExitedAsync() { - GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; + try + { + GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; - Logger.Log("GameProcessExited: Parsing statistics."); + Logger.Log("GameProcessExited: Parsing statistics."); - matchStatistics.ParseStatistics(ProgramConstants.GamePath, ClientConfiguration.Instance.LocalGame, false); + matchStatistics.ParseStatistics(ProgramConstants.GamePath, ClientConfiguration.Instance.LocalGame, false); - Logger.Log("GameProcessExited: Adding match to statistics."); + Logger.Log("GameProcessExited: Adding match to statistics."); - StatisticsManager.Instance.AddMatchAndSaveDatabase(true, matchStatistics); + StatisticsManager.Instance.AddMatchAndSaveDatabase(true, matchStatistics); - ClearReadyStatuses(); + ClearReadyStatuses(); - CopyPlayerDataToUI(); + CopyPlayerDataToUI(); - UpdateDiscordPresence(true); + UpdateDiscordPresence(true); + + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return Task.CompletedTask; } /// /// "Copies" player information from the UI to internal memory, /// applying users' player options changes. /// - protected virtual void CopyPlayerDataFromUI(object sender, EventArgs e) + protected virtual async Task CopyPlayerDataFromUIAsync(object sender, EventArgs e) { - if (PlayerUpdatingInProgress) - return; + try + { + if (PlayerUpdatingInProgress) + return; - var senderDropDown = (XNADropDown)sender; - if ((bool)senderDropDown.Tag) - ClearReadyStatuses(); + var senderDropDown = (XNADropDown)sender; + if ((bool)senderDropDown.Tag) + ClearReadyStatuses(); - var oldSideId = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId; + var oldSideId = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId; - for (int pId = 0; pId < Players.Count; pId++) - { - PlayerInfo pInfo = Players[pId]; + for (int pId = 0; pId < Players.Count; pId++) + { + PlayerInfo pInfo = Players[pId]; - pInfo.ColorId = ddPlayerColors[pId].SelectedIndex; - pInfo.SideId = ddPlayerSides[pId].SelectedIndex; - pInfo.StartingLocation = ddPlayerStarts[pId].SelectedIndex; - pInfo.TeamId = ddPlayerTeams[pId].SelectedIndex; + pInfo.ColorId = ddPlayerColors[pId].SelectedIndex; + pInfo.SideId = ddPlayerSides[pId].SelectedIndex; + pInfo.StartingLocation = ddPlayerStarts[pId].SelectedIndex; + pInfo.TeamId = ddPlayerTeams[pId].SelectedIndex; - if (pInfo.SideId == SideCount + RandomSelectorCount) - pInfo.StartingLocation = 0; + if (pInfo.SideId == SideCount + RandomSelectorCount) + pInfo.StartingLocation = 0; - XNADropDown ddName = ddPlayerNames[pId]; + XNADropDown ddName = ddPlayerNames[pId]; - switch (ddName.SelectedIndex) - { - case 0: - break; - case 1: - ddName.SelectedIndex = 0; - break; - case 2: - KickPlayer(pId); - break; - case 3: - BanPlayer(pId); - break; + switch (ddName.SelectedIndex) + { + case 0: + break; + case 1: + ddName.SelectedIndex = 0; + break; + case 2: + await KickPlayerAsync(pId); + break; + case 3: + await BanPlayerAsync(pId); + break; + } } - } - AIPlayers.Clear(); - for (int cmbId = Players.Count; cmbId < 8; cmbId++) - { - XNADropDown dd = ddPlayerNames[cmbId]; - dd.Items[0].Text = "-"; - - if (dd.SelectedIndex < 1) - continue; - - PlayerInfo aiPlayer = new PlayerInfo + AIPlayers.Clear(); + for (int cmbId = Players.Count; cmbId < 8; cmbId++) { - Name = dd.Items[dd.SelectedIndex].Text, - AILevel = dd.SelectedIndex - 1, - SideId = Math.Max(ddPlayerSides[cmbId].SelectedIndex, 0), - ColorId = Math.Max(ddPlayerColors[cmbId].SelectedIndex, 0), - StartingLocation = Math.Max(ddPlayerStarts[cmbId].SelectedIndex, 0), - TeamId = Map != null && Map.IsCoop ? 1 : Math.Max(ddPlayerTeams[cmbId].SelectedIndex, 0), - IsAI = true - }; + XNADropDown dd = ddPlayerNames[cmbId]; + dd.Items[0].Text = "-"; - AIPlayers.Add(aiPlayer); - } + if (dd.SelectedIndex < 1) + continue; - CopyPlayerDataToUI(); - btnLaunchGame.SetRank(GetRank()); + PlayerInfo aiPlayer = new PlayerInfo + { + Name = dd.Items[dd.SelectedIndex].Text, + AILevel = dd.SelectedIndex - 1, + SideId = Math.Max(ddPlayerSides[cmbId].SelectedIndex, 0), + ColorId = Math.Max(ddPlayerColors[cmbId].SelectedIndex, 0), + StartingLocation = Math.Max(ddPlayerStarts[cmbId].SelectedIndex, 0), + TeamId = Map != null && Map.IsCoop ? 1 : Math.Max(ddPlayerTeams[cmbId].SelectedIndex, 0), + IsAI = true + }; + + AIPlayers.Add(aiPlayer); + } + + CopyPlayerDataToUI(); + btnLaunchGame.SetRank(GetRank()); - if (oldSideId != Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId) - UpdateDiscordPresence(); + if (oldSideId != Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId) + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } /// @@ -2035,25 +2110,27 @@ protected virtual void CopyPlayerDataToUI() /// Override this in a derived class to kick players. /// /// The index of the player that should be kicked. - protected virtual void KickPlayer(int playerIndex) + protected virtual Task KickPlayerAsync(int playerIndex) { // Do nothing by default + return Task.CompletedTask; } /// /// Override this in a derived class to ban players. /// /// The index of the player that should be banned. - protected virtual void BanPlayer(int playerIndex) + protected virtual Task BanPlayerAsync(int playerIndex) { // Do nothing by default + return Task.CompletedTask; } /// /// Changes the current map and game mode. /// /// The new game mode map. - protected virtual void ChangeMap(GameModeMap gameModeMap) + protected virtual async Task ChangeMapAsync(GameModeMap gameModeMap) { GameModeMap = gameModeMap; @@ -2185,7 +2262,7 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) pInfo.TeamId = 1; } - OnGameOptionChanged(); + await OnGameOptionChangedAsync(); MapPreviewBox.GameModeMap = GameModeMap; CopyPlayerDataToUI(); @@ -2429,7 +2506,7 @@ protected string AddGameOptionPreset(string name) return null; } - public bool LoadGameOptionPreset(string name) + public async Task LoadGameOptionPresetAsync(string name) { GameOptionPreset preset = GameOptionPresets.Instance.GetPreset(name); if (preset == null) @@ -2454,7 +2531,7 @@ public bool LoadGameOptionPreset(string name) } disableGameOptionUpdateBroadcast = false; - OnGameOptionChanged(); + await OnGameOptionChangedAsync(); return true; } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index e7902ec2d..0fa444a39 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -11,13 +11,16 @@ using Rampastring.Tools; using Rampastring.XNAUI; using System; +#if !NETFRAMEWORK +using System.Buffers; +#endif using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; - +using System.Threading.Tasks; namespace DTAClient.DXGUI.Multiplayer.GameLobby { @@ -40,7 +43,7 @@ public class LANGameLobby : MultiplayerGameLobby private const string LAUNCH_GAME_COMMAND = "LAUNCH"; private const string FILE_HASH_COMMAND = "FHASH"; private const string DICE_ROLL_COMMAND = "DR"; - public const string PING = "PING"; + private const string PING = "PING"; public LANGameLobby(WindowManager windowManager, string iniName, TopBar topBar, LANColor[] chatColors, MapLoader mapLoader, DiscordHandler discordHandler) : @@ -50,38 +53,47 @@ public LANGameLobby(WindowManager windowManager, string iniName, encoding = Encoding.UTF8; hostCommandHandlers = new CommandHandlerBase[] { - new StringCommandHandler(CHAT_COMMAND, GameHost_HandleChatCommand), - new NoParamCommandHandler(RETURN_COMMAND, GameHost_HandleReturnCommand), - new StringCommandHandler(PLAYER_OPTIONS_REQUEST_COMMAND, HandlePlayerOptionsRequest), - new NoParamCommandHandler(PLAYER_QUIT_COMMAND, HandlePlayerQuit), - new StringCommandHandler(PLAYER_READY_REQUEST, GameHost_HandleReadyRequest), + new StringCommandHandler(CHAT_COMMAND, (sender, data) => GameHost_HandleChatCommandAsync(sender, data)), + new NoParamCommandHandler(RETURN_COMMAND, sender => GameHost_HandleReturnCommandAsync(sender)), + new StringCommandHandler(PLAYER_OPTIONS_REQUEST_COMMAND, (sender, data) => HandlePlayerOptionsRequestAsync(sender, data)), + new NoParamCommandHandler(PLAYER_QUIT_COMMAND, sender => HandlePlayerQuitAsync(sender)), + new StringCommandHandler(PLAYER_READY_REQUEST, (sender, autoReady) => GameHost_HandleReadyRequestAsync(sender, autoReady)), new StringCommandHandler(FILE_HASH_COMMAND, HandleFileHashCommand), - new StringCommandHandler(DICE_ROLL_COMMAND, Host_HandleDiceRoll), - new NoParamCommandHandler(PING, s => { }), + new StringCommandHandler(DICE_ROLL_COMMAND, (sender, result) => Host_HandleDiceRollAsync(sender, result)), + new NoParamCommandHandler(PING, _ => { }) }; playerCommandHandlers = new LANClientCommandHandler[] { new ClientStringCommandHandler(CHAT_COMMAND, Player_HandleChatCommand), - new ClientNoParamCommandHandler(GET_READY_COMMAND, HandleGetReadyCommand), + new ClientNoParamCommandHandler(GET_READY_COMMAND, () => HandleGetReadyCommandAsync()), new ClientStringCommandHandler(RETURN_COMMAND, Player_HandleReturnCommand), new ClientStringCommandHandler(PLAYER_OPTIONS_BROADCAST_COMMAND, HandlePlayerOptionsBroadcast), new ClientStringCommandHandler(PlayerExtraOptions.LAN_MESSAGE_KEY, HandlePlayerExtraOptionsBroadcast), - new ClientStringCommandHandler(LAUNCH_GAME_COMMAND, HandleGameLaunchCommand), - new ClientStringCommandHandler(GAME_OPTIONS_COMMAND, HandleGameOptionsMessage), + new ClientStringCommandHandler(LAUNCH_GAME_COMMAND, gameId => HandleGameLaunchCommandAsync(gameId)), + new ClientStringCommandHandler(GAME_OPTIONS_COMMAND, data => HandleGameOptionsMessageAsync(data)), new ClientStringCommandHandler(DICE_ROLL_COMMAND, Client_HandleDiceRoll), - new ClientNoParamCommandHandler(PING, HandlePing), + new ClientNoParamCommandHandler(PING, () => HandlePingAsync()) }; localGame = ClientConfiguration.Instance.LocalGame; - WindowManager.GameClosing += WindowManager_GameClosing; + WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync(); } - private void WindowManager_GameClosing(object sender, EventArgs e) + private async Task WindowManager_GameClosingAsync() { - if (client != null && client.Connected) - Clear(); + try + { + if (client != null && client.Connected) + await ClearAsync(); + + cancellationTokenSource?.Cancel(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void HandleFileHashCommand(string sender, string fileHash) @@ -95,12 +107,11 @@ private void HandleFileHashCommand(string sender, string fileHash) CopyPlayerDataToUI(); } - public event EventHandler LobbyNotification; public event EventHandler GameLeft; public event EventHandler GameBroadcast; - private TcpListener listener; - private TcpClient client; + private Socket listener; + private Socket client; private IPEndPoint hostEndPoint; private LANColor[] chatColors; @@ -116,51 +127,69 @@ private void HandleFileHashCommand(string sender, string fileHash) private string overMessage = string.Empty; - private string localGame; + private readonly string localGame; private string localFileHash; + private EventHandler lpInfo_ConnectionLostFunc; + + private CancellationTokenSource cancellationTokenSource; + public override void Initialize() { IniNameOverride = nameof(LANGameLobby); + lpInfo_ConnectionLostFunc = (sender, _) => LpInfo_ConnectionLostAsync(sender); base.Initialize(); PostInitialize(); } - public void SetUp(bool isHost, - IPEndPoint hostEndPoint, TcpClient client) + public async Task SetUpAsync(bool isHost, + IPEndPoint hostEndPoint, Socket client) { Refresh(isHost); this.hostEndPoint = hostEndPoint; + cancellationTokenSource?.Dispose(); + cancellationTokenSource = new CancellationTokenSource(); + if (isHost) { RandomSeed = new Random().Next(); - Thread thread = new Thread(ListenForClients); - thread.Start(); + ListenForClientsAsync(cancellationTokenSource.Token); - this.client = new TcpClient(); - this.client.Connect("127.0.0.1", ProgramConstants.LAN_GAME_LOBBY_PORT); + this.client = new Socket(SocketType.Stream, ProtocolType.Tcp); + await this.client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT); - byte[] buffer = encoding.GetBytes(PLAYER_JOIN_COMMAND + - ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME); + string message = PLAYER_JOIN_COMMAND + + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME; +#if NETFRAMEWORK + byte[] buffer1 = encoding.GetBytes(message); + var buffer = new ArraySegment(buffer1); - this.client.GetStream().Write(buffer, 0, buffer.Length); - this.client.GetStream().Flush(); + await this.client.SendAsync(buffer, SocketFlags.None); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); + Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; + + await this.client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); +#endif var fhc = new FileHashCalculator(); fhc.CalculateHashes(GameModeMaps.GameModes); localFileHash = fhc.GetCompleteHash(); - RefreshMapSelectionUI(); + await RefreshMapSelectionUIAsync(); } else { + this.client?.Dispose(); this.client = client; } - new Thread(HandleServerCommunication).Start(); + HandleServerCommunicationAsync(cancellationTokenSource.Token); if (IsHost) CopyPlayerDataToUI(); @@ -168,36 +197,53 @@ public void SetUp(bool isHost, WindowManager.SelectedControl = tbChatInput; } - public void PostJoin() + public async Task PostJoinAsync() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(GameModeMaps.GameModes); - SendMessageToHost(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash()); + await SendMessageToHostAsync(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash(), cancellationTokenSource.Token); ResetAutoReadyCheckbox(); } #region Server code - private void ListenForClients() + private async Task ListenForClientsAsync(CancellationToken cancellationToken) { - listener = new TcpListener(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT); - listener.Start(); + listener = new Socket(SocketType.Stream, ProtocolType.Tcp); + listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); +#if NETFRAMEWORK + listener.Listen(int.MaxValue); +#else + listener.Listen(); +#endif - while (true) + while (!cancellationToken.IsCancellationRequested) { - TcpClient client; + Socket client; + +#if NETFRAMEWORK try { - client = listener.AcceptTcpClient(); + client = await listener.AcceptAsync(); } +#else + try + { + client = await listener.AcceptAsync(cancellationToken); + } + catch (OperationCanceledException) + { + break; + } +#endif catch (Exception ex) { - Logger.Log("Listener error: " + ex.Message); + PreStartup.LogException(ex, "Listener error."); break; } - Logger.Log("New client connected from " + ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString()); + Logger.Log("New client connected from " + ((IPEndPoint)client.RemoteEndPoint).Address); if (Players.Count >= MAX_PLAYER_COUNT) { @@ -216,28 +262,45 @@ private void ListenForClients() LANPlayerInfo lpInfo = new LANPlayerInfo(encoding); lpInfo.SetClient(client); - Thread thread = new Thread(new ParameterizedThreadStart(HandleClientConnection)); - thread.Start(lpInfo); + HandleClientConnectionAsync(lpInfo, cancellationToken); } } - private void HandleClientConnection(object clientInfo) + private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { - var lpInfo = (LANPlayerInfo)clientInfo; - - byte[] message = new byte[1024]; +#if !NETFRAMEWORK + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); - while (true) +#endif + while (!cancellationToken.IsCancellationRequested) { - int bytesRead = 0; + int bytesRead; +#if NETFRAMEWORK + byte[] buffer1; +#else + Memory message; +#endif try { - bytesRead = lpInfo.TcpClient.GetStream().Read(message, 0, message.Length); + +#if NETFRAMEWORK + buffer1 = new byte[1024]; + var message = new ArraySegment(buffer1); + bytesRead = await lpInfo.TcpClient.ReceiveAsync(message, SocketFlags.None); + } +#else + message = memoryOwner.Memory[..1024]; + bytesRead = await lpInfo.TcpClient.ReceiveAsync(message, SocketFlags.None, cancellationToken); + } + catch (OperationCanceledException) + { + break; } +#endif catch (Exception ex) { - Logger.Log("Socket error with client " + lpInfo.IPAddress + "; removing. Message: " + ex.Message); + PreStartup.LogException(ex, "Socket error with client " + lpInfo.IPAddress + "; removing."); break; } @@ -248,8 +311,11 @@ private void HandleClientConnection(object clientInfo) break; } - string msg = encoding.GetString(message, 0, bytesRead); - +#if NETFRAMEWORK + string msg = encoding.GetString(buffer1, 0, bytesRead); +#else + string msg = encoding.GetString(message.Span[..bytesRead]); +#endif string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR); string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR); @@ -262,7 +328,7 @@ private void HandleClientConnection(object clientInfo) { lpInfo.Name = name; - AddCallback(new Action(AddPlayer), lpInfo); + AddCallback(() => AddPlayerAsync(lpInfo, cancellationToken)); return; } @@ -273,51 +339,64 @@ private void HandleClientConnection(object clientInfo) lpInfo.TcpClient.Close(); } - private void AddPlayer(LANPlayerInfo lpInfo) + private async Task AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { - if (Players.Find(p => p.Name == lpInfo.Name) != null || - Players.Count >= MAX_PLAYER_COUNT || Locked) - return; + try + { + if (Players.Find(p => p.Name == lpInfo.Name) != null || + Players.Count >= MAX_PLAYER_COUNT || Locked) + return; - Players.Add(lpInfo); + Players.Add(lpInfo); - if (IsHost && Players.Count == 1) - Players[0].Ready = true; + if (IsHost && Players.Count == 1) + Players[0].Ready = true; - lpInfo.MessageReceived += LpInfo_MessageReceived; - lpInfo.ConnectionLost += LpInfo_ConnectionLost; + lpInfo.MessageReceived += LpInfo_MessageReceived; + lpInfo.ConnectionLost += lpInfo_ConnectionLostFunc; - AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); - lpInfo.StartReceiveLoop(); + AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); + lpInfo.StartReceiveLoop(cancellationToken); - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); - BroadcastPlayerExtraOptions(); - OnGameOptionChanged(); - UpdateDiscordPresence(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerExtraOptionsAsync(); + await OnGameOptionChangedAsync(); + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void LpInfo_ConnectionLost(object sender, EventArgs e) + private async Task LpInfo_ConnectionLostAsync(object sender) { - var lpInfo = (LANPlayerInfo)sender; - CleanUpPlayer(lpInfo); - Players.Remove(lpInfo); + try + { + var lpInfo = (LANPlayerInfo)sender; + CleanUpPlayer(lpInfo); + Players.Remove(lpInfo); - AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); + AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); - if (lpInfo.Name == ProgramConstants.PLAYERNAME) - ResetDiscordPresence(); - else - UpdateDiscordPresence(); + if (lpInfo.Name == ProgramConstants.PLAYERNAME) + ResetDiscordPresence(); + else + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void LpInfo_MessageReceived(object sender, NetworkMessageEventArgs e) { - AddCallback(new Action(HandleClientMessage), - e.Message, (LANPlayerInfo)sender); + AddCallback(() => HandleClientMessage(e.Message, (LANPlayerInfo)sender)); } private void HandleClientMessage(string data, LANPlayerInfo lpInfo) @@ -330,49 +409,65 @@ private void HandleClientMessage(string data, LANPlayerInfo lpInfo) return; } - Logger.Log("Unknown LAN command from " + lpInfo.ToString() + " : " + data); + Logger.Log("Unknown LAN command from " + lpInfo + " : " + data); } private void CleanUpPlayer(LANPlayerInfo lpInfo) { lpInfo.MessageReceived -= LpInfo_MessageReceived; - lpInfo.ConnectionLost -= LpInfo_ConnectionLost; + lpInfo.ConnectionLost -= lpInfo_ConnectionLostFunc; lpInfo.TcpClient.Close(); } #endregion - private void HandleServerCommunication() + private async Task HandleServerCommunicationAsync(CancellationToken cancellationToken) { - byte[] message = new byte[1024]; - - var msg = string.Empty; - - int bytesRead = 0; - if (!client.Connected) return; - var stream = client.GetStream(); +#if !NETFRAMEWORK + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); - while (true) +#endif + while (!cancellationToken.IsCancellationRequested) { - bytesRead = 0; - + int bytesRead; +#if NETFRAMEWORK + byte[] buffer1; +#else + Memory message; +#endif try { - bytesRead = stream.Read(message, 0, message.Length); +#if NETFRAMEWORK + buffer1 = new byte[1024]; + var message = new ArraySegment(buffer1); + bytesRead = await client.ReceiveAsync(message, SocketFlags.None); + } +#else + message = memoryOwner.Memory[..1024]; + bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); + } + catch (OperationCanceledException) + { + break; } +#endif catch (Exception ex) { Logger.Log("Reading data from the server failed! Message: " + ex.Message); - BtnLeaveGame_LeftClick(this, EventArgs.Empty); + await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); break; } if (bytesRead > 0) { - msg = encoding.GetString(message, 0, bytesRead); +#if NETFRAMEWORK + string msg = encoding.GetString(buffer1, 0, bytesRead); +#else + string msg = encoding.GetString(message.Span[..bytesRead]); +#endif msg = overMessage + msg; List commands = new List(); @@ -386,23 +481,21 @@ private void HandleServerCommunication() overMessage = msg; break; } - else - { - commands.Add(msg.Substring(0, index)); - msg = msg.Substring(index + 1); - } + + commands.Add(msg.Substring(0, index)); + msg = msg.Substring(index + 1); } foreach (string cmd in commands) { - AddCallback(new Action(HandleMessageFromServer), cmd); + AddCallback(() => HandleMessageFromServer(cmd)); } continue; } Logger.Log("Reading data from the server failed (0 bytes received)!"); - BtnLeaveGame_LeftClick(this, EventArgs.Empty); + await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); break; } } @@ -420,11 +513,18 @@ private void HandleMessageFromServer(string message) Logger.Log("Unknown LAN command from the server: " + message); } - protected override void BtnLeaveGame_LeftClick(object sender, EventArgs e) + protected override async Task BtnLeaveGame_LeftClickAsync(object sender, EventArgs e) { - Clear(); - GameLeft?.Invoke(this, EventArgs.Empty); - Disable(); + try + { + await ClearAsync(); + GameLeft?.Invoke(this, EventArgs.Empty); + Disable(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } protected override void UpdateDiscordPresence(bool resetTimer = false) @@ -446,24 +546,24 @@ protected override void UpdateDiscordPresence(bool resetTimer = false) "LAN Game", IsHost, false, Locked, resetTimer); } - public override void Clear() + public override async Task ClearAsync() { - base.Clear(); + await base.ClearAsync(); if (IsHost) { - BroadcastMessage(PLAYER_QUIT_COMMAND); + await BroadcastMessageAsync(PLAYER_QUIT_COMMAND); Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p)); Players.Clear(); - listener.Stop(); + listener.Close(); } else { - SendMessageToHost(PLAYER_QUIT_COMMAND); + await SendMessageToHostAsync(PLAYER_QUIT_COMMAND, cancellationTokenSource.Token); } - if (this.client.Connected) - this.client.Close(); + if (client.Connected) + client.Close(); ResetDiscordPresence(); } @@ -479,7 +579,7 @@ public void SetChatColorIndex(int colorIndex) protected override void AddNotice(string message, Color color) => lbChatMessages.AddMessage(null, message, color); - protected override void BroadcastPlayerOptions() + protected override async Task BroadcastPlayerOptionsAsync() { if (!IsHost) return; @@ -504,17 +604,27 @@ protected override void BroadcastPlayerOptions() sb.Append("-1"); } - BroadcastMessage(sb.ToString()); + await BroadcastMessageAsync(sb.ToString()); } - protected override void BroadcastPlayerExtraOptions() + protected override async Task BroadcastPlayerExtraOptionsAsync() { var playerExtraOptions = GetPlayerExtraOptions(); - BroadcastMessage(playerExtraOptions.ToLanMessage(), true); + await BroadcastMessageAsync(playerExtraOptions.ToLanMessage(), true); } - protected override void HostLaunchGame() => BroadcastMessage(LAUNCH_GAME_COMMAND + " " + UniqueGameID); + protected override async Task HostLaunchGameAsync() + { + try + { + await BroadcastMessageAsync(LAUNCH_GAME_COMMAND + " " + UniqueGameID); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + } protected override string GetIPAddressForPlayer(PlayerInfo player) { @@ -522,7 +632,7 @@ protected override string GetIPAddressForPlayer(PlayerInfo player) return lpInfo.IPAddress; } - protected override void RequestPlayerOptions(int side, int color, int start, int team) + protected override Task RequestPlayerOptionsAsync(int side, int color, int start, int team) { var sb = new ExtendedStringBuilder(PLAYER_OPTIONS_REQUEST_COMMAND + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; @@ -530,24 +640,26 @@ protected override void RequestPlayerOptions(int side, int color, int start, int sb.Append(color); sb.Append(start); sb.Append(team); - SendMessageToHost(sb.ToString()); + return SendMessageToHostAsync(sb.ToString(), cancellationTokenSource.Token); } - protected override void RequestReadyStatus() => - SendMessageToHost(PLAYER_READY_REQUEST + " " + Convert.ToInt32(chkAutoReady.Checked)); + protected override Task RequestReadyStatusAsync() + { + return SendMessageToHostAsync(PLAYER_READY_REQUEST + " " + Convert.ToInt32(chkAutoReady.Checked), cancellationTokenSource.Token); + } - protected override void SendChatMessage(string message) + protected override Task SendChatMessageAsync(string message) { var sb = new ExtendedStringBuilder(CHAT_COMMAND + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(chatColorIndex); sb.Append(message); - SendMessageToHost(sb.ToString()); + return SendMessageToHostAsync(sb.ToString(), cancellationTokenSource.Token); } - protected override void OnGameOptionChanged() + protected override async Task OnGameOptionChangedAsync() { - base.OnGameOptionChanged(); + await base.OnGameOptionChangedAsync(); if (!IsHost) return; @@ -570,18 +682,18 @@ protected override void OnGameOptionChanged() sb.Append(FrameSendRate); sb.Append(Convert.ToInt32(RemoveStartingLocations)); - BroadcastMessage(sb.ToString()); + await BroadcastMessageAsync(sb.ToString()); } - protected override void GetReadyNotification() + protected override async Task GetReadyNotificationAsync() { - base.GetReadyNotification(); + await base.GetReadyNotificationAsync(); #if WINFORMS WindowManager.FlashWindow(); #endif if (IsHost) - BroadcastMessage(GET_READY_COMMAND); + await BroadcastMessageAsync(GET_READY_COMMAND); } protected override void ClearPingIndicators() @@ -599,45 +711,74 @@ protected override void UpdatePlayerPingIndicator(PlayerInfo pInfo) /// /// The command to send. /// If true, only send this to other players. Otherwise, even the sender will receive their message. - private void BroadcastMessage(string message, bool otherPlayersOnly = false) + private async Task BroadcastMessageAsync(string message, bool otherPlayersOnly = false) { - if (!IsHost) - return; + try + { + if (!IsHost) + return; - foreach (PlayerInfo pInfo in Players.Where(p => !otherPlayersOnly || p.Name != ProgramConstants.PLAYERNAME)) + foreach (PlayerInfo pInfo in Players.Where(p => !otherPlayersOnly || p.Name != ProgramConstants.PLAYERNAME)) + { + var lpInfo = (LANPlayerInfo)pInfo; + await lpInfo.SendMessageAsync(message, cancellationTokenSource.Token); + } + } + catch (Exception ex) { - var lpInfo = (LANPlayerInfo)pInfo; - lpInfo.SendMessage(message); + PreStartup.HandleException(ex); } } - protected override void PlayerExtraOptions_OptionsChanged(object sender, EventArgs e) + protected override async Task PlayerExtraOptions_OptionsChangedAsync(object sender, EventArgs e) { - base.PlayerExtraOptions_OptionsChanged(sender, e); - BroadcastPlayerExtraOptions(); + try + { + await base.PlayerExtraOptions_OptionsChangedAsync(sender, e); + await BroadcastPlayerExtraOptionsAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void SendMessageToHost(string message) + private async Task SendMessageToHostAsync(string message, CancellationToken cancellationToken) { if (!client.Connected) return; - byte[] buffer = encoding.GetBytes(message + ProgramConstants.LAN_MESSAGE_SEPARATOR); + message += ProgramConstants.LAN_MESSAGE_SEPARATOR; - NetworkStream ns = client.GetStream(); +#if NETFRAMEWORK + try + { + byte[] buffer1 = encoding.GetBytes(message); + var buffer = new ArraySegment(buffer1); + await client.SendAsync(buffer, SocketFlags.None); + } +#else try { - ns.Write(buffer, 0, buffer.Length); - ns.Flush(); + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); + Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; + + await client.SendAsync(buffer, SocketFlags.None, cancellationToken); + } + catch (OperationCanceledException) + { } - catch +#endif + catch (Exception ex) { - Logger.Log("Sending message to game host failed!"); + PreStartup.LogException(ex, "Sending message to game host failed!"); } } - protected override void UnlockGame(bool manual) + protected override Task UnlockGameAsync(bool manual) { Locked = false; @@ -645,9 +786,11 @@ protected override void UnlockGame(bool manual) if (manual) AddNotice("You've unlocked the game room.".L10N("Client:Main:RoomUnockedByYou")); + + return Task.CompletedTask; } - protected override void LockGame() + protected override Task LockGameAsync() { Locked = true; @@ -655,28 +798,35 @@ protected override void LockGame() if (Locked) AddNotice("You've locked the game room.".L10N("Client:Main:RoomLockedByYou")); + + return Task.CompletedTask; } - protected override void GameProcessExited() + protected override async Task GameProcessExitedAsync() { - base.GameProcessExited(); - - SendMessageToHost(RETURN_COMMAND); - - if (IsHost) + try { - RandomSeed = new Random().Next(); - OnGameOptionChanged(); - ClearReadyStatuses(); - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); - BroadcastPlayerExtraOptions(); + await base.GameProcessExitedAsync(); + + await SendMessageToHostAsync(RETURN_COMMAND, cancellationTokenSource.Token); - if (Players.Count < MAX_PLAYER_COUNT) + if (IsHost) { - UnlockGame(true); + RandomSeed = new Random().Next(); + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerExtraOptionsAsync(); + + if (Players.Count < MAX_PLAYER_COUNT) + await UnlockGameAsync(true); } } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void ReturnNotification(string sender) @@ -699,14 +849,14 @@ public override void Update(GameTime gameTime) for (int i = 1; i < Players.Count; i++) { LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i]; - if (!lpInfo.Update(gameTime)) + if (!Task.Run(() => lpInfo.UpdateAsync(gameTime)).Result) { CleanUpPlayer(lpInfo); Players.RemoveAt(i); AddNotice(string.Format("{0} - connection timed out".L10N("Client:Main:PlayerTimeout"), lpInfo.Name)); CopyPlayerDataToUI(); - BroadcastPlayerOptions(); - BroadcastPlayerExtraOptions(); + Task.Run(BroadcastPlayerOptionsAsync).Wait(); + Task.Run(BroadcastPlayerExtraOptionsAsync).Wait(); UpdateDiscordPresence(); i--; } @@ -725,11 +875,7 @@ public override void Update(GameTime gameTime) timeSinceLastReceivedCommand += gameTime.ElapsedGameTime; if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT)) - { - LobbyNotification?.Invoke(this, - new LobbyNotificationEventArgs("Connection to the game host timed out.".L10N("Client:Main:HostConnectTimeOut"))); - BtnLeaveGame_LeftClick(this, EventArgs.Empty); - } + Task.Run(() => BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty)).Wait(); } base.Update(gameTime); @@ -757,19 +903,26 @@ private void BroadcastGame() #region Command Handlers - private void GameHost_HandleChatCommand(string sender, string data) + private async Task GameHost_HandleChatCommandAsync(string sender, string data) { - string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); + try + { + string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); - if (parts.Length < 2) - return; + if (parts.Length < 2) + return; - int colorIndex = Conversions.IntFromString(parts[0], -1); + int colorIndex = Conversions.IntFromString(parts[0], -1); - if (colorIndex < 0 || colorIndex >= chatColors.Length) - return; + if (colorIndex < 0 || colorIndex >= chatColors.Length) + return; - BroadcastMessage(CHAT_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + data); + await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + data); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void Player_HandleChatCommand(string data) @@ -790,77 +943,89 @@ private void Player_HandleChatCommand(string data) chatColors[colorIndex].XNAColor, DateTime.Now, parts[2])); } - private void GameHost_HandleReturnCommand(string sender) - { - BroadcastMessage(RETURN_COMMAND + ProgramConstants.LAN_DATA_SEPARATOR + sender); - } + private Task GameHost_HandleReturnCommandAsync(string sender) + => BroadcastMessageAsync(RETURN_COMMAND + ProgramConstants.LAN_DATA_SEPARATOR + sender); private void Player_HandleReturnCommand(string sender) { ReturnNotification(sender); } - private void HandleGetReadyCommand() + private async Task HandleGetReadyCommandAsync() { - if (!IsHost) - GetReadyNotification(); + try + { + if (!IsHost) + await GetReadyNotificationAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void HandlePlayerOptionsRequest(string sender, string data) + private async Task HandlePlayerOptionsRequestAsync(string sender, string data) { - if (!IsHost) - return; + try + { + if (!IsHost) + return; - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo == null) - return; + if (pInfo == null) + return; - string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); + string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); - if (parts.Length != 4) - return; + if (parts.Length != 4) + return; - int side = Conversions.IntFromString(parts[0], -1); - int color = Conversions.IntFromString(parts[1], -1); - int start = Conversions.IntFromString(parts[2], -1); - int team = Conversions.IntFromString(parts[3], -1); + int side = Conversions.IntFromString(parts[0], -1); + int color = Conversions.IntFromString(parts[1], -1); + int start = Conversions.IntFromString(parts[2], -1); + int team = Conversions.IntFromString(parts[3], -1); - if (side < 0 || side > SideCount + RandomSelectorCount) - return; + if (side < 0 || side > SideCount + RandomSelectorCount) + return; - if (color < 0 || color > MPColors.Count) - return; + if (color < 0 || color > MPColors.Count) + return; - if (Map.CoopInfo != null) - { - if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) + if (Map.CoopInfo != null) + { + if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) + return; + + if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) + return; + } + + if (start < 0 || start > Map.MaxPlayers) return; - if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) + if (team < 0 || team > 4) return; - } - if (start < 0 || start > Map.MaxPlayers) - return; + if (side != pInfo.SideId + || start != pInfo.StartingLocation + || team != pInfo.TeamId) + { + ClearReadyStatuses(); + } - if (team < 0 || team > 4) - return; + pInfo.SideId = side; + pInfo.ColorId = color; + pInfo.StartingLocation = start; + pInfo.TeamId = team; - if (side != pInfo.SideId - || start != pInfo.StartingLocation - || team != pInfo.TeamId) + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + } + catch (Exception ex) { - ClearReadyStatuses(); + PreStartup.HandleException(ex); } - - pInfo.SideId = side; - pInfo.ColorId = color; - pInfo.StartingLocation = start; - pInfo.TeamId = team; - - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); } private void HandlePlayerExtraOptionsBroadcast(string data) => ApplyPlayerExtraOptions(null, data); @@ -947,147 +1112,175 @@ private void HandlePlayerOptionsBroadcast(string data) UpdateDiscordPresence(); } - private void HandlePlayerQuit(string sender) + private async Task HandlePlayerQuitAsync(string sender) { - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + try + { + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo == null) - return; + if (pInfo == null) + return; - AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), pInfo.Name)); - Players.Remove(pInfo); - ClearReadyStatuses(); - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); - UpdateDiscordPresence(); + AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), pInfo.Name)); + Players.Remove(pInfo); + ClearReadyStatuses(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void HandleGameOptionsMessage(string data) + private async Task HandleGameOptionsMessageAsync(string data) { - if (IsHost) - return; - - string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); - - if (parts.Length != CheckBoxes.Count + DropDowns.Count + GAME_OPTION_SPECIAL_FLAG_COUNT) + try { - AddNotice(("The game host has sent an invalid game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid")); - Logger.Log("Invalid game options message from host: " + data); - return; - } + if (IsHost) + return; - int randomSeed = Conversions.IntFromString(parts[parts.Length - GAME_OPTION_SPECIAL_FLAG_COUNT], -1); - if (randomSeed == -1) - return; + string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); - RandomSeed = randomSeed; + if (parts.Length != CheckBoxes.Count + DropDowns.Count + GAME_OPTION_SPECIAL_FLAG_COUNT) + { + AddNotice(("The game host has sent an invalid game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid")); + Logger.Log("Invalid game options message from host: " + data); + return; + } - string mapSHA1 = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 1)]; - string gameMode = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 2)]; + int randomSeed = Conversions.IntFromString(parts[parts.Length - GAME_OPTION_SPECIAL_FLAG_COUNT], -1); + if (randomSeed == -1) + return; - GameModeMap gameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); + RandomSeed = randomSeed; - if (gameModeMap == null) - { - AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + - "The host needs to change the map or you won't be able to play.".L10N("Client:Main:HostNeedChangeMapForYou")); - ChangeMap(null); - return; - } + string mapSHA1 = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 1)]; + string gameMode = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 2)]; - if (GameModeMap != gameModeMap) - ChangeMap(gameModeMap); + GameModeMap gameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); - int frameSendRate = Conversions.IntFromString(parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 3)], FrameSendRate); - if (frameSendRate != FrameSendRate) - { - FrameSendRate = frameSendRate; - AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); - } + if (gameModeMap == null) + { + AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + + "The host needs to change the map or you won't be able to play.".L10N("Client:Main:HostNeedChangeMapForYou")); + await ChangeMapAsync(null); + return; + } - bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString( - parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 4)], Convert.ToInt32(RemoveStartingLocations))); - SetRandomStartingLocations(removeStartingLocations); + if (GameModeMap != gameModeMap) + await ChangeMapAsync(gameModeMap); - for (int i = 0; i < CheckBoxes.Count; i++) - { - GameLobbyCheckBox chkBox = CheckBoxes[i]; + int frameSendRate = Conversions.IntFromString(parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 3)], FrameSendRate); + if (frameSendRate != FrameSendRate) + { + FrameSendRate = frameSendRate; + AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); + } - bool oldValue = chkBox.Checked; - chkBox.Checked = Conversions.IntFromString(parts[i], -1) > 0; + bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString( + parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 4)], Convert.ToInt32(RemoveStartingLocations))); + SetRandomStartingLocations(removeStartingLocations); - if (chkBox.Checked != oldValue) + for (int i = 0; i < CheckBoxes.Count; i++) { - if (chkBox.Checked) - AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), chkBox.Text)); - else - AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), chkBox.Text)); + GameLobbyCheckBox chkBox = CheckBoxes[i]; + + bool oldValue = chkBox.Checked; + chkBox.Checked = Conversions.IntFromString(parts[i], -1) > 0; + + if (chkBox.Checked != oldValue) + { + if (chkBox.Checked) + AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), chkBox.Text)); + else + AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), chkBox.Text)); + } } - } - for (int i = 0; i < DropDowns.Count; i++) - { - int index = Conversions.IntFromString(parts[CheckBoxes.Count + i], -1); + for (int i = 0; i < DropDowns.Count; i++) + { + int index = Conversions.IntFromString(parts[CheckBoxes.Count + i], -1); - GameLobbyDropDown dd = DropDowns[i]; + GameLobbyDropDown dd = DropDowns[i]; - if (index < 0 || index >= dd.Items.Count) - return; + if (index < 0 || index >= dd.Items.Count) + return; - int oldValue = dd.SelectedIndex; - dd.SelectedIndex = index; + int oldValue = dd.SelectedIndex; + dd.SelectedIndex = index; - if (index != oldValue) - { - string ddName = dd.OptionName; - if (dd.OptionName == null) - ddName = dd.Name; + if (index != oldValue) + { + string ddName = dd.OptionName; + if (dd.OptionName == null) + ddName = dd.Name; - AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.SelectedItem.Text)); + AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.SelectedItem.Text)); + } } } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void GameHost_HandleReadyRequest(string sender, string autoReady) + private async Task GameHost_HandleReadyRequestAsync(string sender, string autoReady) { - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + try + { + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo == null) - return; + if (pInfo == null) + return; - pInfo.Ready = true; - pInfo.AutoReady = Convert.ToBoolean(Conversions.IntFromString(autoReady, 0)); - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); + pInfo.Ready = true; + pInfo.AutoReady = Convert.ToBoolean(Conversions.IntFromString(autoReady, 0)); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void HandleGameLaunchCommand(string gameId) + private async Task HandleGameLaunchCommandAsync(string gameId) { - Players.ForEach(pInfo => pInfo.IsInGame = true); - UniqueGameID = Conversions.IntFromString(gameId, -1); - if (UniqueGameID < 0) - return; + try + { + Players.ForEach(pInfo => pInfo.IsInGame = true); + UniqueGameID = Conversions.IntFromString(gameId, -1); + if (UniqueGameID < 0) + return; - CopyPlayerDataToUI(); - StartGame(); - } + CopyPlayerDataToUI(); + await StartGameAsync(); + } - private void HandlePing() + private async Task HandlePingAsync() { - SendMessageToHost(PING); + try + { + await SendMessageToHostAsync(PING, cancellationTokenSource?.Token ?? default); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void BroadcastDiceRoll(int dieSides, int[] results) + protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) { string resultString = string.Join(",", results); - SendMessageToHost($"DR {dieSides},{resultString}"); + await SendMessageToHostAsync($"DR {dieSides},{resultString}", cancellationTokenSource?.Token ?? default); } - private void Host_HandleDiceRoll(string sender, string result) - { - BroadcastMessage($"{DICE_ROLL_COMMAND} {sender}{ProgramConstants.LAN_DATA_SEPARATOR}{result}"); - } + private Task Host_HandleDiceRollAsync(string sender, string result) + => BroadcastMessageAsync($"{DICE_ROLL_COMMAND} {sender}{ProgramConstants.LAN_DATA_SEPARATOR}{result}"); private void Client_HandleDiceRoll(string data) { @@ -1110,16 +1303,6 @@ protected override void WriteSpawnIniAdditions(IniFile iniFile) } } - public class LobbyNotificationEventArgs : EventArgs - { - public LobbyNotificationEventArgs(string notification) - { - Notification = notification; - } - - public string Notification { get; private set; } - } - public class GameBroadcastEventArgs : EventArgs { public GameBroadcastEventArgs(string message) @@ -1127,7 +1310,6 @@ public GameBroadcastEventArgs(string message) Message = message; } - public string Message { get; private set; } + public string Message { get; } } - -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 4b9a44063..e0b2484c1 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -12,6 +12,7 @@ using DTAClient.Domain.Multiplayer; using ClientGUI; using System.Text; +using System.Threading.Tasks; using DTAClient.Domain; using Microsoft.Xna.Framework.Graphics; using ClientCore.Extensions; @@ -34,23 +35,25 @@ public MultiplayerGameLobby(WindowManager windowManager, string iniName, chatBoxCommands = new List { - new ChatBoxCommand("HIDEMAPS", "Hide map list (game host only)".L10N("Client:Main:ChatboxCommandHideMapsHelp"), true, + new("HIDEMAPS", "Hide map list (game host only)".L10N("Client:Main:ChatboxCommandHideMapsHelp"), true, s => HideMapList()), - new ChatBoxCommand("SHOWMAPS", "Show map list (game host only)".L10N("Client:Main:ChatboxCommandShowMapsHelp"), true, + new("SHOWMAPS", "Show map list (game host only)".L10N("Client:Main:ChatboxCommandShowMapsHelp"), true, s => ShowMapList()), - new ChatBoxCommand("FRAMESENDRATE", "Change order lag / FrameSendRate (default 7) (game host only)".L10N("Client:Main:ChatboxCommandFrameSendRateHelp"), true, - s => SetFrameSendRate(s)), - new ChatBoxCommand("MAXAHEAD", "Change MaxAhead (default 0) (game host only)".L10N("Client:Main:ChatboxCommandMaxAheadHelp"), true, - s => SetMaxAhead(s)), - new ChatBoxCommand("PROTOCOLVERSION", "Change ProtocolVersion (default 2) (game host only)".L10N("Client:Main:ChatboxCommandProtocolVersionHelp"), true, - s => SetProtocolVersion(s)), - new ChatBoxCommand("LOADMAP", "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("Client:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), - new ChatBoxCommand("RANDOMSTARTS", "Enables completely random starting locations (Tiberian Sun based games only).".L10N("Client:Main:ChatboxCommandRandomStartsHelp"), true, - s => SetStartingLocationClearance(s)), - new ChatBoxCommand("ROLL", "Roll dice, for example /roll 3d6".L10N("Client:Main:ChatboxCommandRollHelp"), false, RollDiceCommand), - new ChatBoxCommand("SAVEOPTIONS", "Save game option preset so it can be loaded later".L10N("Client:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), - new ChatBoxCommand("LOADOPTIONS", "Load game option preset".L10N("Client:Main:ChatboxCommandLoadOptionsHelp"), true, HandleGameOptionPresetLoadCommand) + new("FRAMESENDRATE", "Change order lag / FrameSendRate (default 7) (game host only)".L10N("Client:Main:ChatboxCommandFrameSendRateHelp"), true, + s => SetFrameSendRateAsync(s)), + new("MAXAHEAD", "Change MaxAhead (default 0) (game host only)".L10N("Client:Main:ChatboxCommandMaxAheadHelp"), true, + s => SetMaxAheadAsync(s)), + new("PROTOCOLVERSION", "Change ProtocolVersion (default 2) (game host only)".L10N("Client:Main:ChatboxCommandProtocolVersionHelp"), true, + s => SetProtocolVersionAsync(s)), + new("LOADMAP", "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("Client:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), + new("RANDOMSTARTS", "Enables completely random starting locations (Tiberian Sun based games only).".L10N("Client:Main:ChatboxCommandRandomStartsHelp"), true, + s => SetStartingLocationClearanceAsync(s)), + new("ROLL", "Roll dice, for example /roll 3d6".L10N("Client:Main:ChatboxCommandRollHelp"), false, dieType => RollDiceCommandAsync(dieType)), + new("SAVEOPTIONS", "Save game option preset so it can be loaded later".L10N("Client:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), + new("LOADOPTIONS", "Load game option preset".L10N("Client:Main:ChatboxCommandLoadOptionsHelp"), true, presetName => HandleGameOptionPresetLoadCommandAsync(presetName)) }; + + chkAutoReady_CheckedChangedFunc = (_, _) => ChkAutoReady_CheckedChangedAsync(); } protected XNAPlayerSlotIndicator[] StatusIndicators; @@ -105,9 +108,11 @@ protected bool Locked private FileSystemWatcher fsw; - private bool gameSaved = false; + private bool gameSaved; + + private bool lastMapChangeWasInvalid; - private bool lastMapChangeWasInvalid = false; + private EventHandler chkAutoReady_CheckedChangedFunc; /// /// Allows derived classes to add their own chat box commands. @@ -158,17 +163,17 @@ public override void Initialize() tbChatInput = FindChild(nameof(tbChatInput)); tbChatInput.MaximumTextLength = 150; - tbChatInput.EnterPressed += TbChatInput_EnterPressed; + tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync(); btnLockGame = FindChild(nameof(btnLockGame)); - btnLockGame.LeftClick += BtnLockGame_LeftClick; + btnLockGame.LeftClick += (_, _) => BtnLockGame_LeftClickAsync(); chkAutoReady = FindChild(nameof(chkAutoReady)); - chkAutoReady.CheckedChanged += ChkAutoReady_CheckedChanged; + chkAutoReady.CheckedChanged += chkAutoReady_CheckedChangedFunc; chkAutoReady.Disable(); MapPreviewBox.LocalStartingLocationSelected += MapPreviewBox_LocalStartingLocationSelected; - MapPreviewBox.StartingLocationApplied += MapPreviewBox_StartingLocationApplied; + MapPreviewBox.StartingLocationApplied += (_, _) => MapPreviewBox_StartingLocationAppliedAsync(); sndJoinSound = new EnhancedSoundEffect("joingame.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyJoinCooldown); sndLeaveSound = new EnhancedSoundEffect("leavegame.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyLeaveCooldown); @@ -230,7 +235,7 @@ protected void PostInitialize() private void fsw_Created(object sender, FileSystemEventArgs e) { - AddCallback(new Action(FSWEvent), e); + AddCallback(() => FSWEvent(e)); } private void FSWEvent(FileSystemEventArgs e) @@ -253,7 +258,7 @@ private void FSWEvent(FileSystemEventArgs e) } } - protected override void StartGame() + protected override Task StartGameAsync() { if (fsw != null) fsw.EnableRaisingEvents = true; @@ -261,29 +266,38 @@ protected override void StartGame() for (int pId = 0; pId < Players.Count; pId++) Players[pId].IsInGame = true; - base.StartGame(); + return base.StartGameAsync(); } - protected override void GameProcessExited() + protected override async Task GameProcessExitedAsync() { - gameSaved = false; + try + { + gameSaved = false; - if (fsw != null) - fsw.EnableRaisingEvents = false; + if (fsw != null) + fsw.EnableRaisingEvents = false; - PlayerInfo pInfo = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); - pInfo.IsInGame = false; + PlayerInfo pInfo = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); - base.GameProcessExited(); + pInfo.IsInGame = false; + + await base.GameProcessExitedAsync(); + + if (IsHost) + { + GenerateGameID(); + await DdGameModeMapFilter_SelectedIndexChangedAsync(); // Refresh ranks + } + else if (chkAutoReady.Checked) + { + await RequestReadyStatusAsync(); + } - if (IsHost) - { - GenerateGameID(); - DdGameModeMapFilter_SelectedIndexChanged(null, EventArgs.Empty); // Refresh ranks } - else if (chkAutoReady.Checked) + catch (Exception ex) { - RequestReadyStatus(); + PreStartup.HandleException(ex); } } @@ -307,158 +321,201 @@ private void GenerateGameID() } } - private void BtnLockGame_LeftClick(object sender, EventArgs e) + private async Task BtnLockGame_LeftClickAsync() { - HandleLockGameButtonClick(); + try + { + await HandleLockGameButtonClickAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected virtual void HandleLockGameButtonClick() + protected virtual async Task HandleLockGameButtonClickAsync() { if (Locked) - UnlockGame(true); + await UnlockGameAsync(true); else - LockGame(); + await LockGameAsync(); } - protected abstract void LockGame(); + protected abstract Task LockGameAsync(); - protected abstract void UnlockGame(bool manual); + protected abstract Task UnlockGameAsync(bool announce); - private void TbChatInput_EnterPressed(object sender, EventArgs e) + private async Task TbChatInput_EnterPressedAsync() { - if (string.IsNullOrEmpty(tbChatInput.Text)) - return; - - if (tbChatInput.Text.StartsWith("/")) + try { - string text = tbChatInput.Text; - string command; - string parameters; - - int spaceIndex = text.IndexOf(' '); + if (string.IsNullOrEmpty(tbChatInput.Text)) + return; - if (spaceIndex == -1) + if (tbChatInput.Text.StartsWith("/")) { - command = text.Substring(1).ToUpper(); - parameters = string.Empty; - } - else - { - command = text.Substring(1, spaceIndex - 1); - parameters = text.Substring(spaceIndex + 1); - } + string text = tbChatInput.Text; + string command; + string parameters; - tbChatInput.Text = string.Empty; + int spaceIndex = text.IndexOf(' '); - foreach (var chatBoxCommand in chatBoxCommands) - { - if (command.ToUpper() == chatBoxCommand.Command) + if (spaceIndex == -1) + { + command = text.Substring(1).ToUpper(); + parameters = string.Empty; + } + else { - if (!IsHost && chatBoxCommand.HostOnly) + command = text.Substring(1, spaceIndex - 1); + parameters = text.Substring(spaceIndex + 1); + } + + tbChatInput.Text = string.Empty; + + foreach (var chatBoxCommand in chatBoxCommands) + { + if (command.ToUpper() == chatBoxCommand.Command) { - AddNotice(string.Format("/{0} is for game hosts only.".L10N("Client:Main:ChatboxCommandHostOnly"), chatBoxCommand.Command)); + if (!IsHost && chatBoxCommand.HostOnly) + { + AddNotice(string.Format("/{0} is for game hosts only.".L10N("Client:Main:ChatboxCommandHostOnly"), chatBoxCommand.Command)); + return; + } + + chatBoxCommand.Action(parameters); return; } + } - chatBoxCommand.Action(parameters); - return; + StringBuilder sb = new StringBuilder("To use a command, start your message with /. Possible chat box commands:".L10N("Client:Main:ChatboxCommandTipText") + " "); + foreach (var chatBoxCommand in chatBoxCommands) + { + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + sb.Append($"{chatBoxCommand.Command}: {chatBoxCommand.Description}"); } + XNAMessageBox.Show(WindowManager, "Chat Box Command Help".L10N("Client:Main:ChatboxCommandTipTitle"), sb.ToString()); + return; } - StringBuilder sb = new StringBuilder("To use a command, start your message with /. Possible chat box commands:".L10N("Client:Main:ChatboxCommandTipText") + " "); - foreach (var chatBoxCommand in chatBoxCommands) - { - sb.Append(Environment.NewLine); - sb.Append(Environment.NewLine); - sb.Append($"{chatBoxCommand.Command}: {chatBoxCommand.Description}"); - } - XNAMessageBox.Show(WindowManager, "Chat Box Command Help".L10N("Client:Main:ChatboxCommandTipTitle"), sb.ToString()); - return; + await SendChatMessageAsync(tbChatInput.Text); + tbChatInput.Text = string.Empty; + } + catch (Exception ex) + { + PreStartup.HandleException(ex); } - - SendChatMessage(tbChatInput.Text); - tbChatInput.Text = string.Empty; } - private void ChkAutoReady_CheckedChanged(object sender, EventArgs e) + private Task ChkAutoReady_CheckedChangedAsync() { UpdateLaunchGameButtonStatus(); - RequestReadyStatus(); + return RequestReadyStatusAsync(); } protected void ResetAutoReadyCheckbox() { - chkAutoReady.CheckedChanged -= ChkAutoReady_CheckedChanged; + chkAutoReady.CheckedChanged -= chkAutoReady_CheckedChangedFunc; chkAutoReady.Checked = false; - chkAutoReady.CheckedChanged += ChkAutoReady_CheckedChanged; + chkAutoReady.CheckedChanged += chkAutoReady_CheckedChangedFunc; UpdateLaunchGameButtonStatus(); } - private void SetFrameSendRate(string value) + private async Task SetFrameSendRateAsync(string value) { - bool success = int.TryParse(value, out int intValue); - - if (!success) + try { - AddNotice("Command syntax: /FrameSendRate ".L10N("Client:Main:ChatboxCommandFrameSendRateSyntax")); - return; - } - - FrameSendRate = intValue; - AddNotice(string.Format("FrameSendRate has been changed to {0}".L10N("Client:Main:FrameSendRateChanged"), intValue)); + bool success = int.TryParse(value, out int intValue); - OnGameOptionChanged(); - ClearReadyStatuses(); - } + if (!success) + { + AddNotice("Command syntax: /FrameSendRate ".L10N("Client:Main:ChatboxCommandFrameSendRateSyntax")); + return; + } - private void SetMaxAhead(string value) - { - bool success = int.TryParse(value, out int intValue); + FrameSendRate = intValue; + AddNotice(string.Format("FrameSendRate has been changed to {0}".L10N("Client:Main:FrameSendRateChanged"), intValue)); - if (!success) + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + ClearReadyStatuses(); + } + catch (Exception ex) { - AddNotice("Command syntax: /MaxAhead ".L10N("Client:Main:ChatboxCommandMaxAheadSyntax")); - return; + PreStartup.HandleException(ex); } - - MaxAhead = intValue; - AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("Client:Main:MaxAheadChanged"), intValue)); - - OnGameOptionChanged(); - ClearReadyStatuses(); } - private void SetProtocolVersion(string value) + private async Task SetMaxAheadAsync(string value) { - bool success = int.TryParse(value, out int intValue); + try + { + bool success = int.TryParse(value, out int intValue); - if (!success) + if (!success) + { + AddNotice("Command syntax: /MaxAhead ".L10N("Client:Main:ChatboxCommandMaxAheadSyntax")); + return; + } + + MaxAhead = intValue; + AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("Client:Main:MaxAheadChanged"), intValue)); + + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + } + catch (Exception ex) { - AddNotice("Command syntax: /ProtocolVersion .".L10N("Client:Main:ChatboxCommandProtocolVersionSyntax")); - return; + PreStartup.HandleException(ex); } + } - if (!(intValue == 0 || intValue == 2)) + private async Task SetProtocolVersionAsync(string value) + { + try { - AddNotice("ProtocolVersion only allows values 0 and 2.".L10N("Client:Main:ChatboxCommandProtocolVersionInvalid")); - return; - } + bool success = int.TryParse(value, out int intValue); - ProtocolVersion = intValue; - AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("Client:Main:ProtocolVersionChanged"), intValue)); + if (!success) + { + AddNotice("Command syntax: /ProtocolVersion .".L10N("Client:Main:ChatboxCommandProtocolVersionSyntax")); + return; + } - OnGameOptionChanged(); - ClearReadyStatuses(); + if (!(intValue == 0 || intValue == 2)) + { + AddNotice("ProtocolVersion only allows values 0 and 2.".L10N("Client:Main:ChatboxCommandProtocolVersionInvalid")); + return; + } + + ProtocolVersion = intValue; + AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("Client:Main:ProtocolVersionChanged"), intValue)); + + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void SetStartingLocationClearance(string value) + private async Task SetStartingLocationClearanceAsync(string value) { - bool removeStartingLocations = Conversions.BooleanFromString(value, RemoveStartingLocations); + try + { + bool removeStartingLocations = Conversions.BooleanFromString(value, RemoveStartingLocations); - SetRandomStartingLocations(removeStartingLocations); + SetRandomStartingLocations(removeStartingLocations); - OnGameOptionChanged(); - ClearReadyStatuses(); + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } /// @@ -482,44 +539,51 @@ protected void SetRandomStartingLocations(bool newValue) /// Handles the dice rolling command. /// /// The parameters given for the command by the user. - private void RollDiceCommand(string dieType) + private async Task RollDiceCommandAsync(string dieType) { - int dieSides = 6; - int dieCount = 1; - - if (!string.IsNullOrEmpty(dieType)) + try { - string[] parts = dieType.Split('d'); - if (parts.Length == 2) + int dieSides = 6; + int dieCount = 1; + + if (!string.IsNullOrEmpty(dieType)) { - if (!int.TryParse(parts[0], out dieCount) || !int.TryParse(parts[1], out dieSides)) + string[] parts = dieType.Split('d'); + if (parts.Length == 2) { - AddNotice("Invalid dice specified. Expected format: /roll d".L10N("Client:Main:ChatboxCommandRollInvalidAndSyntax")); - return; + if (!int.TryParse(parts[0], out dieCount) || !int.TryParse(parts[1], out dieSides)) + { + AddNotice("Invalid dice specified. Expected format: /roll d".L10N("Client:Main:ChatboxCommandRollInvalidAndSyntax")); + return; + } } } - } - if (dieCount > MAX_DICE || dieCount < 1) - { - AddNotice("You can only between 1 to 10 dies at once.".L10N("Client:Main:ChatboxCommandRollInvalid2")); - return; - } + if (dieCount > MAX_DICE || dieCount < 1) + { + AddNotice("You can only between 1 to 10 dies at once.".L10N("Client:Main:ChatboxCommandRollInvalid2")); + return; + } - if (dieSides > MAX_DIE_SIDES || dieSides < 2) - { - AddNotice("You can only have between 2 and 100 sides in a die.".L10N("Client:Main:ChatboxCommandRollInvalid3")); - return; - } + if (dieSides > MAX_DIE_SIDES || dieSides < 2) + { + AddNotice("You can only have between 2 and 100 sides in a die.".L10N("Client:Main:ChatboxCommandRollInvalid3")); + return; + } - int[] results = new int[dieCount]; - Random random = new Random(); - for (int i = 0; i < dieCount; i++) + int[] results = new int[dieCount]; + Random random = new Random(); + for (int i = 0; i < dieCount; i++) + { + results[i] = random.Next(1, dieSides + 1); + } + + await BroadcastDiceRollAsync(dieSides, results); + } + catch (Exception ex) { - results[i] = random.Next(1, dieSides + 1); + PreStartup.HandleException(ex); } - - BroadcastDiceRoll(dieSides, results); } /// @@ -545,7 +609,7 @@ private void LoadCustomMap(string mapName) /// /// The number of sides in the dice. /// The results of the dice roll. - protected abstract void BroadcastDiceRoll(int dieSides, int[] results); + protected abstract Task BroadcastDiceRollAsync(int dieSides, int[] results); /// /// Parses and lists the results of rolling dice. @@ -595,7 +659,7 @@ protected void PrintDiceRollResult(string senderName, int dieSides, int[] result )); } - protected abstract void SendChatMessage(string message); + protected abstract Task SendChatMessageAsync(string message); /// /// Changes the game lobby's UI depending on whether the local player is the host. @@ -734,11 +798,19 @@ private void MapPreviewBox_LocalStartingLocationSelected(object sender, LocalSta ddPlayerStarts[mTopIndex].SelectedIndex = e.StartingLocationIndex; } - private void MapPreviewBox_StartingLocationApplied(object sender, EventArgs e) + private async Task MapPreviewBox_StartingLocationAppliedAsync() { - ClearReadyStatuses(); - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); + try + { + ClearReadyStatuses(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } /// @@ -747,237 +819,342 @@ private void MapPreviewBox_StartingLocationApplied(object sender, EventArgs e) /// launches the game if it's allowed. If the local player isn't the game host, /// sends a ready request. /// - protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e) + protected override async Task BtnLaunchGame_LeftClickAsync(object sender, EventArgs e) { - if (!IsHost) + try { - RequestReadyStatus(); - return; - } - - if (!Locked) - { - LockGameNotification(); - return; - } - - var teamMappingsError = GetTeamMappingsError(); - if (!string.IsNullOrEmpty(teamMappingsError)) - { - AddNotice(teamMappingsError); - return; - } - - List occupiedColorIds = new List(); - foreach (PlayerInfo player in Players) - { - if (occupiedColorIds.Contains(player.ColorId) && player.ColorId > 0) + if (!IsHost) { - SharedColorsNotification(); + await RequestReadyStatusAsync(); return; } - occupiedColorIds.Add(player.ColorId); - } - - if (AIPlayers.Count(pInfo => pInfo.SideId == ddPlayerSides[0].Items.Count - 1) > 0) - { - AISpectatorsNotification(); - return; - } + if (!Locked) + { + await LockGameNotificationAsync(); + return; + } - if (Map.EnforceMaxPlayers) - { - foreach (PlayerInfo pInfo in Players) + var teamMappingsError = GetTeamMappingsError(); + if (!string.IsNullOrEmpty(teamMappingsError)) { - if (pInfo.StartingLocation == 0) - continue; + AddNotice(teamMappingsError); + return; + } - if (Players.Concat(AIPlayers).ToList().Find( - p => p.StartingLocation == pInfo.StartingLocation && - p.Name != pInfo.Name) != null) + List occupiedColorIds = new List(); + foreach (PlayerInfo player in Players) + { + if (occupiedColorIds.Contains(player.ColorId) && player.ColorId > 0) { - SharedStartingLocationNotification(); + await SharedColorsNotificationAsync(); return; } + + occupiedColorIds.Add(player.ColorId); } - for (int aiId = 0; aiId < AIPlayers.Count; aiId++) + if (AIPlayers.Any(pInfo => pInfo.SideId == ddPlayerSides[0].Items.Count - 1)) { - int startingLocation = AIPlayers[aiId].StartingLocation; + await AISpectatorsNotificationAsync(); + return; + } - if (startingLocation == 0) - continue; + if (Map.EnforceMaxPlayers) + { + foreach (PlayerInfo pInfo in Players) + { + if (pInfo.StartingLocation == 0) + continue; - int index = AIPlayers.FindIndex(aip => aip.StartingLocation == startingLocation); + if (Players.Concat(AIPlayers).ToList().Find( + p => p.StartingLocation == pInfo.StartingLocation && + p.Name != pInfo.Name) != null) + { + await SharedStartingLocationNotificationAsync(); + return; + } + } - if (index > -1 && index != aiId) + for (int aiId = 0; aiId < AIPlayers.Count; aiId++) { - SharedStartingLocationNotification(); - return; + int startingLocation = AIPlayers[aiId].StartingLocation; + + if (startingLocation == 0) + continue; + + int index = AIPlayers.FindIndex(aip => aip.StartingLocation == startingLocation); + + if (index > -1 && index != aiId) + { + await SharedStartingLocationNotificationAsync(); + return; + } } - } - int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) - + AIPlayers.Count; + int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + + AIPlayers.Count; - int minPlayers = GameMode.MinPlayersOverride > -1 ? GameMode.MinPlayersOverride : Map.MinPlayers; - if (totalPlayerCount < minPlayers) - { - InsufficientPlayersNotification(); - return; - } + int minPlayers = GameMode.MinPlayersOverride > -1 ? GameMode.MinPlayersOverride : Map.MinPlayers; + if (totalPlayerCount < minPlayers) + { + await InsufficientPlayersNotificationAsync(); + return; + } - if (Map.EnforceMaxPlayers && totalPlayerCount > Map.MaxPlayers) - { - TooManyPlayersNotification(); - return; + if (Map.EnforceMaxPlayers && totalPlayerCount > Map.MaxPlayers) + { + await TooManyPlayersNotificationAsync(); + return; + } } - } - int iId = 0; - foreach (PlayerInfo player in Players) - { - iId++; + int iId = 0; + foreach (PlayerInfo player in Players) + { + iId++; - if (player.Name == ProgramConstants.PLAYERNAME) - continue; + if (player.Name == ProgramConstants.PLAYERNAME) + continue; - if (!player.Verified) - { - NotVerifiedNotification(iId - 1); - return; - } + if (!player.Verified) + { + await NotVerifiedNotificationAsync(iId - 1); + return; + } - if (player.IsInGame) - { - StillInGameNotification(iId - 1); - return; - } - /* - if (DisableSpectatorReadyChecking) - { - // Only account ready status if player is not a spectator - if (!player.Ready && !IsPlayerSpectator(player)) + if (player.IsInGame) { - GetReadyNotification(); + await StillInGameNotificationAsync(iId - 1); return; } - } - else - { + /* + if (DisableSpectatorReadyChecking) + { + // Only account ready status if player is not a spectator + if (!player.Ready && !IsPlayerSpectator(player)) + { + await GetReadyNotificationAsync(); + return; + } + } + else + { + if (!player.Ready) + { + await GetReadyNotificationAsync(); + return; + } + } + */ + if (!player.Ready) { - GetReadyNotification(); + await GetReadyNotificationAsync(); return; } } - */ - if (!player.Ready) - { - GetReadyNotification(); - return; - } - + await HostLaunchGameAsync(); + + } + catch (Exception ex) + { + PreStartup.HandleException(ex); } + } - HostLaunchGame(); + protected virtual Task LockGameNotificationAsync() + { + try + { + AddNotice("You need to lock the game room before launching the game.".L10N("Client:Main:LockGameNotification")); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return Task.CompletedTask; } - protected virtual void LockGameNotification() => - AddNotice("You need to lock the game room before launching the game.".L10N("Client:Main:LockGameNotification")); + protected virtual Task SharedColorsNotificationAsync() + { + try + { + AddNotice("Multiple human players cannot share the same color.".L10N("Client:Main:SharedColorsNotification")); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } - protected virtual void SharedColorsNotification() => - AddNotice("Multiple human players cannot share the same color.".L10N("Client:Main:SharedColorsNotification")); + return Task.CompletedTask; + } - protected virtual void AISpectatorsNotification() => - AddNotice("AI players don't enjoy spectating matches. They want some action!".L10N("Client:Main:AISpectatorsNotification")); + protected virtual Task AISpectatorsNotificationAsync() + { + try + { + AddNotice("AI players don't enjoy spectating matches. They want some action!".L10N("Client:Main:AISpectatorsNotification")); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } - protected virtual void SharedStartingLocationNotification() => - AddNotice("Multiple players cannot share the same starting location on this map.".L10N("Client:Main:SharedStartingLocationNotification")); + return Task.CompletedTask; + } - protected virtual void NotVerifiedNotification(int playerIndex) + protected virtual Task SharedStartingLocationNotificationAsync() { - if (playerIndex > -1 && playerIndex < Players.Count) - AddNotice(string.Format("Unable to launch game. Player {0} hasn't been verified.".L10N("Client:Main:NotVerifiedNotification"), Players[playerIndex].Name)); + try + { + AddNotice("Multiple players cannot share the same starting location on this map.".L10N("Client:Main:SharedStartingLocationNotification")); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return Task.CompletedTask; } - protected virtual void StillInGameNotification(int playerIndex) + protected virtual Task NotVerifiedNotificationAsync(int playerIndex) { - if (playerIndex > -1 && playerIndex < Players.Count) + try + { + if (playerIndex > -1 && playerIndex < Players.Count) + AddNotice(string.Format("Unable to launch game. Player {0} hasn't been verified.".L10N("Client:Main:NotVerifiedNotification"), Players[playerIndex].Name)); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return Task.CompletedTask; + } + + protected virtual Task StillInGameNotificationAsync(int playerIndex) + { + try + { + if (playerIndex > -1 && playerIndex < Players.Count) + { + AddNotice(string.Format("Unable to launch game. Player {0} is still playing the game you started previously.".L10N("Client:Main:StillInGameNotification"), + Players[playerIndex].Name)); + } + } + catch (Exception ex) { - AddNotice(String.Format("Unable to launch game. Player {0} is still playing the game you started previously.".L10N("Client:Main:StillInGameNotification"), - Players[playerIndex].Name)); + PreStartup.HandleException(ex); } + + return Task.CompletedTask; } - protected virtual void GetReadyNotification() + protected virtual Task GetReadyNotificationAsync() { - AddNotice("The host wants to start the game but cannot because not all players are ready!".L10N("Client:Main:GetReadyNotification")); - if (!IsHost && !Players.Find(p => p.Name == ProgramConstants.PLAYERNAME).Ready) - sndGetReadySound.Play(); + try + { + AddNotice("The host wants to start the game but cannot because not all players are ready!".L10N("Client:Main:GetReadyNotification")); + if (!IsHost && !Players.Find(p => p.Name == ProgramConstants.PLAYERNAME).Ready) + sndGetReadySound.Play(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return Task.CompletedTask; } - protected virtual void InsufficientPlayersNotification() + protected virtual Task InsufficientPlayersNotificationAsync() { - if (GameMode != null && GameMode.MinPlayersOverride > -1) - AddNotice(String.Format("Unable to launch game: {0} cannot be played with fewer than {1} players".L10N("Client:Main:InsufficientPlayersNotification1"), - GameMode.UIName, GameMode.MinPlayersOverride)); - else if (Map != null) - AddNotice(String.Format("Unable to launch game: this map cannot be played with fewer than {0} players.".L10N("Client:Main:InsufficientPlayersNotification2"), - Map.MinPlayers)); + try + { + if (GameMode != null && GameMode.MinPlayersOverride > -1) + AddNotice(String.Format("Unable to launch game: {0} cannot be played with fewer than {1} players".L10N("Client:Main:InsufficientPlayersNotification1"), + GameMode.UIName, GameMode.MinPlayersOverride)); + else if (Map != null) + AddNotice(String.Format("Unable to launch game: this map cannot be played with fewer than {0} players.".L10N("Client:Main:InsufficientPlayersNotification2"), + Map.MinPlayers)); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return Task.CompletedTask; } - protected virtual void TooManyPlayersNotification() + protected virtual Task TooManyPlayersNotificationAsync() { - if (Map != null) - AddNotice(String.Format("Unable to launch game: this map cannot be played with more than {0} players.".L10N("Client:Main:TooManyPlayersNotification"), - Map.MaxPlayers)); + try + { + if (Map != null) + AddNotice(String.Format("Unable to launch game: this map cannot be played with more than {0} players.".L10N("Client:Main:TooManyPlayersNotification"), + Map.MaxPlayers)); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return Task.CompletedTask; } - public virtual void Clear() + public virtual Task ClearAsync() { if (!IsHost) AIPlayers.Clear(); Players.Clear(); + + return Task.CompletedTask; } - protected override void OnGameOptionChanged() + protected override async Task OnGameOptionChangedAsync() { - base.OnGameOptionChanged(); + await base.OnGameOptionChangedAsync(); ClearReadyStatuses(); CopyPlayerDataToUI(); } - protected abstract void HostLaunchGame(); + protected abstract Task HostLaunchGameAsync(); - protected override void CopyPlayerDataFromUI(object sender, EventArgs e) + protected override async Task CopyPlayerDataFromUIAsync(object sender, EventArgs e) { - if (PlayerUpdatingInProgress) - return; - - if (IsHost) + try { - base.CopyPlayerDataFromUI(sender, e); - BroadcastPlayerOptions(); - return; - } + if (PlayerUpdatingInProgress) + return; - int mTopIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); + if (IsHost) + { + await base.CopyPlayerDataFromUIAsync(sender, e); + await BroadcastPlayerOptionsAsync(); + return; + } - if (mTopIndex == -1) - return; + int mTopIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); + + if (mTopIndex == -1) + return; - int requestedSide = ddPlayerSides[mTopIndex].SelectedIndex; - int requestedColor = ddPlayerColors[mTopIndex].SelectedIndex; - int requestedStart = ddPlayerStarts[mTopIndex].SelectedIndex; - int requestedTeam = ddPlayerTeams[mTopIndex].SelectedIndex; + int requestedSide = ddPlayerSides[mTopIndex].SelectedIndex; + int requestedColor = ddPlayerColors[mTopIndex].SelectedIndex; + int requestedStart = ddPlayerStarts[mTopIndex].SelectedIndex; + int requestedTeam = ddPlayerTeams[mTopIndex].SelectedIndex; - RequestPlayerOptions(requestedSide, requestedColor, requestedStart, requestedTeam); + await RequestPlayerOptionsAsync(requestedSide, requestedColor, requestedStart, requestedTeam); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } protected override void CopyPlayerDataToUI() @@ -1002,7 +1179,8 @@ protected override void CopyPlayerDataToUI() { StatusIndicators[pId].SwitchTexture("error"); } - else */ if (Players[pId].IsInGame) // If player is ingame + else */ + if (Players[pId].IsInGame) // If player is ingame { StatusIndicators[pId].SwitchTexture(PlayerSlotState.InGame); } @@ -1083,13 +1261,13 @@ private Texture2D GetTextureForPing(int ping) } } - protected abstract void BroadcastPlayerOptions(); + protected abstract Task BroadcastPlayerOptionsAsync(); - protected abstract void BroadcastPlayerExtraOptions(); + protected abstract Task BroadcastPlayerExtraOptionsAsync(); - protected abstract void RequestPlayerOptions(int side, int color, int start, int team); + protected abstract Task RequestPlayerOptionsAsync(int side, int color, int start, int team); - protected abstract void RequestReadyStatus(); + protected abstract Task RequestReadyStatusAsync(); // this public as it is used by the main lobby to notify the user of invitation failure public void AddWarning(string message) @@ -1099,21 +1277,18 @@ public void AddWarning(string message) protected override bool AllowPlayerOptionsChange() => IsHost; - protected override void ChangeMap(GameModeMap gameModeMap) + protected override async Task ChangeMapAsync(GameModeMap gameModeMap) { - base.ChangeMap(gameModeMap); + await base.ChangeMapAsync(gameModeMap); bool resetAutoReady = gameModeMap?.GameMode == null || gameModeMap?.Map == null; ClearReadyStatuses(resetAutoReady); if ((lastMapChangeWasInvalid || resetAutoReady) && chkAutoReady.Checked) - RequestReadyStatus(); + await RequestReadyStatusAsync(); lastMapChangeWasInvalid = resetAutoReady; - - //if (IsHost) - // OnGameOptionChanged(); } protected override void ToggleFavoriteMap() diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs index a94c711ec..c26d16456 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -9,6 +9,7 @@ using ClientGUI; using Rampastring.Tools; using System.IO; +using System.Threading.Tasks; using DTAClient.Domain; using Microsoft.Xna.Framework; using ClientCore.Extensions; @@ -165,29 +166,45 @@ private string CheckGameValidity() return null; } - protected override void BtnLaunchGame_LeftClick(object sender, EventArgs e) + protected override async Task BtnLaunchGame_LeftClickAsync(object sender, EventArgs e) { - string error = CheckGameValidity(); + try + { + string error = CheckGameValidity(); - if (error == null) + if (error == null) + { + SaveSettings(); + await StartGameAsync(); + return; + } + + XNAMessageBox.Show(WindowManager, "Cannot launch game".L10N("Client:Main:LaunchGameErrorTitle"), error); + } + catch (Exception ex) { - SaveSettings(); - StartGame(); - return; + PreStartup.HandleException(ex); } - - XNAMessageBox.Show(WindowManager, "Cannot launch game".L10N("Client:Main:LaunchGameErrorTitle"), error); } - protected override void BtnLeaveGame_LeftClick(object sender, EventArgs e) + protected override Task BtnLeaveGame_LeftClickAsync(object sender, EventArgs e) { - this.Enabled = false; - this.Visible = false; + try + { + Enabled = false; + Visible = false; - Exited?.Invoke(this, EventArgs.Empty); + Exited?.Invoke(this, EventArgs.Empty); - topBar.RemovePrimarySwitchable(this); - ResetDiscordPresence(); + topBar.RemovePrimarySwitchable(this); + ResetDiscordPresence(); + + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + return Task.CompletedTask; } private void PlayerSideChanged(object sender, EventArgs e) @@ -225,13 +242,20 @@ protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap) return StatisticsManager.Instance.GetSkirmishRankForDefaultMap(gameModeMap.Map.UntranslatedName, gameModeMap.Map.MaxPlayers); } - protected override void GameProcessExited() + protected override async Task GameProcessExitedAsync() { - base.GameProcessExited(); + try + { + await base.GameProcessExitedAsync(); - DdGameModeMapFilter_SelectedIndexChanged(null, EventArgs.Empty); // Refresh ranks + await DdGameModeMapFilter_SelectedIndexChangedAsync(); // Refresh ranks - RandomSeed = new Random().Next(); + RandomSeed = new Random().Next(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } public void Open() diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index 3c3635524..ee56fe08b 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -10,11 +10,15 @@ using Rampastring.Tools; using Rampastring.XNAUI; using System; +#if !NETFRAMEWORK +using System.Buffers; +#endif using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace DTAClient.DXGUI.Multiplayer { @@ -46,9 +50,9 @@ DiscordHandler discordHandler hostCommandHandlers = new LANServerCommandHandler[] { - new ServerStringCommandHandler(CHAT_COMMAND, Server_HandleChatMessage), + new ServerStringCommandHandler(CHAT_COMMAND, (sender, data) => Server_HandleChatMessageAsync(sender, data)), new ServerStringCommandHandler(FILE_HASH_COMMAND, Server_HandleFileHashMessage), - new ServerNoParamCommandHandler(READY_STATUS_COMMAND, Server_HandleReadyRequest), + new ServerNoParamCommandHandler(READY_STATUS_COMMAND, sender => Server_HandleReadyRequestAsync(sender)) }; playerCommandHandlers = new LANClientCommandHandler[] @@ -58,29 +62,27 @@ DiscordHandler discordHandler new ClientNoParamCommandHandler(GAME_LAUNCH_COMMAND, Client_HandleStartCommand) }; - WindowManager.GameClosing += WindowManager_GameClosing; + WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync(); } - private void WindowManager_GameClosing(object sender, EventArgs e) + private async Task WindowManager_GameClosingAsync() { - if (client != null && client.Connected) - Clear(); + if (client is { Connected: true }) + await ClearAsync(); } - public event EventHandler LobbyNotification; public event EventHandler GameBroadcast; - private TcpListener listener; - private TcpClient client; + private Socket listener; + private Socket client; - private IPEndPoint hostEndPoint; - private LANColor[] chatColors; + private readonly LANColor[] chatColors; private readonly MapLoader mapLoader; private int chatColorIndex; - private Encoding encoding; + private readonly Encoding encoding; - private LANServerCommandHandler[] hostCommandHandlers; - private LANClientCommandHandler[] playerCommandHandlers; + private readonly LANServerCommandHandler[] hostCommandHandlers; + private readonly LANClientCommandHandler[] playerCommandHandlers; private TimeSpan timeSinceGameBroadcast = TimeSpan.Zero; @@ -88,7 +90,7 @@ private void WindowManager_GameClosing(object sender, EventArgs e) private string overMessage = string.Empty; - private string localGame; + private readonly string localGame; private string localFileHash; @@ -96,34 +98,45 @@ private void WindowManager_GameClosing(object sender, EventArgs e) private int loadedGameId; - private bool started = false; + private bool started; - public void SetUp(bool isHost, - IPEndPoint hostEndPoint, TcpClient client, - int loadedGameId) + private CancellationTokenSource cancellationTokenSource; + + public async Task SetUpAsync(bool isHost, Socket client, int loadedGameId) { Refresh(isHost); - this.hostEndPoint = hostEndPoint; - this.loadedGameId = loadedGameId; started = false; + cancellationTokenSource?.Dispose(); + cancellationTokenSource = new CancellationTokenSource(); + if (isHost) { - Thread thread = new Thread(ListenForClients); - thread.Start(); + ListenForClientsAsync(cancellationTokenSource.Token); + + this.client = new Socket(SocketType.Stream, ProtocolType.Tcp); + await this.client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT); - this.client = new TcpClient(); - this.client.Connect("127.0.0.1", ProgramConstants.LAN_GAME_LOBBY_PORT); + string message = PLAYER_JOIN_COMMAND + + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + + ProgramConstants.LAN_DATA_SEPARATOR + loadedGameId; - byte[] buffer = encoding.GetBytes(PLAYER_JOIN_COMMAND + - ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + - ProgramConstants.LAN_DATA_SEPARATOR + loadedGameId); +#if NETFRAMEWORK + byte[] buffer1 = encoding.GetBytes(message); + var buffer = new ArraySegment(buffer1); - this.client.GetStream().Write(buffer, 0, buffer.Length); - this.client.GetStream().Flush(); + await this.client.SendAsync(buffer, SocketFlags.None); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); + Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; + + await this.client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); +#endif var fhc = new FileHashCalculator(); fhc.CalculateHashes(gameModes); @@ -131,10 +144,11 @@ public void SetUp(bool isHost, } else { + this.client?.Dispose(); this.client = client; } - new Thread(HandleServerCommunication).Start(); + HandleServerCommunicationAsync(cancellationTokenSource.Token); if (IsHost) CopyPlayerDataToUI(); @@ -142,146 +156,208 @@ public void SetUp(bool isHost, WindowManager.SelectedControl = tbChatInput; } - public void PostJoin() + public async Task PostJoinAsync() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(gameModes); - SendMessageToHost(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash()); + await SendMessageToHostAsync(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash(), cancellationTokenSource.Token); UpdateDiscordPresence(true); } #region Server code - private void ListenForClients() + private async Task ListenForClientsAsync(CancellationToken cancellationToken) { - listener = new TcpListener(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT); - listener.Start(); - - while (true) + try { - TcpClient client; - - try - { - client = listener.AcceptTcpClient(); - } - catch (Exception ex) + listener = new Socket(SocketType.Stream, ProtocolType.Tcp); + listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); +#if NETFRAMEWORK + listener.Listen(int.MaxValue); +#else + listener.Listen(); +#endif + + while (!cancellationToken.IsCancellationRequested) { - Logger.Log("Listener error: " + ex.Message); - break; - } + Socket newPlayerSocket; + +#if NETFRAMEWORK + try + { + newPlayerSocket = await listener.AcceptAsync(); + } +#else + try + { + newPlayerSocket = await listener.AcceptAsync(cancellationToken); + } + catch (OperationCanceledException) + { + break; + } +#endif + catch (Exception ex) + { + PreStartup.LogException(ex, "Listener error."); + break; + } - Logger.Log("New client connected from " + ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString()); + Logger.Log("New client connected from " + ((IPEndPoint)newPlayerSocket.RemoteEndPoint).Address); - LANPlayerInfo lpInfo = new LANPlayerInfo(encoding); - lpInfo.SetClient(client); + LANPlayerInfo lpInfo = new LANPlayerInfo(encoding); + lpInfo.SetClient(newPlayerSocket); - Thread thread = new Thread(new ParameterizedThreadStart(HandleClientConnection)); - thread.Start(lpInfo); + HandleClientConnectionAsync(lpInfo, cancellationToken); + } + } + catch (Exception ex) + { + PreStartup.HandleException(ex); } } - private void HandleClientConnection(object clientInfo) + private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { - var lpInfo = (LANPlayerInfo)clientInfo; - - byte[] message = new byte[1024]; - - while (true) + try { - int bytesRead = 0; +#if !NETFRAMEWORK + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); - try +#endif + while (!cancellationToken.IsCancellationRequested) { - bytesRead = lpInfo.TcpClient.GetStream().Read(message, 0, message.Length); - } - catch (Exception ex) - { - Logger.Log("Socket error with client " + lpInfo.IPAddress + "; removing. Message: " + ex.Message); - break; - } + int bytesRead; +#if NETFRAMEWORK + byte[] buffer1; +#else + Memory message; +#endif + + try + { +#if NETFRAMEWORK + buffer1 = new byte[1024]; + var message = new ArraySegment(buffer1); + bytesRead = await client.ReceiveAsync(message, SocketFlags.None); + } +#else + message = memoryOwner.Memory[..1024]; + bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); + } + catch (OperationCanceledException) + { + break; + } +#endif + catch (Exception ex) + { + PreStartup.LogException(ex, "Socket error with client " + lpInfo.IPAddress + "; removing."); + break; + } - if (bytesRead == 0) - { - Logger.Log("Connect attempt from " + lpInfo.IPAddress + " failed! (0 bytes read)"); + if (bytesRead == 0) + { + Logger.Log("Connect attempt from " + lpInfo.IPAddress + " failed! (0 bytes read)"); - break; - } + break; + } - string msg = encoding.GetString(message, 0, bytesRead); +#if NETFRAMEWORK + string msg = encoding.GetString(buffer1, 0, bytesRead); +#else + string msg = encoding.GetString(message.Span[..bytesRead]); +#endif + string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR); + string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR); - string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR); - string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR); + if (parts.Length != 3) + break; - if (parts.Length != 3) - break; + string name = parts[1].Trim(); + int loadedGameId = Conversions.IntFromString(parts[2], -1); - string name = parts[1].Trim(); - int loadedGameId = Conversions.IntFromString(parts[2], -1); + if (parts[0] == "JOIN" && !string.IsNullOrEmpty(name) + && loadedGameId == this.loadedGameId) + { + lpInfo.Name = name; - if (parts[0] == "JOIN" && !string.IsNullOrEmpty(name) - && loadedGameId == this.loadedGameId) - { - lpInfo.Name = name; + AddCallback(() => AddPlayerAsync(lpInfo, cancellationToken)); + return; + } - AddCallback(new Action(AddPlayer), lpInfo); - return; + break; } - break; + if (lpInfo.TcpClient.Connected) + lpInfo.TcpClient.Close(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); } - - if (lpInfo.TcpClient.Connected) - lpInfo.TcpClient.Close(); } - private void AddPlayer(LANPlayerInfo lpInfo) + private async Task AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { - if (Players.Find(p => p.Name == lpInfo.Name) != null || - Players.Count >= SGPlayers.Count || - SGPlayers.Find(p => p.Name == lpInfo.Name) == null) + try { - lpInfo.TcpClient.Close(); - return; - } + if (Players.Find(p => p.Name == lpInfo.Name) != null || + Players.Count >= SGPlayers.Count || + SGPlayers.Find(p => p.Name == lpInfo.Name) == null) + { + lpInfo.TcpClient.Close(); + return; + } - if (Players.Count == 0) - lpInfo.Ready = true; + if (Players.Count == 0) + lpInfo.Ready = true; - Players.Add(lpInfo); + Players.Add(lpInfo); - lpInfo.MessageReceived += LpInfo_MessageReceived; - lpInfo.ConnectionLost += LpInfo_ConnectionLost; + lpInfo.MessageReceived += LpInfo_MessageReceived; + lpInfo.ConnectionLost += (sender, _) => LpInfo_ConnectionLostAsync(sender); - sndJoinSound.Play(); + sndJoinSound.Play(); - AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); - lpInfo.StartReceiveLoop(); + AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); + lpInfo.StartReceiveLoop(cancellationToken); - CopyPlayerDataToUI(); - BroadcastOptions(); - UpdateDiscordPresence(); + CopyPlayerDataToUI(); + await BroadcastOptionsAsync(); + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void LpInfo_ConnectionLost(object sender, EventArgs e) + private async Task LpInfo_ConnectionLostAsync(object sender) { - var lpInfo = (LANPlayerInfo)sender; - CleanUpPlayer(lpInfo); - Players.Remove(lpInfo); + try + { + var lpInfo = (LANPlayerInfo)sender; + CleanUpPlayer(lpInfo); + Players.Remove(lpInfo); - AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); + AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); - sndLeaveSound.Play(); + sndLeaveSound.Play(); - CopyPlayerDataToUI(); - BroadcastOptions(); - UpdateDiscordPresence(); + CopyPlayerDataToUI(); + await BroadcastOptionsAsync(); + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void LpInfo_MessageReceived(object sender, NetworkMessageEventArgs e) { - AddCallback(new Action(HandleClientMessage), - e.Message, (LANPlayerInfo)sender); + AddCallback(() => HandleClientMessage(e.Message, (LANPlayerInfo)sender)); } private void HandleClientMessage(string data, LANPlayerInfo lpInfo) @@ -294,7 +370,7 @@ private void HandleClientMessage(string data, LANPlayerInfo lpInfo) return; } - Logger.Log("Unknown LAN command from " + lpInfo.ToString() + " : " + data); + Logger.Log("Unknown LAN command from " + lpInfo + " : " + data); } private void CleanUpPlayer(LANPlayerInfo lpInfo) @@ -305,37 +381,54 @@ private void CleanUpPlayer(LANPlayerInfo lpInfo) #endregion - private void HandleServerCommunication() + private async Task HandleServerCommunicationAsync(CancellationToken cancellationToken) { - byte[] message = new byte[1024]; - - var msg = string.Empty; - - int bytesRead = 0; - if (!client.Connected) return; - var stream = client.GetStream(); +#if !NETFRAMEWORK + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); - while (true) +#endif + while (!cancellationToken.IsCancellationRequested) { - bytesRead = 0; + int bytesRead; +#if NETFRAMEWORK + byte[] buffer1; +#else + Memory message; +#endif try { - bytesRead = stream.Read(message, 0, message.Length); +#if NETFRAMEWORK + buffer1 = new byte[1024]; + var message = new ArraySegment(buffer1); + bytesRead = await client.ReceiveAsync(message, SocketFlags.None); + } +#else + message = memoryOwner.Memory[..1024]; + bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); + } + catch (OperationCanceledException) + { + break; } +#endif catch (Exception ex) { Logger.Log("Reading data from the server failed! Message: " + ex.Message); - LeaveGame(); + await LeaveGameAsync(); break; } if (bytesRead > 0) { - msg = encoding.GetString(message, 0, bytesRead); +#if NETFRAMEWORK + string msg = encoding.GetString(buffer1, 0, bytesRead); +#else + string msg = encoding.GetString(message.Span[..bytesRead]); +#endif msg = overMessage + msg; List commands = new List(); @@ -349,23 +442,21 @@ private void HandleServerCommunication() overMessage = msg; break; } - else - { - commands.Add(msg.Substring(0, index)); - msg = msg.Substring(index + 1); - } + + commands.Add(msg.Substring(0, index)); + msg = msg.Substring(index + 1); } foreach (string cmd in commands) { - AddCallback(new Action(HandleMessageFromServer), cmd); + AddCallback(() => HandleMessageFromServer(cmd)); } continue; } Logger.Log("Reading data from the server failed (0 bytes received)!"); - LeaveGame(); + await LeaveGameAsync(); break; } } @@ -383,30 +474,39 @@ private void HandleMessageFromServer(string message) Logger.Log("Unknown LAN command from the server: " + message); } - protected override void LeaveGame() + protected override async Task LeaveGameAsync() { - Clear(); - Disable(); + try + { + await ClearAsync(); + Disable(); - base.LeaveGame(); + await base.LeaveGameAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void Clear() + private async Task ClearAsync() { if (IsHost) { - BroadcastMessage(PLAYER_QUIT_COMMAND); + await BroadcastMessageAsync(PLAYER_QUIT_COMMAND, CancellationToken.None); Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p)); Players.Clear(); - listener.Stop(); + listener.Close(); } else { - SendMessageToHost(PLAYER_QUIT_COMMAND); + await SendMessageToHostAsync(PLAYER_QUIT_COMMAND, CancellationToken.None); } - if (this.client.Connected) - this.client.Close(); + cancellationTokenSource.Cancel(); + + if (client.Connected) + client.Close(); } protected override void AddNotice(string message, Color color) @@ -414,7 +514,7 @@ protected override void AddNotice(string message, Color color) lbChatMessages.AddMessage(null, message, color); } - protected override void BroadcastOptions() + protected override async Task BroadcastOptionsAsync() { if (Players.Count > 0) Players[0].Ready = true; @@ -431,30 +531,26 @@ protected override void BroadcastOptions() sb.Append(pInfo.IPAddress); } - BroadcastMessage(sb.ToString()); + await BroadcastMessageAsync(sb.ToString(), cancellationTokenSource.Token); } - protected override void HostStartGame() - { - BroadcastMessage(GAME_LAUNCH_COMMAND); - } + protected override Task HostStartGameAsync() + => BroadcastMessageAsync(GAME_LAUNCH_COMMAND, cancellationTokenSource.Token); - protected override void RequestReadyStatus() - { - SendMessageToHost(READY_STATUS_COMMAND); - } + protected override Task RequestReadyStatusAsync() + => SendMessageToHostAsync(READY_STATUS_COMMAND, cancellationTokenSource.Token); - protected override void SendChatMessage(string message) + protected override async Task SendChatMessageAsync(string message) { - SendMessageToHost(CHAT_COMMAND + " " + chatColorIndex + - ProgramConstants.LAN_DATA_SEPARATOR + message); + await SendMessageToHostAsync(CHAT_COMMAND + " " + chatColorIndex + + ProgramConstants.LAN_DATA_SEPARATOR + message, cancellationTokenSource.Token); sndMessageSound.Play(); } #region Server's command handlers - private void Server_HandleChatMessage(LANPlayerInfo sender, string data) + private async Task Server_HandleChatMessageAsync(LANPlayerInfo sender, string data) { string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); @@ -466,9 +562,9 @@ private void Server_HandleChatMessage(LANPlayerInfo sender, string data) if (colorIndex < 0 || colorIndex >= chatColors.Length) return; - BroadcastMessage(CHAT_COMMAND + " " + sender + + await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + colorIndex + - ProgramConstants.LAN_DATA_SEPARATOR + data); + ProgramConstants.LAN_DATA_SEPARATOR + data, cancellationTokenSource.Token); } private void Server_HandleFileHashMessage(LANPlayerInfo sender, string hash) @@ -478,13 +574,20 @@ private void Server_HandleFileHashMessage(LANPlayerInfo sender, string hash) sender.Verified = true; } - private void Server_HandleReadyRequest(LANPlayerInfo sender) + private async Task Server_HandleReadyRequestAsync(LANPlayerInfo sender) { - if (!sender.Ready) + try { - sender.Ready = true; - CopyPlayerDataToUI(); - BroadcastOptions(); + if (!sender.Ready) + { + sender.Ready = true; + CopyPlayerDataToUI(); + await BroadcastOptionsAsync(); + } + } + catch (Exception ex) + { + PreStartup.HandleException(ex); } } @@ -549,7 +652,7 @@ private void Client_HandleOptionsMessage(string data) } if (Players.Count > 0) // Set IP of host - Players[0].IPAddress = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString(); + Players[0].IPAddress = ((IPEndPoint)client.RemoteEndPoint).Address.ToString(); CopyPlayerDataToUI(); } @@ -567,7 +670,7 @@ private void Client_HandleStartCommand() /// Broadcasts a command to all players in the game as the game host. /// /// The command to send. - private void BroadcastMessage(string message) + private async Task BroadcastMessageAsync(string message, CancellationToken cancellationToken) { if (!IsHost) return; @@ -575,40 +678,47 @@ private void BroadcastMessage(string message) foreach (PlayerInfo pInfo in Players) { var lpInfo = (LANPlayerInfo)pInfo; - lpInfo.SendMessage(message); + await lpInfo.SendMessageAsync(message, cancellationToken); } } - private void SendMessageToHost(string message) + private async Task SendMessageToHostAsync(string message, CancellationToken cancellationToken) { if (!client.Connected) return; - byte[] buffer = encoding.GetBytes( - message + ProgramConstants.LAN_MESSAGE_SEPARATOR); + message += ProgramConstants.LAN_MESSAGE_SEPARATOR; - NetworkStream ns = client.GetStream(); +#if NETFRAMEWORK + byte[] buffer1 = encoding.GetBytes(message); + var buffer = new ArraySegment(buffer1); try { - ns.Write(buffer, 0, buffer.Length); - ns.Flush(); + await client.SendAsync(buffer, SocketFlags.None); } - catch +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); + Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; + + try { - Logger.Log("Sending message to game host failed!"); + await client.SendAsync(buffer, SocketFlags.None, cancellationToken); + } + catch (OperationCanceledException) + { + } +#endif + catch (Exception ex) + { + PreStartup.LogException(ex, "Sending message to game host failed!"); } - } - - public void SetChatColorIndex(int colorIndex) - { - chatColorIndex = colorIndex; } public override string GetSwitchName() - { - return "Load Game".L10N("Client:Main:LoadGameSwitchName"); - } + => "Load Game".L10N("Client:Main:LoadGameSwitchName"); public override void Update(GameTime gameTime) { @@ -617,13 +727,13 @@ public override void Update(GameTime gameTime) for (int i = 1; i < Players.Count; i++) { LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i]; - if (!lpInfo.Update(gameTime)) + if (!Task.Run(() => lpInfo.UpdateAsync(gameTime)).Result) { CleanUpPlayer(lpInfo); Players.RemoveAt(i); AddNotice(string.Format("{0} - connection timed out".L10N("Client:Main:PlayerTimeout"), lpInfo.Name)); CopyPlayerDataToUI(); - BroadcastOptions(); + Task.Run(BroadcastOptionsAsync).Wait(); UpdateDiscordPresence(); i--; } @@ -642,11 +752,7 @@ public override void Update(GameTime gameTime) timeSinceLastReceivedCommand += gameTime.ElapsedGameTime; if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT)) - { - LobbyNotification?.Invoke(this, - new LobbyNotificationEventArgs("Connection to the game host timed out.".L10N("Client:Main:HostConnectTimeOut"))); - LeaveGame(); - } + Task.Run(LeaveGameAsync).Wait(); } base.Update(gameTime); @@ -672,11 +778,18 @@ private void BroadcastGame() GameBroadcast?.Invoke(this, new GameBroadcastEventArgs(sb.ToString())); } - protected override void HandleGameProcessExited() + protected override async Task HandleGameProcessExitedAsync() { - base.HandleGameProcessExited(); + try + { + await base.HandleGameProcessExitedAsync(); - LeaveGame(); + await LeaveGameAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } protected override void UpdateDiscordPresence(bool resetTimer = false) @@ -695,4 +808,4 @@ protected override void UpdateDiscordPresence(bool resetTimer = false) "LAN Game", IsHost, resetTimer); } } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 3cfdec299..95efb93df 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -14,6 +14,9 @@ using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; +#if !NETFRAMEWORK +using System.Buffers; +#endif using System.Collections.Generic; using System.IO; using System.Linq; @@ -22,6 +25,7 @@ using System.Reflection; using System.Text; using System.Threading; +using System.Threading.Tasks; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; @@ -32,7 +36,6 @@ class LANLobby : XNAWindow { private const double ALIVE_MESSAGE_INTERVAL = 5.0; private const double INACTIVITY_REMOVE_TIME = 10.0; - private const double GAME_INACTIVITY_REMOVE_TIME = 20.0; public LANLobby( WindowManager windowManager, @@ -79,10 +82,6 @@ DiscordHandler discordHandler private List gameModes => mapLoader.GameModes; - TimeSpan timeSinceGameRefresh = TimeSpan.Zero; - - EnhancedSoundEffect sndGameCreated; - Socket socket; IPEndPoint endPoint; Encoding encoding; @@ -95,7 +94,9 @@ DiscordHandler discordHandler DiscordHandler discordHandler; - bool initSuccess = false; + bool initSuccess; + + private CancellationTokenSource cancellationTokenSource; public override void Initialize() { @@ -112,21 +113,21 @@ public override void Initialize() btnNewGame.Name = "btnNewGame"; btnNewGame.ClientRectangle = new Rectangle(12, Height - 35, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnNewGame.Text = "Create Game".L10N("Client:Main:CreateGame"); - btnNewGame.LeftClick += BtnNewGame_LeftClick; + btnNewGame.LeftClick += (_, _) => BtnNewGame_LeftClickAsync(); btnJoinGame = new XNAClientButton(WindowManager); btnJoinGame.Name = "btnJoinGame"; btnJoinGame.ClientRectangle = new Rectangle(btnNewGame.Right + 12, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnJoinGame.Text = "Join Game".L10N("Client:Main:JoinGame"); - btnJoinGame.LeftClick += BtnJoinGame_LeftClick; + btnJoinGame.LeftClick += (_, _) => BtnJoinGame_LeftClickAsync(); btnMainMenu = new XNAClientButton(WindowManager); btnMainMenu.Name = "btnMainMenu"; btnMainMenu.ClientRectangle = new Rectangle(Width - 145, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnMainMenu.Text = "Main Menu".L10N("Client:Main:MainMenu"); - btnMainMenu.LeftClick += BtnMainMenu_LeftClick; + btnMainMenu.LeftClick += (_, _) => BtnMainMenu_LeftClickAsync(cancellationTokenSource?.Token ?? default); lbGameList = new GameListBox(WindowManager, mapLoader, localGame); lbGameList.Name = "lbGameList"; @@ -136,7 +137,7 @@ public override void Initialize() lbGameList.GameLifetime = 15.0; // Smaller lifetime in LAN lbGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); - lbGameList.DoubleLeftClick += LbGameList_DoubleLeftClick; + lbGameList.DoubleLeftClick += (_, _) => LbGameList_DoubleLeftClickAsync(); lbGameList.AllowMultiLineItems = false; lbPlayerList = new XNAListBox(WindowManager); @@ -165,7 +166,7 @@ public override void Initialize() btnNewGame.Height); tbChatInput.Suggestion = "Type here to chat...".L10N("Client:Main:ChatHere"); tbChatInput.MaximumTextLength = 200; - tbChatInput.EnterPressed += TbChatInput_EnterPressed; + tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync(cancellationTokenSource?.Token ?? default); lblColor = new XNALabel(WindowManager); lblColor.Name = "lblColor"; @@ -219,16 +220,14 @@ public override void Initialize() gameCreationPanel.AddChild(gameCreationWindow); gameCreationWindow.Disable(); - gameCreationWindow.NewGame += GameCreationWindow_NewGame; - gameCreationWindow.LoadGame += GameCreationWindow_LoadGame; + gameCreationWindow.NewGame += (_, _) => GameCreationWindow_NewGameAsync(); + gameCreationWindow.LoadGame += (_, e) => GameCreationWindow_LoadGameAsync(e); var assembly = Assembly.GetAssembly(typeof(GameCollection)); using Stream unknownIconStream = assembly.GetManifestResourceStream("ClientCore.Resources.unknownicon.png"); unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream)); - sndGameCreated = new EnhancedSoundEffect("gamecreated.wav"); - encoding = Encoding.UTF8; base.Initialize(); @@ -255,68 +254,79 @@ public override void Initialize() ddColor.SelectedIndexChanged += DdColor_SelectedIndexChanged; lanGameLobby.GameLeft += LanGameLobby_GameLeft; - lanGameLobby.GameBroadcast += LanGameLobby_GameBroadcast; + lanGameLobby.GameBroadcast += (_, e) => LanGameLobby_GameBroadcastAsync(e, cancellationTokenSource?.Token ?? default); - lanGameLoadingLobby.GameBroadcast += LanGameLoadingLobby_GameBroadcast; + lanGameLoadingLobby.GameBroadcast += (_, e) => LanGameLoadingLobby_GameBroadcastAsync(e, cancellationTokenSource?.Token ?? default); lanGameLoadingLobby.GameLeft += LanGameLoadingLobby_GameLeft; - WindowManager.GameClosing += WindowManager_GameClosing; + WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync(cancellationTokenSource?.Token ?? default); } private void LanGameLoadingLobby_GameLeft(object sender, EventArgs e) - { - Enable(); - } + => Enable(); - private void WindowManager_GameClosing(object sender, EventArgs e) + private async Task WindowManager_GameClosingAsync(CancellationToken cancellationToken) { - if (socket == null) - return; - - if (socket.IsBound) + try { - try - { - SendMessage("QUIT"); - socket.Close(); - } - catch (ObjectDisposedException) - { + if (socket == null) + return; + if (socket.IsBound) + { + try + { + await SendMessageAsync("QUIT", cancellationToken); + cancellationTokenSource.Cancel(); + socket.Close(); + } + catch (ObjectDisposedException) + { + } } } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void LanGameLobby_GameBroadcast(object sender, GameBroadcastEventArgs e) - { - SendMessage(e.Message); - } + private Task LanGameLobby_GameBroadcastAsync(GameBroadcastEventArgs e, CancellationToken cancellationToken) + => SendMessageAsync(e.Message, cancellationToken); private void LanGameLobby_GameLeft(object sender, EventArgs e) - { - Enable(); - } + => Enable(); - private void LanGameLoadingLobby_GameBroadcast(object sender, GameBroadcastEventArgs e) - { - SendMessage(e.Message); - } + private Task LanGameLoadingLobby_GameBroadcastAsync(GameBroadcastEventArgs e, CancellationToken cancellationToken) + => SendMessageAsync(e.Message, cancellationToken); - private void GameCreationWindow_LoadGame(object sender, GameLoadEventArgs e) + private async Task GameCreationWindow_LoadGameAsync(GameLoadEventArgs e) { - lanGameLoadingLobby.SetUp(true, - new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), - null, e.LoadedGameID); + try + { + await lanGameLoadingLobby.SetUpAsync(true, null, e.LoadedGameID); - lanGameLoadingLobby.Enable(); + lanGameLoadingLobby.Enable(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void GameCreationWindow_NewGame(object sender, EventArgs e) + private async Task GameCreationWindow_NewGameAsync() { - lanGameLobby.SetUp(true, - new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), null); + try + { + await lanGameLobby.SetUpAsync(true, + new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), null); - lanGameLobby.Enable(); + lanGameLobby.Enable(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void SetChatColor() @@ -332,7 +342,7 @@ private void DdColor_SelectedIndexChanged(object sender, EventArgs e) UserINISettings.Instance.SaveSettings(); } - public void Open() + public async Task OpenAsync() { players.Clear(); lbPlayerList.Clear(); @@ -341,11 +351,14 @@ public void Open() Visible = true; Enabled = true; + cancellationTokenSource?.Dispose(); + cancellationTokenSource = new CancellationTokenSource(); + Logger.Log("Creating LAN socket."); try { - socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket = new Socket(SocketType.Dgram, ProtocolType.Udp); socket.EnableBroadcast = true; socket.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_LOBBY_PORT)); endPoint = new IPEndPoint(IPAddress.Broadcast, ProgramConstants.LAN_LOBBY_PORT); @@ -353,59 +366,93 @@ public void Open() } catch (SocketException ex) { - Logger.Log("Creating LAN socket failed! Message: " + ex.Message); + PreStartup.LogException(ex, "Creating LAN socket failed!"); lbChatMessages.AddMessage(new ChatMessage(Color.Red, "Creating LAN socket failed! Message:".L10N("Client:Main:SocketFailure1") + " " + ex.Message)); lbChatMessages.AddMessage(new ChatMessage(Color.Red, "Please check your firewall settings.".L10N("Client:Main:SocketFailure2"))); lbChatMessages.AddMessage(new ChatMessage(Color.Red, - "Also make sure that no other application is listening to traffic on UDP ports 1232 - 1234.".L10N("Client:Main:SocketFailure3"))); + $"Also make sure that no other application is listening to traffic on UDP ports" + + $" {ProgramConstants.LAN_LOBBY_PORT} - {ProgramConstants.LAN_INGAME_PORT}.".L10N("Client:Main:SocketFailure3"))); initSuccess = false; return; } Logger.Log("Starting listener."); - new Thread(new ThreadStart(Listen)).Start(); + ListenAsync(cancellationTokenSource.Token); - SendAlive(); + await SendAliveAsync(cancellationTokenSource.Token); } - private void SendMessage(string message) + private async Task SendMessageAsync(string message, CancellationToken cancellationToken) { - if (!initSuccess) - return; + try + { + if (!initSuccess) + return; - byte[] buffer; +#if NETFRAMEWORK + byte[] buffer1 = encoding.GetBytes(message); + var buffer = new ArraySegment(buffer1); - buffer = encoding.GetBytes(message); + await socket.SendToAsync(buffer, SocketFlags.None, endPoint); + } +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); + Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; - socket.SendTo(buffer, endPoint); + await socket.SendToAsync(buffer, SocketFlags.None, endPoint, cancellationToken); + } + catch (OperationCanceledException) + { + } +#endif + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void Listen() + private async Task ListenAsync(CancellationToken cancellationToken) { try { - while (true) +#if !NETFRAMEWORK + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); +#endif + + while (!cancellationToken.IsCancellationRequested) { EndPoint ep = new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_LOBBY_PORT); - byte[] buffer = new byte[4096]; - int receivedBytes = 0; - receivedBytes = socket.ReceiveFrom(buffer, ref ep); - - IPEndPoint iep = (IPEndPoint)ep; - - string data = encoding.GetString(buffer, 0, receivedBytes); +#if NETFRAMEWORK + byte[] buffer1 = new byte[4096]; + var buffer = new ArraySegment(buffer1); + SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep); +#else + Memory buffer = memoryOwner.Memory[..4096]; + SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep, cancellationToken); +#endif + var iep = (IPEndPoint)ep; +#if NETFRAMEWORK + string data = encoding.GetString(buffer1, 0, socketReceiveFromResult.ReceivedBytes); +#else + string data = encoding.GetString(buffer.Span[..socketReceiveFromResult.ReceivedBytes]); +#endif if (data == string.Empty) continue; - AddCallback(new Action(HandleNetworkMessage), data, iep); + AddCallback(() => HandleNetworkMessage(data, iep)); } } + catch (OperationCanceledException) + { + } catch (Exception ex) { - Logger.Log("LAN socket listener: exception: " + ex.Message); + PreStartup.LogException(ex, "LAN socket listener exception."); } } @@ -419,7 +466,7 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) string command = commandAndParams[0]; string[] parameters = data.Substring(command.Length + 1).Split( - new char[] { ProgramConstants.LAN_DATA_SEPARATOR }, + new[] { ProgramConstants.LAN_DATA_SEPARATOR }, StringSplitOptions.RemoveEmptyEntries); LANLobbyUser user = players.Find(p => p.EndPoint.Equals(endPoint)); @@ -497,142 +544,187 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) } } - private void SendAlive() + private async Task SendAliveAsync(CancellationToken cancellationToken) { StringBuilder sb = new StringBuilder("ALIVE "); sb.Append(localGameIndex); sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); sb.Append(ProgramConstants.PLAYERNAME); - SendMessage(sb.ToString()); + await SendMessageAsync(sb.ToString(), cancellationToken); timeSinceAliveMessage = TimeSpan.Zero; } - private void TbChatInput_EnterPressed(object sender, EventArgs e) + private async Task TbChatInput_EnterPressedAsync(CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(tbChatInput.Text)) - return; + try + { + if (string.IsNullOrEmpty(tbChatInput.Text)) + return; - string chatMessage = tbChatInput.Text.Replace((char)01, '?'); + string chatMessage = tbChatInput.Text.Replace((char)01, '?'); - StringBuilder sb = new StringBuilder("CHAT "); - sb.Append(ddColor.SelectedIndex); - sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); - sb.Append(chatMessage); + StringBuilder sb = new StringBuilder("CHAT "); + sb.Append(ddColor.SelectedIndex); + sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); + sb.Append(chatMessage); - SendMessage(sb.ToString()); + await SendMessageAsync(sb.ToString(), cancellationToken); - tbChatInput.Text = string.Empty; + tbChatInput.Text = string.Empty; + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void LbGameList_DoubleLeftClick(object sender, EventArgs e) + private async Task LbGameList_DoubleLeftClickAsync() { - if (lbGameList.SelectedIndex < 0 || lbGameList.SelectedIndex >= lbGameList.Items.Count) - return; - - HostedLANGame hg = (HostedLANGame)lbGameList.Items[lbGameList.SelectedIndex].Tag; - - if (hg.Game.InternalName.ToUpper() != localGame.ToUpper()) + try { - lbChatMessages.AddMessage( - string.Format("The selected game is for {0}!".L10N("Client:Main:GameIsOfPurpose"), gameCollection.GetGameNameFromInternalName(hg.Game.InternalName))); - return; - } + if (lbGameList.SelectedIndex < 0 || lbGameList.SelectedIndex >= lbGameList.Items.Count) + return; - if (hg.Locked) - { - lbChatMessages.AddMessage("The selected game is locked!".L10N("Client:Main:GameLocked")); - return; - } + HostedLANGame hg = (HostedLANGame)lbGameList.Items[lbGameList.SelectedIndex].Tag; - if (hg.IsLoadedGame) - { - if (!hg.Players.Contains(ProgramConstants.PLAYERNAME)) + if (hg.Game.InternalName.ToUpper() != localGame.ToUpper()) { - lbChatMessages.AddMessage("You do not exist in the saved game!".L10N("Client:Main:NotInSavedGame")); + lbChatMessages.AddMessage( + string.Format("The selected game is for {0}!".L10N("Client:Main:GameIsOfPurpose"), gameCollection.GetGameNameFromInternalName(hg.Game.InternalName))); return; } - } - else - { - if (hg.Players.Contains(ProgramConstants.PLAYERNAME)) + + if (hg.Locked) { - lbChatMessages.AddMessage("Your name is already taken in the game.".L10N("Client:Main:NameOccupied")); + lbChatMessages.AddMessage("The selected game is locked!".L10N("Client:Main:GameLocked")); return; } - } - - if (hg.GameVersion != ProgramConstants.GAME_VERSION) - { - // TODO Show warning - } - - lbChatMessages.AddMessage(string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName)); - - try - { - var client = new TcpClient(hg.EndPoint.Address.ToString(), ProgramConstants.LAN_GAME_LOBBY_PORT); - - byte[] buffer; if (hg.IsLoadedGame) { - var spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); - - int loadedGameId = spawnSGIni.GetIntValue("Settings", "GameID", -1); - - lanGameLoadingLobby.SetUp(false, hg.EndPoint, client, loadedGameId); - lanGameLoadingLobby.Enable(); - - buffer = encoding.GetBytes("JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + - ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + - loadedGameId + ProgramConstants.LAN_MESSAGE_SEPARATOR); - - client.GetStream().Write(buffer, 0, buffer.Length); - client.GetStream().Flush(); - - lanGameLoadingLobby.PostJoin(); + if (!hg.Players.Contains(ProgramConstants.PLAYERNAME)) + { + lbChatMessages.AddMessage("You do not exist in the saved game!".L10N("Client:Main:NotInSavedGame")); + return; + } } else { - lanGameLobby.SetUp(false, hg.EndPoint, client); - lanGameLobby.Enable(); + if (hg.Players.Contains(ProgramConstants.PLAYERNAME)) + { + lbChatMessages.AddMessage("Your name is already taken in the game.".L10N("Client:Main:NameOccupied")); + return; + } + } + + if (hg.GameVersion != ProgramConstants.GAME_VERSION) + { + // TODO Show warning + } - buffer = encoding.GetBytes("JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + - ProgramConstants.PLAYERNAME + ProgramConstants.LAN_MESSAGE_SEPARATOR); + lbChatMessages.AddMessage(string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName)); - client.GetStream().Write(buffer, 0, buffer.Length); - client.GetStream().Flush(); + try + { + using var client = new Socket(SocketType.Stream, ProtocolType.Tcp); + client.Bind(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT)); - lanGameLobby.PostJoin(); + if (hg.IsLoadedGame) + { + var spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); + int loadedGameId = spawnSGIni.GetIntValue("Settings", "GameID", -1); + + await lanGameLoadingLobby.SetUpAsync(false, client, loadedGameId); + lanGameLoadingLobby.Enable(); + + string message = "JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + + loadedGameId + ProgramConstants.LAN_MESSAGE_SEPARATOR; +#if NETFRAMEWORK + byte[] buffer1 = encoding.GetBytes(message); + var buffer = new ArraySegment(buffer1); + + await client.SendAsync(buffer, SocketFlags.None); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); + Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; + + await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); +#endif + + await lanGameLoadingLobby.PostJoinAsync(); + } + else + { + await lanGameLobby.SetUpAsync(false, hg.EndPoint, client); + lanGameLobby.Enable(); + + string message = "JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_MESSAGE_SEPARATOR; +#if NETFRAMEWORK + byte[] buffer1 = encoding.GetBytes(message); + var buffer = new ArraySegment(buffer1); + + await client.SendAsync(buffer, SocketFlags.None); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); + Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; + + await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); +#endif + + await lanGameLobby.PostJoinAsync(); + } + } + catch (Exception ex) + { + PreStartup.LogException(ex, "Connecting to the game failed!"); + lbChatMessages.AddMessage(null, + "Connecting to the game failed! Message:".L10N("Client:Main:ConnectGameFailed") + " " + ex.Message, Color.White); } } catch (Exception ex) { - lbChatMessages.AddMessage(null, - "Connecting to the game failed! Message:".L10N("Client:Main:ConnectGameFailed") + " " + ex.Message, Color.White); + PreStartup.HandleException(ex); } } - private void BtnMainMenu_LeftClick(object sender, EventArgs e) + private async Task BtnMainMenu_LeftClickAsync(CancellationToken cancellationToken) { - Visible = false; - Enabled = false; - SendMessage("QUIT"); - socket.Close(); - Exited?.Invoke(this, EventArgs.Empty); + try + { + Visible = false; + Enabled = false; + await SendMessageAsync("QUIT", cancellationToken); + socket.Close(); + Exited?.Invoke(this, EventArgs.Empty); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void BtnJoinGame_LeftClick(object sender, EventArgs e) - { - LbGameList_DoubleLeftClick(this, EventArgs.Empty); - } + private Task BtnJoinGame_LeftClickAsync() + => LbGameList_DoubleLeftClickAsync(); - private void BtnNewGame_LeftClick(object sender, EventArgs e) + private async Task BtnNewGame_LeftClickAsync() { - if (!ClientConfiguration.Instance.DisableMultiplayerGameLoading) - gameCreationWindow.Open(); - else - GameCreationWindow_NewGame(sender, e); + try + { + if (!ClientConfiguration.Instance.DisableMultiplayerGameLoading) + gameCreationWindow.Open(); + else + await GameCreationWindow_NewGameAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } public override void Update(GameTime gameTime) @@ -651,9 +743,9 @@ public override void Update(GameTime gameTime) timeSinceAliveMessage += gameTime.ElapsedGameTime; if (timeSinceAliveMessage > TimeSpan.FromSeconds(ALIVE_MESSAGE_INTERVAL)) - SendAlive(); + Task.Run(() => SendAliveAsync(cancellationTokenSource?.Token ?? default)).Wait(); base.Update(gameTime); } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index 0bf357b90..0c393c57e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -1,8 +1,9 @@ using ClientCore; using System; -using System.IO; using System.Net; +using System.Net.Http; using System.Threading; +using System.Threading.Tasks; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -11,8 +12,6 @@ namespace DTAClient.Domain.Multiplayer.CnCNet /// public static class CnCNetPlayerCountTask { - public static int PlayerCount { get; private set; } - private static int REFRESH_INTERVAL = 60000; // 1 minute internal static event EventHandler CnCNetGameCountUpdated; @@ -22,44 +21,45 @@ public static class CnCNetPlayerCountTask public static void InitializeService(CancellationTokenSource cts) { cncnetLiveStatusIdentifier = ClientConfiguration.Instance.CnCNetLiveStatusIdentifier; - PlayerCount = GetCnCNetPlayerCount(); - CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(PlayerCount)); - ThreadPool.QueueUserWorkItem(new WaitCallback(RunService), cts); + RunServiceAsync(cts.Token); } - private static void RunService(object tokenObj) + private static async Task RunServiceAsync(CancellationToken cancellationToken) { - var waitHandle = ((CancellationTokenSource)tokenObj).Token.WaitHandle; - - while (true) + while (!cancellationToken.IsCancellationRequested) { - if (waitHandle.WaitOne(REFRESH_INTERVAL)) + CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(await GetCnCNetPlayerCountAsync())); + + try { - // Cancellation signaled - return; + await Task.Delay(REFRESH_INTERVAL, cancellationToken); } - else + catch (OperationCanceledException) { - CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(GetCnCNetPlayerCount())); + break; } } } - private static int GetCnCNetPlayerCount() + private static async Task GetCnCNetPlayerCountAsync() { try { - WebClient client = new WebClient(); - - Stream data = client.OpenRead("http://api.cncnet.org/status"); - - string info = string.Empty; - - using (StreamReader reader = new StreamReader(data)) + var httpClientHandler = new HttpClientHandler { - info = reader.ReadToEnd(); - } +#if NETFRAMEWORK + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate +#else + AutomaticDecompression = DecompressionMethods.All +#endif + }; + using var client = new HttpClient(httpClientHandler, true) + { + Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT) + }; + + string info = await client.GetStringAsync("http://api.cncnet.org/status"); info = info.Replace("{", String.Empty); info = info.Replace("}", String.Empty); @@ -79,8 +79,9 @@ private static int GetCnCNetPlayerCount() return numGames; } - catch + catch (Exception ex) { + PreStartup.LogException(ex); return -1; } } @@ -95,4 +96,4 @@ public PlayerCountEventArgs(int playerCount) public int PlayerCount { get; set; } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 3d6da5979..acf183f0e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Net; +using System.Net.Http; using System.Net.Sockets; #if !NETFRAMEWORK using System.Threading; @@ -65,15 +66,10 @@ public static CnCNetTunnel Parse(string str) return tunnel; } - catch (Exception ex) + catch (Exception ex) when (ex is FormatException or OverflowException or IndexOutOfRangeException) { - if (ex is FormatException || ex is OverflowException || ex is IndexOutOfRangeException) - { - Logger.Log("Parsing tunnel information failed: " + ex.Message + Environment.NewLine + "Parsed string: " + str); - return null; - } - - throw; + PreStartup.LogException(ex, "Parsing tunnel information failed: " + ex.Message + Environment.NewLine + "Parsed string: " + str); + return null; } } @@ -110,7 +106,7 @@ private set /// Gets a list of player ports to use from a specific tunnel server. /// /// A list of player ports to use. - public List GetPlayerPortInfo(int playerCount) + public async Task> GetPlayerPortInfoAsync(int playerCount) { if (Version != Constants.TUNNEL_VERSION_2) throw new InvalidOperationException("GetPlayerPortInfo only works with version 2 tunnels."); @@ -122,28 +118,38 @@ public List GetPlayerPortInfo(int playerCount) string addressString = $"http://{Address}:{Port}/request?clients={playerCount}"; Logger.Log($"Downloading from {addressString}"); - using (var client = new ExtendedWebClient(Constants.TUNNEL_CONNECTION_TIMEOUT)) + var httpClientHandler = new HttpClientHandler + { +#if NETFRAMEWORK + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate +#else + AutomaticDecompression = DecompressionMethods.All +#endif + }; + using var client = new HttpClient(httpClientHandler, true) { - string data = client.DownloadString(addressString); + Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT) + }; - data = data.Replace("[", string.Empty); - data = data.Replace("]", string.Empty); + string data = await client.GetStringAsync(addressString); - string[] portIDs = data.Split(','); - List playerPorts = new List(); + data = data.Replace("[", string.Empty); + data = data.Replace("]", string.Empty); - foreach (string _port in portIDs) - { - playerPorts.Add(Convert.ToInt32(_port)); - Logger.Log($"Added port {_port}"); - } + string[] portIDs = data.Split(','); + List playerPorts = new List(); - return playerPorts; + foreach (string _port in portIDs) + { + playerPorts.Add(Convert.ToInt32(_port)); + Logger.Log($"Added port {_port}"); } + + return playerPorts; } catch (Exception ex) { - Logger.Log("Unable to connect to the specified tunnel server. Returned error message: " + ex.Message); + PreStartup.LogException(ex, "Unable to connect to the specified tunnel server. Returned error message: " + ex.Message); } return new List(); @@ -183,7 +189,7 @@ public async Task UpdatePingAsync() } catch (SocketException ex) { - Logger.Log($"Failed to ping tunnel {Name} ({Address}:{Port}). Message: {ex.Message}"); + PreStartup.LogException(ex, $"Failed to ping tunnel {Name} ({Address}:{Port}). Message: {ex.Message}"); PingInMs = -1; } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/ExtendedWebClient.cs b/DXMainClient/Domain/Multiplayer/CnCNet/ExtendedWebClient.cs deleted file mode 100644 index b7afe0f52..000000000 --- a/DXMainClient/Domain/Multiplayer/CnCNet/ExtendedWebClient.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Net; - -namespace DTAClient.Domain.Multiplayer.CnCNet -{ - /// - /// A web client that supports customizing the timeout of the request. - /// - class ExtendedWebClient : WebClient - { - public ExtendedWebClient(int timeout) - { - this.timeout = timeout; - } - - private int timeout; - - protected override WebRequest GetWebRequest(Uri address) - { - WebRequest webRequest = base.GetWebRequest(address); - webRequest.Timeout = timeout; - return webRequest; - } - } -} diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs index 870dee82d..e3249123c 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs @@ -1,15 +1,15 @@ using System; using System.Collections.Generic; -using System.Text; -using System.IO; -using System.Net; using System.Collections.Specialized; -using System.Globalization; -using System.Threading; -using Rampastring.Tools; -using ClientCore; +using System.IO; using System.IO.Compression; using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using ClientCore; +using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -30,11 +30,11 @@ public static class MapSharer public static event EventHandler MapDownloadStarted; - private volatile static List MapDownloadQueue = new List(); - private volatile static List MapUploadQueue = new List(); - private volatile static List UploadedMaps = new List(); + private volatile static List MapDownloadQueue = new(); + private volatile static List MapUploadQueue = new(); + private volatile static List UploadedMaps = new(); - private static readonly object locker = new object(); + private static readonly object locker = new(); private const string MAPDB_URL = "http://mapdb.cncnet.org/upload"; @@ -56,30 +56,17 @@ public static void UploadMap(Map map, string myGame) MapUploadQueue.Add(map); if (MapUploadQueue.Count == 1) - { - ParameterizedThreadStart pts = new ParameterizedThreadStart(Upload); - Thread thread = new Thread(pts); - object[] mapAndGame = new object[2]; - mapAndGame[0] = map; - mapAndGame[1] = myGame.ToLower(); - thread.Start(mapAndGame); - } + UploadAsync(map, myGame.ToLower()); } } - private static void Upload(object mapAndGame) + private static async Task UploadAsync(Map map, string myGameId) { - object[] mapGameArray = (object[])mapAndGame; - - Map map = (Map)mapGameArray[0]; - string myGameId = (string)mapGameArray[1]; - MapUploadStarted?.Invoke(null, new MapEventArgs(map)); Logger.Log("MapSharer: Starting upload of " + map.BaseFilePath); - bool success = false; - string message = MapUpload(MAPDB_URL, map, myGameId, out success); + (string message, bool success) = await MapUploadAsync(MAPDB_URL, map, myGameId); if (success) { @@ -90,7 +77,7 @@ private static void Upload(object mapAndGame) UploadedMaps.Add(map.SHA1); } - Logger.Log("MapSharer: Uploading map " + map.BaseFilePath + " completed succesfully."); + Logger.Log("MapSharer: Uploading map " + map.BaseFilePath + " completed successfully."); } else { @@ -107,179 +94,101 @@ private static void Upload(object mapAndGame) { Map nextMap = MapUploadQueue[0]; - object[] array = new object[2]; - array[0] = nextMap; - array[1] = myGameId; - Logger.Log("MapSharer: There are additional maps in the queue."); - Upload(array); + UploadAsync(nextMap, myGameId); } } } - private static string MapUpload(string _URL, Map map, string gameName, out bool success) + private static async Task<(string Message, bool Success)> MapUploadAsync(string address, Map map, string gameName) { - ServicePointManager.Expect100Continue = false; - - FileInfo zipFile = SafePath.GetFile(ProgramConstants.GamePath, "Maps", "Custom", FormattableString.Invariant($"{map.SHA1}.zip")); - - if (zipFile.Exists) zipFile.Delete(); - - string mapFileName = map.SHA1 + MapLoader.MAP_FILE_EXTENSION; - - File.Copy(SafePath.CombineFilePath(map.CompleteFilePath), SafePath.CombineFilePath(ProgramConstants.GamePath, mapFileName)); - - CreateZipFile(mapFileName, zipFile.FullName); + using MemoryStream zipStream = CreateZipFile(map.CompleteFilePath); try { - SafePath.DeleteFileIfExists(ProgramConstants.GamePath, mapFileName); - } - catch { } - - // Upload the file to the URI. - // The 'UploadFile(uriString,fileName)' method implicitly uses HTTP POST method. - - try - { - using (FileStream stream = zipFile.Open(FileMode.Open)) - { - List files = new List(); - //{ - // new FileToUpload - // { - // Name = "file", - // Filename = Path.GetFileName(zipFile), - // ContentType = "mapZip", - // Stream = stream - // }; - //}; - - FileToUpload file = new FileToUpload() + var files = new List { - Name = "file", - Filename = zipFile.Name, - ContentType = "mapZip", - Stream = stream + new("file", FormattableString.Invariant($"{map.SHA1}.zip"), "mapZip", zipStream) }; - - files.Add(file); - - NameValueCollection values = new NameValueCollection - { - { "game", gameName.ToLower() }, - }; - - byte[] responseArray = UploadFiles(_URL, files, values); - string response = Encoding.UTF8.GetString(responseArray); - - if (!response.Contains("Upload succeeded!")) + var values = new NameValueCollection { - success = false; - return response; - } - Logger.Log("MapSharer: Upload response: " + response); + { "game", gameName.ToLower() }, + }; + string response = await UploadFilesAsync(address, files, values); - //MessageBox.Show((response)); + if (!response.Contains("Upload succeeded!")) + return (response, false); - success = true; - return String.Empty; - } + Logger.Log("MapSharer: Upload response: " + response); + + return (string.Empty, true); } catch (Exception ex) { - success = false; - return ex.Message; + PreStartup.LogException(ex); + return (ex.Message, false); } } - private static void CopyStream(Stream input, Stream output) + private static async Task UploadFilesAsync(string address, List files, NameValueCollection values) { - byte[] buffer = new byte[32768]; - int read; - while ((read = input.Read(buffer, 0, buffer.Length)) > 0) - { - output.Write(buffer, 0, read); - } - } + using HttpClient client = GetHttpClient(); - private static byte[] UploadFiles(string address, List files, NameValueCollection values) - { - WebRequest request = WebRequest.Create(address); - request.Method = "POST"; - string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x", NumberFormatInfo.InvariantInfo); - request.ContentType = "multipart/form-data; boundary=" + boundary; - boundary = "--" + boundary; + var multipartFormDataContent = new MultipartFormDataContent(); - using (Stream requestStream = request.GetRequestStream()) + // Write the values + foreach (string name in values.Keys) { - // Write the values - foreach (string name in values.Keys) - { - byte[] buffer = Encoding.ASCII.GetBytes(boundary + Environment.NewLine); - requestStream.Write(buffer, 0, buffer.Length); - - buffer = Encoding.ASCII.GetBytes(string.Format("Content-Disposition: form-data; name=\"{0}\"{1}{1}", name, Environment.NewLine)); - requestStream.Write(buffer, 0, buffer.Length); - - buffer = Encoding.UTF8.GetBytes(values[name] + Environment.NewLine); - requestStream.Write(buffer, 0, buffer.Length); - } - - // Write the files - foreach (FileToUpload file in files) - { - var buffer = Encoding.ASCII.GetBytes(boundary + Environment.NewLine); - requestStream.Write(buffer, 0, buffer.Length); - - buffer = Encoding.UTF8.GetBytes(string.Format("Content-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"{2}", file.Name, file.Filename, Environment.NewLine)); - requestStream.Write(buffer, 0, buffer.Length); - - buffer = Encoding.ASCII.GetBytes(string.Format("Content-Type: {0}{1}{1}", file.ContentType, Environment.NewLine)); - requestStream.Write(buffer, 0, buffer.Length); - - CopyStream(file.Stream, requestStream); - - buffer = Encoding.ASCII.GetBytes(Environment.NewLine); - requestStream.Write(buffer, 0, buffer.Length); - } - - byte[] boundaryBuffer = Encoding.ASCII.GetBytes(boundary + "--"); - requestStream.Write(boundaryBuffer, 0, boundaryBuffer.Length); + multipartFormDataContent.Add(new StringContent(values[name]), name); } - using (WebResponse response = request.GetResponse()) + // Write the files + foreach (FileToUpload file in files) { - using (Stream responseStream = response.GetResponseStream()) + var streamContent = new StreamContent(file.Stream) { - using (MemoryStream stream = new MemoryStream()) - { + Headers = { ContentType = new MediaTypeHeaderValue("application/" + file.ContentType) } + }; + multipartFormDataContent.Add(streamContent, file.Name, file.Filename); + } - CopyStream(responseStream, stream); + HttpResponseMessage httpResponseMessage = await client.PostAsync(address, multipartFormDataContent); - return stream.ToArray(); - } - } - } + return await httpResponseMessage.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); } - private static void CreateZipFile(string file, string zipName) + private static HttpClient GetHttpClient() { - using var zipFileStream = new FileStream(zipName, FileMode.CreateNew, FileAccess.Write); - using var archive = new ZipArchive(zipFileStream, ZipArchiveMode.Create); - archive.CreateEntryFromFile(SafePath.CombineFilePath(ProgramConstants.GamePath, file), file); + var httpClientHandler = new HttpClientHandler + { +#if NETFRAMEWORK + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate +#else + AutomaticDecompression = DecompressionMethods.All +#endif + }; + + return new HttpClient(httpClientHandler, true) + { + Timeout = TimeSpan.FromMilliseconds(10000) + }; } - private static string ExtractZipFile(string zipFile, string destDir) + private static MemoryStream CreateZipFile(string file) { - using ZipArchive zipArchive = ZipFile.OpenRead(zipFile); + var zipStream = new MemoryStream(1024); + using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, true); + archive.CreateEntryFromFile(SafePath.CombineFilePath(ProgramConstants.GamePath, file), file); - // here, we extract every entry, but we could extract conditionally - // based on entry name, size, date, checkbox status, etc. - zipArchive.ExtractToDirectory(destDir); + return zipStream; + } - return zipArchive.Entries.FirstOrDefault()?.Name; + private static void ExtractZipFile(Stream stream, string file) + { + using var zipArchive = new ZipArchive(stream, ZipArchiveMode.Read); + + zipArchive.Entries.FirstOrDefault().ExtractToFile(file, true); } public static void DownloadMap(string sha1, string myGame, string mapName) @@ -295,30 +204,14 @@ public static void DownloadMap(string sha1, string myGame, string mapName) MapDownloadQueue.Add(sha1); if (MapDownloadQueue.Count == 1) - { - object[] details = new object[3]; - details[0] = sha1; - details[1] = myGame.ToLower(); - details[2] = mapName; - - ParameterizedThreadStart pts = new ParameterizedThreadStart(Download); - Thread thread = new Thread(pts); - thread.Start(details); - } + DownloadAsync(sha1, myGame.ToLower(), mapName); } } - private static void Download(object details) + private static async Task DownloadAsync(string sha1, string myGameId, string mapName) { - object[] sha1AndGame = (object[])details; - string sha1 = (string)sha1AndGame[0]; - string myGameId = (string)sha1AndGame[1]; - string mapName = (string)sha1AndGame[2]; - Logger.Log("MapSharer: Preparing to download map " + sha1 + " with name: " + mapName); - bool success; - try { Logger.Log("MapSharer: MapDownloadStarted"); @@ -326,10 +219,10 @@ private static void Download(object details) } catch (Exception ex) { - Logger.Log("MapSharer: ERROR " + ex.Message); + PreStartup.LogException(ex, "MapSharer ERROR"); } - string mapPath = DownloadMain(sha1, myGameId, mapName, out success); + (string error, bool success) = await DownloadMainAsync(sha1, myGameId, mapName); lock (locker) { @@ -340,22 +233,16 @@ private static void Download(object details) } else { - Logger.Log("MapSharer: Download of map " + sha1 + "failed! Reason: " + mapPath); + Logger.Log("MapSharer: Download of map " + sha1 + "failed! Reason: " + error); MapDownloadFailed?.Invoke(null, new SHA1EventArgs(sha1, mapName)); } MapDownloadQueue.Remove(sha1); - if (MapDownloadQueue.Count > 0) + if (MapDownloadQueue.Any()) { Logger.Log("MapSharer: Continuing custom map downloads."); - - object[] array = new object[3]; - array[0] = MapDownloadQueue[0]; - array[1] = myGameId; - array[2] = mapName; - - Download(array); + DownloadAsync(MapDownloadQueue[0], myGameId, mapName); } } } @@ -363,78 +250,41 @@ private static void Download(object details) public static string GetMapFileName(string sha1, string mapName) => mapName + "_" + sha1; - private static string DownloadMain(string sha1, string myGame, string mapName, out bool success) + private static async Task<(string Error, bool Success)> DownloadMainAsync(string sha1, string myGame, string mapName) { string customMapsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Maps", "Custom"); - string mapFileName = GetMapFileName(sha1, mapName); + string newFile = SafePath.CombineFilePath(customMapsDirectory, FormattableString.Invariant($"{mapFileName}.map")); + using HttpClient client = GetHttpClient(); + Stream stream; - FileInfo destinationFile = SafePath.GetFile(customMapsDirectory, FormattableString.Invariant($"{mapFileName}.zip")); - - // This string is up here so we can check that there isn't already a .map file for this download. - // This prevents the client from crashing when trying to rename the unzipped file to a duplicate filename. - FileInfo newFile = SafePath.GetFile(customMapsDirectory, FormattableString.Invariant($"{mapFileName}{MapLoader.MAP_FILE_EXTENSION}")); - - destinationFile.Delete(); - newFile.Delete(); - - using (TWebClient webClient = new TWebClient()) + try { - webClient.Proxy = null; - - try - { - Logger.Log("MapSharer: Downloading URL: " + "http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip"); - webClient.DownloadFile("http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip", destinationFile.FullName); - } - catch (Exception ex) - { - /* if (ex.Message.Contains("404")) - { - string messageToSend = "NOTICE " + ChannelName + " " + CTCPChar1 + CTCPChar2 + "READY 1" + CTCPChar2; - CnCNetData.ConnectionBridge.SendMessage(messageToSend); - } - else - { - //GlobalVars.WriteLogfile(ex.StackTrace.ToString(), DateTime.Now.ToString("hh:mm:ss") + " DownloadMap: " + ex.Message + _DestFile); - MessageBox.Show("Download failed:" + _DestFile); - }*/ - success = false; - return ex.Message; - } + string address = "http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip"; + Logger.Log("MapSharer: Downloading URL: " + address); + stream = await client.GetStreamAsync(address); } - - destinationFile.Refresh(); - - if (!destinationFile.Exists) + catch (Exception ex) { - success = false; - return null; - } - - string extractedFile = ExtractZipFile(destinationFile.FullName, customMapsDirectory); + PreStartup.LogException(ex); - if (String.IsNullOrEmpty(extractedFile)) - { - success = false; - return null; + return (ex.Message, false); } - // We can safely assume that there will not be a duplicate file due to deleting it - // earlier if one already existed. - File.Move(SafePath.CombineFilePath(customMapsDirectory, extractedFile), newFile.FullName); - - destinationFile.Delete(); + ExtractZipFile(stream, newFile); - success = true; - return extractedFile; + return (null, true); } - class FileToUpload +#if NETFRAMEWORK + private sealed class FileToUpload { - public FileToUpload() + public FileToUpload(string name, string filename, string contentType, Stream stream) { - ContentType = "application/octet-stream"; + Name = name; + Filename = filename; + ContentType = contentType; + Stream = stream; } public string Name { get; set; } @@ -442,22 +292,8 @@ public FileToUpload() public string ContentType { get; set; } public Stream Stream { get; set; } } - - class TWebClient : WebClient - { - private int Timeout = 10000; - - public TWebClient() - { - this.Proxy = null; - } - - protected override WebRequest GetWebRequest(Uri address) - { - var webRequest = base.GetWebRequest(address); - webRequest.Timeout = Timeout; - return webRequest; - } - } +#else + private readonly record struct FileToUpload(string Name, string Filename, string ContentType, Stream Stream); +#endif } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 1fcea5899..65381cd4b 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -7,9 +7,9 @@ using System.Collections.Generic; using System.IO; using System.Net; -using System.Text; using System.Threading.Tasks; using System.Linq; +using System.Net.Http; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -81,7 +81,7 @@ private async Task RefreshTunnelsAsync() } catch (Exception ex) { - PreStartup.LogException(ex); + PreStartup.HandleException(ex); } } @@ -125,7 +125,7 @@ private async Task PingListTunnelAsync(int index) } catch (Exception ex) { - PreStartup.LogException(ex); + PreStartup.HandleException(ex); } } @@ -145,7 +145,7 @@ private async Task PingCurrentTunnelAsync(bool checkTunnelList = false) } catch (Exception ex) { - PreStartup.LogException(ex); + PreStartup.HandleException(ex); } } @@ -158,27 +158,37 @@ private async Task> DoRefreshTunnelsAsync() FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); List returnValue = new List(); + var httpClientHandler = new HttpClientHandler + { +#if NETFRAMEWORK + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate +#else + AutomaticDecompression = DecompressionMethods.All +#endif + }; + using var client = new HttpClient(httpClientHandler, true) + { + Timeout = TimeSpan.FromSeconds(100) + }; - WebClient client = new WebClient(); - - byte[] data; + string data; Logger.Log("Fetching tunnel server info."); try { - data = await client.DownloadDataTaskAsync(MainClientConstants.CNCNET_TUNNEL_LIST_URL); + data = await client.GetStringAsync(MainClientConstants.CNCNET_TUNNEL_LIST_URL); } - catch (WebException ex) + catch (HttpRequestException ex) { - Logger.Log("Error when downloading tunnel server info: " + ex.Message); - Logger.Log("Retrying."); + PreStartup.LogException(ex, "Error when downloading tunnel server info. Retrying."); try { - data = await client.DownloadDataTaskAsync(MainClientConstants.CNCNET_TUNNEL_LIST_URL); + data = await client.GetStringAsync(MainClientConstants.CNCNET_TUNNEL_LIST_URL); } - catch (WebException) + catch (HttpRequestException ex1) { + PreStartup.LogException(ex1); if (!tunnelCacheFile.Exists) { Logger.Log("Tunnel cache file doesn't exist!"); @@ -187,16 +197,14 @@ private async Task> DoRefreshTunnelsAsync() Logger.Log("Fetching tunnel server list failed. Using cached tunnel data."); #if NETFRAMEWORK - data = File.ReadAllBytes(tunnelCacheFile.FullName); + data = File.ReadAllText(tunnelCacheFile.FullName); #else - data = await File.ReadAllBytesAsync(tunnelCacheFile.FullName); + data = await File.ReadAllTextAsync(tunnelCacheFile.FullName); #endif } } - string convertedData = Encoding.Default.GetString(data); - - string[] serverList = convertedData.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); + string[] serverList = data.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); // skip first header item ("address;country;countrycode;name;password;clients;maxclients;official;latitude;longitude;version;distance") foreach (string serverInfo in serverList.Skip(1)) @@ -219,7 +227,7 @@ private async Task> DoRefreshTunnelsAsync() } catch (Exception ex) { - Logger.Log("Caught an exception when parsing a tunnel server: " + ex.Message); + PreStartup.LogException(ex, "Caught an exception when parsing a tunnel server."); } } @@ -236,14 +244,14 @@ private async Task> DoRefreshTunnelsAsync() clientDirectoryInfo.Create(); #if NETFRAMEWORK - File.WriteAllBytes(tunnelCacheFile.FullName, data); + File.WriteAllText(tunnelCacheFile.FullName, data); #else - await File.WriteAllBytesAsync(tunnelCacheFile.FullName, data); + await File.WriteAllTextAsync(tunnelCacheFile.FullName, data); #endif } catch (Exception ex) { - Logger.Log("Refreshing tunnel cache file failed! Returned error: " + ex.Message); + PreStartup.LogException(ex, "Refreshing tunnel cache file failed!"); } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index 2e8e37c33..9ed2c999d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -102,11 +102,11 @@ public async Task StartAsync(int gamePort) { remoteEndPoint = new IPEndPoint(IPAddress.Loopback, gamePort); #if NETFRAMEWORK - byte[] buffer1 = new byte[1024]; + byte[] buffer1 = new byte[128]; var buffer = new ArraySegment(buffer1); #else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); - Memory buffer = memoryOwner.Memory[..1024]; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(128); + Memory buffer = memoryOwner.Memory[..128]; #endif socket.ReceiveTimeout = Timeout; @@ -151,7 +151,7 @@ public async Task StartAsync(int gamePort) } catch (Exception ex) { - PreStartup.LogException(ex); + PreStartup.HandleException(ex); } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index 726e7f56f..0aef315e8 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -97,7 +97,7 @@ public async Task ConnectAsync() } catch (SocketException ex) { - Logger.Log($"Failed to establish connection to tunnel server. Message: " + ex.Message); + PreStartup.LogException(ex, "Failed to establish connection to tunnel server."); tunnelSocket.Close(); ConnectionFailed?.Invoke(this, EventArgs.Empty); return; @@ -109,7 +109,7 @@ public async Task ConnectAsync() } catch (Exception ex) { - PreStartup.LogException(ex); + PreStartup.HandleException(ex); } } #if NETFRAMEWORK @@ -122,6 +122,10 @@ private async Task ReceiveLoopAsync() { try { +#if !NETFRAMEWORK + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); +#endif + while (true) { if (Aborted) @@ -135,7 +139,6 @@ private async Task ReceiveLoopAsync() byte[] buffer1 = new byte[1024]; var buffer = new ArraySegment(buffer1); #else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); Memory buffer = memoryOwner.Memory[..1024]; #endif @@ -161,7 +164,7 @@ private async Task ReceiveLoopAsync() } catch (SocketException ex) { - Logger.Log("Socket exception in V3 tunnel receive loop: " + ex.Message); + PreStartup.LogException(ex, "Socket exception in V3 tunnel receive loop."); DoClose(); ConnectionCut?.Invoke(this, EventArgs.Empty); } @@ -204,7 +207,7 @@ public async Task SendDataAsync(ReadOnlyMemory data, uint receiverId) data.CopyTo(packet[8..]); #endif - await locker.WaitAsync().ConfigureAwait(false); + await locker.WaitAsync(); try { diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index 974b062b7..3e5d21bb8 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -1,14 +1,15 @@ using ClientCore; using Microsoft.Xna.Framework; -using Rampastring.Tools; -using Rampastring.XNAUI; using System; +#if !NETFRAMEWORK +using System.Buffers; +#endif using System.Collections.Generic; using System.Net; -using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace DTAClient.Domain.Multiplayer.LAN { @@ -17,38 +18,33 @@ public class LANPlayerInfo : PlayerInfo public LANPlayerInfo(Encoding encoding) { this.encoding = encoding; - Port = PORT; + Port = ProgramConstants.LAN_INGAME_PORT; } public event EventHandler MessageReceived; public event EventHandler ConnectionLost; - public event EventHandler PlayerPinged; - private const int PORT = 1234; - private const int LOBBY_PORT = 1233; private const double SEND_PING_TIMEOUT = 10.0; private const double DROP_TIMEOUT = 20.0; - private const int LAN_PING_TIMEOUT = 1000; public TimeSpan TimeSinceLastReceivedMessage { get; set; } public TimeSpan TimeSinceLastSentMessage { get; set; } - public TcpClient TcpClient { get; private set; } + public Socket TcpClient { get; private set; } - NetworkStream networkStream; + private readonly Encoding encoding; - Encoding encoding; + private string overMessage = string.Empty; - string overMessage = string.Empty; + private CancellationTokenSource cancellationTokenSource; - public void SetClient(TcpClient client) + public void SetClient(Socket client) { if (TcpClient != null) throw new InvalidOperationException("TcpClient has already been set for this LANPlayerInfo!"); TcpClient = client; TcpClient.SendTimeout = 1000; - networkStream = client.GetStream(); } /// @@ -56,14 +52,14 @@ public void SetClient(TcpClient client) /// /// Provides a snapshot of timing values. /// True if the player is still considered connected, otherwise false. - public bool Update(GameTime gameTime) + public async Task UpdateAsync(GameTime gameTime) { TimeSinceLastReceivedMessage += gameTime.ElapsedGameTime; TimeSinceLastSentMessage += gameTime.ElapsedGameTime; if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT) || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)) - SendMessage("PING"); + await SendMessageAsync("PING", cancellationTokenSource.Token); if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT)) return false; @@ -76,7 +72,7 @@ public override string IPAddress get { if (TcpClient != null) - return ((IPEndPoint)TcpClient.Client.RemoteEndPoint).Address.ToString(); + return ((IPEndPoint)TcpClient.RemoteEndPoint).Address.ToString(); return base.IPAddress; } @@ -84,7 +80,6 @@ public override string IPAddress set { base.IPAddress = value; - //throw new InvalidOperationException("Cannot set LANPlayerInfo's IPAddress!"); } } @@ -92,123 +87,132 @@ public override string IPAddress /// Sends a message to the player over the network. /// /// The message to send. - public void SendMessage(string message) + public async Task SendMessageAsync(string message, CancellationToken cancellationToken) { - byte[] buffer; + message += ProgramConstants.LAN_MESSAGE_SEPARATOR; - buffer = encoding.GetBytes(message + ProgramConstants.LAN_MESSAGE_SEPARATOR); +#if NETFRAMEWORK + byte[] buffer1 = encoding.GetBytes(message); + var buffer = new ArraySegment(buffer1); + + try + { + await TcpClient.SendAsync(buffer, SocketFlags.None); + } +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); + Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; try { - networkStream.Write(buffer, 0, buffer.Length); - networkStream.Flush(); + await TcpClient.SendAsync(buffer, SocketFlags.None, cancellationToken); } - catch + catch (OperationCanceledException) { - Logger.Log("Sending message to " + ToString() + " failed!"); + } +#endif + catch (Exception ex) + { + PreStartup.LogException(ex, "Sending message to " + ToString() + " failed!"); } TimeSinceLastSentMessage = TimeSpan.Zero; } public override string ToString() - { - return Name + " (" + IPAddress + ")"; - } + => Name + " (" + IPAddress + ")"; /// /// Starts receiving messages from the player asynchronously. /// - public void StartReceiveLoop() - { - Thread thread = new Thread(ReceiveMessages); - thread.Start(); - } + public void StartReceiveLoop(CancellationToken cancellationToken) + => ReceiveMessagesAsync(cancellationToken); /// /// Receives messages sent by the client, /// and hands them over to another class via an event. /// - private void ReceiveMessages() + private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) { - byte[] message = new byte[1024]; - - string msg = String.Empty; - - int bytesRead = 0; - - NetworkStream ns = TcpClient.GetStream(); - - while (true) + try { - bytesRead = 0; - - try - { - //blocks until a client sends a message - bytesRead = ns.Read(message, 0, message.Length); - } - catch (Exception ex) - { - //a socket error has occured - Logger.Log("Socket error with client " + Name + "; removing. Message: " + ex.Message); - ConnectionLost?.Invoke(this, EventArgs.Empty); - break; - } +#if !NETFRAMEWORK + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); - if (bytesRead > 0) +#endif + while (!cancellationToken.IsCancellationRequested) { - msg = encoding.GetString(message, 0, bytesRead); + int bytesRead; +#if NETFRAMEWORK + byte[] buffer1 = new byte[1024]; + var message = new ArraySegment(buffer1); + try + { + bytesRead = await TcpClient.ReceiveAsync(message, SocketFlags.None); + } +#else + Memory message = memoryOwner.Memory[..1024]; - msg = overMessage + msg; - List commands = new List(); + try + { + bytesRead = await TcpClient.ReceiveAsync(message, SocketFlags.None, cancellationToken); + } + catch (OperationCanceledException) + { + ConnectionLost?.Invoke(this, EventArgs.Empty); + break; + } +#endif + catch (Exception ex) + { + PreStartup.LogException(ex, "Socket error with client " + Name + "; removing."); + ConnectionLost?.Invoke(this, EventArgs.Empty); + break; + } - while (true) + if (bytesRead > 0) { - int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR); +#if NETFRAMEWORK + string msg = encoding.GetString(buffer1, 0, bytesRead); +#else + string msg = encoding.GetString(message.Span[..bytesRead]); +#endif - if (index == -1) - { - overMessage = msg; - break; - } - else + msg = overMessage + msg; + List commands = new List(); + + while (true) { + int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR); + + if (index == -1) + { + overMessage = msg; + break; + } + commands.Add(msg.Substring(0, index)); msg = msg.Substring(index + 1); } - } - foreach (string cmd in commands) - { - MessageReceived?.Invoke(this, new NetworkMessageEventArgs(cmd)); + foreach (string cmd in commands) + { + MessageReceived?.Invoke(this, new NetworkMessageEventArgs(cmd)); + } + + continue; } - continue; + ConnectionLost?.Invoke(this, EventArgs.Empty); + break; } - - ConnectionLost?.Invoke(this, EventArgs.Empty); - break; } - } - - public void UpdatePing(WindowManager wm) - { - using (Ping p = new Ping()) + catch (Exception ex) { - try - { - PingReply reply = p.Send(System.Net.IPAddress.Parse(IPAddress), LAN_PING_TIMEOUT); - if (reply.Status == IPStatus.Success) - Ping = Convert.ToInt32(reply.RoundtripTime); - - wm.AddCallback(PlayerPinged, this, EventArgs.Empty); - } - catch (PingException ex) - { - Logger.Log($"Caught an exception when pinging {Name} LAN player: {ex.Message}"); - } + PreStartup.HandleException(ex); } } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index 04c91afa8..842848cc0 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -29,11 +29,6 @@ public class MapLoader public GameModeMapCollection GameModeMaps; - /// - /// An event that is fired when the maps have been loaded. - /// - public event EventHandler MapLoadingComplete; - /// /// A list of game mode aliases. /// Every game mode entry that exists in this dictionary will get @@ -55,11 +50,6 @@ public class MapLoader /// private string[] AllowedGameModes = ClientConfiguration.Instance.AllowedCustomGameModes.Split(','); - /// - /// Loads multiplayer map info asynchonously. - /// - public Task LoadMapsAsync() => Task.Run(LoadMaps); - /// /// Load maps based on INI info as well as those in the custom maps directory. /// @@ -78,8 +68,6 @@ public void LoadMaps() GameModes.RemoveAll(g => g.Maps.Count < 1); GameModeMaps = new GameModeMapCollection(GameModes); - - MapLoadingComplete?.Invoke(this, EventArgs.Empty); } private void LoadMultiMaps(IniFile mpMapsIni) diff --git a/DXMainClient/Online/Channel.cs b/DXMainClient/Online/Channel.cs index 26aa02607..342c38bb8 100644 --- a/DXMainClient/Online/Channel.cs +++ b/DXMainClient/Online/Channel.cs @@ -2,6 +2,7 @@ using DTAClient.Online.EventArguments; using System; using System.Collections.Generic; +using System.Threading.Tasks; using DTAClient.DXGUI; using ClientCore.Extensions; @@ -112,7 +113,7 @@ public void AddUser(ChannelUser user) UserAdded?.Invoke(this, new ChannelUserEventArgs(user)); } - public void OnUserJoined(ChannelUser user) + public async Task OnUserJoinedAsync(ChannelUser user) { AddUser(user); @@ -124,7 +125,7 @@ public void OnUserJoined(ChannelUser user) #if !YR if (Persistent && IsChatChannel && user.IRCUser.Name == ProgramConstants.PLAYERNAME) - RequestUserInfo(); + await RequestUserInfoAsync(); #endif } @@ -254,13 +255,13 @@ public void AddMessage(ChatMessage message) MessageAdded?.Invoke(this, new IRCMessageEventArgs(message)); } - public void SendChatMessage(string message, IRCColor color) + public Task SendChatMessageAsync(string message, IRCColor color) { AddMessage(new ChatMessage(ProgramConstants.PLAYERNAME, color.XnaColor, DateTime.Now, message)); - string colorString = ((char)03).ToString() + color.IrcColorId.ToString("D2"); + string colorString = (char)03 + color.IrcColorId.ToString("D2"); - connection.QueueMessage(QueuedMessageType.CHAT_MESSAGE, 0, + return connection.QueueMessageAsync(QueuedMessageType.CHAT_MESSAGE, 0, "PRIVMSG " + ChannelName + " :" + colorString + message); } @@ -271,12 +272,12 @@ public void SendChatMessage(string message, IRCColor color) /// This can be used to help prevent flooding for multiple options that are changed quickly. It allows for a single message /// for multiple changes. /// - public void SendCTCPMessage(string message, QueuedMessageType qmType, int priority, bool replace = false) + public Task SendCTCPMessageAsync(string message, QueuedMessageType qmType, int priority, bool replace = false) { char CTCPChar1 = (char)58; char CTCPChar2 = (char)01; - connection.QueueMessage(qmType, priority, + return connection.QueueMessageAsync(qmType, priority, "NOTICE " + ChannelName + " " + CTCPChar1 + CTCPChar2 + message + CTCPChar2, replace); } @@ -285,9 +286,9 @@ public void SendCTCPMessage(string message, QueuedMessageType qmType, int priori /// /// The name of the user that should be kicked. /// The priority of the message in the send queue. - public void SendKickMessage(string userName, int priority) + public Task SendKickMessageAsync(string userName, int priority) { - connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority, "KICK " + ChannelName + " " + userName); + return connection.QueueMessageAsync(QueuedMessageType.INSTANT_MESSAGE, priority, "KICK " + ChannelName + " " + userName); } /// @@ -295,13 +296,13 @@ public void SendKickMessage(string userName, int priority) /// /// The host that should be banned. /// The priority of the message in the send queue. - public void SendBanMessage(string host, int priority) + public Task SendBanMessageAsync(string host, int priority) { - connection.QueueMessage(QueuedMessageType.INSTANT_MESSAGE, priority, + return connection.QueueMessageAsync(QueuedMessageType.INSTANT_MESSAGE, priority, string.Format("MODE {0} +b *!*@{1}", ChannelName, host)); } - public void Join() + public Task JoinAsync() { // Wait a random amount of time before joining to prevent join/part floods if (Persistent) @@ -309,35 +310,35 @@ public void Join() int rn = connection.Rng.Next(1, 10000); if (string.IsNullOrEmpty(Password)) - connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "JOIN " + ChannelName); + return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "JOIN " + ChannelName); else - connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "JOIN " + ChannelName + " " + Password); + return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "JOIN " + ChannelName + " " + Password); } else { if (string.IsNullOrEmpty(Password)) - connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, "JOIN " + ChannelName); + return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, "JOIN " + ChannelName); else - connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, "JOIN " + ChannelName + " " + Password); + return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, "JOIN " + ChannelName + " " + Password); } } - public void RequestUserInfo() + public Task RequestUserInfoAsync() { - connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, "WHO " + ChannelName); + return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, "WHO " + ChannelName); } - public void Leave() + public async Task LeaveAsync() { // Wait a random amount of time before joining to prevent join/part floods if (Persistent) { int rn = connection.Rng.Next(1, 10000); - connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "PART " + ChannelName); + await connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "PART " + ChannelName); } else { - connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 9, "PART " + ChannelName); + await connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, "PART " + ChannelName); } ClearUsers(); } diff --git a/DXMainClient/Online/CnCNetManager.cs b/DXMainClient/Online/CnCNetManager.cs index 1b3845952..4ed3f9edf 100644 --- a/DXMainClient/Online/CnCNetManager.cs +++ b/DXMainClient/Online/CnCNetManager.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading.Tasks; namespace DTAClient.Online { @@ -25,8 +26,6 @@ public class CnCNetManager : IConnectionManager // UI thread might be reading, use WindowManager.AddCallback to execute a function // on the UI thread instead of modifying the data or raising events directly. - public delegate void UserListDelegate(string channelName, string[] userNames); - public event EventHandler WelcomeMessageReceived; public event EventHandler AwayMessageReceived; public event EventHandler WhoReplyReceived; @@ -79,7 +78,7 @@ public CnCNetManager(WindowManager wm, GameCollection gc, CnCNetUserData cncNetU public Channel MainChannel { get; private set; } - private bool connected = false; + private bool connected; /// /// Gets a value that determines whether the client is @@ -112,13 +111,6 @@ public bool IsAttemptingConnection private WindowManager wm; - private bool disconnect = false; - - public bool IsCnCNetInitialized() - { - return Connection.IsIdSet(); - } - /// /// Factory method for creating a new channel. /// @@ -155,27 +147,19 @@ public IRCColor[] GetIRCColors() return ircChatColors; } - public void LeaveFromChannel(Channel channel) - { - connection.QueueMessage(QueuedMessageType.SYSTEM_MESSAGE, 10, "PART " + channel.ChannelName); - - if (!channel.Persistent) - channels.Remove(channel); - } - public void SetMainChannel(Channel channel) { MainChannel = channel; } - public void SendCustomMessage(QueuedMessage qm) + public Task SendCustomMessageAsync(QueuedMessage qm) { - connection.QueueMessage(qm); + return connection.QueueMessageAsync(qm); } - public void SendWhoIsMessage(string nick) + public Task SendWhoIsMessageAsync(string nick) { - SendCustomMessage(new QueuedMessage($"WHOIS {nick}", QueuedMessageType.WHOIS_MESSAGE, 0)); + return SendCustomMessageAsync(new QueuedMessage($"WHOIS {nick}", QueuedMessageType.WHOIS_MESSAGE, 0)); } public void OnAttemptedServerChanged(string serverName) @@ -183,7 +167,7 @@ public void OnAttemptedServerChanged(string serverName) // AddCallback is necessary for thread-safety; OnAttemptedServerChanged // is called by the networking thread, and AddCallback schedules DoAttemptedServerChanged // to be executed on the main (UI) thread. - wm.AddCallback(new Action(DoAttemptedServerChanged), serverName); + wm.AddCallback(DoAttemptedServerChanged, serverName); } private void DoAttemptedServerChanged(string serverName) @@ -195,7 +179,7 @@ private void DoAttemptedServerChanged(string serverName) public void OnAwayMessageReceived(string userName, string reason) { - wm.AddCallback(new Action(DoAwayMessageReceived), userName, reason); + wm.AddCallback(DoAwayMessageReceived, userName, reason); } private void DoAwayMessageReceived(string userName, string reason) @@ -205,7 +189,7 @@ private void DoAwayMessageReceived(string userName, string reason) public void OnChannelFull(string channelName) { - wm.AddCallback(new Action(DoChannelFull), channelName); + wm.AddCallback(DoChannelFull, channelName); } private void DoChannelFull(string channelName) @@ -218,7 +202,7 @@ private void DoChannelFull(string channelName) public void OnTargetChangeTooFast(string channelName, string message) { - wm.AddCallback(new Action(DoTargetChangeTooFast), channelName, message); + wm.AddCallback(DoTargetChangeTooFast, channelName, message); } private void DoTargetChangeTooFast(string channelName, string message) @@ -231,7 +215,7 @@ private void DoTargetChangeTooFast(string channelName, string message) public void OnChannelInviteOnly(string channelName) { - wm.AddCallback(new Action(DoChannelInviteOnly), channelName); + wm.AddCallback(DoChannelInviteOnly, channelName); } private void DoChannelInviteOnly(string channelName) @@ -244,8 +228,7 @@ private void DoChannelInviteOnly(string channelName) public void OnChannelModesChanged(string userName, string channelName, string modeString, List modeParameters) { - wm.AddCallback(new Action>(DoChannelModesChanged), - userName, channelName, modeString, modeParameters); + wm.AddCallback(DoChannelModesChanged, userName, channelName, modeString, modeParameters); } private void DoChannelModesChanged(string userName, string channelName, string modeString, List modeParameters) @@ -291,7 +274,7 @@ private void ApplyChannelModes(Channel channel, string modeString, List public void OnChannelTopicReceived(string channelName, string topic) { - wm.AddCallback(new Action(DoChannelTopicReceived), channelName, topic); + wm.AddCallback(DoChannelTopicReceived, channelName, topic); } private void DoChannelTopicReceived(string channelName, string topic) @@ -306,13 +289,12 @@ private void DoChannelTopicReceived(string channelName, string topic) public void OnChannelTopicChanged(string userName, string channelName, string topic) { - wm.AddCallback(new Action(DoChannelTopicReceived), channelName, topic); + wm.AddCallback(DoChannelTopicReceived, channelName, topic); } public void OnChatMessageReceived(string receiver, string senderName, string ident, string message) { - wm.AddCallback(new Action(DoChatMessageReceived), - receiver, senderName, ident, message); + wm.AddCallback(DoChatMessageReceived, receiver, senderName, ident, message); } private void DoChatMessageReceived(string receiver, string senderName, string ident, string message) @@ -375,8 +357,7 @@ private void DoChatMessageReceived(string receiver, string senderName, string id public void OnCTCPParsed(string channelName, string userName, string message) { - wm.AddCallback(new Action(DoCTCPParsed), - channelName, userName, message); + wm.AddCallback(DoCTCPParsed, channelName, userName, message); } private void DoCTCPParsed(string channelName, string userName, string message) @@ -402,7 +383,7 @@ private void DoCTCPParsed(string channelName, string userName, string message) public void OnConnectAttemptFailed() { - wm.AddCallback(new Action(DoConnectAttemptFailed), null); + wm.AddCallback(DoConnectAttemptFailed, null); } private void DoConnectAttemptFailed() @@ -414,7 +395,7 @@ private void DoConnectAttemptFailed() public void OnConnected() { - wm.AddCallback(new Action(DoConnected), null); + wm.AddCallback(DoConnected, null); } private void DoConnected() @@ -427,10 +408,9 @@ private void DoConnected() /// /// Called when the connection has got cut un-intentionally. /// - /// public void OnConnectionLost(string reason) { - wm.AddCallback(new Action(DoConnectionLost), reason); + wm.AddCallback(DoConnectionLost, reason); } private void DoConnectionLost(string reason) @@ -459,10 +439,9 @@ private void DoConnectionLost(string reason) /// /// Disconnects from CnCNet. /// - public void Disconnect() + public async Task DisconnectAsync() { - connection.Disconnect(); - disconnect = true; + await connection.DisconnectAsync(); } /// @@ -470,7 +449,6 @@ public void Disconnect() /// public void Connect() { - disconnect = false; MainChannel.AddMessage(new ChatMessage("Connecting to CnCNet...".L10N("Client:Main:ConnectingToCncNet"))); connection.ConnectAsync(); } @@ -480,7 +458,7 @@ public void Connect() /// public void OnDisconnected() { - wm.AddCallback(new Action(DoDisconnected), null); + wm.AddCallback(DoDisconnected, null); } private void DoDisconnected() @@ -513,7 +491,7 @@ public void OnErrorReceived(string errorMessage) public void OnGenericServerMessageReceived(string message) { - wm.AddCallback(new Action(DoGenericServerMessageReceived), message); + wm.AddCallback(DoGenericServerMessageReceived, message); } private void DoGenericServerMessageReceived(string message) @@ -523,7 +501,7 @@ private void DoGenericServerMessageReceived(string message) public void OnIncorrectChannelPassword(string channelName) { - wm.AddCallback(new Action(DoIncorrectChannelPassword), channelName); + wm.AddCallback(DoIncorrectChannelPassword, channelName); } private void DoIncorrectChannelPassword(string channelName) @@ -540,8 +518,7 @@ public void OnNoticeMessageParsed(string notice, string userName) public void OnPrivateMessageReceived(string sender, string message) { - wm.AddCallback(new Action(DoPrivateMessageReceived), - sender, message); + wm.AddCallback(DoPrivateMessageReceived, sender, message); } private void DoPrivateMessageReceived(string sender, string message) @@ -553,7 +530,7 @@ private void DoPrivateMessageReceived(string sender, string message) public void OnReconnectAttempt() { - wm.AddCallback(new Action(DoReconnectAttempt), null); + wm.AddCallback(DoReconnectAttempt, null); } private void DoReconnectAttempt() @@ -567,62 +544,66 @@ private void DoReconnectAttempt() public void OnUserJoinedChannel(string channelName, string host, string userName, string ident) { - wm.AddCallback(new Action(DoUserJoinedChannel), - channelName, host, userName, ident); + wm.AddCallback(DoUserJoinedChannelAsync, channelName, host, userName, ident); } - private void DoUserJoinedChannel(string channelName, string host, string userName, string userAddress) + private async Task DoUserJoinedChannelAsync(string channelName, string host, string userName, string userAddress) { - Channel channel = FindChannel(channelName); - - if (channel == null) - return; - - bool isAdmin = false; - string name = userName; - - if (userName.StartsWith("@")) + try { - isAdmin = true; - name = userName.Remove(0, 1); - } + Channel channel = FindChannel(channelName); - IRCUser ircUser = null; + if (channel == null) + return; - // Check if we already know this user from another channel - // Avoid LINQ here for performance reasons - foreach (var user in UserList) - { - if (user.Name == name) + bool isAdmin = false; + string name = userName; + + if (userName.StartsWith("@")) { - ircUser = (IRCUser)user.Clone(); - break; + isAdmin = true; + name = userName.Remove(0, 1); } - } - // If we don't know the user, create a new one - if (ircUser == null) - { - string identifier = userAddress.Split('@')[0]; - string[] parts = identifier.Split('.'); - ircUser = new IRCUser(name, identifier, host); + IRCUser ircUser = null; - if (parts.Length > 1) + // Check if we already know this user from another channel + // Avoid LINQ here for performance reasons + foreach (var user in UserList) { - ircUser.GameID = gameCollection.GameList.FindIndex(g => g.InternalName.ToUpper() == parts[0].Replace("~", string.Empty)); + if (user.Name == name) + { + ircUser = (IRCUser)user.Clone(); + break; + } } - AddUserToGlobalUserList(ircUser); - } + // If we don't know the user, create a new one + if (ircUser == null) + { + string identifier = userAddress.Split('@')[0]; + string[] parts = identifier.Split('.'); + ircUser = new IRCUser(name, identifier, host); - var channelUser = new ChannelUser(ircUser); - channelUser.IsAdmin = isAdmin; - channelUser.IsFriend = cncNetUserData.IsFriend(channelUser.IRCUser.Name); + if (parts.Length > 1) + { + ircUser.GameID = gameCollection.GameList.FindIndex(g => g.InternalName.ToUpper() == parts[0].Replace("~", string.Empty)); + } - ircUser.Channels.Add(channelName); - channel.OnUserJoined(channelUser); + AddUserToGlobalUserList(ircUser); + } + + var channelUser = new ChannelUser(ircUser); + channelUser.IsAdmin = isAdmin; + channelUser.IsFriend = cncNetUserData.IsFriend(channelUser.IRCUser.Name); - //UserJoinedChannel?.Invoke(this, new ChannelUserEventArgs(channelName, userName)); + ircUser.Channels.Add(channelName); + await channel.OnUserJoinedAsync(channelUser); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void AddUserToGlobalUserList(IRCUser user) @@ -634,8 +615,7 @@ private void AddUserToGlobalUserList(IRCUser user) public void OnUserKicked(string channelName, string userName) { - wm.AddCallback(new Action(DoUserKicked), - channelName, userName); + wm.AddCallback(DoUserKicked, channelName, userName); } private void DoUserKicked(string channelName, string userName) @@ -666,8 +646,7 @@ private void DoUserKicked(string channelName, string userName) public void OnUserLeftChannel(string channelName, string userName) { - wm.AddCallback(new Action(DoUserLeftChannel), - channelName, userName); + wm.AddCallback(DoUserLeftChannel, channelName, userName); } private void DoUserLeftChannel(string channelName, string userName) @@ -722,8 +701,7 @@ public void RemoveChannelFromUser(string userName, string channelName) public void OnUserListReceived(string channelName, string[] userList) { - wm.AddCallback(new UserListDelegate(DoUserListReceived), - channelName, userList); + wm.AddCallback(DoUserListReceived, channelName, userList); } private void DoUserListReceived(string channelName, string[] userList) @@ -774,7 +752,7 @@ private void DoUserListReceived(string channelName, string[] userList) public void OnUserQuitIRC(string userName) { - wm.AddCallback(new Action(DoUserQuitIRC), userName); + wm.AddCallback(DoUserQuitIRC, userName); } private void DoUserQuitIRC(string userName) @@ -792,7 +770,7 @@ private void DoUserQuitIRC(string userName) public void OnWelcomeMessageReceived(string message) { - wm.AddCallback(new Action(DoWelcomeMessageReceived), message); + wm.AddCallback(DoWelcomeMessageReceived, message); } @@ -823,8 +801,7 @@ private void DoWelcomeMessageReceived(string message) public void OnWhoReplyReceived(string ident, string hostName, string userName, string extraInfo) { - wm.AddCallback(new Action(DoWhoReplyReceived), - ident, hostName, userName, extraInfo); + wm.AddCallback(DoWhoReplyReceived, ident, hostName, userName, extraInfo); } private void DoWhoReplyReceived(string ident, string hostName, string userName, string extraInfo) @@ -859,14 +836,9 @@ private void DoWhoReplyReceived(string ident, string hostName, string userName, } } - public bool GetDisconnectStatus() - { - return disconnect; - } - public void OnNameAlreadyInUse() { - wm.AddCallback(new Action(DoNameAlreadyInUse), null); + wm.AddCallback(DoNameAlreadyInUseAsync, null); } /// @@ -874,43 +846,50 @@ public void OnNameAlreadyInUse() /// IRC user. Adds additional underscores to the name or replaces existing /// characters with underscores. /// - private void DoNameAlreadyInUse() + private async Task DoNameAlreadyInUseAsync() { - var charList = ProgramConstants.PLAYERNAME.ToList(); - int maxNameLength = ClientConfiguration.Instance.MaxNameLength; - - if (charList.Count < maxNameLength) - charList.Add('_'); - else + try { - int lastNonUnderscoreIndex = charList.FindLastIndex(c => c != '_'); + var charList = ProgramConstants.PLAYERNAME.ToList(); + int maxNameLength = ClientConfiguration.Instance.MaxNameLength; - if (lastNonUnderscoreIndex == -1) + if (charList.Count < maxNameLength) + charList.Add('_'); + else { - MainChannel.AddMessage(new ChatMessage(Color.White, - "Your nickname is invalid or already in use. Please change your nickname in the login screen.".L10N("Client:Main:PickAnotherNickName"))); - UserINISettings.Instance.SkipConnectDialog.Value = false; - Disconnect(); - return; - } + int lastNonUnderscoreIndex = charList.FindLastIndex(c => c != '_'); - charList[lastNonUnderscoreIndex] = '_'; - } + if (lastNonUnderscoreIndex == -1) + { + MainChannel.AddMessage(new ChatMessage(Color.White, + "Your nickname is invalid or already in use. Please change your nickname in the login screen.".L10N("Client:Main:PickAnotherNickName"))); + UserINISettings.Instance.SkipConnectDialog.Value = false; + await DisconnectAsync(); + return; + } + + charList[lastNonUnderscoreIndex] = '_'; + } - var sb = new StringBuilder(); - foreach (char c in charList) - sb.Append(c); + var sb = new StringBuilder(); + foreach (char c in charList) + sb.Append(c); - MainChannel.AddMessage(new ChatMessage(Color.White, - string.Format("Your name is already in use. Retrying with {0}...".L10N("Client:Main:NameInUseRetry"), sb.ToString()))); + MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Your name is already in use. Retrying with {0}...".L10N("Client:Main:NameInUseRetry"), sb))); - ProgramConstants.PLAYERNAME = sb.ToString(); - connection.ChangeNickname(); + ProgramConstants.PLAYERNAME = sb.ToString(); + await connection.ChangeNicknameAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } public void OnBannedFromChannel(string channelName) { - wm.AddCallback(new Action(DoBannedFromChannel), channelName); + wm.AddCallback(DoBannedFromChannel, channelName); } private void DoBannedFromChannel(string channelName) @@ -919,7 +898,7 @@ private void DoBannedFromChannel(string channelName) } public void OnUserNicknameChange(string oldNickname, string newNickname) - => wm.AddCallback(new Action(DoUserNicknameChange), oldNickname, newNickname); + => wm.AddCallback(DoUserNicknameChange, oldNickname, newNickname); private void DoUserNicknameChange(string oldNickname, string newNickname) { @@ -956,17 +935,7 @@ public UserEventArgs(IRCUser ircUser) User = ircUser; } - public IRCUser User { get; private set; } - } - - public class IndexEventArgs : EventArgs - { - public IndexEventArgs(int index) - { - Index = index; - } - - public int Index { get; private set; } + public IRCUser User { get; } } public class UserNameChangedEventArgs : EventArgs @@ -980,4 +949,4 @@ public UserNameChangedEventArgs(string oldUserName, IRCUser user) public string OldUserName { get; } public IRCUser User { get; } } -} +} \ No newline at end of file diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index ba1341bab..e33b99d82 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -2,6 +2,9 @@ using ClientCore.Extensions; using Rampastring.Tools; using System; +#if !NETFRAMEWORK +using System.Buffers; +#endif using System.Collections.Generic; using System.IO; using System.Linq; @@ -29,74 +32,59 @@ public Connection(IConnectionManager connectionManager) this.connectionManager = connectionManager; } - IConnectionManager connectionManager; + private readonly IConnectionManager connectionManager; /// /// The list of CnCNet / GameSurge IRC servers to connect to. /// private static readonly IList Servers = new List { - new Server("Burstfire.UK.EU.GameSurge.net", "GameSurge London, UK", new int[3] { 6667, 6668, 7000 }), - new Server("ColoCrossing.IL.US.GameSurge.net", "GameSurge Chicago, IL", new int[5] { 6660, 6666, 6667, 6668, 6669 }), - new Server("Gameservers.NJ.US.GameSurge.net", "GameSurge Newark, NJ", new int[7] { 6665, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("Krypt.CA.US.GameSurge.net", "GameSurge Santa Ana, CA", new int[4] { 6666, 6667, 6668, 6669 }), - new Server("NuclearFallout.WA.US.GameSurge.net", "GameSurge Seattle, WA", new int[2] { 6667, 5960 }), - new Server("Portlane.SE.EU.GameSurge.net", "GameSurge Stockholm, Sweden", new int[5] { 6660, 6666, 6667, 6668, 6669 }), - new Server("Prothid.NY.US.GameSurge.Net", "GameSurge NYC, NY", new int[7] { 5960, 6660, 6666, 6667, 6668, 6669, 6697 }), - new Server("TAL.DE.EU.GameSurge.net", "GameSurge Wuppertal, Germany", new int[5] { 6660, 6666, 6667, 6668, 6669 }), - new Server("208.167.237.120", "GameSurge IP 208.167.237.120", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("192.223.27.109", "GameSurge IP 192.223.27.109", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("108.174.48.100", "GameSurge IP 108.174.48.100", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("208.146.35.105", "GameSurge IP 208.146.35.105", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("195.8.250.180", "GameSurge IP 195.8.250.180", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("91.217.189.76", "GameSurge IP 91.217.189.76", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("195.68.206.250", "GameSurge IP 195.68.206.250", new int[7] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new Server("irc.gamesurge.net", "GameSurge", new int[1] { 6667 }), + new("Burstfire.UK.EU.GameSurge.net", "GameSurge London, UK", new[] { 6667, 6668, 7000 }), + new("ColoCrossing.IL.US.GameSurge.net", "GameSurge Chicago, IL", new[] { 6660, 6666, 6667, 6668, 6669 }), + new("Gameservers.NJ.US.GameSurge.net", "GameSurge Newark, NJ", new[] { 6665, 6666, 6667, 6668, 6669, 7000, 8080 }), + new("Krypt.CA.US.GameSurge.net", "GameSurge Santa Ana, CA",new[] { 6666, 6667, 6668, 6669 }), + new("NuclearFallout.WA.US.GameSurge.net", "GameSurge Seattle, WA", new[] { 6667, 5960 }), + new("Portlane.SE.EU.GameSurge.net", "GameSurge Stockholm, Sweden", new[] { 6660, 6666, 6667, 6668, 6669 }), + new("Prothid.NY.US.GameSurge.Net", "GameSurge NYC, NY", new[] { 5960, 6660, 6666, 6667, 6668, 6669, 6697 }), + new("TAL.DE.EU.GameSurge.net", "GameSurge Wuppertal, Germany", new[] { 6660, 6666, 6667, 6668, 6669 }), + new("208.167.237.120", "GameSurge IP 208.167.237.120", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), + new("192.223.27.109", "GameSurge IP 192.223.27.109", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), + new("108.174.48.100", "GameSurge IP 108.174.48.100", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), + new("208.146.35.105", "GameSurge IP 208.146.35.105", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), + new("195.8.250.180", "GameSurge IP 195.8.250.180", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), + new("91.217.189.76", "GameSurge IP 91.217.189.76", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), + new("195.68.206.250", "GameSurge IP 195.68.206.250", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), + new("irc.gamesurge.net", "GameSurge", new[] { 6667 }), }.AsReadOnly(); - bool _isConnected = false; + bool _isConnected; public bool IsConnected { get { return _isConnected; } } - bool _attemptingConnection = false; + bool _attemptingConnection; public bool AttemptingConnection { get { return _attemptingConnection; } } - Random _rng = new Random(); + Random _rng = new(); public Random Rng { get { return _rng; } } - private List MessageQueue = new List(); + private List MessageQueue = new(); private TimeSpan MessageQueueDelay; - private NetworkStream serverStream; - private TcpClient tcpClient; + private Socket socket; - volatile int reconnectCount = 0; + volatile int reconnectCount; - private volatile bool connectionCut = false; - private volatile bool welcomeMessageReceived = false; - private volatile bool sendQueueExited = false; - bool _disconnect = false; - private bool disconnect - { - get - { - lock (locker) - return _disconnect; - } - set - { - lock (locker) - _disconnect = value; - } - } + private volatile bool connectionCut; + private volatile bool welcomeMessageReceived; + private volatile bool sendQueueExited; private string overMessage; @@ -108,15 +96,14 @@ private bool disconnect /// prevent a server that first accepts a connection and then drops it /// right afterwards from preventing online play. /// - private readonly List failedServerIPs = new List(); + private readonly List failedServerIPs = new(); private volatile string currentConnectedServerIP; - private static readonly object locker = new object(); - private static readonly object messageQueueLocker = new object(); + private static readonly SemaphoreSlim messageQueueLocker = new(1, 1); - private static bool idSet = false; private static string systemId; - private static readonly object idLocker = new object(); + private static readonly object idLocker = new(); + private CancellationTokenSource cancellationTokenSource; public static void SetId(string id) { @@ -124,15 +111,6 @@ public static void SetId(string id) { int maxLength = ID_LENGTH - (ClientConfiguration.Instance.LocalGame.Length + 1); systemId = Utilities.CalculateSHA1ForString(id).Substring(0, maxLength); - idSet = true; - } - } - - public static bool IsIdSet() - { - lock (idLocker) - { - return idSet; } } @@ -150,109 +128,147 @@ public void ConnectAsync() welcomeMessageReceived = false; connectionCut = false; _attemptingConnection = true; - disconnect = false; MessageQueueDelay = TimeSpan.FromMilliseconds(ClientConfiguration.Instance.SendSleep); - Thread connection = new Thread(ConnectToServer); - connection.Start(); + cancellationTokenSource?.Dispose(); + cancellationTokenSource = new CancellationTokenSource(); + + ConnectToServerAsync(cancellationTokenSource.Token); } /// /// Attempts to connect to CnCNet. /// - private void ConnectToServer() + private async Task ConnectToServerAsync(CancellationToken cancellationToken) { - IList sortedServerList = GetServerListSortedByLatency(); - - foreach (Server server in sortedServerList) + try { - try + IList sortedServerList = await GetServerListSortedByLatencyAsync(); + + foreach (Server server in sortedServerList) { - for (int i = 0; i < server.Ports.Length; i++) + try { - connectionManager.OnAttemptedServerChanged(server.Name); + foreach (int port in server.Ports) + { + connectionManager.OnAttemptedServerChanged(server.Name); - TcpClient client = new TcpClient(AddressFamily.InterNetwork); - var result = client.BeginConnect(server.Host, server.Ports[i], null, null); - result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(3), false); + var client = new Socket(SocketType.Stream, ProtocolType.Tcp) + { + ReceiveTimeout = 1000 + }; - Logger.Log("Attempting connection to " + server.Host + ":" + server.Ports[i]); + Logger.Log("Attempting connection to " + server.Host + ":" + port); - if (!client.Connected) - { - Logger.Log("Connecting to " + server.Host + " port " + server.Ports[i] + " timed out!"); - continue; // Start all over again, using the next port - } +#if NETFRAMEWORK + IAsyncResult result = client.BeginConnect(server.Host, port, null, null); + result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(3), false); +#else + try + { + await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(server.Host), port), + new CancellationTokenSource(TimeSpan.FromSeconds(3)).Token); + } + catch (OperationCanceledException) + { } +#endif + + if (!client.Connected) + { + Logger.Log("Connecting to " + server.Host + " port " + port + " timed out!"); + continue; // Start all over again, using the next port + } - Logger.Log("Succesfully connected to " + server.Host + " on port " + server.Ports[i]); - client.EndConnect(result); + Logger.Log("Succesfully connected to " + server.Host + " on port " + port); +#if NETFRAMEWORK + client.EndConnect(result); +#endif - _isConnected = true; - _attemptingConnection = false; + _isConnected = true; + _attemptingConnection = false; - connectionManager.OnConnected(); + connectionManager.OnConnected(); - Thread sendQueueHandler = new Thread(RunSendQueue); - sendQueueHandler.Start(); + RunSendQueueAsync(cancellationToken); - tcpClient = client; - serverStream = tcpClient.GetStream(); - serverStream.ReadTimeout = 1000; + socket?.Dispose(); + socket = client; - currentConnectedServerIP = server.Host; - HandleComm(); - return; + currentConnectedServerIP = server.Host; + await HandleCommAsync(cancellationToken); + return; + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + PreStartup.LogException(ex, "Unable to connect to the server."); } } - catch (Exception ex) - { - Logger.Log("Unable to connect to the server. " + ex.Message); - } - } - Logger.Log("Connecting to CnCNet failed!"); - // Clear the failed server list in case connecting to all servers has failed - failedServerIPs.Clear(); - _attemptingConnection = false; - connectionManager.OnConnectAttemptFailed(); + Logger.Log("Connecting to CnCNet failed!"); + // Clear the failed server list in case connecting to all servers has failed + failedServerIPs.Clear(); + _attemptingConnection = false; + connectionManager.OnConnectAttemptFailed(); + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private void HandleComm() + private async Task HandleCommAsync(CancellationToken cancellationToken) { int errorTimes = 0; - byte[] message = new byte[1024]; +#if NETFRAMEWORK + byte[] message1 = new byte[1024]; + var message = new ArraySegment(message1); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); + Memory message = memoryOwner.Memory[..1024]; +#endif + + await RegisterAsync(); - Register(); + var timer = new System.Timers.Timer(120000) + { + Enabled = true + }; - Timer timer = new Timer(AutoPing, null, 30000, 120000); + timer.Elapsed += (_, _) => AutoPingAsync(); connectionCut = true; - while (true) + while (!cancellationToken.IsCancellationRequested) { - if (connectionManager.GetDisconnectStatus()) - { - connectionManager.OnDisconnected(); - connectionCut = false; // This disconnect is intentional - break; - } - - if (!serverStream.DataAvailable) - { - Thread.Sleep(10); - continue; - } - int bytesRead; try { - bytesRead = serverStream.Read(message, 0, 1024); +#if NETFRAMEWORK + bytesRead = await socket.ReceiveAsync(message, SocketFlags.None); } +#else + bytesRead = await socket.ReceiveAsync(message, SocketFlags.None, cancellationToken); + } + catch (OperationCanceledException) + { + connectionManager.OnDisconnected(); + connectionCut = false; // This disconnect is intentional + break; + } +#endif catch (Exception ex) { - Logger.Log("Disconnected from CnCNet due to a socket error. Message: " + ex.Message); + PreStartup.LogException(ex, "Disconnected from CnCNet due to a socket error."); errorTimes++; if (errorTimes > MAX_RECONNECT_COUNT) @@ -269,24 +285,36 @@ private void HandleComm() errorTimes = 0; - // A message has been succesfully received - string msg = encoding.GetString(message, 0, bytesRead); + // A message has been successfully received +#if NETFRAMEWORK + string msg = encoding.GetString(message1, 0, bytesRead); +#else + string msg = encoding.GetString(message.Span[..bytesRead]); +#endif + Logger.Log("Message received: " + msg); - HandleMessage(msg); - timer.Change(30000, 30000); + await HandleMessageAsync(msg); + timer.Interval = 30000; } - timer.Change(Timeout.Infinite, Timeout.Infinite); +#if NETFRAMEWORK + if (cancellationToken.IsCancellationRequested) + { + connectionManager.OnDisconnected(); + connectionCut = false; // This disconnect is intentional + } + +#endif + timer.Enabled = false; timer.Dispose(); _isConnected = false; - disconnect = false; if (connectionCut) { while (!sendQueueExited) - Thread.Sleep(100); + await Task.Delay(100); reconnectCount++; @@ -296,7 +324,7 @@ private void HandleComm() return; } - Thread.Sleep(RECONNECT_WAIT_DELAY); + await Task.Delay(RECONNECT_WAIT_DELAY); if (IsConnected || AttemptingConnection) { @@ -315,179 +343,137 @@ private void HandleComm() /// Servers that did not respond to ICMP messages in time will be placed at the end of the list. /// /// A list of Lobby servers sorted by latency. - private IList GetServerListSortedByLatency() + private async Task> GetServerListSortedByLatencyAsync() { // Resolve the hostnames. - ICollection>>> - dnsTasks = new List>>>(Servers.Count); - - foreach (Server server in Servers) - { - string serverHostnameOrIPAddress = server.Host; - string serverName = server.Name; - int[] serverPorts = server.Ports; - - Task>> dnsTask = new Task>>(() => - { - Logger.Log($"Attempting to DNS resolve {serverName} ({serverHostnameOrIPAddress})."); - ICollection> _serverInfos = new List>(); - - try - { - // If hostNameOrAddress is an IP address, this address is returned without querying the DNS server. - IEnumerable serverIPAddresses = Dns.GetHostAddresses(serverHostnameOrIPAddress) - .Where(IPAddress => IPAddress.AddressFamily == AddressFamily.InterNetwork); - - Logger.Log($"DNS resolved {serverName} ({serverHostnameOrIPAddress}): " + - $"{string.Join(", ", serverIPAddresses.Select(item => item.ToString()))}"); - - // Store each IPAddress in a different tuple. - foreach (IPAddress serverIPAddress in serverIPAddresses) - { - _serverInfos.Add(new Tuple(serverIPAddress, serverName, serverPorts)); - } - } - catch (SocketException ex) - { - Logger.Log($"Caught an exception when DNS resolving {serverName} ({serverHostnameOrIPAddress}) Lobby server: {ex.Message}"); - } - - return _serverInfos; - }); - - dnsTask.Start(); - dnsTasks.Add(dnsTask); - } - - Task.WaitAll(dnsTasks.ToArray()); + IEnumerable<(IPAddress IpAddress, string Name, int[] Ports)>[] servers = await Task.WhenAll(Servers.Select(ResolveServerAsync)); // Group the tuples by IPAddress to merge duplicate servers. - IEnumerable>> - serverInfosGroupedByIPAddress = dnsTasks.SelectMany(dnsTask => dnsTask.Result) // Tuple - .GroupBy( - serverInfo => serverInfo.Item1, // IPAddress - serverInfo => new Tuple( - serverInfo.Item2, // serverName - serverInfo.Item3 // serverPorts - ) - ); + IEnumerable> serverInfosGroupedByIPAddress = servers + .SelectMany(server => server) + .GroupBy(serverInfo => serverInfo.IpAddress, serverInfo => (serverInfo.Name, serverInfo.Ports)); // Process each group: // 1. Get IPAddress. // 2. Concatenate serverNames. // 3. Remove duplicate ports. // 4. Construct and return a tuple that contains the IPAddress, concatenated serverNames and unique ports. - IEnumerable> serverInfos = serverInfosGroupedByIPAddress.Select(serverInfoGroup => + (IPAddress IpAddress, string Name, int[] Ports)[] serverInfos = serverInfosGroupedByIPAddress.Select(serverInfoGroup => { IPAddress ipAddress = serverInfoGroup.Key; - string serverNames = string.Join(", ", serverInfoGroup.Select(serverInfo => serverInfo.Item1)); - int[] serverPorts = serverInfoGroup.SelectMany(serverInfo => serverInfo.Item2).Distinct().ToArray(); + string serverNames = string.Join(", ", serverInfoGroup.Select(serverInfo => serverInfo.Name)); + int[] serverPorts = serverInfoGroup.SelectMany(serverInfo => serverInfo.Ports).Distinct().ToArray(); - return new Tuple(ipAddress, serverNames, serverPorts); - }); + return (ipAddress, serverNames, serverPorts); + }).ToArray(); // Do logging. - foreach (Tuple serverInfo in serverInfos) + foreach ((IPAddress ipAddress, string name, int[] ports) in serverInfos) { - string serverIPAddress = serverInfo.Item1.ToString(); - string serverNames = string.Join(", ", serverInfo.Item2.ToString()); - string serverPorts = string.Join(", ", serverInfo.Item3.Select(port => port.ToString())); + string serverIPAddress = ipAddress.ToString(); + string serverNames = string.Join(", ", name); + string serverPorts = string.Join(", ", ports.Select(port => port.ToString())); Logger.Log($"Got a Lobby server. IP: {serverIPAddress}; Name: {serverNames}; Ports: {serverPorts}."); } - Logger.Log($"The number of Lobby servers is {serverInfos.Count()}."); + Logger.Log($"The number of Lobby servers is {serverInfos.Length}."); // Test the latency. - ICollection>> pingTasks = new List>>(serverInfos.Count()); - - foreach (Tuple serverInfo in serverInfos) + foreach ((IPAddress ipAddress, string name, int[] _) in serverInfos.Where(q => failedServerIPs.Contains(q.IpAddress.ToString()))) { - IPAddress serverIPAddress = serverInfo.Item1; - string serverNames = serverInfo.Item2; - int[] serverPorts = serverInfo.Item3; + Logger.Log($"Skipped a failed server {name} ({ipAddress})."); + } - if (failedServerIPs.Contains(serverIPAddress.ToString())) - { - Logger.Log($"Skipped a failed server {serverNames} ({serverIPAddress})."); - continue; - } + (Server Server, long Result)[] serverAndLatencyResults = + await Task.WhenAll(serverInfos.Where(q => !failedServerIPs.Contains(q.IpAddress.ToString())).Select(PingServerAsync)); - Task> pingTask = new Task>(() => - { - Logger.Log($"Attempting to ping {serverNames} ({serverIPAddress})."); - Server server = new Server(serverIPAddress.ToString(), serverNames, serverPorts); + // Sort the servers by latency. + (Server Server, long Result)[] sortedServerAndLatencyResults = serverAndLatencyResults + .Select(server => server) + .OrderBy(taskResult => taskResult.Result) + .ToArray(); - using (Ping ping = new Ping()) - { - try - { - PingReply pingReply = ping.Send(serverIPAddress, MAXIMUM_LATENCY); + // Do logging. + foreach ((Server server, long serverLatencyValue) in sortedServerAndLatencyResults) + { + string serverIPAddress = server.Host; + string serverLatencyString = serverLatencyValue <= MAXIMUM_LATENCY ? serverLatencyValue.ToString() : "DNF"; - if (pingReply.Status == IPStatus.Success) - { - long pingInMs = pingReply.RoundtripTime; - Logger.Log($"The latency in milliseconds to the server {serverNames} ({serverIPAddress}): {pingInMs}."); + Logger.Log($"Lobby server IP: {serverIPAddress}, latency: {serverLatencyString}."); + } - return new Tuple(server, pingInMs); - } - else - { - Logger.Log($"Failed to ping the server {serverNames} ({serverIPAddress}): " + - $"{Enum.GetName(typeof(IPStatus), pingReply.Status)}."); + int candidateCount = sortedServerAndLatencyResults.Length; + int closerCount = sortedServerAndLatencyResults.Count( + serverAndLatencyResult => serverAndLatencyResult.Item2 <= MAXIMUM_LATENCY); - return new Tuple(server, long.MaxValue); - } - } - catch (PingException ex) - { - Logger.Log($"Caught an exception when pinging {serverNames} ({serverIPAddress}) Lobby server: {ex.Message}"); + Logger.Log($"Lobby servers: {candidateCount} available, {closerCount} fast."); + connectionManager.OnServerLatencyTested(candidateCount, closerCount); - return new Tuple(server, long.MaxValue); - } - } - }); + return sortedServerAndLatencyResults.Select(taskResult => taskResult.Server).ToList(); + } - pingTask.Start(); - pingTasks.Add(pingTask); - } + private static async Task<(Server Server, long Result)> PingServerAsync((IPAddress IpAddress, string Name, int[] Ports) serverInfo) + { + Logger.Log($"Attempting to ping {serverInfo.Name} ({serverInfo.IpAddress})."); + var server = new Server(serverInfo.IpAddress.ToString(), serverInfo.Name, serverInfo.Ports); + using var ping = new Ping(); - Task.WaitAll(pingTasks.ToArray()); + try + { + PingReply pingReply = await ping.SendPingAsync(serverInfo.IpAddress, MAXIMUM_LATENCY); - // Sort the servers by latency. - IOrderedEnumerable> - sortedServerAndLatencyResults = pingTasks.Select(task => task.Result) // Tuple - .OrderBy(taskResult => taskResult.Item2); // Latency + if (pingReply.Status == IPStatus.Success) + { + long pingInMs = pingReply.RoundtripTime; + Logger.Log($"The latency in milliseconds to the server {serverInfo.Name} ({serverInfo.IpAddress}): {pingInMs}."); - // Do logging. - foreach (Tuple serverAndLatencyResult in sortedServerAndLatencyResults) + return (server, pingInMs); + } + + Logger.Log($"Failed to ping the server {serverInfo.Name} ({serverInfo.IpAddress}): " + + $"{Enum.GetName(typeof(IPStatus), pingReply.Status)}."); + + return (server, long.MaxValue); + } + catch (PingException ex) { - string serverIPAddress = serverAndLatencyResult.Item1.Host; - long serverLatencyValue = serverAndLatencyResult.Item2; - string serverLatencyString = serverLatencyValue <= MAXIMUM_LATENCY ? serverLatencyValue.ToString() : "DNF"; + PreStartup.LogException(ex, $"Caught an exception when pinging {serverInfo.Name} ({serverInfo.IpAddress}) Lobby server."); - Logger.Log($"Lobby server IP: {serverIPAddress}, latency: {serverLatencyString}."); + return (server, long.MaxValue); } + } + private static async Task> ResolveServerAsync(Server server) + { + Logger.Log($"Attempting to DNS resolve {server.Name} ({server.Host})."); + + try { - int candidateCount = sortedServerAndLatencyResults.Count(); - int closerCount = sortedServerAndLatencyResults.Count( - serverAndLatencyResult => serverAndLatencyResult.Item2 <= MAXIMUM_LATENCY); + // If hostNameOrAddress is an IP address, this address is returned without querying the DNS server. + IPAddress[] serverIPAddresses = (await Dns.GetHostAddressesAsync(server.Host)) + .Where(IPAddress => IPAddress.AddressFamily is AddressFamily.InterNetworkV6 or AddressFamily.InterNetwork) + .ToArray(); - Logger.Log($"Lobby servers: {candidateCount} available, {closerCount} fast."); - connectionManager.OnServerLatencyTested(candidateCount, closerCount); + Logger.Log($"DNS resolved {server.Name} ({server.Host}): " + + $"{string.Join(", ", serverIPAddresses.Select(item => item.ToString()))}"); + + // Store each IPAddress in a different tuple. + return serverIPAddresses.Select(serverIPAddress => (serverIPAddress, server.Name, server.Ports)); + } + catch (SocketException ex) + { + PreStartup.LogException(ex, $"Caught an exception when DNS resolving {server.Name} ({server.Host}) Lobby server."); } - return sortedServerAndLatencyResults.Select(taskResult => taskResult.Item1).ToList(); // Server + return Array.Empty<(IPAddress IpAddress, string Name, int[] Ports)>(); } - public void Disconnect() + public async Task DisconnectAsync() { - disconnect = true; - SendMessage("QUIT"); - - tcpClient.Close(); - serverStream.Close(); + await SendMessageAsync("QUIT"); + cancellationTokenSource.Cancel(); + socket.Close(); } #region Handling commands @@ -497,7 +483,7 @@ public void Disconnect() /// message, and handles it accordingly. /// /// The message. - private void HandleMessage(string message) + private async Task HandleMessageAsync(string message) { string msg = overMessage + message; overMessage = ""; @@ -513,14 +499,14 @@ private void HandleMessage(string message) else if (msg.Length != commandEndIndex + 1) { string command = msg.Substring(0, commandEndIndex - 1); - PerformCommand(command); + await PerformCommandAsync(command); msg = msg.Remove(0, commandEndIndex + 1); } else { string command = msg.Substring(0, msg.Length - 1); - PerformCommand(command); + await PerformCommandAsync(command); break; } } @@ -529,22 +515,17 @@ private void HandleMessage(string message) /// /// Handles a specific command received from the IRC server. /// - private void PerformCommand(string message) + private async Task PerformCommandAsync(string message) { - string prefix = String.Empty; - string command = String.Empty; - message = message.Replace("\r", String.Empty); - List parameters = new List(); - ParseIrcMessage(message, out prefix, out command, out parameters); - string paramString = String.Empty; + message = message.Replace("\r", string.Empty); + ParseIrcMessage(message, out string prefix, out string command, out List parameters); + string paramString = string.Empty; foreach (string param in parameters) { paramString = paramString + param + ","; } Logger.Log("RMP: " + prefix + " " + command + " " + paramString); try { - bool success = false; - int commandNumber = -1; - success = Int32.TryParse(command, out commandNumber); + bool success = int.TryParse(command, out int commandNumber); if (success) { @@ -626,7 +607,7 @@ private void PerformCommand(string message) connectionManager.OnNameAlreadyInUse(); break; case 451: // Not registered - Register(); + await RegisterAsync(); connectionManager.OnGenericServerMessageReceived(message); break; case 471: // Returned when attempting to join a channel that is full (basically, player limit met) @@ -671,7 +652,7 @@ private void PerformCommand(string message) break; } } - string noticeParamString = String.Empty; + string noticeParamString = string.Empty; foreach (string param in parameters) noticeParamString = noticeParamString + param + " "; connectionManager.OnGenericServerMessageReceived(prefix + " " + noticeParamString); @@ -705,7 +686,7 @@ private void PerformCommand(string message) for (int pid = 0; pid < parameters.Count - 1; pid++) recipients[pid] = parameters[pid]; string privmsg = parameters[parameters.Count - 1]; - if (parameters[1].StartsWith('\u0001'.ToString() + "ACTION")) + if (parameters[1].StartsWith('\u0001' + "ACTION")) privmsg = privmsg.Substring(1).Remove(privmsg.Length - 2); foreach (string recipient in recipients) { @@ -713,10 +694,6 @@ private void PerformCommand(string message) connectionManager.OnChatMessageReceived(recipient, pmsgUserName, pmsgIdent, privmsg); else if (recipient == ProgramConstants.PLAYERNAME) connectionManager.OnPrivateMessageReceived(pmsgUserName, privmsg); - //else if (pmsgUserName == ProgramConstants.PLAYERNAME) - //{ - // DoPrivateMessageSent(privmsg, recipient); - //} } break; case "MODE": @@ -738,12 +715,12 @@ private void PerformCommand(string message) case "PING": if (parameters.Count > 0) { - QueueMessage(new QueuedMessage("PONG " + parameters[0], QueuedMessageType.SYSTEM_MESSAGE, 5000)); + await QueueMessageAsync(new QueuedMessage("PONG " + parameters[0], QueuedMessageType.SYSTEM_MESSAGE, 5000)); Logger.Log("PONG " + parameters[0]); } else { - QueueMessage(new QueuedMessage("PONG", QueuedMessageType.SYSTEM_MESSAGE, 5000)); + await QueueMessageAsync(new QueuedMessage("PONG", QueuedMessageType.SYSTEM_MESSAGE, 5000)); Logger.Log("PONG"); } break; @@ -766,9 +743,9 @@ private void PerformCommand(string message) break; } } - catch + catch (Exception ex) { - Logger.Log("Warning: Failed to parse command " + message); + PreStartup.LogException(ex, "Warning: Failed to parse command " + message); } } @@ -793,7 +770,7 @@ private string GetIdentFromPrefix(string prefix) private void ParseIrcMessage(string message, out string prefix, out string command, out List parameters) { int prefixEnd = -1; - prefix = command = String.Empty; + prefix = command = string.Empty; parameters = new List(); // Grab the prefix if it is present. If a message begins @@ -821,7 +798,7 @@ private void ParseIrcMessage(string message, out string prefix, out string comma if (commandAndParameters.Length == 0) { - command = String.Empty; + command = string.Empty; Logger.Log("Nonexistant command!"); return; } @@ -849,70 +826,102 @@ private void ParseIrcMessage(string message, out string prefix, out string comma #region Sending commands - private void RunSendQueue() + private async Task RunSendQueueAsync(CancellationToken cancellationToken) { - while (_isConnected) + try { - string message = String.Empty; - - lock (messageQueueLocker) + try { - for (int i = 0; i < MessageQueue.Count; i++) + while (!cancellationToken.IsCancellationRequested) { - QueuedMessage qm = MessageQueue[i]; - if (qm.Delay > 0) - { - if (qm.SendAt < DateTime.Now) - { - message = qm.Command; + string message = string.Empty; - Logger.Log("Delayed message sent: " + qm.ID); + await messageQueueLocker.WaitAsync(cancellationToken); - MessageQueue.RemoveAt(i); - break; + try + { + for (int i = 0; i < MessageQueue.Count; i++) + { + QueuedMessage qm = MessageQueue[i]; + if (qm.Delay > 0) + { + if (qm.SendAt < DateTime.Now) + { + message = qm.Command; + + Logger.Log("Delayed message sent: " + qm.ID); + + MessageQueue.RemoveAt(i); + break; + } + } + else + { + message = qm.Command; + MessageQueue.RemoveAt(i); + break; + } } } - else + finally { - message = qm.Command; - MessageQueue.RemoveAt(i); - break; + messageQueueLocker.Release(); + } + + if (string.IsNullOrEmpty(message)) + { + await Task.Delay(10, cancellationToken); + continue; } + + await SendMessageAsync(message); + await Task.Delay(MessageQueueDelay, cancellationToken); } } - - if (String.IsNullOrEmpty(message)) + catch (OperationCanceledException) { - Thread.Sleep(10); - continue; } + finally + { + await messageQueueLocker.WaitAsync(CancellationToken.None); - SendMessage(message); + try + { + MessageQueue.Clear(); + } + finally + { + messageQueueLocker.Release(); + } - Thread.Sleep(MessageQueueDelay); + sendQueueExited = true; + } } - - lock (messageQueueLocker) + catch (Exception ex) { - MessageQueue.Clear(); + PreStartup.HandleException(ex); } - - sendQueueExited = true; } /// /// Sends a PING message to the server to indicate that we're still connected. /// - /// Just a dummy parameter so that this matches the delegate System.Threading.TimerCallback. - private void AutoPing(object data) + private async Task AutoPingAsync() { - SendMessage("PING LAG" + new Random().Next(100000, 999999)); + try + { + await SendMessageAsync("PING LAG" + new Random().Next(100000, 999999)); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } /// /// Registers the user. /// - private void Register() + private async Task RegisterAsync() { if (welcomeMessageReceived) return; @@ -923,27 +932,27 @@ private void Register() string realname = ProgramConstants.GAME_VERSION + " " + defaultGame + " CnCNet"; - SendMessage(string.Format("USER {0} 0 * :{1}", defaultGame + "." + + await SendMessageAsync(string.Format("USER {0} 0 * :{1}", defaultGame + "." + systemId, realname)); - SendMessage("NICK " + ProgramConstants.PLAYERNAME); + await SendMessageAsync("NICK " + ProgramConstants.PLAYERNAME); } - public void ChangeNickname() + public Task ChangeNicknameAsync() { - SendMessage("NICK " + ProgramConstants.PLAYERNAME); + return SendMessageAsync("NICK " + ProgramConstants.PLAYERNAME); } - public void QueueMessage(QueuedMessageType type, int priority, string message, bool replace = false) + public Task QueueMessageAsync(QueuedMessageType type, int priority, string message, bool replace = false) { QueuedMessage qm = new QueuedMessage(message, type, priority, replace); - QueueMessage(qm); + return QueueMessageAsync(qm); } - public void QueueMessage(QueuedMessageType type, int priority, int delay, string message) + public async Task QueueMessageAsync(QueuedMessageType type, int priority, int delay, string message) { QueuedMessage qm = new QueuedMessage(message, type, priority, delay); - QueueMessage(qm); + await QueueMessageAsync(qm); Logger.Log("Setting delay to " + delay + "ms for " + qm.ID); } @@ -951,29 +960,38 @@ public void QueueMessage(QueuedMessageType type, int priority, int delay, string /// Send a message to the CnCNet server. /// /// The message to send. - private void SendMessage(string message) + private async Task SendMessageAsync(string message) { - if (serverStream == null) + if (!socket?.Connected ?? false) return; Logger.Log("SRM: " + message); - byte[] buffer = encoding.GetBytes(message + "\r\n"); - if (serverStream.CanWrite) +#if NETFRAMEWORK + byte[] buffer1 = encoding.GetBytes(message + "\r\n"); + var buffer = new ArraySegment(buffer1); + + try { - try - { - serverStream.Write(buffer, 0, buffer.Length); - serverStream.Flush(); - } - catch (IOException ex) - { - Logger.Log("Sending message to the server failed! Reason: " + ex.Message); - } + await socket.SendAsync(buffer, SocketFlags.None); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); + Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bytes = encoding.GetBytes((message + "\r\n").AsSpan(), buffer.Span); + buffer = buffer[..bytes]; + + try + { + await socket.SendAsync(buffer, SocketFlags.None, CancellationToken.None); +#endif + } + catch (IOException ex) + { + PreStartup.LogException(ex, "Sending message to the server failed!"); } } - private int NextQueueID { get; set; } = 0; + private int NextQueueID { get; set; } /// /// This will attempt to replace a previously queued message of the same type. @@ -982,7 +1000,9 @@ private void SendMessage(string message) /// Whether or not a replace occurred private bool ReplaceMessage(QueuedMessage qm) { - lock (messageQueueLocker) + messageQueueLocker.Wait(); + + try { var previousMessageIndex = MessageQueue.FindIndex(m => m.MessageType == qm.MessageType); if (previousMessageIndex == -1) @@ -991,14 +1011,17 @@ private bool ReplaceMessage(QueuedMessage qm) MessageQueue[previousMessageIndex] = qm; return true; } + finally + { + messageQueueLocker.Release(); + } } /// /// Adds a message to the send queue. /// /// The message to queue. - /// If true, attempt to replace a previous message of the same type - public void QueueMessage(QueuedMessage qm) + public async Task QueueMessageAsync(QueuedMessage qm) { if (!_isConnected) return; @@ -1008,7 +1031,9 @@ public void QueueMessage(QueuedMessage qm) qm.ID = NextQueueID++; - lock (messageQueueLocker) + await messageQueueLocker.WaitAsync(); + + try { switch (qm.MessageType) { @@ -1025,7 +1050,7 @@ public void QueueMessage(QueuedMessage qm) AddSpecialQueuedMessage(qm); break; case QueuedMessageType.INSTANT_MESSAGE: - SendMessage(qm.Command); + await SendMessageAsync(qm.Command); break; default: int placeInQueue = MessageQueue.FindIndex(m => m.Priority < qm.Priority); @@ -1037,6 +1062,11 @@ public void QueueMessage(QueuedMessage qm) MessageQueue.Insert(placeInQueue, qm); break; } + + } + finally + { + messageQueueLocker.Release(); } } diff --git a/DXMainClient/Online/IConnectionManager.cs b/DXMainClient/Online/IConnectionManager.cs index f283245ab..538348195 100644 --- a/DXMainClient/Online/IConnectionManager.cs +++ b/DXMainClient/Online/IConnectionManager.cs @@ -71,22 +71,6 @@ public interface IConnectionManager void OnConnected(); - bool GetDisconnectStatus(); - void OnServerLatencyTested(int candidateCount, int closerCount); - - //public EventHandler WelcomeMessageReceived; - //public EventHandler GenericServerMessageReceived; - //public EventHandler AwayMessageReceived; - //public EventHandler ChannelTopicReceived; - //public EventHandler UserListReceived; - //public EventHandler WhoReplyReceived; - //public EventHandler ChannelFull; - //public EventHandler IncorrectChannelPassword; - - //public event EventHandler AttemptedServerChanged; - //public event EventHandler ConnectAttemptFailed; - //public event EventHandler ConnectionLost; - //public event EventHandler ReconnectAttempt; } -} +} \ No newline at end of file diff --git a/DXMainClient/PreStartup.cs b/DXMainClient/PreStartup.cs index 2e25c91bb..f48fb36e8 100644 --- a/DXMainClient/PreStartup.cs +++ b/DXMainClient/PreStartup.cs @@ -56,9 +56,9 @@ public static void Initialize(StartupParams parameters) #if WINFORMS Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException); - Application.ThreadException += (sender, args) => HandleException(sender, args.Exception); + Application.ThreadException += (_, args) => HandleException(args.Exception); #endif - AppDomain.CurrentDomain.UnhandledException += (sender, args) => HandleException(sender, (Exception)args.ExceptionObject); + AppDomain.CurrentDomain.UnhandledException += (_, args) => HandleException((Exception)args.ExceptionObject); DirectoryInfo gameDirectory = SafePath.GetDirectory(ProgramConstants.GamePath); @@ -132,7 +132,7 @@ public static void Initialize(StartupParams parameters) } catch (Exception ex) { - Logger.Log("Failed to load the translation file. " + ex.Message); + LogException(ex, "Failed to load the translation file."); Translation.Instance = new Translation(UserINISettings.Instance.Translation); } @@ -163,7 +163,7 @@ public static void Initialize(StartupParams parameters) } catch (Exception ex) { - Logger.Log("Failed to generate the translation stub: " + ex.Message); + LogException(ex, "Failed to generate the translation stub."); } // Delete obsolete files from old target project versions @@ -194,10 +194,20 @@ public static void Initialize(StartupParams parameters) new Startup().Execute(); } - public static void LogException(Exception ex, bool innerException = false) + /// + /// Logs all details of an exception to the logfile without further action. + /// + /// The to log. + /// /// Optional message to accompany the error. + public static void LogException(Exception ex, string message = null) + { + LogExceptionRecursive(ex, message); + } + + private static void LogExceptionRecursive(Exception ex, string message = null, bool innerException = false) { if (!innerException) - Logger.Log("KABOOOOOOM!!! Info:"); + Logger.Log(message); else Logger.Log("InnerException info:"); @@ -207,13 +217,26 @@ public static void LogException(Exception ex, bool innerException = false) Logger.Log("TargetSite.Name: " + ex.TargetSite.Name); Logger.Log("Stacktrace: " + ex.StackTrace); - if (ex.InnerException is not null) - LogException(ex.InnerException, true); + if (ex is AggregateException aggregateException) + { + foreach (Exception aggregateExceptionInnerException in aggregateException.InnerExceptions) + { + LogExceptionRecursive(aggregateExceptionInnerException, null, true); + } + } + else if (ex.InnerException is not null) + { + LogExceptionRecursive(ex.InnerException, null, true); + } } - static void HandleException(object sender, Exception ex) + /// + /// Logs all details of an exception to the logfile, notifies the user, and exits the application. + /// + /// The to log. + public static void HandleException(Exception ex) { - LogException(ex); + LogExceptionRecursive(ex, "KABOOOOOOM!!! Info:"); string errorLogPath = SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, "ClientCrashLogs", FormattableString.Invariant($"ClientCrashLog{DateTime.Now.ToString("_yyyy_MM_dd_HH_mm")}.txt")); bool crashLogCopied = false; diff --git a/DXMainClient/Startup.cs b/DXMainClient/Startup.cs index c268ec00a..69d0e6e63 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -268,7 +268,7 @@ private static void CheckSystemSpecifications() { searcher = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController"); - foreach (ManagementObject mo in searcher.Get()) + foreach (ManagementObject mo in searcher.Get().Cast()) { var currentBitsPerPixel = mo.Properties["CurrentBitsPerPixel"]; var description = mo.Properties["Description"]; @@ -289,7 +289,7 @@ private static void CheckSystemSpecifications() searcher = new ManagementObjectSearcher("Select * From Win32_PhysicalMemory"); ulong total = 0; - foreach (ManagementObject ram in searcher.Get()) + foreach (ManagementObject ram in searcher.Get().Cast()) { total += Convert.ToUInt64(ram.GetPropertyValue("Capacity")); } @@ -323,14 +323,14 @@ private static async Task GenerateOnlineIdAsync() mbsList = mbs.Get(); string cpuid = ""; - foreach (ManagementObject mo in mbsList) + foreach (ManagementObject mo in mbsList.Cast()) cpuid = mo["ProcessorID"].ToString(); ManagementObjectSearcher mos = new ManagementObjectSearcher("SELECT * FROM Win32_BaseBoard"); var moc = mos.Get(); string mbid = ""; - foreach (ManagementObject mo in moc) + foreach (ManagementObject mo in moc.Cast()) mbid = (string)mo["SerialNumber"]; string sid = new SecurityIdentifier((byte[])new DirectoryEntry(string.Format("WinNT://{0},Computer", Environment.MachineName)).Children.Cast().First().InvokeGet("objectSID"), 0).AccountDomainSid.Value; @@ -344,7 +344,7 @@ private static async Task GenerateOnlineIdAsync() Random rn = new Random(); using RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\" + ClientConfiguration.Instance.InstallationPathRegKey); - string str = rn.Next(Int32.MaxValue - 1).ToString(); + string str = rn.Next(int.MaxValue - 1).ToString(); try { From 5d895ac13d2d54e19f43e643055e81186bf0d817 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 21 Aug 2022 00:47:07 +0200 Subject: [PATCH 021/109] LAN connection fixes --- .../Multiplayer/GameLobby/LANGameLobby.cs | 1 + .../GameLobby/MultiplayerGameLobby.cs | 3 +-- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 25 +++++++++++++------ DXMainClient/PreStartup.cs | 2 +- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index 0fa444a39..b5dc5a192 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -555,6 +555,7 @@ public override async Task ClearAsync() await BroadcastMessageAsync(PLAYER_QUIT_COMMAND); Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p)); Players.Clear(); + cancellationTokenSource.Cancel(); listener.Close(); } else diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index e0b2484c1..9a0aaa2a2 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -127,8 +127,7 @@ public override void Initialize() base.Initialize(); // DisableSpectatorReadyChecking = GameOptionsIni.GetBooleanValue("General", "DisableSpectatorReadyChecking", false); - - PingTextures = new Texture2D[5] + PingTextures = new[] { AssetLoader.LoadTexture("ping0.png"), AssetLoader.LoadTexture("ping1.png"), diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 95efb93df..f9635f9df 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -127,7 +127,7 @@ public override void Initialize() btnMainMenu.ClientRectangle = new Rectangle(Width - 145, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnMainMenu.Text = "Main Menu".L10N("Client:Main:MainMenu"); - btnMainMenu.LeftClick += (_, _) => BtnMainMenu_LeftClickAsync(cancellationTokenSource?.Token ?? default); + btnMainMenu.LeftClick += (_, _) => BtnMainMenu_LeftClickAsync(); lbGameList = new GameListBox(WindowManager, mapLoader, localGame); lbGameList.Name = "lbGameList"; @@ -274,15 +274,19 @@ private async Task WindowManager_GameClosingAsync(CancellationToken cancellation if (socket.IsBound) { +#if NETFRAMEWORK try { - await SendMessageAsync("QUIT", cancellationToken); - cancellationTokenSource.Cancel(); - socket.Close(); +#endif + await SendMessageAsync("QUIT", cancellationToken); + cancellationTokenSource.Cancel(); + socket.Close(); +#if NETFRAMEWORK } catch (ObjectDisposedException) { } +#endif } } catch (Exception ex) @@ -434,7 +438,7 @@ private async Task ListenAsync(CancellationToken cancellationToken) Memory buffer = memoryOwner.Memory[..4096]; SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep, cancellationToken); #endif - var iep = (IPEndPoint)ep; + var iep = (IPEndPoint)socketReceiveFromResult.RemoteEndPoint; #if NETFRAMEWORK string data = encoding.GetString(buffer1, 0, socketReceiveFromResult.ReceivedBytes); #else @@ -627,7 +631,11 @@ private async Task LbGameList_DoubleLeftClickAsync() try { using var client = new Socket(SocketType.Stream, ProtocolType.Tcp); - client.Bind(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT)); +#if NETFRAMEWORK + await client.ConnectAsync(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT)); +#else + await client.ConnectAsync(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT), CancellationToken.None); +#endif if (hg.IsLoadedGame) { @@ -693,13 +701,14 @@ private async Task LbGameList_DoubleLeftClickAsync() } } - private async Task BtnMainMenu_LeftClickAsync(CancellationToken cancellationToken) + private async Task BtnMainMenu_LeftClickAsync() { try { Visible = false; Enabled = false; - await SendMessageAsync("QUIT", cancellationToken); + await SendMessageAsync("QUIT", CancellationToken.None); + cancellationTokenSource.Cancel(); socket.Close(); Exited?.Invoke(this, EventArgs.Empty); } diff --git a/DXMainClient/PreStartup.cs b/DXMainClient/PreStartup.cs index f48fb36e8..bc4772ba8 100644 --- a/DXMainClient/PreStartup.cs +++ b/DXMainClient/PreStartup.cs @@ -214,7 +214,7 @@ private static void LogExceptionRecursive(Exception ex, string message = null, b Logger.Log("Type: " + ex.GetType()); Logger.Log("Message: " + ex.Message); Logger.Log("Source: " + ex.Source); - Logger.Log("TargetSite.Name: " + ex.TargetSite.Name); + Logger.Log("TargetSite.Name: " + ex.TargetSite?.Name); Logger.Log("Stacktrace: " + ex.StackTrace); if (ex is AggregateException aggregateException) From f559825197d160a705ead3ebaf1024ac11c51c0c Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 21 Aug 2022 04:17:51 +0200 Subject: [PATCH 022/109] Fix LAN game connections --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 5 ++-- .../Multiplayer/GameLobby/GameLobbyBase.cs | 3 ++- .../Multiplayer/GameLobby/LANGameLobby.cs | 24 ++++++++++--------- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 12 +++++----- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 4 ++-- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 2 +- DXMainClient/Domain/Multiplayer/PlayerInfo.cs | 2 +- 7 files changed, 28 insertions(+), 24 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 8894ad087..2e1e28936 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -16,6 +16,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Text; using System.Threading.Tasks; using DTAClient.Domain.Multiplayer.CnCNet; @@ -985,10 +986,10 @@ private void AbortGameStart() protected override string GetIPAddressForPlayer(PlayerInfo player) { if (isP2P) - return player.IPAddress; + return IPAddress.Parse(player.IPAddress).MapToIPv4().ToString(); if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) - return "127.0.0.1"; + return IPAddress.Loopback.MapToIPv4().ToString(); return base.GetIPAddressForPlayer(player); } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 3bbfb4a5c..0f715cdd4 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Threading.Tasks; using ClientCore.Enums; using DTAClient.DXGUI.Multiplayer.CnCNet; @@ -1545,7 +1546,7 @@ protected bool IsPlayerSpectator(PlayerInfo pInfo) return false; } - protected virtual string GetIPAddressForPlayer(PlayerInfo player) => "0.0.0.0"; + protected virtual string GetIPAddressForPlayer(PlayerInfo player) => IPAddress.Any.MapToIPv4().ToString(); /// /// Override this in a derived class to write game lobby specific code to diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index b5dc5a192..81b229658 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -185,7 +185,6 @@ public async Task SetUpAsync(bool isHost, } else { - this.client?.Dispose(); this.client = client; } @@ -201,7 +200,7 @@ public async Task PostJoinAsync() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(GameModeMaps.GameModes); - await SendMessageToHostAsync(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash(), cancellationTokenSource.Token); + await SendMessageToHostAsync(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash(), cancellationTokenSource?.Token ?? default); ResetAutoReadyCheckbox(); } @@ -560,11 +559,14 @@ public override async Task ClearAsync() } else { - await SendMessageToHostAsync(PLAYER_QUIT_COMMAND, cancellationTokenSource.Token); + await SendMessageToHostAsync(PLAYER_QUIT_COMMAND, cancellationTokenSource?.Token ?? default); } if (client.Connected) + { + cancellationTokenSource.Cancel(); client.Close(); + } ResetDiscordPresence(); } @@ -630,7 +632,7 @@ protected override async Task HostLaunchGameAsync() protected override string GetIPAddressForPlayer(PlayerInfo player) { var lpInfo = (LANPlayerInfo)player; - return lpInfo.IPAddress; + return IPAddress.Parse(lpInfo.IPAddress).MapToIPv4().ToString(); } protected override Task RequestPlayerOptionsAsync(int side, int color, int start, int team) @@ -641,12 +643,12 @@ protected override Task RequestPlayerOptionsAsync(int side, int color, int start sb.Append(color); sb.Append(start); sb.Append(team); - return SendMessageToHostAsync(sb.ToString(), cancellationTokenSource.Token); + return SendMessageToHostAsync(sb.ToString(), cancellationTokenSource?.Token ?? default); } protected override Task RequestReadyStatusAsync() { - return SendMessageToHostAsync(PLAYER_READY_REQUEST + " " + Convert.ToInt32(chkAutoReady.Checked), cancellationTokenSource.Token); + return SendMessageToHostAsync(PLAYER_READY_REQUEST + " " + Convert.ToInt32(chkAutoReady.Checked), cancellationTokenSource?.Token ?? default); } protected override Task SendChatMessageAsync(string message) @@ -655,7 +657,7 @@ protected override Task SendChatMessageAsync(string message) sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(chatColorIndex); sb.Append(message); - return SendMessageToHostAsync(sb.ToString(), cancellationTokenSource.Token); + return SendMessageToHostAsync(sb.ToString(), cancellationTokenSource?.Token ?? default); } protected override async Task OnGameOptionChangedAsync() @@ -722,7 +724,7 @@ private async Task BroadcastMessageAsync(string message, bool otherPlayersOnly = foreach (PlayerInfo pInfo in Players.Where(p => !otherPlayersOnly || p.Name != ProgramConstants.PLAYERNAME)) { var lpInfo = (LANPlayerInfo)pInfo; - await lpInfo.SendMessageAsync(message, cancellationTokenSource.Token); + await lpInfo.SendMessageAsync(message, cancellationTokenSource?.Token ?? default); } } catch (Exception ex) @@ -809,7 +811,7 @@ protected override async Task GameProcessExitedAsync() { await base.GameProcessExitedAsync(); - await SendMessageToHostAsync(RETURN_COMMAND, cancellationTokenSource.Token); + await SendMessageToHostAsync(RETURN_COMMAND, cancellationTokenSource?.Token ?? default); if (IsHost) { @@ -1074,8 +1076,8 @@ private void HandlePlayerOptionsBroadcast(string data) if (team < 0 || team > 4) return; - if (ipAddress == "127.0.0.1") - ipAddress = hostEndPoint.Address.ToString(); + if (IPAddress.IsLoopback(IPAddress.Parse(ipAddress))) + ipAddress = hostEndPoint.Address.MapToIPv4().ToString(); bool isAi = aiLevel > -1; if (aiLevel > 2) diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index ee56fe08b..77d9c2b0d 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -160,7 +160,7 @@ public async Task PostJoinAsync() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(gameModes); - await SendMessageToHostAsync(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash(), cancellationTokenSource.Token); + await SendMessageToHostAsync(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash(), cancellationTokenSource?.Token ?? default); UpdateDiscordPresence(true); } @@ -531,19 +531,19 @@ protected override async Task BroadcastOptionsAsync() sb.Append(pInfo.IPAddress); } - await BroadcastMessageAsync(sb.ToString(), cancellationTokenSource.Token); + await BroadcastMessageAsync(sb.ToString(), cancellationTokenSource?.Token ?? default); } protected override Task HostStartGameAsync() - => BroadcastMessageAsync(GAME_LAUNCH_COMMAND, cancellationTokenSource.Token); + => BroadcastMessageAsync(GAME_LAUNCH_COMMAND, cancellationTokenSource?.Token ?? default); protected override Task RequestReadyStatusAsync() - => SendMessageToHostAsync(READY_STATUS_COMMAND, cancellationTokenSource.Token); + => SendMessageToHostAsync(READY_STATUS_COMMAND, cancellationTokenSource?.Token ?? default); protected override async Task SendChatMessageAsync(string message) { await SendMessageToHostAsync(CHAT_COMMAND + " " + chatColorIndex + - ProgramConstants.LAN_DATA_SEPARATOR + message, cancellationTokenSource.Token); + ProgramConstants.LAN_DATA_SEPARATOR + message, cancellationTokenSource?.Token ?? default); sndMessageSound.Play(); } @@ -564,7 +564,7 @@ private async Task Server_HandleChatMessageAsync(LANPlayerInfo sender, string da await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + colorIndex + - ProgramConstants.LAN_DATA_SEPARATOR + data, cancellationTokenSource.Token); + ProgramConstants.LAN_DATA_SEPARATOR + data, cancellationTokenSource?.Token ?? default); } private void Server_HandleFileHashMessage(LANPlayerInfo sender, string hash) diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index f9635f9df..207e46ae0 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -429,7 +429,7 @@ private async Task ListenAsync(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { - EndPoint ep = new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_LOBBY_PORT); + EndPoint ep = new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT); #if NETFRAMEWORK byte[] buffer1 = new byte[4096]; var buffer = new ArraySegment(buffer1); @@ -630,7 +630,7 @@ private async Task LbGameList_DoubleLeftClickAsync() try { - using var client = new Socket(SocketType.Stream, ProtocolType.Tcp); + var client = new Socket(SocketType.Stream, ProtocolType.Tcp); #if NETFRAMEWORK await client.ConnectAsync(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT)); #else diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index 3e5d21bb8..f51e17924 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -59,7 +59,7 @@ public async Task UpdateAsync(GameTime gameTime) if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT) || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)) - await SendMessageAsync("PING", cancellationTokenSource.Token); + await SendMessageAsync("PING", cancellationTokenSource?.Token ?? default); if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT)) return false; diff --git a/DXMainClient/Domain/Multiplayer/PlayerInfo.cs b/DXMainClient/Domain/Multiplayer/PlayerInfo.cs index 43abbdd00..e87224457 100644 --- a/DXMainClient/Domain/Multiplayer/PlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/PlayerInfo.cs @@ -34,7 +34,7 @@ public PlayerInfo(string name, int sideId, int startingLocation, int colorId, in public bool IsAI { get; set; } public bool IsInGame { get; set; } - public virtual string IPAddress { get; set; } = "0.0.0.0"; + public virtual string IPAddress { get; set; } = System.Net.IPAddress.Any.ToString(); public int Port { get; set; } public bool Verified { get; set; } From 9c0a3c10dda988e766599a59edd235e987d6fdf6 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 21 Aug 2022 10:29:52 +0200 Subject: [PATCH 023/109] Prevent possible runtime errors with missing callback arguments --- DTAConfig/HotkeyConfigurationWindow.cs | 2 +- DTAConfig/OptionPanels/ComponentsPanel.cs | 4 +- .../DXGUI/Generic/CampaignSelector.cs | 2 +- .../DXGUI/Generic/GameLoadingWindow.cs | 2 +- DXMainClient/DXGUI/Generic/MainMenu.cs | 8 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 55 ++++--- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 87 ++++++++---- .../CnCNet/PrivateMessagingWindow.cs | 2 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 6 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 134 ++++++++++-------- .../Multiplayer/GameLobby/GameLobbyBase.cs | 60 ++++---- .../Multiplayer/GameLobby/LANGameLobby.cs | 14 +- .../GameLobby/MultiplayerGameLobby.cs | 19 ++- .../Multiplayer/GameLobby/SkirmishLobby.cs | 4 +- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 68 ++++++--- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 25 ++-- .../Multiplayer/CnCNet/TunnelHandler.cs | 6 +- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 29 ++-- DXMainClient/Online/CnCNetManager.cs | 57 ++++---- 19 files changed, 357 insertions(+), 227 deletions(-) diff --git a/DTAConfig/HotkeyConfigurationWindow.cs b/DTAConfig/HotkeyConfigurationWindow.cs index f80f629dc..fa655c3e3 100644 --- a/DTAConfig/HotkeyConfigurationWindow.cs +++ b/DTAConfig/HotkeyConfigurationWindow.cs @@ -288,7 +288,7 @@ private void HotkeyConfigurationWindow_EnabledChanged(object sender, EventArgs e /// private void GameProcessLogic_GameProcessExited() { - WindowManager.AddCallback(new Action(LoadKeyboardINI), null); + WindowManager.AddCallback(LoadKeyboardINI); } private void LoadKeyboardINI() diff --git a/DTAConfig/OptionPanels/ComponentsPanel.cs b/DTAConfig/OptionPanels/ComponentsPanel.cs index 4448796a3..4fe670311 100644 --- a/DTAConfig/OptionPanels/ComponentsPanel.cs +++ b/DTAConfig/OptionPanels/ComponentsPanel.cs @@ -205,7 +205,7 @@ public void InstallComponent(int id) /// The current download progress percentage. private void cc_DownloadProgressChanged(CustomComponent c, int percentage) { - WindowManager.AddCallback(new Action(HandleDownloadProgressChanged), c, percentage); + WindowManager.AddCallback(() => HandleDownloadProgressChanged(c, percentage)); } private void HandleDownloadProgressChanged(CustomComponent cc, int percentage) @@ -227,7 +227,7 @@ private void HandleDownloadProgressChanged(CustomComponent cc, int percentage) /// True if the download succeeded, otherwise false. private void cc_DownloadFinished(CustomComponent c, bool success) { - WindowManager.AddCallback(new Action(HandleDownloadFinished), c, success); + WindowManager.AddCallback(() => HandleDownloadFinished(c, success)); } private void HandleDownloadFinished(CustomComponent cc, bool success) diff --git a/DXMainClient/DXGUI/Generic/CampaignSelector.cs b/DXMainClient/DXGUI/Generic/CampaignSelector.cs index 3e182e284..c8cfee128 100644 --- a/DXMainClient/DXGUI/Generic/CampaignSelector.cs +++ b/DXMainClient/DXGUI/Generic/CampaignSelector.cs @@ -330,7 +330,7 @@ private int GetComputerDifficulty() => private void GameProcessExited_Callback() { - WindowManager.AddCallback(new Action(GameProcessExited), null); + WindowManager.AddCallback(GameProcessExited); } protected virtual void GameProcessExited() diff --git a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs index c087dd38a..56158c8c0 100644 --- a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs @@ -168,7 +168,7 @@ private void DeleteMsgBox_YesClicked(XNAMessageBox obj) private void GameProcessExited_Callback() { - WindowManager.AddCallback(new Action(GameProcessExited), null); + WindowManager.AddCallback(GameProcessExited); } protected virtual void GameProcessExited() diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 528dd4290..67a80ade1 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -305,7 +305,7 @@ public override void Initialize() cncnetPlayerCountCancellationSource = new CancellationTokenSource(); CnCNetPlayerCountTask.InitializeService(cncnetPlayerCountCancellationSource); - WindowManager.GameClosing += WindowManager_GameClosing; + WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync(); skirmishLobby.Exited += SkirmishLobby_Exited; lanLobby.Exited += LanLobby_Exited; @@ -386,7 +386,7 @@ private void SharedUILogic_GameProcessStarting() } private void Updater_Restart(object sender, EventArgs e) => - WindowManager.AddCallback(new Action(ExitClient), null); + WindowManager.AddCallback(ExitClient); /// /// Applies configuration changes (music playback and volume) @@ -501,7 +501,7 @@ private void FirstRunMessageBox_NoClicked(XNAMessageBox messageBox) private void SharedUILogic_GameProcessStarted() => MusicOff(); - private void WindowManager_GameClosing(object sender, EventArgs e) => CleanAsync(); + private Task WindowManager_GameClosingAsync() => CleanAsync(); private void SkirmishLobby_Exited(object sender, EventArgs e) { @@ -726,7 +726,7 @@ private void CheckForUpdates() } private void Updater_FileIdentifiersUpdated() - => WindowManager.AddCallback(new Action(HandleFileIdentifierUpdate), null); + => WindowManager.AddCallback(HandleFileIdentifierUpdate); /// /// Used for displaying the result of an update check in the UI. diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index c7b859f9e..2e7043d8c 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -118,8 +118,8 @@ public override void Initialize() base.Initialize(); - connectionManager.ConnectionLost += ConnectionManager_ConnectionLost; - connectionManager.Disconnected += ConnectionManager_Disconnected; + connectionManager.ConnectionLost += (_, _) => ConnectionManager_ConnectionLostAsync(); + connectionManager.Disconnected += (_, _) => ConnectionManager_DisconnectedAsync(); tunnelSelectionWindow = new TunnelSelectionWindow(WindowManager, tunnelHandler); tunnelSelectionWindow.Initialize(); @@ -142,18 +142,18 @@ public override void Initialize() gameBroadcastTimer.AutoReset = true; gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL); gameBroadcastTimer.Enabled = true; - gameBroadcastTimer.TimeElapsed += GameBroadcastTimer_TimeElapsed; + gameBroadcastTimer.TimeElapsed += (_, _) => GameBroadcastTimer_TimeElapsedAsync(); WindowManager.AddAndInitializeControl(gameBroadcastTimer); } private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:"); - private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGameAsync(); + private Task GameBroadcastTimer_TimeElapsedAsync() => BroadcastGameAsync(); - private void ConnectionManager_Disconnected(object sender, EventArgs e) => ClearAsync(); + private Task ConnectionManager_DisconnectedAsync() => ClearAsync(); - private void ConnectionManager_ConnectionLost(object sender, ConnectionLostEventArgs e) => ClearAsync(); + private Task ConnectionManager_ConnectionLostAsync() => ClearAsync(); /// /// Sets up events and information before joining the channel. @@ -286,28 +286,49 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage( private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) { - PlayerInfo pInfo = new PlayerInfo(); - pInfo.Name = e.User.IRCUser.Name; + try + { + PlayerInfo pInfo = new PlayerInfo(); + pInfo.Name = e.User.IRCUser.Name; - Players.Add(pInfo); + Players.Add(pInfo); - sndJoinSound.Play(); + sndJoinSound.Play(); - await BroadcastOptionsAsync(); - CopyPlayerDataToUI(); - UpdateDiscordPresence(); + await BroadcastOptionsAsync(); + CopyPlayerDataToUI(); + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private async Task Channel_UserLeftAsync(UserNameEventArgs e) { - await RemovePlayerAsync(e.UserName); - UpdateDiscordPresence(); + try + { + await RemovePlayerAsync(e.UserName); + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private async Task Channel_UserQuitIRCAsync(UserNameEventArgs e) { - await RemovePlayerAsync(e.UserName); - UpdateDiscordPresence(); + try + { + await RemovePlayerAsync(e.UserName); + UpdateDiscordPresence(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private async Task RemovePlayerAsync(string playerName) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 1f7ee26c5..80e5aac15 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -626,16 +626,32 @@ private async Task ConnectionManager_BannedFromChannelAsync(ChannelEventArgs e) } } - private Task SharedUILogic_GameProcessStartedAsync() + private async Task SharedUILogic_GameProcessStartedAsync() { - return connectionManager.SendCustomMessageAsync(new QueuedMessage("AWAY " + (char)58 + "In-game", - QueuedMessageType.SYSTEM_MESSAGE, 0)); + try + { + await connectionManager.SendCustomMessageAsync(new QueuedMessage("AWAY " + (char)58 + "In-game", + QueuedMessageType.SYSTEM_MESSAGE, 0)); + + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - private Task SharedUILogic_GameProcessExitedAsync() + private async Task SharedUILogic_GameProcessExitedAsync() { - return connectionManager.SendCustomMessageAsync(new QueuedMessage("AWAY", - QueuedMessageType.SYSTEM_MESSAGE, 0)); + try + { + await connectionManager.SendCustomMessageAsync(new QueuedMessage("AWAY", + QueuedMessageType.SYSTEM_MESSAGE, 0)); + + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private async Task Instance_SettingsSavedAsync() @@ -1009,14 +1025,21 @@ private async Task GameChannel_InvalidPasswordEntered_NewGameAsync(object sender private async Task GameChannel_UserAddedAsync(object sender, Online.ChannelUserEventArgs e) { - Channel gameChannel = (Channel)sender; + try + { + Channel gameChannel = (Channel)sender; - if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) + if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) + { + ClearGameChannelEvents(gameChannel); + await gameLobby.OnJoinedAsync(); + isInGameRoom = true; + SetLogOutButtonText(); + } + } + catch (Exception ex) { - ClearGameChannelEvents(gameChannel); - await gameLobby.OnJoinedAsync(); - isInGameRoom = true; - SetLogOutButtonText(); + PreStartup.HandleException(ex); } } @@ -1119,25 +1142,39 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + chann private async Task GameChannel_InvalidPasswordEntered_LoadedGameAsync(object sender) { - var channel = (Channel)sender; - channel.UserAdded -= gameLoadingChannel_UserAddedFunc; - channel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; - await gameLoadingLobby.ClearAsync(); - isJoiningGame = false; + try + { + var channel = (Channel)sender; + channel.UserAdded -= gameLoadingChannel_UserAddedFunc; + channel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; + await gameLoadingLobby.ClearAsync(); + isJoiningGame = false; + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private async Task GameLoadingChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) { - Channel gameLoadingChannel = (Channel)sender; - - if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) + try { - gameLoadingChannel.UserAdded -= gameLoadingChannel_UserAddedFunc; - gameLoadingChannel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; + Channel gameLoadingChannel = (Channel)sender; - await gameLoadingLobby.OnJoinedAsync(); - isInGameRoom = true; - isJoiningGame = false; + if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) + { + gameLoadingChannel.UserAdded -= gameLoadingChannel_UserAddedFunc; + gameLoadingChannel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; + + await gameLoadingLobby.OnJoinedAsync(); + isInGameRoom = true; + isJoiningGame = false; + } + } + catch (Exception ex) + { + PreStartup.HandleException(ex); } } diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs index 6d3681c30..22cd7f286 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs @@ -415,7 +415,7 @@ private void PlayerContextMenu_JoinUser(object sender, JoinUserEventArgs args) } private void SharedUILogic_GameProcessExited() => - WindowManager.AddCallback(new Action(HandleGameProcessExited), null); + WindowManager.AddCallback(HandleGameProcessExited); private void HandleGameProcessExited() { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index 498d80aed..9e1b1f7fe 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -174,7 +174,7 @@ public override void Initialize() btnLeaveGame.ClientRectangle = new Rectangle(Width - 145, btnLoadGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLeaveGame.Text = "Leave Game".L10N("Client:Main:LeaveGame"); - btnLeaveGame.LeftClick += BtnLeaveGame_LeftClick; + btnLeaveGame.LeftClick += (_, _) => BtnLeaveGame_LeftClickAsync(); AddChild(lblMapName); AddChild(lblMapNameValue); @@ -216,9 +216,9 @@ public override void Initialize() /// /// Resets Discord Rich Presence to default state. /// - protected void ResetDiscordPresence() => discordHandler.UpdatePresence(); + private void ResetDiscordPresence() => discordHandler.UpdatePresence(); - private void BtnLeaveGame_LeftClick(object sender, EventArgs e) => LeaveGameAsync(); + private async Task BtnLeaveGame_LeftClickAsync() => LeaveGameAsync(); protected virtual Task LeaveGameAsync() { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 2e1e28936..8fb2f0c6e 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -95,8 +95,8 @@ DiscordHandler discordHandler new NotificationHandler("CLRS", HandleNotification, () => SharedColorsNotificationAsync()), new NotificationHandler("SLOC", HandleNotification, () => SharedStartingLocationNotificationAsync()), new NotificationHandler("LCKGME", HandleNotification, () => LockGameNotificationAsync()), - new IntNotificationHandler("NVRFY", HandleIntNotification, (playerIndex) => NotVerifiedNotificationAsync(playerIndex)), - new IntNotificationHandler("INGM", HandleIntNotification, (playerIndex) => StillInGameNotificationAsync(playerIndex)), + new IntNotificationHandler("NVRFY", HandleIntNotification, playerIndex => NotVerifiedNotificationAsync(playerIndex)), + new IntNotificationHandler("INGM", HandleIntNotification, playerIndex => StillInGameNotificationAsync(playerIndex)), new StringCommandHandler(MAP_SHARING_UPLOAD_REQUEST, HandleMapUploadRequest), new StringCommandHandler(MAP_SHARING_FAIL_MESSAGE, HandleMapTransferFailMessage), new StringCommandHandler(MAP_SHARING_DOWNLOAD_REQUEST, HandleMapDownloadRequest), @@ -187,6 +187,7 @@ DiscordHandler discordHandler private EventHandler channel_UserListReceivedFunc; private EventHandler connectionManager_ConnectionLostFunc; private EventHandler connectionManager_DisconnectedFunc; + private EventHandler tunnelHandler_CurrentTunnelFunc; /// /// Set to true if host has selected invalid tunnel server. @@ -209,7 +210,7 @@ public override void Initialize() gameBroadcastTimer.AutoReset = true; gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL); gameBroadcastTimer.Enabled = false; - gameBroadcastTimer.TimeElapsed += GameBroadcastTimer_TimeElapsed; + gameBroadcastTimer.TimeElapsed += (_, _) => GameBroadcastTimer_TimeElapsedAsync(); gameStartTimer = new XNATimerControl(WindowManager); gameStartTimer.AutoReset = false; @@ -242,8 +243,9 @@ public override void Initialize() channel_UserLeftFunc = (_, e) => Channel_UserLeftAsync(e); channel_UserKickedFunc = (_, e) => Channel_UserKickedAsync(e); channel_UserListReceivedFunc = (_, _) => Channel_UserListReceivedAsync(); - connectionManager_ConnectionLostFunc = (sender, e) => ConnectionManager_ConnectionLostAsync(sender, e); - connectionManager_DisconnectedFunc = (sender, e) => ConnectionManager_DisconnectedAsync(sender, e); + connectionManager_ConnectionLostFunc = (_, _) => ConnectionManager_ConnectionLostAsync(); + connectionManager_DisconnectedFunc = (_, _) => ConnectionManager_DisconnectedAsync(); + tunnelHandler_CurrentTunnelFunc = (_, _) => TunnelHandler_CurrentTunnelPingedAsync(); PostInitialize(); } @@ -280,7 +282,7 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")); - private void GameBroadcastTimer_TimeElapsed(object sender, EventArgs e) => BroadcastGameAsync(); + private Task GameBroadcastTimer_TimeElapsedAsync() => BroadcastGameAsync(); public async Task SetUpAsync(Channel channel, bool isHost, int playerLimit, CnCNetTunnel tunnel, string hostName, bool isCustomPassword, bool isP2P) @@ -314,7 +316,7 @@ public async Task SetUpAsync(Channel channel, bool isHost, int playerLimit, } tunnelHandler.CurrentTunnel = tunnel; - tunnelHandler.CurrentTunnelPinged += TunnelHandler_CurrentTunnelPinged; + tunnelHandler.CurrentTunnelPinged += tunnelHandler_CurrentTunnelFunc; connectionManager.ConnectionLost += connectionManager_ConnectionLostFunc; connectionManager.Disconnected += connectionManager_DisconnectedFunc; @@ -322,7 +324,7 @@ public async Task SetUpAsync(Channel channel, bool isHost, int playerLimit, Refresh(isHost); } - private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e) => UpdatePingAsync(); + private Task TunnelHandler_CurrentTunnelPingedAsync() => UpdatePingAsync(); public async Task OnJoinedAsync() { @@ -463,7 +465,7 @@ public override async Task ClearAsync() tbChatInput.Text = string.Empty; tunnelHandler.CurrentTunnel = null; - tunnelHandler.CurrentTunnelPinged -= TunnelHandler_CurrentTunnelPinged; + tunnelHandler.CurrentTunnelPinged -= tunnelHandler_CurrentTunnelFunc; GameLeft?.Invoke(this, EventArgs.Empty); @@ -490,9 +492,9 @@ public async Task LeaveGameLobbyAsync() } } - private Task ConnectionManager_DisconnectedAsync(object sender, EventArgs e) => HandleConnectionLossAsync(); + private Task ConnectionManager_DisconnectedAsync() => HandleConnectionLossAsync(); - private Task ConnectionManager_ConnectionLostAsync(object sender, ConnectionLostEventArgs e) => HandleConnectionLossAsync(); + private Task ConnectionManager_ConnectionLostAsync() => HandleConnectionLossAsync(); private async Task HandleConnectionLossAsync() { @@ -521,7 +523,7 @@ private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) } } - protected override Task BtnLeaveGame_LeftClickAsync(object sender, EventArgs e) => LeaveGameLobbyAsync(); + protected override Task BtnLeaveGame_LeftClickAsync() => LeaveGameLobbyAsync(); protected override void UpdateDiscordPresence(bool resetTimer = false) { @@ -552,7 +554,7 @@ private async Task Channel_UserQuitIRCAsync(UserNameEventArgs e) { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); - await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + await BtnLeaveGame_LeftClickAsync(); } else { @@ -575,7 +577,7 @@ private async Task Channel_UserLeftAsync(UserNameEventArgs e) { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); - await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + await BtnLeaveGame_LeftClickAsync(); } else { @@ -590,24 +592,31 @@ private async Task Channel_UserLeftAsync(UserNameEventArgs e) private async Task Channel_UserKickedAsync(UserNameEventArgs e) { - if (e.UserName == ProgramConstants.PLAYERNAME) + try { - connectionManager.MainChannel.AddMessage(new ChatMessage( - ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); - await ClearAsync(); - this.Visible = false; - this.Enabled = false; - return; - } + if (e.UserName == ProgramConstants.PLAYERNAME) + { + connectionManager.MainChannel.AddMessage(new ChatMessage( + ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); + await ClearAsync(); + Visible = false; + Enabled = false; + return; + } - int index = Players.FindIndex(p => p.Name == e.UserName); + int index = Players.FindIndex(p => p.Name == e.UserName); - if (index > -1) + if (index > -1) + { + Players.RemoveAt(index); + CopyPlayerDataToUI(); + UpdateDiscordPresence(); + ClearReadyStatuses(); + } + } + catch (Exception ex) { - Players.RemoveAt(index); - CopyPlayerDataToUI(); - UpdateDiscordPresence(); - ClearReadyStatuses(); + PreStartup.HandleException(ex); } } @@ -621,7 +630,7 @@ private async Task Channel_UserListReceivedAsync() { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); - await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + await BtnLeaveGame_LeftClickAsync(); } } UpdateDiscordPresence(); @@ -938,40 +947,47 @@ private void HandleTunnelFail(string playerName) private async Task HandleTunnelConnectedAsync(string playerName) { - if (!isStartingGame) - return; - - int index = Players.FindIndex(p => p.Name == playerName); - if (index == -1) + try { - Logger.Log("HandleTunnelConnected: Couldn't find player " + playerName + "!"); - AbortGameStart(); - return; - } + if (!isStartingGame) + return; - isPlayerConnectedToTunnel[index] = true; + int index = Players.FindIndex(p => p.Name == playerName); + if (index == -1) + { + Logger.Log("HandleTunnelConnected: Couldn't find player " + playerName + "!"); + AbortGameStart(); + return; + } - if (isPlayerConnectedToTunnel.All(b => b)) - { - Logger.Log("All players are connected to the tunnel, starting game!"); - AddNotice("All players have connected to the tunnel..."); + isPlayerConnectedToTunnel[index] = true; - // Remove our own ID from the list - List ids = new List(tunnelPlayerIds); - ids.Remove(tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); - List players = new List(Players); - int myIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); - players.RemoveAt(myIndex); - Tuple ports = gameTunnelHandler.CreatePlayerConnections(ids); - for (int i = 0; i < ports.Item1.Length; i++) + if (isPlayerConnectedToTunnel.All(b => b)) { - players[i].Port = ports.Item1[i]; - } + Logger.Log("All players are connected to the tunnel, starting game!"); + AddNotice("All players have connected to the tunnel..."); + + // Remove our own ID from the list + List ids = new List(tunnelPlayerIds); + ids.Remove(tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); + List players = new List(Players); + int myIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); + players.RemoveAt(myIndex); + Tuple ports = gameTunnelHandler.CreatePlayerConnections(ids); + for (int i = 0; i < ports.Item1.Length; i++) + { + players[i].Port = ports.Item1[i]; + } - Players.Single(p => p.Name == ProgramConstants.PLAYERNAME).Port = ports.Item2; - gameStartTimer.Pause(); - btnLaunchGame.InputEnabled = true; - await StartGameAsync(); + Players.Single(p => p.Name == ProgramConstants.PLAYERNAME).Port = ports.Item2; + gameStartTimer.Pause(); + btnLaunchGame.InputEnabled = true; + await StartGameAsync(); + } + } + catch (Exception ex) + { + PreStartup.HandleException(ex); } } @@ -1178,11 +1194,11 @@ protected override Task BroadcastPlayerOptionsAsync() return channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_PLAYERS_MESSAGE, 11); } - protected override async Task PlayerExtraOptions_OptionsChangedAsync(object sender, EventArgs e) + protected override async Task PlayerExtraOptions_OptionsChangedAsync() { try { - await base.PlayerExtraOptions_OptionsChangedAsync(sender, e); + await base.PlayerExtraOptions_OptionsChangedAsync(); await BroadcastPlayerExtraOptionsAsync(); } catch (Exception ex) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 0f715cdd4..55995651a 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -159,11 +159,11 @@ protected GameModeMap GameModeMap protected List RandomSelectors = new List(); - private readonly bool isMultiplayer = false; + private readonly bool isMultiplayer; private MatchStatistics matchStatistics; - private bool disableGameOptionUpdateBroadcast = false; + private bool disableGameOptionUpdateBroadcast; protected EventHandler MultiplayerNameRightClicked; @@ -171,7 +171,7 @@ protected GameModeMap GameModeMap /// If set, the client will remove all starting waypoints from the map /// before launching it. /// - protected bool RemoveStartingLocations { get; set; } = false; + protected bool RemoveStartingLocations { get; set; } protected IniFile GameOptionsIni { get; private set; } protected XNAClientButton BtnSaveLoadGameOptions { get; set; } @@ -185,9 +185,6 @@ protected GameModeMap GameModeMap public override void Initialize() { Name = _iniSectionName; - //if (WindowManager.RenderResolutionY < 800) - // ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX, WindowManager.RenderResolutionY); - //else ClientRectangle = new Rectangle(0, 0, WindowManager.RenderResolutionX - 60, WindowManager.RenderResolutionY - 32); WindowManager.CenterControlOnScreen(this); BackgroundTexture = AssetLoader.LoadTexture("gamelobbybg.png"); @@ -209,10 +206,10 @@ public override void Initialize() PlayerOptionsPanel = FindChild(nameof(PlayerOptionsPanel)); btnLeaveGame = FindChild(nameof(btnLeaveGame)); - btnLeaveGame.LeftClick += (sender, e) => BtnLeaveGame_LeftClickAsync(sender, e); + btnLeaveGame.LeftClick += (_, _) => BtnLeaveGame_LeftClickAsync(); btnLaunchGame = FindChild(nameof(btnLaunchGame)); - btnLaunchGame.LeftClick += (sender, e) => BtnLaunchGame_LeftClickAsync(sender, e); + btnLaunchGame.LeftClick += (_, _) => BtnLaunchGame_LeftClickAsync(); btnLaunchGame.InitStarDisplay(RankTextures); MapPreviewBox = FindChild("MapPreviewBox"); @@ -278,7 +275,7 @@ public override void Initialize() tbMapSearch.InputReceived += TbMapSearch_InputReceived; btnPickRandomMap = FindChild(nameof(btnPickRandomMap)); - btnPickRandomMap.LeftClick += BtnPickRandomMap_LeftClick; + btnPickRandomMap.LeftClick += (_, _) => BtnPickRandomMap_LeftClickAsync(); CheckBoxes.ForEach(chk => chk.CheckedChanged += (sender, _) => ChkBox_CheckedChangedAsync(sender)); DropDowns.ForEach(dd => dd.SelectedIndexChanged += (sender, _) => Dropdown_SelectedIndexChangedAsync(sender)); @@ -423,7 +420,7 @@ protected async Task HandleGameOptionPresetLoadCommandAsync(string presetName) protected abstract void AddNotice(string message, Color color); - private void BtnPickRandomMap_LeftClick(object sender, EventArgs e) => PickRandomMapAsync(); + private async Task BtnPickRandomMap_LeftClickAsync() => PickRandomMapAsync(); private void TbMapSearch_InputReceived(object sender, EventArgs e) => ListMaps(); @@ -472,17 +469,24 @@ protected virtual Task OnGameOptionChangedAsync() protected async Task DdGameModeMapFilter_SelectedIndexChangedAsync() { - gameModeMapFilter = ddGameModeMapFilter.SelectedItem.Tag as GameModeMapFilter; + try + { + gameModeMapFilter = ddGameModeMapFilter.SelectedItem.Tag as GameModeMapFilter; - tbMapSearch.Text = string.Empty; - tbMapSearch.OnSelectedChanged(); + tbMapSearch.Text = string.Empty; + tbMapSearch.OnSelectedChanged(); - ListMaps(); + ListMaps(); - if (lbGameModeMapList.SelectedIndex == -1) - lbGameModeMapList.SelectedIndex = 0; // Select default GameModeMap - else - await ChangeMapAsync(GameModeMap); + if (lbGameModeMapList.SelectedIndex == -1) + lbGameModeMapList.SelectedIndex = 0; // Select default GameModeMap + else + await ChangeMapAsync(GameModeMap); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } protected void BtnPlayerExtraOptions_LeftClick(object sender, EventArgs e) @@ -852,7 +856,7 @@ protected void InitPlayerOptionDropdowns() ddPlayerName.AddItem(String.Empty); ProgramConstants.AI_PLAYER_NAMES.ForEach(ddPlayerName.AddItem); ddPlayerName.AllowDropDown = true; - ddPlayerName.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); + ddPlayerName.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); ddPlayerName.RightClick += MultiplayerName_RightClick; ddPlayerName.Tag = true; @@ -871,7 +875,7 @@ protected void InitPlayerOptionDropdowns() AddSideToDropDown(ddPlayerSide, sideName); ddPlayerSide.AllowDropDown = false; - ddPlayerSide.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); + ddPlayerSide.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); ddPlayerSide.Tag = true; var ddPlayerColor = new XNAClientDropDown(WindowManager); @@ -883,7 +887,7 @@ protected void InitPlayerOptionDropdowns() foreach (MultiplayerColor mpColor in MPColors) ddPlayerColor.AddItem(mpColor.Name, mpColor.XnaColor); ddPlayerColor.AllowDropDown = false; - ddPlayerColor.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); + ddPlayerColor.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); ddPlayerColor.Tag = false; var ddPlayerTeam = new XNAClientDropDown(WindowManager); @@ -894,7 +898,7 @@ protected void InitPlayerOptionDropdowns() ddPlayerTeam.AddItem("-"); ProgramConstants.TEAMS.ForEach(ddPlayerTeam.AddItem); ddPlayerTeam.AllowDropDown = false; - ddPlayerTeam.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); + ddPlayerTeam.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); ddPlayerTeam.Tag = true; var ddPlayerStart = new XNAClientDropDown(WindowManager); @@ -905,7 +909,7 @@ protected void InitPlayerOptionDropdowns() for (int j = 1; j < 9; j++) ddPlayerStart.AddItem(j.ToString()); ddPlayerStart.AllowDropDown = false; - ddPlayerStart.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); + ddPlayerStart.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); ddPlayerStart.Visible = false; ddPlayerStart.Enabled = false; ddPlayerStart.Tag = true; @@ -949,7 +953,7 @@ protected void InitPlayerOptionDropdowns() { PlayerExtraOptionsPanel = FindChild(nameof(PlayerExtraOptionsPanel)); PlayerExtraOptionsPanel.Disable(); - PlayerExtraOptionsPanel.OptionsChanged += (sender, e) => PlayerExtraOptions_OptionsChangedAsync(sender, e); + PlayerExtraOptionsPanel.OptionsChanged += (_, _) => PlayerExtraOptions_OptionsChangedAsync(); btnPlayerExtraOptionsOpen.LeftClick += BtnPlayerExtraOptions_LeftClick; } @@ -968,7 +972,7 @@ private XNALabel GeneratePlayerOptionCaption(string name, string text, int x, in return label; } - protected virtual Task PlayerExtraOptions_OptionsChangedAsync(object sender, EventArgs e) + protected virtual Task PlayerExtraOptions_OptionsChangedAsync() { try { @@ -1058,9 +1062,9 @@ private void GetRandomSelectors(List selectorNames, List selector } } - protected abstract Task BtnLaunchGame_LeftClickAsync(object sender, EventArgs e); + protected abstract Task BtnLaunchGame_LeftClickAsync(); - protected abstract Task BtnLeaveGame_LeftClickAsync(object sender, EventArgs e); + protected abstract Task BtnLeaveGame_LeftClickAsync(); /// /// Updates Discord Rich Presence with actual information. @@ -1878,7 +1882,7 @@ protected virtual Task GameProcessExitedAsync() /// "Copies" player information from the UI to internal memory, /// applying users' player options changes. /// - protected virtual async Task CopyPlayerDataFromUIAsync(object sender, EventArgs e) + protected virtual async Task CopyPlayerDataFromUIAsync(object sender) { try { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index 81b229658..db4730a2c 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -355,7 +355,7 @@ private async Task AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancel lpInfo.ConnectionLost += lpInfo_ConnectionLostFunc; AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); - lpInfo.StartReceiveLoop(cancellationToken); + lpInfo.StartReceiveLoopAsync(cancellationToken); CopyPlayerDataToUI(); await BroadcastPlayerOptionsAsync(); @@ -456,7 +456,7 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation catch (Exception ex) { Logger.Log("Reading data from the server failed! Message: " + ex.Message); - await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + await BtnLeaveGame_LeftClickAsync(); break; } @@ -494,7 +494,7 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation } Logger.Log("Reading data from the server failed (0 bytes received)!"); - await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + await BtnLeaveGame_LeftClickAsync(); break; } } @@ -512,7 +512,7 @@ private void HandleMessageFromServer(string message) Logger.Log("Unknown LAN command from the server: " + message); } - protected override async Task BtnLeaveGame_LeftClickAsync(object sender, EventArgs e) + protected override async Task BtnLeaveGame_LeftClickAsync() { try { @@ -733,11 +733,11 @@ private async Task BroadcastMessageAsync(string message, bool otherPlayersOnly = } } - protected override async Task PlayerExtraOptions_OptionsChangedAsync(object sender, EventArgs e) + protected override async Task PlayerExtraOptions_OptionsChangedAsync() { try { - await base.PlayerExtraOptions_OptionsChangedAsync(sender, e); + await base.PlayerExtraOptions_OptionsChangedAsync(); await BroadcastPlayerExtraOptionsAsync(); } catch (Exception ex) @@ -878,7 +878,7 @@ public override void Update(GameTime gameTime) timeSinceLastReceivedCommand += gameTime.ElapsedGameTime; if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT)) - Task.Run(() => BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty)).Wait(); + Task.Run(BtnLeaveGame_LeftClickAsync).Wait(); } base.Update(gameTime); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 9a0aaa2a2..5d00eb039 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -407,10 +407,17 @@ private async Task TbChatInput_EnterPressedAsync() } } - private Task ChkAutoReady_CheckedChangedAsync() + private async Task ChkAutoReady_CheckedChangedAsync() { - UpdateLaunchGameButtonStatus(); - return RequestReadyStatusAsync(); + try + { + UpdateLaunchGameButtonStatus(); + await RequestReadyStatusAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } protected void ResetAutoReadyCheckbox() @@ -818,7 +825,7 @@ private async Task MapPreviewBox_StartingLocationAppliedAsync() /// launches the game if it's allowed. If the local player isn't the game host, /// sends a ready request. /// - protected override async Task BtnLaunchGame_LeftClickAsync(object sender, EventArgs e) + protected override async Task BtnLaunchGame_LeftClickAsync() { try { @@ -1124,7 +1131,7 @@ protected override async Task OnGameOptionChangedAsync() protected abstract Task HostLaunchGameAsync(); - protected override async Task CopyPlayerDataFromUIAsync(object sender, EventArgs e) + protected override async Task CopyPlayerDataFromUIAsync(object sender) { try { @@ -1133,7 +1140,7 @@ protected override async Task CopyPlayerDataFromUIAsync(object sender, EventArgs if (IsHost) { - await base.CopyPlayerDataFromUIAsync(sender, e); + await base.CopyPlayerDataFromUIAsync(sender); await BroadcastPlayerOptionsAsync(); return; } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs index c26d16456..cf52b520e 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -166,7 +166,7 @@ private string CheckGameValidity() return null; } - protected override async Task BtnLaunchGame_LeftClickAsync(object sender, EventArgs e) + protected override async Task BtnLaunchGame_LeftClickAsync() { try { @@ -187,7 +187,7 @@ protected override async Task BtnLaunchGame_LeftClickAsync(object sender, EventA } } - protected override Task BtnLeaveGame_LeftClickAsync(object sender, EventArgs e) + protected override Task BtnLeaveGame_LeftClickAsync() { try { diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index 77d9c2b0d..2afb44be1 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -67,8 +67,15 @@ DiscordHandler discordHandler private async Task WindowManager_GameClosingAsync() { - if (client is { Connected: true }) - await ClearAsync(); + try + { + if (client is { Connected: true }) + await ClearAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } public event EventHandler GameBroadcast; @@ -321,7 +328,7 @@ private async Task AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancel sndJoinSound.Play(); AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); - lpInfo.StartReceiveLoop(cancellationToken); + lpInfo.StartReceiveLoopAsync(cancellationToken); CopyPlayerDataToUI(); await BroadcastOptionsAsync(); @@ -537,8 +544,17 @@ protected override async Task BroadcastOptionsAsync() protected override Task HostStartGameAsync() => BroadcastMessageAsync(GAME_LAUNCH_COMMAND, cancellationTokenSource?.Token ?? default); - protected override Task RequestReadyStatusAsync() - => SendMessageToHostAsync(READY_STATUS_COMMAND, cancellationTokenSource?.Token ?? default); + protected override async Task RequestReadyStatusAsync() + { + try + { + await SendMessageToHostAsync(READY_STATUS_COMMAND, cancellationTokenSource?.Token ?? default); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + } protected override async Task SendChatMessageAsync(string message) { @@ -552,19 +568,26 @@ await SendMessageToHostAsync(CHAT_COMMAND + " " + chatColorIndex + private async Task Server_HandleChatMessageAsync(LANPlayerInfo sender, string data) { - string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); + try + { + string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); - if (parts.Length < 2) - return; + if (parts.Length < 2) + return; - int colorIndex = Conversions.IntFromString(parts[0], -1); + int colorIndex = Conversions.IntFromString(parts[0], -1); - if (colorIndex < 0 || colorIndex >= chatColors.Length) - return; + if (colorIndex < 0 || colorIndex >= chatColors.Length) + return; - await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + - ProgramConstants.LAN_DATA_SEPARATOR + colorIndex + - ProgramConstants.LAN_DATA_SEPARATOR + data, cancellationTokenSource?.Token ?? default); + await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + + ProgramConstants.LAN_DATA_SEPARATOR + colorIndex + + ProgramConstants.LAN_DATA_SEPARATOR + data, cancellationTokenSource?.Token ?? default); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void Server_HandleFileHashMessage(LANPlayerInfo sender, string hash) @@ -672,13 +695,20 @@ private void Client_HandleStartCommand() /// The command to send. private async Task BroadcastMessageAsync(string message, CancellationToken cancellationToken) { - if (!IsHost) - return; + try + { + if (!IsHost) + return; - foreach (PlayerInfo pInfo in Players) + foreach (PlayerInfo pInfo in Players) + { + var lpInfo = (LANPlayerInfo)pInfo; + await lpInfo.SendMessageAsync(message, cancellationToken); + } + } + catch (Exception ex) { - var lpInfo = (LANPlayerInfo)pInfo; - await lpInfo.SendMessageAsync(message, cancellationToken); + PreStartup.HandleException(ex); } } diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 207e46ae0..346a2c051 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -278,9 +278,9 @@ private async Task WindowManager_GameClosingAsync(CancellationToken cancellation try { #endif - await SendMessageAsync("QUIT", cancellationToken); - cancellationTokenSource.Cancel(); - socket.Close(); + await SendMessageAsync("QUIT", cancellationToken); + cancellationTokenSource.Cancel(); + socket.Close(); #if NETFRAMEWORK } catch (ObjectDisposedException) @@ -550,12 +550,19 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) private async Task SendAliveAsync(CancellationToken cancellationToken) { - StringBuilder sb = new StringBuilder("ALIVE "); - sb.Append(localGameIndex); - sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); - sb.Append(ProgramConstants.PLAYERNAME); - await SendMessageAsync(sb.ToString(), cancellationToken); - timeSinceAliveMessage = TimeSpan.Zero; + try + { + StringBuilder sb = new StringBuilder("ALIVE "); + sb.Append(localGameIndex); + sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); + sb.Append(ProgramConstants.PLAYERNAME); + await SendMessageAsync(sb.ToString(), cancellationToken); + timeSinceAliveMessage = TimeSpan.Zero; + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private async Task TbChatInput_EnterPressedAsync(CancellationToken cancellationToken) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 65381cd4b..0f93e4faa 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -57,13 +57,13 @@ public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) : base(w private void DoTunnelPinged(int index) { if (TunnelPinged != null) - wm.AddCallback(TunnelPinged, index); + wm.AddCallback(() => TunnelPinged(index)); } private void DoCurrentTunnelPinged() { if (CurrentTunnelPinged != null) - wm.AddCallback(CurrentTunnelPinged, this, EventArgs.Empty); + wm.AddCallback(() => CurrentTunnelPinged(this, EventArgs.Empty)); } private void ConnectionManager_Connected(object sender, EventArgs e) => Enabled = true; @@ -77,7 +77,7 @@ private async Task RefreshTunnelsAsync() try { List tunnels = await DoRefreshTunnelsAsync(); - wm.AddCallback(new Action>(HandleRefreshedTunnels), tunnels); + wm.AddCallback(() => HandleRefreshedTunnels(tunnels)); } catch (Exception ex) { diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index f51e17924..807c37f6e 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -54,17 +54,26 @@ public void SetClient(Socket client) /// True if the player is still considered connected, otherwise false. public async Task UpdateAsync(GameTime gameTime) { - TimeSinceLastReceivedMessage += gameTime.ElapsedGameTime; - TimeSinceLastSentMessage += gameTime.ElapsedGameTime; + try + { + TimeSinceLastReceivedMessage += gameTime.ElapsedGameTime; + TimeSinceLastSentMessage += gameTime.ElapsedGameTime; - if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT) - || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)) - await SendMessageAsync("PING", cancellationTokenSource?.Token ?? default); + if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT) + || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)) + await SendMessageAsync("PING", cancellationTokenSource?.Token ?? default); - if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT)) - return false; + if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT)) + return false; - return true; + return true; + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + + return false; } public override string IPAddress @@ -94,7 +103,7 @@ public async Task SendMessageAsync(string message, CancellationToken cancellatio #if NETFRAMEWORK byte[] buffer1 = encoding.GetBytes(message); var buffer = new ArraySegment(buffer1); - + try { await TcpClient.SendAsync(buffer, SocketFlags.None); @@ -127,7 +136,7 @@ public override string ToString() /// /// Starts receiving messages from the player asynchronously. /// - public void StartReceiveLoop(CancellationToken cancellationToken) + public async Task StartReceiveLoopAsync(CancellationToken cancellationToken) => ReceiveMessagesAsync(cancellationToken); /// diff --git a/DXMainClient/Online/CnCNetManager.cs b/DXMainClient/Online/CnCNetManager.cs index 4ed3f9edf..627e00cd1 100644 --- a/DXMainClient/Online/CnCNetManager.cs +++ b/DXMainClient/Online/CnCNetManager.cs @@ -167,7 +167,7 @@ public void OnAttemptedServerChanged(string serverName) // AddCallback is necessary for thread-safety; OnAttemptedServerChanged // is called by the networking thread, and AddCallback schedules DoAttemptedServerChanged // to be executed on the main (UI) thread. - wm.AddCallback(DoAttemptedServerChanged, serverName); + wm.AddCallback(() => DoAttemptedServerChanged(serverName)); } private void DoAttemptedServerChanged(string serverName) @@ -179,7 +179,7 @@ private void DoAttemptedServerChanged(string serverName) public void OnAwayMessageReceived(string userName, string reason) { - wm.AddCallback(DoAwayMessageReceived, userName, reason); + wm.AddCallback(() => DoAwayMessageReceived(userName, reason)); } private void DoAwayMessageReceived(string userName, string reason) @@ -189,7 +189,7 @@ private void DoAwayMessageReceived(string userName, string reason) public void OnChannelFull(string channelName) { - wm.AddCallback(DoChannelFull, channelName); + wm.AddCallback(() => DoChannelFull(channelName)); } private void DoChannelFull(string channelName) @@ -202,7 +202,7 @@ private void DoChannelFull(string channelName) public void OnTargetChangeTooFast(string channelName, string message) { - wm.AddCallback(DoTargetChangeTooFast, channelName, message); + wm.AddCallback(() => DoTargetChangeTooFast(channelName, message)); } private void DoTargetChangeTooFast(string channelName, string message) @@ -215,7 +215,7 @@ private void DoTargetChangeTooFast(string channelName, string message) public void OnChannelInviteOnly(string channelName) { - wm.AddCallback(DoChannelInviteOnly, channelName); + wm.AddCallback(() => DoChannelInviteOnly(channelName)); } private void DoChannelInviteOnly(string channelName) @@ -228,7 +228,7 @@ private void DoChannelInviteOnly(string channelName) public void OnChannelModesChanged(string userName, string channelName, string modeString, List modeParameters) { - wm.AddCallback(DoChannelModesChanged, userName, channelName, modeString, modeParameters); + wm.AddCallback(() => DoChannelModesChanged(userName, channelName, modeString, modeParameters)); } private void DoChannelModesChanged(string userName, string channelName, string modeString, List modeParameters) @@ -274,7 +274,7 @@ private void ApplyChannelModes(Channel channel, string modeString, List public void OnChannelTopicReceived(string channelName, string topic) { - wm.AddCallback(DoChannelTopicReceived, channelName, topic); + wm.AddCallback(() => DoChannelTopicReceived(channelName, topic)); } private void DoChannelTopicReceived(string channelName, string topic) @@ -289,12 +289,12 @@ private void DoChannelTopicReceived(string channelName, string topic) public void OnChannelTopicChanged(string userName, string channelName, string topic) { - wm.AddCallback(DoChannelTopicReceived, channelName, topic); + wm.AddCallback(() => DoChannelTopicReceived(channelName, topic)); } public void OnChatMessageReceived(string receiver, string senderName, string ident, string message) { - wm.AddCallback(DoChatMessageReceived, receiver, senderName, ident, message); + wm.AddCallback(() => DoChatMessageReceived(receiver, senderName, ident, message)); } private void DoChatMessageReceived(string receiver, string senderName, string ident, string message) @@ -357,7 +357,7 @@ private void DoChatMessageReceived(string receiver, string senderName, string id public void OnCTCPParsed(string channelName, string userName, string message) { - wm.AddCallback(DoCTCPParsed, channelName, userName, message); + wm.AddCallback(() => DoCTCPParsed(channelName, userName, message)); } private void DoCTCPParsed(string channelName, string userName, string message) @@ -383,7 +383,7 @@ private void DoCTCPParsed(string channelName, string userName, string message) public void OnConnectAttemptFailed() { - wm.AddCallback(DoConnectAttemptFailed, null); + wm.AddCallback(DoConnectAttemptFailed); } private void DoConnectAttemptFailed() @@ -395,7 +395,7 @@ private void DoConnectAttemptFailed() public void OnConnected() { - wm.AddCallback(DoConnected, null); + wm.AddCallback(DoConnected); } private void DoConnected() @@ -410,7 +410,7 @@ private void DoConnected() /// public void OnConnectionLost(string reason) { - wm.AddCallback(DoConnectionLost, reason); + wm.AddCallback(() => DoConnectionLost(reason)); } private void DoConnectionLost(string reason) @@ -458,7 +458,7 @@ public void Connect() /// public void OnDisconnected() { - wm.AddCallback(DoDisconnected, null); + wm.AddCallback(DoDisconnected); } private void DoDisconnected() @@ -491,7 +491,7 @@ public void OnErrorReceived(string errorMessage) public void OnGenericServerMessageReceived(string message) { - wm.AddCallback(DoGenericServerMessageReceived, message); + wm.AddCallback(() => DoGenericServerMessageReceived(message)); } private void DoGenericServerMessageReceived(string message) @@ -501,7 +501,7 @@ private void DoGenericServerMessageReceived(string message) public void OnIncorrectChannelPassword(string channelName) { - wm.AddCallback(DoIncorrectChannelPassword, channelName); + wm.AddCallback(() => DoIncorrectChannelPassword(channelName)); } private void DoIncorrectChannelPassword(string channelName) @@ -518,7 +518,7 @@ public void OnNoticeMessageParsed(string notice, string userName) public void OnPrivateMessageReceived(string sender, string message) { - wm.AddCallback(DoPrivateMessageReceived, sender, message); + wm.AddCallback(() => DoPrivateMessageReceived(sender, message)); } private void DoPrivateMessageReceived(string sender, string message) @@ -530,7 +530,7 @@ private void DoPrivateMessageReceived(string sender, string message) public void OnReconnectAttempt() { - wm.AddCallback(DoReconnectAttempt, null); + wm.AddCallback(DoReconnectAttempt); } private void DoReconnectAttempt() @@ -544,7 +544,7 @@ private void DoReconnectAttempt() public void OnUserJoinedChannel(string channelName, string host, string userName, string ident) { - wm.AddCallback(DoUserJoinedChannelAsync, channelName, host, userName, ident); + wm.AddCallback(() => DoUserJoinedChannelAsync(channelName, host, userName, ident)); } private async Task DoUserJoinedChannelAsync(string channelName, string host, string userName, string userAddress) @@ -615,7 +615,7 @@ private void AddUserToGlobalUserList(IRCUser user) public void OnUserKicked(string channelName, string userName) { - wm.AddCallback(DoUserKicked, channelName, userName); + wm.AddCallback(() => DoUserKicked(channelName, userName)); } private void DoUserKicked(string channelName, string userName) @@ -646,7 +646,7 @@ private void DoUserKicked(string channelName, string userName) public void OnUserLeftChannel(string channelName, string userName) { - wm.AddCallback(DoUserLeftChannel, channelName, userName); + wm.AddCallback(() => DoUserLeftChannel(channelName, userName)); } private void DoUserLeftChannel(string channelName, string userName) @@ -701,7 +701,7 @@ public void RemoveChannelFromUser(string userName, string channelName) public void OnUserListReceived(string channelName, string[] userList) { - wm.AddCallback(DoUserListReceived, channelName, userList); + wm.AddCallback(() => DoUserListReceived(channelName, userList)); } private void DoUserListReceived(string channelName, string[] userList) @@ -752,7 +752,7 @@ private void DoUserListReceived(string channelName, string[] userList) public void OnUserQuitIRC(string userName) { - wm.AddCallback(DoUserQuitIRC, userName); + wm.AddCallback(() => DoUserQuitIRC(userName)); } private void DoUserQuitIRC(string userName) @@ -770,10 +770,9 @@ private void DoUserQuitIRC(string userName) public void OnWelcomeMessageReceived(string message) { - wm.AddCallback(DoWelcomeMessageReceived, message); + wm.AddCallback(() => DoWelcomeMessageReceived(message)); } - /// /// Finds a channel with the specified internal name, case-insensitively. /// @@ -801,7 +800,7 @@ private void DoWelcomeMessageReceived(string message) public void OnWhoReplyReceived(string ident, string hostName, string userName, string extraInfo) { - wm.AddCallback(DoWhoReplyReceived, ident, hostName, userName, extraInfo); + wm.AddCallback(() => DoWhoReplyReceived(ident, hostName, userName, extraInfo)); } private void DoWhoReplyReceived(string ident, string hostName, string userName, string extraInfo) @@ -838,7 +837,7 @@ private void DoWhoReplyReceived(string ident, string hostName, string userName, public void OnNameAlreadyInUse() { - wm.AddCallback(DoNameAlreadyInUseAsync, null); + wm.AddCallback(DoNameAlreadyInUseAsync); } /// @@ -889,7 +888,7 @@ private async Task DoNameAlreadyInUseAsync() public void OnBannedFromChannel(string channelName) { - wm.AddCallback(DoBannedFromChannel, channelName); + wm.AddCallback(() => DoBannedFromChannel(channelName)); } private void DoBannedFromChannel(string channelName) @@ -898,7 +897,7 @@ private void DoBannedFromChannel(string channelName) } public void OnUserNicknameChange(string oldNickname, string newNickname) - => wm.AddCallback(DoUserNicknameChange, oldNickname, newNickname); + => wm.AddCallback(() => DoUserNicknameChange(oldNickname, newNickname)); private void DoUserNicknameChange(string oldNickname, string newNickname) { From 3fe7c32b6ed0988547c9b216843997dca41b1e47 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 23 Aug 2022 10:04:52 +0200 Subject: [PATCH 024/109] Cleanup --- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 8 ++--- .../DXGUI/Multiplayer/CnCNet/TunnelListBox.cs | 2 +- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 20 +++++------ .../Multiplayer/CnCNet/TunnelHandler.cs | 34 +++++++++---------- DXMainClient/Domain/Multiplayer/PlayerInfo.cs | 27 +++++++-------- 5 files changed, 41 insertions(+), 50 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 80e5aac15..0931f779c 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -876,7 +876,6 @@ private async Task JoinGameByIndexAsync(int gameIndex, string password) /// The game to join. /// The password to join with. /// The message view/list to write error messages to. - /// private async Task JoinGameAsync(HostedCnCNetGame hg, string password, IMessageView messageView) { string error = GetJoinGameError(hg); @@ -1023,7 +1022,7 @@ private async Task GameChannel_InvalidPasswordEntered_NewGameAsync(object sender } } - private async Task GameChannel_UserAddedAsync(object sender, Online.ChannelUserEventArgs e) + private async Task GameChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) { try { @@ -1085,8 +1084,7 @@ private async Task Gcw_GameCreatedAsync(GameCreationEventArgs e) bool isCustomPassword = true; if (string.IsNullOrEmpty(password)) { - password = Rampastring.Tools.Utilities.CalculateSHA1ForString( - channelName + e.GameRoomName).Substring(0, 10); + password = Utilities.CalculateSHA1ForString(channelName + e.GameRoomName).Substring(0, 10); isCustomPassword = false; } @@ -1094,7 +1092,6 @@ private async Task Gcw_GameCreatedAsync(GameCreationEventArgs e) connectionManager.AddChannel(gameChannel); await gameLobby.SetUpAsync(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword, false); gameChannel.UserAdded += gameChannel_UserAddedFunc; - //gameChannel.MessageAdded += GameChannel_MessageAdded; await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + channelName + " " + password, QueuedMessageType.INSTANT_MESSAGE, 0)); connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, @@ -1618,7 +1615,6 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr bool isLoadedGame = Conversions.BooleanFromString(splitMessage[5].Substring(3, 1), false); bool isLadder = Conversions.BooleanFromString(splitMessage[5].Substring(4, 1), false); string[] players = splitMessage[6].Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); - List playerNames = players.ToList(); string mapName = splitMessage[7]; string gameMode = splitMessage[8]; diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs index e2b8fdac1..1944f047a 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs @@ -41,7 +41,7 @@ public TunnelListBox(WindowManager windowManager, TunnelHandler tunnelHandler) : private readonly TunnelHandler tunnelHandler; - private int bestTunnelIndex = 0; + private int bestTunnelIndex; private int lowestTunnelRating = int.MaxValue; private bool isManuallySelectedTunnel; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index acf183f0e..625bb2364 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -8,9 +8,6 @@ using System.Net; using System.Net.Http; using System.Net.Sockets; -#if !NETFRAMEWORK -using System.Threading; -#endif using System.Threading.Tasks; namespace DTAClient.Domain.Multiplayer.CnCNet @@ -33,20 +30,19 @@ internal sealed class CnCNetTunnel public static CnCNetTunnel Parse(string str) { // For the format, check http://cncnet.org/master-list - try { var tunnel = new CnCNetTunnel(); string[] parts = str.Split(';'); string address = parts[0]; - int version = int.Parse(parts[10]); + int version = int.Parse(parts[10], CultureInfo.InvariantCulture); #if NETFRAMEWORK tunnel.Address = address.Substring(0, address.LastIndexOf(':')); - tunnel.Port = int.Parse(address.Substring(address.LastIndexOf(':') + 1)); + tunnel.Port = int.Parse(address.Substring(address.LastIndexOf(':') + 1), CultureInfo.InvariantCulture); #else tunnel.Address = address[..address.LastIndexOf(':')]; - tunnel.Port = int.Parse(address[(address.LastIndexOf(':') + 1)..]); + tunnel.Port = int.Parse(address[(address.LastIndexOf(':') + 1)..], CultureInfo.InvariantCulture); #endif tunnel.Country = parts[1]; tunnel.CountryCode = parts[2]; @@ -68,7 +64,7 @@ public static CnCNetTunnel Parse(string str) } catch (Exception ex) when (ex is FormatException or OverflowException or IndexOutOfRangeException) { - PreStartup.LogException(ex, "Parsing tunnel information failed: " + ex.Message + Environment.NewLine + "Parsed string: " + str); + PreStartup.LogException(ex, "Parsing tunnel information failed. Parsed string: " + str); return null; } } @@ -100,7 +96,7 @@ private set public double Longitude { get; private set; } public int Version { get; private set; } public double Distance { get; private set; } - public int PingInMs { get; set; } = -1; + public int PingInMs { get; private set; } = -1; /// /// Gets a list of player ports to use from a specific tunnel server. @@ -109,7 +105,7 @@ private set public async Task> GetPlayerPortInfoAsync(int playerCount) { if (Version != Constants.TUNNEL_VERSION_2) - throw new InvalidOperationException("GetPlayerPortInfo only works with version 2 tunnels."); + throw new InvalidOperationException($"GetPlayerPortInfo only works with version {Constants.TUNNEL_VERSION_2} tunnels."); try { @@ -149,7 +145,7 @@ public async Task> GetPlayerPortInfoAsync(int playerCount) } catch (Exception ex) { - PreStartup.LogException(ex, "Unable to connect to the specified tunnel server. Returned error message: " + ex.Message); + PreStartup.LogException(ex, "Unable to connect to the specified tunnel server."); } return new List(); @@ -189,7 +185,7 @@ public async Task UpdatePingAsync() } catch (SocketException ex) { - PreStartup.LogException(ex, $"Failed to ping tunnel {Name} ({Address}:{Port}). Message: {ex.Message}"); + PreStartup.LogException(ex, $"Failed to ping tunnel {Name} ({Address}:{Port})."); PingInMs = -1; } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 0f93e4faa..872be6353 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -153,7 +153,7 @@ private async Task PingCurrentTunnelAsync(bool checkTunnelList = false) /// Downloads and parses the list of CnCNet tunnels. /// /// A list of tunnel servers. - private async Task> DoRefreshTunnelsAsync() + private static async Task> DoRefreshTunnelsAsync() { FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); @@ -211,7 +211,7 @@ private async Task> DoRefreshTunnelsAsync() { try { - CnCNetTunnel tunnel = CnCNetTunnel.Parse(serverInfo); + var tunnel = CnCNetTunnel.Parse(serverInfo); if (tunnel == null) continue; @@ -231,28 +231,28 @@ private async Task> DoRefreshTunnelsAsync() } } - if (returnValue.Count > 0) + if (!returnValue.Any()) + return returnValue; + + try { - try - { - if (tunnelCacheFile.Exists) - tunnelCacheFile.Delete(); + if (tunnelCacheFile.Exists) + tunnelCacheFile.Delete(); - DirectoryInfo clientDirectoryInfo = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath); + DirectoryInfo clientDirectoryInfo = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath); - if (!clientDirectoryInfo.Exists) - clientDirectoryInfo.Create(); + if (!clientDirectoryInfo.Exists) + clientDirectoryInfo.Create(); #if NETFRAMEWORK - File.WriteAllText(tunnelCacheFile.FullName, data); + File.WriteAllText(tunnelCacheFile.FullName, data); #else - await File.WriteAllTextAsync(tunnelCacheFile.FullName, data); + await File.WriteAllTextAsync(tunnelCacheFile.FullName, data); #endif - } - catch (Exception ex) - { - PreStartup.LogException(ex, "Refreshing tunnel cache file failed!"); - } + } + catch (Exception ex) + { + PreStartup.LogException(ex, "Refreshing tunnel cache file failed!"); } return returnValue; diff --git a/DXMainClient/Domain/Multiplayer/PlayerInfo.cs b/DXMainClient/Domain/Multiplayer/PlayerInfo.cs index e87224457..a4ff6f809 100644 --- a/DXMainClient/Domain/Multiplayer/PlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/PlayerInfo.cs @@ -80,23 +80,22 @@ public override string ToString() /// A PlayerInfo instance, or null if the string format was invalid. public static PlayerInfo FromString(string str) { - var values = str.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + string[] values = str.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); if (values.Length != 8) return null; - var pInfo = new PlayerInfo(); - - pInfo.Name = values[0]; - pInfo.SideId = Conversions.IntFromString(values[1], 0); - pInfo.StartingLocation = Conversions.IntFromString(values[2], 0); - pInfo.ColorId = Conversions.IntFromString(values[3], 0); - pInfo.TeamId = Conversions.IntFromString(values[4], 0); - pInfo.AILevel = Conversions.IntFromString(values[5], 0); - pInfo.IsAI = Conversions.BooleanFromString(values[6], true); - pInfo.Index = Conversions.IntFromString(values[7], 0); - - return pInfo; + return new PlayerInfo + { + Name = values[0], + SideId = Conversions.IntFromString(values[1], 0), + StartingLocation = Conversions.IntFromString(values[2], 0), + ColorId = Conversions.IntFromString(values[3], 0), + TeamId = Conversions.IntFromString(values[4], 0), + AILevel = Conversions.IntFromString(values[5], 0), + IsAI = Conversions.BooleanFromString(values[6], true), + Index = Conversions.IntFromString(values[7], 0) + }; } } -} +} \ No newline at end of file From 6afccc9a8da8ad9dfbe54aaa5ff344752cb4a199 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 26 Aug 2022 19:48:44 +0200 Subject: [PATCH 025/109] Use Task.Run() --- ClientCore/INIProcessing/PreprocessorBackgroundTask.cs | 2 +- DXMainClient/DXGUI/Generic/GameInProgressWindow.cs | 2 +- DXMainClient/Domain/Multiplayer/MapLoader.cs | 3 +-- DXMainClient/Startup.cs | 4 ++-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs b/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs index 2d129d73d..159928db0 100644 --- a/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs +++ b/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs @@ -33,7 +33,7 @@ public static PreprocessorBackgroundTask Instance public void Run() { - task = Task.Factory.StartNew(CheckFiles); + task = Task.Run(CheckFiles); } private static void CheckFiles() diff --git a/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs b/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs index ec7ad9e8e..38021a960 100644 --- a/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs @@ -166,7 +166,7 @@ private void HandleGameProcessExited() DateTime dtn = DateTime.Now; #if ARES - Task.Factory.StartNew(ProcessScreenshots); + Task.Run(ProcessScreenshots); // TODO: Ares debug log handling should be addressed in Ares DLL itself. // For now the following are handled here: diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index 842848cc0..ced49460f 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -158,8 +158,7 @@ private void LoadCustomMaps() foreach (FileInfo mapFile in mapFiles) { - // this must be Task.Factory.StartNew for XNA/.Net 4.0 compatibility - tasks.Add(Task.Factory.StartNew(() => + tasks.Add(Task.Run(() => { string baseFilePath = mapFile.FullName.Substring(ProgramConstants.GamePath.Length); baseFilePath = baseFilePath.Substring(0, baseFilePath.Length - 4); diff --git a/DXMainClient/Startup.cs b/DXMainClient/Startup.cs index 69d0e6e63..77484ea9a 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -64,9 +64,9 @@ public void Execute() GenerateOnlineIdAsync(); #if ARES - Task.Factory.StartNew(() => PruneFiles(SafePath.GetDirectory(ProgramConstants.GamePath, "debug"), DateTime.Now.AddDays(-7))); + Task.Run(() => PruneFiles(SafePath.GetDirectory(ProgramConstants.GamePath, "debug"), DateTime.Now.AddDays(-7))); #endif - Task.Factory.StartNew(MigrateOldLogFiles); + Task.Run(MigrateOldLogFiles); DirectoryInfo updaterFolder = SafePath.GetDirectory(ProgramConstants.GamePath, "Updater"); From 668a813982702ab29212e9258144f6b120fd29e2 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 2 Sep 2022 10:28:36 +0200 Subject: [PATCH 026/109] Use https requests (HTTP/2) --- DXMainClient/Domain/MainClientConstants.cs | 2 +- .../Multiplayer/CnCNet/CnCNetPlayerCountTask.cs | 7 +++++-- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 7 +++++-- DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs | 11 +++++++---- .../Domain/Multiplayer/CnCNet/TunnelHandler.cs | 5 ++++- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/DXMainClient/Domain/MainClientConstants.cs b/DXMainClient/Domain/MainClientConstants.cs index 0197f113f..06055f3b6 100644 --- a/DXMainClient/Domain/MainClientConstants.cs +++ b/DXMainClient/Domain/MainClientConstants.cs @@ -4,7 +4,7 @@ namespace DTAClient.Domain { public static class MainClientConstants { - public const string CNCNET_TUNNEL_LIST_URL = "http://cncnet.org/master-list"; + public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/master-list"; public static string GAME_NAME_LONG = "CnCNet Client"; public static string GAME_NAME_SHORT = "CnCNet"; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index 0c393c57e..aa50cbb3c 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -56,10 +56,13 @@ private static async Task GetCnCNetPlayerCountAsync() }; using var client = new HttpClient(httpClientHandler, true) { - Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT) + Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT), +#if !NETFRAMEWORK + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher +#endif }; - string info = await client.GetStringAsync("http://api.cncnet.org/status"); + string info = await client.GetStringAsync("https://api.cncnet.org/status"); info = info.Replace("{", String.Empty); info = info.Replace("}", String.Empty); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 625bb2364..6609a44f7 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -29,7 +29,7 @@ internal sealed class CnCNetTunnel /// A CnCNetTunnel instance parsed from the given string. public static CnCNetTunnel Parse(string str) { - // For the format, check http://cncnet.org/master-list + // For the format, check https://cncnet.org/master-list try { var tunnel = new CnCNetTunnel(); @@ -124,7 +124,10 @@ public async Task> GetPlayerPortInfoAsync(int playerCount) }; using var client = new HttpClient(httpClientHandler, true) { - Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT) + Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT), +#if !NETFRAMEWORK + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher +#endif }; string data = await client.GetStringAsync(addressString); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs index e3249123c..62a4c16f0 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs @@ -36,7 +36,7 @@ public static class MapSharer private static readonly object locker = new(); - private const string MAPDB_URL = "http://mapdb.cncnet.org/upload"; + private const string MAPDB_URL = "https://mapdb.cncnet.org/upload"; /// /// Adds a map into the CnCNet map upload queue. @@ -113,7 +113,7 @@ private static async Task UploadAsync(Map map, string myGameId) }; var values = new NameValueCollection { - { "game", gameName.ToLower() }, + { "game", gameName.ToLower() } }; string response = await UploadFilesAsync(address, files, values); @@ -171,7 +171,10 @@ private static HttpClient GetHttpClient() return new HttpClient(httpClientHandler, true) { - Timeout = TimeSpan.FromMilliseconds(10000) + Timeout = TimeSpan.FromMilliseconds(10000), +#if !NETFRAMEWORK + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher +#endif }; } @@ -260,7 +263,7 @@ public static string GetMapFileName(string sha1, string mapName) try { - string address = "http://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip"; + string address = "https://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip"; Logger.Log("MapSharer: Downloading URL: " + address); stream = await client.GetStreamAsync(address); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 872be6353..3e7479b96 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -168,7 +168,10 @@ private static async Task> DoRefreshTunnelsAsync() }; using var client = new HttpClient(httpClientHandler, true) { - Timeout = TimeSpan.FromSeconds(100) + Timeout = TimeSpan.FromSeconds(100), +#if !NETFRAMEWORK + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher +#endif }; string data; From b169c9a0f37fb464ad7a96b7f2ead15132fa058f Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 12 Sep 2022 20:57:27 +0200 Subject: [PATCH 027/109] Tunnel IPv6/IPv4 selection --- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 28 +++++++++++++++---- .../Multiplayer/CnCNet/TunnelHandler.cs | 2 +- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 6609a44f7..eba3b2f79 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -34,15 +34,33 @@ public static CnCNetTunnel Parse(string str) { var tunnel = new CnCNetTunnel(); string[] parts = str.Split(';'); - string address = parts[0]; + string addressAndPort = parts[0]; + string secondaryAddress = parts.Length > 12 ? parts[12] : null; int version = int.Parse(parts[10], CultureInfo.InvariantCulture); +#if NETFRAMEWORK + string primaryAddress = addressAndPort.Substring(0, addressAndPort.LastIndexOf(':')); +#else + string primaryAddress = addressAndPort[..addressAndPort.LastIndexOf(':')]; +#endif + var primaryIpAddress = IPAddress.Parse(primaryAddress); + IPAddress secondaryIpAddress = string.IsNullOrWhiteSpace(secondaryAddress) ? null : IPAddress.Parse(secondaryAddress); + + if (Socket.OSSupportsIPv6 && primaryIpAddress.AddressFamily is AddressFamily.InterNetworkV6) + tunnel.Address = primaryIpAddress.ToString(); + else if (Socket.OSSupportsIPv6 && secondaryIpAddress?.AddressFamily is AddressFamily.InterNetworkV6) + tunnel.Address = secondaryIpAddress.ToString(); + else if (Socket.OSSupportsIPv4 && primaryIpAddress.AddressFamily is AddressFamily.InterNetwork) + tunnel.Address = primaryIpAddress.ToString(); + else if (Socket.OSSupportsIPv4 && secondaryIpAddress?.AddressFamily is AddressFamily.InterNetwork) + tunnel.Address = secondaryIpAddress.ToString(); + else + throw new($"No supported IP address found ({nameof(Socket.OSSupportsIPv6)}={Socket.OSSupportsIPv6}," + + $" {nameof(Socket.OSSupportsIPv4)}={Socket.OSSupportsIPv4}) for {str}."); #if NETFRAMEWORK - tunnel.Address = address.Substring(0, address.LastIndexOf(':')); - tunnel.Port = int.Parse(address.Substring(address.LastIndexOf(':') + 1), CultureInfo.InvariantCulture); + tunnel.Port = int.Parse(addressAndPort.Substring(addressAndPort.LastIndexOf(':') + 1), CultureInfo.InvariantCulture); #else - tunnel.Address = address[..address.LastIndexOf(':')]; - tunnel.Port = int.Parse(address[(address.LastIndexOf(':') + 1)..], CultureInfo.InvariantCulture); + tunnel.Port = int.Parse(addressAndPort[(addressAndPort.LastIndexOf(':') + 1)..], CultureInfo.InvariantCulture); #endif tunnel.Country = parts[1]; tunnel.CountryCode = parts[2]; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 3e7479b96..9324f7b70 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -209,7 +209,7 @@ private static async Task> DoRefreshTunnelsAsync() string[] serverList = data.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); - // skip first header item ("address;country;countrycode;name;password;clients;maxclients;official;latitude;longitude;version;distance") + // skip the header foreach (string serverInfo in serverList.Skip(1)) { try From ba26e86439f238aed02d09fc94a68c5f1e4c54e5 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 13 Sep 2022 19:53:02 +0200 Subject: [PATCH 028/109] Fix IRC Exception when user *.GameSurge.net sets MODE --- DXMainClient/Online/Connection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index e33b99d82..04813d0a3 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -697,7 +697,7 @@ private async Task PerformCommandAsync(string message) } break; case "MODE": - string modeUserName = prefix.Substring(0, prefix.IndexOf('!')); + string modeUserName = prefix.Contains('!') ? prefix.Substring(0, prefix.IndexOf('!')) : prefix; string modeChannelName = parameters[0]; string modeString = parameters[1]; List modeParameters = From eaf761cfb51a67c73de0a9597ec3b126d1f802f4 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 13 Sep 2022 20:33:29 +0200 Subject: [PATCH 029/109] Fix IRC disconnect --- DXMainClient/Online/Connection.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index 04813d0a3..d303deffd 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -261,8 +261,6 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) } catch (OperationCanceledException) { - connectionManager.OnDisconnected(); - connectionCut = false; // This disconnect is intentional break; } #endif @@ -298,14 +296,12 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) timer.Interval = 30000; } -#if NETFRAMEWORK if (cancellationToken.IsCancellationRequested) { connectionManager.OnDisconnected(); connectionCut = false; // This disconnect is intentional } -#endif timer.Enabled = false; timer.Dispose(); @@ -314,7 +310,7 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) if (connectionCut) { while (!sendQueueExited) - await Task.Delay(100); + await Task.Delay(100, cancellationToken); reconnectCount++; @@ -324,7 +320,7 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) return; } - await Task.Delay(RECONNECT_WAIT_DELAY); + await Task.Delay(RECONNECT_WAIT_DELAY, cancellationToken); if (IsConnected || AttemptingConnection) { From bceb8f5dae08ff81a039c4f5ce1e58adbdd2795e Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 14 Sep 2022 00:41:43 +0200 Subject: [PATCH 030/109] Update IRC server selection --- DXMainClient/DXGUI/Generic/TopBar.cs | 2 +- .../Multiplayer/CnCNet/GlobalContextMenu.cs | 6 +-- .../CnCNet/PrivateMessagingWindow.cs | 2 +- .../Multiplayer/CnCNet/RecentPlayerTable.cs | 2 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 2 +- .../Multiplayer/GameLobby/GameLobbyBase.cs | 2 +- .../Multiplayer/GameLobby/LANGameLobby.cs | 2 +- .../GameLobby/MultiplayerGameLobby.cs | 2 +- .../Multiplayer/GameLobby/SkirmishLobby.cs | 2 +- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 2 +- DXMainClient/Online/Channel.cs | 3 +- DXMainClient/Online/CnCNetManager.cs | 2 +- DXMainClient/Online/Connection.cs | 52 +++++++++++-------- DXMainClient/Online/PrivateMessageHandler.cs | 2 +- 14 files changed, 45 insertions(+), 38 deletions(-) diff --git a/DXMainClient/DXGUI/Generic/TopBar.cs b/DXMainClient/DXGUI/Generic/TopBar.cs index 89a039aa6..e0681e4a6 100644 --- a/DXMainClient/DXGUI/Generic/TopBar.cs +++ b/DXMainClient/DXGUI/Generic/TopBar.cs @@ -20,7 +20,7 @@ namespace DTAClient.DXGUI.Generic /// /// A top bar that allows switching between various client windows. /// - public class TopBar : XNAPanel + internal sealed class TopBar : XNAPanel { /// /// The number of seconds that the top bar will stay down after it has diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs index 37d6b7d42..ea261a5dd 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs @@ -14,7 +14,7 @@ namespace DTAClient.DXGUI.Multiplayer.CnCNet { - public class GlobalContextMenu : XNAContextMenu + internal sealed class GlobalContextMenu : XNAContextMenu { private readonly string PRIVATE_MESSAGE = "Private Message".L10N("Client:Main:PrivateMessage"); private readonly string ADD_FRIEND = "Add Friend".L10N("Client:Main:AddFriend"); @@ -36,8 +36,8 @@ public class GlobalContextMenu : XNAContextMenu private XNAContextMenuItem copyLinkItem; private XNAContextMenuItem openLinkItem; - protected readonly CnCNetManager connectionManager; - protected GlobalContextMenuData contextMenuData; + private readonly CnCNetManager connectionManager; + private GlobalContextMenuData contextMenuData; public EventHandler JoinEvent; diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs index 22cd7f286..362cdf701 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs @@ -21,7 +21,7 @@ namespace DTAClient.DXGUI.Multiplayer.CnCNet { - public class PrivateMessagingWindow : XNAWindow, ISwitchable + internal sealed class PrivateMessagingWindow : XNAWindow, ISwitchable { private const int MESSAGES_INDEX = 0; private const int FRIEND_LIST_VIEW_INDEX = 1; diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/RecentPlayerTable.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/RecentPlayerTable.cs index d7be10987..85068a0f8 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/RecentPlayerTable.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/RecentPlayerTable.cs @@ -7,7 +7,7 @@ namespace DTAClient.DXGUI.Multiplayer.CnCNet { - public class RecentPlayerTable : XNAMultiColumnListBox + internal sealed class RecentPlayerTable : XNAMultiColumnListBox { private readonly CnCNetManager connectionManager; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index 9e1b1f7fe..44fd36194 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -218,7 +218,7 @@ public override void Initialize() /// private void ResetDiscordPresence() => discordHandler.UpdatePresence(); - private async Task BtnLeaveGame_LeftClickAsync() => LeaveGameAsync(); + private Task BtnLeaveGame_LeftClickAsync() => LeaveGameAsync(); protected virtual Task LeaveGameAsync() { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 55995651a..1f764e33b 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -420,7 +420,7 @@ protected async Task HandleGameOptionPresetLoadCommandAsync(string presetName) protected abstract void AddNotice(string message, Color color); - private async Task BtnPickRandomMap_LeftClickAsync() => PickRandomMapAsync(); + private Task BtnPickRandomMap_LeftClickAsync() => PickRandomMapAsync(); private void TbMapSearch_InputReceived(object sender, EventArgs e) => ListMaps(); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index db4730a2c..c264f5c4e 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -24,7 +24,7 @@ namespace DTAClient.DXGUI.Multiplayer.GameLobby { - public class LANGameLobby : MultiplayerGameLobby + internal sealed class LANGameLobby : MultiplayerGameLobby { private const int GAME_OPTION_SPECIAL_FLAG_COUNT = 5; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 5d00eb039..43caa5f4e 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -22,7 +22,7 @@ namespace DTAClient.DXGUI.Multiplayer.GameLobby /// /// A generic base class for multiplayer game lobbies (CnCNet and LAN). /// - public abstract class MultiplayerGameLobby : GameLobbyBase, ISwitchable + internal abstract class MultiplayerGameLobby : GameLobbyBase, ISwitchable { private const int MAX_DICE = 10; private const int MAX_DIE_SIDES = 100; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs index cf52b520e..c5d58df75 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -16,7 +16,7 @@ namespace DTAClient.DXGUI.Multiplayer.GameLobby { - public class SkirmishLobby : GameLobbyBase, ISwitchable + internal sealed class SkirmishLobby : GameLobbyBase, ISwitchable { private const string SETTINGS_PATH = "Client/SkirmishSettings.ini"; diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index 807c37f6e..1872ee671 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -136,7 +136,7 @@ public override string ToString() /// /// Starts receiving messages from the player asynchronously. /// - public async Task StartReceiveLoopAsync(CancellationToken cancellationToken) + public Task StartReceiveLoopAsync(CancellationToken cancellationToken) => ReceiveMessagesAsync(cancellationToken); /// diff --git a/DXMainClient/Online/Channel.cs b/DXMainClient/Online/Channel.cs index 342c38bb8..0f1a9bb11 100644 --- a/DXMainClient/Online/Channel.cs +++ b/DXMainClient/Online/Channel.cs @@ -8,7 +8,7 @@ namespace DTAClient.Online { - public class Channel : IMessageView + internal sealed class Channel : IMessageView { const int MESSAGE_LIMIT = 1024; @@ -115,6 +115,7 @@ public void AddUser(ChannelUser user) public async Task OnUserJoinedAsync(ChannelUser user) { + await Task.CompletedTask; AddUser(user); if (notifyOnUserListChange) diff --git a/DXMainClient/Online/CnCNetManager.cs b/DXMainClient/Online/CnCNetManager.cs index 627e00cd1..d231f7ada 100644 --- a/DXMainClient/Online/CnCNetManager.cs +++ b/DXMainClient/Online/CnCNetManager.cs @@ -17,7 +17,7 @@ namespace DTAClient.Online /// Acts as an interface between the CnCNet connection class /// and the user-interface's classes. /// - public class CnCNetManager : IConnectionManager + internal sealed class CnCNetManager : IConnectionManager { // When implementing IConnectionManager functions, pay special attention // to thread-safety. diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index d303deffd..2852dd720 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -20,7 +20,7 @@ namespace DTAClient.Online /// /// The CnCNet connection handler. /// - public class Connection + internal sealed class Connection { private const int MAX_RECONNECT_COUNT = 8; private const int RECONNECT_WAIT_DELAY = 4000; @@ -40,21 +40,14 @@ public Connection(IConnectionManager connectionManager) private static readonly IList Servers = new List { new("Burstfire.UK.EU.GameSurge.net", "GameSurge London, UK", new[] { 6667, 6668, 7000 }), - new("ColoCrossing.IL.US.GameSurge.net", "GameSurge Chicago, IL", new[] { 6660, 6666, 6667, 6668, 6669 }), + new("VortexServers.IL.US.GameSurge.net", "GameSurge Chicago, IL", new[] { 6660, 6666, 6667, 6668, 6669 }), new("Gameservers.NJ.US.GameSurge.net", "GameSurge Newark, NJ", new[] { 6665, 6666, 6667, 6668, 6669, 7000, 8080 }), new("Krypt.CA.US.GameSurge.net", "GameSurge Santa Ana, CA",new[] { 6666, 6667, 6668, 6669 }), new("NuclearFallout.WA.US.GameSurge.net", "GameSurge Seattle, WA", new[] { 6667, 5960 }), - new("Portlane.SE.EU.GameSurge.net", "GameSurge Stockholm, Sweden", new[] { 6660, 6666, 6667, 6668, 6669 }), - new("Prothid.NY.US.GameSurge.Net", "GameSurge NYC, NY", new[] { 5960, 6660, 6666, 6667, 6668, 6669, 6697 }), + new("Stockholm.SE.EU.GameSurge.net", "GameSurge Stockholm, Sweden", new[] { 6660, 6666, 6667, 6668, 6669 }), + new("Prothid.NY.US.GameSurge.Net", "GameSurge NYC, NY", new[] { 5960, 6660, 6666, 6667, 6668, 6669 }), new("TAL.DE.EU.GameSurge.net", "GameSurge Wuppertal, Germany", new[] { 6660, 6666, 6667, 6668, 6669 }), - new("208.167.237.120", "GameSurge IP 208.167.237.120", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new("192.223.27.109", "GameSurge IP 192.223.27.109", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new("108.174.48.100", "GameSurge IP 108.174.48.100", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new("208.146.35.105", "GameSurge IP 208.146.35.105", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new("195.8.250.180", "GameSurge IP 195.8.250.180", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new("91.217.189.76", "GameSurge IP 91.217.189.76", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new("195.68.206.250", "GameSurge IP 195.68.206.250", new[] { 6660, 6666, 6667, 6668, 6669, 7000, 8080 }), - new("irc.gamesurge.net", "GameSurge", new[] { 6667 }), + new("irc.gamesurge.net", "GameSurge", new[] { 6667 }) }.AsReadOnly(); bool _isConnected; @@ -357,7 +350,8 @@ private async Task> GetServerListSortedByLatencyAsync() (IPAddress IpAddress, string Name, int[] Ports)[] serverInfos = serverInfosGroupedByIPAddress.Select(serverInfoGroup => { IPAddress ipAddress = serverInfoGroup.Key; - string serverNames = string.Join(", ", serverInfoGroup.Select(serverInfo => serverInfo.Name)); + string serverNames = string.Join(", ", serverInfoGroup.Where(serverInfo => !"GameSurge".Equals(serverInfo.Name)) + .Select(serverInfo => serverInfo.Name)); int[] serverPorts = serverInfoGroup.SelectMany(serverInfo => serverInfo.Ports).Distinct().ToArray(); return (ipAddress, serverNames, serverPorts); @@ -381,22 +375,34 @@ private async Task> GetServerListSortedByLatencyAsync() Logger.Log($"Skipped a failed server {name} ({ipAddress})."); } - (Server Server, long Result)[] serverAndLatencyResults = + (Server Server, IPAddress IpAddress, long Result)[] serverAndLatencyResults = await Task.WhenAll(serverInfos.Where(q => !failedServerIPs.Contains(q.IpAddress.ToString())).Select(PingServerAsync)); - // Sort the servers by latency. - (Server Server, long Result)[] sortedServerAndLatencyResults = serverAndLatencyResults + // Sort the servers by AddressFamily & latency. + (Server Server, IPAddress IpAddress, long Result)[] sortedServerAndLatencyResults = serverAndLatencyResults + .Where(server => server.IpAddress.AddressFamily is AddressFamily.InterNetworkV6 && server.Result is not long.MaxValue) .Select(server => server) .OrderBy(taskResult => taskResult.Result) + .Concat(serverAndLatencyResults + .Where(server => server.IpAddress.AddressFamily is AddressFamily.InterNetwork && server.Result is not long.MaxValue) + .Select(server => server) + .OrderBy(taskResult => taskResult.Result)) + .Concat(serverAndLatencyResults + .Where(server => server.IpAddress.AddressFamily is AddressFamily.InterNetworkV6 && server.Result is long.MaxValue) + .Select(server => server) + .OrderBy(taskResult => taskResult.Result)) + .Concat(serverAndLatencyResults + .Where(server => server.IpAddress.AddressFamily is AddressFamily.InterNetwork && server.Result is long.MaxValue) + .Select(server => server) + .OrderBy(taskResult => taskResult.Result)) .ToArray(); // Do logging. - foreach ((Server server, long serverLatencyValue) in sortedServerAndLatencyResults) + foreach ((Server _, IPAddress ipAddress, long serverLatencyValue) in sortedServerAndLatencyResults) { - string serverIPAddress = server.Host; string serverLatencyString = serverLatencyValue <= MAXIMUM_LATENCY ? serverLatencyValue.ToString() : "DNF"; - Logger.Log($"Lobby server IP: {serverIPAddress}, latency: {serverLatencyString}."); + Logger.Log($"Lobby server IP: {ipAddress}, latency: {serverLatencyString}."); } int candidateCount = sortedServerAndLatencyResults.Length; @@ -409,7 +415,7 @@ private async Task> GetServerListSortedByLatencyAsync() return sortedServerAndLatencyResults.Select(taskResult => taskResult.Server).ToList(); } - private static async Task<(Server Server, long Result)> PingServerAsync((IPAddress IpAddress, string Name, int[] Ports) serverInfo) + private static async Task<(Server Server, IPAddress IpAddress, long Result)> PingServerAsync((IPAddress IpAddress, string Name, int[] Ports) serverInfo) { Logger.Log($"Attempting to ping {serverInfo.Name} ({serverInfo.IpAddress})."); var server = new Server(serverInfo.IpAddress.ToString(), serverInfo.Name, serverInfo.Ports); @@ -424,19 +430,19 @@ private async Task> GetServerListSortedByLatencyAsync() long pingInMs = pingReply.RoundtripTime; Logger.Log($"The latency in milliseconds to the server {serverInfo.Name} ({serverInfo.IpAddress}): {pingInMs}."); - return (server, pingInMs); + return (server, serverInfo.IpAddress, pingInMs); } Logger.Log($"Failed to ping the server {serverInfo.Name} ({serverInfo.IpAddress}): " + $"{Enum.GetName(typeof(IPStatus), pingReply.Status)}."); - return (server, long.MaxValue); + return (server, serverInfo.IpAddress, long.MaxValue); } catch (PingException ex) { PreStartup.LogException(ex, $"Caught an exception when pinging {serverInfo.Name} ({serverInfo.IpAddress}) Lobby server."); - return (server, long.MaxValue); + return (server, serverInfo.IpAddress, long.MaxValue); } } diff --git a/DXMainClient/Online/PrivateMessageHandler.cs b/DXMainClient/Online/PrivateMessageHandler.cs index 4e03f1875..aa1e5b262 100644 --- a/DXMainClient/Online/PrivateMessageHandler.cs +++ b/DXMainClient/Online/PrivateMessageHandler.cs @@ -8,7 +8,7 @@ namespace DTAClient.Online /// as to whether the message should be ignored, independent from any GUI. This will then forward valid private message /// events to other consumers. /// - public class PrivateMessageHandler + internal sealed class PrivateMessageHandler { private readonly CnCNetUserData _cncnetUserData; private readonly CnCNetManager _connectionManager; From 65e28b79406ceeae004376a46ec5f77c546fd6e9 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 23 Sep 2022 23:04:55 +0200 Subject: [PATCH 031/109] Fix LAN lobby crash when no map selected --- DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index c264f5c4e..964163c24 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -886,6 +886,9 @@ public override void Update(GameTime gameTime) private void BroadcastGame() { + if (GameMode == null || Map == null) + return; + var sb = new ExtendedStringBuilder("GAME ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(ProgramConstants.LAN_PROTOCOL_REVISION); From 4b27b0b39ca7799deedb21ff18837f299ac4e551 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 23 Sep 2022 23:08:05 +0200 Subject: [PATCH 032/109] Don't send LAN host player joined using UI thread --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 1 - .../Multiplayer/GameLobby/LANGameLobby.cs | 52 ++++++++++++------- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 6 +-- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 8fb2f0c6e..14aa70025 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -666,7 +666,6 @@ private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) { // Changing the map applies forced settings (co-op sides etc.) to the // new player, and it also sends an options broadcast message - //CopyPlayerDataToUI(); This is also called by ChangeMap() await ChangeMapAsync(GameModeMap); await BroadcastPlayerOptionsAsync(); await BroadcastPlayerExtraOptionsAsync(); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index 964163c24..5f043c0f9 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -157,25 +157,7 @@ public async Task SetUpAsync(bool isHost, { RandomSeed = new Random().Next(); ListenForClientsAsync(cancellationTokenSource.Token); - - this.client = new Socket(SocketType.Stream, ProtocolType.Tcp); - await this.client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT); - - string message = PLAYER_JOIN_COMMAND + - ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME; -#if NETFRAMEWORK - byte[] buffer1 = encoding.GetBytes(message); - var buffer = new ArraySegment(buffer1); - - await this.client.SendAsync(buffer, SocketFlags.None); -#else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); - Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; - int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); - buffer = buffer[..bytes]; - - await this.client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); -#endif + SendHostPlayerJoinedMessageAsync(cancellationTokenSource.Token); var fhc = new FileHashCalculator(); fhc.CalculateHashes(GameModeMaps.GameModes); @@ -196,6 +178,38 @@ public async Task SetUpAsync(bool isHost, WindowManager.SelectedControl = tbChatInput; } + private async Task SendHostPlayerJoinedMessageAsync(CancellationToken cancellationToken) + { + try + { + client = new Socket(SocketType.Stream, ProtocolType.Tcp); + await client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT, cancellationToken); + + string message = PLAYER_JOIN_COMMAND + + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME; +#if NETFRAMEWORK + byte[] buffer1 = encoding.GetBytes(message); + var buffer = new ArraySegment(buffer1); + + await client.SendAsync(buffer, SocketFlags.None); +#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); + Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; + + await client.SendAsync(buffer, SocketFlags.None, cancellationToken); +#endif + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + } + public async Task PostJoinAsync() { var fhc = new FileHashCalculator(); diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 346a2c051..f5d304711 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -278,9 +278,9 @@ private async Task WindowManager_GameClosingAsync(CancellationToken cancellation try { #endif - await SendMessageAsync("QUIT", cancellationToken); - cancellationTokenSource.Cancel(); - socket.Close(); + await SendMessageAsync("QUIT", cancellationToken); + cancellationTokenSource.Cancel(); + socket.Close(); #if NETFRAMEWORK } catch (ObjectDisposedException) From 0eb89e863399275002c37703e50640595c888086 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 23 Sep 2022 23:22:30 +0200 Subject: [PATCH 033/109] Fix net48 build --- DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index 5f043c0f9..7bd3e3f86 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -183,7 +183,11 @@ private async Task SendHostPlayerJoinedMessageAsync(CancellationToken cancellati try { client = new Socket(SocketType.Stream, ProtocolType.Tcp); +#if NETFRAMEWORK + await client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT); +#else await client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT, cancellationToken); +#endif string message = PLAYER_JOIN_COMMAND + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME; From e38f0190866deee7bdf06d0d47c427ece62a896f Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 22 Nov 2022 16:03:16 +0100 Subject: [PATCH 034/109] Rebase fix --- DXMainClient/DXGUI/Generic/LoadingScreen.cs | 22 ++++++++++------- DXMainClient/Domain/Multiplayer/MapLoader.cs | 25 +++++++++++++------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/DXMainClient/DXGUI/Generic/LoadingScreen.cs b/DXMainClient/DXGUI/Generic/LoadingScreen.cs index e612faf12..96347ad96 100644 --- a/DXMainClient/DXGUI/Generic/LoadingScreen.cs +++ b/DXMainClient/DXGUI/Generic/LoadingScreen.cs @@ -16,7 +16,7 @@ namespace DTAClient.DXGUI.Generic { - public class LoadingScreen : XNAWindow + internal class LoadingScreen : XNAWindow { public LoadingScreen( CnCNetManager cncnetManager, @@ -55,12 +55,9 @@ public override void Initialize() bool initUpdater = !ClientConfiguration.Instance.ModMode; if (initUpdater) - { - updaterInitTask = new Task(InitUpdater); - updaterInitTask.Start(); - } + updaterInitTask = Task.Run(InitUpdater); - mapLoadTask = mapLoader.LoadMapsAsync(); + mapLoadTask = Task.Run(() => mapLoader.LoadMaps()); if (Cursor.Visible) { @@ -71,8 +68,15 @@ public override void Initialize() private void InitUpdater() { - Updater.OnLocalFileVersionsChecked += LogGameClientVersion; - Updater.CheckLocalFileVersions(); + try + { + Updater.OnLocalFileVersionsChecked += LogGameClientVersion; + Updater.CheckLocalFileVersions(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void LogGameClientVersion() @@ -83,7 +87,7 @@ private void LogGameClientVersion() private void Finish() { - ProgramConstants.GAME_VERSION = ClientConfiguration.Instance.ModMode ? + ProgramConstants.GAME_VERSION = ClientConfiguration.Instance.ModMode ? "N/A" : Updater.GameVersion; MainMenu mainMenu = serviceProvider.GetService(); diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index ced49460f..cee83d81e 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -55,19 +55,26 @@ public class MapLoader /// public void LoadMaps() { - string mpMapsPath = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath); + try + { + string mpMapsPath = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath); - Logger.Log($"Loading maps from {mpMapsPath}."); + Logger.Log($"Loading maps from {mpMapsPath}."); - IniFile mpMapsIni = new IniFile(mpMapsPath); + IniFile mpMapsIni = new IniFile(mpMapsPath); - LoadGameModes(mpMapsIni); - LoadGameModeAliases(mpMapsIni); - LoadMultiMaps(mpMapsIni); - LoadCustomMaps(); + LoadGameModes(mpMapsIni); + LoadGameModeAliases(mpMapsIni); + LoadMultiMaps(mpMapsIni); + LoadCustomMaps(); - GameModes.RemoveAll(g => g.Maps.Count < 1); - GameModeMaps = new GameModeMapCollection(GameModes); + GameModes.RemoveAll(g => g.Maps.Count < 1); + GameModeMaps = new GameModeMapCollection(GameModes); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } private void LoadMultiMaps(IniFile mpMapsIni) From 083079933517e739f2bf0b6e83cf0fb1d3e7fc3a Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 25 Nov 2022 20:30:29 +0100 Subject: [PATCH 035/109] Remove net48 code after net7 rebase --- .../DXGUI/Generic/CampaignSelector.cs | 6 +- .../DXGUI/Generic/GameLoadingWindow.cs | 4 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 2 +- .../Multiplayer/GameLobby/GameLobbyBase.cs | 2 +- .../Multiplayer/GameLobby/LANGameLobby.cs | 91 +++---------------- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 79 +++------------- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 71 +++------------ DXMainClient/Domain/DiscordHandler.cs | 6 +- .../Domain/Multiplayer/AllianceHolder.cs | 6 +- .../CnCNet/CnCNetPlayerCountTask.cs | 6 -- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 27 +----- .../Multiplayer/CnCNet/GameTunnelHandler.cs | 8 -- .../Domain/Multiplayer/CnCNet/MapSharer.cs | 24 ----- .../Multiplayer/CnCNet/TunnelHandler.cs | 15 --- .../CnCNet/TunneledPlayerConnection.cs | 29 +----- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 56 +++--------- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 35 ++----- DXMainClient/Online/Connection.cs | 41 +-------- 18 files changed, 77 insertions(+), 431 deletions(-) diff --git a/DXMainClient/DXGUI/Generic/CampaignSelector.cs b/DXMainClient/DXGUI/Generic/CampaignSelector.cs index c8cfee128..c758551a2 100644 --- a/DXMainClient/DXGUI/Generic/CampaignSelector.cs +++ b/DXMainClient/DXGUI/Generic/CampaignSelector.cs @@ -269,12 +269,12 @@ private void LaunchMission(Mission mission) { bool copyMapsToSpawnmapINI = ClientConfiguration.Instance.CopyMissionsToSpawnmapINI; - Logger.Log("About to write spawn.ini."); + Logger.Log($"About to write {ProgramConstants.SPAWNER_SETTINGS}."); using var spawnStreamWriter = new StreamWriter(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS)); spawnStreamWriter.WriteLine("; Generated by DTA Client"); spawnStreamWriter.WriteLine("[Settings]"); if (copyMapsToSpawnmapINI) - spawnStreamWriter.WriteLine("Scenario=spawnmap.ini"); + spawnStreamWriter.WriteLine($"Scenario={ProgramConstants.SPAWNMAP_INI}"); else spawnStreamWriter.WriteLine("Scenario=" + mission.Scenario); @@ -311,7 +311,7 @@ private void LaunchMission(Mission mission) { var mapIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, mission.Scenario)); IniFile.ConsolidateIniFiles(mapIni, difficultyIni); - mapIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "spawnmap.ini")); + mapIni.WriteIniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI)); } UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value; diff --git a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs index 56158c8c0..8ce37652d 100644 --- a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs @@ -114,7 +114,7 @@ private void BtnLaunch_LeftClick(object sender, EventArgs e) using StreamWriter spawnStreamWriter = new StreamWriter(spawnerSettingsFile.FullName); spawnStreamWriter.WriteLine("; generated by DTA Client"); spawnStreamWriter.WriteLine("[Settings]"); - spawnStreamWriter.WriteLine("Scenario=spawnmap.ini"); + spawnStreamWriter.WriteLine($"Scenario={ProgramConstants.SPAWNMAP_INI}"); spawnStreamWriter.WriteLine("SaveGameName=" + sg.FileName); spawnStreamWriter.WriteLine("LoadSaveGame=Yes"); spawnStreamWriter.WriteLine("SidebarHack=" + ClientConfiguration.Instance.SidebarHack); @@ -123,7 +123,7 @@ private void BtnLaunch_LeftClick(object sender, EventArgs e) spawnStreamWriter.WriteLine("GameSpeed=" + UserINISettings.Instance.GameSpeed); spawnStreamWriter.WriteLine(); - FileInfo spawnMapIniFile = SafePath.GetFile(ProgramConstants.GamePath, "spawnmap.ini"); + FileInfo spawnMapIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI); if (spawnMapIniFile.Exists) spawnMapIniFile.Delete(); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index 44fd36194..fb70b63de 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -343,7 +343,7 @@ protected void LoadGame() WriteSpawnIniAdditions(spawnIni); spawnIni.WriteIniFile(); - FileInfo spawnMapFileInfo = SafePath.GetFile(ProgramConstants.GamePath, "spawnmap.ini"); + FileInfo spawnMapFileInfo = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI); spawnMapFileInfo.Delete(); using StreamWriter spawnMapStreamWriter = new StreamWriter(spawnMapFileInfo.FullName); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 1f764e33b..56c48a483 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1345,7 +1345,7 @@ protected virtual PlayerHouseInfo[] Randomize(List teamStartMa /// private PlayerHouseInfo[] WriteSpawnIni() { - Logger.Log("Writing spawn.ini"); + Logger.Log($"Writing {ProgramConstants.SPAWNER_SETTINGS}"); FileInfo spawnerSettingsFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index 7bd3e3f86..421acd33d 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -11,9 +11,7 @@ using Rampastring.Tools; using Rampastring.XNAUI; using System; -#if !NETFRAMEWORK using System.Buffers; -#endif using System.Collections.Generic; using System.Linq; using System.Net; @@ -183,27 +181,19 @@ private async Task SendHostPlayerJoinedMessageAsync(CancellationToken cancellati try { client = new Socket(SocketType.Stream, ProtocolType.Tcp); -#if NETFRAMEWORK - await client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT); -#else - await client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT, cancellationToken); -#endif - string message = PLAYER_JOIN_COMMAND + - ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME; -#if NETFRAMEWORK - byte[] buffer1 = encoding.GetBytes(message); - var buffer = new ArraySegment(buffer1); + await client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT, cancellationToken); - await client.SendAsync(buffer, SocketFlags.None); -#else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); - Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + string message = PLAYER_JOIN_COMMAND + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME; + const int charSize = sizeof(char); + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; await client.SendAsync(buffer, SocketFlags.None, cancellationToken); -#endif } catch (OperationCanceledException) { @@ -227,24 +217,14 @@ public async Task PostJoinAsync() private async Task ListenForClientsAsync(CancellationToken cancellationToken) { listener = new Socket(SocketType.Stream, ProtocolType.Tcp); + listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); -#if NETFRAMEWORK - listener.Listen(int.MaxValue); -#else listener.Listen(); -#endif while (!cancellationToken.IsCancellationRequested) { Socket client; -#if NETFRAMEWORK - - try - { - client = await listener.AcceptAsync(); - } -#else try { client = await listener.AcceptAsync(cancellationToken); @@ -253,7 +233,6 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) { break; } -#endif catch (Exception ex) { PreStartup.LogException(ex, "Listener error."); @@ -285,28 +264,15 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { -#if !NETFRAMEWORK using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); -#endif while (!cancellationToken.IsCancellationRequested) { int bytesRead; -#if NETFRAMEWORK - byte[] buffer1; -#else Memory message; -#endif try { - -#if NETFRAMEWORK - buffer1 = new byte[1024]; - var message = new ArraySegment(buffer1); - bytesRead = await lpInfo.TcpClient.ReceiveAsync(message, SocketFlags.None); - } -#else message = memoryOwner.Memory[..1024]; bytesRead = await lpInfo.TcpClient.ReceiveAsync(message, SocketFlags.None, cancellationToken); } @@ -314,7 +280,6 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio { break; } -#endif catch (Exception ex) { PreStartup.LogException(ex, "Socket error with client " + lpInfo.IPAddress + "; removing."); @@ -328,11 +293,7 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio break; } -#if NETFRAMEWORK - string msg = encoding.GetString(buffer1, 0, bytesRead); -#else string msg = encoding.GetString(message.Span[..bytesRead]); -#endif string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR); string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR); @@ -443,26 +404,15 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation if (!client.Connected) return; -#if !NETFRAMEWORK using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); -#endif while (!cancellationToken.IsCancellationRequested) { int bytesRead; -#if NETFRAMEWORK - byte[] buffer1; -#else Memory message; -#endif + try { -#if NETFRAMEWORK - buffer1 = new byte[1024]; - var message = new ArraySegment(buffer1); - bytesRead = await client.ReceiveAsync(message, SocketFlags.None); - } -#else message = memoryOwner.Memory[..1024]; bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); } @@ -470,7 +420,6 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation { break; } -#endif catch (Exception ex) { Logger.Log("Reading data from the server failed! Message: " + ex.Message); @@ -480,13 +429,10 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation if (bytesRead > 0) { -#if NETFRAMEWORK - string msg = encoding.GetString(buffer1, 0, bytesRead); -#else string msg = encoding.GetString(message.Span[..bytesRead]); -#endif msg = overMessage + msg; + List commands = new List(); while (true) @@ -771,20 +717,14 @@ private async Task SendMessageToHostAsync(string message, CancellationToken canc message += ProgramConstants.LAN_MESSAGE_SEPARATOR; -#if NETFRAMEWORK - try - { - byte[] buffer1 = encoding.GetBytes(message); - var buffer = new ArraySegment(buffer1); - - await client.SendAsync(buffer, SocketFlags.None); - } -#else try { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); - Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + const int charSize = sizeof(char); + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; await client.SendAsync(buffer, SocketFlags.None, cancellationToken); @@ -792,7 +732,6 @@ private async Task SendMessageToHostAsync(string message, CancellationToken canc catch (OperationCanceledException) { } -#endif catch (Exception ex) { PreStartup.LogException(ex, "Sending message to game host failed!"); diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index 2afb44be1..f1f25d482 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -10,9 +10,7 @@ using Rampastring.Tools; using Rampastring.XNAUI; using System; -#if !NETFRAMEWORK using System.Buffers; -#endif using System.Collections.Generic; using System.Net; using System.Net.Sockets; @@ -131,19 +129,15 @@ public async Task SetUpAsync(bool isHost, Socket client, int loadedGameId) ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + loadedGameId; -#if NETFRAMEWORK - byte[] buffer1 = encoding.GetBytes(message); - var buffer = new ArraySegment(buffer1); - - await this.client.SendAsync(buffer, SocketFlags.None); -#else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); - Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + const int charSize = sizeof(char); + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; await this.client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); -#endif var fhc = new FileHashCalculator(); fhc.CalculateHashes(gameModes); @@ -179,22 +173,12 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) { listener = new Socket(SocketType.Stream, ProtocolType.Tcp); listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); -#if NETFRAMEWORK - listener.Listen(int.MaxValue); -#else listener.Listen(); -#endif while (!cancellationToken.IsCancellationRequested) { Socket newPlayerSocket; -#if NETFRAMEWORK - try - { - newPlayerSocket = await listener.AcceptAsync(); - } -#else try { newPlayerSocket = await listener.AcceptAsync(cancellationToken); @@ -203,7 +187,6 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) { break; } -#endif catch (Exception ex) { PreStartup.LogException(ex, "Listener error."); @@ -228,27 +211,15 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio { try { -#if !NETFRAMEWORK using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); -#endif while (!cancellationToken.IsCancellationRequested) { int bytesRead; -#if NETFRAMEWORK - byte[] buffer1; -#else Memory message; -#endif try { -#if NETFRAMEWORK - buffer1 = new byte[1024]; - var message = new ArraySegment(buffer1); - bytesRead = await client.ReceiveAsync(message, SocketFlags.None); - } -#else message = memoryOwner.Memory[..1024]; bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); } @@ -256,7 +227,6 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio { break; } -#endif catch (Exception ex) { PreStartup.LogException(ex, "Socket error with client " + lpInfo.IPAddress + "; removing."); @@ -270,11 +240,7 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio break; } -#if NETFRAMEWORK - string msg = encoding.GetString(buffer1, 0, bytesRead); -#else string msg = encoding.GetString(message.Span[..bytesRead]); -#endif string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR); string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR); @@ -393,27 +359,15 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation if (!client.Connected) return; -#if !NETFRAMEWORK using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); -#endif while (!cancellationToken.IsCancellationRequested) { int bytesRead; -#if NETFRAMEWORK - byte[] buffer1; -#else Memory message; -#endif try { -#if NETFRAMEWORK - buffer1 = new byte[1024]; - var message = new ArraySegment(buffer1); - bytesRead = await client.ReceiveAsync(message, SocketFlags.None); - } -#else message = memoryOwner.Memory[..1024]; bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); } @@ -421,7 +375,6 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation { break; } -#endif catch (Exception ex) { Logger.Log("Reading data from the server failed! Message: " + ex.Message); @@ -431,13 +384,10 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation if (bytesRead > 0) { -#if NETFRAMEWORK - string msg = encoding.GetString(buffer1, 0, bytesRead); -#else string msg = encoding.GetString(message.Span[..bytesRead]); -#endif msg = overMessage + msg; + List commands = new List(); while (true) @@ -719,18 +669,12 @@ private async Task SendMessageToHostAsync(string message, CancellationToken canc message += ProgramConstants.LAN_MESSAGE_SEPARATOR; -#if NETFRAMEWORK - byte[] buffer1 = encoding.GetBytes(message); - var buffer = new ArraySegment(buffer1); - - try - { - await client.SendAsync(buffer, SocketFlags.None); - } -#else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); - Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + const int charSize = sizeof(char); + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; try @@ -740,7 +684,6 @@ private async Task SendMessageToHostAsync(string message, CancellationToken canc catch (OperationCanceledException) { } -#endif catch (Exception ex) { PreStartup.LogException(ex, "Sending message to game host failed!"); diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index f5d304711..c128bc6dc 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -14,9 +14,7 @@ using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; -#if !NETFRAMEWORK using System.Buffers; -#endif using System.Collections.Generic; using System.IO; using System.Linq; @@ -274,19 +272,9 @@ private async Task WindowManager_GameClosingAsync(CancellationToken cancellation if (socket.IsBound) { -#if NETFRAMEWORK - try - { -#endif await SendMessageAsync("QUIT", cancellationToken); cancellationTokenSource.Cancel(); socket.Close(); -#if NETFRAMEWORK - } - catch (ObjectDisposedException) - { - } -#endif } } catch (Exception ex) @@ -395,16 +383,12 @@ private async Task SendMessageAsync(string message, CancellationToken cancellati if (!initSuccess) return; -#if NETFRAMEWORK - byte[] buffer1 = encoding.GetBytes(message); - var buffer = new ArraySegment(buffer1); - - await socket.SendToAsync(buffer, SocketFlags.None, endPoint); - } -#else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); - Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + const int charSize = sizeof(char); + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; await socket.SendToAsync(buffer, SocketFlags.None, endPoint, cancellationToken); @@ -412,7 +396,6 @@ private async Task SendMessageAsync(string message, CancellationToken cancellati catch (OperationCanceledException) { } -#endif catch (Exception ex) { PreStartup.HandleException(ex); @@ -423,27 +406,15 @@ private async Task ListenAsync(CancellationToken cancellationToken) { try { -#if !NETFRAMEWORK using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); -#endif while (!cancellationToken.IsCancellationRequested) { EndPoint ep = new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT); -#if NETFRAMEWORK - byte[] buffer1 = new byte[4096]; - var buffer = new ArraySegment(buffer1); - SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep); -#else Memory buffer = memoryOwner.Memory[..4096]; SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep, cancellationToken); -#endif var iep = (IPEndPoint)socketReceiveFromResult.RemoteEndPoint; -#if NETFRAMEWORK - string data = encoding.GetString(buffer1, 0, socketReceiveFromResult.ReceivedBytes); -#else string data = encoding.GetString(buffer.Span[..socketReceiveFromResult.ReceivedBytes]); -#endif if (data == string.Empty) continue; @@ -638,11 +609,9 @@ private async Task LbGameList_DoubleLeftClickAsync() try { var client = new Socket(SocketType.Stream, ProtocolType.Tcp); -#if NETFRAMEWORK - await client.ConnectAsync(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT)); -#else await client.ConnectAsync(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT), CancellationToken.None); -#endif + + const int charSize = sizeof(char); if (hg.IsLoadedGame) { @@ -655,20 +624,13 @@ private async Task LbGameList_DoubleLeftClickAsync() string message = "JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + loadedGameId + ProgramConstants.LAN_MESSAGE_SEPARATOR; -#if NETFRAMEWORK - byte[] buffer1 = encoding.GetBytes(message); - var buffer = new ArraySegment(buffer1); - - await client.SendAsync(buffer, SocketFlags.None); -#else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); - Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); buffer = buffer[..bytes]; await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); -#endif - await lanGameLoadingLobby.PostJoinAsync(); } else @@ -678,20 +640,13 @@ private async Task LbGameList_DoubleLeftClickAsync() string message = "JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_MESSAGE_SEPARATOR; -#if NETFRAMEWORK - byte[] buffer1 = encoding.GetBytes(message); - var buffer = new ArraySegment(buffer1); - - await client.SendAsync(buffer, SocketFlags.None); -#else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); - Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); buffer = buffer[..bytes]; await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); -#endif - await lanGameLobby.PostJoinAsync(); } } diff --git a/DXMainClient/Domain/DiscordHandler.cs b/DXMainClient/Domain/DiscordHandler.cs index b87e2bbfa..bb424b6a3 100644 --- a/DXMainClient/Domain/DiscordHandler.cs +++ b/DXMainClient/Domain/DiscordHandler.cs @@ -2,9 +2,7 @@ using ClientCore; using DiscordRPC; using DiscordRPC.Message; -using Microsoft.Xna.Framework; using Rampastring.Tools; -using Rampastring.XNAUI; using System.Text.RegularExpressions; namespace DTAClient.Domain @@ -12,7 +10,7 @@ namespace DTAClient.Domain /// /// A class for handling Discord integration. /// - public class DiscordHandler: IDisposable + public class DiscordHandler : IDisposable { private DiscordRpcClient client; @@ -313,4 +311,4 @@ public void Dispose() client.Dispose(); } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/AllianceHolder.cs b/DXMainClient/Domain/Multiplayer/AllianceHolder.cs index 3f7929695..ba811338f 100644 --- a/DXMainClient/Domain/Multiplayer/AllianceHolder.cs +++ b/DXMainClient/Domain/Multiplayer/AllianceHolder.cs @@ -10,12 +10,11 @@ public static class AllianceHolder { public static void WriteInfoToSpawnIni( List players, - List aiPlayers, + List aiPlayers, List multiCmbIndexes, List playerHouseInfos, List teamStartMappings, - IniFile spawnIni - ) + IniFile spawnIni) { List team1MultiMemberIds = new List(); List team2MultiMemberIds = new List(); @@ -58,7 +57,6 @@ IniFile spawnIni if (teamId <= 0) teamId = teamStartMappings?.Find(sa => sa.StartingWaypoint == phi.StartingWaypoint)?.TeamId ?? 0; - if (teamId > 0) { switch (teamId) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index aa50cbb3c..28de11bdc 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -48,18 +48,12 @@ private static async Task GetCnCNetPlayerCountAsync() { var httpClientHandler = new HttpClientHandler { -#if NETFRAMEWORK - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate -#else AutomaticDecompression = DecompressionMethods.All -#endif }; using var client = new HttpClient(httpClientHandler, true) { Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT), -#if !NETFRAMEWORK DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher -#endif }; string info = await client.GetStringAsync("https://api.cncnet.org/status"); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index eba3b2f79..504b586f4 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -1,8 +1,6 @@ using Rampastring.Tools; using System; -#if !NETFRAMEWORK using System.Buffers; -#endif using System.Collections.Generic; using System.Globalization; using System.Net; @@ -37,11 +35,7 @@ public static CnCNetTunnel Parse(string str) string addressAndPort = parts[0]; string secondaryAddress = parts.Length > 12 ? parts[12] : null; int version = int.Parse(parts[10], CultureInfo.InvariantCulture); -#if NETFRAMEWORK - string primaryAddress = addressAndPort.Substring(0, addressAndPort.LastIndexOf(':')); -#else string primaryAddress = addressAndPort[..addressAndPort.LastIndexOf(':')]; -#endif var primaryIpAddress = IPAddress.Parse(primaryAddress); IPAddress secondaryIpAddress = string.IsNullOrWhiteSpace(secondaryAddress) ? null : IPAddress.Parse(secondaryAddress); @@ -57,11 +51,7 @@ public static CnCNetTunnel Parse(string str) throw new($"No supported IP address found ({nameof(Socket.OSSupportsIPv6)}={Socket.OSSupportsIPv6}," + $" {nameof(Socket.OSSupportsIPv4)}={Socket.OSSupportsIPv4}) for {str}."); -#if NETFRAMEWORK - tunnel.Port = int.Parse(addressAndPort.Substring(addressAndPort.LastIndexOf(':') + 1), CultureInfo.InvariantCulture); -#else tunnel.Port = int.Parse(addressAndPort[(addressAndPort.LastIndexOf(':') + 1)..], CultureInfo.InvariantCulture); -#endif tunnel.Country = parts[1]; tunnel.CountryCode = parts[2]; tunnel.Name = parts[3] + " V" + version; @@ -134,18 +124,12 @@ public async Task> GetPlayerPortInfoAsync(int playerCount) var httpClientHandler = new HttpClientHandler { -#if NETFRAMEWORK - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate -#else AutomaticDecompression = DecompressionMethods.All -#endif }; using var client = new HttpClient(httpClientHandler, true) { Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT), -#if !NETFRAMEWORK DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher -#endif }; string data = await client.GetStringAsync(addressString); @@ -182,22 +166,13 @@ public async Task UpdatePingAsync() try { EndPoint ep = new IPEndPoint(IPAddress, Port); -#if NETFRAMEWORK - byte[] buffer1 = new byte[PING_PACKET_SEND_SIZE]; - var buffer = new ArraySegment(buffer1); -#else using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(PING_PACKET_SEND_SIZE); Memory buffer = memoryOwner.Memory[..PING_PACKET_SEND_SIZE]; -#endif - long ticks = DateTime.Now.Ticks; + await socket.SendToAsync(buffer, SocketFlags.None, ep); -#if NETFRAMEWORK - buffer = new ArraySegment(buffer1, 0, PING_PACKET_RECEIVE_SIZE); -#else buffer = buffer[..PING_PACKET_RECEIVE_SIZE]; -#endif await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs index 95f219e26..e177f5240 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs @@ -99,11 +99,7 @@ public void Clear() } } -#if NETFRAMEWORK - public async Task PlayerConnection_PacketReceivedAsync(TunneledPlayerConnection sender, byte[] data) -#else public async Task PlayerConnection_PacketReceivedAsync(TunneledPlayerConnection sender, ReadOnlyMemory data) -#endif { await locker.WaitAsync(); @@ -118,11 +114,7 @@ public async Task PlayerConnection_PacketReceivedAsync(TunneledPlayerConnection } } -#if NETFRAMEWORK - public async Task TunnelConnection_MessageReceivedAsync(byte[] data, uint senderId) -#else public async Task TunnelConnection_MessageReceivedAsync(ReadOnlyMemory data, uint senderId) -#endif { await locker.WaitAsync(); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs index 62a4c16f0..0515628d7 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs @@ -162,19 +162,13 @@ private static HttpClient GetHttpClient() { var httpClientHandler = new HttpClientHandler { -#if NETFRAMEWORK - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate -#else AutomaticDecompression = DecompressionMethods.All -#endif }; return new HttpClient(httpClientHandler, true) { Timeout = TimeSpan.FromMilliseconds(10000), -#if !NETFRAMEWORK DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher -#endif }; } @@ -279,24 +273,6 @@ public static string GetMapFileName(string sha1, string mapName) return (null, true); } -#if NETFRAMEWORK - private sealed class FileToUpload - { - public FileToUpload(string name, string filename, string contentType, Stream stream) - { - Name = name; - Filename = filename; - ContentType = contentType; - Stream = stream; - } - - public string Name { get; set; } - public string Filename { get; set; } - public string ContentType { get; set; } - public Stream Stream { get; set; } - } -#else private readonly record struct FileToUpload(string Name, string Filename, string ContentType, Stream Stream); -#endif } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 9324f7b70..576f90504 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -156,22 +156,15 @@ private async Task PingCurrentTunnelAsync(bool checkTunnelList = false) private static async Task> DoRefreshTunnelsAsync() { FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); - List returnValue = new List(); var httpClientHandler = new HttpClientHandler { -#if NETFRAMEWORK - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate -#else AutomaticDecompression = DecompressionMethods.All -#endif }; using var client = new HttpClient(httpClientHandler, true) { Timeout = TimeSpan.FromSeconds(100), -#if !NETFRAMEWORK DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher -#endif }; string data; @@ -199,11 +192,7 @@ private static async Task> DoRefreshTunnelsAsync() } Logger.Log("Fetching tunnel server list failed. Using cached tunnel data."); -#if NETFRAMEWORK - data = File.ReadAllText(tunnelCacheFile.FullName); -#else data = await File.ReadAllTextAsync(tunnelCacheFile.FullName); -#endif } } @@ -247,11 +236,7 @@ private static async Task> DoRefreshTunnelsAsync() if (!clientDirectoryInfo.Exists) clientDirectoryInfo.Create(); -#if NETFRAMEWORK - File.WriteAllText(tunnelCacheFile.FullName, data); -#else await File.WriteAllTextAsync(tunnelCacheFile.FullName, data); -#endif } catch (Exception ex) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index 9ed2c999d..2607ba0fb 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -1,9 +1,5 @@ using System; -#if !NETFRAMEWORK using System.Buffers; -#else -using ClientCore; -#endif using System.Net; using System.Net.Sockets; using System.Threading; @@ -84,11 +80,7 @@ public void CreateSocket() endPoint = new IPEndPoint(IPAddress.Loopback, 0); // Disable ICMP port not reachable exceptions, happens when the game is still loading and has not yet opened the socket. -#if !NETFRAMEWORK if (OperatingSystem.IsWindows()) -#else - if (!ProgramConstants.ISMONO) -#endif socket.IOControl(unchecked((int)SIO_UDP_CONNRESET), new byte[] { 0 }, null); socket.Bind(endPoint); @@ -101,13 +93,9 @@ public async Task StartAsync(int gamePort) try { remoteEndPoint = new IPEndPoint(IPAddress.Loopback, gamePort); -#if NETFRAMEWORK - byte[] buffer1 = new byte[128]; - var buffer = new ArraySegment(buffer1); -#else + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(128); Memory buffer = memoryOwner.Memory[..128]; -#endif socket.ReceiveTimeout = Timeout; @@ -119,15 +107,7 @@ public async Task StartAsync(int gamePort) break; SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint); - -#if NETFRAMEWORK - byte[] data = new byte[socketReceiveFromResult.ReceivedBytes]; - Array.Copy(buffer1, data, socketReceiveFromResult.ReceivedBytes); - Array.Clear(buffer1, 0, socketReceiveFromResult.ReceivedBytes); -#else - Memory data = buffer[..socketReceiveFromResult.ReceivedBytes]; -#endif await gameTunnelHandler.PlayerConnection_PacketReceivedAsync(this, data); } @@ -155,15 +135,8 @@ public async Task StartAsync(int gamePort) } } -#if NETFRAMEWORK - public async Task SendPacketAsync(byte[] buffer) - { - var packet = new ArraySegment(buffer); - -#else public async Task SendPacketAsync(ReadOnlyMemory packet) { -#endif await locker.WaitAsync(); try diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index 0aef315e8..470e567db 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -1,8 +1,6 @@ using Rampastring.Tools; using System; -#if !NETFRAMEWORK using System.Buffers; -#endif using System.Net; using System.Net.Sockets; using System.Threading; @@ -80,15 +78,11 @@ public async Task ConnectAsync() try { -#if NETFRAMEWORK - byte[] buffer1 = new byte[50]; - WriteSenderIdToBuffer(buffer1); - var buffer = new ArraySegment(buffer1); -#else using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); Memory buffer = memoryOwner.Memory[..50]; - if (!BitConverter.TryWriteBytes(buffer.Span[..4], SenderId)) throw new Exception(); -#endif + + if (!BitConverter.TryWriteBytes(buffer.Span[..4], SenderId)) + throw new Exception(); await tunnelSocket.SendToAsync(buffer, SocketFlags.None, tunnelEndPoint); @@ -112,19 +106,12 @@ public async Task ConnectAsync() PreStartup.HandleException(ex); } } -#if NETFRAMEWORK - - private void WriteSenderIdToBuffer(byte[] buffer) => - Array.Copy(BitConverter.GetBytes(SenderId), buffer, sizeof(uint)); -#endif private async Task ReceiveLoopAsync() { try { -#if !NETFRAMEWORK using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); -#endif while (true) { @@ -135,13 +122,7 @@ private async Task ReceiveLoopAsync() return; } -#if NETFRAMEWORK - byte[] buffer1 = new byte[1024]; - var buffer = new ArraySegment(buffer1); -#else Memory buffer = memoryOwner.Memory[..1024]; -#endif - SocketReceiveFromResult socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync(buffer, SocketFlags.None, tunnelEndPoint); if (socketReceiveFromResult.ReceivedBytes < 8) @@ -150,14 +131,8 @@ private async Task ReceiveLoopAsync() continue; } -#if NETFRAMEWORK - byte[] data = new byte[socketReceiveFromResult.ReceivedBytes - 8]; - Array.Copy(buffer1, 8, data, 0, data.Length); - uint senderId = BitConverter.ToUInt32(buffer1, 0); -#else Memory data = buffer[8..socketReceiveFromResult.ReceivedBytes]; uint senderId = BitConverter.ToUInt32(buffer[..4].Span); -#endif await gameTunnelHandler.TunnelConnection_MessageReceivedAsync(data, senderId); } @@ -189,23 +164,20 @@ private void DoClose() Logger.Log("Connection to tunnel server closed."); } -#if NETFRAMEWORK - public async Task SendDataAsync(byte[] data, uint receiverId) - { - byte[] buffer = new byte[data.Length + 8]; // 8 = sizeof(uint) * 2 - WriteSenderIdToBuffer(buffer); - Array.Copy(BitConverter.GetBytes(receiverId), 0, buffer, 4, sizeof(uint)); - Array.Copy(data, 0, buffer, 8, data.Length); - var packet = new ArraySegment(buffer); -#else public async Task SendDataAsync(ReadOnlyMemory data, uint receiverId) { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(data.Length + 8); - Memory packet = memoryOwner.Memory[..(data.Length + 8)]; - if (!BitConverter.TryWriteBytes(packet.Span[..4], SenderId)) throw new Exception(); - if (!BitConverter.TryWriteBytes(packet.Span[4..8], receiverId)) throw new Exception(); + const int idsSize = sizeof(uint) * 2; + int bufferSize = data.Length + idsSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory packet = memoryOwner.Memory[..bufferSize]; + + if (!BitConverter.TryWriteBytes(packet.Span[..4], SenderId)) + throw new Exception(); + + if (!BitConverter.TryWriteBytes(packet.Span[4..8], receiverId)) + throw new Exception(); + data.CopyTo(packet[8..]); -#endif await locker.WaitAsync(); diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index 1872ee671..9afde5690 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -1,9 +1,7 @@ using ClientCore; using Microsoft.Xna.Framework; using System; -#if !NETFRAMEWORK using System.Buffers; -#endif using System.Collections.Generic; using System.Net; using System.Net.Sockets; @@ -100,18 +98,12 @@ public async Task SendMessageAsync(string message, CancellationToken cancellatio { message += ProgramConstants.LAN_MESSAGE_SEPARATOR; -#if NETFRAMEWORK - byte[] buffer1 = encoding.GetBytes(message); - var buffer = new ArraySegment(buffer1); - - try - { - await TcpClient.SendAsync(buffer, SocketFlags.None); - } -#else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); - Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + const int charSize = sizeof(char); + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; try @@ -121,7 +113,6 @@ public async Task SendMessageAsync(string message, CancellationToken cancellatio catch (OperationCanceledException) { } -#endif catch (Exception ex) { PreStartup.LogException(ex, "Sending message to " + ToString() + " failed!"); @@ -147,21 +138,11 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) { try { -#if !NETFRAMEWORK using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); -#endif while (!cancellationToken.IsCancellationRequested) { int bytesRead; -#if NETFRAMEWORK - byte[] buffer1 = new byte[1024]; - var message = new ArraySegment(buffer1); - try - { - bytesRead = await TcpClient.ReceiveAsync(message, SocketFlags.None); - } -#else Memory message = memoryOwner.Memory[..1024]; try @@ -173,7 +154,6 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) ConnectionLost?.Invoke(this, EventArgs.Empty); break; } -#endif catch (Exception ex) { PreStartup.LogException(ex, "Socket error with client " + Name + "; removing."); @@ -183,13 +163,10 @@ private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) if (bytesRead > 0) { -#if NETFRAMEWORK - string msg = encoding.GetString(buffer1, 0, bytesRead); -#else string msg = encoding.GetString(message.Span[..bytesRead]); -#endif msg = overMessage + msg; + List commands = new List(); while (true) diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index 2852dd720..cbf5d2464 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -2,9 +2,7 @@ using ClientCore.Extensions; using Rampastring.Tools; using System; -#if !NETFRAMEWORK using System.Buffers; -#endif using System.Collections.Generic; using System.IO; using System.Linq; @@ -154,10 +152,6 @@ private async Task ConnectToServerAsync(CancellationToken cancellationToken) Logger.Log("Attempting connection to " + server.Host + ":" + port); -#if NETFRAMEWORK - IAsyncResult result = client.BeginConnect(server.Host, port, null, null); - result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(3), false); -#else try { await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(server.Host), port), @@ -165,7 +159,6 @@ await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(server.Host), port), } catch (OperationCanceledException) { } -#endif if (!client.Connected) { @@ -174,9 +167,6 @@ await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(server.Host), port), } Logger.Log("Succesfully connected to " + server.Host + " on port " + port); -#if NETFRAMEWORK - client.EndConnect(result); -#endif _isConnected = true; _attemptingConnection = false; @@ -221,13 +211,8 @@ await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(server.Host), port), private async Task HandleCommAsync(CancellationToken cancellationToken) { int errorTimes = 0; -#if NETFRAMEWORK - byte[] message1 = new byte[1024]; - var message = new ArraySegment(message1); -#else using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); Memory message = memoryOwner.Memory[..1024]; -#endif await RegisterAsync(); @@ -246,17 +231,12 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) try { -#if NETFRAMEWORK - bytesRead = await socket.ReceiveAsync(message, SocketFlags.None); - } -#else bytesRead = await socket.ReceiveAsync(message, SocketFlags.None, cancellationToken); } catch (OperationCanceledException) { break; } -#endif catch (Exception ex) { PreStartup.LogException(ex, "Disconnected from CnCNet due to a socket error."); @@ -277,11 +257,7 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) errorTimes = 0; // A message has been successfully received -#if NETFRAMEWORK - string msg = encoding.GetString(message1, 0, bytesRead); -#else string msg = encoding.GetString(message.Span[..bytesRead]); -#endif Logger.Log("Message received: " + msg); @@ -969,23 +945,17 @@ private async Task SendMessageAsync(string message) Logger.Log("SRM: " + message); -#if NETFRAMEWORK - byte[] buffer1 = encoding.GetBytes(message + "\r\n"); - var buffer = new ArraySegment(buffer1); - - try - { - await socket.SendAsync(buffer, SocketFlags.None); -#else - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(message.Length * 2); - Memory buffer = memoryOwner.Memory[..(message.Length * 2)]; + const int charSize = sizeof(char); + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; int bytes = encoding.GetBytes((message + "\r\n").AsSpan(), buffer.Span); + buffer = buffer[..bytes]; try { await socket.SendAsync(buffer, SocketFlags.None, CancellationToken.None); -#endif } catch (IOException ex) { @@ -1064,7 +1034,6 @@ public async Task QueueMessageAsync(QueuedMessage qm) MessageQueue.Insert(placeInQueue, qm); break; } - } finally { From 3edfa0d00ca07afe1961172faadd4a8d2ac95617 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 02:00:15 +0100 Subject: [PATCH 036/109] Use SocketsHttpHandler instead of HttpClientHandler --- ClientCore/ClientConfiguration.cs | 6 ++-- ClientCore/Extensions/StringExtensions.cs | 10 +++--- .../DXGUI/Generic/PrivacyNotification.cs | 7 ++-- DXMainClient/Domain/MainClientConstants.cs | 2 +- .../CnCNet/CnCNetPlayerCountTask.cs | 14 ++++---- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 17 +++++----- .../Domain/Multiplayer/CnCNet/MapSharer.cs | 32 ++++++++++--------- .../Multiplayer/CnCNet/TunnelHandler.cs | 14 ++++---- DXMainClient/Startup.cs | 9 ++---- 9 files changed, 57 insertions(+), 54 deletions(-) diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 5f172d05e..0dd3b14ec 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -209,13 +209,13 @@ public void RefreshSettings() public string LongGameName => clientDefinitionsIni.GetStringValue(SETTINGS, "LongGameName", "Tiberian Sun"); - public string LongSupportURL => clientDefinitionsIni.GetStringValue(SETTINGS, "LongSupportURL", "http://www.moddb.com/members/rampastring"); + public string LongSupportURL => clientDefinitionsIni.GetStringValue(SETTINGS, "LongSupportURL", $"{Uri.UriSchemeHttps}://www.moddb.com/members/rampastring"); public string ShortSupportURL => clientDefinitionsIni.GetStringValue(SETTINGS, "ShortSupportURL", "www.moddb.com/members/rampastring"); - public string ChangelogURL => clientDefinitionsIni.GetStringValue(SETTINGS, "ChangelogURL", "http://www.moddb.com/mods/the-dawn-of-the-tiberium-age/tutorials/change-log"); + public string ChangelogURL => clientDefinitionsIni.GetStringValue(SETTINGS, "ChangelogURL", $"{Uri.UriSchemeHttps}://www.moddb.com/mods/the-dawn-of-the-tiberium-age/tutorials/change-log"); - public string CreditsURL => clientDefinitionsIni.GetStringValue(SETTINGS, "CreditsURL", "http://www.moddb.com/mods/the-dawn-of-the-tiberium-age/tutorials/credits#Rampastring"); + public string CreditsURL => clientDefinitionsIni.GetStringValue(SETTINGS, "CreditsURL", $"{Uri.UriSchemeHttps}://www.moddb.com/mods/the-dawn-of-the-tiberium-age/tutorials/credits#Rampastring"); public string ManualDownloadURL => clientDefinitionsIni.GetStringValue(SETTINGS, "ManualDownloadURL", string.Empty); diff --git a/ClientCore/Extensions/StringExtensions.cs b/ClientCore/Extensions/StringExtensions.cs index 11a9d2368..31fb5d6a8 100644 --- a/ClientCore/Extensions/StringExtensions.cs +++ b/ClientCore/Extensions/StringExtensions.cs @@ -10,11 +10,11 @@ public static string GetLink(this string text) if (string.IsNullOrWhiteSpace(text)) return null; - int index = text.IndexOf("http://", StringComparison.Ordinal); - if (index == -1) - index = text.IndexOf("ftp://", StringComparison.Ordinal); - if (index == -1) - index = text.IndexOf("https://", StringComparison.Ordinal); + int index = text.IndexOf($"{Uri.UriSchemeHttp}://", StringComparison.Ordinal); + if (index == -1) + index = text.IndexOf($"{Uri.UriSchemeFtp}://", StringComparison.Ordinal); + if (index == -1) + index = text.IndexOf($"{Uri.UriSchemeHttps}://", StringComparison.Ordinal); if (index == -1) return null; // No link found diff --git a/DXMainClient/DXGUI/Generic/PrivacyNotification.cs b/DXMainClient/DXGUI/Generic/PrivacyNotification.cs index 1bdc4a6fe..8b3463606 100644 --- a/DXMainClient/DXGUI/Generic/PrivacyNotification.cs +++ b/DXMainClient/DXGUI/Generic/PrivacyNotification.cs @@ -1,4 +1,5 @@ -using ClientCore; +using System; +using ClientCore; using ClientGUI; using ClientCore.Extensions; using Microsoft.Xna.Framework; @@ -42,7 +43,7 @@ public override void Initialize() lblTermsAndConditions.Name = nameof(lblTermsAndConditions); lblTermsAndConditions.X = lblMoreInformation.Right + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN; lblTermsAndConditions.Y = lblMoreInformation.Y; - lblTermsAndConditions.Text = "https://cncnet.org/terms-and-conditions"; + lblTermsAndConditions.Text = $"{Uri.UriSchemeHttps}://cncnet.org/terms-and-conditions"; lblTermsAndConditions.LeftClick += (s, e) => ProcessLauncher.StartShellProcess(lblTermsAndConditions.Text); AddChild(lblTermsAndConditions); @@ -50,7 +51,7 @@ public override void Initialize() lblPrivacyPolicy.Name = nameof(lblPrivacyPolicy); lblPrivacyPolicy.X = lblTermsAndConditions.Right + UIDesignConstants.CONTROL_HORIZONTAL_MARGIN; lblPrivacyPolicy.Y = lblMoreInformation.Y; - lblPrivacyPolicy.Text = "https://cncnet.org/privacy-policy"; + lblPrivacyPolicy.Text = $"{Uri.UriSchemeHttps}://cncnet.org/privacy-policy"; lblPrivacyPolicy.LeftClick += (s, e) => ProcessLauncher.StartShellProcess(lblPrivacyPolicy.Text); AddChild(lblPrivacyPolicy); diff --git a/DXMainClient/Domain/MainClientConstants.cs b/DXMainClient/Domain/MainClientConstants.cs index 06055f3b6..bdca2ccd0 100644 --- a/DXMainClient/Domain/MainClientConstants.cs +++ b/DXMainClient/Domain/MainClientConstants.cs @@ -9,7 +9,7 @@ public static class MainClientConstants public static string GAME_NAME_LONG = "CnCNet Client"; public static string GAME_NAME_SHORT = "CnCNet"; - public static string CREDITS_URL = "http://rampastring.cncnet.org/TS/Credits.txt"; + public static string CREDITS_URL = string.Empty; public static string SUPPORT_URL_SHORT = "www.cncnet.org"; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index 28de11bdc..5a7fe5f03 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -46,17 +46,19 @@ private static async Task GetCnCNetPlayerCountAsync() { try { - var httpClientHandler = new HttpClientHandler - { - AutomaticDecompression = DecompressionMethods.All - }; - using var client = new HttpClient(httpClientHandler, true) + using var client = new HttpClient( + new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + AutomaticDecompression = DecompressionMethods.All + }, + true) { Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT), DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; - string info = await client.GetStringAsync("https://api.cncnet.org/status"); + string info = await client.GetStringAsync($"{Uri.UriSchemeHttps}://api.cncnet.org/status"); info = info.Replace("{", String.Empty); info = info.Replace("}", String.Empty); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 504b586f4..918fada2d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -113,25 +113,26 @@ private set public async Task> GetPlayerPortInfoAsync(int playerCount) { if (Version != Constants.TUNNEL_VERSION_2) - throw new InvalidOperationException($"GetPlayerPortInfo only works with version {Constants.TUNNEL_VERSION_2} tunnels."); + throw new InvalidOperationException($"{nameof(GetPlayerPortInfoAsync)} only works with version {Constants.TUNNEL_VERSION_2} tunnels."); try { Logger.Log($"Contacting tunnel at {Address}:{Port}"); - string addressString = $"http://{Address}:{Port}/request?clients={playerCount}"; + string addressString = $"{Uri.UriSchemeHttp}://{Address}:{Port}/request?clients={playerCount}"; Logger.Log($"Downloading from {addressString}"); - var httpClientHandler = new HttpClientHandler - { - AutomaticDecompression = DecompressionMethods.All - }; - using var client = new HttpClient(httpClientHandler, true) + using var client = new HttpClient( + new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + AutomaticDecompression = DecompressionMethods.All + }, + true) { Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT), DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; - string data = await client.GetStringAsync(addressString); data = data.Replace("[", string.Empty); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs index 0515628d7..c134efa14 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs @@ -36,7 +36,7 @@ public static class MapSharer private static readonly object locker = new(); - private const string MAPDB_URL = "https://mapdb.cncnet.org/upload"; + private const string MAPDB_URL = "https://mapdb.cncnet.org/"; /// /// Adds a map into the CnCNet map upload queue. @@ -66,7 +66,7 @@ private static async Task UploadAsync(Map map, string myGameId) Logger.Log("MapSharer: Starting upload of " + map.BaseFilePath); - (string message, bool success) = await MapUploadAsync(MAPDB_URL, map, myGameId); + (string message, bool success) = await MapUploadAsync(map, myGameId); if (success) { @@ -101,7 +101,7 @@ private static async Task UploadAsync(Map map, string myGameId) } } - private static async Task<(string Message, bool Success)> MapUploadAsync(string address, Map map, string gameName) + private static async Task<(string Message, bool Success)> MapUploadAsync(Map map, string gameName) { using MemoryStream zipStream = CreateZipFile(map.CompleteFilePath); @@ -115,7 +115,7 @@ private static async Task UploadAsync(Map map, string myGameId) { { "game", gameName.ToLower() } }; - string response = await UploadFilesAsync(address, files, values); + string response = await UploadFilesAsync(files, values); if (!response.Contains("Upload succeeded!")) return (response, false); @@ -131,7 +131,7 @@ private static async Task UploadAsync(Map map, string myGameId) } } - private static async Task UploadFilesAsync(string address, List files, NameValueCollection values) + private static async Task UploadFilesAsync(List files, NameValueCollection values) { using HttpClient client = GetHttpClient(); @@ -153,21 +153,23 @@ private static async Task UploadFilesAsync(string address, List mapName + "_" + sha1; + => FormattableString.Invariant($"{mapName}_{sha1}"); private static async Task<(string Error, bool Success)> DownloadMainAsync(string sha1, string myGame, string mapName) { @@ -257,8 +259,8 @@ public static string GetMapFileName(string sha1, string mapName) try { - string address = "https://mapdb.cncnet.org/" + myGame + "/" + sha1 + ".zip"; - Logger.Log("MapSharer: Downloading URL: " + address); + string address = FormattableString.Invariant($"{myGame}/{sha1}.zip"); + Logger.Log($"MapSharer: Downloading URL: {MAPDB_URL}{address})"); stream = await client.GetStreamAsync(address); } catch (Exception ex) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 576f90504..ed5bc1269 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -156,12 +156,14 @@ private async Task PingCurrentTunnelAsync(bool checkTunnelList = false) private static async Task> DoRefreshTunnelsAsync() { FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); - List returnValue = new List(); - var httpClientHandler = new HttpClientHandler - { - AutomaticDecompression = DecompressionMethods.All - }; - using var client = new HttpClient(httpClientHandler, true) + var returnValue = new List(); + using var client = new HttpClient( + new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + AutomaticDecompression = DecompressionMethods.All + }, + true) { Timeout = TimeSpan.FromSeconds(100), DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher diff --git a/DXMainClient/Startup.cs b/DXMainClient/Startup.cs index 77484ea9a..803dd32a2 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Threading; using Microsoft.Win32; using DTAClient.Domain; using ClientCore; @@ -25,7 +24,7 @@ namespace DTAClient /// /// A class that handles initialization of the Client. /// - public class Startup + internal sealed class Startup { /// /// The main method for startup and initialization. @@ -40,11 +39,8 @@ public void Execute() throw new DirectoryNotFoundException("Theme directory not found!" + Environment.NewLine + ProgramConstants.RESOURCES_DIR); Logger.Log("Initializing updater."); - SafePath.DeleteFileIfExists(ProgramConstants.GamePath, "version_u"); - Updater.Initialize(ProgramConstants.GamePath, ProgramConstants.GetBaseResourcePath(), ClientConfiguration.Instance.SettingsIniName, ClientConfiguration.Instance.LocalGame, SafePath.GetFile(ProgramConstants.StartupExecutable).Name); - Logger.Log("OSDescription: " + RuntimeInformation.OSDescription); Logger.Log("OSArchitecture: " + RuntimeInformation.OSArchitecture); Logger.Log("ProcessArchitecture: " + RuntimeInformation.ProcessArchitecture); @@ -57,8 +53,7 @@ public void Execute() { // The query in CheckSystemSpecifications takes lots of time, // so we'll do it in a separate thread to make startup faster - Thread thread = new Thread(CheckSystemSpecifications); - thread.Start(); + Task.Run(CheckSystemSpecifications); } GenerateOnlineIdAsync(); From 61453fca928504d3451cf35df51a5559763bbf56 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 18:12:17 +0100 Subject: [PATCH 037/109] Clean up Task Exception handling --- ClientCore/CnCNet5/NameValidator.cs | 4 +- ClientCore/Extensions/EnumExtensions.cs | 19 +- ClientCore/Extensions/StringExtensions.cs | 2 +- ClientCore/ProfanityFilter.cs | 2 +- .../GameParsers/LogFileStatisticsParser.cs | 16 +- ClientGUI/XNAClientPreferredItemDropDown.cs | 2 +- DXMainClient/DXGUI/GameClass.cs | 4 +- DXMainClient/DXGUI/Generic/LoadingScreen.cs | 15 +- DXMainClient/DXGUI/Generic/MainMenu.cs | 46 +- DXMainClient/DXGUI/Generic/TopBar.cs | 15 +- DXMainClient/DXGUI/Generic/UpdateWindow.cs | 7 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 473 +++--- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 791 ++++------ .../Multiplayer/CnCNet/GameCreationWindow.cs | 2 +- .../Multiplayer/CnCNet/GlobalContextMenu.cs | 65 +- .../CnCNet/PrivateMessagingWindow.cs | 69 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 124 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 1395 +++++++---------- .../CommandHandlers/IntCommandHandler.cs | 2 +- .../CommandHandlers/IntNotificationHandler.cs | 2 +- .../CommandHandlers/StringCommandHandler.cs | 2 +- .../Multiplayer/GameLobby/GameLobbyBase.cs | 348 ++-- .../Multiplayer/GameLobby/LANGameLobby.cs | 535 +++---- .../Multiplayer/GameLobby/MapPreviewBox.cs | 4 - .../GameLobby/MultiplayerGameLobby.cs | 690 ++++---- .../Multiplayer/GameLobby/SkirmishLobby.cs | 52 +- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 333 ++-- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 322 ++-- DXMainClient/Domain/MainClientConstants.cs | 4 +- .../CnCNet/CnCNetPlayerCountTask.cs | 2 +- .../Multiplayer/CnCNet/GameTunnelHandler.cs | 4 +- .../Multiplayer/CnCNet/TunnelHandler.cs | 52 +- .../CnCNet/TunneledPlayerConnection.cs | 55 +- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 55 +- .../LAN/ClientIntCommandHandler.cs | 2 +- .../LAN/ClientStringCommandHandler.cs | 2 +- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 121 +- DXMainClient/Domain/Multiplayer/Map.cs | 6 +- DXMainClient/Domain/Multiplayer/MapLoader.cs | 45 +- DXMainClient/Extensions/TaskExtensions.cs | 33 + DXMainClient/Online/CnCNetManager.cs | 140 +- DXMainClient/Online/Connection.cs | 142 +- DXMainClient/PreStartup.cs | 2 +- DXMainClient/Startup.cs | 2 +- 44 files changed, 2385 insertions(+), 3623 deletions(-) create mode 100644 DXMainClient/Extensions/TaskExtensions.cs diff --git a/ClientCore/CnCNet5/NameValidator.cs b/ClientCore/CnCNet5/NameValidator.cs index c0dfa731f..a71268fc2 100644 --- a/ClientCore/CnCNet5/NameValidator.cs +++ b/ClientCore/CnCNet5/NameValidator.cs @@ -21,7 +21,7 @@ public static string IsNameValid(string name) if (profanityFilter.IsOffensive(name)) return "Please enter a name that is less offensive.".L10N("Client:ClientCore:NameOffensive"); - if (int.TryParse(name.Substring(0, 1), out _)) + if (int.TryParse(name[..1], out _)) return "The first character in the player name cannot be a number.".L10N("Client:ClientCore:NameFirstIsNumber"); if (name[0] == '-') @@ -59,7 +59,7 @@ public static string GetValidOfflineName(string name) string validName = new string(name.Trim().Where(c => !disallowedCharacters.Contains(c)).ToArray()); if (validName.Length > ClientConfiguration.Instance.MaxNameLength) - return validName.Substring(0, ClientConfiguration.Instance.MaxNameLength); + return validName[..ClientConfiguration.Instance.MaxNameLength]; return validName; } diff --git a/ClientCore/Extensions/EnumExtensions.cs b/ClientCore/Extensions/EnumExtensions.cs index 6dd2f4afc..d5b961d36 100644 --- a/ClientCore/Extensions/EnumExtensions.cs +++ b/ClientCore/Extensions/EnumExtensions.cs @@ -4,21 +4,18 @@ namespace ClientCore.Extensions { public static class EnumExtensions { - public static T Next(this T src) where T : Enum + public static T Next(this T src) + where T : Enum { - T[] Arr = GetValues(src); - int nextIndex = Array.IndexOf(Arr, src) + 1; - return Arr.Length == nextIndex ? Arr[0] : Arr[nextIndex]; + T[] values = GetValues(src); + int nextIndex = Array.IndexOf(values, src) + 1; + return values.Length == nextIndex ? values[0] : values[nextIndex]; } - public static T First(this T src) where T : Enum - { - return GetValues(src)[0]; - } - - private static T[] GetValues(T src) where T : Enum + private static T[] GetValues(T src) + where T : Enum { return (T[])Enum.GetValues(src.GetType()); } } -} +} \ No newline at end of file diff --git a/ClientCore/Extensions/StringExtensions.cs b/ClientCore/Extensions/StringExtensions.cs index 31fb5d6a8..57b1a7ef5 100644 --- a/ClientCore/Extensions/StringExtensions.cs +++ b/ClientCore/Extensions/StringExtensions.cs @@ -19,7 +19,7 @@ public static string GetLink(this string text) if (index == -1) return null; // No link found - string link = text.Substring(index); + string link = text[index..]; return link.Split(' ')[0]; // Nuke any words coming after the link } diff --git a/ClientCore/ProfanityFilter.cs b/ClientCore/ProfanityFilter.cs index 87970f0dd..e6e2be844 100644 --- a/ClientCore/ProfanityFilter.cs +++ b/ClientCore/ProfanityFilter.cs @@ -80,7 +80,7 @@ private string ToRegexPattern(string wildcardSearch) regexPattern = regexPattern.Replace(@"\?", "."); if (regexPattern.StartsWith(".*?")) { - regexPattern = regexPattern.Substring(3); + regexPattern = regexPattern[3..]; regexPattern = @"(^\b)*?" + regexPattern; } regexPattern = @"\b" + regexPattern + @"\b"; diff --git a/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs b/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs index a507bd560..103edf0bb 100644 --- a/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs +++ b/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs @@ -53,7 +53,7 @@ protected override void ParseStatistics(string gamepath) { // Player found, game saw completion sawCompletion = true; - string playerName = line.Substring(0, line.Length - 7); + string playerName = line[..^7]; currentPlayer = Statistics.GetEmptyPlayerByName(playerName); if (isLoadedGame && currentPlayer == null) @@ -77,7 +77,7 @@ protected override void ParseStatistics(string gamepath) { // Player found, game saw completion sawCompletion = true; - string playerName = line.Substring(0, line.Length - 8); + string playerName = line[..^8]; currentPlayer = Statistics.GetEmptyPlayerByName(playerName); if (isLoadedGame && currentPlayer == null) @@ -103,23 +103,23 @@ protected override void ParseStatistics(string gamepath) else if (line.Contains("Game loop finished. Average FPS")) { // Game loop finished. Average FPS = - string fpsString = line.Substring(34); + string fpsString = line[34..]; Statistics.AverageFPS = Int32.Parse(fpsString); } if (currentPlayer == null || line.Length < 1) continue; - line = line.Substring(1); + line = line[1..]; if (line.StartsWith("Lost = ")) - currentPlayer.Losses = Int32.Parse(line.Substring(7)); + currentPlayer.Losses = Int32.Parse(line[7..]); else if (line.StartsWith("Kills = ")) - currentPlayer.Kills = Int32.Parse(line.Substring(8)); + currentPlayer.Kills = Int32.Parse(line[8..]); else if (line.StartsWith("Score = ")) - currentPlayer.Score = Int32.Parse(line.Substring(8)); + currentPlayer.Score = Int32.Parse(line[8..]); else if (line.StartsWith(economyString + " = ")) - currentPlayer.Economy = Int32.Parse(line.Substring(economyString.Length + 2)); + currentPlayer.Economy = Int32.Parse(line[(economyString.Length + 2)..]); } // Check empty players for take-over by AIs diff --git a/ClientGUI/XNAClientPreferredItemDropDown.cs b/ClientGUI/XNAClientPreferredItemDropDown.cs index 815c571e1..eadceb546 100644 --- a/ClientGUI/XNAClientPreferredItemDropDown.cs +++ b/ClientGUI/XNAClientPreferredItemDropDown.cs @@ -60,7 +60,7 @@ public override void Draw(GameTime gameTime) PreferredItemIndexes.ForEach(i => { XNADropDownItem preferredItem = Items[i]; - preferredItem.Text = preferredItem.Text.Substring(0, preferredItem.Text.Length - PreferredItemLabel.Length - 1); + preferredItem.Text = preferredItem.Text[..(preferredItem.Text.Length - PreferredItemLabel.Length - 1)]; }); } else diff --git a/DXMainClient/DXGUI/GameClass.cs b/DXMainClient/DXGUI/GameClass.cs index 98a56753f..534f28b7b 100644 --- a/DXMainClient/DXGUI/GameClass.cs +++ b/DXMainClient/DXGUI/GameClass.cs @@ -173,14 +173,14 @@ protected override void Initialize() if (UserINISettings.Instance.AutoRemoveUnderscoresFromName) { while (playerName.EndsWith("_")) - playerName = playerName.Substring(0, playerName.Length - 1); + playerName = playerName[..^1]; } if (string.IsNullOrEmpty(playerName)) { playerName = Environment.UserName; - playerName = playerName.Substring(playerName.IndexOf("\\") + 1); + playerName = playerName[(playerName.IndexOf("\\") + 1)..]; } playerName = Renderer.GetSafeString(NameValidator.GetValidOfflineName(playerName), 0); diff --git a/DXMainClient/DXGUI/Generic/LoadingScreen.cs b/DXMainClient/DXGUI/Generic/LoadingScreen.cs index 96347ad96..b75ae0286 100644 --- a/DXMainClient/DXGUI/Generic/LoadingScreen.cs +++ b/DXMainClient/DXGUI/Generic/LoadingScreen.cs @@ -55,9 +55,9 @@ public override void Initialize() bool initUpdater = !ClientConfiguration.Instance.ModMode; if (initUpdater) - updaterInitTask = Task.Run(InitUpdater); + updaterInitTask = Task.Run(InitUpdater).HandleTaskAsync(); - mapLoadTask = Task.Run(() => mapLoader.LoadMaps()); + mapLoadTask = mapLoader.LoadMapsAsync().HandleTaskAsync(); if (Cursor.Visible) { @@ -68,15 +68,8 @@ public override void Initialize() private void InitUpdater() { - try - { - Updater.OnLocalFileVersionsChecked += LogGameClientVersion; - Updater.CheckLocalFileVersions(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + Updater.OnLocalFileVersionsChecked += LogGameClientVersion; + Updater.CheckLocalFileVersions(); } private void LogGameClientVersion() diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 67a80ade1..2f14eae39 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -186,7 +186,7 @@ public override void Initialize() btnLan.IdleTexture = AssetLoader.LoadTexture("MainMenu/lan.png"); btnLan.HoverTexture = AssetLoader.LoadTexture("MainMenu/lan_c.png"); btnLan.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); - btnLan.LeftClick += (_, _) => BtnLan_LeftClickAsync(); + btnLan.LeftClick += (_, _) => BtnLan_LeftClickAsync().HandleTask(); btnOptions = new XNAClientButton(WindowManager); btnOptions.Name = nameof(btnOptions); @@ -305,7 +305,7 @@ public override void Initialize() cncnetPlayerCountCancellationSource = new CancellationTokenSource(); CnCNetPlayerCountTask.InitializeService(cncnetPlayerCountCancellationSource); - WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync(); + WindowManager.GameClosing += (_, _) => CleanAsync().HandleTask(); skirmishLobby.Exited += SkirmishLobby_Exited; lanLobby.Exited += LanLobby_Exited; @@ -501,8 +501,6 @@ private void FirstRunMessageBox_NoClicked(XNAMessageBox messageBox) private void SharedUILogic_GameProcessStarted() => MusicOff(); - private Task WindowManager_GameClosingAsync() => CleanAsync(); - private void SkirmishLobby_Exited(object sender, EventArgs e) { if (UserINISettings.Instance.StopMusicOnMenu) @@ -536,22 +534,15 @@ private void CnCNetInfoController_CnCNetGameCountUpdated(object sender, PlayerCo /// private async Task CleanAsync() { - try - { - Updater.FileIdentifiersUpdated -= Updater_FileIdentifiersUpdated; + Updater.FileIdentifiersUpdated -= Updater_FileIdentifiersUpdated; - if (cncnetPlayerCountCancellationSource != null) cncnetPlayerCountCancellationSource.Cancel(); - topBar.Clean(); - if (UpdateInProgress) - Updater.StopUpdate(); + if (cncnetPlayerCountCancellationSource != null) cncnetPlayerCountCancellationSource.Cancel(); + topBar.Clean(); + if (UpdateInProgress) + Updater.StopUpdate(); - if (connectionManager.IsConnected) - await connectionManager.DisconnectAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (connectionManager.IsConnected) + await connectionManager.DisconnectAsync(); } /// @@ -844,22 +835,15 @@ private void BtnLoadGame_LeftClick(object sender, EventArgs e) private async Task BtnLan_LeftClickAsync() { - try - { - await lanLobby.OpenAsync(); + await lanLobby.OpenAsync(); - if (UserINISettings.Instance.StopMusicOnMenu) - MusicOff(); + if (UserINISettings.Instance.StopMusicOnMenu) + MusicOff(); - if (connectionManager.IsConnected) - await connectionManager.DisconnectAsync(); + if (connectionManager.IsConnected) + await connectionManager.DisconnectAsync(); - topBar.SetLanMode(true); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + topBar.SetLanMode(true); } private void BtnCnCNet_LeftClick(object sender, EventArgs e) => topBar.SwitchToSecondary(); diff --git a/DXMainClient/DXGUI/Generic/TopBar.cs b/DXMainClient/DXGUI/Generic/TopBar.cs index e0681e4a6..7a7e8ba35 100644 --- a/DXMainClient/DXGUI/Generic/TopBar.cs +++ b/DXMainClient/DXGUI/Generic/TopBar.cs @@ -173,7 +173,7 @@ public override void Initialize() btnLogout.FontIndex = 1; btnLogout.Text = "Log Out".L10N("Client:Main:TopBarLogOut"); btnLogout.AllowClick = false; - btnLogout.LeftClick += (_, _) => BtnLogout_LeftClickAsync(); + btnLogout.LeftClick += (_, _) => BtnLogout_LeftClickAsync().HandleTask(); btnOptions = new XNAClientButton(WindowManager); btnOptions.Name = "btnOptions"; @@ -291,16 +291,9 @@ private void ConnectionEvent(string text) private async Task BtnLogout_LeftClickAsync() { - try - { - await connectionManager.DisconnectAsync(); - LogoutEvent?.Invoke(this, null); - SwitchToPrimary(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await connectionManager.DisconnectAsync(); + LogoutEvent?.Invoke(this, null); + SwitchToPrimary(); } private void ConnectionManager_Connected(object sender, EventArgs e) diff --git a/DXMainClient/DXGUI/Generic/UpdateWindow.cs b/DXMainClient/DXGUI/Generic/UpdateWindow.cs index 44b03a8dd..01eb67033 100644 --- a/DXMainClient/DXGUI/Generic/UpdateWindow.cs +++ b/DXMainClient/DXGUI/Generic/UpdateWindow.cs @@ -26,15 +26,12 @@ public class UpdateWindow : XNAWindow public delegate void UpdateFailureEventHandler(object sender, UpdateFailureEventArgs e); public event UpdateFailureEventHandler UpdateFailed; - delegate void UpdateProgressChangedDelegate(string fileName, int filePercentage, int totalPercentage); - delegate void FileDownloadCompletedDelegate(string archiveName); - private const double DOT_TIME = 0.66; private const int MAX_DOTS = 5; - public UpdateWindow(WindowManager windowManager) : base(windowManager) + public UpdateWindow(WindowManager windowManager) + : base(windowManager) { - } private XNALabel lblDescription; diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index 2e7043d8c..5ec64e704 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -46,8 +46,8 @@ public CnCNetGameLoadingLobby( TunnelHandler tunnelHandler, MapLoader mapLoader, GameCollection gameCollection, - DiscordHandler discordHandler - ) : base(windowManager, discordHandler) + DiscordHandler discordHandler) + : base(windowManager, discordHandler) { this.connectionManager = connectionManager; this.tunnelHandler = tunnelHandler; @@ -57,15 +57,15 @@ DiscordHandler discordHandler ctcpCommandHandlers = new CommandHandlerBase[] { - new NoParamCommandHandler(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, sender => HandleNotAllPresentNotificationAsync(sender)), - new NoParamCommandHandler(GET_READY_CTCP_COMMAND, sender => HandleGetReadyNotificationAsync(sender)), - new StringCommandHandler(FILE_HASH_CTCP_COMMAND, (sender, fileHash) => HandleFileHashCommandAsync(sender, fileHash)), - new StringCommandHandler(INVALID_FILE_HASH_CTCP_COMMAND, (sender, cheaterName) => HandleCheaterNotificationAsync(sender, cheaterName)), + new NoParamCommandHandler(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, sender => HandleNotAllPresentNotificationAsync(sender).HandleTask()), + new NoParamCommandHandler(GET_READY_CTCP_COMMAND, sender => HandleGetReadyNotificationAsync(sender).HandleTask()), + new StringCommandHandler(FILE_HASH_CTCP_COMMAND, (sender, fileHash) => HandleFileHashCommandAsync(sender, fileHash).HandleTask()), + new StringCommandHandler(INVALID_FILE_HASH_CTCP_COMMAND, (sender, cheaterName) => HandleCheaterNotificationAsync(sender, cheaterName).HandleTask()), new IntCommandHandler(TUNNEL_PING_CTCP_COMMAND, HandleTunnelPing), - new StringCommandHandler(OPTIONS_CTCP_COMMAND, (sender, data) => HandleOptionsMessageAsync(sender, data)), + new StringCommandHandler(OPTIONS_CTCP_COMMAND, (sender, data) => HandleOptionsMessageAsync(sender, data).HandleTask()), new NoParamCommandHandler(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, HandleInvalidSaveIndexCommand), new StringCommandHandler(START_GAME_CTCP_COMMAND, HandleStartGameCommand), - new IntCommandHandler(PLAYER_READY_CTCP_COMMAND, (sender, readyStatus) => HandlePlayerReadyRequestAsync(sender, readyStatus)), + new IntCommandHandler(PLAYER_READY_CTCP_COMMAND, (sender, readyStatus) => HandlePlayerReadyRequestAsync(sender, readyStatus).HandleTask()), new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, HandleTunnelServerChangeMessage) }; } @@ -108,18 +108,12 @@ DiscordHandler discordHandler public override void Initialize() { dp = new DarkeningPanel(WindowManager); - //WindowManager.AddAndInitializeControl(dp); - - //dp.AddChildWithoutInitialize(this); - - //dp.Alpha = 0.0f; - //dp.Hide(); localGame = ClientConfiguration.Instance.LocalGame; base.Initialize(); - connectionManager.ConnectionLost += (_, _) => ConnectionManager_ConnectionLostAsync(); - connectionManager.Disconnected += (_, _) => ConnectionManager_DisconnectedAsync(); + connectionManager.ConnectionLost += (_, _) => ClearAsync().HandleTask(); + connectionManager.Disconnected += (_, _) => ClearAsync().HandleTask(); tunnelSelectionWindow = new TunnelSelectionWindow(WindowManager, tunnelHandler); tunnelSelectionWindow.Initialize(); @@ -128,7 +122,7 @@ public override void Initialize() DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow); tunnelSelectionWindow.CenterOnParent(); tunnelSelectionWindow.Disable(); - tunnelSelectionWindow.TunnelSelected += (_, e) => TunnelSelectionWindow_TunnelSelectedAsync(e); + tunnelSelectionWindow.TunnelSelected += (_, e) => TunnelSelectionWindow_TunnelSelectedAsync(e).HandleTask(); btnChangeTunnel = new XNAClientButton(WindowManager); btnChangeTunnel.Name = nameof(btnChangeTunnel); @@ -142,31 +136,24 @@ public override void Initialize() gameBroadcastTimer.AutoReset = true; gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL); gameBroadcastTimer.Enabled = true; - gameBroadcastTimer.TimeElapsed += (_, _) => GameBroadcastTimer_TimeElapsedAsync(); + gameBroadcastTimer.TimeElapsed += (_, _) => BroadcastGameAsync().HandleTask(); WindowManager.AddAndInitializeControl(gameBroadcastTimer); } private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:"); - private Task GameBroadcastTimer_TimeElapsedAsync() => BroadcastGameAsync(); - - private Task ConnectionManager_DisconnectedAsync() => ClearAsync(); - - private Task ConnectionManager_ConnectionLostAsync() => ClearAsync(); - /// /// Sets up events and information before joining the channel. /// - public void SetUp(bool isHost, CnCNetTunnel tunnel, Channel channel, - string hostName) + public void SetUp(bool isHost, CnCNetTunnel tunnel, Channel channel, string hostName) { this.channel = channel; this.hostName = hostName; - channel_UserLeftFunc = (_, args) => Channel_UserLeftAsync(args); - channel_UserQuitIRCFunc = (_, args) => Channel_UserQuitIRCAsync(args); - channel_UserAddedFunc = (_, args) => Channel_UserAddedAsync(args); + channel_UserLeftFunc = (_, args) => OnPlayerLeftAsync(args).HandleTask(); + channel_UserQuitIRCFunc = (_, args) => OnPlayerLeftAsync(args).HandleTask(); + channel_UserAddedFunc = (_, args) => Channel_UserAddedAsync(args).HandleTask(); channel.MessageAdded += Channel_MessageAdded; channel.UserAdded += channel_UserAddedFunc; @@ -192,41 +179,34 @@ private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e) /// public async Task ClearAsync() { - try - { - gameBroadcastTimer.Enabled = false; + gameBroadcastTimer.Enabled = false; - if (channel != null) - { - // TODO leave channel only if we've joined the channel - await channel.LeaveAsync(); + if (channel != null) + { + // TODO leave channel only if we've joined the channel + await channel.LeaveAsync(); - channel.MessageAdded -= Channel_MessageAdded; - channel.UserAdded -= channel_UserAddedFunc; - channel.UserLeft -= channel_UserLeftFunc; - channel.UserQuitIRC -= channel_UserQuitIRCFunc; - channel.CTCPReceived -= Channel_CTCPReceived; + channel.MessageAdded -= Channel_MessageAdded; + channel.UserAdded -= channel_UserAddedFunc; + channel.UserLeft -= channel_UserLeftFunc; + channel.UserQuitIRC -= channel_UserQuitIRCFunc; + channel.CTCPReceived -= Channel_CTCPReceived; - connectionManager.RemoveChannel(channel); - } + connectionManager.RemoveChannel(channel); + } - if (Enabled) - { - Enabled = false; - Visible = false; + if (Enabled) + { + Enabled = false; + Visible = false; - await base.LeaveGameAsync(); - } + await base.LeaveGameAsync(); + } - tunnelHandler.CurrentTunnel = null; - tunnelHandler.CurrentTunnelPinged -= TunnelHandler_CurrentTunnelPinged; + tunnelHandler.CurrentTunnel = null; + tunnelHandler.CurrentTunnelPinged -= TunnelHandler_CurrentTunnelPinged; - topBar.RemovePrimarySwitchable(this); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + topBar.RemovePrimarySwitchable(this); } private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e) @@ -286,49 +266,22 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage( private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) { - try - { - PlayerInfo pInfo = new PlayerInfo(); - pInfo.Name = e.User.IRCUser.Name; - - Players.Add(pInfo); + PlayerInfo pInfo = new PlayerInfo(); + pInfo.Name = e.User.IRCUser.Name; - sndJoinSound.Play(); + Players.Add(pInfo); - await BroadcastOptionsAsync(); - CopyPlayerDataToUI(); - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } - } + sndJoinSound.Play(); - private async Task Channel_UserLeftAsync(UserNameEventArgs e) - { - try - { - await RemovePlayerAsync(e.UserName); - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await BroadcastOptionsAsync(); + CopyPlayerDataToUI(); + UpdateDiscordPresence(); } - private async Task Channel_UserQuitIRCAsync(UserNameEventArgs e) + private async Task OnPlayerLeftAsync(UserNameEventArgs e) { - try - { - await RemovePlayerAsync(e.UserName); - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await RemovePlayerAsync(e.UserName); + UpdateDiscordPresence(); } private async Task RemovePlayerAsync(string playerName) @@ -422,91 +375,58 @@ private void ShowTunnelSelectionWindow(string description) private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) { - try - { - await channel.SendCTCPMessageAsync($"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", - QueuedMessageType.SYSTEM_MESSAGE, 10); - HandleTunnelServerChange(e.Tunnel); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await channel.SendCTCPMessageAsync( + $"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", + QueuedMessageType.SYSTEM_MESSAGE, + 10); + HandleTunnelServerChange(e.Tunnel); } #region CTCP Handlers private async Task HandleGetReadyNotificationAsync(string sender) { - try - { - if (sender != hostName) - return; + if (sender != hostName) + return; - await GetReadyNotificationAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await GetReadyNotificationAsync(); } private async Task HandleNotAllPresentNotificationAsync(string sender) { - try - { - if (sender != hostName) - return; + if (sender != hostName) + return; - await NotAllPresentNotificationAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await NotAllPresentNotificationAsync(); } private async Task HandleFileHashCommandAsync(string sender, string fileHash) { - try - { - if (!IsHost) - return; + if (!IsHost) + return; - if (fileHash != gameFilesHash) - { - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + if (fileHash != gameFilesHash) + { + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo == null) - return; + if (pInfo == null) + return; - pInfo.Verified = true; + pInfo.Verified = true; - await HandleCheaterNotificationAsync(hostName, sender); // This is kinda hacky - } - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + await HandleCheaterNotificationAsync(hostName, sender); // This is kinda hacky } } private async Task HandleCheaterNotificationAsync(string sender, string cheaterName) { - try - { - if (sender != hostName) - return; + if (sender != hostName) + return; - AddNotice(string.Format("{0} - modified files detected! They could be cheating!".L10N("Client:Main:PlayerCheating"), cheaterName), Color.Red); + AddNotice(string.Format("{0} - modified files detected! They could be cheating!".L10N("Client:Main:PlayerCheating"), cheaterName), Color.Red); - if (IsHost) - await channel.SendCTCPMessageAsync(INVALID_FILE_HASH_CTCP_COMMAND + " " + cheaterName, QueuedMessageType.SYSTEM_MESSAGE, 0); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await channel.SendCTCPMessageAsync(INVALID_FILE_HASH_CTCP_COMMAND + " " + cheaterName, QueuedMessageType.SYSTEM_MESSAGE, 0); } private void HandleTunnelPing(string sender, int pingInMs) @@ -522,57 +442,50 @@ private void HandleTunnelPing(string sender, int pingInMs) /// private async Task HandleOptionsMessageAsync(string sender, string data) { - try - { - if (sender != hostName) - return; - - string[] parts = data.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + if (sender != hostName) + return; - if (parts.Length < 1) - return; + string[] parts = data.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - int sgIndex = Conversions.IntFromString(parts[0], -1); + if (parts.Length < 1) + return; - if (sgIndex < 0) - return; + int sgIndex = Conversions.IntFromString(parts[0], -1); - if (sgIndex >= ddSavedGame.Items.Count) - { - AddNotice("The game host has selected an invalid saved game index!".L10N("Client:Main:HostInvalidIndex") + " " + sgIndex); - await channel.SendCTCPMessageAsync(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, QueuedMessageType.SYSTEM_MESSAGE, 10); - return; - } + if (sgIndex < 0) + return; - ddSavedGame.SelectedIndex = sgIndex; + if (sgIndex >= ddSavedGame.Items.Count) + { + AddNotice("The game host has selected an invalid saved game index!".L10N("Client:Main:HostInvalidIndex") + " " + sgIndex); + await channel.SendCTCPMessageAsync(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, QueuedMessageType.SYSTEM_MESSAGE, 10); + return; + } - Players.Clear(); + ddSavedGame.SelectedIndex = sgIndex; - for (int i = 1; i < parts.Length; i++) - { - string[] playerAndReadyStatus = parts[i].Split(':'); - if (playerAndReadyStatus.Length < 2) - return; + Players.Clear(); - string playerName = playerAndReadyStatus[0]; - int readyStatus = Conversions.IntFromString(playerAndReadyStatus[1], -1); + for (int i = 1; i < parts.Length; i++) + { + string[] playerAndReadyStatus = parts[i].Split(':'); + if (playerAndReadyStatus.Length < 2) + return; - if (string.IsNullOrEmpty(playerName) || readyStatus == -1) - return; + string playerName = playerAndReadyStatus[0]; + int readyStatus = Conversions.IntFromString(playerAndReadyStatus[1], -1); - PlayerInfo pInfo = new PlayerInfo(); - pInfo.Name = playerName; - pInfo.Ready = Convert.ToBoolean(readyStatus); + if (string.IsNullOrEmpty(playerName) || readyStatus == -1) + return; - Players.Add(pInfo); - } + PlayerInfo pInfo = new PlayerInfo(); + pInfo.Name = playerName; + pInfo.Ready = Convert.ToBoolean(readyStatus); - CopyPlayerDataToUI(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + Players.Add(pInfo); } + + CopyPlayerDataToUI(); } private void HandleInvalidSaveIndexCommand(string sender) @@ -628,24 +541,17 @@ private void HandleStartGameCommand(string sender, string data) private async Task HandlePlayerReadyRequestAsync(string sender, int readyStatus) { - try - { - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo == null) - return; + if (pInfo == null) + return; - pInfo.Ready = Convert.ToBoolean(readyStatus); + pInfo.Ready = Convert.ToBoolean(readyStatus); - CopyPlayerDataToUI(); + CopyPlayerDataToUI(); - if (IsHost) - await BroadcastOptionsAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await BroadcastOptionsAsync(); } private void HandleTunnelServerChangeMessage(string sender, string tunnelAddressAndPort) @@ -686,43 +592,37 @@ private void HandleTunnelServerChange(CnCNetTunnel tunnel) protected override async Task HostStartGameAsync() { - try + AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); + List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(SGPlayers.Count); + + if (playerPorts.Count < Players.Count) { - AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); - List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(SGPlayers.Count); - - if (playerPorts.Count < Players.Count) - { - ShowTunnelSelectionWindow(("An error occured while contacting " + - "the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); - AddNotice(("An error occured while contacting the specified CnCNet " + - "tunnel server. Please try using a different tunnel server").L10N("Client:Main:ConnectTunnelError2"), Color.Yellow); - return; - } - - StringBuilder sb = new StringBuilder(START_GAME_CTCP_COMMAND + " "); - for (int pId = 0; pId < Players.Count; pId++) - { - Players[pId].Port = playerPorts[pId]; - sb.Append(Players[pId].Name); - sb.Append(";"); - sb.Append("0.0.0.0:"); - sb.Append(playerPorts[pId]); - sb.Append(";"); - } - sb.Remove(sb.Length - 1, 1); - await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 9); - - AddNotice("Starting game...".L10N("Client:Main:StartingGame")); - - started = true; - - LoadGame(); + ShowTunnelSelectionWindow(("An error occured while contacting " + + "the CnCNet tunnel server." + Environment.NewLine + + "Try picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); + AddNotice(("An error occured while contacting the specified CnCNet " + + "tunnel server. Please try using a different tunnel server ").L10N("Client:Main:ConnectTunnelError2"), Color.Yellow); + return; } - catch (Exception ex) + + StringBuilder sb = new StringBuilder(START_GAME_CTCP_COMMAND + " "); + for (int pId = 0; pId < Players.Count; pId++) { - PreStartup.HandleException(ex); + Players[pId].Port = playerPorts[pId]; + sb.Append(Players[pId].Name); + sb.Append(";"); + sb.Append("0.0.0.0:"); + sb.Append(playerPorts[pId]); + sb.Append(";"); } + sb.Remove(sb.Length - 1, 1); + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 9); + + AddNotice("Starting game...".L10N("Client:Main:StartingGame")); + + started = true; + + LoadGame(); } protected override void WriteSpawnIniAdditions(IniFile spawnIni) @@ -735,16 +635,8 @@ protected override void WriteSpawnIniAdditions(IniFile spawnIni) protected override async Task HandleGameProcessExitedAsync() { - try - { - await base.HandleGameProcessExitedAsync(); - - await ClearAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await base.HandleGameProcessExitedAsync(); + await ClearAsync(); } protected override Task LeaveGameAsync() => ClearAsync(); @@ -757,55 +649,48 @@ public void ChangeChatColor(IRCColor chatColor) private async Task BroadcastGameAsync() { - try - { - Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); - - if (broadcastChannel == null) - return; + Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); - StringBuilder sb = new StringBuilder("GAME "); - sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); - sb.Append(";"); - sb.Append(ProgramConstants.GAME_VERSION); - sb.Append(";"); - sb.Append(SGPlayers.Count); - sb.Append(";"); - sb.Append(channel.ChannelName); - sb.Append(";"); - sb.Append(channel.UIName); - sb.Append(";"); - if (started || Players.Count == SGPlayers.Count) - sb.Append("1"); - else - sb.Append("0"); - sb.Append("0"); // IsCustomPassword - sb.Append("0"); // Closed - sb.Append("1"); // IsLoadedGame - sb.Append("0"); // IsLadder - sb.Append(";"); - foreach (SavedGamePlayer sgPlayer in SGPlayers) - { - sb.Append(sgPlayer.Name); - sb.Append(","); - } - - sb.Remove(sb.Length - 1, 1); - sb.Append(";"); - sb.Append((string)lblMapNameValue.Tag); - sb.Append(";"); - sb.Append((string)lblGameModeValue.Tag); - sb.Append(";"); - sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); - sb.Append(";"); - sb.Append(0); // LoadedGameId + if (broadcastChannel == null) + return; - await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + StringBuilder sb = new StringBuilder("GAME "); + sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); + sb.Append(";"); + sb.Append(ProgramConstants.GAME_VERSION); + sb.Append(";"); + sb.Append(SGPlayers.Count); + sb.Append(";"); + sb.Append(channel.ChannelName); + sb.Append(";"); + sb.Append(channel.UIName); + sb.Append(";"); + if (started || Players.Count == SGPlayers.Count) + sb.Append("1"); + else + sb.Append("0"); + sb.Append("0"); // IsCustomPassword + sb.Append("0"); // Closed + sb.Append("1"); // IsLoadedGame + sb.Append("0"); // IsLadder + sb.Append(";"); + foreach (SavedGamePlayer sgPlayer in SGPlayers) + { + sb.Append(sgPlayer.Name); + sb.Append(","); + } + + sb.Remove(sb.Length - 1, 1); + sb.Append(";"); + sb.Append((string)lblMapNameValue.Tag); + sb.Append(";"); + sb.Append((string)lblGameModeValue.Tag); + sb.Append(";"); + sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); + sb.Append(";"); + sb.Append(0); // LoadedGameId + + await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); } public override string GetSwitchName() => "Load Game".L10N("Client:Main:LoadGame"); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 0931f779c..474307512 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -53,7 +53,7 @@ public CnCNetLobby(WindowManager windowManager, CnCNetManager connectionManager, ctcpCommandHandlers = new CommandHandlerBase[] { - new StringCommandHandler(ProgramConstants.GAME_INVITE_CTCP_COMMAND, (sender, argumentsString) => HandleGameInviteCommandAsync(sender, argumentsString)), + new StringCommandHandler(ProgramConstants.GAME_INVITE_CTCP_COMMAND, (sender, argumentsString) => HandleGameInviteCommandAsync(sender, argumentsString).HandleTask()), new NoParamCommandHandler(ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND, HandleGameInvitationFailedNotification) }; @@ -175,19 +175,18 @@ public override void Initialize() btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnJoinGame.Text = "Join Game".L10N("Client:Main:JoinGame"); btnJoinGame.AllowClick = false; - btnJoinGame.LeftClick += BtnJoinGame_LeftClick; + btnJoinGame.LeftClick += (_, _) => JoinSelectedGameAsync().HandleTask(); btnLogout = new XNAClientButton(WindowManager); btnLogout.Name = nameof(btnLogout); btnLogout.ClientRectangle = new Rectangle(Width - 145, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLogout.Text = "Log Out".L10N("Client:Main:ButtonLogOut"); - btnLogout.LeftClick += (_, _) => BtnLogout_LeftClickAsync(); + btnLogout.LeftClick += (_, _) => BtnLogout_LeftClickAsync().HandleTask(); var gameListRectangle = new Rectangle( btnNewGame.X, 41, - btnJoinGame.Right - btnNewGame.X, btnNewGame.Y - 47 - ); + btnJoinGame.Right - btnNewGame.X, btnNewGame.Y - 47); panelGameFilters = new GameFiltersPanel(WindowManager); panelGameFilters.Name = nameof(panelGameFilters); @@ -199,7 +198,7 @@ public override void Initialize() lbGameList.ClientRectangle = gameListRectangle; lbGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); - lbGameList.DoubleLeftClick += LbGameList_DoubleLeftClick; + lbGameList.DoubleLeftClick += (_, _) => JoinSelectedGameAsync().HandleTask(); lbGameList.AllowMultiLineItems = false; lbGameList.ClientRectangleUpdated += GameList_ClientRectangleUpdated; @@ -215,7 +214,7 @@ public override void Initialize() lbPlayerList.RightClick += LbPlayerList_RightClick; globalContextMenu = new GlobalContextMenu(WindowManager, connectionManager, cncnetUserData, pmWindow); - globalContextMenu.JoinEvent += (_, args) => JoinUserAsync(args.IrcUser, connectionManager.MainChannel); + globalContextMenu.JoinEvent += (_, args) => JoinUserAsync(args.IrcUser, connectionManager.MainChannel).HandleTask(); lbChatMessages = new ChatListBox(WindowManager); lbChatMessages.Name = nameof(lbChatMessages); @@ -235,7 +234,7 @@ public override void Initialize() tbChatInput.Suggestion = "Type here to chat...".L10N("Client:Main:ChatHere"); tbChatInput.Enabled = false; tbChatInput.MaximumTextLength = 200; - tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync(); + tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync().HandleTask(); lblColor = new XNALabel(WindowManager); lblColor.Name = nameof(lblColor); @@ -274,7 +273,7 @@ public override void Initialize() ddCurrentChannel.ClientRectangle = new Rectangle( lbChatMessages.Right - 200, ddColor.Y, 200, 21); - ddCurrentChannel.SelectedIndexChanged += (_, _) => DdCurrentChannel_SelectedIndexChangedAsync(); + ddCurrentChannel.SelectedIndexChanged += (_, _) => DdCurrentChannel_SelectedIndexChangedAsync().HandleTask(); ddCurrentChannel.AllowDropDown = false; lblCurrentChannel = new XNALabel(WindowManager); @@ -300,8 +299,7 @@ public override void Initialize() tbGameSearch = new XNASuggestionTextBox(WindowManager); tbGameSearch.Name = nameof(tbGameSearch); - tbGameSearch.ClientRectangle = new Rectangle(lbGameList.X, - 12, lbGameList.Width - 62, 21); + tbGameSearch.ClientRectangle = new Rectangle(lbGameList.X, 12, lbGameList.Width - 62, 21); tbGameSearch.Suggestion = "Filter by name, map, game mode, player...".L10N("Client:Main:FilterByBlahBlah"); tbGameSearch.MaximumTextLength = 64; tbGameSearch.InputReceived += TbGameSearch_InputReceived; @@ -357,15 +355,15 @@ public override void Initialize() CnCNetPlayerCountTask.CnCNetGameCountUpdated += OnCnCNetGameCountUpdated; - gameChannel_UserAddedFunc = (sender, e) => GameChannel_UserAddedAsync(sender, e); - gameChannel_InvalidPasswordEntered_LoadedGameFunc = (sender, _) => GameChannel_InvalidPasswordEntered_LoadedGameAsync(sender); - gameLoadingChannel_UserAddedFunc = (sender, e) => GameLoadingChannel_UserAddedAsync(sender, e); - gameChannel_InvalidPasswordEntered_NewGameFunc = (sender, _) => GameChannel_InvalidPasswordEntered_NewGameAsync(sender); - gameChannel_InviteOnlyErrorOnJoinFunc = (sender, _) => GameChannel_InviteOnlyErrorOnJoinAsync(sender); - gameChannel_ChannelFullFunc = (sender, _) => GameChannel_ChannelFullAsync(sender); - gameChannel_TargetChangeTooFastFunc = (sender, e) => GameChannel_TargetChangeTooFastAsync(sender, e); + gameChannel_UserAddedFunc = (sender, e) => GameChannel_UserAddedAsync(sender, e).HandleTask(); + gameChannel_InvalidPasswordEntered_LoadedGameFunc = (sender, _) => GameChannel_InvalidPasswordEntered_LoadedGameAsync(sender).HandleTask(); + gameLoadingChannel_UserAddedFunc = (sender, e) => GameLoadingChannel_UserAddedAsync(sender, e).HandleTask(); + gameChannel_InvalidPasswordEntered_NewGameFunc = (sender, _) => GameChannel_InvalidPasswordEntered_NewGameAsync(sender).HandleTask(); + gameChannel_InviteOnlyErrorOnJoinFunc = (sender, _) => OnGameLocked(sender).HandleTask(); + gameChannel_ChannelFullFunc = (sender, _) => OnGameLocked(sender).HandleTask(); + gameChannel_TargetChangeTooFastFunc = (sender, e) => GameChannel_TargetChangeTooFastAsync(sender, e).HandleTask(); - pmWindow.SetJoinUserAction((user, messageView) => JoinUserAsync(user, messageView)); + pmWindow.SetJoinUserAction((user, messageView) => JoinUserAsync(user, messageView).HandleTask()); base.Initialize(); @@ -536,7 +534,7 @@ private void PostUIInit() unknownGameIcon = AssetLoader.TextureFromImage(Image.Load(unknownIconStream)); - connectionManager.WelcomeMessageReceived += (_, _) => ConnectionManager_WelcomeMessageReceivedAsync(); + connectionManager.WelcomeMessageReceived += (_, _) => ConnectionManager_WelcomeMessageReceivedAsync().HandleTask(); connectionManager.Disconnected += ConnectionManager_Disconnected; connectionManager.PrivateCTCPReceived += ConnectionManager_PrivateCTCPReceived; @@ -550,8 +548,8 @@ private void PostUIInit() gameCreationPanel.AddChild(gcw); gameCreationPanel.Tag = gcw; gcw.Cancelled += Gcw_Cancelled; - gcw.GameCreated += (_, e) => Gcw_GameCreatedAsync(e); - gcw.LoadedGameCreated += (_, e) => Gcw_LoadedGameCreatedAsync(e); + gcw.GameCreated += (_, e) => Gcw_GameCreatedAsync(e).HandleTask(); + gcw.LoadedGameCreated += (_, e) => Gcw_LoadedGameCreatedAsync(e).HandleTask(); gameCreationPanel.Hide(); @@ -559,7 +557,7 @@ private void PostUIInit() string.Format("*** DTA CnCNet Client version {0} ***".L10N("Client:Main:CnCNetClientVersionMessage"), Assembly.GetAssembly(typeof(CnCNetLobby)).GetName().Version), lbChatMessages.FontIndex))); - connectionManager.BannedFromChannel += (_, e) => ConnectionManager_BannedFromChannelAsync(e); + connectionManager.BannedFromChannel += (_, e) => ConnectionManager_BannedFromChannelAsync(e).HandleTask(); loginWindow = new CnCNetLoginWindow(WindowManager); loginWindow.Connect += LoginWindow_Connect; @@ -573,7 +571,7 @@ private void PostUIInit() loginWindow.Disable(); passwordRequestWindow = new PasswordRequestWindow(WindowManager, pmWindow); - passwordRequestWindow.PasswordEntered += PasswordRequestWindow_PasswordEntered; + passwordRequestWindow.PasswordEntered += (_, hostedGame) => JoinGameAsync(hostedGame.HostedGame, hostedGame.Password).HandleTask(); var passwordRequestWindowPanel = new DarkeningPanel(WindowManager); passwordRequestWindowPanel.Alpha = 0.0f; @@ -584,10 +582,9 @@ private void PostUIInit() gameLobby.GameLeft += GameLobby_GameLeft; gameLoadingLobby.GameLeft += GameLoadingLobby_GameLeft; - UserINISettings.Instance.SettingsSaved += (_, _) => Instance_SettingsSavedAsync(); - - GameProcessLogic.GameProcessStarted += () => SharedUILogic_GameProcessStartedAsync(); - GameProcessLogic.GameProcessExited += () => SharedUILogic_GameProcessExitedAsync(); + UserINISettings.Instance.SettingsSaved += (_, _) => Instance_SettingsSavedAsync().HandleTask(); + GameProcessLogic.GameProcessStarted += () => SharedUILogic_GameProcessStartedAsync().HandleTask(); + GameProcessLogic.GameProcessExited += () => SharedUILogic_GameProcessExitedAsync().HandleTask(); } /// @@ -596,96 +593,63 @@ private void PostUIInit() /// private async Task ConnectionManager_BannedFromChannelAsync(ChannelEventArgs e) { - try - { - var game = lbGameList.HostedGames.Find(hg => ((HostedCnCNetGame)hg).ChannelName == e.ChannelName); - - if (game == null) - { - var chatChannel = connectionManager.FindChannel(e.ChannelName); - chatChannel?.AddMessage(new ChatMessage(Color.White, string.Format( - "Cannot join chat channel {0}, you're banned!".L10N("Client:Main:PlayerBannedByChannel"), chatChannel.UIName))); - return; - } - - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format( - "Cannot join game {0}, you've been banned by the game host!".L10N("Client:Main:PlayerBannedByHost"), game.RoomName))); + var game = lbGameList.HostedGames.Find(hg => ((HostedCnCNetGame)hg).ChannelName == e.ChannelName); - isJoiningGame = false; - if (gameOfLastJoinAttempt != null) - { - if (gameOfLastJoinAttempt.IsLoadedGame) - await gameLoadingLobby.ClearAsync(); - else - await gameLobby.ClearAsync(); - } - } - catch (Exception ex) + if (game == null) { - PreStartup.HandleException(ex); + var chatChannel = connectionManager.FindChannel(e.ChannelName); + chatChannel?.AddMessage(new ChatMessage(Color.White, string.Format( + "Cannot join chat channel {0}, you're banned!".L10N("Client:Main:PlayerBannedByChannel"), chatChannel.UIName))); + return; } - } - private async Task SharedUILogic_GameProcessStartedAsync() - { - try - { - await connectionManager.SendCustomMessageAsync(new QueuedMessage("AWAY " + (char)58 + "In-game", - QueuedMessageType.SYSTEM_MESSAGE, 0)); + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format( + "Cannot join game {0}, you've been banned by the game host!".L10N("Client:Main:PlayerBannedByHost"), game.RoomName))); - } - catch (Exception ex) + isJoiningGame = false; + if (gameOfLastJoinAttempt != null) { - PreStartup.HandleException(ex); + if (gameOfLastJoinAttempt.IsLoadedGame) + await gameLoadingLobby.ClearAsync(); + else + await gameLobby.ClearAsync(); } } - private async Task SharedUILogic_GameProcessExitedAsync() - { - try - { - await connectionManager.SendCustomMessageAsync(new QueuedMessage("AWAY", - QueuedMessageType.SYSTEM_MESSAGE, 0)); + private Task SharedUILogic_GameProcessStartedAsync() + => connectionManager.SendCustomMessageAsync(new QueuedMessage( + "AWAY " + (char)58 + "In-game", + QueuedMessageType.SYSTEM_MESSAGE, + 0)); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } - } + private Task SharedUILogic_GameProcessExitedAsync() + => connectionManager.SendCustomMessageAsync(new QueuedMessage("AWAY", QueuedMessageType.SYSTEM_MESSAGE, 0)); private async Task Instance_SettingsSavedAsync() { - try - { - if (!connectionManager.IsConnected) - return; + if (!connectionManager.IsConnected) + return; - foreach (CnCNetGame game in gameCollection.GameList) - { - if (!game.Supported) - continue; + foreach (CnCNetGame game in gameCollection.GameList) + { + if (!game.Supported) + continue; - if (game.InternalName.ToUpper() == localGameID) - continue; + if (game.InternalName.ToUpper() == localGameID) + continue; - if (followedGames.Contains(game.InternalName) && - !UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) - { - await connectionManager.FindChannel(game.GameBroadcastChannel).LeaveAsync(); - followedGames.Remove(game.InternalName); - } - else if (!followedGames.Contains(game.InternalName) && - UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) - { - await connectionManager.FindChannel(game.GameBroadcastChannel).JoinAsync(); - followedGames.Add(game.InternalName); - } + if (followedGames.Contains(game.InternalName) && + !UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) + { + await connectionManager.FindChannel(game.GameBroadcastChannel).LeaveAsync(); + followedGames.Remove(game.InternalName); + } + else if (!followedGames.Contains(game.InternalName) && + UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) + { + await connectionManager.FindChannel(game.GameBroadcastChannel).JoinAsync(); + followedGames.Add(game.InternalName); } - } - catch (Exception ex) - { - PreStartup.HandleException(ex); } } @@ -790,12 +754,6 @@ private void SetLogOutButtonText() btnLogout.Text = "Log Out".L10N("Client:Main:LogOut"); } - private void BtnJoinGame_LeftClick(object sender, EventArgs e) => JoinSelectedGameAsync(); - - private void LbGameList_DoubleLeftClick(object sender, EventArgs e) => JoinSelectedGameAsync(); - - private void PasswordRequestWindow_PasswordEntered(object sender, PasswordEventArgs e) => _JoinGameAsync(e.HostedGame, e.Password); - private string GetJoinGameErrorBase() { if (isJoiningGame) @@ -844,18 +802,11 @@ private string GetJoinGameError(HostedCnCNetGame hg) private async Task JoinSelectedGameAsync() { - try - { - var listedGame = (HostedCnCNetGame)lbGameList.SelectedItem?.Tag; - if (listedGame == null) - return; - var hostedGameIndex = lbGameList.HostedGames.IndexOf(listedGame); - await JoinGameByIndexAsync(hostedGameIndex, string.Empty); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + var listedGame = (HostedCnCNetGame)lbGameList.SelectedItem?.Tag; + if (listedGame == null) + return; + var hostedGameIndex = lbGameList.HostedGames.IndexOf(listedGame); + await JoinGameByIndexAsync(hostedGameIndex, string.Empty); } private async Task JoinGameByIndexAsync(int gameIndex, string password) @@ -909,95 +860,70 @@ private async Task JoinGameAsync(HostedCnCNetGame hg, string password, IMe if (!hg.IsLoadedGame) { password = Utilities.CalculateSHA1ForString - (hg.ChannelName + hg.RoomName).Substring(0, 10); + (hg.ChannelName + hg.RoomName)[..10]; } else { IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "Saved Games", "spawnSG.ini")); password = Utilities.CalculateSHA1ForString( - spawnSGIni.GetStringValue("Settings", "GameID", string.Empty)).Substring(0, 10); + spawnSGIni.GetStringValue("Settings", "GameID", string.Empty))[..10]; } } - await _JoinGameAsync(hg, password); + await JoinGameAsync(hg, password); return true; } - private async Task _JoinGameAsync(HostedCnCNetGame hg, string password) + private async Task JoinGameAsync(HostedCnCNetGame hg, string password) { - try - { - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, - string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName))); - isJoiningGame = true; - gameOfLastJoinAttempt = hg; + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName))); + isJoiningGame = true; + gameOfLastJoinAttempt = hg; - Channel gameChannel = connectionManager.CreateChannel(hg.RoomName, hg.ChannelName, false, true, password); - connectionManager.AddChannel(gameChannel); + Channel gameChannel = connectionManager.CreateChannel(hg.RoomName, hg.ChannelName, false, true, password); + connectionManager.AddChannel(gameChannel); - if (hg.IsLoadedGame) - { - gameLoadingLobby.SetUp(false, hg.TunnelServer, gameChannel, hg.HostName); - gameChannel.UserAdded += gameLoadingChannel_UserAddedFunc; - gameChannel.InvalidPasswordEntered += gameChannel_InvalidPasswordEntered_LoadedGameFunc; - } - else - { - await gameLobby.SetUpAsync(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded, false); - gameChannel.UserAdded += gameChannel_UserAddedFunc; - gameChannel.InvalidPasswordEntered += gameChannel_InvalidPasswordEntered_NewGameFunc; - gameChannel.InviteOnlyErrorOnJoin += gameChannel_InviteOnlyErrorOnJoinFunc; - gameChannel.ChannelFull += gameChannel_ChannelFullFunc; - gameChannel.TargetChangeTooFast += gameChannel_TargetChangeTooFastFunc; - } - - await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + hg.ChannelName + " " + password, - QueuedMessageType.INSTANT_MESSAGE, 0)); + if (hg.IsLoadedGame) + { + gameLoadingLobby.SetUp(false, hg.TunnelServer, gameChannel, hg.HostName); + gameChannel.UserAdded += gameLoadingChannel_UserAddedFunc; + gameChannel.InvalidPasswordEntered += gameChannel_InvalidPasswordEntered_LoadedGameFunc; } - catch (Exception ex) + else { - PreStartup.HandleException(ex); + await gameLobby.SetUpAsync(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded, false); + gameChannel.UserAdded += gameChannel_UserAddedFunc; + gameChannel.InvalidPasswordEntered += gameChannel_InvalidPasswordEntered_NewGameFunc; + gameChannel.InviteOnlyErrorOnJoin += gameChannel_InviteOnlyErrorOnJoinFunc; + gameChannel.ChannelFull += gameChannel_ChannelFullFunc; + gameChannel.TargetChangeTooFast += gameChannel_TargetChangeTooFastFunc; } + + await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + hg.ChannelName + " " + password, + QueuedMessageType.INSTANT_MESSAGE, 0)); } private async Task GameChannel_TargetChangeTooFastAsync(object sender, MessageEventArgs e) { - try - { - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, e.Message)); - await ClearGameJoinAttemptAsync((Channel)sender); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, e.Message)); + await ClearGameJoinAttemptAsync((Channel)sender); } - private Task GameChannel_ChannelFullAsync(object sender) => - // We'd do the exact same things here, so we can just call the method below - GameChannel_InviteOnlyErrorOnJoinAsync(sender); - - private async Task GameChannel_InviteOnlyErrorOnJoinAsync(object sender) + private async Task OnGameLocked(object sender) { - try - { - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "The selected game is locked!".L10N("Client:Main:GameLocked"))); - var channel = (Channel)sender; - - var game = FindGameByChannelName(channel.ChannelName); - if (game != null) - { - game.Locked = true; - SortAndRefreshHostedGames(); - } + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "The selected game is locked!".L10N("Client:Main:GameLocked"))); + var channel = (Channel)sender; - await ClearGameJoinAttemptAsync((Channel)sender); - } - catch (Exception ex) + var game = FindGameByChannelName(channel.ChannelName); + if (game != null) { - PreStartup.HandleException(ex); + game.Locked = true; + SortAndRefreshHostedGames(); } + + await ClearGameJoinAttemptAsync((Channel)sender); } private HostedCnCNetGame FindGameByChannelName(string channelName) @@ -1011,34 +937,20 @@ private HostedCnCNetGame FindGameByChannelName(string channelName) private async Task GameChannel_InvalidPasswordEntered_NewGameAsync(object sender) { - try - { - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "Incorrect password!".L10N("Client:Main:PasswordWrong"))); - await ClearGameJoinAttemptAsync((Channel)sender); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "Incorrect password!".L10N("Client:Main:PasswordWrong"))); + await ClearGameJoinAttemptAsync((Channel)sender); } private async Task GameChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) { - try - { - Channel gameChannel = (Channel)sender; + Channel gameChannel = (Channel)sender; - if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) - { - ClearGameChannelEvents(gameChannel); - await gameLobby.OnJoinedAsync(); - isInGameRoom = true; - SetLogOutButtonText(); - } - } - catch (Exception ex) + if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) { - PreStartup.HandleException(ex); + ClearGameChannelEvents(gameChannel); + await gameLobby.OnJoinedAsync(); + isInGameRoom = true; + SetLogOutButtonText(); } } @@ -1074,104 +986,76 @@ private void BtnNewGame_LeftClick(object sender, EventArgs e) private async Task Gcw_GameCreatedAsync(GameCreationEventArgs e) { - try + if (gameLobby.Enabled || gameLoadingLobby.Enabled) + return; + + string channelName = RandomizeChannelName(); + string password = e.Password; + bool isCustomPassword = true; + if (string.IsNullOrEmpty(password)) { - if (gameLobby.Enabled || gameLoadingLobby.Enabled) - return; + password = Utilities.CalculateSHA1ForString(channelName + e.GameRoomName)[..10]; + isCustomPassword = false; + } - string channelName = RandomizeChannelName(); - string password = e.Password; - bool isCustomPassword = true; - if (string.IsNullOrEmpty(password)) - { - password = Utilities.CalculateSHA1ForString(channelName + e.GameRoomName).Substring(0, 10); - isCustomPassword = false; - } + Channel gameChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, password); + connectionManager.AddChannel(gameChannel); + await gameLobby.SetUpAsync(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword, false); + gameChannel.UserAdded += gameChannel_UserAddedFunc; + await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + channelName + " " + password, + QueuedMessageType.INSTANT_MESSAGE, 0)); + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); - Channel gameChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, password); - connectionManager.AddChannel(gameChannel); - await gameLobby.SetUpAsync(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword, false); - gameChannel.UserAdded += gameChannel_UserAddedFunc; - await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + channelName + " " + password, - QueuedMessageType.INSTANT_MESSAGE, 0)); - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, - string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); - - gameCreationPanel.Hide(); + gameCreationPanel.Hide(); - // update the friends window so it can enable the Invite option - pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + // update the friends window so it can enable the Invite option + pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); } private async Task Gcw_LoadedGameCreatedAsync(GameCreationEventArgs e) { - try - { - if (gameLobby.Enabled || gameLoadingLobby.Enabled) - return; + if (gameLobby.Enabled || gameLoadingLobby.Enabled) + return; - string channelName = RandomizeChannelName(); + string channelName = RandomizeChannelName(); - Channel gameLoadingChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, e.Password); - connectionManager.AddChannel(gameLoadingChannel); - gameLoadingLobby.SetUp(true, e.Tunnel, gameLoadingChannel, ProgramConstants.PLAYERNAME); - gameLoadingChannel.UserAdded += gameLoadingChannel_UserAddedFunc; - await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + channelName + " " + e.Password, - QueuedMessageType.INSTANT_MESSAGE, 0)); - connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, - string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); + Channel gameLoadingChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, e.Password); + connectionManager.AddChannel(gameLoadingChannel); + gameLoadingLobby.SetUp(true, e.Tunnel, gameLoadingChannel, ProgramConstants.PLAYERNAME); + gameLoadingChannel.UserAdded += gameLoadingChannel_UserAddedFunc; + await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + channelName + " " + e.Password, + QueuedMessageType.INSTANT_MESSAGE, 0)); + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); - gameCreationPanel.Hide(); + gameCreationPanel.Hide(); - // update the friends window so it can enable the Invite option - pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + // update the friends window so it can enable the Invite option + pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); } private async Task GameChannel_InvalidPasswordEntered_LoadedGameAsync(object sender) { - try - { - var channel = (Channel)sender; - channel.UserAdded -= gameLoadingChannel_UserAddedFunc; - channel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; - await gameLoadingLobby.ClearAsync(); - isJoiningGame = false; - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + var channel = (Channel)sender; + channel.UserAdded -= gameLoadingChannel_UserAddedFunc; + channel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; + await gameLoadingLobby.ClearAsync(); + isJoiningGame = false; } private async Task GameLoadingChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) { - try - { - Channel gameLoadingChannel = (Channel)sender; + Channel gameLoadingChannel = (Channel)sender; - if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) - { - gameLoadingChannel.UserAdded -= gameLoadingChannel_UserAddedFunc; - gameLoadingChannel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; - - await gameLoadingLobby.OnJoinedAsync(); - isInGameRoom = true; - isJoiningGame = false; - } - } - catch (Exception ex) + if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) { - PreStartup.HandleException(ex); + gameLoadingChannel.UserAdded -= gameLoadingChannel_UserAddedFunc; + gameLoadingChannel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; + + await gameLoadingLobby.OnJoinedAsync(); + isInGameRoom = true; + isJoiningGame = false; } } @@ -1194,21 +1078,14 @@ private string RandomizeChannelName() private async Task TbChatInput_EnterPressedAsync() { - try - { - if (string.IsNullOrEmpty(tbChatInput.Text)) - return; + if (string.IsNullOrEmpty(tbChatInput.Text)) + return; - IRCColor selectedColor = (IRCColor)ddColor.SelectedItem.Tag; + IRCColor selectedColor = (IRCColor)ddColor.SelectedItem.Tag; - await currentChatChannel.SendChatMessageAsync(tbChatInput.Text, selectedColor); + await currentChatChannel.SendChatMessageAsync(tbChatInput.Text, selectedColor); - tbChatInput.Text = string.Empty; - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + tbChatInput.Text = string.Empty; } private void SetChatColor() @@ -1253,45 +1130,38 @@ private void ConnectionManager_Disconnected(object sender, EventArgs e) private async Task ConnectionManager_WelcomeMessageReceivedAsync() { - try - { - btnNewGame.AllowClick = true; - btnJoinGame.AllowClick = true; - ddCurrentChannel.AllowDropDown = true; - tbChatInput.Enabled = true; + btnNewGame.AllowClick = true; + btnJoinGame.AllowClick = true; + ddCurrentChannel.AllowDropDown = true; + tbChatInput.Enabled = true; - Channel cncnetChannel = connectionManager.FindChannel("#cncnet"); - await cncnetChannel.JoinAsync(); + Channel cncnetChannel = connectionManager.FindChannel("#cncnet"); + await cncnetChannel.JoinAsync(); - string localGameChatChannelName = gameCollection.GetGameChatChannelNameFromIdentifier(localGameID); - await connectionManager.FindChannel(localGameChatChannelName).JoinAsync(); + string localGameChatChannelName = gameCollection.GetGameChatChannelNameFromIdentifier(localGameID); + await connectionManager.FindChannel(localGameChatChannelName).JoinAsync(); - string localGameBroadcastChannel = gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGameID); - await connectionManager.FindChannel(localGameBroadcastChannel).JoinAsync(); + string localGameBroadcastChannel = gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGameID); + await connectionManager.FindChannel(localGameBroadcastChannel).JoinAsync(); - foreach (CnCNetGame game in gameCollection.GameList) - { - if (!game.Supported) - continue; + foreach (CnCNetGame game in gameCollection.GameList) + { + if (!game.Supported) + continue; - if (game.InternalName.ToUpper() != localGameID) + if (game.InternalName.ToUpper() != localGameID) + { + if (UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) { - if (UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) - { - await connectionManager.FindChannel(game.GameBroadcastChannel).JoinAsync(); - followedGames.Add(game.InternalName); - } + await connectionManager.FindChannel(game.GameBroadcastChannel).JoinAsync(); + followedGames.Add(game.InternalName); } } - - gameCheckCancellation = new CancellationTokenSource(); - CnCNetGameCheck gameCheck = new CnCNetGameCheck(); - gameCheck.InitializeService(gameCheckCancellation); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); } + + gameCheckCancellation = new CancellationTokenSource(); + CnCNetGameCheck gameCheck = new CnCNetGameCheck(); + gameCheck.InitializeService(gameCheckCancellation); } private void ConnectionManager_PrivateCTCPReceived(object sender, PrivateCTCPEventArgs e) @@ -1307,107 +1177,91 @@ private void ConnectionManager_PrivateCTCPReceived(object sender, PrivateCTCPEve private async Task HandleGameInviteCommandAsync(string sender, string argumentsString) { - try - { - // arguments are semicolon-delimited - var arguments = argumentsString.Split(';'); + // arguments are semicolon-delimited + var arguments = argumentsString.Split(';'); - // we expect to be given a channel name, a (human-friendly) game name and optionally a password - if (arguments.Length < 2 || arguments.Length > 3) - return; + // we expect to be given a channel name, a (human-friendly) game name and optionally a password + if (arguments.Length < 2 || arguments.Length > 3) + return; - string channelName = arguments[0]; - string gameName = arguments[1]; - string password = (arguments.Length == 3) ? arguments[2] : string.Empty; + string channelName = arguments[0]; + string gameName = arguments[1]; + string password = (arguments.Length == 3) ? arguments[2] : string.Empty; - if (!CanReceiveInvitationMessagesFrom(sender)) - return; + if (!CanReceiveInvitationMessagesFrom(sender)) + return; - var gameIndex = lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName); + var gameIndex = lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName); - // also enforce user preference on whether to accept invitations from non-friends - // this is kept separate from CanReceiveInvitationMessagesFrom() as we still - // want to let the host know that we couldn't receive the invitation - if (!string.IsNullOrEmpty(GetJoinGameErrorByIndex(gameIndex)) || - (UserINISettings.Instance.AllowGameInvitesFromFriendsOnly && - !cncnetUserData.IsFriend(sender))) - { - // let the host know that we can't accept - // note this is not reached for the rejection case - await connectionManager.SendCustomMessageAsync(new QueuedMessage("PRIVMSG " + sender + " :\u0001" + - ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND + "\u0001", - QueuedMessageType.CHAT_MESSAGE, 0)); + // also enforce user preference on whether to accept invitations from non-friends + // this is kept separate from CanReceiveInvitationMessagesFrom() as we still + // want to let the host know that we couldn't receive the invitation + if (!string.IsNullOrEmpty(GetJoinGameErrorByIndex(gameIndex)) || + (UserINISettings.Instance.AllowGameInvitesFromFriendsOnly && + !cncnetUserData.IsFriend(sender))) + { + // let the host know that we can't accept + // note this is not reached for the rejection case + await connectionManager.SendCustomMessageAsync(new QueuedMessage("PRIVMSG " + sender + " :\u0001" + + ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND + "\u0001", + QueuedMessageType.CHAT_MESSAGE, 0)); - return; - } + return; + } - // if there's already an outstanding invitation from this user/channel combination, - // we don't want to display another - // we won't bother telling the host though, since their old invitation is still - // available to us - var invitationIdentity = new UserChannelPair(sender, channelName); + // if there's already an outstanding invitation from this user/channel combination, + // we don't want to display another + // we won't bother telling the host though, since their old invitation is still + // available to us + var invitationIdentity = new UserChannelPair(sender, channelName); - if (invitationIndex.ContainsKey(invitationIdentity)) - { - return; - } + if (invitationIndex.ContainsKey(invitationIdentity)) + { + return; + } - var gameInviteChoiceBox = new ChoiceNotificationBox(WindowManager); + var gameInviteChoiceBox = new ChoiceNotificationBox(WindowManager); - WindowManager.AddAndInitializeControl(gameInviteChoiceBox); + WindowManager.AddAndInitializeControl(gameInviteChoiceBox); - // show the invitation at top left; it will remain until it is acted upon or the target game is closed - gameInviteChoiceBox.Show( - "GAME INVITATION".L10N("Client:Main:GameInviteTitle"), - GetUserTexture(sender), - sender, - string.Format("Join {0}?".L10N("Client:Main:GameInviteText"), gameName), - "Yes".L10N("Client:Main:ButtonYes"), "No".L10N("Client:Main:ButtonNo"), 0); + // show the invitation at top left; it will remain until it is acted upon or the target game is closed + gameInviteChoiceBox.Show( + "GAME INVITATION".L10N("Client:Main:GameInviteTitle"), + GetUserTexture(sender), + sender, + string.Format("Join {0}?".L10N("Client:Main:GameInviteText"), gameName), + "Yes".L10N("Client:Main:ButtonYes"), "No".L10N("Client:Main:ButtonNo"), 0); - // add the invitation to the index so we can remove it if the target game is closed - // also lets us silently ignore new invitations from the same person while this one is still outstanding - invitationIndex[invitationIdentity] = - new WeakReference(gameInviteChoiceBox); + // add the invitation to the index so we can remove it if the target game is closed + // also lets us silently ignore new invitations from the same person while this one is still outstanding + invitationIndex[invitationIdentity] = new WeakReference(gameInviteChoiceBox); - gameInviteChoiceBox.AffirmativeClickedAction = async _ => - { - try - { - // if we're currently in a game lobby, first leave that channel - if (isInGameRoom) - { - await gameLobby.LeaveGameLobbyAsync(); - } - - // JoinGameByIndex does bounds checking so we're safe to pass -1 if the game doesn't exist - if (!await JoinGameByIndexAsync(lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName), password)) - { - XNAMessageBox.Show(WindowManager, - "Failed to join".L10N("Client:Main:JoinFailedTitle"), - string.Format("Unable to join {0}'s game. The game may be locked or closed.".L10N("Client:Main:JoinFailedText"), sender)); - } - - // clean up the index as this invitation no longer exists - invitationIndex.Remove(invitationIdentity); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } - }; + gameInviteChoiceBox.AffirmativeClickedAction = _ => AffirmativeClickedActionAsync(channelName, password, sender, invitationIdentity).HandleTask(); - gameInviteChoiceBox.NegativeClickedAction = delegate (ChoiceNotificationBox choiceBox) - { - // clean up the index as this invitation no longer exists - invitationIndex.Remove(invitationIdentity); - }; + // clean up the index as this invitation no longer exists + gameInviteChoiceBox.NegativeClickedAction = _ => invitationIndex.Remove(invitationIdentity); + + sndGameInviteReceived.Play(); + } - sndGameInviteReceived.Play(); + private async Task AffirmativeClickedActionAsync(string channelName, string password, string sender, UserChannelPair invitationIdentity) + { + // if we're currently in a game lobby, first leave that channel + if (isInGameRoom) + { + await gameLobby.LeaveGameLobbyAsync(); } - catch (Exception ex) + + // JoinGameByIndex does bounds checking so we're safe to pass -1 if the game doesn't exist + if (!await JoinGameByIndexAsync(lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName), password)) { - PreStartup.HandleException(ex); + XNAMessageBox.Show(WindowManager, + "Failed to join".L10N("Client:Main:JoinFailedTitle"), + string.Format("Unable to join {0}'s game. The game may be locked or closed.".L10N("Client:Main:JoinFailedText"), sender)); } + + // clean up the index as this invitation no longer exists + invitationIndex.Remove(invitationIdentity); } private void HandleGameInvitationFailedNotification(string sender) @@ -1426,58 +1280,51 @@ private void HandleGameInvitationFailedNotification(string sender) private async Task DdCurrentChannel_SelectedIndexChangedAsync() { - try + if (currentChatChannel != null) { - if (currentChatChannel != null) + currentChatChannel.UserAdded -= RefreshPlayerList; + currentChatChannel.UserLeft -= RefreshPlayerList; + currentChatChannel.UserQuitIRC -= RefreshPlayerList; + currentChatChannel.UserKicked -= RefreshPlayerList; + currentChatChannel.UserListReceived -= RefreshPlayerList; + currentChatChannel.MessageAdded -= CurrentChatChannel_MessageAdded; + currentChatChannel.UserGameIndexUpdated -= CurrentChatChannel_UserGameIndexUpdated; + + if (currentChatChannel.ChannelName != "#cncnet" && + currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) { - currentChatChannel.UserAdded -= RefreshPlayerList; - currentChatChannel.UserLeft -= RefreshPlayerList; - currentChatChannel.UserQuitIRC -= RefreshPlayerList; - currentChatChannel.UserKicked -= RefreshPlayerList; - currentChatChannel.UserListReceived -= RefreshPlayerList; - currentChatChannel.MessageAdded -= CurrentChatChannel_MessageAdded; - currentChatChannel.UserGameIndexUpdated -= CurrentChatChannel_UserGameIndexUpdated; - - if (currentChatChannel.ChannelName != "#cncnet" && - currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) + // Remove the assigned channels from the users so we don't have ghost users on the PM user list + currentChatChannel.Users.DoForAllUsers(user => { - // Remove the assigned channels from the users so we don't have ghost users on the PM user list - currentChatChannel.Users.DoForAllUsers(user => - { - connectionManager.RemoveChannelFromUser(user.IRCUser.Name, currentChatChannel.ChannelName); - }); + connectionManager.RemoveChannelFromUser(user.IRCUser.Name, currentChatChannel.ChannelName); + }); - await currentChatChannel.LeaveAsync(); - } + await currentChatChannel.LeaveAsync(); } + } - currentChatChannel = (Channel)ddCurrentChannel.SelectedItem.Tag; - currentChatChannel.UserAdded += RefreshPlayerList; - currentChatChannel.UserLeft += RefreshPlayerList; - currentChatChannel.UserQuitIRC += RefreshPlayerList; - currentChatChannel.UserKicked += RefreshPlayerList; - currentChatChannel.UserListReceived += RefreshPlayerList; - currentChatChannel.MessageAdded += CurrentChatChannel_MessageAdded; - currentChatChannel.UserGameIndexUpdated += CurrentChatChannel_UserGameIndexUpdated; - connectionManager.SetMainChannel(currentChatChannel); + currentChatChannel = (Channel)ddCurrentChannel.SelectedItem.Tag; + currentChatChannel.UserAdded += RefreshPlayerList; + currentChatChannel.UserLeft += RefreshPlayerList; + currentChatChannel.UserQuitIRC += RefreshPlayerList; + currentChatChannel.UserKicked += RefreshPlayerList; + currentChatChannel.UserListReceived += RefreshPlayerList; + currentChatChannel.MessageAdded += CurrentChatChannel_MessageAdded; + currentChatChannel.UserGameIndexUpdated += CurrentChatChannel_UserGameIndexUpdated; + connectionManager.SetMainChannel(currentChatChannel); - lbPlayerList.TopIndex = 0; + lbPlayerList.TopIndex = 0; - lbChatMessages.TopIndex = 0; - lbChatMessages.Clear(); - currentChatChannel.Messages.ForEach(msg => AddMessageToChat(msg)); + lbChatMessages.TopIndex = 0; + lbChatMessages.Clear(); + currentChatChannel.Messages.ForEach(msg => AddMessageToChat(msg)); - RefreshPlayerList(this, EventArgs.Empty); + RefreshPlayerList(this, EventArgs.Empty); - if (currentChatChannel.ChannelName != "#cncnet" && - currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) - { - await currentChatChannel.JoinAsync(); - } - } - catch (Exception ex) + if (currentChatChannel.ChannelName != "#cncnet" && + currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) { - PreStartup.HandleException(ex); + await currentChatChannel.JoinAsync(); } } @@ -1578,7 +1425,7 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr e.Message.StartsWith("UPDATE ") && e.Message.Length > 7) { - string version = e.Message.Substring(7); + string version = e.Message[7..]; if (version != ProgramConstants.GAME_VERSION) { var updateMessageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, "Update available".L10N("Client:Main:UpdateAvailableTitle"), @@ -1591,7 +1438,7 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr if (!e.Message.StartsWith("GAME ")) return; - string msg = e.Message.Substring(5); // Cut out GAME part + string msg = e.Message[5..]; // Cut out GAME part string[] splitMessage = msg.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); if (splitMessage.Length != 11) @@ -1609,7 +1456,7 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr int maxPlayers = Conversions.IntFromString(splitMessage[2], 0); string gameRoomChannelName = splitMessage[3]; string gameRoomDisplayName = splitMessage[4]; - bool locked = Conversions.BooleanFromString(splitMessage[5].Substring(0, 1), true); + bool locked = Conversions.BooleanFromString(splitMessage[5][..1], true); bool isCustomPassword = Conversions.BooleanFromString(splitMessage[5].Substring(1, 1), false); bool isClosed = Conversions.BooleanFromString(splitMessage[5].Substring(2, 1), true); bool isLoadedGame = Conversions.BooleanFromString(splitMessage[5].Substring(3, 1), false); @@ -1695,26 +1542,19 @@ private void UpdateMessageBox_YesClicked(XNAMessageBox messageBox) => private async Task BtnLogout_LeftClickAsync() { - try + if (isInGameRoom) { - if (isInGameRoom) - { - topBar.SwitchToPrimary(); - return; - } - - if (connectionManager.IsConnected && - !UserINISettings.Instance.PersistentMode) - { - await connectionManager.DisconnectAsync(); - } - topBar.SwitchToPrimary(); + return; } - catch (Exception ex) + + if (connectionManager.IsConnected && + !UserINISettings.Instance.PersistentMode) { - PreStartup.HandleException(ex); + await connectionManager.DisconnectAsync(); } + + topBar.SwitchToPrimary(); } public void SwitchOn() @@ -1819,27 +1659,20 @@ private HostedCnCNetGame GetHostedGameForUser(IRCUser user) /// The message view/list to write error messages to. private async Task JoinUserAsync(IRCUser user, IMessageView messageView) { - try + if (user == null) { - if (user == null) - { - // can happen if a user is selected while offline - messageView.AddMessage(new ChatMessage(Color.White, "User is not currently available!".L10N("Client:Main:UserNotAvailable"))); - return; - } - var game = GetHostedGameForUser(user); - if (game == null) - { - messageView.AddMessage(new ChatMessage(Color.White, string.Format("{0} is not in a game!".L10N("Client:Main:UserNotInGame"), user.Name))); - return; - } - - await JoinGameAsync(game, string.Empty, messageView); + // can happen if a user is selected while offline + messageView.AddMessage(new ChatMessage(Color.White, "User is not currently available!".L10N("Client:Main:UserNotAvailable"))); + return; } - catch (Exception ex) + var game = GetHostedGameForUser(user); + if (game == null) { - PreStartup.HandleException(ex); + messageView.AddMessage(new ChatMessage(Color.White, string.Format("{0} is not in a game!".L10N("Client:Main:UserNotInGame"), user.Name))); + return; } + + await JoinGameAsync(game, string.Empty, messageView); } } } \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/GameCreationWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/GameCreationWindow.cs index 2a5a95917..69877ec2f 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/GameCreationWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/GameCreationWindow.cs @@ -199,7 +199,7 @@ private void BtnLoadMPGame_LeftClick(object sender, EventArgs e) new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); string password = Utilities.CalculateSHA1ForString( - spawnSGIni.GetStringValue("Settings", "GameID", string.Empty)).Substring(0, 10); + spawnSGIni.GetStringValue("Settings", "GameID", string.Empty))[..10]; GameCreationEventArgs ea = new GameCreationEventArgs(gameName, spawnSGIni.GetIntValue("Settings", "PlayerCount", 2), password, diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs index ea261a5dd..befea5e1a 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs @@ -73,12 +73,12 @@ public override void Initialize() toggleIgnoreItem = new XNAContextMenuItem() { Text = BLOCK, - SelectAction = () => GetIrcUserIdentAsync(cncnetUserData.ToggleIgnoreUser) + SelectAction = () => GetIrcUserIdentAsync(cncnetUserData.ToggleIgnoreUser).HandleTask() }; invitePlayerItem = new XNAContextMenuItem() { Text = INVITE, - SelectAction = () => InviteAsync() + SelectAction = () => InviteAsync().HandleTask() }; joinPlayerItem = new XNAContextMenuItem() { @@ -107,29 +107,21 @@ public override void Initialize() private async Task InviteAsync() { - try + // note it's assumed that if the channel name is specified, the game name must be also + if (string.IsNullOrEmpty(contextMenuData.inviteChannelName) || ProgramConstants.IsInGame) { - // note it's assumed that if the channel name is specified, the game name must be also - if (string.IsNullOrEmpty(contextMenuData.inviteChannelName) || ProgramConstants.IsInGame) - { - return; - } - - string messageBody = ProgramConstants.GAME_INVITE_CTCP_COMMAND + " " + contextMenuData.inviteChannelName + ";" + contextMenuData.inviteGameName; + return; + } - if (!string.IsNullOrEmpty(contextMenuData.inviteChannelPassword)) - { - messageBody += ";" + contextMenuData.inviteChannelPassword; - } + string messageBody = ProgramConstants.GAME_INVITE_CTCP_COMMAND + " " + contextMenuData.inviteChannelName + ";" + contextMenuData.inviteGameName; - await connectionManager.SendCustomMessageAsync(new QueuedMessage( - "PRIVMSG " + GetIrcUser().Name + " :\u0001" + messageBody + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0 - )); - } - catch (Exception ex) + if (!string.IsNullOrEmpty(contextMenuData.inviteChannelPassword)) { - PreStartup.HandleException(ex); + messageBody += ";" + contextMenuData.inviteChannelPassword; } + + await connectionManager.SendCustomMessageAsync(new QueuedMessage( + "PRIVMSG " + GetIrcUser().Name + " :\u0001" + messageBody + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0)); } private void UpdateButtons() @@ -195,30 +187,23 @@ private void CopyLink(string link) private async Task GetIrcUserIdentAsync(Action callback) { - try - { - var ircUser = GetIrcUser(); - - if (!string.IsNullOrEmpty(ircUser.Ident)) - { - callback.Invoke(ircUser.Ident); - return; - } - - void WhoIsReply(object sender, WhoEventArgs whoEventargs) - { - ircUser.Ident = whoEventargs.Ident; - callback.Invoke(whoEventargs.Ident); - connectionManager.WhoReplyReceived -= WhoIsReply; - } + var ircUser = GetIrcUser(); - connectionManager.WhoReplyReceived += WhoIsReply; - await connectionManager.SendWhoIsMessageAsync(ircUser.Name); + if (!string.IsNullOrEmpty(ircUser.Ident)) + { + callback.Invoke(ircUser.Ident); + return; } - catch (Exception ex) + + void WhoIsReply(object sender, WhoEventArgs whoEventargs) { - PreStartup.HandleException(ex); + ircUser.Ident = whoEventargs.Ident; + callback.Invoke(whoEventargs.Ident); + connectionManager.WhoReplyReceived -= WhoIsReply; } + + connectionManager.WhoReplyReceived += WhoIsReply; + await connectionManager.SendWhoIsMessageAsync(ircUser.Name); } private IRCUser GetIrcUser() diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs index 362cdf701..a6a8cdb3c 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs @@ -181,7 +181,7 @@ public override void Initialize() tbMessageInput.Name = nameof(tbMessageInput); tbMessageInput.ClientRectangle = new Rectangle(lbMessages.X, lbMessages.Bottom + 6, lbMessages.Width, 19); - tbMessageInput.EnterPressed += (_, _) => TbMessageInput_EnterPressedAsync(); + tbMessageInput.EnterPressed += (_, _) => TbMessageInput_EnterPressedAsync().HandleTask(); tbMessageInput.MaximumTextLength = 200; tbMessageInput.Enabled = false; @@ -518,57 +518,50 @@ private void ShowNotification(IRCUser ircUser, string message) private async Task TbMessageInput_EnterPressedAsync() { - try - { - if (string.IsNullOrEmpty(tbMessageInput.Text)) - return; + if (string.IsNullOrEmpty(tbMessageInput.Text)) + return; - if (lbUserList.SelectedItem == null) - return; + if (lbUserList.SelectedItem == null) + return; - string userName = lbUserList.SelectedItem.Text; + string userName = lbUserList.SelectedItem.Text; - await connectionManager.SendCustomMessageAsync(new QueuedMessage("PRIVMSG " + userName + " :" + tbMessageInput.Text, - QueuedMessageType.CHAT_MESSAGE, 0)); + await connectionManager.SendCustomMessageAsync(new QueuedMessage("PRIVMSG " + userName + " :" + tbMessageInput.Text, + QueuedMessageType.CHAT_MESSAGE, 0)); - PrivateMessageUser pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == userName); - if (pmUser == null) - { - IRCUser iu = connectionManager.UserList.Find(u => u.Name == userName); - - if (iu == null) - { - Logger.Log("Null IRCUser in private messaging?"); - return; - } + PrivateMessageUser pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == userName); + if (pmUser == null) + { + IRCUser iu = connectionManager.UserList.Find(u => u.Name == userName); - pmUser = new PrivateMessageUser(iu); - privateMessageUsers.Add(pmUser); + if (iu == null) + { + Logger.Log("Null IRCUser in private messaging?"); + return; } - ChatMessage sentMessage = new ChatMessage(ProgramConstants.PLAYERNAME, - personalMessageColor, DateTime.Now, tbMessageInput.Text); + pmUser = new PrivateMessageUser(iu); + privateMessageUsers.Add(pmUser); + } - pmUser.Messages.Add(sentMessage); + ChatMessage sentMessage = new ChatMessage(ProgramConstants.PLAYERNAME, + personalMessageColor, DateTime.Now, tbMessageInput.Text); - lbMessages.AddMessage(sentMessage); - if (sndMessageSound != null) - sndMessageSound.Play(); + pmUser.Messages.Add(sentMessage); - lastConversationPartner = userName; + lbMessages.AddMessage(sentMessage); + if (sndMessageSound != null) + sndMessageSound.Play(); - if (tabControl.SelectedTab != MESSAGES_INDEX) - { - tabControl.SelectedTab = MESSAGES_INDEX; - lbUserList.SelectedIndex = FindItemIndexForName(userName); - } + lastConversationPartner = userName; - tbMessageInput.Text = string.Empty; - } - catch (Exception ex) + if (tabControl.SelectedTab != MESSAGES_INDEX) { - PreStartup.HandleException(ex); + tabControl.SelectedTab = MESSAGES_INDEX; + lbUserList.SelectedIndex = FindItemIndexForName(userName); } + + tbMessageInput.Text = string.Empty; } private void LbUserList_SelectedIndexChanged(object sender, EventArgs e) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index fb70b63de..b8f26cd83 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -145,7 +145,7 @@ public override void Initialize() ddSavedGame.ClientRectangle = new Rectangle(lblSavedGameTime.X, panelPlayers.Bottom - 21, Width - lblSavedGameTime.X - 12, 21); - ddSavedGame.SelectedIndexChanged += (_, _) => DdSavedGame_SelectedIndexChangedAsync(); + ddSavedGame.SelectedIndexChanged += (_, _) => DdSavedGame_SelectedIndexChangedAsync().HandleTask(); lbChatMessages = new ChatListBox(WindowManager); lbChatMessages.Name = nameof(lbChatMessages); @@ -160,21 +160,21 @@ public override void Initialize() tbChatInput.ClientRectangle = new Rectangle(lbChatMessages.X, lbChatMessages.Bottom + 3, lbChatMessages.Width, 19); tbChatInput.MaximumTextLength = 200; - tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync(); + tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync().HandleTask(); btnLoadGame = new XNAClientButton(WindowManager); btnLoadGame.Name = nameof(btnLoadGame); btnLoadGame.ClientRectangle = new Rectangle(lbChatMessages.X, tbChatInput.Bottom + 6, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLoadGame.Text = "Load Game".L10N("Client:Main:LoadGame"); - btnLoadGame.LeftClick += (_, _) => BtnLoadGame_LeftClickAsync(); + btnLoadGame.LeftClick += (_, _) => BtnLoadGame_LeftClickAsync().HandleTask(); btnLeaveGame = new XNAClientButton(WindowManager); btnLeaveGame.Name = nameof(btnLeaveGame); btnLeaveGame.ClientRectangle = new Rectangle(Width - 145, btnLoadGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLeaveGame.Text = "Leave Game".L10N("Client:Main:LeaveGame"); - btnLeaveGame.LeftClick += (_, _) => BtnLeaveGame_LeftClickAsync(); + btnLeaveGame.LeftClick += (_, _) => LeaveGameAsync().HandleTask(); AddChild(lblMapName); AddChild(lblMapNameValue); @@ -218,19 +218,10 @@ public override void Initialize() /// private void ResetDiscordPresence() => discordHandler.UpdatePresence(); - private Task BtnLeaveGame_LeftClickAsync() => LeaveGameAsync(); - protected virtual Task LeaveGameAsync() { - try - { - GameLeft?.Invoke(this, EventArgs.Empty); - ResetDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + GameLeft?.Invoke(this, EventArgs.Empty); + ResetDiscordPresence(); return Task.CompletedTask; } @@ -250,29 +241,22 @@ private void HandleFSWEvent(FileSystemEventArgs e) private async Task BtnLoadGame_LeftClickAsync() { - try + if (!IsHost) { - if (!IsHost) - { - await RequestReadyStatusAsync(); - return; - } - - if (Players.Find(p => !p.Ready) != null) - { - await GetReadyNotificationAsync(); - return; - } + await RequestReadyStatusAsync(); + return; + } - if (Players.Count != SGPlayers.Count) - { - await NotAllPresentNotificationAsync(); - return; - } + if (Players.Find(p => !p.Ready) != null) + { + await GetReadyNotificationAsync(); + return; } - catch (Exception ex) + + if (Players.Count != SGPlayers.Count) { - PreStartup.HandleException(ex); + await NotAllPresentNotificationAsync(); + return; } await HostStartGameAsync(); @@ -361,37 +345,31 @@ protected void LoadGame() UpdateDiscordPresence(true); } - private void SharedUILogic_GameProcessExited() => AddCallback(HandleGameProcessExitedAsync); + private void SharedUILogic_GameProcessExited() => AddCallback(() => HandleGameProcessExitedAsync().HandleTask()); protected virtual Task HandleGameProcessExitedAsync() { - try - { - fsw.EnableRaisingEvents = false; + fsw.EnableRaisingEvents = false; - GameProcessLogic.GameProcessExited -= SharedUILogic_GameProcessExited; + GameProcessLogic.GameProcessExited -= SharedUILogic_GameProcessExited; - var matchStatistics = StatisticsManager.Instance.GetMatchWithGameID(uniqueGameId); + var matchStatistics = StatisticsManager.Instance.GetMatchWithGameID(uniqueGameId); - if (matchStatistics != null) - { - int newLength = matchStatistics.LengthInSeconds + - (int)(DateTime.Now - gameLoadTime).TotalSeconds; + if (matchStatistics != null) + { + int newLength = matchStatistics.LengthInSeconds + + (int)(DateTime.Now - gameLoadTime).TotalSeconds; - matchStatistics.ParseStatistics(ProgramConstants.GamePath, - ClientConfiguration.Instance.LocalGame, true); + matchStatistics.ParseStatistics(ProgramConstants.GamePath, + ClientConfiguration.Instance.LocalGame, true); - matchStatistics.LengthInSeconds = newLength; + matchStatistics.LengthInSeconds = newLength; - StatisticsManager.Instance.SaveDatabase(); - } - UpdateDiscordPresence(true); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + StatisticsManager.Instance.SaveDatabase(); } + UpdateDiscordPresence(true); + return Task.CompletedTask; } @@ -502,40 +480,26 @@ protected void CopyPlayerDataToUI() private async Task DdSavedGame_SelectedIndexChangedAsync() { - try - { - if (!IsHost) - return; + if (!IsHost) + return; - for (int i = 1; i < Players.Count; i++) - Players[i].Ready = false; + for (int i = 1; i < Players.Count; i++) + Players[i].Ready = false; - CopyPlayerDataToUI(); + CopyPlayerDataToUI(); - if (!isSettingUp) - await BroadcastOptionsAsync(); - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (!isSettingUp) + await BroadcastOptionsAsync(); + UpdateDiscordPresence(); } private async Task TbChatInput_EnterPressedAsync() { - try - { - if (string.IsNullOrEmpty(tbChatInput.Text)) - return; + if (string.IsNullOrEmpty(tbChatInput.Text)) + return; - await SendChatMessageAsync(tbChatInput.Text); - tbChatInput.Text = string.Empty; - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await SendChatMessageAsync(tbChatInput.Text); + tbChatInput.Text = string.Empty; } /// diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 14aa70025..374e1df54 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -60,15 +60,16 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby #endregion public CnCNetGameLobby( - WindowManager windowManager, - TopBar topBar, + WindowManager windowManager, + TopBar topBar, CnCNetManager connectionManager, - TunnelHandler tunnelHandler, - GameCollection gameCollection, - CnCNetUserData cncnetUserData, - MapLoader mapLoader, - DiscordHandler discordHandler - ) : base(windowManager, "MultiplayerGameLobby", topBar, mapLoader, discordHandler) + TunnelHandler tunnelHandler, + GameCollection gameCollection, + CnCNetUserData cncnetUserData, + MapLoader mapLoader, + DiscordHandler discordHandler, + PrivateMessagingWindow pmWindow) + : base(windowManager, "MultiplayerGameLobby", topBar, mapLoader, discordHandler) { this.connectionManager = connectionManager; localGame = ClientConfiguration.Instance.LocalGame; @@ -79,50 +80,57 @@ DiscordHandler discordHandler ctcpCommandHandlers = new CommandHandlerBase[] { - new IntCommandHandler("OR", (playerName, options) => HandleOptionsRequestAsync(playerName, options)), - new IntCommandHandler("R", (playerName, options) => HandleReadyRequestAsync(playerName, options)), + new IntCommandHandler("OR", (playerName, options) => HandleOptionsRequestAsync(playerName, options).HandleTask()), + new IntCommandHandler("R", (playerName, options) => HandleReadyRequestAsync(playerName, options).HandleTask()), new StringCommandHandler("PO", ApplyPlayerOptions), new StringCommandHandler(PlayerExtraOptions.CNCNET_MESSAGE_KEY, ApplyPlayerExtraOptions), - new StringCommandHandler("GO", (sender, message) => ApplyGameOptionsAsync(sender, message)), - new StringCommandHandler(GAME_START_MESSAGE, (sender, message) => NonHostLaunchGameAsync(sender, message)), + new StringCommandHandler("GO", (sender, message) => ApplyGameOptionsAsync(sender, message).HandleTask()), + new StringCommandHandler(GAME_START_MESSAGE, (sender, message) => NonHostLaunchGameAsync(sender, message).HandleTask()), new StringCommandHandler(GAME_START_MESSAGE_V3, HandleGameStartV3TunnelMessage), - new NoParamCommandHandler(TUNNEL_CONNECTION_OK_MESSAGE, playerName => HandleTunnelConnectedAsync(playerName)), + new NoParamCommandHandler(TUNNEL_CONNECTION_OK_MESSAGE, playerName => HandleTunnelConnectedAsync(playerName).HandleTask()), new NoParamCommandHandler(TUNNEL_CONNECTION_FAIL_MESSAGE, HandleTunnelFail), - new NotificationHandler("AISPECS", HandleNotification, () => AISpectatorsNotificationAsync()), - new NotificationHandler("GETREADY", HandleNotification, () => GetReadyNotificationAsync()), - new NotificationHandler("INSFSPLRS", HandleNotification, () => InsufficientPlayersNotificationAsync()), - new NotificationHandler("TMPLRS", HandleNotification, () => TooManyPlayersNotificationAsync()), - new NotificationHandler("CLRS", HandleNotification, () => SharedColorsNotificationAsync()), - new NotificationHandler("SLOC", HandleNotification, () => SharedStartingLocationNotificationAsync()), - new NotificationHandler("LCKGME", HandleNotification, () => LockGameNotificationAsync()), - new IntNotificationHandler("NVRFY", HandleIntNotification, playerIndex => NotVerifiedNotificationAsync(playerIndex)), - new IntNotificationHandler("INGM", HandleIntNotification, playerIndex => StillInGameNotificationAsync(playerIndex)), + new NotificationHandler("AISPECS", HandleNotification, () => AISpectatorsNotificationAsync().HandleTask()), + new NotificationHandler("GETREADY", HandleNotification, () => GetReadyNotificationAsync().HandleTask()), + new NotificationHandler("INSFSPLRS", HandleNotification, () => InsufficientPlayersNotificationAsync().HandleTask()), + new NotificationHandler("TMPLRS", HandleNotification, () => TooManyPlayersNotificationAsync().HandleTask()), + new NotificationHandler("CLRS", HandleNotification, () => SharedColorsNotificationAsync().HandleTask()), + new NotificationHandler("SLOC", HandleNotification, () => SharedStartingLocationNotificationAsync().HandleTask()), + new NotificationHandler("LCKGME", HandleNotification, () => LockGameNotificationAsync().HandleTask()), + new IntNotificationHandler("NVRFY", HandleIntNotification, playerIndex => NotVerifiedNotificationAsync(playerIndex).HandleTask()), + new IntNotificationHandler("INGM", HandleIntNotification, playerIndex => StillInGameNotificationAsync(playerIndex).HandleTask()), new StringCommandHandler(MAP_SHARING_UPLOAD_REQUEST, HandleMapUploadRequest), new StringCommandHandler(MAP_SHARING_FAIL_MESSAGE, HandleMapTransferFailMessage), new StringCommandHandler(MAP_SHARING_DOWNLOAD_REQUEST, HandleMapDownloadRequest), new NoParamCommandHandler(MAP_SHARING_DISABLED_MESSAGE, HandleMapSharingBlockedMessage), new NoParamCommandHandler("RETURN", ReturnNotification), new IntCommandHandler("TNLPNG", HandleTunnelPing), - new StringCommandHandler("FHSH", (sender, filesHash) => FileHashNotificationAsync(sender, filesHash)), + new StringCommandHandler("FHSH", (sender, filesHash) => FileHashNotificationAsync(sender, filesHash).HandleTask()), new StringCommandHandler("MM", CheaterNotification), new StringCommandHandler(DICE_ROLL_MESSAGE, HandleDiceRollResult), new NoParamCommandHandler(CHEAT_DETECTED_MESSAGE, HandleCheatDetectedMessage), - new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, (sender, tunnelAddressAndPort) => HandleTunnelServerChangeMessageAsync(sender, tunnelAddressAndPort)) + new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, (sender, tunnelAddressAndPort) => HandleTunnelServerChangeMessageAsync(sender, tunnelAddressAndPort).HandleTask()) }; - MapSharer.MapDownloadFailed += MapSharer_MapDownloadFailed; - MapSharer.MapDownloadComplete += MapSharer_MapDownloadComplete; - MapSharer.MapUploadFailed += MapSharer_MapUploadFailed; - MapSharer.MapUploadComplete += MapSharer_MapUploadComplete; - - AddChatBoxCommand(new ChatBoxCommand("TUNNELINFO", - "View tunnel server information".L10N("Client:Main:TunnelInfoCommand"), false, PrintTunnelServerInformation)); - AddChatBoxCommand(new ChatBoxCommand("CHANGETUNNEL", + MapSharer.MapDownloadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapDownloadFailedAsync(e).HandleTask()); + MapSharer.MapDownloadComplete += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapDownloadCompleteAsync(e).HandleTask()); + MapSharer.MapUploadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapUploadFailedAsync(e).HandleTask()); + MapSharer.MapUploadComplete += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapUploadCompleteAsync(e).HandleTask()); + + AddChatBoxCommand(new ChatBoxCommand( + "TUNNELINFO", + "View tunnel server information".L10N("Client:Main:TunnelInfoCommand"), + false, + PrintTunnelServerInformation)); + AddChatBoxCommand(new ChatBoxCommand( + "CHANGETUNNEL", "Change the used CnCNet tunnel server (game host only)".L10N("Client:Main:ChangeTunnelCommand"), - true, (s) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServerCommand")))); - AddChatBoxCommand(new ChatBoxCommand("DOWNLOADMAP", + true, + _ => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServerCommand")))); + AddChatBoxCommand(new ChatBoxCommand( + "DOWNLOADMAP", "Download a map from CNCNet's map server using a map ID and an optional filename.\nExample: \"/downloadmap MAPID [2] My Battle Map\"".L10N("Client:Main:DownloadMapCommandDescription"), - false, DownloadMapByIdCommand)); + false, + DownloadMapByIdCommand)); } public event EventHandler GameLeft; @@ -200,8 +208,8 @@ public override void Initialize() base.Initialize(); gameTunnelHandler = new GameTunnelHandler(); - gameTunnelHandler.Connected += GameTunnelHandler_Connected; - gameTunnelHandler.ConnectionFailed += GameTunnelHandler_ConnectionFailed; + gameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + gameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); btnChangeTunnel = FindChild(nameof(btnChangeTunnel)); btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick; @@ -210,7 +218,7 @@ public override void Initialize() gameBroadcastTimer.AutoReset = true; gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL); gameBroadcastTimer.Enabled = false; - gameBroadcastTimer.TimeElapsed += (_, _) => GameBroadcastTimer_TimeElapsedAsync(); + gameBroadcastTimer.TimeElapsed += (_, _) => BroadcastGameAsync().HandleTask(); gameStartTimer = new XNATimerControl(WindowManager); gameStartTimer.AutoReset = false; @@ -224,7 +232,7 @@ public override void Initialize() DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow); tunnelSelectionWindow.CenterOnParent(); tunnelSelectionWindow.Disable(); - tunnelSelectionWindow.TunnelSelected += (_, e) => TunnelSelectionWindow_TunnelSelectedAsync(e); + tunnelSelectionWindow.TunnelSelected += (_, e) => TunnelSelectionWindow_TunnelSelectedAsync(e).HandleTask(); mapSharingConfirmationPanel = new MapSharingConfirmationPanel(WindowManager); MapPreviewBox.AddChild(mapSharingConfirmationPanel); @@ -238,14 +246,14 @@ public override void Initialize() MultiplayerNameRightClicked += MultiplayerName_RightClick; - channel_UserAddedFunc = (_, e) => Channel_UserAddedAsync(e); - channel_UserQuitIRCFunc = (_, e) => Channel_UserQuitIRCAsync(e); - channel_UserLeftFunc = (_, e) => Channel_UserLeftAsync(e); - channel_UserKickedFunc = (_, e) => Channel_UserKickedAsync(e); - channel_UserListReceivedFunc = (_, _) => Channel_UserListReceivedAsync(); - connectionManager_ConnectionLostFunc = (_, _) => ConnectionManager_ConnectionLostAsync(); - connectionManager_DisconnectedFunc = (_, _) => ConnectionManager_DisconnectedAsync(); - tunnelHandler_CurrentTunnelFunc = (_, _) => TunnelHandler_CurrentTunnelPingedAsync(); + channel_UserAddedFunc = (_, e) => Channel_UserAddedAsync(e).HandleTask(); + channel_UserQuitIRCFunc = (_, e) => Channel_UserQuitIRCAsync(e).HandleTask(); + channel_UserLeftFunc = (_, e) => Channel_UserLeftAsync(e).HandleTask(); + channel_UserKickedFunc = (_, e) => Channel_UserKickedAsync(e).HandleTask(); + channel_UserListReceivedFunc = (_, _) => Channel_UserListReceivedAsync().HandleTask(); + connectionManager_ConnectionLostFunc = (_, _) => HandleConnectionLossAsync().HandleTask(); + connectionManager_DisconnectedFunc = (_, _) => HandleConnectionLossAsync().HandleTask(); + tunnelHandler_CurrentTunnelFunc = (_, _) => UpdatePingAsync().HandleTask(); PostInitialize(); } @@ -282,8 +290,6 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")); - private Task GameBroadcastTimer_TimeElapsedAsync() => BroadcastGameAsync(); - public async Task SetUpAsync(Channel channel, bool isHost, int playerLimit, CnCNetTunnel tunnel, string hostName, bool isCustomPassword, bool isP2P) { @@ -324,8 +330,6 @@ public async Task SetUpAsync(Channel channel, bool isHost, int playerLimit, Refresh(isHost); } - private Task TunnelHandler_CurrentTunnelPingedAsync() => UpdatePingAsync(); - public async Task OnJoinedAsync() { FileHashCalculator fhc = new FileHashCalculator(); @@ -364,23 +368,16 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage( private async Task UpdatePingAsync() { - try - { - if (tunnelHandler.CurrentTunnel == null) - return; + if (tunnelHandler.CurrentTunnel == null) + return; - await channel.SendCTCPMessageAsync("TNLPNG " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync("TNLPNG " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); - PlayerInfo pInfo = Players.Find(p => p.Name.Equals(ProgramConstants.PLAYERNAME)); - if (pInfo != null) - { - pInfo.Ping = tunnelHandler.CurrentTunnel.PingInMs; - UpdatePlayerPingIndicator(pInfo); - } - } - catch (Exception ex) + PlayerInfo pInfo = Players.Find(p => p.Name.Equals(ProgramConstants.PLAYERNAME)); + if (pInfo != null) { - PreStartup.HandleException(ex); + pInfo.Ping = tunnelHandler.CurrentTunnel.PingInMs; + UpdatePlayerPingIndicator(pInfo); } } @@ -414,16 +411,11 @@ private void ShowTunnelSelectionWindow(string description) private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) { - try - { - await channel.SendCTCPMessageAsync($"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", - QueuedMessageType.SYSTEM_MESSAGE, 10); - await HandleTunnelServerChangeAsync(e.Tunnel); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await channel.SendCTCPMessageAsync( + $"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", + QueuedMessageType.SYSTEM_MESSAGE, + 10); + await HandleTunnelServerChangeAsync(e.Tunnel); } public void ChangeChatColor(IRCColor chatColor) @@ -475,39 +467,20 @@ public override async Task ClearAsync() public async Task LeaveGameLobbyAsync() { - try - { - if (IsHost) - { - closed = true; - await BroadcastGameAsync(); - } - - await ClearAsync(); - await channel.LeaveAsync(); - } - catch (Exception ex) + if (IsHost) { - PreStartup.HandleException(ex); + closed = true; + await BroadcastGameAsync(); } - } - - private Task ConnectionManager_DisconnectedAsync() => HandleConnectionLossAsync(); - private Task ConnectionManager_ConnectionLostAsync() => HandleConnectionLossAsync(); + await ClearAsync(); + await channel.LeaveAsync(); + } private async Task HandleConnectionLossAsync() { - try - { - await ClearAsync(); - Disable(); - - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await ClearAsync(); + Disable(); } private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) @@ -523,7 +496,8 @@ private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) } } - protected override Task BtnLeaveGame_LeftClickAsync() => LeaveGameLobbyAsync(); + protected override Task BtnLeaveGame_LeftClickAsync() + => LeaveGameLobbyAsync(); protected override void UpdateDiscordPresence(bool resetTimer = false) { @@ -546,146 +520,112 @@ protected override void UpdateDiscordPresence(bool resetTimer = false) private async Task Channel_UserQuitIRCAsync(UserNameEventArgs e) { - try - { - await RemovePlayerAsync(e.UserName); + await RemovePlayerAsync(e.UserName); - if (e.UserName == hostName) - { - connectionManager.MainChannel.AddMessage(new ChatMessage( - ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); - await BtnLeaveGame_LeftClickAsync(); - } - else - { - UpdateDiscordPresence(); - } + if (e.UserName == hostName) + { + connectionManager.MainChannel.AddMessage(new ChatMessage( + ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); + await BtnLeaveGame_LeftClickAsync(); } - catch (Exception ex) + else { - PreStartup.HandleException(ex); + UpdateDiscordPresence(); } } private async Task Channel_UserLeftAsync(UserNameEventArgs e) { - try - { - await RemovePlayerAsync(e.UserName); + await RemovePlayerAsync(e.UserName); - if (e.UserName == hostName) - { - connectionManager.MainChannel.AddMessage(new ChatMessage( - ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); - await BtnLeaveGame_LeftClickAsync(); - } - else - { - UpdateDiscordPresence(); - } + if (e.UserName == hostName) + { + connectionManager.MainChannel.AddMessage(new ChatMessage( + ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); + await BtnLeaveGame_LeftClickAsync(); } - catch (Exception ex) + else { - PreStartup.HandleException(ex); + UpdateDiscordPresence(); } } private async Task Channel_UserKickedAsync(UserNameEventArgs e) { - try + if (e.UserName == ProgramConstants.PLAYERNAME) { - if (e.UserName == ProgramConstants.PLAYERNAME) - { - connectionManager.MainChannel.AddMessage(new ChatMessage( - ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); - await ClearAsync(); - Visible = false; - Enabled = false; - return; - } + connectionManager.MainChannel.AddMessage(new ChatMessage( + ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); + await ClearAsync(); + Visible = false; + Enabled = false; + return; + } - int index = Players.FindIndex(p => p.Name == e.UserName); + int index = Players.FindIndex(p => p.Name == e.UserName); - if (index > -1) - { - Players.RemoveAt(index); - CopyPlayerDataToUI(); - UpdateDiscordPresence(); - ClearReadyStatuses(); - } - } - catch (Exception ex) + if (index > -1) { - PreStartup.HandleException(ex); + Players.RemoveAt(index); + CopyPlayerDataToUI(); + UpdateDiscordPresence(); + ClearReadyStatuses(); } } private async Task Channel_UserListReceivedAsync() { - try + if (!IsHost) { - if (!IsHost) + if (channel.Users.Find(hostName) == null) { - if (channel.Users.Find(hostName) == null) - { - connectionManager.MainChannel.AddMessage(new ChatMessage( - ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); - await BtnLeaveGame_LeftClickAsync(); - } + connectionManager.MainChannel.AddMessage(new ChatMessage( + ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); + await BtnLeaveGame_LeftClickAsync(); } - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); } + + UpdateDiscordPresence(); } private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) { - try - { - PlayerInfo pInfo = new PlayerInfo(e.User.IRCUser.Name); - Players.Add(pInfo); + PlayerInfo pInfo = new PlayerInfo(e.User.IRCUser.Name); + Players.Add(pInfo); - if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT && AIPlayers.Count > 0) - AIPlayers.RemoveAt(AIPlayers.Count - 1); + if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT && AIPlayers.Count > 0) + AIPlayers.RemoveAt(AIPlayers.Count - 1); - sndJoinSound.Play(); + sndJoinSound.Play(); #if WINFORMS - WindowManager.FlashWindow(); + WindowManager.FlashWindow(); #endif - if (!IsHost) - { - CopyPlayerDataToUI(); - return; - } - - if (e.User.IRCUser.Name != ProgramConstants.PLAYERNAME) - { - // Changing the map applies forced settings (co-op sides etc.) to the - // new player, and it also sends an options broadcast message - await ChangeMapAsync(GameModeMap); - await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); - UpdateDiscordPresence(); - } - else - { - Players[0].Ready = true; - CopyPlayerDataToUI(); - } + if (!IsHost) + { + CopyPlayerDataToUI(); + return; + } - if (Players.Count >= playerLimit) - { - AddNotice("Player limit reached. The game room has been locked.".L10N("Client:Main:GameRoomNumberLimitReached")); - await LockGameAsync(); - } + if (e.User.IRCUser.Name != ProgramConstants.PLAYERNAME) + { + // Changing the map applies forced settings (co-op sides etc.) to the + // new player, and it also sends an options broadcast message + await ChangeMapAsync(GameModeMap); + await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerExtraOptionsAsync(); + UpdateDiscordPresence(); } - catch (Exception ex) + else { - PreStartup.HandleException(ex); + Players[0].Ready = true; + CopyPlayerDataToUI(); + } + + if (Players.Count >= playerLimit) + { + AddNotice("Player limit reached. The game room has been locked.".L10N("Client:Main:GameRoomNumberLimitReached")); + await LockGameAsync(); } } @@ -766,44 +706,37 @@ private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) /// protected override async Task HostLaunchGameAsync() { - try + if (Players.Count > 1) { - if (Players.Count > 1) - { - if (isP2P) - throw new NotImplementedException("Peer-to-peer is not implemented yet."); + if (isP2P) + throw new NotImplementedException("Peer-to-peer is not implemented yet."); - AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); + AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); - if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) - { - await StartGame_V2TunnelAsync(); - } - else if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) - { - await StartGame_V3TunnelAsync(); - } - else - { - throw new InvalidOperationException("Unknown tunnel server version!"); - } - - return; + if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) + { + await StartGame_V2TunnelAsync(); + } + else if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) + { + await StartGame_V3TunnelAsync(); + } + else + { + throw new InvalidOperationException("Unknown tunnel server version!"); } - Logger.Log("One player MP -- starting!"); + return; + } - Players.ForEach(pInfo => pInfo.IsInGame = true); - CopyPlayerDataToUI(); + Logger.Log("One player MP -- starting!"); - cncnetUserData.AddRecentPlayers(Players.Select(p => p.Name), channel.UIName); + Players.ForEach(pInfo => pInfo.IsInGame = true); + CopyPlayerDataToUI(); - await StartGameAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + cncnetUserData.AddRecentPlayers(Players.Select(p => p.Name), channel.UIName); + + await StartGameAsync(); } private async Task StartGame_V2TunnelAsync() @@ -900,40 +833,16 @@ private void ContactTunnel() gameStartTimer.Start(); } - private void GameTunnelHandler_Connected(object sender, EventArgs e) - { - AddCallback(GameTunnelHandler_Connected_CallbackAsync); - } - private async Task GameTunnelHandler_Connected_CallbackAsync() { - try - { - isPlayerConnectedToTunnel[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)] = true; - await channel.SendCTCPMessageAsync(TUNNEL_CONNECTION_OK_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } - } - - private void GameTunnelHandler_ConnectionFailed(object sender, EventArgs e) - { - AddCallback(GameTunnelHandler_ConnectionFailed_CallbackAsync); + isPlayerConnectedToTunnel[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)] = true; + await channel.SendCTCPMessageAsync(TUNNEL_CONNECTION_OK_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); } private async Task GameTunnelHandler_ConnectionFailed_CallbackAsync() { - try - { - await channel.SendCTCPMessageAsync(TUNNEL_CONNECTION_FAIL_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); - HandleTunnelFail(ProgramConstants.PLAYERNAME); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await channel.SendCTCPMessageAsync(TUNNEL_CONNECTION_FAIL_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); + HandleTunnelFail(ProgramConstants.PLAYERNAME); } private void HandleTunnelFail(string playerName) @@ -946,47 +855,40 @@ private void HandleTunnelFail(string playerName) private async Task HandleTunnelConnectedAsync(string playerName) { - try + if (!isStartingGame) + return; + + int index = Players.FindIndex(p => p.Name == playerName); + if (index == -1) { - if (!isStartingGame) - return; + Logger.Log("HandleTunnelConnected: Couldn't find player " + playerName + "!"); + AbortGameStart(); + return; + } - int index = Players.FindIndex(p => p.Name == playerName); - if (index == -1) - { - Logger.Log("HandleTunnelConnected: Couldn't find player " + playerName + "!"); - AbortGameStart(); - return; - } + isPlayerConnectedToTunnel[index] = true; - isPlayerConnectedToTunnel[index] = true; + if (isPlayerConnectedToTunnel.All(b => b)) + { + Logger.Log("All players are connected to the tunnel, starting game!"); + AddNotice("All players have connected to the tunnel..."); - if (isPlayerConnectedToTunnel.All(b => b)) + // Remove our own ID from the list + List ids = new List(tunnelPlayerIds); + ids.Remove(tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); + List players = new List(Players); + int myIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); + players.RemoveAt(myIndex); + Tuple ports = gameTunnelHandler.CreatePlayerConnections(ids); + for (int i = 0; i < ports.Item1.Length; i++) { - Logger.Log("All players are connected to the tunnel, starting game!"); - AddNotice("All players have connected to the tunnel..."); - - // Remove our own ID from the list - List ids = new List(tunnelPlayerIds); - ids.Remove(tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); - List players = new List(Players); - int myIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); - players.RemoveAt(myIndex); - Tuple ports = gameTunnelHandler.CreatePlayerConnections(ids); - for (int i = 0; i < ports.Item1.Length; i++) - { - players[i].Port = ports.Item1[i]; - } - - Players.Single(p => p.Name == ProgramConstants.PLAYERNAME).Port = ports.Item2; - gameStartTimer.Pause(); - btnLaunchGame.InputEnabled = true; - await StartGameAsync(); + players[i].Port = ports.Item1[i]; } - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + + Players.Single(p => p.Name == ProgramConstants.PLAYERNAME).Port = ports.Item2; + gameStartTimer.Pause(); + btnLaunchGame.InputEnabled = true; + await StartGameAsync(); } } @@ -1056,71 +958,64 @@ protected override async Task RequestReadyStatusAsync() /// private async Task HandleOptionsRequestAsync(string playerName, int options) { - try - { - if (!IsHost) - return; - - if (ProgramConstants.IsInGame) - return; - - PlayerInfo pInfo = Players.Find(p => p.Name == playerName); + if (!IsHost) + return; - if (pInfo == null) - return; + if (ProgramConstants.IsInGame) + return; - byte[] bytes = BitConverter.GetBytes(options); + PlayerInfo pInfo = Players.Find(p => p.Name == playerName); - int side = bytes[0]; - int color = bytes[1]; - int start = bytes[2]; - int team = bytes[3]; + if (pInfo == null) + return; - if (side < 0 || side > SideCount + RandomSelectorCount) - return; + byte[] bytes = BitConverter.GetBytes(options); - if (color < 0 || color > MPColors.Count) - return; + int side = bytes[0]; + int color = bytes[1]; + int start = bytes[2]; + int team = bytes[3]; - var disallowedSides = GetDisallowedSides(); + if (side < 0 || side > SideCount + RandomSelectorCount) + return; - if (side > 0 && side <= SideCount && disallowedSides[side - 1]) - return; + if (color < 0 || color > MPColors.Count) + return; - if (Map.CoopInfo != null) - { - if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) - return; + var disallowedSides = GetDisallowedSides(); - if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) - return; - } + if (side > 0 && side <= SideCount && disallowedSides[side - 1]) + return; - if (start < 0 || start > Map.MaxPlayers) + if (Map.CoopInfo != null) + { + if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) return; - if (team < 0 || team > 4) + if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) return; + } - if (side != pInfo.SideId - || start != pInfo.StartingLocation - || team != pInfo.TeamId) - { - ClearReadyStatuses(); - } + if (start < 0 || start > Map.MaxPlayers) + return; - pInfo.SideId = side; - pInfo.ColorId = color; - pInfo.StartingLocation = start; - pInfo.TeamId = team; + if (team < 0 || team > 4) + return; - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - } - catch (Exception ex) + if (side != pInfo.SideId + || start != pInfo.StartingLocation + || team != pInfo.TeamId) { - PreStartup.HandleException(ex); + ClearReadyStatuses(); } + + pInfo.SideId = side; + pInfo.ColorId = color; + pInfo.StartingLocation = start; + pInfo.TeamId = team; + + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); } /// @@ -1128,27 +1023,19 @@ private async Task HandleOptionsRequestAsync(string playerName, int options) /// private async Task HandleReadyRequestAsync(string playerName, int readyStatus) { - try - { - if (!IsHost) - return; + if (!IsHost) + return; - PlayerInfo pInfo = Players.Find(p => p.Name == playerName); + PlayerInfo pInfo = Players.Find(p => p.Name == playerName); - if (pInfo == null) - return; + if (pInfo == null) + return; - pInfo.Ready = readyStatus > 0; - pInfo.AutoReady = readyStatus > 1; + pInfo.Ready = readyStatus > 0; + pInfo.AutoReady = readyStatus > 1; - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); } /// @@ -1195,17 +1082,8 @@ protected override Task BroadcastPlayerOptionsAsync() protected override async Task PlayerExtraOptions_OptionsChangedAsync() { - try - { - await base.PlayerExtraOptions_OptionsChangedAsync(); - await BroadcastPlayerExtraOptionsAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } - - await Task.CompletedTask; + await base.PlayerExtraOptions_OptionsChangedAsync(); + await BroadcastPlayerExtraOptionsAsync(); } protected override async Task BroadcastPlayerExtraOptionsAsync() @@ -1373,175 +1251,168 @@ protected override async Task OnGameOptionChangedAsync() /// private async Task ApplyGameOptionsAsync(string sender, string message) { - try - { - if (sender != hostName) - return; - - string[] parts = message.Split(';'); + if (sender != hostName) + return; - int checkBoxIntegerCount = (CheckBoxes.Count / 32) + 1; + string[] parts = message.Split(';'); - int partIndex = checkBoxIntegerCount + DropDowns.Count; + int checkBoxIntegerCount = (CheckBoxes.Count / 32) + 1; - if (parts.Length < partIndex + 6) - { - AddNotice(("The game host has sent an invalid game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); - return; - } + int partIndex = checkBoxIntegerCount + DropDowns.Count; - string mapOfficial = parts[partIndex]; - bool isMapOfficial = Conversions.BooleanFromString(mapOfficial, true); + if (parts.Length < partIndex + 6) + { + AddNotice(("The game host has sent an invalid game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); + return; + } - string mapSHA1 = parts[partIndex + 1]; + string mapOfficial = parts[partIndex]; + bool isMapOfficial = Conversions.BooleanFromString(mapOfficial, true); - string gameMode = parts[partIndex + 2]; + string mapSHA1 = parts[partIndex + 1]; - int frameSendRate = Conversions.IntFromString(parts[partIndex + 3], FrameSendRate); - if (frameSendRate != FrameSendRate) - { - FrameSendRate = frameSendRate; - AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); - } + string gameMode = parts[partIndex + 2]; - int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead); - if (maxAhead != MaxAhead) - { - MaxAhead = maxAhead; - AddNotice(string.Format("The game host has changed MaxAhead to {0}".L10N("Client:Main:HostChangeMaxAhead"), maxAhead)); - } + int frameSendRate = Conversions.IntFromString(parts[partIndex + 3], FrameSendRate); + if (frameSendRate != FrameSendRate) + { + FrameSendRate = frameSendRate; + AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); + } - int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion); - if (protocolVersion != ProtocolVersion) - { - ProtocolVersion = protocolVersion; - AddNotice(string.Format("The game host has changed ProtocolVersion to {0}".L10N("Client:Main:HostChangeProtocolVersion"), protocolVersion)); - } + int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead); + if (maxAhead != MaxAhead) + { + MaxAhead = maxAhead; + AddNotice(string.Format("The game host has changed MaxAhead to {0}".L10N("Client:Main:HostChangeMaxAhead"), maxAhead)); + } - string mapName = parts[partIndex + 8]; - GameModeMap currentGameModeMap = GameModeMap; + int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion); + if (protocolVersion != ProtocolVersion) + { + ProtocolVersion = protocolVersion; + AddNotice(string.Format("The game host has changed ProtocolVersion to {0}".L10N("Client:Main:HostChangeProtocolVersion"), protocolVersion)); + } - lastMapSHA1 = mapSHA1; - lastMapName = mapName; + string mapName = parts[partIndex + 8]; + GameModeMap currentGameModeMap = GameModeMap; - GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); - if (GameModeMap == null) - { - await ChangeMapAsync(null); + lastMapSHA1 = mapSHA1; + lastMapName = mapName; - if (!isMapOfficial) - await RequestMapAsync(); - else - await ShowOfficialMapMissingMessageAsync(mapSHA1); - } - else if (GameModeMap != currentGameModeMap) - { - await ChangeMapAsync(GameModeMap); - } + GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); + if (GameModeMap == null) + { + await ChangeMapAsync(null); - // By changing the game options after changing the map, we know which - // game options were changed by the map and which were changed by the game host + if (!isMapOfficial) + await RequestMapAsync(); + else + await ShowOfficialMapMissingMessageAsync(mapSHA1); + } + else if (GameModeMap != currentGameModeMap) + { + await ChangeMapAsync(GameModeMap); + } - // If the map doesn't exist on the local installation, it's impossible - // to know which options were set by the host and which were set by the - // map, so we'll just assume that the host has set all the options. - // Very few (if any) custom maps force options, so it'll be correct nearly always + // By changing the game options after changing the map, we know which + // game options were changed by the map and which were changed by the game host - for (int i = 0; i < checkBoxIntegerCount; i++) - { - if (parts.Length <= i) - return; + // If the map doesn't exist on the local installation, it's impossible + // to know which options were set by the host and which were set by the + // map, so we'll just assume that the host has set all the options. + // Very few (if any) custom maps force options, so it'll be correct nearly always - int checkBoxStatusInt; - bool success = int.TryParse(parts[i], out checkBoxStatusInt); + for (int i = 0; i < checkBoxIntegerCount; i++) + { + if (parts.Length <= i) + return; - if (!success) - { - AddNotice(("Failed to parse check box options sent by game host!" + - "The game host's game version might be different from yours.").L10N("Client:Main:HostCheckBoxParseError"), Color.Red); - return; - } + int checkBoxStatusInt; + bool success = int.TryParse(parts[i], out checkBoxStatusInt); - byte[] byteArray = BitConverter.GetBytes(checkBoxStatusInt); - bool[] boolArray = Conversions.BytesIntoBoolArray(byteArray); + if (!success) + { + AddNotice(("Failed to parse check box options sent by game host!" + + "The game host's game version might be different from yours.").L10N("Client:Main:HostCheckBoxParseError"), Color.Red); + return; + } - for (int optionIndex = 0; optionIndex < boolArray.Length; optionIndex++) - { - int gameOptionIndex = i * 32 + optionIndex; + byte[] byteArray = BitConverter.GetBytes(checkBoxStatusInt); + bool[] boolArray = Conversions.BytesIntoBoolArray(byteArray); - if (gameOptionIndex >= CheckBoxes.Count) - break; + for (int optionIndex = 0; optionIndex < boolArray.Length; optionIndex++) + { + int gameOptionIndex = i * 32 + optionIndex; - GameLobbyCheckBox checkBox = CheckBoxes[gameOptionIndex]; + if (gameOptionIndex >= CheckBoxes.Count) + break; - if (checkBox.Checked != boolArray[optionIndex]) - { - if (boolArray[optionIndex]) - AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), checkBox.Text)); - else - AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), checkBox.Text)); - } + GameLobbyCheckBox checkBox = CheckBoxes[gameOptionIndex]; - CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex]; + if (checkBox.Checked != boolArray[optionIndex]) + { + if (boolArray[optionIndex]) + AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), checkBox.Text)); + else + AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), checkBox.Text)); } + + CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex]; } + } - for (int i = checkBoxIntegerCount; i < DropDowns.Count + checkBoxIntegerCount; i++) + for (int i = checkBoxIntegerCount; i < DropDowns.Count + checkBoxIntegerCount; i++) + { + if (parts.Length <= i) { - if (parts.Length <= i) - { - AddNotice(("The game host has sent an invalid game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); - return; - } - - int ddSelectedIndex; - bool success = int.TryParse(parts[i], out ddSelectedIndex); + AddNotice(("The game host has sent an invalid game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); + return; + } - if (!success) - { - AddNotice(("Failed to parse drop down options sent by game host (2)! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalidTheSecondTime"), Color.Red); - return; - } + int ddSelectedIndex; + bool success = int.TryParse(parts[i], out ddSelectedIndex); - GameLobbyDropDown dd = DropDowns[i - checkBoxIntegerCount]; + if (!success) + { + AddNotice(("Failed to parse drop down options sent by game host (2)! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalidTheSecondTime"), Color.Red); + return; + } - if (ddSelectedIndex < -1 || ddSelectedIndex >= dd.Items.Count) - continue; + GameLobbyDropDown dd = DropDowns[i - checkBoxIntegerCount]; - if (dd.SelectedIndex != ddSelectedIndex) - { - string ddName = dd.OptionName; - if (dd.OptionName == null) - ddName = dd.Name; + if (ddSelectedIndex < -1 || ddSelectedIndex >= dd.Items.Count) + continue; - AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); - } + if (dd.SelectedIndex != ddSelectedIndex) + { + string ddName = dd.OptionName; + if (dd.OptionName == null) + ddName = dd.Name; - DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex; + AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); } - int randomSeed; - bool parseSuccess = int.TryParse(parts[partIndex + 6], out randomSeed); - - if (!parseSuccess) - { - AddNotice(("Failed to parse random seed from game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostRandomSeedError"), Color.Red); - } + DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex; + } - bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString(parts[partIndex + 7], - Convert.ToInt32(RemoveStartingLocations))); - SetRandomStartingLocations(removeStartingLocations); + int randomSeed; + bool parseSuccess = int.TryParse(parts[partIndex + 6], out randomSeed); - RandomSeed = randomSeed; - } - catch (Exception ex) + if (!parseSuccess) { - PreStartup.HandleException(ex); + AddNotice(("Failed to parse random seed from game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostRandomSeedError"), Color.Red); } + + bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString(parts[partIndex + 7], + Convert.ToInt32(RemoveStartingLocations))); + SetRandomStartingLocations(removeStartingLocations); + + RandomSeed = randomSeed; } private async Task RequestMapAsync() @@ -1588,29 +1459,21 @@ protected override Task ChangeMapAsync(GameModeMap gameModeMap) /// protected override async Task GameProcessExitedAsync() { - try - { - await base.GameProcessExitedAsync(); - - await channel.SendCTCPMessageAsync("RETURN", QueuedMessageType.SYSTEM_MESSAGE, 20); - ReturnNotification(ProgramConstants.PLAYERNAME); + await base.GameProcessExitedAsync(); + await channel.SendCTCPMessageAsync("RETURN", QueuedMessageType.SYSTEM_MESSAGE, 20); + ReturnNotification(ProgramConstants.PLAYERNAME); - if (IsHost) - { - RandomSeed = new Random().Next(); - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); - - if (Players.Count < playerLimit) - await UnlockGameAsync(true); - } - } - catch (Exception ex) + if (IsHost) { - PreStartup.HandleException(ex); + RandomSeed = new Random().Next(); + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerExtraOptionsAsync(); + + if (Players.Count < playerLimit) + await UnlockGameAsync(true); } } @@ -1619,59 +1482,51 @@ protected override async Task GameProcessExitedAsync() /// private async Task NonHostLaunchGameAsync(string sender, string message) { - try - { - if (tunnelHandler.CurrentTunnel.Version != Constants.TUNNEL_VERSION_2) - return; - - if (sender != hostName) - return; - - string[] parts = message.Split(';'); + if (tunnelHandler.CurrentTunnel.Version != Constants.TUNNEL_VERSION_2) + return; - if (parts.Length < 1) - return; + if (sender != hostName) + return; - UniqueGameID = Conversions.IntFromString(parts[0], -1); - if (UniqueGameID < 0) - return; + string[] parts = message.Split(';'); - var recentPlayers = new List(); + if (parts.Length < 1) + return; - for (int i = 1; i < parts.Length; i += 2) - { - if (parts.Length <= i + 1) - return; + UniqueGameID = Conversions.IntFromString(parts[0], -1); + if (UniqueGameID < 0) + return; - string pName = parts[i]; - string[] ipAndPort = parts[i + 1].Split(':'); + var recentPlayers = new List(); - if (ipAndPort.Length < 2) - return; + for (int i = 1; i < parts.Length; i += 2) + { + if (parts.Length <= i + 1) + return; - int port; - bool success = int.TryParse(ipAndPort[1], out port); + string pName = parts[i]; + string[] ipAndPort = parts[i + 1].Split(':'); - if (!success) - return; + if (ipAndPort.Length < 2) + return; - PlayerInfo pInfo = Players.Find(p => p.Name == pName); + int port; + bool success = int.TryParse(ipAndPort[1], out port); - if (pInfo == null) - return; + if (!success) + return; - pInfo.Port = port; - recentPlayers.Add(pName); - } - cncnetUserData.AddRecentPlayers(recentPlayers, channel.UIName); + PlayerInfo pInfo = Players.Find(p => p.Name == pName); - await StartGameAsync(); + if (pInfo == null) + return; + pInfo.Port = port; + recentPlayers.Add(pName); } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + + cncnetUserData.AddRecentPlayers(recentPlayers, channel.UIName); + await StartGameAsync(); } protected override async Task StartGameAsync() @@ -1736,141 +1591,78 @@ private void HandleIntNotification(string sender, int parameter, Action han protected override async Task GetReadyNotificationAsync() { - try - { - await base.GetReadyNotificationAsync(); + await base.GetReadyNotificationAsync(); #if WINFORMS WindowManager.FlashWindow(); #endif - TopBar.SwitchToPrimary(); + TopBar.SwitchToPrimary(); - if (IsHost) - await channel.SendCTCPMessageAsync("GETREADY", QueuedMessageType.GAME_GET_READY_MESSAGE, 0); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await channel.SendCTCPMessageAsync("GETREADY", QueuedMessageType.GAME_GET_READY_MESSAGE, 0); } protected override async Task AISpectatorsNotificationAsync() { - try - { - await base.AISpectatorsNotificationAsync(); + await base.AISpectatorsNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync("AISPECS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await channel.SendCTCPMessageAsync("AISPECS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task InsufficientPlayersNotificationAsync() { - try - { - await base.InsufficientPlayersNotificationAsync(); + await base.InsufficientPlayersNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync("INSFSPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await channel.SendCTCPMessageAsync("INSFSPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task TooManyPlayersNotificationAsync() { - try - { - await base.TooManyPlayersNotificationAsync(); + await base.TooManyPlayersNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync("TMPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await channel.SendCTCPMessageAsync("TMPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task SharedColorsNotificationAsync() { - try - { - await base.SharedColorsNotificationAsync(); + await base.SharedColorsNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync("CLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await channel.SendCTCPMessageAsync("CLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task SharedStartingLocationNotificationAsync() { - try - { - await base.SharedStartingLocationNotificationAsync(); + await base.SharedStartingLocationNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync("SLOC", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await channel.SendCTCPMessageAsync("SLOC", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task LockGameNotificationAsync() { - try - { - await base.LockGameNotificationAsync(); + await base.LockGameNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync("LCKGME", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await channel.SendCTCPMessageAsync("LCKGME", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task NotVerifiedNotificationAsync(int playerIndex) { - try - { - await base.NotVerifiedNotificationAsync(playerIndex); + await base.NotVerifiedNotificationAsync(playerIndex); - if (IsHost) - await channel.SendCTCPMessageAsync("NVRFY " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await channel.SendCTCPMessageAsync("NVRFY " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task StillInGameNotificationAsync(int playerIndex) { - try - { - await base.StillInGameNotificationAsync(playerIndex); + await base.StillInGameNotificationAsync(playerIndex); - if (IsHost) - await channel.SendCTCPMessageAsync("INGM " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (IsHost) + await channel.SendCTCPMessageAsync("INGM " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } private void ReturnNotification(string sender) @@ -1898,28 +1690,20 @@ private void HandleTunnelPing(string sender, int ping) private async Task FileHashNotificationAsync(string sender, string filesHash) { - try - { - if (!IsHost) - return; - - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + if (!IsHost) + return; - if (pInfo != null) - pInfo.Verified = true; + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - CopyPlayerDataToUI(); + if (pInfo != null) + pInfo.Verified = true; - if (filesHash != gameFilesHash) - { - await channel.SendCTCPMessageAsync("MM " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10); - CheaterNotification(ProgramConstants.PLAYERNAME, sender); - } + CopyPlayerDataToUI(); - } - catch (Exception ex) + if (filesHash != gameFilesHash) { - PreStartup.HandleException(ex); + await channel.SendCTCPMessageAsync("MM " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10); + CheaterNotification(ProgramConstants.PLAYERNAME, sender); } } @@ -2015,35 +1799,29 @@ private void HandleCheatDetectedMessage(string sender) => private async Task HandleTunnelServerChangeMessageAsync(string sender, string tunnelAddressAndPort) { - try - { - if (sender != hostName) - return; - - string[] split = tunnelAddressAndPort.Split(':'); - string tunnelAddress = split[0]; - int tunnelPort = int.Parse(split[1]); + if (sender != hostName) + return; - CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); - if (tunnel == null) - { - tunnelErrorMode = true; - AddNotice(("The game host has selected an invalid tunnel server! " + - "The game host needs to change the server or you will be unable " + - "to participate in the match.").L10N("Client:Main:HostInvalidTunnel"), - Color.Yellow); - UpdateLaunchGameButtonStatus(); - return; - } + string[] split = tunnelAddressAndPort.Split(':'); + string tunnelAddress = split[0]; + int tunnelPort = int.Parse(split[1]); - tunnelErrorMode = false; - await HandleTunnelServerChangeAsync(tunnel); - UpdateLaunchGameButtonStatus(); - } - catch (Exception ex) + CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); + if (tunnel == null) { - PreStartup.HandleException(ex); + tunnelErrorMode = true; + AddNotice(("The game host has selected an invalid tunnel server! " + + "The game host needs to change the server or you will be unable " + + "to participate in the match.").L10N("Client:Main:HostInvalidTunnel"), + Color.Yellow); + UpdateLaunchGameButtonStatus(); + return; } + + + tunnelErrorMode = false; + await HandleTunnelServerChangeAsync(tunnel); + UpdateLaunchGameButtonStatus(); } /// @@ -2065,126 +1843,86 @@ protected override bool UpdateLaunchGameButtonStatus() #region CnCNet map sharing - private void MapSharer_MapDownloadFailed(object sender, SHA1EventArgs e) - => WindowManager.AddCallback(MapSharer_HandleMapDownloadFailedAsync, e); - private async Task MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) { - try + // If the host has already uploaded the map, we shouldn't request them to re-upload it + if (hostUploadedMaps.Contains(e.SHA1)) { - // If the host has already uploaded the map, we shouldn't request them to re-upload it - if (hostUploadedMaps.Contains(e.SHA1)) - { - AddNotice("Download of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:DownloadCustomMapFailed")); - mapSharingConfirmationPanel.SetFailedStatus(); + AddNotice("Download of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:DownloadCustomMapFailed")); + mapSharingConfirmationPanel.SetFailedStatus(); - await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - return; - } - - if (chatCommandDownloadedMaps.Contains(e.SHA1)) - { - // Notify the user that their chat command map download failed. - // Do not notify other users with a CTCP message as this is irrelevant to them. - AddNotice("Downloading map via chat command has failed. Check the map ID and try again.".L10N("Client:Main:DownloadMapCommandFailedGeneric")); - mapSharingConfirmationPanel.SetFailedStatus(); - return; - } - - AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("Client:Main:RequestHostUploadMapToDB")); - - await channel.SendCTCPMessageAsync(MAP_SHARING_UPLOAD_REQUEST + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + return; } - catch (Exception ex) + + if (chatCommandDownloadedMaps.Contains(e.SHA1)) { - PreStartup.HandleException(ex); + // Notify the user that their chat command map download failed. + // Do not notify other users with a CTCP message as this is irrelevant to them. + AddNotice("Downloading map via chat command has failed. Check the map ID and try again.".L10N("Client:Main:DownloadMapCommandFailedGeneric")); + mapSharingConfirmationPanel.SetFailedStatus(); + return; } - } - private void MapSharer_MapDownloadComplete(object sender, SHA1EventArgs e) => - WindowManager.AddCallback(MapSharer_HandleMapDownloadCompleteAsync, e); + AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("Client:Main:RequestHostUploadMapToDB")); + + await channel.SendCTCPMessageAsync(MAP_SHARING_UPLOAD_REQUEST + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } private async Task MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) { - try + string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName); + Logger.Log("Map " + mapFileName + " downloaded, parsing."); + string mapPath = "Maps/Custom/" + mapFileName; + Map map = MapLoader.LoadCustomMap(mapPath, out string returnMessage); + if (map != null) { - string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName); - Logger.Log("Map " + mapFileName + " downloaded, parsing."); - string mapPath = "Maps/Custom/" + mapFileName; - Map map = MapLoader.LoadCustomMap(mapPath, out string returnMessage); - if (map != null) - { - AddNotice(returnMessage); - if (lastMapSHA1 == e.SHA1) - { - GameModeMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == lastMapSHA1); - await ChangeMapAsync(GameModeMap); - } - } - else if (chatCommandDownloadedMaps.Contains(e.SHA1)) - { - // Somehow the user has managed to download an already existing sha1 hash. - // This special case prevents user confusion from the file successfully downloading but showing an error anyway. - AddNotice(returnMessage, Color.Yellow); - AddNotice("Map was downloaded, but a duplicate is already loaded from a different filename. This may cause strange behavior.".L10N("Client:Main:DownloadMapCommandDuplicateMapFileLoaded"), - Color.Yellow); - } - else + AddNotice(returnMessage); + if (lastMapSHA1 == e.SHA1) { - AddNotice(returnMessage, Color.Red); - AddNotice("Transfer of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:MapTransferFailed")); - mapSharingConfirmationPanel.SetFailedStatus(); - await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + GameModeMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == lastMapSHA1); + await ChangeMapAsync(GameModeMap); } } - catch (Exception ex) + else if (chatCommandDownloadedMaps.Contains(e.SHA1)) + { + // Somehow the user has managed to download an already existing sha1 hash. + // This special case prevents user confusion from the file successfully downloading but showing an error anyway. + AddNotice(returnMessage, Color.Yellow); + AddNotice("Map was downloaded, but a duplicate is already loaded from a different filename. This may cause strange behavior.".L10N("Client:Main:DownloadMapCommandDuplicateMapFileLoaded"), + Color.Yellow); + } + else { - PreStartup.HandleException(ex); + AddNotice(returnMessage, Color.Red); + AddNotice("Transfer of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:MapTransferFailed")); + mapSharingConfirmationPanel.SetFailedStatus(); + await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } } - private void MapSharer_MapUploadFailed(object sender, MapEventArgs e) => - WindowManager.AddCallback(MapSharer_HandleMapUploadFailedAsync, e); - private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) { - try - { - Map map = e.Map; + Map map = e.Map; - hostUploadedMaps.Add(map.SHA1); + hostUploadedMaps.Add(map.SHA1); - AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("Client:Main:UpdateMapToDBFailed"), map.Name)); - if (map == Map) - { - AddNotice("You need to change the map or some players won't be able to participate in this match.".L10N("Client:Main:YouMustReplaceMap")); - await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - } - } - catch (Exception ex) + AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("Client:Main:UpdateMapToDBFailed"), map.Name)); + if (map == Map) { - PreStartup.HandleException(ex); + AddNotice("You need to change the map or some players won't be able to participate in this match.".L10N("Client:Main:YouMustReplaceMap")); + await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } } - private void MapSharer_MapUploadComplete(object sender, MapEventArgs e) => - WindowManager.AddCallback(MapSharer_HandleMapUploadCompleteAsync, e); - private async Task MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) { - try - { - hostUploadedMaps.Add(e.Map.SHA1); + hostUploadedMaps.Add(e.Map.SHA1); - AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); - if (e.Map == Map) - { - await channel.SendCTCPMessageAsync(MAP_SHARING_DOWNLOAD_REQUEST + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - } - } - catch (Exception ex) + AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); + if (e.Map == Map) { - PreStartup.HandleException(ex); + await channel.SendCTCPMessageAsync(MAP_SHARING_DOWNLOAD_REQUEST + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } } @@ -2328,8 +2066,8 @@ private void DownloadMapByIdCommand(string parameters) else { // User supplied a map name. - sha1 = parameters.Substring(0, firstSpaceIndex); - mapName = parameters.Substring(firstSpaceIndex + 1); + sha1 = parameters[..firstSpaceIndex]; + mapName = parameters[(firstSpaceIndex + 1)..]; mapName = mapName.Trim(); } @@ -2378,61 +2116,54 @@ private void AccelerateGameBroadcasting() => private async Task BroadcastGameAsync() { - try - { - Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); - - if (broadcastChannel == null) - return; - - if (ProgramConstants.IsInGame && broadcastChannel.Users.Count > 500) - return; + Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); - if (GameMode == null || Map == null) - return; + if (broadcastChannel == null) + return; - StringBuilder sb = new StringBuilder("GAME "); - sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); - sb.Append(";"); - sb.Append(ProgramConstants.GAME_VERSION); - sb.Append(";"); - sb.Append(playerLimit); - sb.Append(";"); - sb.Append(channel.ChannelName); - sb.Append(";"); - sb.Append(channel.UIName); - sb.Append(";"); - if (Locked) - sb.Append("1"); - else - sb.Append("0"); - sb.Append(Convert.ToInt32(isCustomPassword)); - sb.Append(Convert.ToInt32(closed)); - sb.Append("0"); // IsLoadedGame - sb.Append("0"); // IsLadder - sb.Append(";"); - foreach (PlayerInfo pInfo in Players) - { - sb.Append(pInfo.Name); - sb.Append(","); - } + if (ProgramConstants.IsInGame && broadcastChannel.Users.Count > 500) + return; - sb.Remove(sb.Length - 1, 1); - sb.Append(";"); - sb.Append(Map.UntranslatedName); - sb.Append(";"); - sb.Append(GameMode.UntranslatedUIName); - sb.Append(";"); - sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); - sb.Append(";"); - sb.Append(0); // LoadedGameId + if (GameMode == null || Map == null) + return; - await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); - } - catch (Exception ex) + StringBuilder sb = new StringBuilder("GAME "); + sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); + sb.Append(";"); + sb.Append(ProgramConstants.GAME_VERSION); + sb.Append(";"); + sb.Append(playerLimit); + sb.Append(";"); + sb.Append(channel.ChannelName); + sb.Append(";"); + sb.Append(channel.UIName); + sb.Append(";"); + if (Locked) + sb.Append("1"); + else + sb.Append("0"); + sb.Append(Convert.ToInt32(isCustomPassword)); + sb.Append(Convert.ToInt32(closed)); + sb.Append("0"); // IsLoadedGame + sb.Append("0"); // IsLadder + sb.Append(";"); + foreach (PlayerInfo pInfo in Players) { - PreStartup.HandleException(ex); + sb.Append(pInfo.Name); + sb.Append(","); } + + sb.Remove(sb.Length - 1, 1); + sb.Append(";"); + sb.Append(Map.UntranslatedName); + sb.Append(";"); + sb.Append(GameMode.UntranslatedUIName); + sb.Append(";"); + sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); + sb.Append(";"); + sb.Append(0); // LoadedGameId + + await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); } #endregion diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntCommandHandler.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntCommandHandler.cs index be6e38fc4..736be0d30 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntCommandHandler.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntCommandHandler.cs @@ -19,7 +19,7 @@ public override bool Handle(string sender, string message) if (message.StartsWith(CommandName)) { int value; - bool success = int.TryParse(message.Substring(CommandName.Length + 1), out value); + bool success = int.TryParse(message[(CommandName.Length + 1)..], out value); if (success) { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntNotificationHandler.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntNotificationHandler.cs index 5815b9375..a96b919b6 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntNotificationHandler.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/IntNotificationHandler.cs @@ -18,7 +18,7 @@ public override bool Handle(string sender, string message) { if (message.StartsWith(CommandName)) { - string intPart = message.Substring(CommandName.Length + 1); + string intPart = message[(CommandName.Length + 1)..]; int value; bool success = int.TryParse(intPart, out value); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/StringCommandHandler.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/StringCommandHandler.cs index b59f16828..2b7d92610 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/StringCommandHandler.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CommandHandlers/StringCommandHandler.cs @@ -18,7 +18,7 @@ public override bool Handle(string sender, string message) if (message.StartsWith(CommandName + " ")) { - string parameters = message.Substring(CommandName.Length + 1); + string parameters = message[(CommandName.Length + 1)..]; commandHandler.Invoke(sender, parameters); //commandHandler(sender, message.Substring(CommandName.Length + 1)); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 56c48a483..f0fd40013 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -206,10 +206,10 @@ public override void Initialize() PlayerOptionsPanel = FindChild(nameof(PlayerOptionsPanel)); btnLeaveGame = FindChild(nameof(btnLeaveGame)); - btnLeaveGame.LeftClick += (_, _) => BtnLeaveGame_LeftClickAsync(); + btnLeaveGame.LeftClick += (_, _) => BtnLeaveGame_LeftClickAsync().HandleTask(); btnLaunchGame = FindChild(nameof(btnLaunchGame)); - btnLaunchGame.LeftClick += (_, _) => BtnLaunchGame_LeftClickAsync(); + btnLaunchGame.LeftClick += (_, _) => BtnLaunchGame_LeftClickAsync().HandleTask(); btnLaunchGame.InitStarDisplay(RankTextures); MapPreviewBox = FindChild("MapPreviewBox"); @@ -251,8 +251,7 @@ public override void Initialize() XNAPanel rankHeader = new XNAPanel(WindowManager); rankHeader.BackgroundTexture = AssetLoader.LoadTexture("rank.png"); - rankHeader.ClientRectangle = new Rectangle(0, 0, rankHeader.BackgroundTexture.Width, - 19); + rankHeader.ClientRectangle = new Rectangle(0, 0, rankHeader.BackgroundTexture.Width, 19); XNAListBox rankListBox = new XNAListBox(WindowManager); rankListBox.TextBorderDistance = 2; @@ -261,7 +260,7 @@ public override void Initialize() lbGameModeMapList.AddColumn("MAP NAME".L10N("Client:Main:MapNameHeader"), lbGameModeMapList.Width - RankTextures[1].Width - 3); ddGameModeMapFilter = FindChild("ddGameMode"); // ddGameMode for backwards compatibility - ddGameModeMapFilter.SelectedIndexChanged += (_, _) => DdGameModeMapFilter_SelectedIndexChangedAsync(); + ddGameModeMapFilter.SelectedIndexChanged += (_, _) => DdGameModeMapFilter_SelectedIndexChangedAsync().HandleTask(); ddGameModeMapFilter.AddItem(CreateGameFilterItem(FavoriteMapsLabel, new GameModeMapFilter(GetFavoriteGameModeMaps))); foreach (GameMode gm in GameModeMaps.GameModes) @@ -275,12 +274,12 @@ public override void Initialize() tbMapSearch.InputReceived += TbMapSearch_InputReceived; btnPickRandomMap = FindChild(nameof(btnPickRandomMap)); - btnPickRandomMap.LeftClick += (_, _) => BtnPickRandomMap_LeftClickAsync(); + btnPickRandomMap.LeftClick += (_, _) => PickRandomMapAsync().HandleTask(); - CheckBoxes.ForEach(chk => chk.CheckedChanged += (sender, _) => ChkBox_CheckedChangedAsync(sender)); - DropDowns.ForEach(dd => dd.SelectedIndexChanged += (sender, _) => Dropdown_SelectedIndexChangedAsync(sender)); + CheckBoxes.ForEach(chk => chk.CheckedChanged += (sender, _) => ChkBox_CheckedChangedAsync(sender).HandleTask()); + DropDowns.ForEach(dd => dd.SelectedIndexChanged += (sender, _) => Dropdown_SelectedIndexChangedAsync(sender).HandleTask()); - lbGameModeMapList_SelectedIndexChangedFunc = (_, _) => LbGameModeMapList_SelectedIndexChangedAsync(); + lbGameModeMapList_SelectedIndexChangedFunc = (_, _) => LbGameModeMapList_SelectedIndexChangedAsync().HandleTask(); InitializeGameOptionPresetUI(); } @@ -299,8 +298,7 @@ private void InitBtnMapSort() btnMapSortAlphabetically.Name = nameof(btnMapSortAlphabetically); btnMapSortAlphabetically.ClientRectangle = new Rectangle( ddGameModeMapFilter.X + -ddGameModeMapFilter.Height - 4, ddGameModeMapFilter.Y, - ddGameModeMapFilter.Height, ddGameModeMapFilter.Height - ); + ddGameModeMapFilter.Height, ddGameModeMapFilter.Height); btnMapSortAlphabetically.LeftClick += BtnMapSortAlphabetically_LeftClick; btnMapSortAlphabetically.SetToolTipText("Sort Maps Alphabetically".L10N("Client:Main:MapSortAlphabeticallyToolTip")); RefreshMapSortAlphabeticallyBtn(); @@ -399,63 +397,40 @@ protected void HandleGameOptionPresetSaveCommand(string presetName) AddNotice(error); } - protected void HandleGameOptionPresetLoadCommand(GameOptionPresetEventArgs e) => HandleGameOptionPresetLoadCommandAsync(e.PresetName); + protected void HandleGameOptionPresetLoadCommand(GameOptionPresetEventArgs e) => HandleGameOptionPresetLoadCommandAsync(e.PresetName).HandleTask(); protected async Task HandleGameOptionPresetLoadCommandAsync(string presetName) { - try - { - if (await LoadGameOptionPresetAsync(presetName)) - AddNotice("Game option preset loaded succesfully.".L10N("Client:Main:PresetLoaded")); - else - AddNotice(string.Format("Preset {0} not found!".L10N("Client:Main:PresetNotFound"), presetName)); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (await LoadGameOptionPresetAsync(presetName)) + AddNotice("Game option preset loaded succesfully.".L10N("Client:Main:PresetLoaded")); + else + AddNotice(string.Format("Preset {0} not found!".L10N("Client:Main:PresetNotFound"), presetName)); } protected void AddNotice(string message) => AddNotice(message, Color.White); protected abstract void AddNotice(string message, Color color); - private Task BtnPickRandomMap_LeftClickAsync() => PickRandomMapAsync(); - private void TbMapSearch_InputReceived(object sender, EventArgs e) => ListMaps(); private async Task Dropdown_SelectedIndexChangedAsync(object sender) { - try - { - if (disableGameOptionUpdateBroadcast) - return; + if (disableGameOptionUpdateBroadcast) + return; - var dd = (GameLobbyDropDown)sender; - dd.HostSelectedIndex = dd.SelectedIndex; - await OnGameOptionChangedAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + var dd = (GameLobbyDropDown)sender; + dd.HostSelectedIndex = dd.SelectedIndex; + await OnGameOptionChangedAsync(); } private async Task ChkBox_CheckedChangedAsync(object sender) { - try - { - if (disableGameOptionUpdateBroadcast) - return; + if (disableGameOptionUpdateBroadcast) + return; - var checkBox = (GameLobbyCheckBox)sender; - checkBox.HostChecked = checkBox.Checked; - await OnGameOptionChangedAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + var checkBox = (GameLobbyCheckBox)sender; + checkBox.HostChecked = checkBox.Checked; + await OnGameOptionChangedAsync(); } protected virtual Task OnGameOptionChangedAsync() @@ -469,27 +444,20 @@ protected virtual Task OnGameOptionChangedAsync() protected async Task DdGameModeMapFilter_SelectedIndexChangedAsync() { - try - { - gameModeMapFilter = ddGameModeMapFilter.SelectedItem.Tag as GameModeMapFilter; + gameModeMapFilter = ddGameModeMapFilter.SelectedItem.Tag as GameModeMapFilter; - tbMapSearch.Text = string.Empty; - tbMapSearch.OnSelectedChanged(); + tbMapSearch.Text = string.Empty; + tbMapSearch.OnSelectedChanged(); - ListMaps(); + ListMaps(); - if (lbGameModeMapList.SelectedIndex == -1) - lbGameModeMapList.SelectedIndex = 0; // Select default GameModeMap - else - await ChangeMapAsync(GameModeMap); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (lbGameModeMapList.SelectedIndex == -1) + lbGameModeMapList.SelectedIndex = 0; // Select default GameModeMap + else + await ChangeMapAsync(GameModeMap); } - protected void BtnPlayerExtraOptions_LeftClick(object sender, EventArgs e) + private void BtnPlayerExtraOptions_LeftClick(object sender, EventArgs e) { if (PlayerExtraOptionsPanel.Enabled) PlayerExtraOptionsPanel.Disable(); @@ -649,7 +617,7 @@ private void DeleteMapConfirmation() var messageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, "Delete Confirmation".L10N("Client:Main:DeleteMapConfirmTitle"), string.Format("Are you sure you wish to delete the custom map {0}?".L10N("Client:Main:DeleteMapConfirmText"), Map.Name)); - messageBox.YesClickedAction = _ => DeleteSelectedMapAsync(); + messageBox.YesClickedAction = _ => DeleteSelectedMapAsync().HandleTask(); } private void MapPreviewBox_ToggleFavorite(object sender, EventArgs e) => @@ -701,33 +669,21 @@ private async Task DeleteSelectedMapAsync() XNAMessageBox.Show(WindowManager, "Deleting Map Failed".L10N("Client:Main:DeleteMapFailedTitle"), "Deleting map failed! Reason:".L10N("Client:Main:DeleteMapFailedText") + " " + ex.Message); } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } } private async Task LbGameModeMapList_SelectedIndexChangedAsync() { - try + if (lbGameModeMapList.SelectedIndex < 0 || lbGameModeMapList.SelectedIndex >= lbGameModeMapList.ItemCount) { - if (lbGameModeMapList.SelectedIndex < 0 || lbGameModeMapList.SelectedIndex >= lbGameModeMapList.ItemCount) - { - await ChangeMapAsync(GameModeMap); - return; - } - - XNAListBoxItem item = lbGameModeMapList.GetItem(1, lbGameModeMapList.SelectedIndex); + await ChangeMapAsync(GameModeMap); + return; + } - GameModeMap = (GameModeMap)item.Tag; + XNAListBoxItem item = lbGameModeMapList.GetItem(1, lbGameModeMapList.SelectedIndex); - await ChangeMapAsync(GameModeMap); + GameModeMap = (GameModeMap)item.Tag; - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await ChangeMapAsync(GameModeMap); } private void LbGameModeMapList_HoveredIndexChanged(object sender, EventArgs e) @@ -748,28 +704,21 @@ private void LbGameModeMapList_HoveredIndexChanged(object sender, EventArgs e) private async Task PickRandomMapAsync() { - try - { - int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) - + AIPlayers.Count; - List maps = GetMapList(totalPlayerCount); - if (maps.Count < 1) - return; + int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + + AIPlayers.Count; + List maps = GetMapList(totalPlayerCount); + if (maps.Count < 1) + return; - int random = new Random().Next(0, maps.Count); - GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode == GameMode && gmm.Map == maps[random]); + int random = new Random().Next(0, maps.Count); + GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode == GameMode && gmm.Map == maps[random]); - Logger.Log("PickRandomMap: Rolled " + random + " out of " + maps.Count + ". Picked map: " + Map.Name); + Logger.Log("PickRandomMap: Rolled " + random + " out of " + maps.Count + ". Picked map: " + Map.Name); - await ChangeMapAsync(GameModeMap); - tbMapSearch.Text = string.Empty; - tbMapSearch.OnSelectedChanged(); - ListMaps(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await ChangeMapAsync(GameModeMap); + tbMapSearch.Text = string.Empty; + tbMapSearch.OnSelectedChanged(); + ListMaps(); } private List GetMapList(int playerCount) @@ -833,9 +782,6 @@ protected void InitPlayerOptionDropdowns() int teamWidth = ConfigIni.GetIntValue(Name, "TeamWidth", 46); int locationX = ConfigIni.GetIntValue(Name, "PlayerOptionLocationX", 25); int locationY = ConfigIni.GetIntValue(Name, "PlayerOptionLocationY", 24); - - // InitPlayerOptionDropdowns(136, 91, 79, 49, 46, new Point(25, 24)); - string[] sides = ClientConfiguration.Instance.Sides.Split(',').ToArray(); SideCount = sides.Length; @@ -856,7 +802,7 @@ protected void InitPlayerOptionDropdowns() ddPlayerName.AddItem(String.Empty); ProgramConstants.AI_PLAYER_NAMES.ForEach(ddPlayerName.AddItem); ddPlayerName.AllowDropDown = true; - ddPlayerName.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); + ddPlayerName.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender).HandleTask(); ddPlayerName.RightClick += MultiplayerName_RightClick; ddPlayerName.Tag = true; @@ -875,7 +821,7 @@ protected void InitPlayerOptionDropdowns() AddSideToDropDown(ddPlayerSide, sideName); ddPlayerSide.AllowDropDown = false; - ddPlayerSide.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); + ddPlayerSide.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender).HandleTask(); ddPlayerSide.Tag = true; var ddPlayerColor = new XNAClientDropDown(WindowManager); @@ -887,7 +833,7 @@ protected void InitPlayerOptionDropdowns() foreach (MultiplayerColor mpColor in MPColors) ddPlayerColor.AddItem(mpColor.Name, mpColor.XnaColor); ddPlayerColor.AllowDropDown = false; - ddPlayerColor.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); + ddPlayerColor.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender).HandleTask(); ddPlayerColor.Tag = false; var ddPlayerTeam = new XNAClientDropDown(WindowManager); @@ -898,7 +844,7 @@ protected void InitPlayerOptionDropdowns() ddPlayerTeam.AddItem("-"); ProgramConstants.TEAMS.ForEach(ddPlayerTeam.AddItem); ddPlayerTeam.AllowDropDown = false; - ddPlayerTeam.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); + ddPlayerTeam.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender).HandleTask(); ddPlayerTeam.Tag = true; var ddPlayerStart = new XNAClientDropDown(WindowManager); @@ -909,7 +855,7 @@ protected void InitPlayerOptionDropdowns() for (int j = 1; j < 9; j++) ddPlayerStart.AddItem(j.ToString()); ddPlayerStart.AllowDropDown = false; - ddPlayerStart.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); + ddPlayerStart.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender).HandleTask(); ddPlayerStart.Visible = false; ddPlayerStart.Enabled = false; ddPlayerStart.Tag = true; @@ -953,7 +899,7 @@ protected void InitPlayerOptionDropdowns() { PlayerExtraOptionsPanel = FindChild(nameof(PlayerExtraOptionsPanel)); PlayerExtraOptionsPanel.Disable(); - PlayerExtraOptionsPanel.OptionsChanged += (_, _) => PlayerExtraOptions_OptionsChangedAsync(); + PlayerExtraOptionsPanel.OptionsChanged += (_, _) => PlayerExtraOptions_OptionsChangedAsync().HandleTask(); btnPlayerExtraOptionsOpen.LeftClick += BtnPlayerExtraOptions_LeftClick; } @@ -974,29 +920,22 @@ private XNALabel GeneratePlayerOptionCaption(string name, string text, int x, in protected virtual Task PlayerExtraOptions_OptionsChangedAsync() { - try - { - var playerExtraOptions = GetPlayerExtraOptions(); + var playerExtraOptions = GetPlayerExtraOptions(); - for (int i = 0; i < ddPlayerSides.Length; i++) - EnablePlayerOptionDropDown(ddPlayerSides[i], i, !playerExtraOptions.IsForceRandomSides); + for (int i = 0; i < ddPlayerSides.Length; i++) + EnablePlayerOptionDropDown(ddPlayerSides[i], i, !playerExtraOptions.IsForceRandomSides); - for (int i = 0; i < ddPlayerTeams.Length; i++) - EnablePlayerOptionDropDown(ddPlayerTeams[i], i, !playerExtraOptions.IsForceRandomTeams); + for (int i = 0; i < ddPlayerTeams.Length; i++) + EnablePlayerOptionDropDown(ddPlayerTeams[i], i, !playerExtraOptions.IsForceRandomTeams); - for (int i = 0; i < ddPlayerColors.Length; i++) - EnablePlayerOptionDropDown(ddPlayerColors[i], i, !playerExtraOptions.IsForceRandomColors); + for (int i = 0; i < ddPlayerColors.Length; i++) + EnablePlayerOptionDropDown(ddPlayerColors[i], i, !playerExtraOptions.IsForceRandomColors); - for (int i = 0; i < ddPlayerStarts.Length; i++) - EnablePlayerOptionDropDown(ddPlayerStarts[i], i, !playerExtraOptions.IsForceRandomStarts); + for (int i = 0; i < ddPlayerStarts.Length; i++) + EnablePlayerOptionDropDown(ddPlayerStarts[i], i, !playerExtraOptions.IsForceRandomStarts); - UpdateMapPreviewBoxEnabledStatus(); - RefreshBtnPlayerExtraOptionsOpenTexture(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + UpdateMapPreviewBoxEnabledStatus(); + RefreshBtnPlayerExtraOptionsOpenTexture(); return Task.CompletedTask; } @@ -1847,33 +1786,19 @@ protected virtual Task StartGameAsync() return Task.CompletedTask; } - private void GameProcessExited_Callback() => AddCallback(GameProcessExitedAsync); + private void GameProcessExited_Callback() => AddCallback(() => GameProcessExitedAsync().HandleTask()); protected virtual Task GameProcessExitedAsync() { - try - { - GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; - - Logger.Log("GameProcessExited: Parsing statistics."); - - matchStatistics.ParseStatistics(ProgramConstants.GamePath, ClientConfiguration.Instance.LocalGame, false); - - Logger.Log("GameProcessExited: Adding match to statistics."); - - StatisticsManager.Instance.AddMatchAndSaveDatabase(true, matchStatistics); - - ClearReadyStatuses(); - - CopyPlayerDataToUI(); + GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; - UpdateDiscordPresence(true); - - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + Logger.Log("GameProcessExited: Parsing statistics."); + matchStatistics.ParseStatistics(ProgramConstants.GamePath, ClientConfiguration.Instance.LocalGame, false); + Logger.Log("GameProcessExited: Adding match to statistics."); + StatisticsManager.Instance.AddMatchAndSaveDatabase(true, matchStatistics); + ClearReadyStatuses(); + CopyPlayerDataToUI(); + UpdateDiscordPresence(true); return Task.CompletedTask; } @@ -1884,80 +1809,73 @@ protected virtual Task GameProcessExitedAsync() /// protected virtual async Task CopyPlayerDataFromUIAsync(object sender) { - try - { - if (PlayerUpdatingInProgress) - return; + if (PlayerUpdatingInProgress) + return; - var senderDropDown = (XNADropDown)sender; - if ((bool)senderDropDown.Tag) - ClearReadyStatuses(); + var senderDropDown = (XNADropDown)sender; + if ((bool)senderDropDown.Tag) + ClearReadyStatuses(); - var oldSideId = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId; + var oldSideId = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId; - for (int pId = 0; pId < Players.Count; pId++) - { - PlayerInfo pInfo = Players[pId]; - - pInfo.ColorId = ddPlayerColors[pId].SelectedIndex; - pInfo.SideId = ddPlayerSides[pId].SelectedIndex; - pInfo.StartingLocation = ddPlayerStarts[pId].SelectedIndex; - pInfo.TeamId = ddPlayerTeams[pId].SelectedIndex; + for (int pId = 0; pId < Players.Count; pId++) + { + PlayerInfo pInfo = Players[pId]; - if (pInfo.SideId == SideCount + RandomSelectorCount) - pInfo.StartingLocation = 0; + pInfo.ColorId = ddPlayerColors[pId].SelectedIndex; + pInfo.SideId = ddPlayerSides[pId].SelectedIndex; + pInfo.StartingLocation = ddPlayerStarts[pId].SelectedIndex; + pInfo.TeamId = ddPlayerTeams[pId].SelectedIndex; - XNADropDown ddName = ddPlayerNames[pId]; + if (pInfo.SideId == SideCount + RandomSelectorCount) + pInfo.StartingLocation = 0; - switch (ddName.SelectedIndex) - { - case 0: - break; - case 1: - ddName.SelectedIndex = 0; - break; - case 2: - await KickPlayerAsync(pId); - break; - case 3: - await BanPlayerAsync(pId); - break; - } - } + XNADropDown ddName = ddPlayerNames[pId]; - AIPlayers.Clear(); - for (int cmbId = Players.Count; cmbId < 8; cmbId++) + switch (ddName.SelectedIndex) { - XNADropDown dd = ddPlayerNames[cmbId]; - dd.Items[0].Text = "-"; + case 0: + break; + case 1: + ddName.SelectedIndex = 0; + break; + case 2: + await KickPlayerAsync(pId); + break; + case 3: + await BanPlayerAsync(pId); + break; + } + } - if (dd.SelectedIndex < 1) - continue; + AIPlayers.Clear(); + for (int cmbId = Players.Count; cmbId < 8; cmbId++) + { + XNADropDown dd = ddPlayerNames[cmbId]; + dd.Items[0].Text = "-"; - PlayerInfo aiPlayer = new PlayerInfo - { - Name = dd.Items[dd.SelectedIndex].Text, - AILevel = dd.SelectedIndex - 1, - SideId = Math.Max(ddPlayerSides[cmbId].SelectedIndex, 0), - ColorId = Math.Max(ddPlayerColors[cmbId].SelectedIndex, 0), - StartingLocation = Math.Max(ddPlayerStarts[cmbId].SelectedIndex, 0), - TeamId = Map != null && Map.IsCoop ? 1 : Math.Max(ddPlayerTeams[cmbId].SelectedIndex, 0), - IsAI = true - }; - - AIPlayers.Add(aiPlayer); - } + if (dd.SelectedIndex < 1) + continue; - CopyPlayerDataToUI(); - btnLaunchGame.SetRank(GetRank()); + PlayerInfo aiPlayer = new PlayerInfo + { + Name = dd.Items[dd.SelectedIndex].Text, + AILevel = dd.SelectedIndex - 1, + SideId = Math.Max(ddPlayerSides[cmbId].SelectedIndex, 0), + ColorId = Math.Max(ddPlayerColors[cmbId].SelectedIndex, 0), + StartingLocation = Math.Max(ddPlayerStarts[cmbId].SelectedIndex, 0), + TeamId = Map != null && Map.IsCoop ? 1 : Math.Max(ddPlayerTeams[cmbId].SelectedIndex, 0), + IsAI = true + }; - if (oldSideId != Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId) - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + AIPlayers.Add(aiPlayer); } + + CopyPlayerDataToUI(); + btnLaunchGame.SetRank(GetRank()); + + if (oldSideId != Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId) + UpdateDiscordPresence(); } /// diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index 421acd33d..a07cfc1a1 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -43,55 +43,53 @@ internal sealed class LANGameLobby : MultiplayerGameLobby private const string DICE_ROLL_COMMAND = "DR"; private const string PING = "PING"; - public LANGameLobby(WindowManager windowManager, string iniName, - TopBar topBar, LANColor[] chatColors, MapLoader mapLoader, DiscordHandler discordHandler) : - base(windowManager, iniName, topBar, mapLoader, discordHandler) + public LANGameLobby( + WindowManager windowManager, + string iniName, + TopBar topBar, + LANColor[] chatColors, + MapLoader mapLoader, + DiscordHandler discordHandler) + : base(windowManager, iniName, topBar, mapLoader, discordHandler) { this.chatColors = chatColors; encoding = Encoding.UTF8; hostCommandHandlers = new CommandHandlerBase[] { - new StringCommandHandler(CHAT_COMMAND, (sender, data) => GameHost_HandleChatCommandAsync(sender, data)), - new NoParamCommandHandler(RETURN_COMMAND, sender => GameHost_HandleReturnCommandAsync(sender)), - new StringCommandHandler(PLAYER_OPTIONS_REQUEST_COMMAND, (sender, data) => HandlePlayerOptionsRequestAsync(sender, data)), - new NoParamCommandHandler(PLAYER_QUIT_COMMAND, sender => HandlePlayerQuitAsync(sender)), - new StringCommandHandler(PLAYER_READY_REQUEST, (sender, autoReady) => GameHost_HandleReadyRequestAsync(sender, autoReady)), + new StringCommandHandler(CHAT_COMMAND, (sender, data) => GameHost_HandleChatCommandAsync(sender, data).HandleTask()), + new NoParamCommandHandler(RETURN_COMMAND, sender => GameHost_HandleReturnCommandAsync(sender).HandleTask()), + new StringCommandHandler(PLAYER_OPTIONS_REQUEST_COMMAND, (sender, data) => HandlePlayerOptionsRequestAsync(sender, data).HandleTask()), + new NoParamCommandHandler(PLAYER_QUIT_COMMAND, sender => HandlePlayerQuitAsync(sender).HandleTask()), + new StringCommandHandler(PLAYER_READY_REQUEST, (sender, autoReady) => GameHost_HandleReadyRequestAsync(sender, autoReady).HandleTask()), new StringCommandHandler(FILE_HASH_COMMAND, HandleFileHashCommand), - new StringCommandHandler(DICE_ROLL_COMMAND, (sender, result) => Host_HandleDiceRollAsync(sender, result)), + new StringCommandHandler(DICE_ROLL_COMMAND, (sender, result) => Host_HandleDiceRollAsync(sender, result).HandleTask()), new NoParamCommandHandler(PING, _ => { }) }; playerCommandHandlers = new LANClientCommandHandler[] { new ClientStringCommandHandler(CHAT_COMMAND, Player_HandleChatCommand), - new ClientNoParamCommandHandler(GET_READY_COMMAND, () => HandleGetReadyCommandAsync()), + new ClientNoParamCommandHandler(GET_READY_COMMAND, () => HandleGetReadyCommandAsync().HandleTask()), new ClientStringCommandHandler(RETURN_COMMAND, Player_HandleReturnCommand), new ClientStringCommandHandler(PLAYER_OPTIONS_BROADCAST_COMMAND, HandlePlayerOptionsBroadcast), new ClientStringCommandHandler(PlayerExtraOptions.LAN_MESSAGE_KEY, HandlePlayerExtraOptionsBroadcast), - new ClientStringCommandHandler(LAUNCH_GAME_COMMAND, gameId => HandleGameLaunchCommandAsync(gameId)), - new ClientStringCommandHandler(GAME_OPTIONS_COMMAND, data => HandleGameOptionsMessageAsync(data)), + new ClientStringCommandHandler(LAUNCH_GAME_COMMAND, gameId => HandleGameLaunchCommandAsync(gameId).HandleTask()), + new ClientStringCommandHandler(GAME_OPTIONS_COMMAND, data => HandleGameOptionsMessageAsync(data).HandleTask()), new ClientStringCommandHandler(DICE_ROLL_COMMAND, Client_HandleDiceRoll), - new ClientNoParamCommandHandler(PING, () => HandlePingAsync()) + new ClientNoParamCommandHandler(PING, () => HandlePingAsync().HandleTask()) }; localGame = ClientConfiguration.Instance.LocalGame; - WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync(); + WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync().HandleTask(); } private async Task WindowManager_GameClosingAsync() { - try - { - if (client != null && client.Connected) - await ClearAsync(); + if (client is { Connected: true }) + await ClearAsync(); - cancellationTokenSource?.Cancel(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + cancellationTokenSource?.Cancel(); } private void HandleFileHashCommand(string sender, string fileHash) @@ -136,13 +134,12 @@ private void HandleFileHashCommand(string sender, string fileHash) public override void Initialize() { IniNameOverride = nameof(LANGameLobby); - lpInfo_ConnectionLostFunc = (sender, _) => LpInfo_ConnectionLostAsync(sender); + lpInfo_ConnectionLostFunc = (sender, _) => LpInfo_ConnectionLostAsync(sender).HandleTask(); base.Initialize(); PostInitialize(); } - public async Task SetUpAsync(bool isHost, - IPEndPoint hostEndPoint, Socket client) + public async Task SetUpAsync(bool isHost, IPEndPoint hostEndPoint, Socket client) { Refresh(isHost); @@ -154,8 +151,8 @@ public async Task SetUpAsync(bool isHost, if (isHost) { RandomSeed = new Random().Next(); - ListenForClientsAsync(cancellationTokenSource.Token); - SendHostPlayerJoinedMessageAsync(cancellationTokenSource.Token); + ListenForClientsAsync(cancellationTokenSource.Token).HandleTask(); + SendHostPlayerJoinedMessageAsync(cancellationTokenSource.Token).HandleTask(); var fhc = new FileHashCalculator(); fhc.CalculateHashes(GameModeMaps.GameModes); @@ -168,7 +165,7 @@ public async Task SetUpAsync(bool isHost, this.client = client; } - HandleServerCommunicationAsync(cancellationTokenSource.Token); + HandleServerCommunicationAsync(cancellationTokenSource.Token).HandleTask(); if (IsHost) CopyPlayerDataToUI(); @@ -198,10 +195,6 @@ private async Task SendHostPlayerJoinedMessageAsync(CancellationToken cancellati catch (OperationCanceledException) { } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } } public async Task PostJoinAsync() @@ -306,7 +299,7 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio { lpInfo.Name = name; - AddCallback(() => AddPlayerAsync(lpInfo, cancellationToken)); + AddCallback(() => AddPlayerAsync(lpInfo, cancellationToken).HandleTask()); return; } @@ -319,57 +312,43 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio private async Task AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { - try - { - if (Players.Find(p => p.Name == lpInfo.Name) != null || - Players.Count >= MAX_PLAYER_COUNT || Locked) - return; + if (Players.Find(p => p.Name == lpInfo.Name) != null || + Players.Count >= MAX_PLAYER_COUNT || Locked) + return; - Players.Add(lpInfo); + Players.Add(lpInfo); - if (IsHost && Players.Count == 1) - Players[0].Ready = true; + if (IsHost && Players.Count == 1) + Players[0].Ready = true; - lpInfo.MessageReceived += LpInfo_MessageReceived; - lpInfo.ConnectionLost += lpInfo_ConnectionLostFunc; + lpInfo.MessageReceived += LpInfo_MessageReceived; + lpInfo.ConnectionLost += lpInfo_ConnectionLostFunc; - AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); - lpInfo.StartReceiveLoopAsync(cancellationToken); + AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); + lpInfo.StartReceiveLoopAsync(cancellationToken).HandleTask(); - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); - await OnGameOptionChangedAsync(); - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerExtraOptionsAsync(); + await OnGameOptionChangedAsync(); + UpdateDiscordPresence(); } private async Task LpInfo_ConnectionLostAsync(object sender) { - try - { - var lpInfo = (LANPlayerInfo)sender; - CleanUpPlayer(lpInfo); - Players.Remove(lpInfo); + var lpInfo = (LANPlayerInfo)sender; + CleanUpPlayer(lpInfo); + Players.Remove(lpInfo); - AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); + AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); - if (lpInfo.Name == ProgramConstants.PLAYERNAME) - ResetDiscordPresence(); - else - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (lpInfo.Name == ProgramConstants.PLAYERNAME) + ResetDiscordPresence(); + else + UpdateDiscordPresence(); } private void LpInfo_MessageReceived(object sender, NetworkMessageEventArgs e) @@ -445,8 +424,8 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation break; } - commands.Add(msg.Substring(0, index)); - msg = msg.Substring(index + 1); + commands.Add(msg[..index]); + msg = msg[(index + 1)..]; } foreach (string cmd in commands) @@ -478,16 +457,9 @@ private void HandleMessageFromServer(string message) protected override async Task BtnLeaveGame_LeftClickAsync() { - try - { - await ClearAsync(); - GameLeft?.Invoke(this, EventArgs.Empty); - Disable(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await ClearAsync(); + GameLeft?.Invoke(this, EventArgs.Empty); + Disable(); } protected override void UpdateDiscordPresence(bool resetTimer = false) @@ -581,17 +553,7 @@ protected override async Task BroadcastPlayerExtraOptionsAsync() await BroadcastMessageAsync(playerExtraOptions.ToLanMessage(), true); } - protected override async Task HostLaunchGameAsync() - { - try - { - await BroadcastMessageAsync(LAUNCH_GAME_COMMAND + " " + UniqueGameID); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } - } + protected override Task HostLaunchGameAsync() => BroadcastMessageAsync(LAUNCH_GAME_COMMAND + " " + UniqueGameID); protected override string GetIPAddressForPlayer(PlayerInfo player) { @@ -680,34 +642,20 @@ protected override void UpdatePlayerPingIndicator(PlayerInfo pInfo) /// If true, only send this to other players. Otherwise, even the sender will receive their message. private async Task BroadcastMessageAsync(string message, bool otherPlayersOnly = false) { - try - { - if (!IsHost) - return; + if (!IsHost) + return; - foreach (PlayerInfo pInfo in Players.Where(p => !otherPlayersOnly || p.Name != ProgramConstants.PLAYERNAME)) - { - var lpInfo = (LANPlayerInfo)pInfo; - await lpInfo.SendMessageAsync(message, cancellationTokenSource?.Token ?? default); - } - } - catch (Exception ex) + foreach (PlayerInfo pInfo in Players.Where(p => !otherPlayersOnly || p.Name != ProgramConstants.PLAYERNAME)) { - PreStartup.HandleException(ex); + var lpInfo = (LANPlayerInfo)pInfo; + await lpInfo.SendMessageAsync(message, cancellationTokenSource?.Token ?? default); } } protected override async Task PlayerExtraOptions_OptionsChangedAsync() { - try - { - await base.PlayerExtraOptions_OptionsChangedAsync(); - await BroadcastPlayerExtraOptionsAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await base.PlayerExtraOptions_OptionsChangedAsync(); + await BroadcastPlayerExtraOptionsAsync(); } private async Task SendMessageToHostAsync(string message, CancellationToken cancellationToken) @@ -764,28 +712,20 @@ protected override Task LockGameAsync() protected override async Task GameProcessExitedAsync() { - try - { - await base.GameProcessExitedAsync(); - - await SendMessageToHostAsync(RETURN_COMMAND, cancellationTokenSource?.Token ?? default); + await base.GameProcessExitedAsync(); + await SendMessageToHostAsync(RETURN_COMMAND, cancellationTokenSource?.Token ?? default); - if (IsHost) - { - RandomSeed = new Random().Next(); - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); - - if (Players.Count < MAX_PLAYER_COUNT) - await UnlockGameAsync(true); - } - } - catch (Exception ex) + if (IsHost) { - PreStartup.HandleException(ex); + RandomSeed = new Random().Next(); + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerExtraOptionsAsync(); + + if (Players.Count < MAX_PLAYER_COUNT) + await UnlockGameAsync(true); } } @@ -835,7 +775,7 @@ public override void Update(GameTime gameTime) timeSinceLastReceivedCommand += gameTime.ElapsedGameTime; if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT)) - Task.Run(BtnLeaveGame_LeftClickAsync).Wait(); + Task.Run(() => BtnLeaveGame_LeftClickAsync().HandleTask()).Wait(); } base.Update(gameTime); @@ -868,24 +808,17 @@ private void BroadcastGame() private async Task GameHost_HandleChatCommandAsync(string sender, string data) { - try - { - string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); + string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); - if (parts.Length < 2) - return; + if (parts.Length < 2) + return; - int colorIndex = Conversions.IntFromString(parts[0], -1); + int colorIndex = Conversions.IntFromString(parts[0], -1); - if (colorIndex < 0 || colorIndex >= chatColors.Length) - return; + if (colorIndex < 0 || colorIndex >= chatColors.Length) + return; - await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + data); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + data); } private void Player_HandleChatCommand(string data) @@ -916,79 +849,65 @@ private void Player_HandleReturnCommand(string sender) private async Task HandleGetReadyCommandAsync() { - try - { - if (!IsHost) - await GetReadyNotificationAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (!IsHost) + await GetReadyNotificationAsync(); } private async Task HandlePlayerOptionsRequestAsync(string sender, string data) { - try - { - if (!IsHost) - return; - - PlayerInfo pInfo = Players.Find(p => p.Name == sender); - - if (pInfo == null) - return; + if (!IsHost) + return; - string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (parts.Length != 4) - return; + if (pInfo == null) + return; - int side = Conversions.IntFromString(parts[0], -1); - int color = Conversions.IntFromString(parts[1], -1); - int start = Conversions.IntFromString(parts[2], -1); - int team = Conversions.IntFromString(parts[3], -1); + string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); - if (side < 0 || side > SideCount + RandomSelectorCount) - return; + if (parts.Length != 4) + return; - if (color < 0 || color > MPColors.Count) - return; + int side = Conversions.IntFromString(parts[0], -1); + int color = Conversions.IntFromString(parts[1], -1); + int start = Conversions.IntFromString(parts[2], -1); + int team = Conversions.IntFromString(parts[3], -1); - if (Map.CoopInfo != null) - { - if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) - return; + if (side < 0 || side > SideCount + RandomSelectorCount) + return; - if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) - return; - } + if (color < 0 || color > MPColors.Count) + return; - if (start < 0 || start > Map.MaxPlayers) + if (Map.CoopInfo != null) + { + if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) return; - if (team < 0 || team > 4) + if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) return; + } - if (side != pInfo.SideId - || start != pInfo.StartingLocation - || team != pInfo.TeamId) - { - ClearReadyStatuses(); - } + if (start < 0 || start > Map.MaxPlayers) + return; - pInfo.SideId = side; - pInfo.ColorId = color; - pInfo.StartingLocation = start; - pInfo.TeamId = team; + if (team < 0 || team > 4) + return; - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - } - catch (Exception ex) + if (side != pInfo.SideId + || start != pInfo.StartingLocation + || team != pInfo.TeamId) { - PreStartup.HandleException(ex); + ClearReadyStatuses(); } + + pInfo.SideId = side; + pInfo.ColorId = color; + pInfo.StartingLocation = start; + pInfo.TeamId = team; + + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); } private void HandlePlayerExtraOptionsBroadcast(string data) => ApplyPlayerExtraOptions(null, data); @@ -1077,165 +996,135 @@ private void HandlePlayerOptionsBroadcast(string data) private async Task HandlePlayerQuitAsync(string sender) { - try - { - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo == null) - return; + if (pInfo == null) + return; - AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), pInfo.Name)); - Players.Remove(pInfo); - ClearReadyStatuses(); - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), pInfo.Name)); + Players.Remove(pInfo); + ClearReadyStatuses(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + UpdateDiscordPresence(); } private async Task HandleGameOptionsMessageAsync(string data) { - try - { - if (IsHost) - return; + if (IsHost) + return; - string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); + string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); - if (parts.Length != CheckBoxes.Count + DropDowns.Count + GAME_OPTION_SPECIAL_FLAG_COUNT) - { - AddNotice(("The game host has sent an invalid game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid")); - Logger.Log("Invalid game options message from host: " + data); - return; - } + if (parts.Length != CheckBoxes.Count + DropDowns.Count + GAME_OPTION_SPECIAL_FLAG_COUNT) + { + AddNotice(("The game host has sent an invalid game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid")); + Logger.Log("Invalid game options message from host: " + data); + return; + } - int randomSeed = Conversions.IntFromString(parts[parts.Length - GAME_OPTION_SPECIAL_FLAG_COUNT], -1); - if (randomSeed == -1) - return; + int randomSeed = Conversions.IntFromString(parts[parts.Length - GAME_OPTION_SPECIAL_FLAG_COUNT], -1); + if (randomSeed == -1) + return; - RandomSeed = randomSeed; + RandomSeed = randomSeed; - string mapSHA1 = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 1)]; - string gameMode = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 2)]; + string mapSHA1 = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 1)]; + string gameMode = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 2)]; - GameModeMap gameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); + GameModeMap gameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); - if (gameModeMap == null) - { - AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + - "The host needs to change the map or you won't be able to play.".L10N("Client:Main:HostNeedChangeMapForYou")); - await ChangeMapAsync(null); - return; - } + if (gameModeMap == null) + { + AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + + "The host needs to change the map or you won't be able to play.".L10N("Client:Main:HostNeedChangeMapForYou")); + await ChangeMapAsync(null); + return; + } - if (GameModeMap != gameModeMap) - await ChangeMapAsync(gameModeMap); + if (GameModeMap != gameModeMap) + await ChangeMapAsync(gameModeMap); - int frameSendRate = Conversions.IntFromString(parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 3)], FrameSendRate); - if (frameSendRate != FrameSendRate) - { - FrameSendRate = frameSendRate; - AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); - } + int frameSendRate = Conversions.IntFromString(parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 3)], FrameSendRate); + if (frameSendRate != FrameSendRate) + { + FrameSendRate = frameSendRate; + AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); + } - bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString( - parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 4)], Convert.ToInt32(RemoveStartingLocations))); - SetRandomStartingLocations(removeStartingLocations); + bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString( + parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 4)], Convert.ToInt32(RemoveStartingLocations))); + SetRandomStartingLocations(removeStartingLocations); - for (int i = 0; i < CheckBoxes.Count; i++) - { - GameLobbyCheckBox chkBox = CheckBoxes[i]; + for (int i = 0; i < CheckBoxes.Count; i++) + { + GameLobbyCheckBox chkBox = CheckBoxes[i]; - bool oldValue = chkBox.Checked; - chkBox.Checked = Conversions.IntFromString(parts[i], -1) > 0; + bool oldValue = chkBox.Checked; + chkBox.Checked = Conversions.IntFromString(parts[i], -1) > 0; - if (chkBox.Checked != oldValue) - { - if (chkBox.Checked) - AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), chkBox.Text)); - else - AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), chkBox.Text)); - } + if (chkBox.Checked != oldValue) + { + if (chkBox.Checked) + AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), chkBox.Text)); + else + AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), chkBox.Text)); } + } - for (int i = 0; i < DropDowns.Count; i++) - { - int index = Conversions.IntFromString(parts[CheckBoxes.Count + i], -1); + for (int i = 0; i < DropDowns.Count; i++) + { + int index = Conversions.IntFromString(parts[CheckBoxes.Count + i], -1); - GameLobbyDropDown dd = DropDowns[i]; + GameLobbyDropDown dd = DropDowns[i]; - if (index < 0 || index >= dd.Items.Count) - return; + if (index < 0 || index >= dd.Items.Count) + return; - int oldValue = dd.SelectedIndex; - dd.SelectedIndex = index; + int oldValue = dd.SelectedIndex; + dd.SelectedIndex = index; - if (index != oldValue) - { - string ddName = dd.OptionName; - if (dd.OptionName == null) - ddName = dd.Name; + if (index != oldValue) + { + string ddName = dd.OptionName; + if (dd.OptionName == null) + ddName = dd.Name; - AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.SelectedItem.Text)); - } + AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.SelectedItem.Text)); } } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } } private async Task GameHost_HandleReadyRequestAsync(string sender, string autoReady) { - try - { - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo == null) - return; + if (pInfo == null) + return; - pInfo.Ready = true; - pInfo.AutoReady = Convert.ToBoolean(Conversions.IntFromString(autoReady, 0)); - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + pInfo.Ready = true; + pInfo.AutoReady = Convert.ToBoolean(Conversions.IntFromString(autoReady, 0)); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); } private async Task HandleGameLaunchCommandAsync(string gameId) { - try - { - Players.ForEach(pInfo => pInfo.IsInGame = true); - UniqueGameID = Conversions.IntFromString(gameId, -1); - if (UniqueGameID < 0) - return; + Players.ForEach(pInfo => pInfo.IsInGame = true); + UniqueGameID = Conversions.IntFromString(gameId, -1); - CopyPlayerDataToUI(); - await StartGameAsync(); - } + if (UniqueGameID < 0) + return; - private async Task HandlePingAsync() - { - try - { - await SendMessageToHostAsync(PING, cancellationTokenSource?.Token ?? default); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + CopyPlayerDataToUI(); + await StartGameAsync(); } + + private Task HandlePingAsync() + => SendMessageToHostAsync(PING, cancellationTokenSource?.Token ?? default); + protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) { string resultString = string.Join(",", results); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs index 31744b247..fd1f86ab5 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs @@ -36,9 +36,6 @@ public class MapPreviewBox : XNAPanel { private const int MAX_STARTING_LOCATIONS = 8; - public delegate void LocalStartingLocationSelectedEventHandler(object sender, - LocalStartingLocationEventArgs e); - public event EventHandler LocalStartingLocationSelected; public event EventHandler StartingLocationApplied; @@ -49,7 +46,6 @@ public MapPreviewBox(WindowManager windowManager) : base(windowManager) FontIndex = 1; } - public void SetFields(List players, List aiPlayers, List mpColors, string[] sides, IniFile gameOptionsIni) { this.players = players; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 43caa5f4e..6f91dda39 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -27,8 +27,12 @@ internal abstract class MultiplayerGameLobby : GameLobbyBase, ISwitchable private const int MAX_DICE = 10; private const int MAX_DIE_SIDES = 100; - public MultiplayerGameLobby(WindowManager windowManager, string iniName, - TopBar topBar, MapLoader mapLoader, DiscordHandler discordHandler) + public MultiplayerGameLobby( + WindowManager windowManager, + string iniName, + TopBar topBar, + MapLoader mapLoader, + DiscordHandler discordHandler) : base(windowManager, iniName, mapLoader, true, discordHandler) { TopBar = topBar; @@ -40,20 +44,20 @@ public MultiplayerGameLobby(WindowManager windowManager, string iniName, new("SHOWMAPS", "Show map list (game host only)".L10N("Client:Main:ChatboxCommandShowMapsHelp"), true, s => ShowMapList()), new("FRAMESENDRATE", "Change order lag / FrameSendRate (default 7) (game host only)".L10N("Client:Main:ChatboxCommandFrameSendRateHelp"), true, - s => SetFrameSendRateAsync(s)), + s => SetFrameSendRateAsync(s).HandleTask()), new("MAXAHEAD", "Change MaxAhead (default 0) (game host only)".L10N("Client:Main:ChatboxCommandMaxAheadHelp"), true, - s => SetMaxAheadAsync(s)), + s => SetMaxAheadAsync(s).HandleTask()), new("PROTOCOLVERSION", "Change ProtocolVersion (default 2) (game host only)".L10N("Client:Main:ChatboxCommandProtocolVersionHelp"), true, - s => SetProtocolVersionAsync(s)), + s => SetProtocolVersionAsync(s).HandleTask()), new("LOADMAP", "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("Client:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), new("RANDOMSTARTS", "Enables completely random starting locations (Tiberian Sun based games only).".L10N("Client:Main:ChatboxCommandRandomStartsHelp"), true, - s => SetStartingLocationClearanceAsync(s)), - new("ROLL", "Roll dice, for example /roll 3d6".L10N("Client:Main:ChatboxCommandRollHelp"), false, dieType => RollDiceCommandAsync(dieType)), + s => SetStartingLocationClearanceAsync(s).HandleTask()), + new("ROLL", "Roll dice, for example /roll 3d6".L10N("Client:Main:ChatboxCommandRollHelp"), false, dieType => RollDiceCommandAsync(dieType).HandleTask()), new("SAVEOPTIONS", "Save game option preset so it can be loaded later".L10N("Client:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), - new("LOADOPTIONS", "Load game option preset".L10N("Client:Main:ChatboxCommandLoadOptionsHelp"), true, presetName => HandleGameOptionPresetLoadCommandAsync(presetName)) + new("LOADOPTIONS", "Load game option preset".L10N("Client:Main:ChatboxCommandLoadOptionsHelp"), true, presetName => HandleGameOptionPresetLoadCommandAsync(presetName).HandleTask()) }; - chkAutoReady_CheckedChangedFunc = (_, _) => ChkAutoReady_CheckedChangedAsync(); + chkAutoReady_CheckedChangedFunc = (_, _) => ChkAutoReady_CheckedChangedAsync().HandleTask(); } protected XNAPlayerSlotIndicator[] StatusIndicators; @@ -162,17 +166,17 @@ public override void Initialize() tbChatInput = FindChild(nameof(tbChatInput)); tbChatInput.MaximumTextLength = 150; - tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync(); + tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync().HandleTask(); btnLockGame = FindChild(nameof(btnLockGame)); - btnLockGame.LeftClick += (_, _) => BtnLockGame_LeftClickAsync(); + btnLockGame.LeftClick += (_, _) => HandleLockGameButtonClickAsync().HandleTask(); chkAutoReady = FindChild(nameof(chkAutoReady)); chkAutoReady.CheckedChanged += chkAutoReady_CheckedChangedFunc; chkAutoReady.Disable(); MapPreviewBox.LocalStartingLocationSelected += MapPreviewBox_LocalStartingLocationSelected; - MapPreviewBox.StartingLocationApplied += (_, _) => MapPreviewBox_StartingLocationAppliedAsync(); + MapPreviewBox.StartingLocationApplied += (_, _) => MapPreviewBox_StartingLocationAppliedAsync().HandleTask(); sndJoinSound = new EnhancedSoundEffect("joingame.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyJoinCooldown); sndLeaveSound = new EnhancedSoundEffect("leavegame.wav", 0.0, 0.0, ClientConfiguration.Instance.SoundGameLobbyLeaveCooldown); @@ -270,33 +274,25 @@ protected override Task StartGameAsync() protected override async Task GameProcessExitedAsync() { - try - { - gameSaved = false; + gameSaved = false; - if (fsw != null) - fsw.EnableRaisingEvents = false; + if (fsw != null) + fsw.EnableRaisingEvents = false; - PlayerInfo pInfo = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); + PlayerInfo pInfo = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); - pInfo.IsInGame = false; + pInfo.IsInGame = false; - await base.GameProcessExitedAsync(); - - if (IsHost) - { - GenerateGameID(); - await DdGameModeMapFilter_SelectedIndexChangedAsync(); // Refresh ranks - } - else if (chkAutoReady.Checked) - { - await RequestReadyStatusAsync(); - } + await base.GameProcessExitedAsync(); + if (IsHost) + { + GenerateGameID(); + await DdGameModeMapFilter_SelectedIndexChangedAsync(); // Refresh ranks } - catch (Exception ex) + else if (chkAutoReady.Checked) { - PreStartup.HandleException(ex); + await RequestReadyStatusAsync(); } } @@ -320,18 +316,6 @@ private void GenerateGameID() } } - private async Task BtnLockGame_LeftClickAsync() - { - try - { - await HandleLockGameButtonClickAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } - } - protected virtual async Task HandleLockGameButtonClickAsync() { if (Locked) @@ -346,78 +330,64 @@ protected virtual async Task HandleLockGameButtonClickAsync() private async Task TbChatInput_EnterPressedAsync() { - try - { - if (string.IsNullOrEmpty(tbChatInput.Text)) - return; + if (string.IsNullOrEmpty(tbChatInput.Text)) + return; - if (tbChatInput.Text.StartsWith("/")) - { - string text = tbChatInput.Text; - string command; - string parameters; + if (tbChatInput.Text.StartsWith("/")) + { + string text = tbChatInput.Text; + string command; + string parameters; - int spaceIndex = text.IndexOf(' '); + int spaceIndex = text.IndexOf(' '); - if (spaceIndex == -1) - { - command = text.Substring(1).ToUpper(); - parameters = string.Empty; - } - else - { - command = text.Substring(1, spaceIndex - 1); - parameters = text.Substring(spaceIndex + 1); - } + if (spaceIndex == -1) + { + command = text[1..].ToUpper(); + parameters = string.Empty; + } + else + { + command = text.Substring(1, spaceIndex - 1); + parameters = text[(spaceIndex + 1)..]; + } - tbChatInput.Text = string.Empty; + tbChatInput.Text = string.Empty; - foreach (var chatBoxCommand in chatBoxCommands) + foreach (var chatBoxCommand in chatBoxCommands) + { + if (command.ToUpper() == chatBoxCommand.Command) { - if (command.ToUpper() == chatBoxCommand.Command) + if (!IsHost && chatBoxCommand.HostOnly) { - if (!IsHost && chatBoxCommand.HostOnly) - { - AddNotice(string.Format("/{0} is for game hosts only.".L10N("Client:Main:ChatboxCommandHostOnly"), chatBoxCommand.Command)); - return; - } - - chatBoxCommand.Action(parameters); + AddNotice(string.Format("/{0} is for game hosts only.".L10N("Client:Main:ChatboxCommandHostOnly"), chatBoxCommand.Command)); return; } - } - StringBuilder sb = new StringBuilder("To use a command, start your message with /. Possible chat box commands:".L10N("Client:Main:ChatboxCommandTipText") + " "); - foreach (var chatBoxCommand in chatBoxCommands) - { - sb.Append(Environment.NewLine); - sb.Append(Environment.NewLine); - sb.Append($"{chatBoxCommand.Command}: {chatBoxCommand.Description}"); + chatBoxCommand.Action(parameters); + return; } - XNAMessageBox.Show(WindowManager, "Chat Box Command Help".L10N("Client:Main:ChatboxCommandTipTitle"), sb.ToString()); - return; } - await SendChatMessageAsync(tbChatInput.Text); - tbChatInput.Text = string.Empty; - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + StringBuilder sb = new StringBuilder("To use a command, start your message with /. Possible chat box commands:".L10N("Client:Main:ChatboxCommandTipText") + " "); + foreach (var chatBoxCommand in chatBoxCommands) + { + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + sb.Append($"{chatBoxCommand.Command}: {chatBoxCommand.Description}"); + } + XNAMessageBox.Show(WindowManager, "Chat Box Command Help".L10N("Client:Main:ChatboxCommandTipTitle"), sb.ToString()); + return; } + + await SendChatMessageAsync(tbChatInput.Text); + tbChatInput.Text = string.Empty; } private async Task ChkAutoReady_CheckedChangedAsync() { - try - { - UpdateLaunchGameButtonStatus(); - await RequestReadyStatusAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + UpdateLaunchGameButtonStatus(); + await RequestReadyStatusAsync(); } protected void ResetAutoReadyCheckbox() @@ -430,98 +400,70 @@ protected void ResetAutoReadyCheckbox() private async Task SetFrameSendRateAsync(string value) { - try - { - bool success = int.TryParse(value, out int intValue); + bool success = int.TryParse(value, out int intValue); - if (!success) - { - AddNotice("Command syntax: /FrameSendRate ".L10N("Client:Main:ChatboxCommandFrameSendRateSyntax")); - return; - } - - FrameSendRate = intValue; - AddNotice(string.Format("FrameSendRate has been changed to {0}".L10N("Client:Main:FrameSendRateChanged"), intValue)); - - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); - ClearReadyStatuses(); - } - catch (Exception ex) + if (!success) { - PreStartup.HandleException(ex); + AddNotice("Command syntax: /FrameSendRate ".L10N("Client:Main:ChatboxCommandFrameSendRateSyntax")); + return; } + + FrameSendRate = intValue; + AddNotice(string.Format("FrameSendRate has been changed to {0}".L10N("Client:Main:FrameSendRateChanged"), intValue)); + + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + ClearReadyStatuses(); } private async Task SetMaxAheadAsync(string value) { - try - { - bool success = int.TryParse(value, out int intValue); - - if (!success) - { - AddNotice("Command syntax: /MaxAhead ".L10N("Client:Main:ChatboxCommandMaxAheadSyntax")); - return; - } - - MaxAhead = intValue; - AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("Client:Main:MaxAheadChanged"), intValue)); + bool success = int.TryParse(value, out int intValue); - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); - } - catch (Exception ex) + if (!success) { - PreStartup.HandleException(ex); + AddNotice("Command syntax: /MaxAhead ".L10N("Client:Main:ChatboxCommandMaxAheadSyntax")); + return; } + + MaxAhead = intValue; + AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("Client:Main:MaxAheadChanged"), intValue)); + + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); } private async Task SetProtocolVersionAsync(string value) { - try - { - bool success = int.TryParse(value, out int intValue); - - if (!success) - { - AddNotice("Command syntax: /ProtocolVersion .".L10N("Client:Main:ChatboxCommandProtocolVersionSyntax")); - return; - } + bool success = int.TryParse(value, out int intValue); - if (!(intValue == 0 || intValue == 2)) - { - AddNotice("ProtocolVersion only allows values 0 and 2.".L10N("Client:Main:ChatboxCommandProtocolVersionInvalid")); - return; - } - - ProtocolVersion = intValue; - AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("Client:Main:ProtocolVersionChanged"), intValue)); - - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); + if (!success) + { + AddNotice("Command syntax: /ProtocolVersion .".L10N("Client:Main:ChatboxCommandProtocolVersionSyntax")); + return; } - catch (Exception ex) + + if (!(intValue == 0 || intValue == 2)) { - PreStartup.HandleException(ex); + AddNotice("ProtocolVersion only allows values 0 and 2.".L10N("Client:Main:ChatboxCommandProtocolVersionInvalid")); + return; } + + ProtocolVersion = intValue; + AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("Client:Main:ProtocolVersionChanged"), intValue)); + + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); } private async Task SetStartingLocationClearanceAsync(string value) { - try - { - bool removeStartingLocations = Conversions.BooleanFromString(value, RemoveStartingLocations); + bool removeStartingLocations = Conversions.BooleanFromString(value, RemoveStartingLocations); - SetRandomStartingLocations(removeStartingLocations); + SetRandomStartingLocations(removeStartingLocations); - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); } /// @@ -547,49 +489,42 @@ protected void SetRandomStartingLocations(bool newValue) /// The parameters given for the command by the user. private async Task RollDiceCommandAsync(string dieType) { - try - { - int dieSides = 6; - int dieCount = 1; + int dieSides = 6; + int dieCount = 1; - if (!string.IsNullOrEmpty(dieType)) + if (!string.IsNullOrEmpty(dieType)) + { + string[] parts = dieType.Split('d'); + if (parts.Length == 2) { - string[] parts = dieType.Split('d'); - if (parts.Length == 2) + if (!int.TryParse(parts[0], out dieCount) || !int.TryParse(parts[1], out dieSides)) { - if (!int.TryParse(parts[0], out dieCount) || !int.TryParse(parts[1], out dieSides)) - { - AddNotice("Invalid dice specified. Expected format: /roll d".L10N("Client:Main:ChatboxCommandRollInvalidAndSyntax")); - return; - } + AddNotice("Invalid dice specified. Expected format: /roll d".L10N("Client:Main:ChatboxCommandRollInvalidAndSyntax")); + return; } } + } - if (dieCount > MAX_DICE || dieCount < 1) - { - AddNotice("You can only between 1 to 10 dies at once.".L10N("Client:Main:ChatboxCommandRollInvalid2")); - return; - } - - if (dieSides > MAX_DIE_SIDES || dieSides < 2) - { - AddNotice("You can only have between 2 and 100 sides in a die.".L10N("Client:Main:ChatboxCommandRollInvalid3")); - return; - } - - int[] results = new int[dieCount]; - Random random = new Random(); - for (int i = 0; i < dieCount; i++) - { - results[i] = random.Next(1, dieSides + 1); - } + if (dieCount > MAX_DICE || dieCount < 1) + { + AddNotice("You can only between 1 to 10 dies at once.".L10N("Client:Main:ChatboxCommandRollInvalid2")); + return; + } - await BroadcastDiceRollAsync(dieSides, results); + if (dieSides > MAX_DIE_SIDES || dieSides < 2) + { + AddNotice("You can only have between 2 and 100 sides in a die.".L10N("Client:Main:ChatboxCommandRollInvalid3")); + return; } - catch (Exception ex) + + int[] results = new int[dieCount]; + Random random = new Random(); + for (int i = 0; i < dieCount; i++) { - PreStartup.HandleException(ex); + results[i] = random.Next(1, dieSides + 1); } + + await BroadcastDiceRollAsync(dieSides, results); } /// @@ -806,17 +741,9 @@ private void MapPreviewBox_LocalStartingLocationSelected(object sender, LocalSta private async Task MapPreviewBox_StartingLocationAppliedAsync() { - try - { - ClearReadyStatuses(); - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + ClearReadyStatuses(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); } /// @@ -827,234 +754,183 @@ private async Task MapPreviewBox_StartingLocationAppliedAsync() /// protected override async Task BtnLaunchGame_LeftClickAsync() { - try + if (!IsHost) { - if (!IsHost) - { - await RequestReadyStatusAsync(); - return; - } + await RequestReadyStatusAsync(); + return; + } - if (!Locked) - { - await LockGameNotificationAsync(); - return; - } + if (!Locked) + { + await LockGameNotificationAsync(); + return; + } + + var teamMappingsError = GetTeamMappingsError(); + if (!string.IsNullOrEmpty(teamMappingsError)) + { + AddNotice(teamMappingsError); + return; + } - var teamMappingsError = GetTeamMappingsError(); - if (!string.IsNullOrEmpty(teamMappingsError)) + List occupiedColorIds = new List(); + foreach (PlayerInfo player in Players) + { + if (occupiedColorIds.Contains(player.ColorId) && player.ColorId > 0) { - AddNotice(teamMappingsError); + await SharedColorsNotificationAsync(); return; } - List occupiedColorIds = new List(); - foreach (PlayerInfo player in Players) - { - if (occupiedColorIds.Contains(player.ColorId) && player.ColorId > 0) - { - await SharedColorsNotificationAsync(); - return; - } + occupiedColorIds.Add(player.ColorId); + } - occupiedColorIds.Add(player.ColorId); - } + if (AIPlayers.Any(pInfo => pInfo.SideId == ddPlayerSides[0].Items.Count - 1)) + { + await AISpectatorsNotificationAsync(); + return; + } - if (AIPlayers.Any(pInfo => pInfo.SideId == ddPlayerSides[0].Items.Count - 1)) + if (Map.EnforceMaxPlayers) + { + foreach (PlayerInfo pInfo in Players) { - await AISpectatorsNotificationAsync(); - return; - } + if (pInfo.StartingLocation == 0) + continue; - if (Map.EnforceMaxPlayers) - { - foreach (PlayerInfo pInfo in Players) + if (Players.Concat(AIPlayers).ToList().Find( + p => p.StartingLocation == pInfo.StartingLocation && + p.Name != pInfo.Name) != null) { - if (pInfo.StartingLocation == 0) - continue; - - if (Players.Concat(AIPlayers).ToList().Find( - p => p.StartingLocation == pInfo.StartingLocation && - p.Name != pInfo.Name) != null) - { - await SharedStartingLocationNotificationAsync(); - return; - } + await SharedStartingLocationNotificationAsync(); + return; } + } - for (int aiId = 0; aiId < AIPlayers.Count; aiId++) - { - int startingLocation = AIPlayers[aiId].StartingLocation; - - if (startingLocation == 0) - continue; - - int index = AIPlayers.FindIndex(aip => aip.StartingLocation == startingLocation); + for (int aiId = 0; aiId < AIPlayers.Count; aiId++) + { + int startingLocation = AIPlayers[aiId].StartingLocation; - if (index > -1 && index != aiId) - { - await SharedStartingLocationNotificationAsync(); - return; - } - } + if (startingLocation == 0) + continue; - int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) - + AIPlayers.Count; + int index = AIPlayers.FindIndex(aip => aip.StartingLocation == startingLocation); - int minPlayers = GameMode.MinPlayersOverride > -1 ? GameMode.MinPlayersOverride : Map.MinPlayers; - if (totalPlayerCount < minPlayers) + if (index > -1 && index != aiId) { - await InsufficientPlayersNotificationAsync(); + await SharedStartingLocationNotificationAsync(); return; } + } - if (Map.EnforceMaxPlayers && totalPlayerCount > Map.MaxPlayers) - { - await TooManyPlayersNotificationAsync(); - return; - } + int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + + AIPlayers.Count; + + int minPlayers = GameMode.MinPlayersOverride > -1 ? GameMode.MinPlayersOverride : Map.MinPlayers; + if (totalPlayerCount < minPlayers) + { + await InsufficientPlayersNotificationAsync(); + return; } - int iId = 0; - foreach (PlayerInfo player in Players) + if (Map.EnforceMaxPlayers && totalPlayerCount > Map.MaxPlayers) { - iId++; + await TooManyPlayersNotificationAsync(); + return; + } + } - if (player.Name == ProgramConstants.PLAYERNAME) - continue; + int iId = 0; + foreach (PlayerInfo player in Players) + { + iId++; - if (!player.Verified) - { - await NotVerifiedNotificationAsync(iId - 1); - return; - } + if (player.Name == ProgramConstants.PLAYERNAME) + continue; + if (!player.Verified) + { + await NotVerifiedNotificationAsync(iId - 1); + return; + } - if (player.IsInGame) + if (player.IsInGame) + { + await StillInGameNotificationAsync(iId - 1); + return; + } + /* + if (DisableSpectatorReadyChecking) + { + // Only account ready status if player is not a spectator + if (!player.Ready && !IsPlayerSpectator(player)) { - await StillInGameNotificationAsync(iId - 1); + await GetReadyNotificationAsync(); return; } - /* - if (DisableSpectatorReadyChecking) - { - // Only account ready status if player is not a spectator - if (!player.Ready && !IsPlayerSpectator(player)) - { - await GetReadyNotificationAsync(); - return; - } - } - else - { - if (!player.Ready) - { - await GetReadyNotificationAsync(); - return; - } - } - */ - + } + else + { if (!player.Ready) { await GetReadyNotificationAsync(); return; } } + */ - await HostLaunchGameAsync(); - - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + if (!player.Ready) + { + await GetReadyNotificationAsync(); + return; + } } + + await HostLaunchGameAsync(); } protected virtual Task LockGameNotificationAsync() { - try - { - AddNotice("You need to lock the game room before launching the game.".L10N("Client:Main:LockGameNotification")); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + AddNotice("You need to lock the game room before launching the game.".L10N("Client:Main:LockGameNotification")); return Task.CompletedTask; } protected virtual Task SharedColorsNotificationAsync() { - try - { - AddNotice("Multiple human players cannot share the same color.".L10N("Client:Main:SharedColorsNotification")); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + AddNotice("Multiple human players cannot share the same color.".L10N("Client:Main:SharedColorsNotification")); return Task.CompletedTask; } protected virtual Task AISpectatorsNotificationAsync() { - try - { - AddNotice("AI players don't enjoy spectating matches. They want some action!".L10N("Client:Main:AISpectatorsNotification")); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + AddNotice("AI players don't enjoy spectating matches. They want some action!".L10N("Client:Main:AISpectatorsNotification")); return Task.CompletedTask; } protected virtual Task SharedStartingLocationNotificationAsync() { - try - { - AddNotice("Multiple players cannot share the same starting location on this map.".L10N("Client:Main:SharedStartingLocationNotification")); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + AddNotice("Multiple players cannot share the same starting location on this map.".L10N("Client:Main:SharedStartingLocationNotification")); return Task.CompletedTask; } protected virtual Task NotVerifiedNotificationAsync(int playerIndex) { - try - { - if (playerIndex > -1 && playerIndex < Players.Count) - AddNotice(string.Format("Unable to launch game. Player {0} hasn't been verified.".L10N("Client:Main:NotVerifiedNotification"), Players[playerIndex].Name)); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (playerIndex > -1 && playerIndex < Players.Count) + AddNotice(string.Format("Unable to launch game. Player {0} hasn't been verified.".L10N("Client:Main:NotVerifiedNotification"), Players[playerIndex].Name)); return Task.CompletedTask; } protected virtual Task StillInGameNotificationAsync(int playerIndex) { - try - { - if (playerIndex > -1 && playerIndex < Players.Count) - { - AddNotice(string.Format("Unable to launch game. Player {0} is still playing the game you started previously.".L10N("Client:Main:StillInGameNotification"), - Players[playerIndex].Name)); - } - } - catch (Exception ex) + if (playerIndex > -1 && playerIndex < Players.Count) { - PreStartup.HandleException(ex); + AddNotice(string.Format("Unable to launch game. Player {0} is still playing the game you started previously.".L10N("Client:Main:StillInGameNotification"), + Players[playerIndex].Name)); } return Task.CompletedTask; @@ -1062,51 +938,30 @@ protected virtual Task StillInGameNotificationAsync(int playerIndex) protected virtual Task GetReadyNotificationAsync() { - try - { - AddNotice("The host wants to start the game but cannot because not all players are ready!".L10N("Client:Main:GetReadyNotification")); - if (!IsHost && !Players.Find(p => p.Name == ProgramConstants.PLAYERNAME).Ready) - sndGetReadySound.Play(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + AddNotice("The host wants to start the game but cannot because not all players are ready!".L10N("Client:Main:GetReadyNotification")); + if (!IsHost && !Players.Find(p => p.Name == ProgramConstants.PLAYERNAME).Ready) + sndGetReadySound.Play(); return Task.CompletedTask; } protected virtual Task InsufficientPlayersNotificationAsync() { - try - { - if (GameMode != null && GameMode.MinPlayersOverride > -1) - AddNotice(String.Format("Unable to launch game: {0} cannot be played with fewer than {1} players".L10N("Client:Main:InsufficientPlayersNotification1"), - GameMode.UIName, GameMode.MinPlayersOverride)); - else if (Map != null) - AddNotice(String.Format("Unable to launch game: this map cannot be played with fewer than {0} players.".L10N("Client:Main:InsufficientPlayersNotification2"), - Map.MinPlayers)); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (GameMode != null && GameMode.MinPlayersOverride > -1) + AddNotice(String.Format("Unable to launch game: {0} cannot be played with fewer than {1} players".L10N("Client:Main:InsufficientPlayersNotification1"), + GameMode.UIName, GameMode.MinPlayersOverride)); + else if (Map != null) + AddNotice(String.Format("Unable to launch game: this map cannot be played with fewer than {0} players.".L10N("Client:Main:InsufficientPlayersNotification2"), + Map.MinPlayers)); return Task.CompletedTask; } protected virtual Task TooManyPlayersNotificationAsync() { - try - { - if (Map != null) - AddNotice(String.Format("Unable to launch game: this map cannot be played with more than {0} players.".L10N("Client:Main:TooManyPlayersNotification"), - Map.MaxPlayers)); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (Map != null) + AddNotice(String.Format("Unable to launch game: this map cannot be played with more than {0} players.".L10N("Client:Main:TooManyPlayersNotification"), + Map.MaxPlayers)); return Task.CompletedTask; } @@ -1133,34 +988,27 @@ protected override async Task OnGameOptionChangedAsync() protected override async Task CopyPlayerDataFromUIAsync(object sender) { - try - { - if (PlayerUpdatingInProgress) - return; + if (PlayerUpdatingInProgress) + return; - if (IsHost) - { - await base.CopyPlayerDataFromUIAsync(sender); - await BroadcastPlayerOptionsAsync(); - return; - } + if (IsHost) + { + await base.CopyPlayerDataFromUIAsync(sender); + await BroadcastPlayerOptionsAsync(); + return; + } - int mTopIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); + int mTopIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); - if (mTopIndex == -1) - return; + if (mTopIndex == -1) + return; - int requestedSide = ddPlayerSides[mTopIndex].SelectedIndex; - int requestedColor = ddPlayerColors[mTopIndex].SelectedIndex; - int requestedStart = ddPlayerStarts[mTopIndex].SelectedIndex; - int requestedTeam = ddPlayerTeams[mTopIndex].SelectedIndex; + int requestedSide = ddPlayerSides[mTopIndex].SelectedIndex; + int requestedColor = ddPlayerColors[mTopIndex].SelectedIndex; + int requestedStart = ddPlayerStarts[mTopIndex].SelectedIndex; + int requestedTeam = ddPlayerTeams[mTopIndex].SelectedIndex; - await RequestPlayerOptionsAsync(requestedSide, requestedColor, requestedStart, requestedTeam); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await RequestPlayerOptionsAsync(requestedSide, requestedColor, requestedStart, requestedTeam); } protected override void CopyPlayerDataToUI() @@ -1287,7 +1135,7 @@ protected override async Task ChangeMapAsync(GameModeMap gameModeMap) { await base.ChangeMapAsync(gameModeMap); - bool resetAutoReady = gameModeMap?.GameMode == null || gameModeMap?.Map == null; + bool resetAutoReady = gameModeMap?.GameMode == null || gameModeMap.Map == null; ClearReadyStatuses(resetAutoReady); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs index c5d58df75..1cdf7a786 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -168,42 +168,28 @@ private string CheckGameValidity() protected override async Task BtnLaunchGame_LeftClickAsync() { - try - { - string error = CheckGameValidity(); + string error = CheckGameValidity(); - if (error == null) - { - SaveSettings(); - await StartGameAsync(); - return; - } - - XNAMessageBox.Show(WindowManager, "Cannot launch game".L10N("Client:Main:LaunchGameErrorTitle"), error); - } - catch (Exception ex) + if (error == null) { - PreStartup.HandleException(ex); + SaveSettings(); + await StartGameAsync(); + return; } + + XNAMessageBox.Show(WindowManager, "Cannot launch game".L10N("Client:Main:LaunchGameErrorTitle"), error); } protected override Task BtnLeaveGame_LeftClickAsync() { - try - { - Enabled = false; - Visible = false; + Enabled = false; + Visible = false; - Exited?.Invoke(this, EventArgs.Empty); + Exited?.Invoke(this, EventArgs.Empty); - topBar.RemovePrimarySwitchable(this); - ResetDiscordPresence(); + topBar.RemovePrimarySwitchable(this); + ResetDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } return Task.CompletedTask; } @@ -244,18 +230,10 @@ protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap) protected override async Task GameProcessExitedAsync() { - try - { - await base.GameProcessExitedAsync(); + await base.GameProcessExitedAsync(); + await DdGameModeMapFilter_SelectedIndexChangedAsync(); // Refresh ranks - await DdGameModeMapFilter_SelectedIndexChangedAsync(); // Refresh ranks - - RandomSeed = new Random().Next(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + RandomSeed = new Random().Next(); } public void Open() diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index f1f25d482..d10e78f9d 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -20,7 +20,7 @@ namespace DTAClient.DXGUI.Multiplayer { - class LANGameLoadingLobby : GameLoadingLobbyBase + internal sealed class LANGameLoadingLobby : GameLoadingLobbyBase { private const double DROPOUT_TIMEOUT = 20.0; private const double GAME_BROADCAST_INTERVAL = 10.0; @@ -37,8 +37,8 @@ public LANGameLoadingLobby( WindowManager windowManager, LANColor[] chatColors, MapLoader mapLoader, - DiscordHandler discordHandler - ) : base(windowManager, discordHandler) + DiscordHandler discordHandler) + : base(windowManager, discordHandler) { encoding = ProgramConstants.LAN_ENCODING; this.chatColors = chatColors; @@ -48,9 +48,9 @@ DiscordHandler discordHandler hostCommandHandlers = new LANServerCommandHandler[] { - new ServerStringCommandHandler(CHAT_COMMAND, (sender, data) => Server_HandleChatMessageAsync(sender, data)), + new ServerStringCommandHandler(CHAT_COMMAND, (sender, data) => Server_HandleChatMessageAsync(sender, data).HandleTask()), new ServerStringCommandHandler(FILE_HASH_COMMAND, Server_HandleFileHashMessage), - new ServerNoParamCommandHandler(READY_STATUS_COMMAND, sender => Server_HandleReadyRequestAsync(sender)) + new ServerNoParamCommandHandler(READY_STATUS_COMMAND, sender => Server_HandleReadyRequestAsync(sender).HandleTask()) }; playerCommandHandlers = new LANClientCommandHandler[] @@ -60,20 +60,13 @@ DiscordHandler discordHandler new ClientNoParamCommandHandler(GAME_LAUNCH_COMMAND, Client_HandleStartCommand) }; - WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync(); + WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync().HandleTask(); } private async Task WindowManager_GameClosingAsync() { - try - { - if (client is { Connected: true }) - await ClearAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (client is { Connected: true }) + await ClearAsync(); } public event EventHandler GameBroadcast; @@ -120,7 +113,7 @@ public async Task SetUpAsync(bool isHost, Socket client, int loadedGameId) if (isHost) { - ListenForClientsAsync(cancellationTokenSource.Token); + ListenForClientsAsync(cancellationTokenSource.Token).HandleTask(); this.client = new Socket(SocketType.Stream, ProtocolType.Tcp); await this.client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT); @@ -169,163 +162,135 @@ public async Task PostJoinAsync() private async Task ListenForClientsAsync(CancellationToken cancellationToken) { - try + listener = new Socket(SocketType.Stream, ProtocolType.Tcp); + listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); + listener.Listen(); + + while (!cancellationToken.IsCancellationRequested) { - listener = new Socket(SocketType.Stream, ProtocolType.Tcp); - listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); - listener.Listen(); + Socket newPlayerSocket; - while (!cancellationToken.IsCancellationRequested) + try { - Socket newPlayerSocket; - - try - { - newPlayerSocket = await listener.AcceptAsync(cancellationToken); - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - PreStartup.LogException(ex, "Listener error."); - break; - } + newPlayerSocket = await listener.AcceptAsync(cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + PreStartup.LogException(ex, "Listener error."); + break; + } - Logger.Log("New client connected from " + ((IPEndPoint)newPlayerSocket.RemoteEndPoint).Address); + Logger.Log("New client connected from " + ((IPEndPoint)newPlayerSocket.RemoteEndPoint).Address); - LANPlayerInfo lpInfo = new LANPlayerInfo(encoding); - lpInfo.SetClient(newPlayerSocket); + LANPlayerInfo lpInfo = new LANPlayerInfo(encoding); + lpInfo.SetClient(newPlayerSocket); - HandleClientConnectionAsync(lpInfo, cancellationToken); - } - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + HandleClientConnectionAsync(lpInfo, cancellationToken).HandleTask(); } } private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { - try + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); + + while (!cancellationToken.IsCancellationRequested) { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); + int bytesRead; + Memory message; - while (!cancellationToken.IsCancellationRequested) + try { - int bytesRead; - Memory message; - - try - { - message = memoryOwner.Memory[..1024]; - bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); - } - catch (OperationCanceledException) - { - break; - } - catch (Exception ex) - { - PreStartup.LogException(ex, "Socket error with client " + lpInfo.IPAddress + "; removing."); - break; - } - - if (bytesRead == 0) - { - Logger.Log("Connect attempt from " + lpInfo.IPAddress + " failed! (0 bytes read)"); + message = memoryOwner.Memory[..1024]; + bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + PreStartup.LogException(ex, "Socket error with client " + lpInfo.IPAddress + "; removing."); + break; + } - break; - } + if (bytesRead == 0) + { + Logger.Log("Connect attempt from " + lpInfo.IPAddress + " failed! (0 bytes read)"); - string msg = encoding.GetString(message.Span[..bytesRead]); - string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR); - string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR); + break; + } - if (parts.Length != 3) - break; + string msg = encoding.GetString(message.Span[..bytesRead]); + string[] command = msg.Split(ProgramConstants.LAN_MESSAGE_SEPARATOR); + string[] parts = command[0].Split(ProgramConstants.LAN_DATA_SEPARATOR); - string name = parts[1].Trim(); - int loadedGameId = Conversions.IntFromString(parts[2], -1); + if (parts.Length != 3) + break; - if (parts[0] == "JOIN" && !string.IsNullOrEmpty(name) - && loadedGameId == this.loadedGameId) - { - lpInfo.Name = name; + string name = parts[1].Trim(); + int loadedGameId = Conversions.IntFromString(parts[2], -1); - AddCallback(() => AddPlayerAsync(lpInfo, cancellationToken)); - return; - } + if (parts[0] == "JOIN" && !string.IsNullOrEmpty(name) + && loadedGameId == this.loadedGameId) + { + lpInfo.Name = name; - break; + AddCallback(() => AddPlayerAsync(lpInfo, cancellationToken).HandleTask()); + return; } - if (lpInfo.TcpClient.Connected) - lpInfo.TcpClient.Close(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + break; } + + if (lpInfo.TcpClient.Connected) + lpInfo.TcpClient.Close(); } private async Task AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { - try + if (Players.Find(p => p.Name == lpInfo.Name) != null || + Players.Count >= SGPlayers.Count || + SGPlayers.Find(p => p.Name == lpInfo.Name) == null) { - if (Players.Find(p => p.Name == lpInfo.Name) != null || - Players.Count >= SGPlayers.Count || - SGPlayers.Find(p => p.Name == lpInfo.Name) == null) - { - lpInfo.TcpClient.Close(); - return; - } + lpInfo.TcpClient.Close(); + return; + } - if (Players.Count == 0) - lpInfo.Ready = true; + if (Players.Count == 0) + lpInfo.Ready = true; - Players.Add(lpInfo); + Players.Add(lpInfo); - lpInfo.MessageReceived += LpInfo_MessageReceived; - lpInfo.ConnectionLost += (sender, _) => LpInfo_ConnectionLostAsync(sender); + lpInfo.MessageReceived += LpInfo_MessageReceived; + lpInfo.ConnectionLost += (sender, _) => LpInfo_ConnectionLostAsync(sender).HandleTask(); - sndJoinSound.Play(); + sndJoinSound.Play(); - AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); - lpInfo.StartReceiveLoopAsync(cancellationToken); + AddNotice(string.Format("{0} connected from {1}".L10N("Client:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); + lpInfo.StartReceiveLoopAsync(cancellationToken).HandleTask(); - CopyPlayerDataToUI(); - await BroadcastOptionsAsync(); - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + CopyPlayerDataToUI(); + await BroadcastOptionsAsync(); + UpdateDiscordPresence(); } private async Task LpInfo_ConnectionLostAsync(object sender) { - try - { - var lpInfo = (LANPlayerInfo)sender; - CleanUpPlayer(lpInfo); - Players.Remove(lpInfo); + var lpInfo = (LANPlayerInfo)sender; + CleanUpPlayer(lpInfo); + Players.Remove(lpInfo); - AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); + AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); - sndLeaveSound.Play(); + sndLeaveSound.Play(); - CopyPlayerDataToUI(); - await BroadcastOptionsAsync(); - UpdateDiscordPresence(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + CopyPlayerDataToUI(); + await BroadcastOptionsAsync(); + UpdateDiscordPresence(); } private void LpInfo_MessageReceived(object sender, NetworkMessageEventArgs e) @@ -400,8 +365,8 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation break; } - commands.Add(msg.Substring(0, index)); - msg = msg.Substring(index + 1); + commands.Add(msg[..index]); + msg = msg[(index + 1)..]; } foreach (string cmd in commands) @@ -433,17 +398,9 @@ private void HandleMessageFromServer(string message) protected override async Task LeaveGameAsync() { - try - { - await ClearAsync(); - Disable(); - - await base.LeaveGameAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await ClearAsync(); + Disable(); + await base.LeaveGameAsync(); } private async Task ClearAsync() @@ -494,17 +451,8 @@ protected override async Task BroadcastOptionsAsync() protected override Task HostStartGameAsync() => BroadcastMessageAsync(GAME_LAUNCH_COMMAND, cancellationTokenSource?.Token ?? default); - protected override async Task RequestReadyStatusAsync() - { - try - { - await SendMessageToHostAsync(READY_STATUS_COMMAND, cancellationTokenSource?.Token ?? default); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } - } + protected override Task RequestReadyStatusAsync() + => SendMessageToHostAsync(READY_STATUS_COMMAND, cancellationTokenSource?.Token ?? default); protected override async Task SendChatMessageAsync(string message) { @@ -518,26 +466,19 @@ await SendMessageToHostAsync(CHAT_COMMAND + " " + chatColorIndex + private async Task Server_HandleChatMessageAsync(LANPlayerInfo sender, string data) { - try - { - string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); + string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); - if (parts.Length < 2) - return; + if (parts.Length < 2) + return; - int colorIndex = Conversions.IntFromString(parts[0], -1); + int colorIndex = Conversions.IntFromString(parts[0], -1); - if (colorIndex < 0 || colorIndex >= chatColors.Length) - return; + if (colorIndex < 0 || colorIndex >= chatColors.Length) + return; - await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + - ProgramConstants.LAN_DATA_SEPARATOR + colorIndex + - ProgramConstants.LAN_DATA_SEPARATOR + data, cancellationTokenSource?.Token ?? default); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + + ProgramConstants.LAN_DATA_SEPARATOR + colorIndex + + ProgramConstants.LAN_DATA_SEPARATOR + data, cancellationTokenSource?.Token ?? default); } private void Server_HandleFileHashMessage(LANPlayerInfo sender, string hash) @@ -549,19 +490,12 @@ private void Server_HandleFileHashMessage(LANPlayerInfo sender, string hash) private async Task Server_HandleReadyRequestAsync(LANPlayerInfo sender) { - try - { - if (!sender.Ready) - { - sender.Ready = true; - CopyPlayerDataToUI(); - await BroadcastOptionsAsync(); - } - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (sender.Ready) + return; + + sender.Ready = true; + CopyPlayerDataToUI(); + await BroadcastOptionsAsync(); } #endregion @@ -645,20 +579,13 @@ private void Client_HandleStartCommand() /// The command to send. private async Task BroadcastMessageAsync(string message, CancellationToken cancellationToken) { - try - { - if (!IsHost) - return; + if (!IsHost) + return; - foreach (PlayerInfo pInfo in Players) - { - var lpInfo = (LANPlayerInfo)pInfo; - await lpInfo.SendMessageAsync(message, cancellationToken); - } - } - catch (Exception ex) + foreach (PlayerInfo pInfo in Players) { - PreStartup.HandleException(ex); + var lpInfo = (LANPlayerInfo)pInfo; + await lpInfo.SendMessageAsync(message, cancellationToken); } } @@ -725,7 +652,7 @@ public override void Update(GameTime gameTime) timeSinceLastReceivedCommand += gameTime.ElapsedGameTime; if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT)) - Task.Run(LeaveGameAsync).Wait(); + Task.Run(() => LeaveGameAsync().HandleTaskAsync()).Wait(); } base.Update(gameTime); @@ -753,16 +680,8 @@ private void BroadcastGame() protected override async Task HandleGameProcessExitedAsync() { - try - { - await base.HandleGameProcessExitedAsync(); - - await LeaveGameAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await base.HandleGameProcessExitedAsync(); + await LeaveGameAsync(); } protected override void UpdateDiscordPresence(bool resetTimer = false) diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index c128bc6dc..2036dc44a 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -104,28 +104,27 @@ public override void Initialize() WindowManager.RenderResolutionY - 64); localGame = ClientConfiguration.Instance.LocalGame; - localGameIndex = gameCollection.GameList.FindIndex( - g => g.InternalName.ToUpper() == localGame.ToUpper()); + localGameIndex = gameCollection.GameList.FindIndex(g => g.InternalName.Equals(localGame, StringComparison.InvariantCultureIgnoreCase)); btnNewGame = new XNAClientButton(WindowManager); btnNewGame.Name = "btnNewGame"; btnNewGame.ClientRectangle = new Rectangle(12, Height - 35, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnNewGame.Text = "Create Game".L10N("Client:Main:CreateGame"); - btnNewGame.LeftClick += (_, _) => BtnNewGame_LeftClickAsync(); + btnNewGame.LeftClick += (_, _) => BtnNewGame_LeftClickAsync().HandleTask(); btnJoinGame = new XNAClientButton(WindowManager); btnJoinGame.Name = "btnJoinGame"; btnJoinGame.ClientRectangle = new Rectangle(btnNewGame.Right + 12, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnJoinGame.Text = "Join Game".L10N("Client:Main:JoinGame"); - btnJoinGame.LeftClick += (_, _) => BtnJoinGame_LeftClickAsync(); + btnJoinGame.LeftClick += (_, _) => JoinGameAsync().HandleTask(); btnMainMenu = new XNAClientButton(WindowManager); btnMainMenu.Name = "btnMainMenu"; btnMainMenu.ClientRectangle = new Rectangle(Width - 145, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnMainMenu.Text = "Main Menu".L10N("Client:Main:MainMenu"); - btnMainMenu.LeftClick += (_, _) => BtnMainMenu_LeftClickAsync(); + btnMainMenu.LeftClick += (_, _) => BtnMainMenu_LeftClickAsync().HandleTask(); lbGameList = new GameListBox(WindowManager, mapLoader, localGame); lbGameList.Name = "lbGameList"; @@ -135,7 +134,7 @@ public override void Initialize() lbGameList.GameLifetime = 15.0; // Smaller lifetime in LAN lbGameList.PanelBackgroundDrawMode = PanelBackgroundImageDrawMode.STRETCHED; lbGameList.BackgroundTexture = AssetLoader.CreateTexture(new Color(0, 0, 0, 128), 1, 1); - lbGameList.DoubleLeftClick += (_, _) => LbGameList_DoubleLeftClickAsync(); + lbGameList.DoubleLeftClick += (_, _) => JoinGameAsync().HandleTask(); lbGameList.AllowMultiLineItems = false; lbPlayerList = new XNAListBox(WindowManager); @@ -164,7 +163,7 @@ public override void Initialize() btnNewGame.Height); tbChatInput.Suggestion = "Type here to chat...".L10N("Client:Main:ChatHere"); tbChatInput.MaximumTextLength = 200; - tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync(cancellationTokenSource?.Token ?? default); + tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync(cancellationTokenSource?.Token ?? default).HandleTask(); lblColor = new XNALabel(WindowManager); lblColor.Name = "lblColor"; @@ -218,8 +217,8 @@ public override void Initialize() gameCreationPanel.AddChild(gameCreationWindow); gameCreationWindow.Disable(); - gameCreationWindow.NewGame += (_, _) => GameCreationWindow_NewGameAsync(); - gameCreationWindow.LoadGame += (_, e) => GameCreationWindow_LoadGameAsync(e); + gameCreationWindow.NewGame += (_, _) => GameCreationWindow_NewGameAsync().HandleTask(); + gameCreationWindow.LoadGame += (_, e) => GameCreationWindow_LoadGameAsync(e).HandleTask(); var assembly = Assembly.GetAssembly(typeof(GameCollection)); using Stream unknownIconStream = assembly.GetManifestResourceStream("ClientCore.Resources.unknownicon.png"); @@ -251,74 +250,41 @@ public override void Initialize() SetChatColor(); ddColor.SelectedIndexChanged += DdColor_SelectedIndexChanged; - lanGameLobby.GameLeft += LanGameLobby_GameLeft; - lanGameLobby.GameBroadcast += (_, e) => LanGameLobby_GameBroadcastAsync(e, cancellationTokenSource?.Token ?? default); + lanGameLobby.GameLeft += (_, _) => Enable(); + lanGameLobby.GameBroadcast += (_, e) => SendMessageAsync(e.Message, cancellationTokenSource?.Token ?? default).HandleTask(); - lanGameLoadingLobby.GameBroadcast += (_, e) => LanGameLoadingLobby_GameBroadcastAsync(e, cancellationTokenSource?.Token ?? default); - lanGameLoadingLobby.GameLeft += LanGameLoadingLobby_GameLeft; + lanGameLoadingLobby.GameBroadcast += (_, e) => SendMessageAsync(e.Message, cancellationTokenSource?.Token ?? default).HandleTask(); + lanGameLoadingLobby.GameLeft += (_, _) => Enable(); - WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync(cancellationTokenSource?.Token ?? default); + WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync(cancellationTokenSource?.Token ?? default).HandleTask(); } - private void LanGameLoadingLobby_GameLeft(object sender, EventArgs e) - => Enable(); - private async Task WindowManager_GameClosingAsync(CancellationToken cancellationToken) { - try - { - if (socket == null) - return; + if (socket == null) + return; - if (socket.IsBound) - { - await SendMessageAsync("QUIT", cancellationToken); - cancellationTokenSource.Cancel(); - socket.Close(); - } - } - catch (Exception ex) + if (socket.IsBound) { - PreStartup.HandleException(ex); + await SendMessageAsync("QUIT", cancellationToken); + cancellationTokenSource.Cancel(); + socket.Close(); } } - private Task LanGameLobby_GameBroadcastAsync(GameBroadcastEventArgs e, CancellationToken cancellationToken) - => SendMessageAsync(e.Message, cancellationToken); - - private void LanGameLobby_GameLeft(object sender, EventArgs e) - => Enable(); - - private Task LanGameLoadingLobby_GameBroadcastAsync(GameBroadcastEventArgs e, CancellationToken cancellationToken) - => SendMessageAsync(e.Message, cancellationToken); - private async Task GameCreationWindow_LoadGameAsync(GameLoadEventArgs e) { - try - { - await lanGameLoadingLobby.SetUpAsync(true, null, e.LoadedGameID); + await lanGameLoadingLobby.SetUpAsync(true, null, e.LoadedGameID); - lanGameLoadingLobby.Enable(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + lanGameLoadingLobby.Enable(); } private async Task GameCreationWindow_NewGameAsync() { - try - { - await lanGameLobby.SetUpAsync(true, - new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), null); + await lanGameLobby.SetUpAsync(true, + new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), null); - lanGameLobby.Enable(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + lanGameLobby.Enable(); } private void SetChatColor() @@ -396,10 +362,6 @@ private async Task SendMessageAsync(string message, CancellationToken cancellati catch (OperationCanceledException) { } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } } private async Task ListenAsync(CancellationToken cancellationToken) @@ -440,7 +402,7 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) string command = commandAndParams[0]; - string[] parameters = data.Substring(command.Length + 1).Split( + string[] parameters = data[(command.Length + 1)..].Split( new[] { ProgramConstants.LAN_DATA_SEPARATOR }, StringSplitOptions.RemoveEmptyEntries); @@ -521,181 +483,143 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) private async Task SendAliveAsync(CancellationToken cancellationToken) { - try - { - StringBuilder sb = new StringBuilder("ALIVE "); - sb.Append(localGameIndex); - sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); - sb.Append(ProgramConstants.PLAYERNAME); - await SendMessageAsync(sb.ToString(), cancellationToken); - timeSinceAliveMessage = TimeSpan.Zero; - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + StringBuilder sb = new StringBuilder("ALIVE "); + sb.Append(localGameIndex); + sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); + sb.Append(ProgramConstants.PLAYERNAME); + await SendMessageAsync(sb.ToString(), cancellationToken); + timeSinceAliveMessage = TimeSpan.Zero; } private async Task TbChatInput_EnterPressedAsync(CancellationToken cancellationToken) { - try - { - if (string.IsNullOrEmpty(tbChatInput.Text)) - return; + if (string.IsNullOrEmpty(tbChatInput.Text)) + return; - string chatMessage = tbChatInput.Text.Replace((char)01, '?'); + string chatMessage = tbChatInput.Text.Replace((char)01, '?'); - StringBuilder sb = new StringBuilder("CHAT "); - sb.Append(ddColor.SelectedIndex); - sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); - sb.Append(chatMessage); + StringBuilder sb = new StringBuilder("CHAT "); + sb.Append(ddColor.SelectedIndex); + sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); + sb.Append(chatMessage); - await SendMessageAsync(sb.ToString(), cancellationToken); + await SendMessageAsync(sb.ToString(), cancellationToken); - tbChatInput.Text = string.Empty; - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + tbChatInput.Text = string.Empty; } - private async Task LbGameList_DoubleLeftClickAsync() + private async Task JoinGameAsync() { - try + if (lbGameList.SelectedIndex < 0 || lbGameList.SelectedIndex >= lbGameList.Items.Count) + return; + + HostedLANGame hg = (HostedLANGame)lbGameList.Items[lbGameList.SelectedIndex].Tag; + + if (hg.Game.InternalName.ToUpper() != localGame.ToUpper()) { - if (lbGameList.SelectedIndex < 0 || lbGameList.SelectedIndex >= lbGameList.Items.Count) - return; + lbChatMessages.AddMessage( + string.Format("The selected game is for {0}!".L10N("Client:Main:GameIsOfPurpose"), gameCollection.GetGameNameFromInternalName(hg.Game.InternalName))); + return; + } - HostedLANGame hg = (HostedLANGame)lbGameList.Items[lbGameList.SelectedIndex].Tag; + if (hg.Locked) + { + lbChatMessages.AddMessage("The selected game is locked!".L10N("Client:Main:GameLocked")); + return; + } - if (hg.Game.InternalName.ToUpper() != localGame.ToUpper()) + if (hg.IsLoadedGame) + { + if (!hg.Players.Contains(ProgramConstants.PLAYERNAME)) { - lbChatMessages.AddMessage( - string.Format("The selected game is for {0}!".L10N("Client:Main:GameIsOfPurpose"), gameCollection.GetGameNameFromInternalName(hg.Game.InternalName))); + lbChatMessages.AddMessage("You do not exist in the saved game!".L10N("Client:Main:NotInSavedGame")); return; } - - if (hg.Locked) + } + else + { + if (hg.Players.Contains(ProgramConstants.PLAYERNAME)) { - lbChatMessages.AddMessage("The selected game is locked!".L10N("Client:Main:GameLocked")); + lbChatMessages.AddMessage("Your name is already taken in the game.".L10N("Client:Main:NameOccupied")); return; } + } - if (hg.IsLoadedGame) - { - if (!hg.Players.Contains(ProgramConstants.PLAYERNAME)) - { - lbChatMessages.AddMessage("You do not exist in the saved game!".L10N("Client:Main:NotInSavedGame")); - return; - } - } - else - { - if (hg.Players.Contains(ProgramConstants.PLAYERNAME)) - { - lbChatMessages.AddMessage("Your name is already taken in the game.".L10N("Client:Main:NameOccupied")); - return; - } - } - - if (hg.GameVersion != ProgramConstants.GAME_VERSION) - { - // TODO Show warning - } + if (hg.GameVersion != ProgramConstants.GAME_VERSION) + { + // TODO Show warning + } - lbChatMessages.AddMessage(string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName)); + lbChatMessages.AddMessage(string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName)); - try - { - var client = new Socket(SocketType.Stream, ProtocolType.Tcp); - await client.ConnectAsync(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT), CancellationToken.None); + try + { + var client = new Socket(SocketType.Stream, ProtocolType.Tcp); + await client.ConnectAsync(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT), CancellationToken.None); - const int charSize = sizeof(char); + const int charSize = sizeof(char); - if (hg.IsLoadedGame) - { - var spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); - int loadedGameId = spawnSGIni.GetIntValue("Settings", "GameID", -1); - - await lanGameLoadingLobby.SetUpAsync(false, client, loadedGameId); - lanGameLoadingLobby.Enable(); - - string message = "JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + - ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + - loadedGameId + ProgramConstants.LAN_MESSAGE_SEPARATOR; - int bufferSize = message.Length * charSize; - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); - Memory buffer = memoryOwner.Memory[..bufferSize]; - int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); - buffer = buffer[..bytes]; - - await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); - await lanGameLoadingLobby.PostJoinAsync(); - } - else - { - await lanGameLobby.SetUpAsync(false, hg.EndPoint, client); - lanGameLobby.Enable(); - - string message = "JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + - ProgramConstants.PLAYERNAME + ProgramConstants.LAN_MESSAGE_SEPARATOR; - int bufferSize = message.Length * charSize; - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); - Memory buffer = memoryOwner.Memory[..bufferSize]; - int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); - buffer = buffer[..bytes]; - - await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); - await lanGameLobby.PostJoinAsync(); - } + if (hg.IsLoadedGame) + { + var spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); + int loadedGameId = spawnSGIni.GetIntValue("Settings", "GameID", -1); + + await lanGameLoadingLobby.SetUpAsync(false, client, loadedGameId); + lanGameLoadingLobby.Enable(); + + string message = "JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + + loadedGameId + ProgramConstants.LAN_MESSAGE_SEPARATOR; + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; + + await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); + await lanGameLoadingLobby.PostJoinAsync(); } - catch (Exception ex) + else { - PreStartup.LogException(ex, "Connecting to the game failed!"); - lbChatMessages.AddMessage(null, - "Connecting to the game failed! Message:".L10N("Client:Main:ConnectGameFailed") + " " + ex.Message, Color.White); + await lanGameLobby.SetUpAsync(false, hg.EndPoint, client); + lanGameLobby.Enable(); + + string message = "JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_MESSAGE_SEPARATOR; + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + buffer = buffer[..bytes]; + + await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); + await lanGameLobby.PostJoinAsync(); } } catch (Exception ex) { - PreStartup.HandleException(ex); + PreStartup.LogException(ex, "Connecting to the game failed!"); + lbChatMessages.AddMessage(null, + "Connecting to the game failed! Message:".L10N("Client:Main:ConnectGameFailed") + " " + ex.Message, Color.White); } } private async Task BtnMainMenu_LeftClickAsync() { - try - { - Visible = false; - Enabled = false; - await SendMessageAsync("QUIT", CancellationToken.None); - cancellationTokenSource.Cancel(); - socket.Close(); - Exited?.Invoke(this, EventArgs.Empty); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + Visible = false; + Enabled = false; + await SendMessageAsync("QUIT", CancellationToken.None); + cancellationTokenSource.Cancel(); + socket.Close(); + Exited?.Invoke(this, EventArgs.Empty); } - private Task BtnJoinGame_LeftClickAsync() - => LbGameList_DoubleLeftClickAsync(); - private async Task BtnNewGame_LeftClickAsync() { - try - { - if (!ClientConfiguration.Instance.DisableMultiplayerGameLoading) - gameCreationWindow.Open(); - else - await GameCreationWindow_NewGameAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (!ClientConfiguration.Instance.DisableMultiplayerGameLoading) + gameCreationWindow.Open(); + else + await GameCreationWindow_NewGameAsync(); } public override void Update(GameTime gameTime) @@ -714,7 +638,7 @@ public override void Update(GameTime gameTime) timeSinceAliveMessage += gameTime.ElapsedGameTime; if (timeSinceAliveMessage > TimeSpan.FromSeconds(ALIVE_MESSAGE_INTERVAL)) - Task.Run(() => SendAliveAsync(cancellationTokenSource?.Token ?? default)).Wait(); + Task.Run(() => SendAliveAsync(cancellationTokenSource?.Token ?? default).HandleTaskAsync()).Wait(); base.Update(gameTime); } diff --git a/DXMainClient/Domain/MainClientConstants.cs b/DXMainClient/Domain/MainClientConstants.cs index bdca2ccd0..72e6cd635 100644 --- a/DXMainClient/Domain/MainClientConstants.cs +++ b/DXMainClient/Domain/MainClientConstants.cs @@ -4,7 +4,7 @@ namespace DTAClient.Domain { public static class MainClientConstants { - public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/master-list"; + public const string CNCNET_TUNNEL_LIST_URL = "http://cncnet.org/master-list"; public static string GAME_NAME_LONG = "CnCNet Client"; public static string GAME_NAME_SHORT = "CnCNet"; @@ -48,4 +48,4 @@ public static void Initialize() } } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index 5a7fe5f03..ac8767742 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -71,7 +71,7 @@ private static async Task GetCnCNetPlayerCountAsync() { if (value.Contains(cncnetLiveStatusIdentifier)) { - numGames = Convert.ToInt32(value.Substring(cncnetLiveStatusIdentifier.Length + 1)); + numGames = Convert.ToInt32(value[(cncnetLiveStatusIdentifier.Length + 1)..]); return numGames; } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs index e177f5240..34f0ba090 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs @@ -31,7 +31,7 @@ public void ConnectToTunnel() if (tunnelConnection == null) throw new InvalidOperationException("GameTunnelHandler: Call SetUp before calling ConnectToTunnel."); - tunnelConnection.ConnectAsync(); + tunnelConnection.ConnectAsync().HandleTask(); } public Tuple CreatePlayerConnections(List playerIds) @@ -51,7 +51,7 @@ public Tuple CreatePlayerConnections(List playerIds) foreach (KeyValuePair playerConnection in playerConnections) { - playerConnection.Value.StartAsync(gamePort); + playerConnection.Value.StartAsync(gamePort).HandleTask(); } return new Tuple(ports, gamePort); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index ed5bc1269..ceb43a427 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -74,15 +74,8 @@ private void DoCurrentTunnelPinged() private async Task RefreshTunnelsAsync() { - try - { - List tunnels = await DoRefreshTunnelsAsync(); - wm.AddCallback(() => HandleRefreshedTunnels(tunnels)); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + List tunnels = await DoRefreshTunnelsAsync(); + wm.AddCallback(() => HandleRefreshedTunnels(tunnels)); } private void HandleRefreshedTunnels(List tunnels) @@ -95,7 +88,7 @@ private void HandleRefreshedTunnels(List tunnels) for (int i = 0; i < Tunnels.Count; i++) { if (UserINISettings.Instance.PingUnofficialCnCNetTunnels || Tunnels[i].Official || Tunnels[i].Recommended) - PingListTunnelAsync(i); + PingListTunnelAsync(i).HandleTask(); } if (CurrentTunnel != null) @@ -111,41 +104,30 @@ private void HandleRefreshedTunnels(List tunnels) else { // tunnel is not in the list anymore so it's not updated with a list instance and pinged - PingCurrentTunnelAsync(); + PingCurrentTunnel(); } } } private async Task PingListTunnelAsync(int index) { - try - { - await Tunnels[index].UpdatePingAsync(); - DoTunnelPinged(index); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await Tunnels[index].UpdatePingAsync(); + DoTunnelPinged(index); } + private void PingCurrentTunnel(bool checkTunnelList = false) + => PingCurrentTunnelAsync(checkTunnelList).HandleTask(); + private async Task PingCurrentTunnelAsync(bool checkTunnelList = false) { - try - { - await CurrentTunnel.UpdatePingAsync(); - DoCurrentTunnelPinged(); + await CurrentTunnel.UpdatePingAsync(); + DoCurrentTunnelPinged(); - if (checkTunnelList) - { - int tunnelIndex = Tunnels.FindIndex(t => t.Address == CurrentTunnel.Address && t.Port == CurrentTunnel.Port); - if (tunnelIndex > -1) - DoTunnelPinged(tunnelIndex); - } - } - catch (Exception ex) + if (checkTunnelList) { - PreStartup.HandleException(ex); + int tunnelIndex = Tunnels.FindIndex(t => t.Address == CurrentTunnel.Address && t.Port == CurrentTunnel.Port); + if (tunnelIndex > -1) + DoTunnelPinged(tunnelIndex); } } @@ -255,11 +237,11 @@ public override void Update(GameTime gameTime) if (skipCount % CYCLES_PER_TUNNEL_LIST_REFRESH == 0) { skipCount = 0; - RefreshTunnelsAsync(); + RefreshTunnelsAsync().HandleTask(); } else if (CurrentTunnel != null) { - PingCurrentTunnelAsync(true); + PingCurrentTunnel(true); } timeSinceTunnelRefresh = TimeSpan.Zero; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs index 2607ba0fb..f3323f826 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs @@ -90,48 +90,41 @@ public void CreateSocket() public async Task StartAsync(int gamePort) { - try - { - remoteEndPoint = new IPEndPoint(IPAddress.Loopback, gamePort); + remoteEndPoint = new IPEndPoint(IPAddress.Loopback, gamePort); - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(128); - Memory buffer = memoryOwner.Memory[..128]; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(128); + Memory buffer = memoryOwner.Memory[..128]; - socket.ReceiveTimeout = Timeout; + socket.ReceiveTimeout = Timeout; - try + try + { + while (true) { - while (true) - { - if (Aborted) - break; + if (Aborted) + break; - SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint); - Memory data = buffer[..socketReceiveFromResult.ReceivedBytes]; + SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint); + Memory data = buffer[..socketReceiveFromResult.ReceivedBytes]; - await gameTunnelHandler.PlayerConnection_PacketReceivedAsync(this, data); - } - } - catch (SocketException) - { - // Timeout + await gameTunnelHandler.PlayerConnection_PacketReceivedAsync(this, data); } + } + catch (SocketException) + { + // Timeout + } - await locker.WaitAsync(); + await locker.WaitAsync(); - try - { - aborted = true; - socket.Close(); - } - finally - { - locker.Release(); - } + try + { + aborted = true; + socket.Close(); } - catch (Exception ex) + finally { - PreStartup.HandleException(ex); + locker.Release(); } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index 470e567db..bf0c5aa9d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -66,45 +66,38 @@ private set public async Task ConnectAsync() { - try - { - Logger.Log($"Attempting to establish connection to V3 tunnel server " + - $"{tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); + Logger.Log($"Attempting to establish connection to V3 tunnel server " + + $"{tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); - tunnelEndPoint = new IPEndPoint(tunnel.IPAddress, tunnel.Port); - tunnelSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); - tunnelSocket.SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; - tunnelSocket.ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; + tunnelEndPoint = new IPEndPoint(tunnel.IPAddress, tunnel.Port); + tunnelSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); + tunnelSocket.SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; - try - { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); - Memory buffer = memoryOwner.Memory[..50]; + try + { + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); + Memory buffer = memoryOwner.Memory[..50]; - if (!BitConverter.TryWriteBytes(buffer.Span[..4], SenderId)) - throw new Exception(); + if (!BitConverter.TryWriteBytes(buffer.Span[..4], SenderId)) + throw new Exception(); - await tunnelSocket.SendToAsync(buffer, SocketFlags.None, tunnelEndPoint); + await tunnelSocket.SendToAsync(buffer, SocketFlags.None, tunnelEndPoint); - Logger.Log($"Connection to tunnel server established."); - Connected?.Invoke(this, EventArgs.Empty); - } - catch (SocketException ex) - { - PreStartup.LogException(ex, "Failed to establish connection to tunnel server."); - tunnelSocket.Close(); - ConnectionFailed?.Invoke(this, EventArgs.Empty); - return; - } - - tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; - - await ReceiveLoopAsync(); + Logger.Log($"Connection to tunnel server established."); + Connected?.Invoke(this, EventArgs.Empty); } - catch (Exception ex) + catch (SocketException ex) { - PreStartup.HandleException(ex); + PreStartup.LogException(ex, "Failed to establish connection to tunnel server."); + tunnelSocket.Close(); + ConnectionFailed?.Invoke(this, EventArgs.Empty); + return; } + + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; + + await ReceiveLoopAsync(); } private async Task ReceiveLoopAsync() diff --git a/DXMainClient/Domain/Multiplayer/LAN/ClientIntCommandHandler.cs b/DXMainClient/Domain/Multiplayer/LAN/ClientIntCommandHandler.cs index 952229104..48e17bf16 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/ClientIntCommandHandler.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/ClientIntCommandHandler.cs @@ -20,7 +20,7 @@ public override bool Handle(string message) return false; int value; - bool success = int.TryParse(message.Substring(CommandName.Length + 1), out value); + bool success = int.TryParse(message[(CommandName.Length + 1)..], out value); if (!success) return false; diff --git a/DXMainClient/Domain/Multiplayer/LAN/ClientStringCommandHandler.cs b/DXMainClient/Domain/Multiplayer/LAN/ClientStringCommandHandler.cs index dd96bbf5d..4d3486488 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/ClientStringCommandHandler.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/ClientStringCommandHandler.cs @@ -16,7 +16,7 @@ public override bool Handle(string message) if (!message.StartsWith(CommandName)) return false; - action(message.Substring(CommandName.Length + 1)); + action(message[(CommandName.Length + 1)..]); return true; } } diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index 9afde5690..3c9a9aa90 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -52,26 +52,17 @@ public void SetClient(Socket client) /// True if the player is still considered connected, otherwise false. public async Task UpdateAsync(GameTime gameTime) { - try - { - TimeSinceLastReceivedMessage += gameTime.ElapsedGameTime; - TimeSinceLastSentMessage += gameTime.ElapsedGameTime; + TimeSinceLastReceivedMessage += gameTime.ElapsedGameTime; + TimeSinceLastSentMessage += gameTime.ElapsedGameTime; - if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT) - || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)) - await SendMessageAsync("PING", cancellationTokenSource?.Token ?? default); + if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT) + || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)) + await SendMessageAsync("PING", cancellationTokenSource?.Token ?? default); - if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT)) - return false; + if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT)) + return false; - return true; - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } - - return false; + return true; } public override string IPAddress @@ -127,77 +118,63 @@ public override string ToString() /// /// Starts receiving messages from the player asynchronously. /// - public Task StartReceiveLoopAsync(CancellationToken cancellationToken) - => ReceiveMessagesAsync(cancellationToken); - - /// - /// Receives messages sent by the client, - /// and hands them over to another class via an event. - /// - private async Task ReceiveMessagesAsync(CancellationToken cancellationToken) + public async Task StartReceiveLoopAsync(CancellationToken cancellationToken) { - try + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); + + while (!cancellationToken.IsCancellationRequested) { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); + int bytesRead; + Memory message = memoryOwner.Memory[..1024]; - while (!cancellationToken.IsCancellationRequested) + try + { + bytesRead = await TcpClient.ReceiveAsync(message, SocketFlags.None, cancellationToken); + } + catch (OperationCanceledException) + { + ConnectionLost?.Invoke(this, EventArgs.Empty); + break; + } + catch (Exception ex) { - int bytesRead; - Memory message = memoryOwner.Memory[..1024]; + PreStartup.LogException(ex, "Socket error with client " + Name + "; removing."); + ConnectionLost?.Invoke(this, EventArgs.Empty); + break; + } - try - { - bytesRead = await TcpClient.ReceiveAsync(message, SocketFlags.None, cancellationToken); - } - catch (OperationCanceledException) - { - ConnectionLost?.Invoke(this, EventArgs.Empty); - break; - } - catch (Exception ex) - { - PreStartup.LogException(ex, "Socket error with client " + Name + "; removing."); - ConnectionLost?.Invoke(this, EventArgs.Empty); - break; - } + if (bytesRead > 0) + { + string msg = encoding.GetString(message.Span[..bytesRead]); - if (bytesRead > 0) - { - string msg = encoding.GetString(message.Span[..bytesRead]); + msg = overMessage + msg; - msg = overMessage + msg; + List commands = new List(); - List commands = new List(); + while (true) + { + int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR); - while (true) + if (index == -1) { - int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR); - - if (index == -1) - { - overMessage = msg; - break; - } - - commands.Add(msg.Substring(0, index)); - msg = msg.Substring(index + 1); + overMessage = msg; + break; } - foreach (string cmd in commands) - { - MessageReceived?.Invoke(this, new NetworkMessageEventArgs(cmd)); - } + commands.Add(msg[..index]); + msg = msg[(index + 1)..]; + } - continue; + foreach (string cmd in commands) + { + MessageReceived?.Invoke(this, new NetworkMessageEventArgs(cmd)); } - ConnectionLost?.Invoke(this, EventArgs.Empty); - break; + continue; } - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + + ConnectionLost?.Invoke(this, EventArgs.Empty); + break; } } } diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index a78b6859f..2480d2ee6 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -548,7 +548,7 @@ public bool SetInfoFromCustomMap() for (int i = 0; i < GameModes.Length; i++) { string gameMode = GameModes[i].Trim(); - GameModes[i] = gameMode.Substring(0, 1).ToUpperInvariant() + gameMode.Substring(1); + GameModes[i] = gameMode[..1].ToUpperInvariant() + gameMode[1..]; } MinPlayers = 0; @@ -875,8 +875,8 @@ private static Point GetIsometricWaypointCoords(string waypoint, string[] actual int xCoordIndex = parts[0].Length - 3; - int isoTileY = Convert.ToInt32(parts[0].Substring(0, xCoordIndex), CultureInfo.InvariantCulture); - int isoTileX = Convert.ToInt32(parts[0].Substring(xCoordIndex), CultureInfo.InvariantCulture); + int isoTileY = Convert.ToInt32(parts[0][..xCoordIndex], CultureInfo.InvariantCulture); + int isoTileX = Convert.ToInt32(parts[0][xCoordIndex..], CultureInfo.InvariantCulture); int level = 0; diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index cee83d81e..b5d185d15 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -53,28 +53,21 @@ public class MapLoader /// /// Load maps based on INI info as well as those in the custom maps directory. /// - public void LoadMaps() + public async Task LoadMapsAsync() { - try - { - string mpMapsPath = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath); + string mpMapsPath = SafePath.CombineFilePath(ProgramConstants.GamePath, ClientConfiguration.Instance.MPMapsIniPath); - Logger.Log($"Loading maps from {mpMapsPath}."); + Logger.Log($"Loading maps from {mpMapsPath}."); - IniFile mpMapsIni = new IniFile(mpMapsPath); + IniFile mpMapsIni = new IniFile(mpMapsPath); - LoadGameModes(mpMapsIni); - LoadGameModeAliases(mpMapsIni); - LoadMultiMaps(mpMapsIni); - LoadCustomMaps(); + LoadGameModes(mpMapsIni); + LoadGameModeAliases(mpMapsIni); + LoadMultiMaps(mpMapsIni); + await LoadCustomMapsAsync(); - GameModes.RemoveAll(g => g.Maps.Count < 1); - GameModeMaps = new GameModeMapCollection(GameModes); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + GameModes.RemoveAll(g => g.Maps.Count < 1); + GameModeMaps = new GameModeMapCollection(GameModes); } private void LoadMultiMaps(IniFile mpMapsIni) @@ -147,7 +140,7 @@ private void LoadGameModeAliases(IniFile mpMapsIni) } } - private void LoadCustomMaps() + private async Task LoadCustomMapsAsync() { DirectoryInfo customMapsDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, CUSTOM_MAPS_DIRECTORY); @@ -158,7 +151,7 @@ private void LoadCustomMaps() } IEnumerable mapFiles = customMapsDirectory.EnumerateFiles($"*{MAP_FILE_EXTENSION}"); - ConcurrentDictionary customMapCache = LoadCustomMapCache(); + ConcurrentDictionary customMapCache = await LoadCustomMapCacheAsync(); var localMapSHAs = new List(); var tasks = new List(); @@ -167,8 +160,8 @@ private void LoadCustomMaps() { tasks.Add(Task.Run(() => { - string baseFilePath = mapFile.FullName.Substring(ProgramConstants.GamePath.Length); - baseFilePath = baseFilePath.Substring(0, baseFilePath.Length - 4); + string baseFilePath = mapFile.FullName[ProgramConstants.GamePath.Length..]; + baseFilePath = baseFilePath[..^4]; var map = new Map(baseFilePath .Replace(Path.DirectorySeparatorChar, '/') @@ -180,7 +173,7 @@ private void LoadCustomMaps() })); } - Task.WaitAll(tasks.ToArray()); + await Task.WhenAll(tasks.ToArray()); // remove cached maps that no longer exist locally foreach (var missingSHA in customMapCache.Keys.Where(cachedSHA => !localMapSHAs.Contains(cachedSHA))) @@ -217,14 +210,12 @@ private void CacheCustomMaps(ConcurrentDictionary customMaps) /// Load previously cached custom maps /// /// - private ConcurrentDictionary LoadCustomMapCache() + private async Task> LoadCustomMapCacheAsync() { try { - var jsonData = File.ReadAllText(CUSTOM_MAPS_CACHE); - - var customMapCache = JsonSerializer.Deserialize(jsonData, jsonSerializerOptions); - + await using var jsonData = File.OpenRead(CUSTOM_MAPS_CACHE); + var customMapCache = await JsonSerializer.DeserializeAsync(jsonData, jsonSerializerOptions); var customMaps = customMapCache?.Version == CurrentCustomMapCacheVersion && customMapCache.Maps != null ? customMapCache.Maps : new ConcurrentDictionary(); diff --git a/DXMainClient/Extensions/TaskExtensions.cs b/DXMainClient/Extensions/TaskExtensions.cs new file mode 100644 index 000000000..3a99d5968 --- /dev/null +++ b/DXMainClient/Extensions/TaskExtensions.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; + +namespace DTAClient; + +internal static class TaskExtensions +{ + /// + /// Asynchronously awaits a and guarantees all exceptions are caught and handled when the is not directly awaited. + /// + /// The who's exceptions will be handled. + /// Returns a that awaited and handled the original . + public static async Task HandleTaskAsync(this Task task) + { + try + { + await task; + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } + } + + /// + /// Synchronously awaits a and guarantees all exceptions are caught and handled when the is not directly awaited. + /// + /// The who's exceptions will be handled. + public static void HandleTask(this Task task) +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + => task.HandleTaskAsync(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed +} \ No newline at end of file diff --git a/DXMainClient/Online/CnCNetManager.cs b/DXMainClient/Online/CnCNetManager.cs index d231f7ada..028cbe40d 100644 --- a/DXMainClient/Online/CnCNetManager.cs +++ b/DXMainClient/Online/CnCNetManager.cs @@ -544,66 +544,59 @@ private void DoReconnectAttempt() public void OnUserJoinedChannel(string channelName, string host, string userName, string ident) { - wm.AddCallback(() => DoUserJoinedChannelAsync(channelName, host, userName, ident)); + wm.AddCallback(() => DoUserJoinedChannelAsync(channelName, host, userName, ident).HandleTask()); } private async Task DoUserJoinedChannelAsync(string channelName, string host, string userName, string userAddress) { - try - { - Channel channel = FindChannel(channelName); + Channel channel = FindChannel(channelName); - if (channel == null) - return; + if (channel == null) + return; - bool isAdmin = false; - string name = userName; + bool isAdmin = false; + string name = userName; - if (userName.StartsWith("@")) - { - isAdmin = true; - name = userName.Remove(0, 1); - } + if (userName.StartsWith("@")) + { + isAdmin = true; + name = userName.Remove(0, 1); + } - IRCUser ircUser = null; + IRCUser ircUser = null; - // Check if we already know this user from another channel - // Avoid LINQ here for performance reasons - foreach (var user in UserList) + // Check if we already know this user from another channel + // Avoid LINQ here for performance reasons + foreach (var user in UserList) + { + if (user.Name == name) { - if (user.Name == name) - { - ircUser = (IRCUser)user.Clone(); - break; - } + ircUser = (IRCUser)user.Clone(); + break; } + } - // If we don't know the user, create a new one - if (ircUser == null) - { - string identifier = userAddress.Split('@')[0]; - string[] parts = identifier.Split('.'); - ircUser = new IRCUser(name, identifier, host); - - if (parts.Length > 1) - { - ircUser.GameID = gameCollection.GameList.FindIndex(g => g.InternalName.ToUpper() == parts[0].Replace("~", string.Empty)); - } + // If we don't know the user, create a new one + if (ircUser == null) + { + string identifier = userAddress.Split('@')[0]; + string[] parts = identifier.Split('.'); + ircUser = new IRCUser(name, identifier, host); - AddUserToGlobalUserList(ircUser); + if (parts.Length > 1) + { + ircUser.GameID = gameCollection.GameList.FindIndex(g => g.InternalName.ToUpper() == parts[0].Replace("~", string.Empty)); } - var channelUser = new ChannelUser(ircUser); - channelUser.IsAdmin = isAdmin; - channelUser.IsFriend = cncNetUserData.IsFriend(channelUser.IRCUser.Name); - - ircUser.Channels.Add(channelName); - await channel.OnUserJoinedAsync(channelUser); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); + AddUserToGlobalUserList(ircUser); } + + var channelUser = new ChannelUser(ircUser); + channelUser.IsAdmin = isAdmin; + channelUser.IsFriend = cncNetUserData.IsFriend(channelUser.IRCUser.Name); + + ircUser.Channels.Add(channelName); + await channel.OnUserJoinedAsync(channelUser); } private void AddUserToGlobalUserList(IRCUser user) @@ -721,10 +714,10 @@ private void DoUserListReceived(string channelName, string[] userList) if (userName.StartsWith("@")) { isAdmin = true; - name = userName.Substring(1); + name = userName[1..]; } else if (userName.StartsWith("+")) - name = userName.Substring(1); + name = userName[1..]; // Check if we already know the IRC user from another channel IRCUser ircUser = UserList.Find(u => u.Name == name); @@ -837,7 +830,7 @@ private void DoWhoReplyReceived(string ident, string hostName, string userName, public void OnNameAlreadyInUse() { - wm.AddCallback(DoNameAlreadyInUseAsync); + wm.AddCallback(() => DoNameAlreadyInUseAsync().HandleTask()); } /// @@ -847,43 +840,36 @@ public void OnNameAlreadyInUse() /// private async Task DoNameAlreadyInUseAsync() { - try + var charList = ProgramConstants.PLAYERNAME.ToList(); + int maxNameLength = ClientConfiguration.Instance.MaxNameLength; + + if (charList.Count < maxNameLength) + charList.Add('_'); + else { - var charList = ProgramConstants.PLAYERNAME.ToList(); - int maxNameLength = ClientConfiguration.Instance.MaxNameLength; + int lastNonUnderscoreIndex = charList.FindLastIndex(c => c != '_'); - if (charList.Count < maxNameLength) - charList.Add('_'); - else + if (lastNonUnderscoreIndex == -1) { - int lastNonUnderscoreIndex = charList.FindLastIndex(c => c != '_'); - - if (lastNonUnderscoreIndex == -1) - { - MainChannel.AddMessage(new ChatMessage(Color.White, - "Your nickname is invalid or already in use. Please change your nickname in the login screen.".L10N("Client:Main:PickAnotherNickName"))); - UserINISettings.Instance.SkipConnectDialog.Value = false; - await DisconnectAsync(); - return; - } - - charList[lastNonUnderscoreIndex] = '_'; + MainChannel.AddMessage(new ChatMessage(Color.White, + "Your nickname is invalid or already in use. Please change your nickname in the login screen.".L10N("Client:Main:PickAnotherNickName"))); + UserINISettings.Instance.SkipConnectDialog.Value = false; + await DisconnectAsync(); + return; } - var sb = new StringBuilder(); - foreach (char c in charList) - sb.Append(c); + charList[lastNonUnderscoreIndex] = '_'; + } - MainChannel.AddMessage(new ChatMessage(Color.White, - string.Format("Your name is already in use. Retrying with {0}...".L10N("Client:Main:NameInUseRetry"), sb))); + var sb = new StringBuilder(); + foreach (char c in charList) + sb.Append(c); - ProgramConstants.PLAYERNAME = sb.ToString(); - await connection.ChangeNicknameAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Your name is already in use. Retrying with {0}...".L10N("Client:Main:NameInUseRetry"), sb))); + + ProgramConstants.PLAYERNAME = sb.ToString(); + await connection.ChangeNicknameAsync(); } public void OnBannedFromChannel(string channelName) diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index cbf5d2464..5f0faedaa 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -101,7 +101,7 @@ public static void SetId(string id) lock (idLocker) { int maxLength = ID_LENGTH - (ClientConfiguration.Instance.LocalGame.Length + 1); - systemId = Utilities.CalculateSHA1ForString(id).Substring(0, maxLength); + systemId = Utilities.CalculateSHA1ForString(id)[..maxLength]; } } @@ -125,7 +125,7 @@ public void ConnectAsync() cancellationTokenSource?.Dispose(); cancellationTokenSource = new CancellationTokenSource(); - ConnectToServerAsync(cancellationTokenSource.Token); + ConnectToServerAsync(cancellationTokenSource.Token).HandleTask(); } /// @@ -173,7 +173,7 @@ await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(server.Host), port), connectionManager.OnConnected(); - RunSendQueueAsync(cancellationToken); + RunSendQueueAsync(cancellationToken).HandleTask(); socket?.Dispose(); socket = client; @@ -202,10 +202,6 @@ await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(server.Host), port), catch (OperationCanceledException) { } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } } private async Task HandleCommAsync(CancellationToken cancellationToken) @@ -221,7 +217,7 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) Enabled = true }; - timer.Elapsed += (_, _) => AutoPingAsync(); + timer.Elapsed += (_, _) => AutoPingAsync().HandleTask(); connectionCut = true; @@ -476,14 +472,14 @@ private async Task HandleMessageAsync(string message) } else if (msg.Length != commandEndIndex + 1) { - string command = msg.Substring(0, commandEndIndex - 1); + string command = msg[..(commandEndIndex - 1)]; await PerformCommandAsync(command); msg = msg.Remove(0, commandEndIndex + 1); } else { - string command = msg.Substring(0, msg.Length - 1); + string command = msg[..^1]; await PerformCommandAsync(command); break; } @@ -617,14 +613,14 @@ private async Task PerformCommandAsync(string message) string channelName = parameters[0]; string ctcpMessage = parameters[1]; ctcpMessage = ctcpMessage.Remove(0, 1).Remove(ctcpMessage.Length - 2); - string ctcpSender = prefix.Substring(0, noticeExclamIndex); + string ctcpSender = prefix[..noticeExclamIndex]; connectionManager.OnCTCPParsed(channelName, ctcpSender, ctcpMessage); return; } else { - string noticeUserName = prefix.Substring(0, noticeExclamIndex); + string noticeUserName = prefix[..noticeExclamIndex]; string notice = parameters[parameters.Count - 1]; connectionManager.OnNoticeMessageParsed(notice, noticeUserName); break; @@ -639,18 +635,18 @@ private async Task PerformCommandAsync(string message) string channel = parameters[0]; int atIndex = prefix.IndexOf('@'); int exclamIndex = prefix.IndexOf('!'); - string userName = prefix.Substring(0, exclamIndex); + string userName = prefix[..exclamIndex]; string ident = prefix.Substring(exclamIndex + 1, atIndex - (exclamIndex + 1)); - string host = prefix.Substring(atIndex + 1); + string host = prefix[(atIndex + 1)..]; connectionManager.OnUserJoinedChannel(channel, host, userName, ident); break; case "PART": string pChannel = parameters[0]; - string pUserName = prefix.Substring(0, prefix.IndexOf('!')); + string pUserName = prefix[..prefix.IndexOf('!')]; connectionManager.OnUserLeftChannel(pChannel, pUserName); break; case "QUIT": - string qUserName = prefix.Substring(0, prefix.IndexOf('!')); + string qUserName = prefix[..prefix.IndexOf('!')]; connectionManager.OnUserQuitIRC(qUserName); break; case "PRIVMSG": @@ -658,14 +654,14 @@ private async Task PerformCommandAsync(string message) { goto case "NOTICE"; } - string pmsgUserName = prefix.Substring(0, prefix.IndexOf('!')); + string pmsgUserName = prefix[..prefix.IndexOf('!')]; string pmsgIdent = GetIdentFromPrefix(prefix); string[] recipients = new string[parameters.Count - 1]; for (int pid = 0; pid < parameters.Count - 1; pid++) recipients[pid] = parameters[pid]; string privmsg = parameters[parameters.Count - 1]; if (parameters[1].StartsWith('\u0001' + "ACTION")) - privmsg = privmsg.Substring(1).Remove(privmsg.Length - 2); + privmsg = privmsg[1..].Remove(privmsg.Length - 2); foreach (string recipient in recipients) { if (recipient.StartsWith("#")) @@ -675,7 +671,7 @@ private async Task PerformCommandAsync(string message) } break; case "MODE": - string modeUserName = prefix.Contains('!') ? prefix.Substring(0, prefix.IndexOf('!')) : prefix; + string modeUserName = prefix.Contains('!') ? prefix[..prefix.IndexOf('!')] : prefix; string modeChannelName = parameters[0]; string modeString = parameters[1]; List modeParameters = @@ -706,14 +702,14 @@ private async Task PerformCommandAsync(string message) if (parameters.Count < 2) break; - connectionManager.OnChannelTopicChanged(prefix.Substring(0, prefix.IndexOf('!')), + connectionManager.OnChannelTopicChanged(prefix[..prefix.IndexOf('!')], parameters[0], parameters[1]); break; case "NICK": int nickExclamIndex = prefix.IndexOf('!'); if (nickExclamIndex > -1 || parameters.Count < 1) { - string oldNick = prefix.Substring(0, nickExclamIndex); + string oldNick = prefix[..nickExclamIndex]; string newNick = parameters[0]; Logger.Log("Nick change - " + oldNick + " -> " + newNick); connectionManager.OnUserNicknameChange(oldNick, newNick); @@ -766,7 +762,7 @@ private void ParseIrcMessage(string message, out string prefix, out string comma int trailingStart = message.IndexOf(" :"); string trailing = null; if (trailingStart >= 0) - trailing = message.Substring(trailingStart + 2); + trailing = message[(trailingStart + 2)..]; else trailingStart = message.Length; @@ -808,93 +804,77 @@ private async Task RunSendQueueAsync(CancellationToken cancellationToken) { try { - try + while (!cancellationToken.IsCancellationRequested) { - while (!cancellationToken.IsCancellationRequested) - { - string message = string.Empty; + string message = string.Empty; - await messageQueueLocker.WaitAsync(cancellationToken); + await messageQueueLocker.WaitAsync(cancellationToken); - try + try + { + for (int i = 0; i < MessageQueue.Count; i++) { - for (int i = 0; i < MessageQueue.Count; i++) + QueuedMessage qm = MessageQueue[i]; + if (qm.Delay > 0) { - QueuedMessage qm = MessageQueue[i]; - if (qm.Delay > 0) + if (qm.SendAt < DateTime.Now) { - if (qm.SendAt < DateTime.Now) - { - message = qm.Command; + message = qm.Command; - Logger.Log("Delayed message sent: " + qm.ID); + Logger.Log("Delayed message sent: " + qm.ID); - MessageQueue.RemoveAt(i); - break; - } - } - else - { - message = qm.Command; MessageQueue.RemoveAt(i); break; } } + else + { + message = qm.Command; + MessageQueue.RemoveAt(i); + break; + } } - finally - { - messageQueueLocker.Release(); - } - - if (string.IsNullOrEmpty(message)) - { - await Task.Delay(10, cancellationToken); - continue; - } - - await SendMessageAsync(message); - await Task.Delay(MessageQueueDelay, cancellationToken); - } - } - catch (OperationCanceledException) - { - } - finally - { - await messageQueueLocker.WaitAsync(CancellationToken.None); - - try - { - MessageQueue.Clear(); } finally { messageQueueLocker.Release(); } - sendQueueExited = true; + if (string.IsNullOrEmpty(message)) + { + await Task.Delay(10, cancellationToken); + continue; + } + + await SendMessageAsync(message); + await Task.Delay(MessageQueueDelay, cancellationToken); } } - catch (Exception ex) + catch (OperationCanceledException) { - PreStartup.HandleException(ex); + } + finally + { + await messageQueueLocker.WaitAsync(CancellationToken.None); + + try + { + MessageQueue.Clear(); + } + finally + { + messageQueueLocker.Release(); + } + + sendQueueExited = true; } } /// /// Sends a PING message to the server to indicate that we're still connected. /// - private async Task AutoPingAsync() - { - try - { - await SendMessageAsync("PING LAG" + new Random().Next(100000, 999999)); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } - } + private Task AutoPingAsync() + => SendMessageAsync("PING LAG" + new Random().Next(100000, 999999)); /// /// Registers the user. diff --git a/DXMainClient/PreStartup.cs b/DXMainClient/PreStartup.cs index bc4772ba8..eb6337cc3 100644 --- a/DXMainClient/PreStartup.cs +++ b/DXMainClient/PreStartup.cs @@ -234,7 +234,7 @@ private static void LogExceptionRecursive(Exception ex, string message = null, b /// Logs all details of an exception to the logfile, notifies the user, and exits the application. /// /// The to log. - public static void HandleException(Exception ex) + internal static void HandleException(Exception ex) { LogExceptionRecursive(ex, "KABOOOOOOM!!! Info:"); diff --git a/DXMainClient/Startup.cs b/DXMainClient/Startup.cs index 803dd32a2..0cd1f7c61 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -56,7 +56,7 @@ public void Execute() Task.Run(CheckSystemSpecifications); } - GenerateOnlineIdAsync(); + GenerateOnlineIdAsync().HandleTask(); #if ARES Task.Run(() => PruneFiles(SafePath.GetDirectory(ProgramConstants.GamePath, "debug"), DateTime.Now.AddDays(-7))); From 45e4b14172f4807a145d47d1988384d0518151c0 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 18:50:18 +0100 Subject: [PATCH 038/109] Update Task exception handling --- DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs | 2 +- DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs | 2 +- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 2 +- .../Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs | 2 +- DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index a07cfc1a1..a94024e8c 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -251,7 +251,7 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) LANPlayerInfo lpInfo = new LANPlayerInfo(encoding); lpInfo.SetClient(client); - HandleClientConnectionAsync(lpInfo, cancellationToken); + HandleClientConnectionAsync(lpInfo, cancellationToken).HandleTask(); } } diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index d10e78f9d..8ac90ced3 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -142,7 +142,7 @@ public async Task SetUpAsync(bool isHost, Socket client, int loadedGameId) this.client = client; } - HandleServerCommunicationAsync(cancellationTokenSource.Token); + HandleServerCommunicationAsync(cancellationTokenSource.Token).HandleTask(); if (IsHost) CopyPlayerDataToUI(); diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 2036dc44a..bbb54affb 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -337,7 +337,7 @@ public async Task OpenAsync() } Logger.Log("Starting listener."); - ListenAsync(cancellationTokenSource.Token); + ListenAsync(cancellationTokenSource.Token).HandleTask(); await SendAliveAsync(cancellationTokenSource.Token); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index ac8767742..883571941 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -22,7 +22,7 @@ public static void InitializeService(CancellationTokenSource cts) { cncnetLiveStatusIdentifier = ClientConfiguration.Instance.CnCNetLiveStatusIdentifier; - RunServiceAsync(cts.Token); + RunServiceAsync(cts.Token).HandleTask(); } private static async Task RunServiceAsync(CancellationToken cancellationToken) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs index c134efa14..4afaeb8b8 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs @@ -56,7 +56,7 @@ public static void UploadMap(Map map, string myGame) MapUploadQueue.Add(map); if (MapUploadQueue.Count == 1) - UploadAsync(map, myGame.ToLower()); + UploadAsync(map, myGame.ToLower()).HandleTask(); } } @@ -96,7 +96,7 @@ private static async Task UploadAsync(Map map, string myGameId) Logger.Log("MapSharer: There are additional maps in the queue."); - UploadAsync(nextMap, myGameId); + UploadAsync(nextMap, myGameId).HandleTask(); } } } @@ -203,7 +203,7 @@ public static void DownloadMap(string sha1, string myGame, string mapName) MapDownloadQueue.Add(sha1); if (MapDownloadQueue.Count == 1) - DownloadAsync(sha1, myGame.ToLower(), mapName); + DownloadAsync(sha1, myGame.ToLower(), mapName).HandleTask(); } } @@ -241,7 +241,7 @@ private static async Task DownloadAsync(string sha1, string myGameId, string map if (MapDownloadQueue.Any()) { Logger.Log("MapSharer: Continuing custom map downloads."); - DownloadAsync(MapDownloadQueue[0], myGameId, mapName); + DownloadAsync(MapDownloadQueue[0], myGameId, mapName).HandleTask(); } } } From cb7b574dc8e4d02d07e5b36adadf69261f8a8a46 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 18:51:47 +0100 Subject: [PATCH 039/109] Update tunnel api url --- DXMainClient/Domain/MainClientConstants.cs | 2 +- DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DXMainClient/Domain/MainClientConstants.cs b/DXMainClient/Domain/MainClientConstants.cs index 72e6cd635..5afdf092f 100644 --- a/DXMainClient/Domain/MainClientConstants.cs +++ b/DXMainClient/Domain/MainClientConstants.cs @@ -4,7 +4,7 @@ namespace DTAClient.Domain { public static class MainClientConstants { - public const string CNCNET_TUNNEL_LIST_URL = "http://cncnet.org/master-list"; + public const string CNCNET_TUNNEL_LIST_URL = "https://core-api.cncnet.org/tunnels/master-list"; public static string GAME_NAME_LONG = "CnCNet Client"; public static string GAME_NAME_SHORT = "CnCNet"; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 918fada2d..9b5685476 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -27,7 +27,7 @@ internal sealed class CnCNetTunnel /// A CnCNetTunnel instance parsed from the given string. public static CnCNetTunnel Parse(string str) { - // For the format, check https://cncnet.org/master-list + // For the format, check https://core-api.cncnet.org/tunnels/master-list try { var tunnel = new CnCNetTunnel(); From e985f18d3ffcbe73457afbc1190f47b9f814d410 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 19:18:29 +0100 Subject: [PATCH 040/109] Saved game constants --- ClientCore/ProgramConstants.cs | 3 ++- ClientCore/SavedGameManager.cs | 8 +++----- DXMainClient/DXGUI/Generic/GameLoadingWindow.cs | 6 ++---- DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 2 +- DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 6 +++--- .../DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs | 2 +- DXMainClient/Startup.cs | 2 +- 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index 8464a16c4..457753a8d 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -42,7 +42,8 @@ public static class ProgramConstants public const string SPAWNMAP_INI = "spawnmap.ini"; public const string SPAWNER_SETTINGS = "spawn.ini"; - public const string SAVED_GAME_SPAWN_INI = "Saved Games/spawnSG.ini"; + public const string SAVED_GAME_SPAWN_INI = SAVED_GAMES_DIRECTORY + "/spawnSG.ini"; + public const string SAVED_GAMES_DIRECTORY = "Saved Games"; /// /// The locale code that corresponds to the language the hardcoded client strings are in. diff --git a/ClientCore/SavedGameManager.cs b/ClientCore/SavedGameManager.cs index 89506a3a7..d65fb6c59 100644 --- a/ClientCore/SavedGameManager.cs +++ b/ClientCore/SavedGameManager.cs @@ -10,8 +10,6 @@ namespace ClientCore /// public static class SavedGameManager { - private const string SAVED_GAMES_DIRECTORY = "Saved Games"; - private static bool saveRenameInProgress = false; public static int GetSaveGameCount() @@ -62,7 +60,7 @@ public static bool AreSavedGamesAvailable() private static string GetSaveGameDirectoryPath() { - return SafePath.CombineDirectoryPath(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY); + return SafePath.CombineDirectoryPath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAMES_DIRECTORY); } /// @@ -78,8 +76,8 @@ public static bool InitSavedGames() try { Logger.Log("Writing spawn.ini for saved game."); - SafePath.DeleteFileIfExists(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, "spawnSG.ini"); - File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS), SafePath.CombineFilePath(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, "spawnSG.ini")); + SafePath.DeleteFileIfExists(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI); + File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS), SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); } catch (Exception ex) { diff --git a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs index 8ce37652d..74cf92c18 100644 --- a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs @@ -18,8 +18,6 @@ namespace DTAClient.DXGUI.Generic /// public class GameLoadingWindow : XNAWindow { - private const string SAVED_GAMES_DIRECTORY = "Saved Games"; - public GameLoadingWindow(WindowManager windowManager, DiscordHandler discordHandler) : base(windowManager) { this.discordHandler = discordHandler; @@ -162,7 +160,7 @@ private void DeleteMsgBox_YesClicked(XNAMessageBox obj) SavedGame sg = savedGames[lbSaveGameList.SelectedIndex]; Logger.Log("Deleting saved game " + sg.FileName); - SafePath.DeleteFileIfExists(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY, sg.FileName); + SafePath.DeleteFileIfExists(ProgramConstants.GamePath, ProgramConstants.SAVED_GAMES_DIRECTORY, sg.FileName); ListSaves(); } @@ -183,7 +181,7 @@ public void ListSaves() lbSaveGameList.ClearItems(); lbSaveGameList.SelectedIndex = -1; - DirectoryInfo savedGamesDirectoryInfo = SafePath.GetDirectory(ProgramConstants.GamePath, SAVED_GAMES_DIRECTORY); + DirectoryInfo savedGamesDirectoryInfo = SafePath.GetDirectory(ProgramConstants.GamePath, ProgramConstants.SAVED_GAMES_DIRECTORY); if (!savedGamesDirectoryInfo.Exists) { diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 474307512..cff05b4af 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -864,7 +864,7 @@ private async Task JoinGameAsync(HostedCnCNetGame hg, string password, IMe } else { - IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "Saved Games", "spawnSG.ini")); + IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); password = Utilities.CalculateSHA1ForString( spawnSGIni.GetStringValue("Settings", "GameID", string.Empty))[..10]; } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index b8f26cd83..48d68cf8b 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -200,7 +200,7 @@ public override void Initialize() if (SavedGameManager.AreSavedGamesAvailable()) { - fsw = new FileSystemWatcher(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Saved Games"), "*.NET"); + fsw = new FileSystemWatcher(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAMES_DIRECTORY), "*.NET"); fsw.EnableRaisingEvents = false; fsw.Created += fsw_Created; fsw.Changed += fsw_Created; @@ -291,7 +291,7 @@ protected void LoadGame() spawnFileInfo.Delete(); - File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, "Saved Games", "spawnSG.ini"), spawnFileInfo.FullName); + File.Copy(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI), spawnFileInfo.FullName); IniFile spawnIni = new IniFile(spawnFileInfo.FullName); @@ -401,7 +401,7 @@ public void Refresh(bool isHost) ddSavedGame.AllowDropDown = isHost; btnLoadGame.Text = isHost ? "Load Game".L10N("Client:Main:ButtonLoadGame") : "I'm Ready".L10N("Client:Main:ButtonGetReady"); - IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, "Saved Games", "spawnSG.ini")); + IniFile spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); lblMapNameValue.Tag = spawnSGIni.GetStringValue("Settings", "UIMapName", string.Empty); lblMapNameValue.Text = ((string)lblGameModeValue.Tag).L10N($"INI:Maps:{spawnSGIni.GetStringValue("Settings", "MapID", string.Empty)}:Description"); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 6f91dda39..59eaf302f 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -186,7 +186,7 @@ public override void Initialize() if (SavedGameManager.AreSavedGamesAvailable()) { - fsw = new FileSystemWatcher(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Saved Games"), "*.NET"); + fsw = new FileSystemWatcher(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAMES_DIRECTORY), "*.NET"); fsw.Created += fsw_Created; fsw.Changed += fsw_Created; fsw.EnableRaisingEvents = false; diff --git a/DXMainClient/Startup.cs b/DXMainClient/Startup.cs index 0cd1f7c61..a9e757577 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -79,7 +79,7 @@ public void Execute() if (ClientConfiguration.Instance.CreateSavedGamesDirectory) { - DirectoryInfo savedGamesFolder = SafePath.GetDirectory(ProgramConstants.GamePath, "Saved Games"); + DirectoryInfo savedGamesFolder = SafePath.GetDirectory(ProgramConstants.GamePath, ProgramConstants.SAVED_GAMES_DIRECTORY); if (!savedGamesFolder.Exists) { From 3f85b94e3d04f741fa81ebe7f391d1fc18aa3691 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 21:47:49 +0100 Subject: [PATCH 041/109] Exception logging --- ClientCore/ClientConfiguration.cs | 3 +- .../Extensions/TaskExtensions.cs | 26 ++++- .../PreprocessorBackgroundTask.cs | 3 +- ClientCore/ProgramConstants.cs | 84 ++++++++++++++ ClientCore/SavedGameManager.cs | 6 +- .../GameParsers/LogFileStatisticsParser.cs | 2 +- ClientCore/Statistics/StatisticsManager.cs | 4 +- ClientGUI/GameProcessLogic.cs | 4 +- DTAConfig/OptionPanels/DisplayOptionsPanel.cs | 15 ++- DTAConfig/OptionsWindow.cs | 2 +- DXMainClient/DXGUI/GameClass.cs | 49 ++++---- DXMainClient/DXGUI/Generic/ExtrasWindow.cs | 2 +- .../DXGUI/Generic/GameInProgressWindow.cs | 32 ++++-- DXMainClient/DXGUI/Generic/LoadingScreen.cs | 1 + DXMainClient/DXGUI/Generic/MainMenu.cs | 22 ++-- DXMainClient/DXGUI/Generic/TopBar.cs | 1 + DXMainClient/DXGUI/Generic/UpdateWindow.cs | 8 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 1 + .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 3 +- .../Multiplayer/CnCNet/GlobalContextMenu.cs | 3 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 1 + .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 1 + .../Multiplayer/GameLobby/GameLobbyBase.cs | 8 +- .../Multiplayer/GameLobby/LANGameLobby.cs | 17 +-- .../GameLobby/MultiplayerGameLobby.cs | 1 + .../Multiplayer/GameLobby/SkirmishLobby.cs | 2 +- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 13 ++- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 7 +- DXMainClient/Domain/FinalSunSettings.cs | 7 +- DXMainClient/Domain/MainClientConstants.cs | 51 --------- .../CnCNet/CnCNetPlayerCountTask.cs | 3 +- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 9 +- .../Multiplayer/CnCNet/GameTunnelHandler.cs | 1 + .../Domain/Multiplayer/CnCNet/MapSharer.cs | 7 +- .../Multiplayer/CnCNet/TunnelHandler.cs | 13 ++- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 5 +- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 4 +- DXMainClient/Domain/Multiplayer/Map.cs | 24 ++-- DXMainClient/Domain/Multiplayer/MapLoader.cs | 3 +- .../Domain/Multiplayer/MapPreviewExtractor.cs | 14 ++- .../Domain/Multiplayer/MultiplayerColor.cs | 4 +- DXMainClient/Domain/SavedGame.cs | 3 +- DXMainClient/Online/CnCNetGameCheck.cs | 22 +++- DXMainClient/Online/CnCNetManager.cs | 1 + DXMainClient/Online/CnCNetUserData.cs | 12 +- DXMainClient/Online/Connection.cs | 13 ++- DXMainClient/PreStartup.cs | 106 +++++------------- DXMainClient/Startup.cs | 60 +++++----- 48 files changed, 368 insertions(+), 315 deletions(-) rename {DXMainClient => ClientCore}/Extensions/TaskExtensions.cs (59%) delete mode 100644 DXMainClient/Domain/MainClientConstants.cs diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 0dd3b14ec..4f564082b 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -400,7 +400,8 @@ public OSVersion GetOperatingSystemVersion() /// public class ClientConfigurationException : Exception { - public ClientConfigurationException(string message) : base(message) + public ClientConfigurationException(string message, Exception ex = null) + : base(message, ex) { } } diff --git a/DXMainClient/Extensions/TaskExtensions.cs b/ClientCore/Extensions/TaskExtensions.cs similarity index 59% rename from DXMainClient/Extensions/TaskExtensions.cs rename to ClientCore/Extensions/TaskExtensions.cs index 3a99d5968..16bdc1776 100644 --- a/DXMainClient/Extensions/TaskExtensions.cs +++ b/ClientCore/Extensions/TaskExtensions.cs @@ -1,9 +1,9 @@ using System; using System.Threading.Tasks; -namespace DTAClient; +namespace ClientCore.Extensions; -internal static class TaskExtensions +public static class TaskExtensions { /// /// Asynchronously awaits a and guarantees all exceptions are caught and handled when the is not directly awaited. @@ -18,10 +18,30 @@ public static async Task HandleTaskAsync(this Task task) } catch (Exception ex) { - PreStartup.HandleException(ex); + ProgramConstants.HandleException(ex); } } + /// + /// Asynchronously awaits a and guarantees all exceptions are caught and handled when the is not directly awaited. + /// + /// The type of 's return value. + /// The who's exceptions will be handled. + /// Returns a that awaited and handled the original . + public static async Task HandleTaskAsync(this Task task) + { + try + { + return await task; + } + catch (Exception ex) + { + ProgramConstants.HandleException(ex); + } + + return default; + } + /// /// Synchronously awaits a and guarantees all exceptions are caught and handled when the is not directly awaited. /// diff --git a/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs b/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs index 159928db0..127c56f08 100644 --- a/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs +++ b/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs @@ -2,6 +2,7 @@ using System.IO; using System.Threading.Tasks; using System.Collections.Generic; +using ClientCore.Extensions; namespace ClientCore.INIProcessing { @@ -33,7 +34,7 @@ public static PreprocessorBackgroundTask Instance public void Run() { - task = Task.Run(CheckFiles); + task = Task.Run(CheckFiles).HandleTaskAsync(); } private static void CheckFiles() diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index 457753a8d..fc260194f 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -27,6 +27,12 @@ public static class ProgramConstants #endif public static string ClientUserFilesPath => SafePath.CombineDirectoryPath(GamePath, "Client"); + public static string CREDITS_URL = string.Empty; + public static bool USE_ISOMETRIC_CELLS = true; + public static int TDRA_WAYPOINT_COEFFICIENT = 128; + public static int MAP_CELL_SIZE_X = 48; + public static int MAP_CELL_SIZE_Y = 24; + public static OSVersion OSId = OSVersion.UNKNOWN; public static event EventHandler PlayerNameChanged; @@ -58,11 +64,15 @@ public static class ProgramConstants /// public const string INI_NEWLINE_PATTERN = "@"; + public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/master-list"; public const int GAME_ID_MAX_LENGTH = 4; public static readonly Encoding LAN_ENCODING = Encoding.UTF8; public static string GAME_VERSION = "Undefined"; + public static string GAME_NAME_LONG = "CnCNet Client"; + public static string GAME_NAME_SHORT = "CnCNet"; + public static string SUPPORT_URL_SHORT = "www.cncnet.org"; private static string PlayerName = "No name"; public static string PLAYERNAME @@ -127,5 +137,79 @@ public static string GetAILevelName(int aiLevel) if (exit) Environment.Exit(1); }; + + /// + /// Logs all details of an exception to the logfile without further action. + /// + /// The to log. + /// /// Optional message to accompany the error. + public static void LogException(Exception ex, string message = null) + { + LogExceptionRecursive(ex, message); + } + + private static void LogExceptionRecursive(Exception ex, string message = null, bool innerException = false) + { + if (!innerException) + Logger.Log(message); + else + Logger.Log("InnerException info:"); + + Logger.Log("Type: " + ex.GetType()); + Logger.Log("Message: " + ex.Message); + Logger.Log("Source: " + ex.Source); + Logger.Log("TargetSite.Name: " + ex.TargetSite?.Name); + Logger.Log("Stacktrace: " + ex.StackTrace); + + if (ex is AggregateException aggregateException) + { + foreach (Exception aggregateExceptionInnerException in aggregateException.InnerExceptions) + { + LogExceptionRecursive(aggregateExceptionInnerException, null, true); + } + } + else if (ex.InnerException is not null) + { + LogExceptionRecursive(ex.InnerException, null, true); + } + } + + /// + /// Logs all details of an exception to the logfile, notifies the user, and exits the application. + /// + /// The to log. + public static void HandleException(Exception ex) + { + LogExceptionRecursive(ex, "KABOOOOOOM!!! Info:"); + + string errorLogPath = SafePath.CombineFilePath(ClientUserFilesPath, "ClientCrashLogs", FormattableString.Invariant($"ClientCrashLog{DateTime.Now.ToString("_yyyy_MM_dd_HH_mm")}.txt")); + bool crashLogCopied = false; + + try + { + DirectoryInfo crashLogsDirectoryInfo = SafePath.GetDirectory(ClientUserFilesPath, "ClientCrashLogs"); + + if (!crashLogsDirectoryInfo.Exists) + crashLogsDirectoryInfo.Create(); + + File.Copy(SafePath.CombineFilePath(ClientUserFilesPath, "client.log"), errorLogPath, true); + crashLogCopied = true; + } + catch + { + } + + string error = string.Format("{0} has crashed. Error message:".L10N("Client:Main:FatalErrorText1") + Environment.NewLine + Environment.NewLine + + ex.Message + Environment.NewLine + Environment.NewLine + (crashLogCopied ? + "A crash log has been saved to the following file:".L10N("Client:Main:FatalErrorText2") + " " + Environment.NewLine + Environment.NewLine + + errorLogPath + Environment.NewLine + Environment.NewLine : "") + + (crashLogCopied ? "If the issue is repeatable, contact the {1} staff at {2} and provide the crash log file.".L10N("Client:Main:FatalErrorText3") : + "If the issue is repeatable, contact the {1} staff at {2}.".L10N("Client:Main:FatalErrorText4")), + GAME_NAME_LONG, + GAME_NAME_SHORT, + SUPPORT_URL_SHORT); + + DisplayErrorAction("KABOOOOOOOM".L10N("Client:Main:FatalErrorTitle"), error, true); + } } } \ No newline at end of file diff --git a/ClientCore/SavedGameManager.cs b/ClientCore/SavedGameManager.cs index d65fb6c59..8b6a8951c 100644 --- a/ClientCore/SavedGameManager.cs +++ b/ClientCore/SavedGameManager.cs @@ -81,7 +81,7 @@ public static bool InitSavedGames() } catch (Exception ex) { - Logger.Log("Writing spawn.ini for saved game failed! Exception message: " + ex.Message); + ProgramConstants.LogException(ex, "Writing spawn.ini for saved game failed!"); return false; } @@ -138,7 +138,7 @@ public static void RenameSavedGame() } catch (Exception ex) { - Logger.Log("Renaming saved game failed! Exception message: " + ex.Message); + ProgramConstants.LogException(ex, "Renaming saved game failed!"); } tryCount++; @@ -170,7 +170,7 @@ public static bool EraseSavedGames() } catch (Exception ex) { - Logger.Log("Erasing previous MP saved games failed! Exception message: " + ex.Message); + ProgramConstants.LogException(ex, "Erasing previous MP saved games failed!"); return false; } diff --git a/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs b/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs index 103edf0bb..5067e130e 100644 --- a/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs +++ b/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs @@ -150,7 +150,7 @@ protected override void ParseStatistics(string gamepath) } catch (Exception ex) { - Logger.Log("DTAStatisticsParser: Error parsing statistics from match! Message: " + ex.Message); + ProgramConstants.LogException(ex, "DTAStatisticsParser: Error parsing statistics from match!"); } } } diff --git a/ClientCore/Statistics/StatisticsManager.cs b/ClientCore/Statistics/StatisticsManager.cs index 787ee730c..6cd558321 100644 --- a/ClientCore/Statistics/StatisticsManager.cs +++ b/ClientCore/Statistics/StatisticsManager.cs @@ -107,7 +107,7 @@ private bool ReadFile(string filePath) } catch (Exception ex) { - Logger.Log("Error reading statistics: " + ex.Message); + ProgramConstants.LogException(ex, "Error reading statistics."); } return returnValue; @@ -259,7 +259,7 @@ private void ReadDatabase(string filePath, int version) } catch (Exception ex) { - Logger.Log("Reading the statistics file failed! Message: " + ex.Message); + ProgramConstants.LogException(ex, "Reading the statistics file failed!"); } } diff --git a/ClientGUI/GameProcessLogic.cs b/ClientGUI/GameProcessLogic.cs index d725da2f8..a1e0965c6 100644 --- a/ClientGUI/GameProcessLogic.cs +++ b/ClientGUI/GameProcessLogic.cs @@ -94,7 +94,7 @@ public static void StartGameProcess(WindowManager windowManager) } catch (Exception ex) { - Logger.Log("Error launching QRes: " + ex.Message); + ProgramConstants.LogException(ex, "Error launching QRes"); XNAMessageBox.Show(windowManager, "Error launching game", "Error launching " + ProgramConstants.QRES_EXECUTABLE + ". Please check that your anti-virus isn't blocking the CnCNet Client. " + "You can also try running the client as an administrator." + Environment.NewLine + Environment.NewLine + "You are unable to participate in this match." + Environment.NewLine + Environment.NewLine + "Returned error: " + ex.Message); @@ -133,7 +133,7 @@ public static void StartGameProcess(WindowManager windowManager) } catch (Exception ex) { - Logger.Log("Error launching " + gameFileInfo.Name + ": " + ex.Message); + ProgramConstants.LogException(ex, "Error launching " + gameFileInfo.Name); XNAMessageBox.Show(windowManager, "Error launching game", "Error launching " + gameFileInfo.Name + ". Please check that your anti-virus isn't blocking the CnCNet Client. " + "You can also try running the client as an administrator." + Environment.NewLine + Environment.NewLine + "You are unable to participate in this match." + Environment.NewLine + Environment.NewLine + "Returned error: " + ex.Message); diff --git a/DTAConfig/OptionPanels/DisplayOptionsPanel.cs b/DTAConfig/OptionPanels/DisplayOptionsPanel.cs index f69439ad2..bdb26855f 100644 --- a/DTAConfig/OptionPanels/DisplayOptionsPanel.cs +++ b/DTAConfig/OptionPanels/DisplayOptionsPanel.cs @@ -441,10 +441,13 @@ private void MessageBox_NoClicked(XNAMessageBox messageBox) } catch (Exception ex) { - Logger.Log("Setting TSCompatFixDeclined failed! Returned error: " + ex.Message); + ProgramConstants.LogException(ex, "Setting TSCompatFixDeclined failed!"); } } - catch { } + catch (Exception ex) + { + ProgramConstants.LogException(ex); + } } [SupportedOSPlatform("windows")] @@ -478,7 +481,7 @@ private void BtnGameCompatibilityFix_LeftClick(object sender, EventArgs e) } catch (Exception ex) { - Logger.Log("Uninstalling DTA/TI/TS Compatibility Fix failed. Error message: " + ex.Message); + ProgramConstants.LogException(ex, "Uninstalling DTA/TI/TS Compatibility Fix failed."); XNAMessageBox.Show(WindowManager, "Uninstalling Compatibility Fix Failed".L10N("Client:DTAConfig:TSFixUninstallFailTitle"), "Uninstalling DTA/TI/TS Compatibility Fix failed. Returned error:".L10N("Client:DTAConfig:TSFixUninstallFailText") + " " + ex.Message); } @@ -506,7 +509,7 @@ private void BtnGameCompatibilityFix_LeftClick(object sender, EventArgs e) } catch (Exception ex) { - Logger.Log("Installing DTA/TI/TS Compatibility Fix failed. Error message: " + ex.Message); + ProgramConstants.LogException(ex, "Installing DTA/TI/TS Compatibility Fix failed."); XNAMessageBox.Show(WindowManager, "Installing Compatibility Fix Failed".L10N("Client:DTAConfig:TSFixInstallFailTitle"), "Installing DTA/TI/TS Compatibility Fix failed. Error message:".L10N("Client:DTAConfig:TSFixInstallFailText") + " " + ex.Message); } @@ -537,7 +540,7 @@ private void BtnMapEditorCompatibilityFix_LeftClick(object sender, EventArgs e) } catch (Exception ex) { - Logger.Log("Uninstalling FinalSun Compatibility Fix failed. Error message: " + ex.Message); + ProgramConstants.LogException(ex, "Uninstalling FinalSun Compatibility Fix failed."); XNAMessageBox.Show(WindowManager, "Uninstalling Compatibility Fix Failed".L10N("Client:DTAConfig:TSFinalSunFixUninstallFailedTitle"), "Uninstalling FinalSun Compatibility Fix failed. Error message:".L10N("Client:DTAConfig:TSFinalSunFixUninstallFailedText") + " " + ex.Message); } @@ -565,7 +568,7 @@ private void BtnMapEditorCompatibilityFix_LeftClick(object sender, EventArgs e) } catch (Exception ex) { - Logger.Log("Installing FinalSun Compatibility Fix failed. Error message: " + ex.Message); + ProgramConstants.LogException(ex, "Installing FinalSun Compatibility Fix failed."); XNAMessageBox.Show(WindowManager, "Installing Compatibility Fix Failed".L10N("Client:DTAConfig:TSFinalSunCompatibilityFixInstalledFailedTitle"), "Installing FinalSun Compatibility Fix failed. Error message:".L10N("Client:DTAConfig:TSFinalSunCompatibilityFixInstalledFailedText") + " " + ex.Message); } diff --git a/DTAConfig/OptionsWindow.cs b/DTAConfig/OptionsWindow.cs index 60a2ac597..4b7718e9a 100644 --- a/DTAConfig/OptionsWindow.cs +++ b/DTAConfig/OptionsWindow.cs @@ -191,7 +191,7 @@ private void SaveSettings() } catch (Exception ex) { - Logger.Log("Saving settings failed! Error message: " + ex.Message); + ProgramConstants.LogException(ex, "Saving settings failed!"); XNAMessageBox.Show(WindowManager, "Saving Settings Failed".L10N("Client:DTAConfig:SaveSettingFailTitle"), "Saving settings failed! Error message:".L10N("Client:DTAConfig:SaveSettingFailText") + " " + ex.Message); } diff --git a/DXMainClient/DXGUI/GameClass.cs b/DXMainClient/DXGUI/GameClass.cs index 534f28b7b..f64232e18 100644 --- a/DXMainClient/DXGUI/GameClass.cs +++ b/DXMainClient/DXGUI/GameClass.cs @@ -59,7 +59,7 @@ protected override void Initialize() string windowTitle = ClientConfiguration.Instance.WindowTitle; Window.Title = string.IsNullOrEmpty(windowTitle) ? - string.Format("{0} Client", MainClientConstants.GAME_NAME_SHORT) : windowTitle; + string.Format("{0} Client", ProgramConstants.GAME_NAME_SHORT) : windowTitle; base.Initialize(); @@ -86,41 +86,34 @@ protected override void Initialize() _ = AssetLoader.LoadTextureUncached("checkBoxClear.png"); } - catch (Exception ex) + catch (Exception ex) when (ex.Message.Contains("DeviceRemoved")) { - if (ex.Message.Contains("DeviceRemoved")) - { - Logger.Log($"Creating texture on startup failed! Creating {startupFailureFile} file and re-launching client launcher."); + ProgramConstants.LogException(ex, $"Creating texture on startup failed! Creating {startupFailureFile} file and re-launching client launcher."); - DirectoryInfo clientDirectory = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath); + DirectoryInfo clientDirectory = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath); - if (!clientDirectory.Exists) - clientDirectory.Create(); + if (!clientDirectory.Exists) + clientDirectory.Create(); - // Create startup failure file that the launcher can check for this error - // and handle it by redirecting the user to another version instead + // Create startup failure file that the launcher can check for this error + // and handle it by redirecting the user to another version instead + File.WriteAllBytes(SafePath.CombineFilePath(clientDirectory.FullName, startupFailureFile), new byte[] { 1 }); - File.WriteAllBytes(SafePath.CombineFilePath(clientDirectory.FullName, startupFailureFile), new byte[] { 1 }); + string launcherExe = ClientConfiguration.Instance.LauncherExe; + if (string.IsNullOrEmpty(launcherExe)) + { + // LauncherExe is unspecified, just throw the exception forward + // because we can't handle it + Logger.Log("No LauncherExe= specified in ClientDefinitions.ini! " + + "Forwarding exception to regular exception handler."); - string launcherExe = ClientConfiguration.Instance.LauncherExe; - if (string.IsNullOrEmpty(launcherExe)) - { - // LauncherExe is unspecified, just throw the exception forward - // because we can't handle it + throw; + } - Logger.Log("No LauncherExe= specified in ClientDefinitions.ini! " + - "Forwarding exception to regular exception handler."); + Logger.Log("Starting " + launcherExe + " and exiting."); - throw; - } - else - { - Logger.Log("Starting " + launcherExe + " and exiting."); - - Process.Start(SafePath.CombineFilePath(ProgramConstants.GamePath, launcherExe)); - Environment.Exit(1); - } - } + Process.Start(SafePath.CombineFilePath(ProgramConstants.GamePath, launcherExe)); + Environment.Exit(1); } #endif diff --git a/DXMainClient/DXGUI/Generic/ExtrasWindow.cs b/DXMainClient/DXGUI/Generic/ExtrasWindow.cs index 8a72062d2..88d425aed 100644 --- a/DXMainClient/DXGUI/Generic/ExtrasWindow.cs +++ b/DXMainClient/DXGUI/Generic/ExtrasWindow.cs @@ -80,7 +80,7 @@ private void BtnExMapEditor_LeftClick(object sender, EventArgs e) private void BtnExCredits_LeftClick(object sender, EventArgs e) { - ProcessLauncher.StartShellProcess(MainClientConstants.CREDITS_URL); + ProcessLauncher.StartShellProcess(ProgramConstants.CREDITS_URL); } private void BtnExCancel_LeftClick(object sender, EventArgs e) diff --git a/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs b/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs index 38021a960..c3cd0a7e8 100644 --- a/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs @@ -9,6 +9,7 @@ using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; #if ARES +using ClientCore.Extensions; using System.Linq; using System.Collections.Generic; using System.Threading.Tasks; @@ -87,15 +88,18 @@ public override void Initialize() if (debugLogFileInfo.Exists) debugLogLastWriteTime = debugLogFileInfo.LastWriteTime; } - catch { } + catch (Exception ex) + { + ProgramConstants.LogException(ex); + } #endif } private void SharedUILogic_GameProcessStarted() { - #if ARES debugSnapshotDirectories = GetAllDebugSnapshotDirectories(); + #else try { @@ -108,7 +112,7 @@ private void SharedUILogic_GameProcessStarted() } catch (Exception ex) { - Logger.Log("Exception when deleting error log files! Message: " + ex.Message); + ProgramConstants.LogException(ex, "Exception when deleting error log files!"); deletingLogFilesFailed = true; } #endif @@ -166,7 +170,7 @@ private void HandleGameProcessExited() DateTime dtn = DateTime.Now; #if ARES - Task.Run(ProcessScreenshots); + Task.Run(ProcessScreenshots).HandleTask(); // TODO: Ares debug log handling should be addressed in Ares DLL itself. // For now the following are handled here: @@ -246,8 +250,9 @@ private bool CopyErrorLog(string directory, string filename, DateTime? dateTime) } catch (Exception ex) { - Logger.Log("An error occured while checking for " + filename + " file. Message: " + ex.Message); + ProgramConstants.LogException(ex, "An error occurred while checking for " + filename + " file."); } + return copied; } @@ -290,8 +295,9 @@ private bool CopySyncErrorLogs(string directory, DateTime? dateTime) } catch (Exception ex) { - Logger.Log("An error occured while checking for SYNCX.TXT files. Message: " + ex.Message); + ProgramConstants.LogException(ex, "An error occured while checking for SYNCX.TXT files."); } + return copied; } @@ -319,7 +325,10 @@ private string GetNewestDebugSnapshotDirectory() { Directory.Delete(directory); } - catch { } + catch (Exception ex) + { + ProgramConstants.LogException(ex); + } } } } @@ -339,7 +348,10 @@ private List GetAllDebugSnapshotDirectories() { directories.AddRange(Directory.GetDirectories(SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "debug"), "snapshot-*")); } - catch { } + catch (Exception ex) + { + ProgramConstants.LogException(ex); + } return directories; } @@ -360,7 +372,7 @@ private void ProcessScreenshots() } catch (Exception ex) { - Logger.Log("ProcessScreenshots: An error occured trying to create Screenshots directory. Message: " + ex.Message); + ProgramConstants.LogException(ex, "ProcessScreenshots: An error occured trying to create Screenshots directory."); return; } } @@ -378,7 +390,7 @@ private void ProcessScreenshots() } catch (Exception ex) { - Logger.Log("ProcessScreenshots: Error occured when trying to save " + Path.GetFileNameWithoutExtension(file.FullName) + ".png. Message: " + ex.Message); + ProgramConstants.LogException(ex, "ProcessScreenshots: Error occured when trying to save " + Path.GetFileNameWithoutExtension(file.FullName) + ".png."); continue; } diff --git a/DXMainClient/DXGUI/Generic/LoadingScreen.cs b/DXMainClient/DXGUI/Generic/LoadingScreen.cs index b75ae0286..340e9c0d2 100644 --- a/DXMainClient/DXGUI/Generic/LoadingScreen.cs +++ b/DXMainClient/DXGUI/Generic/LoadingScreen.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using ClientCore; using ClientCore.CnCNet5; +using ClientCore.Extensions; using ClientGUI; using ClientUpdater; using DTAClient.Domain.Multiplayer; diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 2f14eae39..0c9a3f3f5 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -21,6 +21,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using ClientCore.Extensions; using ClientUpdater; using DTAClient.Domain.Multiplayer; @@ -378,10 +379,7 @@ private void SharedUILogic_GameProcessStarting() } catch (Exception ex) { - Logger.Log("Refreshing settings failed! Exception message: " + ex.Message); - // We don't want to show the dialog when starting a game - //XNAMessageBox.Show(WindowManager, "Saving settings failed", - // "Saving settings failed! Error message: " + ex.Message); + ProgramConstants.LogException(ex, "Refreshing settings failed!"); } } @@ -644,7 +642,7 @@ private void UpdateWindow_UpdateFailed(object sender, UpdateFailureEventArgs e) innerPanel.Show(null); // Darkening XNAMessageBox msgBox = new XNAMessageBox(WindowManager, "Update failed".L10N("Client:Main:UpdateFailedTitle"), string.Format(("An error occured while updating. Returned error was: {0}\n\nIf you are connected to the Internet and your firewall isn't blocking\n{1}, and the issue is reproducible, contact us at\n{2} for support.").L10N("Client:Main:UpdateFailedText"), - e.Reason, Path.GetFileName(ProgramConstants.StartupExecutable), MainClientConstants.SUPPORT_URL_SHORT), XNAMessageBoxButtons.OK); + e.Reason, Path.GetFileName(ProgramConstants.StartupExecutable), ProgramConstants.SUPPORT_URL_SHORT), XNAMessageBoxButtons.OK); msgBox.OKClickedAction = MsgBox_OKClicked; msgBox.Show(); } @@ -667,7 +665,7 @@ private void UpdateWindow_UpdateCompleted(object sender, EventArgs e) { innerPanel.Hide(); lblUpdateStatus.Text = string.Format("{0} was succesfully updated to v.{1}".L10N("Client:Main:UpdateSuccess"), - MainClientConstants.GAME_NAME_SHORT, Updater.GameVersion); + ProgramConstants.GAME_NAME_SHORT, Updater.GameVersion); lblVersion.Text = Updater.GameVersion; UpdateInProgress = false; lblUpdateStatus.Enabled = true; @@ -731,7 +729,7 @@ private void HandleFileIdentifierUpdate() if (Updater.VersionState == VersionState.UPTODATE) { - lblUpdateStatus.Text = string.Format("{0} is up to date.".L10N("Client:Main:GameUpToDate"), MainClientConstants.GAME_NAME_SHORT); + lblUpdateStatus.Text = string.Format("{0} is up to date.".L10N("Client:Main:GameUpToDate"), ProgramConstants.GAME_NAME_SHORT); lblUpdateStatus.Enabled = true; lblUpdateStatus.DrawUnderline = false; } @@ -863,7 +861,7 @@ private void BtnStatistics_LeftClick(object sender, EventArgs e) => private void BtnCredits_LeftClick(object sender, EventArgs e) { - ProcessLauncher.StartShellProcess(MainClientConstants.CREDITS_URL); + ProcessLauncher.StartShellProcess(ProgramConstants.CREDITS_URL); } private void BtnExtras_LeftClick(object sender, EventArgs e) => @@ -940,7 +938,7 @@ private void PlayMusic() } catch (InvalidOperationException ex) { - Logger.Log("Playing main menu music failed! " + ex.Message); + ProgramConstants.LogException(ex, "Playing main menu music failed!"); } } } @@ -1035,7 +1033,7 @@ private void MusicOff() } catch (Exception ex) { - Logger.Log("Turning music off failed! Message: " + ex.Message); + ProgramConstants.LogException(ex, "Turning music off failed!"); } } @@ -1051,9 +1049,9 @@ private bool IsMediaPlayerAvailable() MediaState state = MediaPlayer.State; return true; } - catch (Exception e) + catch (Exception ex) { - Logger.Log("Error encountered when checking media player availability. Error message: " + e.Message); + ProgramConstants.LogException(ex, "Error encountered when checking media player availability."); return false; } } diff --git a/DXMainClient/DXGUI/Generic/TopBar.cs b/DXMainClient/DXGUI/Generic/TopBar.cs index 7a7e8ba35..7ffae4692 100644 --- a/DXMainClient/DXGUI/Generic/TopBar.cs +++ b/DXMainClient/DXGUI/Generic/TopBar.cs @@ -10,6 +10,7 @@ using ClientCore; using System.Threading; using System.Threading.Tasks; +using ClientCore.Extensions; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.Online.EventArguments; using DTAConfig; diff --git a/DXMainClient/DXGUI/Generic/UpdateWindow.cs b/DXMainClient/DXGUI/Generic/UpdateWindow.cs index 01eb67033..122f5fc6a 100644 --- a/DXMainClient/DXGUI/Generic/UpdateWindow.cs +++ b/DXMainClient/DXGUI/Generic/UpdateWindow.cs @@ -5,6 +5,7 @@ using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; using System; +using ClientCore; #if WINFORMS using System.Runtime.InteropServices; #endif @@ -227,8 +228,9 @@ private void HandleUpdateProgressChange() tbp.SetState(WindowManager.GetWindowHandle(), TaskbarProgress.TaskbarStates.Normal); tbp.SetValue(WindowManager.GetWindowHandle(), prgTotal.Value, prgTotal.Maximum); } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); } #endif } @@ -289,14 +291,14 @@ private void CloseWindow() public void SetData(string newGameVersion) { - lblDescription.Text = string.Format(("Please wait while {0} is updated to version {1}.\nThis window will automatically close once the update is complete.\n\nThe client may also restart after the update has been downloaded.").L10N("Client:Main:UpdateVersionPleaseWait"), MainClientConstants.GAME_NAME_SHORT, newGameVersion); + lblDescription.Text = string.Format(("Please wait while {0} is updated to version {1}.\nThis window will automatically close once the update is complete.\n\nThe client may also restart after the update has been downloaded.").L10N("Client:Main:UpdateVersionPleaseWait"), ProgramConstants.GAME_NAME_SHORT, newGameVersion); lblUpdaterStatus.Text = "Preparing".L10N("Client:Main:StatusPreparing"); } public void ForceUpdate() { isStartingForceUpdate = true; - lblDescription.Text = string.Format("Force updating {0} to latest version...".L10N("Client:Main:ForceUpdateToLatest"), MainClientConstants.GAME_NAME_SHORT); + lblDescription.Text = string.Format("Force updating {0} to latest version...".L10N("Client:Main:ForceUpdateToLatest"), ProgramConstants.GAME_NAME_SHORT); lblUpdaterStatus.Text = "Connecting".L10N("Client:Main:UpdateStatusConnecting"); Updater.CheckForUpdates(); } diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index 5ec64e704..de37794f5 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.DXGUI.Multiplayer.CnCNet { diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index cff05b4af..865da1e60 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -1527,11 +1527,12 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr lbGameList.AddGame(game); } + SortAndRefreshHostedGames(); } catch (Exception ex) { - Logger.Log("Game parsing error: " + ex.Message); + ProgramConstants.LogException(ex, "Game parsing error"); } } diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs index befea5e1a..59886277e 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs @@ -179,8 +179,9 @@ private void CopyLink(string link) { ClipboardService.SetText(link); } - catch (Exception) + catch (Exception ex) { + ProgramConstants.LogException(ex, "Unable to copy link."); XNAMessageBox.Show(WindowManager, "Error".L10N("Client:Main:Error"), "Unable to copy link".L10N("Client:Main:ClipboardCopyLinkFailed")); } } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index 48d68cf8b..a4678529f 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.DXGUI.Multiplayer { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 374e1df54..59a97de96 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -19,6 +19,7 @@ using System.Net; using System.Text; using System.Threading.Tasks; +using ClientCore.Extensions; using DTAClient.Domain.Multiplayer.CnCNet; using ClientCore.Extensions; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index f0fd40013..45eb9ca72 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -15,6 +15,7 @@ using System.Net; using System.Threading.Tasks; using ClientCore.Enums; +using ClientCore.Extensions; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.Online.EventArguments; using ClientCore.Extensions; @@ -665,7 +666,7 @@ private async Task DeleteSelectedMapAsync() } catch (IOException ex) { - Logger.Log($"Deleting map {Map.BaseFilePath} failed! Message: {ex.Message}"); + ProgramConstants.LogException(ex, $"Deleting map {Map.BaseFilePath} failed!"); XNAMessageBox.Show(WindowManager, "Deleting Map Failed".L10N("Client:Main:DeleteMapFailedTitle"), "Deleting map failed! Reason:".L10N("Client:Main:DeleteMapFailedText") + " " + ex.Message); } @@ -991,7 +992,10 @@ private void GetRandomSelectors(List selectorNames, List selector randomSides = Array.ConvertAll(tmp, int.Parse).Distinct().ToList(); randomSides.RemoveAll(x => (x >= SideCount || x < 0)); } - catch (FormatException) { } + catch (FormatException ex) + { + ProgramConstants.LogException(ex); + } if (randomSides.Count > 1) { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index a94024e8c..d18fd8f3a 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -19,6 +19,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.DXGUI.Multiplayer.GameLobby { @@ -228,7 +229,7 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) } catch (Exception ex) { - PreStartup.LogException(ex, "Listener error."); + ProgramConstants.LogException(ex, "Listener error."); break; } @@ -275,7 +276,7 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio } catch (Exception ex) { - PreStartup.LogException(ex, "Socket error with client " + lpInfo.IPAddress + "; removing."); + ProgramConstants.LogException(ex, "Socket error with client " + lpInfo.IPAddress + "; removing."); break; } @@ -401,7 +402,7 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation } catch (Exception ex) { - Logger.Log("Reading data from the server failed! Message: " + ex.Message); + ProgramConstants.LogException(ex, "Reading data from the server failed!"); await BtnLeaveGame_LeftClickAsync(); break; } @@ -682,7 +683,7 @@ private async Task SendMessageToHostAsync(string message, CancellationToken canc } catch (Exception ex) { - PreStartup.LogException(ex, "Sending message to game host failed!"); + ProgramConstants.LogException(ex, "Sending message to game host failed!"); } } @@ -749,14 +750,14 @@ public override void Update(GameTime gameTime) for (int i = 1; i < Players.Count; i++) { LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i]; - if (!Task.Run(() => lpInfo.UpdateAsync(gameTime)).Result) + if (!Task.Run(() => lpInfo.UpdateAsync(gameTime).HandleTaskAsync()).Result) { CleanUpPlayer(lpInfo); Players.RemoveAt(i); AddNotice(string.Format("{0} - connection timed out".L10N("Client:Main:PlayerTimeout"), lpInfo.Name)); CopyPlayerDataToUI(); - Task.Run(BroadcastPlayerOptionsAsync).Wait(); - Task.Run(BroadcastPlayerExtraOptionsAsync).Wait(); + Task.Run(() => BroadcastPlayerOptionsAsync().HandleTaskAsync()).Wait(); + Task.Run(() => BroadcastPlayerExtraOptionsAsync().HandleTaskAsync()).Wait(); UpdateDiscordPresence(); i--; } @@ -775,7 +776,7 @@ public override void Update(GameTime gameTime) timeSinceLastReceivedCommand += gameTime.ElapsedGameTime; if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT)) - Task.Run(() => BtnLeaveGame_LeftClickAsync().HandleTask()).Wait(); + Task.Run(() => BtnLeaveGame_LeftClickAsync().HandleTaskAsync()).Wait(); } base.Update(gameTime); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 59eaf302f..25898e40a 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -13,6 +13,7 @@ using ClientGUI; using System.Text; using System.Threading.Tasks; +using ClientCore.Extensions; using DTAClient.Domain; using Microsoft.Xna.Framework.Graphics; using ClientCore.Extensions; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs index 1cdf7a786..e35785378 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -298,7 +298,7 @@ private void SaveSettings() } catch (Exception ex) { - Logger.Log("Saving skirmish settings failed! Reason: " + ex.Message); + ProgramConstants.LogException(ex, "Saving skirmish settings failed!"); } } diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index 8ac90ced3..9d13285f4 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -17,6 +17,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.DXGUI.Multiplayer { @@ -180,7 +181,7 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) } catch (Exception ex) { - PreStartup.LogException(ex, "Listener error."); + ProgramConstants.LogException(ex, "Listener error."); break; } @@ -213,7 +214,7 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio } catch (Exception ex) { - PreStartup.LogException(ex, "Socket error with client " + lpInfo.IPAddress + "; removing."); + ProgramConstants.LogException(ex, "Socket error with client " + lpInfo.IPAddress + "; removing."); break; } @@ -342,7 +343,7 @@ private async Task HandleServerCommunicationAsync(CancellationToken cancellation } catch (Exception ex) { - Logger.Log("Reading data from the server failed! Message: " + ex.Message); + ProgramConstants.LogException(ex, "Reading data from the server failed!"); await LeaveGameAsync(); break; } @@ -613,7 +614,7 @@ private async Task SendMessageToHostAsync(string message, CancellationToken canc } catch (Exception ex) { - PreStartup.LogException(ex, "Sending message to game host failed!"); + ProgramConstants.LogException(ex, "Sending message to game host failed!"); } } @@ -627,13 +628,13 @@ public override void Update(GameTime gameTime) for (int i = 1; i < Players.Count; i++) { LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i]; - if (!Task.Run(() => lpInfo.UpdateAsync(gameTime)).Result) + if (!Task.Run(() => lpInfo.UpdateAsync(gameTime).HandleTaskAsync()).Result) { CleanUpPlayer(lpInfo); Players.RemoveAt(i); AddNotice(string.Format("{0} - connection timed out".L10N("Client:Main:PlayerTimeout"), lpInfo.Name)); CopyPlayerDataToUI(); - Task.Run(BroadcastOptionsAsync).Wait(); + Task.Run(() => BroadcastOptionsAsync().HandleTaskAsync()).Wait(); UpdateDiscordPresence(); i--; } diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index bbb54affb..5fef4f205 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -24,6 +24,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using ClientCore.Extensions; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; @@ -324,7 +325,7 @@ public async Task OpenAsync() } catch (SocketException ex) { - PreStartup.LogException(ex, "Creating LAN socket failed!"); + ProgramConstants.LogException(ex, "Creating LAN socket failed!"); lbChatMessages.AddMessage(new ChatMessage(Color.Red, "Creating LAN socket failed! Message:".L10N("Client:Main:SocketFailure1") + " " + ex.Message)); lbChatMessages.AddMessage(new ChatMessage(Color.Red, @@ -389,7 +390,7 @@ private async Task ListenAsync(CancellationToken cancellationToken) } catch (Exception ex) { - PreStartup.LogException(ex, "LAN socket listener exception."); + ProgramConstants.LogException(ex, "LAN socket listener exception."); } } @@ -598,7 +599,7 @@ private async Task JoinGameAsync() } catch (Exception ex) { - PreStartup.LogException(ex, "Connecting to the game failed!"); + ProgramConstants.LogException(ex, "Connecting to the game failed!"); lbChatMessages.AddMessage(null, "Connecting to the game failed! Message:".L10N("Client:Main:ConnectGameFailed") + " " + ex.Message, Color.White); } diff --git a/DXMainClient/Domain/FinalSunSettings.cs b/DXMainClient/Domain/FinalSunSettings.cs index 867ae1a8f..1cb66c552 100644 --- a/DXMainClient/Domain/FinalSunSettings.cs +++ b/DXMainClient/Domain/FinalSunSettings.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using Rampastring.Tools; using ClientCore; using ClientCore.PlatformShim; @@ -58,9 +59,9 @@ public static void WriteFinalSunIni() sw.WriteLine("DisableAutoLat=0"); sw.WriteLine("ShowBuildingCells=0"); } - catch + catch (Exception ex) { - Logger.Log("An exception occurred while checking the existence of FinalSun settings"); + ProgramConstants.LogException(ex, "An exception occurred while checking the existence of FinalSun settings."); } } } diff --git a/DXMainClient/Domain/MainClientConstants.cs b/DXMainClient/Domain/MainClientConstants.cs deleted file mode 100644 index 5afdf092f..000000000 --- a/DXMainClient/Domain/MainClientConstants.cs +++ /dev/null @@ -1,51 +0,0 @@ -using ClientCore; - -namespace DTAClient.Domain -{ - public static class MainClientConstants - { - public const string CNCNET_TUNNEL_LIST_URL = "https://core-api.cncnet.org/tunnels/master-list"; - - public static string GAME_NAME_LONG = "CnCNet Client"; - public static string GAME_NAME_SHORT = "CnCNet"; - - public static string CREDITS_URL = string.Empty; - - public static string SUPPORT_URL_SHORT = "www.cncnet.org"; - - public static bool USE_ISOMETRIC_CELLS = true; - public static int TDRA_WAYPOINT_COEFFICIENT = 128; - public static int MAP_CELL_SIZE_X = 48; - public static int MAP_CELL_SIZE_Y = 24; - - public static OSVersion OSId = OSVersion.UNKNOWN; - - public static void Initialize() - { - var clientConfiguration = ClientConfiguration.Instance; - - OSId = clientConfiguration.GetOperatingSystemVersion(); - - GAME_NAME_SHORT = clientConfiguration.LocalGame; - GAME_NAME_LONG = clientConfiguration.LongGameName; - - SUPPORT_URL_SHORT = clientConfiguration.ShortSupportURL; - - CREDITS_URL = clientConfiguration.CreditsURL; - - USE_ISOMETRIC_CELLS = clientConfiguration.UseIsometricCells; - TDRA_WAYPOINT_COEFFICIENT = clientConfiguration.WaypointCoefficient; - MAP_CELL_SIZE_X = clientConfiguration.MapCellSizeX; - MAP_CELL_SIZE_Y = clientConfiguration.MapCellSizeY; - - if (string.IsNullOrEmpty(GAME_NAME_SHORT)) - throw new ClientConfigurationException("LocalGame is set to an empty value."); - - if (GAME_NAME_SHORT.Length > ProgramConstants.GAME_ID_MAX_LENGTH) - { - throw new ClientConfigurationException("LocalGame is set to a value that exceeds length limit of " + - ProgramConstants.GAME_ID_MAX_LENGTH + " characters."); - } - } - } -} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index 883571941..b6e1857e1 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -80,7 +81,7 @@ private static async Task GetCnCNetPlayerCountAsync() } catch (Exception ex) { - PreStartup.LogException(ex); + ProgramConstants.LogException(ex); return -1; } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 9b5685476..7453a5a33 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -7,6 +7,7 @@ using System.Net.Http; using System.Net.Sockets; using System.Threading.Tasks; +using ClientCore; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -27,7 +28,7 @@ internal sealed class CnCNetTunnel /// A CnCNetTunnel instance parsed from the given string. public static CnCNetTunnel Parse(string str) { - // For the format, check https://core-api.cncnet.org/tunnels/master-list + // For the format, check https://cncnet.org/master-list try { var tunnel = new CnCNetTunnel(); @@ -72,7 +73,7 @@ public static CnCNetTunnel Parse(string str) } catch (Exception ex) when (ex is FormatException or OverflowException or IndexOutOfRangeException) { - PreStartup.LogException(ex, "Parsing tunnel information failed. Parsed string: " + str); + ProgramConstants.LogException(ex, "Parsing tunnel information failed. Parsed string: " + str); return null; } } @@ -151,7 +152,7 @@ public async Task> GetPlayerPortInfoAsync(int playerCount) } catch (Exception ex) { - PreStartup.LogException(ex, "Unable to connect to the specified tunnel server."); + ProgramConstants.LogException(ex, "Unable to connect to the specified tunnel server."); } return new List(); @@ -182,7 +183,7 @@ public async Task UpdatePingAsync() } catch (SocketException ex) { - PreStartup.LogException(ex, $"Failed to ping tunnel {Name} ({Address}:{Port})."); + ProgramConstants.LogException(ex, $"Failed to ping tunnel {Name} ({Address}:{Port})."); PingInMs = -1; } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs index 34f0ba090..e2aeac9a4 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs @@ -5,6 +5,7 @@ using System.Net.NetworkInformation; using System.Threading; using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.Domain.Multiplayer.CnCNet { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs index 4afaeb8b8..653bd0303 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs @@ -9,6 +9,7 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using ClientCore; +using ClientCore.Extensions; using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet @@ -126,7 +127,7 @@ private static async Task UploadAsync(Map map, string myGameId) } catch (Exception ex) { - PreStartup.LogException(ex); + ProgramConstants.LogException(ex); return (ex.Message, false); } } @@ -218,7 +219,7 @@ private static async Task DownloadAsync(string sha1, string myGameId, string map } catch (Exception ex) { - PreStartup.LogException(ex, "MapSharer ERROR"); + ProgramConstants.LogException(ex, "MapSharer ERROR"); } (string error, bool success) = await DownloadMainAsync(sha1, myGameId, mapName); @@ -265,7 +266,7 @@ public static string GetMapFileName(string sha1, string mapName) } catch (Exception ex) { - PreStartup.LogException(ex); + ProgramConstants.LogException(ex); return (ex.Message, false); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index ceb43a427..188647b65 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using System.Linq; using System.Net.Http; +using ClientCore.Extensions; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -157,18 +158,18 @@ private static async Task> DoRefreshTunnelsAsync() try { - data = await client.GetStringAsync(MainClientConstants.CNCNET_TUNNEL_LIST_URL); + data = await client.GetStringAsync(ProgramConstants.CNCNET_TUNNEL_LIST_URL); } catch (HttpRequestException ex) { - PreStartup.LogException(ex, "Error when downloading tunnel server info. Retrying."); + ProgramConstants.LogException(ex, "Error when downloading tunnel server info. Retrying."); try { - data = await client.GetStringAsync(MainClientConstants.CNCNET_TUNNEL_LIST_URL); + data = await client.GetStringAsync(ProgramConstants.CNCNET_TUNNEL_LIST_URL); } catch (HttpRequestException ex1) { - PreStartup.LogException(ex1); + ProgramConstants.LogException(ex1); if (!tunnelCacheFile.Exists) { Logger.Log("Tunnel cache file doesn't exist!"); @@ -203,7 +204,7 @@ private static async Task> DoRefreshTunnelsAsync() } catch (Exception ex) { - PreStartup.LogException(ex, "Caught an exception when parsing a tunnel server."); + ProgramConstants.LogException(ex, "Caught an exception when parsing a tunnel server."); } } @@ -224,7 +225,7 @@ private static async Task> DoRefreshTunnelsAsync() } catch (Exception ex) { - PreStartup.LogException(ex, "Refreshing tunnel cache file failed!"); + ProgramConstants.LogException(ex, "Refreshing tunnel cache file failed!"); } return returnValue; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index bf0c5aa9d..1d88f17bd 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -5,6 +5,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using ClientCore; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -89,7 +90,7 @@ public async Task ConnectAsync() } catch (SocketException ex) { - PreStartup.LogException(ex, "Failed to establish connection to tunnel server."); + ProgramConstants.LogException(ex, "Failed to establish connection to tunnel server."); tunnelSocket.Close(); ConnectionFailed?.Invoke(this, EventArgs.Empty); return; @@ -132,7 +133,7 @@ private async Task ReceiveLoopAsync() } catch (SocketException ex) { - PreStartup.LogException(ex, "Socket exception in V3 tunnel receive loop."); + ProgramConstants.LogException(ex, "Socket exception in V3 tunnel receive loop."); DoClose(); ConnectionCut?.Invoke(this, EventArgs.Empty); } diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index 3c9a9aa90..4a8478913 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -106,7 +106,7 @@ public async Task SendMessageAsync(string message, CancellationToken cancellatio } catch (Exception ex) { - PreStartup.LogException(ex, "Sending message to " + ToString() + " failed!"); + ProgramConstants.LogException(ex, "Sending message to " + ToString() + " failed!"); } TimeSinceLastSentMessage = TimeSpan.Zero; @@ -138,7 +138,7 @@ public async Task StartReceiveLoopAsync(CancellationToken cancellationToken) } catch (Exception ex) { - PreStartup.LogException(ex, "Socket error with client " + Name + "; removing."); + ProgramConstants.LogException(ex, "Socket error with client " + Name + "; removing."); ConnectionLost?.Invoke(this, EventArgs.Empty); break; } diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index 2480d2ee6..37a117ede 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -11,6 +11,7 @@ using System.Text.Json.Serialization; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; +using Exception = System.Exception; using Point = Microsoft.Xna.Framework.Point; using Utilities = Rampastring.Tools.Utilities; using static System.Collections.Specialized.BitVector32; @@ -428,8 +429,7 @@ public bool SetInfoFromMpMapsINI(IniFile iniFile) } catch (Exception ex) { - Logger.Log("Setting info for " + BaseFilePath + " failed! Reason: " + ex.Message); - PreStartup.LogException(ex); + ProgramConstants.LogException(ex, "Setting info for " + BaseFilePath + " failed!"); return false; } } @@ -455,9 +455,9 @@ private void GetTeamStartMappingPresets(IniSection section) TeamStartMappings = TeamStartMapping.FromListString(teamStartMappingPreset) }); } - catch (Exception e) + catch (Exception ex) { - Logger.Log($"Unable to parse team start mappings. Map: \"{Name}\", Error: {e.Message}"); + ProgramConstants.LogException(ex, $"Unable to parse team start mappings. Map: \"{Name}\"."); TeamStartMappingPresets = new List(); } } @@ -628,9 +628,9 @@ public bool SetInfoFromCustomMap() return true; } - catch + catch (Exception ex) { - Logger.Log("Loading custom map " + customMapFilePath + " failed!"); + ProgramConstants.LogException(ex, "Loading custom map " + customMapFilePath + " failed!"); return false; } } @@ -891,15 +891,15 @@ private static Point GetIsoTilePixelCoord(int isoTileX, int isoTileY, string[] a int rx = isoTileX - isoTileY + Convert.ToInt32(actualSizeValues[2], CultureInfo.InvariantCulture) - 1; int ry = isoTileX + isoTileY - Convert.ToInt32(actualSizeValues[2], CultureInfo.InvariantCulture) - 1; - int pixelPosX = rx * MainClientConstants.MAP_CELL_SIZE_X / 2; - int pixelPosY = ry * MainClientConstants.MAP_CELL_SIZE_Y / 2 - level * MainClientConstants.MAP_CELL_SIZE_Y / 2; + int pixelPosX = rx * ProgramConstants.MAP_CELL_SIZE_X / 2; + int pixelPosY = ry * ProgramConstants.MAP_CELL_SIZE_Y / 2 - level * ProgramConstants.MAP_CELL_SIZE_Y / 2; - pixelPosX = pixelPosX - (Convert.ToInt32(localSizeValues[0], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_X); - pixelPosY = pixelPosY - (Convert.ToInt32(localSizeValues[1], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_Y); + pixelPosX = pixelPosX - (Convert.ToInt32(localSizeValues[0], CultureInfo.InvariantCulture) * ProgramConstants.MAP_CELL_SIZE_X); + pixelPosY = pixelPosY - (Convert.ToInt32(localSizeValues[1], CultureInfo.InvariantCulture) * ProgramConstants.MAP_CELL_SIZE_Y); // Calculate map size - int mapSizeX = Convert.ToInt32(localSizeValues[2], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_X; - int mapSizeY = Convert.ToInt32(localSizeValues[3], CultureInfo.InvariantCulture) * MainClientConstants.MAP_CELL_SIZE_Y; + int mapSizeX = Convert.ToInt32(localSizeValues[2], CultureInfo.InvariantCulture) * ProgramConstants.MAP_CELL_SIZE_X; + int mapSizeY = Convert.ToInt32(localSizeValues[3], CultureInfo.InvariantCulture) * ProgramConstants.MAP_CELL_SIZE_Y; double ratioX = Convert.ToDouble(pixelPosX) / mapSizeX; double ratioY = Convert.ToDouble(pixelPosY) / mapSizeY; diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index b5d185d15..6dfeb4b34 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -224,8 +224,9 @@ private async Task> LoadCustomMapCacheAsync() return customMaps; } - catch (Exception) + catch (Exception ex) { + ProgramConstants.LogException(ex); return new ConcurrentDictionary(); } } diff --git a/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs b/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs index 9e7b5263f..f6835089c 100644 --- a/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs +++ b/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs @@ -66,9 +66,9 @@ public static Image ExtractMapPreview(IniFile mapIni) { dataSource = Convert.FromBase64String(sb.ToString()); } - catch (Exception) + catch (Exception ex) { - Logger.Log("MapPreviewExtractor: " + baseFilename + " - [PreviewPack] is malformed, unable to extract preview."); + ProgramConstants.LogException(ex, "MapPreviewExtractor: " + baseFilename + " - [PreviewPack] is malformed, unable to extract preview."); return null; } @@ -134,9 +134,10 @@ private static byte[] DecompressPreviewData(byte[] dataSource, int decompressedD errorMessage = null; return dataDest; } - catch (Exception e) + catch (Exception ex) { - errorMessage = "Error encountered decompressing preview data. Message: " + e.Message; + ProgramConstants.LogException(ex, "Error encountered decompressing preview data."); + errorMessage = "Error encountered decompressing preview data. Message: " + ex.Message; return null; } } @@ -213,9 +214,10 @@ private static Image CreatePreviewBitmapFromImageData(int width, int height, byt return image; } - catch (Exception e) + catch (Exception ex) { - errorMessage = "Error encountered creating preview bitmap. Message: " + e.Message; + ProgramConstants.LogException(ex, "Error encountered creating preview bitmap."); + errorMessage = "Error encountered creating preview bitmap. Message: " + ex.Message; return null; } } diff --git a/DXMainClient/Domain/Multiplayer/MultiplayerColor.cs b/DXMainClient/Domain/Multiplayer/MultiplayerColor.cs index e23204cc1..88311cf91 100644 --- a/DXMainClient/Domain/Multiplayer/MultiplayerColor.cs +++ b/DXMainClient/Domain/Multiplayer/MultiplayerColor.cs @@ -62,9 +62,9 @@ public static List LoadColors() MultiplayerColor mpColor = MultiplayerColor.CreateFromStringArray(key.L10N($"INI:Colors:{key}"), values); mpColors.Add(mpColor); } - catch + catch (Exception ex) { - throw new ClientConfigurationException("Invalid MPColor specified in GameOptions.ini: " + key); + throw new ClientConfigurationException("Invalid MPColor specified in GameOptions.ini: " + key, ex); } } diff --git a/DXMainClient/Domain/SavedGame.cs b/DXMainClient/Domain/SavedGame.cs index 05eeac406..7983a7cbd 100644 --- a/DXMainClient/Domain/SavedGame.cs +++ b/DXMainClient/Domain/SavedGame.cs @@ -56,8 +56,7 @@ public bool ParseInfo() } catch (Exception ex) { - Logger.Log("An error occured while parsing saved game " + FileName + ":" + - ex.Message); + ProgramConstants.LogException(ex, "An error occurred while parsing saved game " + FileName); return false; } } diff --git a/DXMainClient/Online/CnCNetGameCheck.cs b/DXMainClient/Online/CnCNetGameCheck.cs index 4951ed9ba..952b69aec 100644 --- a/DXMainClient/Online/CnCNetGameCheck.cs +++ b/DXMainClient/Online/CnCNetGameCheck.cs @@ -1,4 +1,5 @@ -using ClientCore; +using System; +using ClientCore; using System.Diagnostics; using System.Threading; @@ -36,7 +37,8 @@ private void CheatEngineWatchEvent() Process[] processlist = Process.GetProcesses(); foreach (Process process in processlist) { - try { + try + { if (process.ProcessName.Contains("cheatengine") || process.MainWindowTitle.ToLower().Contains("cheat engine") || process.MainWindowHandle.ToString().ToLower().Contains("cheat engine") @@ -45,7 +47,10 @@ private void CheatEngineWatchEvent() KillGameInstance(); } } - catch { } + catch (Exception ex) + { + ProgramConstants.LogException(ex); + } process.Dispose(); } @@ -64,19 +69,24 @@ private void KillGameInstance() Process[] processlist = Process.GetProcesses(); foreach (Process process in processlist) { - try { + try + { if (process.ProcessName.Contains(gameExecutableName)) { process.Kill(); } } - catch { } + catch (Exception ex) + { + ProgramConstants.LogException(ex); + } process.Dispose(); } } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); } } } diff --git a/DXMainClient/Online/CnCNetManager.cs b/DXMainClient/Online/CnCNetManager.cs index 028cbe40d..5d70adb60 100644 --- a/DXMainClient/Online/CnCNetManager.cs +++ b/DXMainClient/Online/CnCNetManager.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.Online { diff --git a/DXMainClient/Online/CnCNetUserData.cs b/DXMainClient/Online/CnCNetUserData.cs index ea6f34142..667c56bb9 100644 --- a/DXMainClient/Online/CnCNetUserData.cs +++ b/DXMainClient/Online/CnCNetUserData.cs @@ -60,9 +60,9 @@ private static List LoadTextList(string path) Logger.Log($"Loading {path} failed! File does not exist."); return new(); } - catch + catch (Exception ex) { - Logger.Log($"Loading {path} list failed!"); + ProgramConstants.LogException(ex, $"Loading {path} list failed!"); return new(); } } @@ -79,9 +79,9 @@ private static List LoadJsonList(string path) Logger.Log($"Loading {path} failed! File does not exist."); return new(); } - catch + catch (Exception ex) { - Logger.Log($"Loading {path} list failed!"); + ProgramConstants.LogException(ex, $"Loading {path} list failed!"); return new(); } } @@ -99,7 +99,7 @@ private static void SaveTextList(string path, List textList) } catch (Exception ex) { - Logger.Log($"Saving {path} failed! Error message: " + ex.Message); + ProgramConstants.LogException(ex, $"Saving {path} failed!"); } } @@ -116,7 +116,7 @@ private static void SaveJsonList(string path, IReadOnlyCollection jsonList } catch (Exception ex) { - Logger.Log($"Saving {path} failed! Error message: " + ex.Message); + ProgramConstants.LogException(ex, $"Saving {path} failed!"); } } diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index 5f0faedaa..55e85cef6 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -12,6 +12,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.Online { @@ -189,7 +190,7 @@ await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(server.Host), port), } catch (Exception ex) { - PreStartup.LogException(ex, "Unable to connect to the server."); + ProgramConstants.LogException(ex, "Unable to connect to the server."); } } @@ -235,7 +236,7 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) } catch (Exception ex) { - PreStartup.LogException(ex, "Disconnected from CnCNet due to a socket error."); + ProgramConstants.LogException(ex, "Disconnected from CnCNet due to a socket error."); errorTimes++; if (errorTimes > MAX_RECONNECT_COUNT) @@ -412,7 +413,7 @@ private async Task> GetServerListSortedByLatencyAsync() } catch (PingException ex) { - PreStartup.LogException(ex, $"Caught an exception when pinging {serverInfo.Name} ({serverInfo.IpAddress}) Lobby server."); + ProgramConstants.LogException(ex, $"Caught an exception when pinging {serverInfo.Name} ({serverInfo.IpAddress}) Lobby server."); return (server, serverInfo.IpAddress, long.MaxValue); } @@ -437,7 +438,7 @@ private async Task> GetServerListSortedByLatencyAsync() } catch (SocketException ex) { - PreStartup.LogException(ex, $"Caught an exception when DNS resolving {server.Name} ({server.Host}) Lobby server."); + ProgramConstants.LogException(ex, $"Caught an exception when DNS resolving {server.Name} ({server.Host}) Lobby server."); } return Array.Empty<(IPAddress IpAddress, string Name, int[] Ports)>(); @@ -719,7 +720,7 @@ private async Task PerformCommandAsync(string message) } catch (Exception ex) { - PreStartup.LogException(ex, "Warning: Failed to parse command " + message); + ProgramConstants.LogException(ex, "Warning: Failed to parse command " + message); } } @@ -939,7 +940,7 @@ private async Task SendMessageAsync(string message) } catch (IOException ex) { - PreStartup.LogException(ex, "Sending message to the server failed!"); + ProgramConstants.LogException(ex, "Sending message to the server failed!"); } } diff --git a/DXMainClient/PreStartup.cs b/DXMainClient/PreStartup.cs index eb6337cc3..741bfc95b 100644 --- a/DXMainClient/PreStartup.cs +++ b/DXMainClient/PreStartup.cs @@ -56,9 +56,9 @@ public static void Initialize(StartupParams parameters) #if WINFORMS Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException); - Application.ThreadException += (_, args) => HandleException(args.Exception); + Application.ThreadException += (_, args) => ProgramConstants.HandleException(args.Exception); #endif - AppDomain.CurrentDomain.UnhandledException += (_, args) => HandleException((Exception)args.ExceptionObject); + AppDomain.CurrentDomain.UnhandledException += (_, args) => ProgramConstants.HandleException((Exception)args.ExceptionObject); DirectoryInfo gameDirectory = SafePath.GetDirectory(ProgramConstants.GamePath); @@ -80,9 +80,24 @@ public static void Initialize(StartupParams parameters) if (!clientUserFilesDirectory.Exists) clientUserFilesDirectory.Create(); - MainClientConstants.Initialize(); + ProgramConstants.OSId = ClientConfiguration.Instance.GetOperatingSystemVersion(); + ProgramConstants.GAME_NAME_SHORT = ClientConfiguration.Instance.LocalGame; + ProgramConstants.GAME_NAME_LONG = ClientConfiguration.Instance.LongGameName; + ProgramConstants.SUPPORT_URL_SHORT = ClientConfiguration.Instance.ShortSupportURL; + ProgramConstants.CREDITS_URL = ClientConfiguration.Instance.CreditsURL; + ProgramConstants.MAP_CELL_SIZE_X = ClientConfiguration.Instance.MapCellSizeX; + ProgramConstants.MAP_CELL_SIZE_Y = ClientConfiguration.Instance.MapCellSizeY; - Logger.Log("***Logfile for " + MainClientConstants.GAME_NAME_LONG + " client***"); + if (string.IsNullOrEmpty(ProgramConstants.GAME_NAME_SHORT)) + throw new ClientConfigurationException("LocalGame is set to an empty value."); + + if (ProgramConstants.GAME_NAME_SHORT.Length > ProgramConstants.GAME_ID_MAX_LENGTH) + { + throw new ClientConfigurationException("LocalGame is set to a value that exceeds length limit of " + + ProgramConstants.GAME_ID_MAX_LENGTH + " characters."); + } + + Logger.Log("***Logfile for " + ProgramConstants.GAME_NAME_LONG + " client***"); Logger.Log("Client version: " + Assembly.GetAssembly(typeof(PreStartup)).GetName().Version); // Log information about given startup params @@ -132,7 +147,7 @@ public static void Initialize(StartupParams parameters) } catch (Exception ex) { - LogException(ex, "Failed to load the translation file."); + ProgramConstants.LogException(ex, "Failed to load the translation file."); Translation.Instance = new Translation(UserINISettings.Instance.Translation); } @@ -163,7 +178,7 @@ public static void Initialize(StartupParams parameters) } catch (Exception ex) { - LogException(ex, "Failed to generate the translation stub."); + ProgramConstants.LogException(ex, "Failed to generate the translation stub."); } // Delete obsolete files from old target project versions @@ -177,7 +192,7 @@ public static void Initialize(StartupParams parameters) } catch (Exception ex) { - LogException(ex); + ProgramConstants.LogException(ex); string error = "Deleting wsock32.dll failed! Please close any " + "applications that could be using the file, and then start the client again." @@ -194,88 +209,16 @@ public static void Initialize(StartupParams parameters) new Startup().Execute(); } - /// - /// Logs all details of an exception to the logfile without further action. - /// - /// The to log. - /// /// Optional message to accompany the error. - public static void LogException(Exception ex, string message = null) - { - LogExceptionRecursive(ex, message); - } - - private static void LogExceptionRecursive(Exception ex, string message = null, bool innerException = false) - { - if (!innerException) - Logger.Log(message); - else - Logger.Log("InnerException info:"); - - Logger.Log("Type: " + ex.GetType()); - Logger.Log("Message: " + ex.Message); - Logger.Log("Source: " + ex.Source); - Logger.Log("TargetSite.Name: " + ex.TargetSite?.Name); - Logger.Log("Stacktrace: " + ex.StackTrace); - - if (ex is AggregateException aggregateException) - { - foreach (Exception aggregateExceptionInnerException in aggregateException.InnerExceptions) - { - LogExceptionRecursive(aggregateExceptionInnerException, null, true); - } - } - else if (ex.InnerException is not null) - { - LogExceptionRecursive(ex.InnerException, null, true); - } - } - - /// - /// Logs all details of an exception to the logfile, notifies the user, and exits the application. - /// - /// The to log. - internal static void HandleException(Exception ex) - { - LogExceptionRecursive(ex, "KABOOOOOOM!!! Info:"); - - string errorLogPath = SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, "ClientCrashLogs", FormattableString.Invariant($"ClientCrashLog{DateTime.Now.ToString("_yyyy_MM_dd_HH_mm")}.txt")); - bool crashLogCopied = false; - - try - { - DirectoryInfo crashLogsDirectoryInfo = SafePath.GetDirectory(ProgramConstants.ClientUserFilesPath, "ClientCrashLogs"); - - if (!crashLogsDirectoryInfo.Exists) - crashLogsDirectoryInfo.Create(); - - File.Copy(SafePath.CombineFilePath(ProgramConstants.ClientUserFilesPath, "client.log"), errorLogPath, true); - crashLogCopied = true; - } - catch { } - - string error = string.Format("{0} has crashed. Error message:".L10N("Client:Main:FatalErrorText1") + Environment.NewLine + Environment.NewLine + - ex.Message + Environment.NewLine + Environment.NewLine + (crashLogCopied ? - "A crash log has been saved to the following file:".L10N("Client:Main:FatalErrorText2") + " " + Environment.NewLine + Environment.NewLine + - errorLogPath + Environment.NewLine + Environment.NewLine : "") + - (crashLogCopied ? "If the issue is repeatable, contact the {1} staff at {2} and provide the crash log file.".L10N("Client:Main:FatalErrorText3") : - "If the issue is repeatable, contact the {1} staff at {2}.".L10N("Client:Main:FatalErrorText4")), - MainClientConstants.GAME_NAME_LONG, - MainClientConstants.GAME_NAME_SHORT, - MainClientConstants.SUPPORT_URL_SHORT); - - ProgramConstants.DisplayErrorAction("KABOOOOOOOM".L10N("Client:Main:FatalErrorTitle"), error, true); - } - [SupportedOSPlatform("windows")] private static void CheckPermissions() { if (UserHasDirectoryAccessRights(ProgramConstants.GamePath, FileSystemRights.Modify)) return; - string error = string.Format(("You seem to be running {0} from a write-protected directory.\n\n" + + string error = string.Format(("You seem to be running {0} from a write-protected directory.\n\n" + "For {1} to function properly when run from a write-protected directory, it needs administrative priveleges.\n\n" + "Would you like to restart the client with administrative rights?\n\n" + - "Please also make sure that your security software isn't blocking {1}.").L10N("Client:Main:AdminRequiredText"), MainClientConstants.GAME_NAME_LONG, MainClientConstants.GAME_NAME_SHORT); + "Please also make sure that your security software isn't blocking {1}.").L10N("Client:Main:AdminRequiredText"), ProgramConstants.GAME_NAME_LONG, ProgramConstants.GAME_NAME_SHORT); ProgramConstants.DisplayErrorAction("Administrative privileges required".L10N("Client:Main:AdminRequiredTitle"), error, false); @@ -343,6 +286,7 @@ private static bool UserHasDirectoryAccessRights(string path, FileSystemRights a { return false; } + return isInRoleWithAccess; } } diff --git a/DXMainClient/Startup.cs b/DXMainClient/Startup.cs index a9e757577..099409890 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -16,6 +16,7 @@ using System.Management; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using ClientCore.Extensions; using ClientCore.Settings; using Microsoft.Xna.Framework.Graphics; @@ -46,22 +47,22 @@ public void Execute() Logger.Log("ProcessArchitecture: " + RuntimeInformation.ProcessArchitecture); Logger.Log("FrameworkDescription: " + RuntimeInformation.FrameworkDescription); Logger.Log("RuntimeIdentifier: " + RuntimeInformation.RuntimeIdentifier); - Logger.Log("Selected OS profile: " + MainClientConstants.OSId); + Logger.Log("Selected OS profile: " + ProgramConstants.OSId); Logger.Log("Current culture: " + CultureInfo.CurrentCulture); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { // The query in CheckSystemSpecifications takes lots of time, // so we'll do it in a separate thread to make startup faster - Task.Run(CheckSystemSpecifications); + Task.Run(CheckSystemSpecifications).HandleTask(); } GenerateOnlineIdAsync().HandleTask(); #if ARES - Task.Run(() => PruneFiles(SafePath.GetDirectory(ProgramConstants.GamePath, "debug"), DateTime.Now.AddDays(-7))); + Task.Run(() => PruneFiles(SafePath.GetDirectory(ProgramConstants.GamePath, "debug"), DateTime.Now.AddDays(-7))).HandleTask(); #endif - Task.Run(MigrateOldLogFiles); + Task.Run(MigrateOldLogFiles).HandleTask(); DirectoryInfo updaterFolder = SafePath.GetDirectory(ProgramConstants.GamePath, "Updater"); @@ -72,8 +73,9 @@ public void Execute() { updaterFolder.Delete(true); } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); } } @@ -88,8 +90,9 @@ public void Execute() { savedGamesFolder.Create(); } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); } } } @@ -103,9 +106,9 @@ public void Execute() { SafePath.DeleteFileIfExists(ProgramConstants.GamePath, FormattableString.Invariant($"{component.LocalPath}_u")); } - catch + catch (Exception ex) { - + ProgramConstants.LogException(ex); } } } @@ -157,11 +160,9 @@ private void PruneFiles(DirectoryInfo directory, DateTime pruneThresholdTime) if (fileInfo.CreationTime <= pruneThresholdTime) fileInfo.Delete(); } - catch (Exception e) + catch (Exception ex) { - Logger.Log("PruneFiles: Could not delete file " + fsEntry.Name + - ". Error message: " + e.Message); - continue; + ProgramConstants.LogException(ex, "PruneFiles: Could not delete file " + fsEntry.Name + "."); } } } @@ -171,8 +172,7 @@ private void PruneFiles(DirectoryInfo directory, DateTime pruneThresholdTime) } catch (Exception ex) { - Logger.Log("PruneFiles: An error occurred while pruning files from " + - directory.Name + ". Message: " + ex.Message); + ProgramConstants.LogException(ex, "PruneFiles: An error occurred while pruning files from " + directory.Name + "."); } } #endif @@ -226,9 +226,9 @@ private static void MigrateLogFiles(DirectoryInfo newDirectory, string searchPat } catch (Exception ex) { - Logger.Log("MigrateLogFiles: An error occured while moving log files from " + + ProgramConstants.LogException(ex, "MigrateLogFiles: An error occurred while moving log files from " + currentDirectory.Name + " to " + - newDirectory.Name + ". Message: " + ex.Message); + newDirectory.Name + "."); } } @@ -252,10 +252,11 @@ private static void CheckSystemSpecifications() { cpu = cpu + proc["Name"].ToString().Trim() + " (" + proc["NumberOfCores"] + " cores) "; } - } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); + cpu = "CPU info not found"; } @@ -274,8 +275,10 @@ private static void CheckSystemSpecifications() } } } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); + cpu = "Video controller info not found"; } @@ -292,8 +295,10 @@ private static void CheckSystemSpecifications() if (total != 0) memory = "Total physical memory: " + (total >= 1073741824 ? total / 1073741824 + "GB" : total / 1048576 + "MB"); } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); + cpu = "Memory info not found"; } @@ -334,8 +339,9 @@ private static async Task GenerateOnlineIdAsync() using RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\" + ClientConfiguration.Instance.InstallationPathRegKey); key.SetValue("Ident", cpuid + mbid + sid); } - catch (Exception) + catch (Exception ex) { + ProgramConstants.LogException(ex); Random rn = new Random(); using RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\" + ClientConfiguration.Instance.InstallationPathRegKey); @@ -349,7 +355,10 @@ private static async Task GenerateOnlineIdAsync() else str = o.ToString(); } - catch { } + catch (Exception ex1) + { + ProgramConstants.LogException(ex1); + } Connection.SetId(str); } @@ -364,8 +373,9 @@ private static async Task GenerateOnlineIdAsync() Connection.SetId(machineId); } - catch (Exception) + catch (Exception ex) { + ProgramConstants.LogException(ex); Connection.SetId(new Random().Next(int.MaxValue - 1).ToString()); } } @@ -391,9 +401,9 @@ private static void WriteInstallPathToRegistry() using RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\" + ClientConfiguration.Instance.InstallationPathRegKey); key.SetValue("InstallPath", ProgramConstants.GamePath); } - catch + catch (Exception ex) { - Logger.Log("Failed to write installation path to the Windows registry"); + ProgramConstants.LogException(ex, "Failed to write installation path to the Windows registry"); } } } From 3e7b37ca2467891831253d644f87066dda005fcf Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 27 Nov 2022 16:27:26 +0100 Subject: [PATCH 042/109] Group network commands --- .editorconfig | 4 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 60 ++--- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 20 +- .../Multiplayer/CnCNet/GlobalContextMenu.cs | 5 +- .../CnCNet/PrivateMessagingWindow.cs | 3 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 210 ++++++++---------- .../Multiplayer/GameLobby/LANGameLobby.cs | 86 +++---- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 42 ++-- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 20 +- .../Multiplayer/CnCNet/CnCNetCommands.cs | 45 ++++ .../Domain/Multiplayer/CnCNet/IRCCommands.cs | 23 ++ .../Domain/Multiplayer/LAN/LANCommands.cs | 28 +++ .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 2 +- .../Domain/Multiplayer/PlayerExtraOptions.cs | 10 +- DXMainClient/Online/Channel.cs | 33 ++- DXMainClient/Online/CnCNetManager.cs | 5 +- DXMainClient/Online/Connection.cs | 61 +++-- DXMainClient/Online/QueuedMessage.cs | 8 +- 18 files changed, 353 insertions(+), 312 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/CnCNetCommands.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/IRCCommands.cs create mode 100644 DXMainClient/Domain/Multiplayer/LAN/LANCommands.cs diff --git a/.editorconfig b/.editorconfig index 200de86ef..32839b6fe 100644 --- a/.editorconfig +++ b/.editorconfig @@ -123,7 +123,7 @@ csharp_style_prefer_range_operator = true:warning csharp_style_prefer_tuple_swap = true:warning csharp_style_throw_expression = true:warning csharp_style_unused_value_assignment_preference = discard_variable:warning -csharp_style_unused_value_expression_statement_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion # 'using' directive preferences csharp_using_directive_placement = outside_namespace:warning @@ -279,7 +279,7 @@ dotnet_diagnostic.IDE0055.severity = warning dotnet_diagnostic.IDE0054.severity = warning dotnet_diagnostic.IDE0056.severity = warning dotnet_diagnostic.IDE0057.severity = warning -dotnet_diagnostic.IDE0058.severity = warning +dotnet_diagnostic.IDE0058.severity = suggestion dotnet_diagnostic.IDE0060.severity = warning dotnet_diagnostic.IDE0066.severity = warning dotnet_diagnostic.IDE0059.severity = warning diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index de37794f5..a9af05314 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -29,23 +29,11 @@ internal sealed class CnCNetGameLoadingLobby : GameLoadingLobbyBase private const double GAME_BROADCAST_INTERVAL = 20.0; private const double INITIAL_GAME_BROADCAST_DELAY = 10.0; - private const string NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND = "NPRSNT"; - private const string GET_READY_CTCP_COMMAND = "GTRDY"; - private const string FILE_HASH_CTCP_COMMAND = "FHSH"; - private const string INVALID_FILE_HASH_CTCP_COMMAND = "IHSH"; - private const string TUNNEL_PING_CTCP_COMMAND = "TNLPNG"; - private const string OPTIONS_CTCP_COMMAND = "OP"; - private const string INVALID_SAVED_GAME_INDEX_CTCP_COMMAND = "ISGI"; - private const string START_GAME_CTCP_COMMAND = "START"; - private const string PLAYER_READY_CTCP_COMMAND = "READY"; - private const string CHANGE_TUNNEL_SERVER_MESSAGE = "CHTNL"; - public CnCNetGameLoadingLobby( WindowManager windowManager, TopBar topBar, CnCNetManager connectionManager, TunnelHandler tunnelHandler, - MapLoader mapLoader, GameCollection gameCollection, DiscordHandler discordHandler) : base(windowManager, discordHandler) @@ -54,20 +42,19 @@ public CnCNetGameLoadingLobby( this.tunnelHandler = tunnelHandler; this.topBar = topBar; this.gameCollection = gameCollection; - this.mapLoader = mapLoader; ctcpCommandHandlers = new CommandHandlerBase[] { - new NoParamCommandHandler(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, sender => HandleNotAllPresentNotificationAsync(sender).HandleTask()), - new NoParamCommandHandler(GET_READY_CTCP_COMMAND, sender => HandleGetReadyNotificationAsync(sender).HandleTask()), - new StringCommandHandler(FILE_HASH_CTCP_COMMAND, (sender, fileHash) => HandleFileHashCommandAsync(sender, fileHash).HandleTask()), - new StringCommandHandler(INVALID_FILE_HASH_CTCP_COMMAND, (sender, cheaterName) => HandleCheaterNotificationAsync(sender, cheaterName).HandleTask()), - new IntCommandHandler(TUNNEL_PING_CTCP_COMMAND, HandleTunnelPing), - new StringCommandHandler(OPTIONS_CTCP_COMMAND, (sender, data) => HandleOptionsMessageAsync(sender, data).HandleTask()), - new NoParamCommandHandler(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, HandleInvalidSaveIndexCommand), - new StringCommandHandler(START_GAME_CTCP_COMMAND, HandleStartGameCommand), - new IntCommandHandler(PLAYER_READY_CTCP_COMMAND, (sender, readyStatus) => HandlePlayerReadyRequestAsync(sender, readyStatus).HandleTask()), - new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, HandleTunnelServerChangeMessage) + new NoParamCommandHandler(CnCNetCommands.NOT_ALL_PLAYERS_PRESENT, sender => HandleNotAllPresentNotificationAsync(sender).HandleTask()), + new NoParamCommandHandler(CnCNetCommands.GET_READY, sender => HandleGetReadyNotificationAsync(sender).HandleTask()), + new StringCommandHandler(CnCNetCommands.FILE_HASH, (sender, fileHash) => HandleFileHashCommandAsync(sender, fileHash).HandleTask()), + new StringCommandHandler(CnCNetCommands.INVALID_FILE_HASH, (sender, cheaterName) => HandleCheaterNotificationAsync(sender, cheaterName).HandleTask()), + new IntCommandHandler(CnCNetCommands.TUNNEL_PING, HandleTunnelPing), + new StringCommandHandler(CnCNetCommands.OPTIONS, (sender, data) => HandleOptionsMessageAsync(sender, data).HandleTask()), + new NoParamCommandHandler(CnCNetCommands.INVALID_SAVED_GAME_INDEX, HandleInvalidSaveIndexCommand), + new StringCommandHandler(CnCNetCommands.START_GAME, HandleStartGameCommand), + new IntCommandHandler(CnCNetCommands.PLAYER_READY, (sender, readyStatus) => HandlePlayerReadyRequestAsync(sender, readyStatus).HandleTask()), + new StringCommandHandler(CnCNetCommands.CHANGE_TUNNEL_SERVER, HandleTunnelServerChangeMessage) }; } @@ -78,7 +65,6 @@ public CnCNetGameLoadingLobby( private List gameModes; private TunnelHandler tunnelHandler; - private readonly MapLoader mapLoader; private TunnelSelectionWindow tunnelSelectionWindow; private XNAClientButton btnChangeTunnel; @@ -232,12 +218,12 @@ public async Task OnJoinedAsync() if (IsHost) { await connectionManager.SendCustomMessageAsync(new QueuedMessage( - string.Format("MODE {0} +klnNs {1} {2}", channel.ChannelName, + string.Format(IRCCommands.MODE + " {0} +klnNs {1} {2}", channel.ChannelName, channel.Password, SGPlayers.Count), QueuedMessageType.SYSTEM_MESSAGE, 50)); await connectionManager.SendCustomMessageAsync(new QueuedMessage( - string.Format("TOPIC {0} :{1}", channel.ChannelName, + string.Format(IRCCommands.TOPIC + " {0} :{1}", channel.ChannelName, ProgramConstants.CNCNET_PROTOCOL_REVISION + ";" + localGame.ToLower()), QueuedMessageType.SYSTEM_MESSAGE, 50)); @@ -249,9 +235,9 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage( } else { - await channel.SendCTCPMessageAsync(FILE_HASH_CTCP_COMMAND + " " + fhc.GetCompleteHash(), QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.FILE_HASH + " " + fhc.GetCompleteHash(), QueuedMessageType.SYSTEM_MESSAGE, 10); - await channel.SendCTCPMessageAsync(TUNNEL_PING_CTCP_COMMAND + " " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_PING + " " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); if (tunnelHandler.CurrentTunnel.PingInMs < 0) AddNotice(string.Format("{0} - unknown ping to tunnel server.".L10N("Client:Main:PlayerUnknownPing"), ProgramConstants.PLAYERNAME)); @@ -325,7 +311,7 @@ protected override async Task BroadcastOptionsAsync() //if (Players.Count > 0) Players[0].Ready = true; - StringBuilder message = new StringBuilder(OPTIONS_CTCP_COMMAND + " "); + StringBuilder message = new StringBuilder(CnCNetCommands.OPTIONS + " "); message.Append(ddSavedGame.SelectedIndex); message.Append(";"); foreach (PlayerInfo pInfo in Players) @@ -348,7 +334,7 @@ protected override Task SendChatMessageAsync(string message) } protected override Task RequestReadyStatusAsync() => - channel.SendCTCPMessageAsync(PLAYER_READY_CTCP_COMMAND + " 1", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 10); + channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_READY + " 1", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 10); protected override async Task GetReadyNotificationAsync() { @@ -357,7 +343,7 @@ protected override async Task GetReadyNotificationAsync() topBar.SwitchToPrimary(); if (IsHost) - await channel.SendCTCPMessageAsync(GET_READY_CTCP_COMMAND, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.GET_READY, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); } protected override async Task NotAllPresentNotificationAsync() @@ -366,7 +352,7 @@ protected override async Task NotAllPresentNotificationAsync() if (IsHost) { - await channel.SendCTCPMessageAsync(NOT_ALL_PLAYERS_PRESENT_CTCP_COMMAND, + await channel.SendCTCPMessageAsync(CnCNetCommands.NOT_ALL_PLAYERS_PRESENT, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } } @@ -377,7 +363,7 @@ private void ShowTunnelSelectionWindow(string description) private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) { await channel.SendCTCPMessageAsync( - $"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", + $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Address}:{e.Tunnel.Port}", QueuedMessageType.SYSTEM_MESSAGE, 10); HandleTunnelServerChange(e.Tunnel); @@ -427,7 +413,7 @@ private async Task HandleCheaterNotificationAsync(string sender, string cheaterN AddNotice(string.Format("{0} - modified files detected! They could be cheating!".L10N("Client:Main:PlayerCheating"), cheaterName), Color.Red); if (IsHost) - await channel.SendCTCPMessageAsync(INVALID_FILE_HASH_CTCP_COMMAND + " " + cheaterName, QueuedMessageType.SYSTEM_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.INVALID_FILE_HASH + " " + cheaterName, QueuedMessageType.SYSTEM_MESSAGE, 0); } private void HandleTunnelPing(string sender, int pingInMs) @@ -459,7 +445,7 @@ private async Task HandleOptionsMessageAsync(string sender, string data) if (sgIndex >= ddSavedGame.Items.Count) { AddNotice("The game host has selected an invalid saved game index!".L10N("Client:Main:HostInvalidIndex") + " " + sgIndex); - await channel.SendCTCPMessageAsync(INVALID_SAVED_GAME_INDEX_CTCP_COMMAND, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.INVALID_SAVED_GAME_INDEX, QueuedMessageType.SYSTEM_MESSAGE, 10); return; } @@ -606,7 +592,7 @@ protected override async Task HostStartGameAsync() return; } - StringBuilder sb = new StringBuilder(START_GAME_CTCP_COMMAND + " "); + StringBuilder sb = new StringBuilder(CnCNetCommands.START_GAME + " "); for (int pId = 0; pId < Players.Count; pId++) { Players[pId].Port = playerPorts[pId]; @@ -655,7 +641,7 @@ private async Task BroadcastGameAsync() if (broadcastChannel == null) return; - StringBuilder sb = new StringBuilder("GAME "); + StringBuilder sb = new StringBuilder(CnCNetCommands.GAME + " "); sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); sb.Append(";"); sb.Append(ProgramConstants.GAME_VERSION); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 865da1e60..7ffff834d 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -53,8 +53,8 @@ public CnCNetLobby(WindowManager windowManager, CnCNetManager connectionManager, ctcpCommandHandlers = new CommandHandlerBase[] { - new StringCommandHandler(ProgramConstants.GAME_INVITE_CTCP_COMMAND, (sender, argumentsString) => HandleGameInviteCommandAsync(sender, argumentsString).HandleTask()), - new NoParamCommandHandler(ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND, HandleGameInvitationFailedNotification) + new StringCommandHandler(CnCNetCommands.GAME_INVITE, (sender, argumentsString) => HandleGameInviteCommandAsync(sender, argumentsString).HandleTask()), + new NoParamCommandHandler(CnCNetCommands.GAME_INVITATION_FAILED, HandleGameInvitationFailedNotification) }; topBar.LogoutEvent += LogoutEvent; @@ -618,12 +618,12 @@ private async Task ConnectionManager_BannedFromChannelAsync(ChannelEventArgs e) private Task SharedUILogic_GameProcessStartedAsync() => connectionManager.SendCustomMessageAsync(new QueuedMessage( - "AWAY " + (char)58 + "In-game", + IRCCommands.AWAY + " " + (char)58 + "In-game", QueuedMessageType.SYSTEM_MESSAGE, 0)); private Task SharedUILogic_GameProcessExitedAsync() - => connectionManager.SendCustomMessageAsync(new QueuedMessage("AWAY", QueuedMessageType.SYSTEM_MESSAGE, 0)); + => connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.AWAY, QueuedMessageType.SYSTEM_MESSAGE, 0)); private async Task Instance_SettingsSavedAsync() { @@ -901,7 +901,7 @@ private async Task JoinGameAsync(HostedCnCNetGame hg, string password) gameChannel.TargetChangeTooFast += gameChannel_TargetChangeTooFastFunc; } - await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + hg.ChannelName + " " + password, + await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.JOIN + " " + hg.ChannelName + " " + password, QueuedMessageType.INSTANT_MESSAGE, 0)); } @@ -1002,7 +1002,7 @@ private async Task Gcw_GameCreatedAsync(GameCreationEventArgs e) connectionManager.AddChannel(gameChannel); await gameLobby.SetUpAsync(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword, false); gameChannel.UserAdded += gameChannel_UserAddedFunc; - await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + channelName + " " + password, + await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.JOIN + " " + channelName + " " + password, QueuedMessageType.INSTANT_MESSAGE, 0)); connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); @@ -1024,7 +1024,7 @@ private async Task Gcw_LoadedGameCreatedAsync(GameCreationEventArgs e) connectionManager.AddChannel(gameLoadingChannel); gameLoadingLobby.SetUp(true, e.Tunnel, gameLoadingChannel, ProgramConstants.PLAYERNAME); gameLoadingChannel.UserAdded += gameLoadingChannel_UserAddedFunc; - await connectionManager.SendCustomMessageAsync(new QueuedMessage("JOIN " + channelName + " " + e.Password, + await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.JOIN + " " + channelName + " " + e.Password, QueuedMessageType.INSTANT_MESSAGE, 0)); connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); @@ -1202,8 +1202,8 @@ private async Task HandleGameInviteCommandAsync(string sender, string argumentsS { // let the host know that we can't accept // note this is not reached for the rejection case - await connectionManager.SendCustomMessageAsync(new QueuedMessage("PRIVMSG " + sender + " :\u0001" + - ProgramConstants.GAME_INVITATION_FAILED_CTCP_COMMAND + "\u0001", + await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.PRIVMSG + " " + sender + " :\u0001" + + CnCNetCommands.GAME_INVITATION_FAILED + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0)); return; @@ -1435,7 +1435,7 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr } } - if (!e.Message.StartsWith("GAME ")) + if (!e.Message.StartsWith(CnCNetCommands.GAME + " ")) return; string msg = e.Message[5..]; // Cut out GAME part diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs index 59886277e..0c27c1c17 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs @@ -4,6 +4,7 @@ using ClientCore; using ClientCore.Extensions; using ClientGUI; +using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.Online; using DTAClient.Online.EventArguments; using ClientCore.Extensions; @@ -113,7 +114,7 @@ private async Task InviteAsync() return; } - string messageBody = ProgramConstants.GAME_INVITE_CTCP_COMMAND + " " + contextMenuData.inviteChannelName + ";" + contextMenuData.inviteGameName; + string messageBody = CnCNetCommands.GAME_INVITE + " " + contextMenuData.inviteChannelName + ";" + contextMenuData.inviteGameName; if (!string.IsNullOrEmpty(contextMenuData.inviteChannelPassword)) { @@ -121,7 +122,7 @@ private async Task InviteAsync() } await connectionManager.SendCustomMessageAsync(new QueuedMessage( - "PRIVMSG " + GetIrcUser().Name + " :\u0001" + messageBody + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0)); + IRCCommands.PRIVMSG + " " + GetIrcUser().Name + " :\u0001" + messageBody + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0)); } private void UpdateButtons() diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs index a6a8cdb3c..8bdeafffe 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using ClientCore.Enums; using ClientCore.Extensions; +using DTAClient.Domain.Multiplayer.CnCNet; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; @@ -526,7 +527,7 @@ private async Task TbMessageInput_EnterPressedAsync() string userName = lbUserList.SelectedItem.Text; - await connectionManager.SendCustomMessageAsync(new QueuedMessage("PRIVMSG " + userName + " :" + tbMessageInput.Text, + await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.PRIVMSG + " " + userName + " :" + tbMessageInput.Text, QueuedMessageType.CHAT_MESSAGE, 0)); PrivateMessageUser pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == userName); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 59a97de96..d89215d4b 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -38,22 +38,6 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; - #region Commands - - private const string MAP_SHARING_FAIL_MESSAGE = "MAPFAIL"; - private const string MAP_SHARING_DOWNLOAD_REQUEST = "MAPOK"; - private const string MAP_SHARING_UPLOAD_REQUEST = "MAPREQ"; - private const string MAP_SHARING_DISABLED_MESSAGE = "MAPSDISABLED"; - private const string CHEAT_DETECTED_MESSAGE = "CD"; - private const string DICE_ROLL_MESSAGE = "DR"; - private const string CHANGE_TUNNEL_SERVER_MESSAGE = "CHTNL"; - private const string GAME_START_MESSAGE = "START"; - private const string GAME_START_MESSAGE_V3 = "STARTV3"; - private const string TUNNEL_CONNECTION_OK_MESSAGE = "TNLOK"; - private const string TUNNEL_CONNECTION_FAIL_MESSAGE = "TNLFAIL"; - - #endregion - #region Priorities private const int PRIORITY_START_GAME = 10; @@ -81,35 +65,35 @@ public CnCNetGameLobby( ctcpCommandHandlers = new CommandHandlerBase[] { - new IntCommandHandler("OR", (playerName, options) => HandleOptionsRequestAsync(playerName, options).HandleTask()), - new IntCommandHandler("R", (playerName, options) => HandleReadyRequestAsync(playerName, options).HandleTask()), - new StringCommandHandler("PO", ApplyPlayerOptions), - new StringCommandHandler(PlayerExtraOptions.CNCNET_MESSAGE_KEY, ApplyPlayerExtraOptions), - new StringCommandHandler("GO", (sender, message) => ApplyGameOptionsAsync(sender, message).HandleTask()), - new StringCommandHandler(GAME_START_MESSAGE, (sender, message) => NonHostLaunchGameAsync(sender, message).HandleTask()), - new StringCommandHandler(GAME_START_MESSAGE_V3, HandleGameStartV3TunnelMessage), - new NoParamCommandHandler(TUNNEL_CONNECTION_OK_MESSAGE, playerName => HandleTunnelConnectedAsync(playerName).HandleTask()), - new NoParamCommandHandler(TUNNEL_CONNECTION_FAIL_MESSAGE, HandleTunnelFail), - new NotificationHandler("AISPECS", HandleNotification, () => AISpectatorsNotificationAsync().HandleTask()), - new NotificationHandler("GETREADY", HandleNotification, () => GetReadyNotificationAsync().HandleTask()), - new NotificationHandler("INSFSPLRS", HandleNotification, () => InsufficientPlayersNotificationAsync().HandleTask()), - new NotificationHandler("TMPLRS", HandleNotification, () => TooManyPlayersNotificationAsync().HandleTask()), - new NotificationHandler("CLRS", HandleNotification, () => SharedColorsNotificationAsync().HandleTask()), - new NotificationHandler("SLOC", HandleNotification, () => SharedStartingLocationNotificationAsync().HandleTask()), - new NotificationHandler("LCKGME", HandleNotification, () => LockGameNotificationAsync().HandleTask()), - new IntNotificationHandler("NVRFY", HandleIntNotification, playerIndex => NotVerifiedNotificationAsync(playerIndex).HandleTask()), - new IntNotificationHandler("INGM", HandleIntNotification, playerIndex => StillInGameNotificationAsync(playerIndex).HandleTask()), - new StringCommandHandler(MAP_SHARING_UPLOAD_REQUEST, HandleMapUploadRequest), - new StringCommandHandler(MAP_SHARING_FAIL_MESSAGE, HandleMapTransferFailMessage), - new StringCommandHandler(MAP_SHARING_DOWNLOAD_REQUEST, HandleMapDownloadRequest), - new NoParamCommandHandler(MAP_SHARING_DISABLED_MESSAGE, HandleMapSharingBlockedMessage), - new NoParamCommandHandler("RETURN", ReturnNotification), - new IntCommandHandler("TNLPNG", HandleTunnelPing), - new StringCommandHandler("FHSH", (sender, filesHash) => FileHashNotificationAsync(sender, filesHash).HandleTask()), - new StringCommandHandler("MM", CheaterNotification), - new StringCommandHandler(DICE_ROLL_MESSAGE, HandleDiceRollResult), - new NoParamCommandHandler(CHEAT_DETECTED_MESSAGE, HandleCheatDetectedMessage), - new StringCommandHandler(CHANGE_TUNNEL_SERVER_MESSAGE, (sender, tunnelAddressAndPort) => HandleTunnelServerChangeMessageAsync(sender, tunnelAddressAndPort).HandleTask()) + new IntCommandHandler(CnCNetCommands.OPTIONS_REQUEST, (playerName, options) => HandleOptionsRequestAsync(playerName, options).HandleTask()), + new IntCommandHandler(CnCNetCommands.READY_REQUEST, (playerName, options) => HandleReadyRequestAsync(playerName, options).HandleTask()), + new StringCommandHandler(CnCNetCommands.PLAYER_OPTIONS, ApplyPlayerOptions), + new StringCommandHandler(CnCNetCommands.PLAYER_EXTRA_OPTIONS, ApplyPlayerExtraOptions), + new StringCommandHandler(CnCNetCommands.GAME_OPTIONS, (sender, message) => ApplyGameOptionsAsync(sender, message).HandleTask()), + new StringCommandHandler(CnCNetCommands.GAME_START_V2, (sender, message) => NonHostLaunchGameAsync(sender, message).HandleTask()), + new StringCommandHandler(CnCNetCommands.GAME_START_V3, HandleGameStartV3TunnelMessage), + new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_OK, playerName => HandleTunnelConnectedAsync(playerName).HandleTask()), + new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_FAIL, HandleTunnelFail), + new NotificationHandler(CnCNetCommands.AI_SPECTATORS, HandleNotification, () => AISpectatorsNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.GET_READY_LOBBY, HandleNotification, () => GetReadyNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.INSUFFICIENT_PLAYERS, HandleNotification, () => InsufficientPlayersNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.TOO_MANY_PLAYERS, HandleNotification, () => TooManyPlayersNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.SHARED_COLORS, HandleNotification, () => SharedColorsNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.SHARED_STARTING_LOCATIONS, HandleNotification, () => SharedStartingLocationNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.LOCK_GAME, HandleNotification, () => LockGameNotificationAsync().HandleTask()), + new IntNotificationHandler(CnCNetCommands.NOT_VERIFIED, HandleIntNotification, playerIndex => NotVerifiedNotificationAsync(playerIndex).HandleTask()), + new IntNotificationHandler(CnCNetCommands.STILL_IN_GAME, HandleIntNotification, playerIndex => StillInGameNotificationAsync(playerIndex).HandleTask()), + new StringCommandHandler(CnCNetCommands.MAP_SHARING_UPLOAD, HandleMapUploadRequest), + new StringCommandHandler(CnCNetCommands.MAP_SHARING_FAIL, HandleMapTransferFailMessage), + new StringCommandHandler(CnCNetCommands.MAP_SHARING_DOWNLOAD, HandleMapDownloadRequest), + new NoParamCommandHandler(CnCNetCommands.MAP_SHARING_DISABLED, HandleMapSharingBlockedMessage), + new NoParamCommandHandler(CnCNetCommands.RETURN, ReturnNotification), + new IntCommandHandler(CnCNetCommands.TUNNEL_PING, HandleTunnelPing), + new StringCommandHandler(CnCNetCommands.FILE_HASH, (sender, filesHash) => FileHashNotificationAsync(sender, filesHash).HandleTask()), + new StringCommandHandler(CnCNetCommands.CHEATER, CheaterNotification), + new StringCommandHandler(CnCNetCommands.DICE_ROLL, HandleDiceRollResult), + new NoParamCommandHandler(CnCNetCommands.CHEAT_DETECTED, HandleCheatDetectedMessage), + new StringCommandHandler(CnCNetCommands.CHANGE_TUNNEL_SERVER, (sender, tunnelAddressAndPort) => HandleTunnelServerChangeMessageAsync(sender, tunnelAddressAndPort).HandleTask()) }; MapSharer.MapDownloadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapDownloadFailedAsync(e).HandleTask()); @@ -117,17 +101,17 @@ public CnCNetGameLobby( MapSharer.MapUploadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapUploadFailedAsync(e).HandleTask()); MapSharer.MapUploadComplete += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapUploadCompleteAsync(e).HandleTask()); - AddChatBoxCommand(new ChatBoxCommand( + AddChatBoxCommand(new( "TUNNELINFO", "View tunnel server information".L10N("Client:Main:TunnelInfoCommand"), false, PrintTunnelServerInformation)); - AddChatBoxCommand(new ChatBoxCommand( + AddChatBoxCommand(new( "CHANGETUNNEL", "Change the used CnCNet tunnel server (game host only)".L10N("Client:Main:ChangeTunnelCommand"), true, - _ => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServerCommand")))); - AddChatBoxCommand(new ChatBoxCommand( + _ => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")))); + AddChatBoxCommand(new( "DOWNLOADMAP", "Download a map from CNCNet's map server using a map ID and an optional filename.\nExample: \"/downloadmap MAPID [2] My Battle Map\"".L10N("Client:Main:DownloadMapCommandDescription"), false, @@ -136,22 +120,22 @@ public CnCNetGameLobby( public event EventHandler GameLeft; - private TunnelHandler tunnelHandler; + private readonly TunnelHandler tunnelHandler; private TunnelSelectionWindow tunnelSelectionWindow; private XNAClientButton btnChangeTunnel; private Channel channel; - private CnCNetManager connectionManager; - private string localGame; + private readonly CnCNetManager connectionManager; + private readonly string localGame; - private GameCollection gameCollection; - private CnCNetUserData cncnetUserData; + private readonly GameCollection gameCollection; + private readonly CnCNetUserData cncnetUserData; private readonly PrivateMessagingWindow pmWindow; private GlobalContextMenu globalContextMenu; private string hostName; - private CommandHandlerBase[] ctcpCommandHandlers; + private readonly CommandHandlerBase[] ctcpCommandHandlers; private IRCColor chatColor; @@ -165,15 +149,16 @@ public CnCNetGameLobby( private bool isCustomPassword; private bool isP2P; - private List tunnelPlayerIds = new List(); + private readonly List tunnelPlayerIds = new(); private bool[] isPlayerConnectedToTunnel; private GameTunnelHandler gameTunnelHandler; private bool isStartingGame; private string gameFilesHash; - private List hostUploadedMaps = new List(); - private List chatCommandDownloadedMaps = new List(); + private readonly List hostUploadedMaps = new(); + private readonly List chatCommandDownloadedMaps = new(); + private List tunnels = new(); private MapSharingConfirmationPanel mapSharingConfirmationPanel; @@ -208,25 +193,25 @@ public override void Initialize() IniNameOverride = nameof(CnCNetGameLobby); base.Initialize(); - gameTunnelHandler = new GameTunnelHandler(); + gameTunnelHandler = new(); gameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); gameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); btnChangeTunnel = FindChild(nameof(btnChangeTunnel)); btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick; - gameBroadcastTimer = new XNATimerControl(WindowManager); + gameBroadcastTimer = new(WindowManager); gameBroadcastTimer.AutoReset = true; gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL); gameBroadcastTimer.Enabled = false; gameBroadcastTimer.TimeElapsed += (_, _) => BroadcastGameAsync().HandleTask(); - gameStartTimer = new XNATimerControl(WindowManager); + gameStartTimer = new(WindowManager); gameStartTimer.AutoReset = false; gameStartTimer.Interval = TimeSpan.FromSeconds(MAX_TIME_FOR_GAME_LAUNCH); gameStartTimer.TimeElapsed += GameStartTimer_TimeElapsed; - tunnelSelectionWindow = new TunnelSelectionWindow(WindowManager, tunnelHandler); + tunnelSelectionWindow = new(WindowManager, tunnelHandler); tunnelSelectionWindow.Initialize(); tunnelSelectionWindow.DrawOrder = 1; tunnelSelectionWindow.UpdateOrder = 1; @@ -235,13 +220,13 @@ public override void Initialize() tunnelSelectionWindow.Disable(); tunnelSelectionWindow.TunnelSelected += (_, e) => TunnelSelectionWindow_TunnelSelectedAsync(e).HandleTask(); - mapSharingConfirmationPanel = new MapSharingConfirmationPanel(WindowManager); + mapSharingConfirmationPanel = new(WindowManager); MapPreviewBox.AddChild(mapSharingConfirmationPanel); mapSharingConfirmationPanel.MapDownloadConfirmed += MapSharingConfirmationPanel_MapDownloadConfirmed; WindowManager.AddAndInitializeControl(gameBroadcastTimer); - globalContextMenu = new GlobalContextMenu(WindowManager, connectionManager, cncnetUserData, pmWindow); + globalContextMenu = new(WindowManager, connectionManager, cncnetUserData, pmWindow); AddChild(globalContextMenu); AddChild(gameStartTimer); @@ -276,7 +261,6 @@ private void GameStartTimer_TimeElapsed(object sender, EventArgs e) AddNotice($"Some players ({playerString}) failed to connect within the time limit. " + $"Aborting game launch."); - AbortGameStart(); } @@ -322,6 +306,8 @@ public async Task SetUpAsync(Channel channel, bool isHost, int playerLimit, btnChangeTunnel.Disable(); } + tunnels = tunnelHandler.Tunnels; + tunnelHandler.CurrentTunnel = tunnel; tunnelHandler.CurrentTunnelPinged += tunnelHandler_CurrentTunnelFunc; @@ -340,13 +326,13 @@ public async Task OnJoinedAsync() if (IsHost) { - await connectionManager.SendCustomMessageAsync(new QueuedMessage( - string.Format("MODE {0} +klnNs {1} {2}", channel.ChannelName, + await connectionManager.SendCustomMessageAsync(new( + string.Format(IRCCommands.MODE + " {0} +klnNs {1} {2}", channel.ChannelName, channel.Password, playerLimit), QueuedMessageType.SYSTEM_MESSAGE, 50)); - await connectionManager.SendCustomMessageAsync(new QueuedMessage( - string.Format("TOPIC {0} :{1}", channel.ChannelName, + await connectionManager.SendCustomMessageAsync(new( + string.Format(IRCCommands.TOPIC + " {0} :{1}", channel.ChannelName, ProgramConstants.CNCNET_PROTOCOL_REVISION + ";" + localGame.ToLower()), QueuedMessageType.SYSTEM_MESSAGE, 50)); @@ -356,7 +342,7 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage( } else { - await channel.SendCTCPMessageAsync("FHSH " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.FILE_HASH + " " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); } TopBar.AddPrimarySwitchable(this); @@ -372,7 +358,7 @@ private async Task UpdatePingAsync() if (tunnelHandler.CurrentTunnel == null) return; - await channel.SendCTCPMessageAsync("TNLPNG " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_PING + " " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); PlayerInfo pInfo = Players.Find(p => p.Name.Equals(ProgramConstants.PLAYERNAME)); if (pInfo != null) @@ -413,7 +399,7 @@ private void ShowTunnelSelectionWindow(string description) private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) { await channel.SendCTCPMessageAsync( - $"{CHANGE_TUNNEL_SERVER_MESSAGE} {e.Tunnel.Address}:{e.Tunnel.Port}", + $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Address}:{e.Tunnel.Port}", QueuedMessageType.SYSTEM_MESSAGE, 10); await HandleTunnelServerChangeAsync(e.Tunnel); @@ -525,7 +511,7 @@ private async Task Channel_UserQuitIRCAsync(UserNameEventArgs e) if (e.UserName == hostName) { - connectionManager.MainChannel.AddMessage(new ChatMessage( + connectionManager.MainChannel.AddMessage(new( ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); await BtnLeaveGame_LeftClickAsync(); } @@ -541,7 +527,7 @@ private async Task Channel_UserLeftAsync(UserNameEventArgs e) if (e.UserName == hostName) { - connectionManager.MainChannel.AddMessage(new ChatMessage( + connectionManager.MainChannel.AddMessage(new( ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); await BtnLeaveGame_LeftClickAsync(); } @@ -555,7 +541,7 @@ private async Task Channel_UserKickedAsync(UserNameEventArgs e) { if (e.UserName == ProgramConstants.PLAYERNAME) { - connectionManager.MainChannel.AddMessage(new ChatMessage( + connectionManager.MainChannel.AddMessage(new( ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); await ClearAsync(); Visible = false; @@ -580,7 +566,7 @@ private async Task Channel_UserListReceivedAsync() { if (channel.Users.Find(hostName) == null) { - connectionManager.MainChannel.AddMessage(new ChatMessage( + connectionManager.MainChannel.AddMessage(new( ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); await BtnLeaveGame_LeftClickAsync(); } @@ -754,7 +740,7 @@ private async Task StartGame_V2TunnelAsync() return; } - StringBuilder sb = new StringBuilder(GAME_START_MESSAGE + " "); + StringBuilder sb = new StringBuilder(CnCNetCommands.GAME_START_V2 + " "); sb.Append(UniqueGameID); for (int pId = 0; pId < Players.Count; pId++) { @@ -779,7 +765,7 @@ private async Task StartGame_V3TunnelAsync() Random random = new Random(); uint randomNumber = (uint)random.Next(0, int.MaxValue - (MAX_PLAYER_COUNT / 2)) * (uint)random.Next(1, 3); - StringBuilder sb = new StringBuilder(GAME_START_MESSAGE_V3 + " "); + StringBuilder sb = new StringBuilder(CnCNetCommands.GAME_START_V3 + " "); sb.Append(UniqueGameID); tunnelPlayerIds.Clear(); for (int i = 0; i < Players.Count; i++) @@ -837,12 +823,12 @@ private void ContactTunnel() private async Task GameTunnelHandler_Connected_CallbackAsync() { isPlayerConnectedToTunnel[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)] = true; - await channel.SendCTCPMessageAsync(TUNNEL_CONNECTION_OK_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_OK, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); } private async Task GameTunnelHandler_ConnectionFailed_CallbackAsync() { - await channel.SendCTCPMessageAsync(TUNNEL_CONNECTION_FAIL_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_FAIL, QueuedMessageType.INSTANT_MESSAGE, 0); HandleTunnelFail(ProgramConstants.PLAYERNAME); } @@ -924,7 +910,7 @@ protected override Task RequestPlayerOptionsAsync(int side, int color, int start int intValue = BitConverter.ToInt32(value, 0); return channel.SendCTCPMessageAsync( - string.Format("OR {0}", intValue), + FormattableString.Invariant($"{CnCNetCommands.OPTIONS_REQUEST} {intValue}"), QueuedMessageType.GAME_SETTINGS_MESSAGE, 6); } @@ -936,7 +922,7 @@ protected override async Task RequestReadyStatusAsync() "you will be unable to participate in the match.").L10N("Client:Main:HostMustReplaceMap")); if (chkAutoReady.Checked) - await channel.SendCTCPMessageAsync("R 0", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); + await channel.SendCTCPMessageAsync(CnCNetCommands.READY_REQUEST + " 0", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); return; } @@ -949,10 +935,10 @@ protected override async Task RequestReadyStatusAsync() else if (!pInfo.Ready) readyState = 1; - await channel.SendCTCPMessageAsync($"R {readyState}", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); + await channel.SendCTCPMessageAsync($"{CnCNetCommands.READY_REQUEST} {readyState}", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); } - protected override void AddNotice(string message, Color color) => channel.AddMessage(new ChatMessage(color, message)); + protected override void AddNotice(string message, Color color) => channel.AddMessage(new(color, message)); /// /// Handles player option requests received from non-host players. @@ -1045,7 +1031,7 @@ private async Task HandleReadyRequestAsync(string playerName, int readyStatus) protected override Task BroadcastPlayerOptionsAsync() { // Broadcast player options - StringBuilder sb = new StringBuilder("PO "); + StringBuilder sb = new StringBuilder(CnCNetCommands.PLAYER_OPTIONS + " "); foreach (PlayerInfo pInfo in Players.Concat(AIPlayers)) { if (pInfo.IsAI) @@ -1222,7 +1208,7 @@ protected override async Task OnGameOptionChangedAsync() int integerCount = byteList.Count / 4; byte[] byteArray = byteList.ToArray(); - ExtendedStringBuilder sb = new ExtendedStringBuilder("GO ", true, ';'); + ExtendedStringBuilder sb = new ExtendedStringBuilder(CnCNetCommands.GAME_OPTIONS + " ", true, ';'); for (int i = 0; i < integerCount; i++) sb.Append(BitConverter.ToInt32(byteArray, i * 4)); @@ -1428,7 +1414,7 @@ private async Task RequestMapAsync() AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + " " + ("Because you've disabled map sharing, it cannot be transferred. The game host needs " + "to change the map or you will be unable to participate in the match.").L10N("Client:Main:MapSharingDisabledNotice")); - await channel.SendCTCPMessageAsync(MAP_SHARING_DISABLED_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DISABLED, QueuedMessageType.SYSTEM_MESSAGE, 9); } } @@ -1437,7 +1423,7 @@ private Task ShowOfficialMapMissingMessageAsync(string sha1) AddNotice(("The game host has selected an official map that doesn't exist on your installation. " + "This could mean that the game host has modified game files, or is running a different game version. " + "They need to change the map or you will be unable to participate in the match.").L10N("Client:Main:OfficialMapNotExist")); - return channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + sha1, QueuedMessageType.SYSTEM_MESSAGE, 9); + return channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + sha1, QueuedMessageType.SYSTEM_MESSAGE, 9); } private void MapSharingConfirmationPanel_MapDownloadConfirmed(object sender, EventArgs e) @@ -1461,7 +1447,7 @@ protected override Task ChangeMapAsync(GameModeMap gameModeMap) protected override async Task GameProcessExitedAsync() { await base.GameProcessExitedAsync(); - await channel.SendCTCPMessageAsync("RETURN", QueuedMessageType.SYSTEM_MESSAGE, 20); + await channel.SendCTCPMessageAsync(CnCNetCommands.RETURN, QueuedMessageType.SYSTEM_MESSAGE, 20); ReturnNotification(ProgramConstants.PLAYERNAME); if (IsHost) @@ -1542,7 +1528,7 @@ protected override async Task StartGameAsync() if (gameFilesHash != fhc.GetCompleteHash()) { Logger.Log("Game files modified during client session!"); - await channel.SendCTCPMessageAsync(CHEAT_DETECTED_MESSAGE, QueuedMessageType.INSTANT_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.CHEAT_DETECTED, QueuedMessageType.INSTANT_MESSAGE, 0); HandleCheatDetectedMessage(ProgramConstants.PLAYERNAME); } @@ -1599,7 +1585,7 @@ protected override async Task GetReadyNotificationAsync() TopBar.SwitchToPrimary(); if (IsHost) - await channel.SendCTCPMessageAsync("GETREADY", QueuedMessageType.GAME_GET_READY_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.GET_READY_LOBBY, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); } protected override async Task AISpectatorsNotificationAsync() @@ -1607,7 +1593,7 @@ protected override async Task AISpectatorsNotificationAsync() await base.AISpectatorsNotificationAsync(); if (IsHost) - await channel.SendCTCPMessageAsync("AISPECS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.AI_SPECTATORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task InsufficientPlayersNotificationAsync() @@ -1615,7 +1601,7 @@ protected override async Task InsufficientPlayersNotificationAsync() await base.InsufficientPlayersNotificationAsync(); if (IsHost) - await channel.SendCTCPMessageAsync("INSFSPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.INSUFFICIENT_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task TooManyPlayersNotificationAsync() @@ -1623,7 +1609,7 @@ protected override async Task TooManyPlayersNotificationAsync() await base.TooManyPlayersNotificationAsync(); if (IsHost) - await channel.SendCTCPMessageAsync("TMPLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.TOO_MANY_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task SharedColorsNotificationAsync() @@ -1631,7 +1617,7 @@ protected override async Task SharedColorsNotificationAsync() await base.SharedColorsNotificationAsync(); if (IsHost) - await channel.SendCTCPMessageAsync("CLRS", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_COLORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task SharedStartingLocationNotificationAsync() @@ -1639,7 +1625,7 @@ protected override async Task SharedStartingLocationNotificationAsync() await base.SharedStartingLocationNotificationAsync(); if (IsHost) - await channel.SendCTCPMessageAsync("SLOC", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_STARTING_LOCATIONS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task LockGameNotificationAsync() @@ -1647,7 +1633,7 @@ protected override async Task LockGameNotificationAsync() await base.LockGameNotificationAsync(); if (IsHost) - await channel.SendCTCPMessageAsync("LCKGME", QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.LOCK_GAME, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task NotVerifiedNotificationAsync(int playerIndex) @@ -1655,7 +1641,7 @@ protected override async Task NotVerifiedNotificationAsync(int playerIndex) await base.NotVerifiedNotificationAsync(playerIndex); if (IsHost) - await channel.SendCTCPMessageAsync("NVRFY " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.NOT_VERIFIED + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } protected override async Task StillInGameNotificationAsync(int playerIndex) @@ -1663,7 +1649,7 @@ protected override async Task StillInGameNotificationAsync(int playerIndex) await base.StillInGameNotificationAsync(playerIndex); if (IsHost) - await channel.SendCTCPMessageAsync("INGM " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.STILL_IN_GAME + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } private void ReturnNotification(string sender) @@ -1703,7 +1689,7 @@ private async Task FileHashNotificationAsync(string sender, string filesHash) if (filesHash != gameFilesHash) { - await channel.SendCTCPMessageAsync("MM " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.CHEATER + " " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10); CheaterNotification(ProgramConstants.PLAYERNAME, sender); } } @@ -1719,7 +1705,7 @@ private void CheaterNotification(string sender, string cheaterName) protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) { string resultString = string.Join(",", results); - await channel.SendCTCPMessageAsync($"{DICE_ROLL_MESSAGE} {dieSides},{resultString}", QueuedMessageType.CHAT_MESSAGE, 0); + await channel.SendCTCPMessageAsync($"{CnCNetCommands.DICE_ROLL} {dieSides},{resultString}", QueuedMessageType.CHAT_MESSAGE, 0); PrintDiceRollResult(ProgramConstants.PLAYERNAME, dieSides, results); } @@ -1747,8 +1733,8 @@ protected override async Task HandleLockGameButtonClickAsync() protected override async Task LockGameAsync() { - await connectionManager.SendCustomMessageAsync(new QueuedMessage( - string.Format("MODE {0} +i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); + await connectionManager.SendCustomMessageAsync(new( + string.Format(IRCCommands.MODE + " {0} +i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); Locked = true; btnLockGame.Text = "Unlock Game".L10N("Client:Main:UnlockGame"); @@ -1757,8 +1743,8 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage( protected override async Task UnlockGameAsync(bool announce) { - await connectionManager.SendCustomMessageAsync(new QueuedMessage( - string.Format("MODE {0} -i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); + await connectionManager.SendCustomMessageAsync(new( + string.Format(IRCCommands.MODE + " {0} -i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); Locked = false; if (announce) @@ -1852,7 +1838,7 @@ private async Task MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) AddNotice("Download of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:DownloadCustomMapFailed")); mapSharingConfirmationPanel.SetFailedStatus(); - await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); return; } @@ -1867,7 +1853,7 @@ private async Task MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("Client:Main:RequestHostUploadMapToDB")); - await channel.SendCTCPMessageAsync(MAP_SHARING_UPLOAD_REQUEST + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_UPLOAD + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } private async Task MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) @@ -1898,7 +1884,7 @@ private async Task MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) AddNotice(returnMessage, Color.Red); AddNotice("Transfer of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:MapTransferFailed")); mapSharingConfirmationPanel.SetFailedStatus(); - await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } } @@ -1912,7 +1898,7 @@ private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) if (map == Map) { AddNotice("You need to change the map or some players won't be able to participate in this match.".L10N("Client:Main:YouMustReplaceMap")); - await channel.SendCTCPMessageAsync(MAP_SHARING_FAIL_MESSAGE + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } } @@ -1923,7 +1909,7 @@ private async Task MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); if (e.Map == Map) { - await channel.SendCTCPMessageAsync(MAP_SHARING_DOWNLOAD_REQUEST + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DOWNLOAD + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } } @@ -2094,7 +2080,7 @@ private void DownloadMapByIdCommand(string parameters) char replaceUnsafeCharactersWith = '-'; // Use a hashset instead of an array for quick lookups in `invalidChars.Contains()`. HashSet invalidChars = new HashSet(Path.GetInvalidFileNameChars()); - string safeMapName = new String(mapName.Select(c => invalidChars.Contains(c) ? replaceUnsafeCharactersWith : c).ToArray()); + string safeMapName = new(mapName.Select(c => invalidChars.Contains(c) ? replaceUnsafeCharactersWith : c).ToArray()); chatCommandDownloadedMaps.Add(sha1); @@ -2128,7 +2114,7 @@ private async Task BroadcastGameAsync() if (GameMode == null || Map == null) return; - StringBuilder sb = new StringBuilder("GAME "); + StringBuilder sb = new StringBuilder(CnCNetCommands.GAME + " "); sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); sb.Append(";"); sb.Append(ProgramConstants.GAME_VERSION); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index d18fd8f3a..a1412649c 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -30,20 +30,6 @@ internal sealed class LANGameLobby : MultiplayerGameLobby private const double DROPOUT_TIMEOUT = 20.0; private const double GAME_BROADCAST_INTERVAL = 10.0; - private const string CHAT_COMMAND = "GLCHAT"; - private const string RETURN_COMMAND = "RETURN"; - private const string GET_READY_COMMAND = "GETREADY"; - private const string PLAYER_OPTIONS_REQUEST_COMMAND = "POREQ"; - private const string PLAYER_OPTIONS_BROADCAST_COMMAND = "POPTS"; - private const string PLAYER_JOIN_COMMAND = "JOIN"; - private const string PLAYER_QUIT_COMMAND = "QUIT"; - private const string GAME_OPTIONS_COMMAND = "OPTS"; - private const string PLAYER_READY_REQUEST = "READY"; - private const string LAUNCH_GAME_COMMAND = "LAUNCH"; - private const string FILE_HASH_COMMAND = "FHASH"; - private const string DICE_ROLL_COMMAND = "DR"; - private const string PING = "PING"; - public LANGameLobby( WindowManager windowManager, string iniName, @@ -57,27 +43,27 @@ public LANGameLobby( encoding = Encoding.UTF8; hostCommandHandlers = new CommandHandlerBase[] { - new StringCommandHandler(CHAT_COMMAND, (sender, data) => GameHost_HandleChatCommandAsync(sender, data).HandleTask()), - new NoParamCommandHandler(RETURN_COMMAND, sender => GameHost_HandleReturnCommandAsync(sender).HandleTask()), - new StringCommandHandler(PLAYER_OPTIONS_REQUEST_COMMAND, (sender, data) => HandlePlayerOptionsRequestAsync(sender, data).HandleTask()), - new NoParamCommandHandler(PLAYER_QUIT_COMMAND, sender => HandlePlayerQuitAsync(sender).HandleTask()), - new StringCommandHandler(PLAYER_READY_REQUEST, (sender, autoReady) => GameHost_HandleReadyRequestAsync(sender, autoReady).HandleTask()), - new StringCommandHandler(FILE_HASH_COMMAND, HandleFileHashCommand), - new StringCommandHandler(DICE_ROLL_COMMAND, (sender, result) => Host_HandleDiceRollAsync(sender, result).HandleTask()), - new NoParamCommandHandler(PING, _ => { }) + new StringCommandHandler(LANCommands.CHAT_LOBBY_COMMAND, (sender, data) => GameHost_HandleChatCommandAsync(sender, data).HandleTask()), + new NoParamCommandHandler(LANCommands.RETURN, sender => GameHost_HandleReturnCommandAsync(sender).HandleTask()), + new StringCommandHandler(LANCommands.PLAYER_OPTIONS_REQUEST, (sender, data) => HandlePlayerOptionsRequestAsync(sender, data).HandleTask()), + new NoParamCommandHandler(LANCommands.PLAYER_QUIT_COMMAND, sender => HandlePlayerQuitAsync(sender).HandleTask()), + new StringCommandHandler(LANCommands.PLAYER_READY_REQUEST, (sender, autoReady) => GameHost_HandleReadyRequestAsync(sender, autoReady).HandleTask()), + new StringCommandHandler(LANCommands.FILE_HASH, HandleFileHashCommand), + new StringCommandHandler(LANCommands.DICE_ROLL, (sender, result) => Host_HandleDiceRollAsync(sender, result).HandleTask()), + new NoParamCommandHandler(LANCommands.PING, _ => { }) }; playerCommandHandlers = new LANClientCommandHandler[] { - new ClientStringCommandHandler(CHAT_COMMAND, Player_HandleChatCommand), - new ClientNoParamCommandHandler(GET_READY_COMMAND, () => HandleGetReadyCommandAsync().HandleTask()), - new ClientStringCommandHandler(RETURN_COMMAND, Player_HandleReturnCommand), - new ClientStringCommandHandler(PLAYER_OPTIONS_BROADCAST_COMMAND, HandlePlayerOptionsBroadcast), - new ClientStringCommandHandler(PlayerExtraOptions.LAN_MESSAGE_KEY, HandlePlayerExtraOptionsBroadcast), - new ClientStringCommandHandler(LAUNCH_GAME_COMMAND, gameId => HandleGameLaunchCommandAsync(gameId).HandleTask()), - new ClientStringCommandHandler(GAME_OPTIONS_COMMAND, data => HandleGameOptionsMessageAsync(data).HandleTask()), - new ClientStringCommandHandler(DICE_ROLL_COMMAND, Client_HandleDiceRoll), - new ClientNoParamCommandHandler(PING, () => HandlePingAsync().HandleTask()) + new ClientStringCommandHandler(LANCommands.CHAT_LOBBY_COMMAND, Player_HandleChatCommand), + new ClientNoParamCommandHandler(LANCommands.GET_READY, () => HandleGetReadyCommandAsync().HandleTask()), + new ClientStringCommandHandler(LANCommands.RETURN, Player_HandleReturnCommand), + new ClientStringCommandHandler(LANCommands.PLAYER_OPTIONS_BROADCAST, HandlePlayerOptionsBroadcast), + new ClientStringCommandHandler(LANCommands.PLAYER_EXTRA_OPTIONS, HandlePlayerExtraOptionsBroadcast), + new ClientStringCommandHandler(LANCommands.LAUNCH_GAME, gameId => HandleGameLaunchCommandAsync(gameId).HandleTask()), + new ClientStringCommandHandler(LANCommands.GAME_OPTIONS, data => HandleGameOptionsMessageAsync(data).HandleTask()), + new ClientStringCommandHandler(LANCommands.DICE_ROLL, Client_HandleDiceRoll), + new ClientNoParamCommandHandler(LANCommands.PING, () => HandlePingAsync().HandleTask()) }; localGame = ClientConfiguration.Instance.LocalGame; @@ -182,7 +168,7 @@ private async Task SendHostPlayerJoinedMessageAsync(CancellationToken cancellati await client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT, cancellationToken); - string message = PLAYER_JOIN_COMMAND + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME; + string message = LANCommands.PLAYER_JOIN + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME; const int charSize = sizeof(char); int bufferSize = message.Length * charSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); @@ -202,7 +188,7 @@ public async Task PostJoinAsync() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(GameModeMaps.GameModes); - await SendMessageToHostAsync(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash(), cancellationTokenSource?.Token ?? default); + await SendMessageToHostAsync(LANCommands.FILE_HASH + " " + fhc.GetCompleteHash(), cancellationTokenSource?.Token ?? default); ResetAutoReadyCheckbox(); } @@ -296,7 +282,7 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio string name = parts[1].Trim(); - if (parts[0] == "JOIN" && !string.IsNullOrEmpty(name)) + if (parts[0] == LANCommands.PLAYER_JOIN && !string.IsNullOrEmpty(name)) { lpInfo.Name = name; @@ -488,7 +474,7 @@ public override async Task ClearAsync() if (IsHost) { - await BroadcastMessageAsync(PLAYER_QUIT_COMMAND); + await BroadcastMessageAsync(LANCommands.PLAYER_QUIT_COMMAND); Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p)); Players.Clear(); cancellationTokenSource.Cancel(); @@ -496,7 +482,7 @@ public override async Task ClearAsync() } else { - await SendMessageToHostAsync(PLAYER_QUIT_COMMAND, cancellationTokenSource?.Token ?? default); + await SendMessageToHostAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationTokenSource?.Token ?? default); } if (client.Connected) @@ -524,7 +510,7 @@ protected override async Task BroadcastPlayerOptionsAsync() if (!IsHost) return; - var sb = new ExtendedStringBuilder(PLAYER_OPTIONS_BROADCAST_COMMAND + " ", true); + var sb = new ExtendedStringBuilder(LANCommands.PLAYER_OPTIONS_BROADCAST + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; foreach (PlayerInfo pInfo in Players.Concat(AIPlayers)) { @@ -554,7 +540,7 @@ protected override async Task BroadcastPlayerExtraOptionsAsync() await BroadcastMessageAsync(playerExtraOptions.ToLanMessage(), true); } - protected override Task HostLaunchGameAsync() => BroadcastMessageAsync(LAUNCH_GAME_COMMAND + " " + UniqueGameID); + protected override Task HostLaunchGameAsync() => BroadcastMessageAsync(LANCommands.LAUNCH_GAME + " " + UniqueGameID); protected override string GetIPAddressForPlayer(PlayerInfo player) { @@ -564,7 +550,7 @@ protected override string GetIPAddressForPlayer(PlayerInfo player) protected override Task RequestPlayerOptionsAsync(int side, int color, int start, int team) { - var sb = new ExtendedStringBuilder(PLAYER_OPTIONS_REQUEST_COMMAND + " ", true); + var sb = new ExtendedStringBuilder(LANCommands.PLAYER_OPTIONS_REQUEST + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(side); sb.Append(color); @@ -575,12 +561,12 @@ protected override Task RequestPlayerOptionsAsync(int side, int color, int start protected override Task RequestReadyStatusAsync() { - return SendMessageToHostAsync(PLAYER_READY_REQUEST + " " + Convert.ToInt32(chkAutoReady.Checked), cancellationTokenSource?.Token ?? default); + return SendMessageToHostAsync(LANCommands.PLAYER_READY_REQUEST + " " + Convert.ToInt32(chkAutoReady.Checked), cancellationTokenSource?.Token ?? default); } protected override Task SendChatMessageAsync(string message) { - var sb = new ExtendedStringBuilder(CHAT_COMMAND + " ", true); + var sb = new ExtendedStringBuilder(LANCommands.CHAT_LOBBY_COMMAND + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(chatColorIndex); sb.Append(message); @@ -594,7 +580,7 @@ protected override async Task OnGameOptionChangedAsync() if (!IsHost) return; - var sb = new ExtendedStringBuilder(GAME_OPTIONS_COMMAND + " ", true); + var sb = new ExtendedStringBuilder(LANCommands.GAME_OPTIONS + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; foreach (GameLobbyCheckBox chkBox in CheckBoxes) { @@ -623,7 +609,7 @@ protected override async Task GetReadyNotificationAsync() #endif if (IsHost) - await BroadcastMessageAsync(GET_READY_COMMAND); + await BroadcastMessageAsync(LANCommands.GET_READY); } protected override void ClearPingIndicators() @@ -714,7 +700,7 @@ protected override Task LockGameAsync() protected override async Task GameProcessExitedAsync() { await base.GameProcessExitedAsync(); - await SendMessageToHostAsync(RETURN_COMMAND, cancellationTokenSource?.Token ?? default); + await SendMessageToHostAsync(LANCommands.RETURN, cancellationTokenSource?.Token ?? default); if (IsHost) { @@ -787,7 +773,7 @@ private void BroadcastGame() if (GameMode == null || Map == null) return; - var sb = new ExtendedStringBuilder("GAME ", true); + var sb = new ExtendedStringBuilder(LANCommands.GAME + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(ProgramConstants.LAN_PROTOCOL_REVISION); sb.Append(ProgramConstants.GAME_VERSION); @@ -819,7 +805,7 @@ private async Task GameHost_HandleChatCommandAsync(string sender, string data) if (colorIndex < 0 || colorIndex >= chatColors.Length) return; - await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + data); + await BroadcastMessageAsync(LANCommands.CHAT_LOBBY_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + data); } private void Player_HandleChatCommand(string data) @@ -841,7 +827,7 @@ private void Player_HandleChatCommand(string data) } private Task GameHost_HandleReturnCommandAsync(string sender) - => BroadcastMessageAsync(RETURN_COMMAND + ProgramConstants.LAN_DATA_SEPARATOR + sender); + => BroadcastMessageAsync(LANCommands.RETURN + ProgramConstants.LAN_DATA_SEPARATOR + sender); private void Player_HandleReturnCommand(string sender) { @@ -1124,16 +1110,16 @@ private async Task HandleGameLaunchCommandAsync(string gameId) private Task HandlePingAsync() - => SendMessageToHostAsync(PING, cancellationTokenSource?.Token ?? default); + => SendMessageToHostAsync(LANCommands.PING, cancellationTokenSource?.Token ?? default); protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) { string resultString = string.Join(",", results); - await SendMessageToHostAsync($"DR {dieSides},{resultString}", cancellationTokenSource?.Token ?? default); + await SendMessageToHostAsync($"{LANCommands.DICE_ROLL} {dieSides},{resultString}", cancellationTokenSource?.Token ?? default); } private Task Host_HandleDiceRollAsync(string sender, string result) - => BroadcastMessageAsync($"{DICE_ROLL_COMMAND} {sender}{ProgramConstants.LAN_DATA_SEPARATOR}{result}"); + => BroadcastMessageAsync($"{LANCommands.DICE_ROLL} {sender}{ProgramConstants.LAN_DATA_SEPARATOR}{result}"); private void Client_HandleDiceRoll(string data) { diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index 9d13285f4..d11fd604c 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -26,14 +26,6 @@ internal sealed class LANGameLoadingLobby : GameLoadingLobbyBase private const double DROPOUT_TIMEOUT = 20.0; private const double GAME_BROADCAST_INTERVAL = 10.0; - private const string OPTIONS_COMMAND = "OPTS"; - private const string GAME_LAUNCH_COMMAND = "START"; - private const string READY_STATUS_COMMAND = "READY"; - private const string CHAT_COMMAND = "CHAT"; - private const string PLAYER_QUIT_COMMAND = "QUIT"; - private const string PLAYER_JOIN_COMMAND = "JOIN"; - private const string FILE_HASH_COMMAND = "FHASH"; - public LANGameLoadingLobby( WindowManager windowManager, LANColor[] chatColors, @@ -49,16 +41,16 @@ public LANGameLoadingLobby( hostCommandHandlers = new LANServerCommandHandler[] { - new ServerStringCommandHandler(CHAT_COMMAND, (sender, data) => Server_HandleChatMessageAsync(sender, data).HandleTask()), - new ServerStringCommandHandler(FILE_HASH_COMMAND, Server_HandleFileHashMessage), - new ServerNoParamCommandHandler(READY_STATUS_COMMAND, sender => Server_HandleReadyRequestAsync(sender).HandleTask()) + new ServerStringCommandHandler(LANCommands.CHAT_GAME_LOADING_COMMAND, (sender, data) => Server_HandleChatMessageAsync(sender, data).HandleTask()), + new ServerStringCommandHandler(LANCommands.FILE_HASH, Server_HandleFileHashMessage), + new ServerNoParamCommandHandler(LANCommands.READY_STATUS, sender => Server_HandleReadyRequestAsync(sender).HandleTask()) }; playerCommandHandlers = new LANClientCommandHandler[] { - new ClientStringCommandHandler(CHAT_COMMAND, Client_HandleChatMessage), - new ClientStringCommandHandler(OPTIONS_COMMAND, Client_HandleOptionsMessage), - new ClientNoParamCommandHandler(GAME_LAUNCH_COMMAND, Client_HandleStartCommand) + new ClientStringCommandHandler(LANCommands.CHAT_GAME_LOADING_COMMAND, Client_HandleChatMessage), + new ClientStringCommandHandler(LANCommands.OPTIONS, Client_HandleOptionsMessage), + new ClientNoParamCommandHandler(LANCommands.GAME_START, Client_HandleStartCommand) }; WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync().HandleTask(); @@ -119,7 +111,7 @@ public async Task SetUpAsync(bool isHost, Socket client, int loadedGameId) this.client = new Socket(SocketType.Stream, ProtocolType.Tcp); await this.client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT); - string message = PLAYER_JOIN_COMMAND + + string message = LANCommands.PLAYER_JOIN + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + loadedGameId; @@ -155,7 +147,7 @@ public async Task PostJoinAsync() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(gameModes); - await SendMessageToHostAsync(FILE_HASH_COMMAND + " " + fhc.GetCompleteHash(), cancellationTokenSource?.Token ?? default); + await SendMessageToHostAsync(LANCommands.FILE_HASH + " " + fhc.GetCompleteHash(), cancellationTokenSource?.Token ?? default); UpdateDiscordPresence(true); } @@ -235,7 +227,7 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio string name = parts[1].Trim(); int loadedGameId = Conversions.IntFromString(parts[2], -1); - if (parts[0] == "JOIN" && !string.IsNullOrEmpty(name) + if (parts[0] == LANCommands.PLAYER_JOIN && !string.IsNullOrEmpty(name) && loadedGameId == this.loadedGameId) { lpInfo.Name = name; @@ -408,14 +400,14 @@ private async Task ClearAsync() { if (IsHost) { - await BroadcastMessageAsync(PLAYER_QUIT_COMMAND, CancellationToken.None); + await BroadcastMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None); Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p)); Players.Clear(); listener.Close(); } else { - await SendMessageToHostAsync(PLAYER_QUIT_COMMAND, CancellationToken.None); + await SendMessageToHostAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None); } cancellationTokenSource.Cancel(); @@ -434,7 +426,7 @@ protected override async Task BroadcastOptionsAsync() if (Players.Count > 0) Players[0].Ready = true; - var sb = new ExtendedStringBuilder(OPTIONS_COMMAND + " ", true); + var sb = new ExtendedStringBuilder(LANCommands.OPTIONS + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(ddSavedGame.SelectedIndex); @@ -450,14 +442,14 @@ protected override async Task BroadcastOptionsAsync() } protected override Task HostStartGameAsync() - => BroadcastMessageAsync(GAME_LAUNCH_COMMAND, cancellationTokenSource?.Token ?? default); + => BroadcastMessageAsync(LANCommands.GAME_START, cancellationTokenSource?.Token ?? default); protected override Task RequestReadyStatusAsync() - => SendMessageToHostAsync(READY_STATUS_COMMAND, cancellationTokenSource?.Token ?? default); + => SendMessageToHostAsync(LANCommands.READY_STATUS, cancellationTokenSource?.Token ?? default); protected override async Task SendChatMessageAsync(string message) { - await SendMessageToHostAsync(CHAT_COMMAND + " " + chatColorIndex + + await SendMessageToHostAsync(LANCommands.CHAT_GAME_LOADING_COMMAND + " " + chatColorIndex + ProgramConstants.LAN_DATA_SEPARATOR + message, cancellationTokenSource?.Token ?? default); sndMessageSound.Play(); @@ -477,7 +469,7 @@ private async Task Server_HandleChatMessageAsync(LANPlayerInfo sender, string da if (colorIndex < 0 || colorIndex >= chatColors.Length) return; - await BroadcastMessageAsync(CHAT_COMMAND + " " + sender + + await BroadcastMessageAsync(LANCommands.CHAT_GAME_LOADING_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + colorIndex + ProgramConstants.LAN_DATA_SEPARATOR + data, cancellationTokenSource?.Token ?? default); } @@ -661,7 +653,7 @@ public override void Update(GameTime gameTime) private void BroadcastGame() { - var sb = new ExtendedStringBuilder("GAME ", true); + var sb = new ExtendedStringBuilder(LANCommands.GAME + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; sb.Append(ProgramConstants.LAN_PROTOCOL_REVISION); sb.Append(ProgramConstants.GAME_VERSION); diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 5fef4f205..db2a65cd2 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -267,7 +267,7 @@ private async Task WindowManager_GameClosingAsync(CancellationToken cancellation if (socket.IsBound) { - await SendMessageAsync("QUIT", cancellationToken); + await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationToken); cancellationTokenSource.Cancel(); socket.Close(); } @@ -411,7 +411,7 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) switch (command) { - case "ALIVE": + case LANCommands.ALIVE: if (parameters.Length < 2) return; @@ -433,7 +433,7 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) user.TimeWithoutRefresh = TimeSpan.Zero; break; - case "CHAT": + case LANCommands.CHAT: if (user == null) return; @@ -449,7 +449,7 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) chatColors[colorIndex].XNAColor, DateTime.Now, parameters[1])); break; - case "QUIT": + case LANCommands.QUIT: if (user == null) return; @@ -458,7 +458,7 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) players.RemoveAt(index); lbPlayerList.Items.RemoveAt(index); break; - case "GAME": + case LANCommands.GAME: if (user == null) return; @@ -484,7 +484,7 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) private async Task SendAliveAsync(CancellationToken cancellationToken) { - StringBuilder sb = new StringBuilder("ALIVE "); + StringBuilder sb = new StringBuilder(LANCommands.ALIVE + " "); sb.Append(localGameIndex); sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); sb.Append(ProgramConstants.PLAYERNAME); @@ -499,7 +499,7 @@ private async Task TbChatInput_EnterPressedAsync(CancellationToken cancellationT string chatMessage = tbChatInput.Text.Replace((char)01, '?'); - StringBuilder sb = new StringBuilder("CHAT "); + StringBuilder sb = new StringBuilder(LANCommands.CHAT + " "); sb.Append(ddColor.SelectedIndex); sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); sb.Append(chatMessage); @@ -568,7 +568,7 @@ private async Task JoinGameAsync() await lanGameLoadingLobby.SetUpAsync(false, client, loadedGameId); lanGameLoadingLobby.Enable(); - string message = "JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + + string message = LANCommands.PLAYER_JOIN + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + loadedGameId + ProgramConstants.LAN_MESSAGE_SEPARATOR; int bufferSize = message.Length * charSize; @@ -585,7 +585,7 @@ private async Task JoinGameAsync() await lanGameLobby.SetUpAsync(false, hg.EndPoint, client); lanGameLobby.Enable(); - string message = "JOIN" + ProgramConstants.LAN_DATA_SEPARATOR + + string message = LANCommands.PLAYER_JOIN + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + ProgramConstants.LAN_MESSAGE_SEPARATOR; int bufferSize = message.Length * charSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); @@ -609,7 +609,7 @@ private async Task BtnMainMenu_LeftClickAsync() { Visible = false; Enabled = false; - await SendMessageAsync("QUIT", CancellationToken.None); + await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None); cancellationTokenSource.Cancel(); socket.Close(); Exited?.Invoke(this, EventArgs.Empty); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetCommands.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetCommands.cs new file mode 100644 index 000000000..4a0d6650c --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetCommands.cs @@ -0,0 +1,45 @@ +#pragma warning disable SA1310 +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal static class CnCNetCommands +{ + public const string GAME_INVITE = "INVITE"; + public const string GAME_INVITATION_FAILED = "INVITATION_FAILED"; + public const string NOT_ALL_PLAYERS_PRESENT = "NPRSNT"; + public const string GET_READY = "GTRDY"; + public const string FILE_HASH = "FHSH"; + public const string INVALID_FILE_HASH = "IHSH"; + public const string TUNNEL_PING = "TNLPNG"; + public const string OPTIONS = "OP"; + public const string INVALID_SAVED_GAME_INDEX = "ISGI"; + public const string START_GAME = "START"; + public const string PLAYER_READY = "READY"; + public const string CHANGE_TUNNEL_SERVER = "CHTNL"; + public const string RETURN = "RETURN"; + public const string GET_READY_LOBBY = "GETREADY"; + public const string PLAYER_EXTRA_OPTIONS = "PEO"; + public const string MAP_SHARING_FAIL = "MAPFAIL"; + public const string MAP_SHARING_DOWNLOAD = "MAPOK"; + public const string MAP_SHARING_UPLOAD = "MAPREQ"; + public const string MAP_SHARING_DISABLED = "MAPSDISABLED"; + public const string CHEAT_DETECTED = "CD"; + public const string DICE_ROLL = "DR"; + public const string GAME_START_V3 = "STARTV3"; + public const string TUNNEL_CONNECTION_OK = "TNLOK"; + public const string TUNNEL_CONNECTION_FAIL = "TNLFAIL"; + public const string GAME_START_V2 = "START"; + public const string OPTIONS_REQUEST = "OR"; + public const string READY_REQUEST = "R"; + public const string PLAYER_OPTIONS = "PO"; + public const string GAME_OPTIONS = "GO"; + public const string AI_SPECTATORS = "AISPECS"; + public const string INSUFFICIENT_PLAYERS = "INSFSPLRS"; + public const string TOO_MANY_PLAYERS = "TMPLRS"; + public const string SHARED_COLORS = "CLRS"; + public const string SHARED_STARTING_LOCATIONS = "SLOC"; + public const string LOCK_GAME = "LCKGME"; + public const string NOT_VERIFIED = "NVRFY"; + public const string STILL_IN_GAME = "INGM"; + public const string CHEATER = "MM"; + public const string GAME = "GAME"; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/IRCCommands.cs b/DXMainClient/Domain/Multiplayer/CnCNet/IRCCommands.cs new file mode 100644 index 000000000..2f42e7ce3 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/IRCCommands.cs @@ -0,0 +1,23 @@ +#pragma warning disable SA1310 +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal static class IRCCommands +{ + public const string JOIN = "JOIN"; + public const string QUIT = "QUIT"; + public const string NOTICE = "NOTICE"; + public const string PART = "PART"; + public const string PRIVMSG = "PRIVMSG"; + public const string MODE = "MODE"; + public const string KICK = "KICK"; + public const string ERROR = "ERROR"; + public const string PING = "PING"; + public const string PONG = "PONG"; + public const string TOPIC = "TOPIC"; + public const string NICK = "NICK"; + public const string PRIVMSG_ACTION = "ACTION"; + public const string PING_LAG = "PING LAG"; + public const string AWAY = "AWAY"; + public const string WHOIS = "WHOIS"; + public const string USER = "USER"; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANCommands.cs b/DXMainClient/Domain/Multiplayer/LAN/LANCommands.cs new file mode 100644 index 000000000..3ef602c38 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/LAN/LANCommands.cs @@ -0,0 +1,28 @@ +#pragma warning disable SA1310 +namespace DTAClient.Domain.Multiplayer.LAN; + +internal static class LANCommands +{ + public const string PLAYER_READY_REQUEST = "READY"; + public const string CHAT_GAME_LOADING_COMMAND = "CHAT"; + public const string CHAT_LOBBY_COMMAND = "GLCHAT"; + public const string RETURN = "RETURN"; + public const string GET_READY = "GETREADY"; + public const string PLAYER_OPTIONS_REQUEST = "POREQ"; + public const string PLAYER_OPTIONS_BROADCAST = "POPTS"; + public const string PLAYER_JOIN = "JOIN"; + public const string PLAYER_QUIT_COMMAND = "QUIT"; + public const string GAME_OPTIONS = "OPTS"; + public const string LAUNCH_GAME = "LAUNCH"; + public const string FILE_HASH = "FHASH"; + public const string DICE_ROLL = "DR"; + public const string PING = "PING"; + public const string OPTIONS = "OPTS"; + public const string PLAYER_EXTRA_OPTIONS = "PEOPTS"; + public const string READY_STATUS = "READY"; + public const string GAME_START = "START"; + public const string CHAT = "CHAT"; + public const string ALIVE = "ALIVE"; + public const string QUIT = "QUIT"; + public const string GAME = "GAME"; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index 4a8478913..afb23ef6c 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -57,7 +57,7 @@ public async Task UpdateAsync(GameTime gameTime) if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT) || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)) - await SendMessageAsync("PING", cancellationTokenSource?.Token ?? default); + await SendMessageAsync(LANCommands.PING, cancellationTokenSource?.Token ?? default); if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT)) return false; diff --git a/DXMainClient/Domain/Multiplayer/PlayerExtraOptions.cs b/DXMainClient/Domain/Multiplayer/PlayerExtraOptions.cs index e3a34d210..11667ba0d 100644 --- a/DXMainClient/Domain/Multiplayer/PlayerExtraOptions.cs +++ b/DXMainClient/Domain/Multiplayer/PlayerExtraOptions.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using DTAClient.Domain.Multiplayer.CnCNet; +using DTAClient.Domain.Multiplayer.LAN; namespace DTAClient.Domain.Multiplayer { @@ -10,14 +12,10 @@ public class PlayerExtraOptions { private static string INVALID_OPTIONS_MESSAGE => "Invalid player extra options message".L10N("Client:Main:InvalidPlayerExtraOptionsMessage"); private static string MAPPING_ERROR_PREFIX => "Auto Allying:".L10N("Client:Main:AutoAllyingPrefix"); - protected static string NOT_ALL_MAPPINGS_ASSIGNED => MAPPING_ERROR_PREFIX + " " + "You must have all mappings assigned.".L10N("Client:Main:NotAllMappingsAssigned"); protected static string MULTIPLE_MAPPINGS_ASSIGNED_TO_SAME_START => MAPPING_ERROR_PREFIX + " " + "Multiple mappings assigned to the same start location.".L10N("Client:Main:MultipleMappingsAssigned"); protected static string ONLY_ONE_TEAM => MAPPING_ERROR_PREFIX + " " + "You must have more than one team assigned.".L10N("Client:Main:OnlyOneTeam"); private const char MESSAGE_SEPARATOR = ';'; - public const string CNCNET_MESSAGE_KEY = "PEO"; - public const string LAN_MESSAGE_KEY = "PEOPTS"; - public bool IsForceRandomSides { get; set; } public bool IsForceRandomColors { get; set; } public bool IsForceRandomTeams { get; set; } @@ -41,9 +39,9 @@ public string GetTeamMappingsError() return null; } - public string ToCncnetMessage() => $"{CNCNET_MESSAGE_KEY} {ToString()}"; + public string ToCncnetMessage() => $"{CnCNetCommands.PLAYER_EXTRA_OPTIONS} {ToString()}"; - public string ToLanMessage() => $"{LAN_MESSAGE_KEY} {ToString()}"; + public string ToLanMessage() => $"{LANCommands.PLAYER_EXTRA_OPTIONS} {ToString()}"; public override string ToString() { diff --git a/DXMainClient/Online/Channel.cs b/DXMainClient/Online/Channel.cs index 0f1a9bb11..27d89137b 100644 --- a/DXMainClient/Online/Channel.cs +++ b/DXMainClient/Online/Channel.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI; using ClientCore.Extensions; @@ -263,7 +264,7 @@ public Task SendChatMessageAsync(string message, IRCColor color) string colorString = (char)03 + color.IrcColorId.ToString("D2"); return connection.QueueMessageAsync(QueuedMessageType.CHAT_MESSAGE, 0, - "PRIVMSG " + ChannelName + " :" + colorString + message); + IRCCommands.PRIVMSG + " " + ChannelName + " :" + colorString + message); } /// @@ -289,7 +290,7 @@ public Task SendCTCPMessageAsync(string message, QueuedMessageType qmType, int p /// The priority of the message in the send queue. public Task SendKickMessageAsync(string userName, int priority) { - return connection.QueueMessageAsync(QueuedMessageType.INSTANT_MESSAGE, priority, "KICK " + ChannelName + " " + userName); + return connection.QueueMessageAsync(QueuedMessageType.INSTANT_MESSAGE, priority, IRCCommands.KICK + " " + ChannelName + " " + userName); } /// @@ -300,7 +301,7 @@ public Task SendKickMessageAsync(string userName, int priority) public Task SendBanMessageAsync(string host, int priority) { return connection.QueueMessageAsync(QueuedMessageType.INSTANT_MESSAGE, priority, - string.Format("MODE {0} +b *!*@{1}", ChannelName, host)); + string.Format(IRCCommands.MODE + " {0} +b *!*@{1}", ChannelName, host)); } public Task JoinAsync() @@ -311,22 +312,15 @@ public Task JoinAsync() int rn = connection.Rng.Next(1, 10000); if (string.IsNullOrEmpty(Password)) - return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "JOIN " + ChannelName); - else - return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "JOIN " + ChannelName + " " + Password); - } - else - { - if (string.IsNullOrEmpty(Password)) - return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, "JOIN " + ChannelName); - else - return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, "JOIN " + ChannelName + " " + Password); + return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, IRCCommands.JOIN + " " + ChannelName); + + return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, IRCCommands.JOIN + " " + ChannelName + " " + Password); } - } - public Task RequestUserInfoAsync() - { - return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, "WHO " + ChannelName); + if (string.IsNullOrEmpty(Password)) + return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, IRCCommands.JOIN + " " + ChannelName); + + return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, IRCCommands.JOIN + " " + ChannelName + " " + Password); } public async Task LeaveAsync() @@ -335,12 +329,13 @@ public async Task LeaveAsync() if (Persistent) { int rn = connection.Rng.Next(1, 10000); - await connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, "PART " + ChannelName); + await connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, IRCCommands.PART + " " + ChannelName); } else { - await connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, "PART " + ChannelName); + await connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, IRCCommands.PART + " " + ChannelName); } + ClearUsers(); } diff --git a/DXMainClient/Online/CnCNetManager.cs b/DXMainClient/Online/CnCNetManager.cs index 5d70adb60..621806ce3 100644 --- a/DXMainClient/Online/CnCNetManager.cs +++ b/DXMainClient/Online/CnCNetManager.cs @@ -11,6 +11,7 @@ using System.Text; using System.Threading.Tasks; using ClientCore.Extensions; +using DTAClient.Domain.Multiplayer.CnCNet; namespace DTAClient.Online { @@ -160,7 +161,7 @@ public Task SendCustomMessageAsync(QueuedMessage qm) public Task SendWhoIsMessageAsync(string nick) { - return SendCustomMessageAsync(new QueuedMessage($"WHOIS {nick}", QueuedMessageType.WHOIS_MESSAGE, 0)); + return SendCustomMessageAsync(new QueuedMessage($"{IRCCommands.WHOIS} {nick}", QueuedMessageType.WHOIS_MESSAGE, 0)); } public void OnAttemptedServerChanged(string serverName) @@ -308,7 +309,7 @@ private void DoChatMessageReceived(string receiver, string senderName, string id Color foreColor; // Handle ACTION - if (message.Contains("ACTION")) + if (message.Contains(IRCCommands.PRIVMSG_ACTION)) { message = message.Remove(0, 7); message = "====> " + senderName + " " + message; diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index 55e85cef6..e0da78d66 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -13,6 +13,7 @@ using System.Threading; using System.Threading.Tasks; using ClientCore.Extensions; +using DTAClient.Domain.Multiplayer.CnCNet; namespace DTAClient.Online { @@ -446,7 +447,7 @@ private async Task> GetServerListSortedByLatencyAsync() public async Task DisconnectAsync() { - await SendMessageAsync("QUIT"); + await SendMessageAsync(IRCCommands.QUIT); cancellationTokenSource.Cancel(); socket.Close(); } @@ -604,11 +605,11 @@ private async Task PerformCommandAsync(string message) switch (command) { - case "NOTICE": + case IRCCommands.NOTICE: int noticeExclamIndex = prefix.IndexOf('!'); if (noticeExclamIndex > -1) { - if (parameters.Count > 1 && parameters[1][0] == 1)//Conversions.IntFromString(parameters[1].Substring(0, 1), -1) == 1) + if (parameters.Count > 1 && parameters[1][0] == 1) { // CTCP string channelName = parameters[0]; @@ -619,20 +620,18 @@ private async Task PerformCommandAsync(string message) return; } - else - { - string noticeUserName = prefix[..noticeExclamIndex]; - string notice = parameters[parameters.Count - 1]; - connectionManager.OnNoticeMessageParsed(notice, noticeUserName); - break; - } + + string noticeUserName = prefix[..noticeExclamIndex]; + string notice = parameters[parameters.Count - 1]; + connectionManager.OnNoticeMessageParsed(notice, noticeUserName); + break; } string noticeParamString = string.Empty; foreach (string param in parameters) noticeParamString = noticeParamString + param + " "; connectionManager.OnGenericServerMessageReceived(prefix + " " + noticeParamString); break; - case "JOIN": + case IRCCommands.JOIN: string channel = parameters[0]; int atIndex = prefix.IndexOf('@'); int exclamIndex = prefix.IndexOf('!'); @@ -641,19 +640,19 @@ private async Task PerformCommandAsync(string message) string host = prefix[(atIndex + 1)..]; connectionManager.OnUserJoinedChannel(channel, host, userName, ident); break; - case "PART": + case IRCCommands.PART: string pChannel = parameters[0]; string pUserName = prefix[..prefix.IndexOf('!')]; connectionManager.OnUserLeftChannel(pChannel, pUserName); break; - case "QUIT": + case IRCCommands.QUIT: string qUserName = prefix[..prefix.IndexOf('!')]; connectionManager.OnUserQuitIRC(qUserName); break; - case "PRIVMSG": - if (parameters.Count > 1 && Convert.ToInt32(parameters[1][0]) == 1 && !parameters[1].Contains("ACTION")) + case IRCCommands.PRIVMSG: + if (parameters.Count > 1 && Convert.ToInt32(parameters[1][0]) == 1 && !parameters[1].Contains(IRCCommands.PRIVMSG_ACTION)) { - goto case "NOTICE"; + goto case IRCCommands.NOTICE; } string pmsgUserName = prefix[..prefix.IndexOf('!')]; string pmsgIdent = GetIdentFromPrefix(prefix); @@ -661,7 +660,7 @@ private async Task PerformCommandAsync(string message) for (int pid = 0; pid < parameters.Count - 1; pid++) recipients[pid] = parameters[pid]; string privmsg = parameters[parameters.Count - 1]; - if (parameters[1].StartsWith('\u0001' + "ACTION")) + if (parameters[1].StartsWith('\u0001' + IRCCommands.PRIVMSG_ACTION)) privmsg = privmsg[1..].Remove(privmsg.Length - 2); foreach (string recipient in recipients) { @@ -671,7 +670,7 @@ private async Task PerformCommandAsync(string message) connectionManager.OnPrivateMessageReceived(pmsgUserName, privmsg); } break; - case "MODE": + case IRCCommands.MODE: string modeUserName = prefix.Contains('!') ? prefix[..prefix.IndexOf('!')] : prefix; string modeChannelName = parameters[0]; string modeString = parameters[1]; @@ -679,34 +678,34 @@ private async Task PerformCommandAsync(string message) parameters.Count > 2 ? parameters.GetRange(2, parameters.Count - 2) : new List(); connectionManager.OnChannelModesChanged(modeUserName, modeChannelName, modeString, modeParameters); break; - case "KICK": + case IRCCommands.KICK: string kickChannelName = parameters[0]; string kickUserName = parameters[1]; connectionManager.OnUserKicked(kickChannelName, kickUserName); break; - case "ERROR": + case IRCCommands.ERROR: connectionManager.OnErrorReceived(message); break; - case "PING": + case IRCCommands.PING: if (parameters.Count > 0) { - await QueueMessageAsync(new QueuedMessage("PONG " + parameters[0], QueuedMessageType.SYSTEM_MESSAGE, 5000)); - Logger.Log("PONG " + parameters[0]); + await QueueMessageAsync(new QueuedMessage(IRCCommands.PONG + " " + parameters[0], QueuedMessageType.SYSTEM_MESSAGE, 5000)); + Logger.Log(IRCCommands.PONG + " " + parameters[0]); } else { - await QueueMessageAsync(new QueuedMessage("PONG", QueuedMessageType.SYSTEM_MESSAGE, 5000)); - Logger.Log("PONG"); + await QueueMessageAsync(new QueuedMessage(IRCCommands.PONG, QueuedMessageType.SYSTEM_MESSAGE, 5000)); + Logger.Log(IRCCommands.PONG); } break; - case "TOPIC": + case IRCCommands.TOPIC: if (parameters.Count < 2) break; connectionManager.OnChannelTopicChanged(prefix[..prefix.IndexOf('!')], parameters[0], parameters[1]); break; - case "NICK": + case IRCCommands.NICK: int nickExclamIndex = prefix.IndexOf('!'); if (nickExclamIndex > -1 || parameters.Count < 1) { @@ -875,7 +874,7 @@ private async Task RunSendQueueAsync(CancellationToken cancellationToken) /// Sends a PING message to the server to indicate that we're still connected. /// private Task AutoPingAsync() - => SendMessageAsync("PING LAG" + new Random().Next(100000, 999999)); + => SendMessageAsync(IRCCommands.PING_LAG + new Random().Next(100000, 999999)); /// /// Registers the user. @@ -891,15 +890,15 @@ private async Task RegisterAsync() string realname = ProgramConstants.GAME_VERSION + " " + defaultGame + " CnCNet"; - await SendMessageAsync(string.Format("USER {0} 0 * :{1}", defaultGame + "." + + await SendMessageAsync(string.Format(IRCCommands.USER + " {0} 0 * :{1}", defaultGame + "." + systemId, realname)); - await SendMessageAsync("NICK " + ProgramConstants.PLAYERNAME); + await SendMessageAsync(IRCCommands.NICK + " " + ProgramConstants.PLAYERNAME); } public Task ChangeNicknameAsync() { - return SendMessageAsync("NICK " + ProgramConstants.PLAYERNAME); + return SendMessageAsync(IRCCommands.NICK + " " + ProgramConstants.PLAYERNAME); } public Task QueueMessageAsync(QueuedMessageType type, int priority, string message, bool replace = false) diff --git a/DXMainClient/Online/QueuedMessage.cs b/DXMainClient/Online/QueuedMessage.cs index b980b5071..c87e030e8 100644 --- a/DXMainClient/Online/QueuedMessage.cs +++ b/DXMainClient/Online/QueuedMessage.cs @@ -9,13 +9,13 @@ public class QueuedMessage { private const int DEFAULT_DELAY = -1; private const int REPLACE_DELAY = 1; - - public QueuedMessage(string command, QueuedMessageType type, int priority) : + + public QueuedMessage(string command, QueuedMessageType type, int priority) : this(command, type, priority, DEFAULT_DELAY, false) { } - public QueuedMessage(string command, QueuedMessageType type, int priority, bool replace) : + public QueuedMessage(string command, QueuedMessageType type, int priority, bool replace) : this(command, type, priority, replace ? REPLACE_DELAY : DEFAULT_DELAY, replace) { } @@ -31,7 +31,7 @@ private QueuedMessage(string command, QueuedMessageType type, int priority, int MessageType = type; Priority = priority; Delay = delay; - SendAt = Delay < 0 ? DateTime.Now : DateTime.Now.AddMilliseconds(Delay); + SendAt = Delay < 0 ? DateTime.Now : DateTime.Now.AddMilliseconds(Delay); Replace = replace; } From 8b7380052318bc9a52fc8e18b9f92df83bb4946e Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 29 Nov 2022 13:23:16 +0100 Subject: [PATCH 043/109] Dynamic V3 tunnel selection --- ClientCore/ClientConfiguration.cs | 9 +- ClientCore/Extensions/TaskExtensions.cs | 7 +- ClientCore/ProgramConstants.cs | 4 +- ClientCore/Settings/UserINISettings.cs | 45 + DXMainClient/DXGUI/Generic/MainMenu.cs | 7 +- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 56 +- .../DXGUI/Multiplayer/GameInformationPanel.cs | 3 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 2 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 3453 +++++++++-------- .../Multiplayer/GameLobby/GameLobbyBase.cs | 52 +- .../Multiplayer/GameLobby/MapPreviewBox.cs | 2 +- .../GameLobby/MultiplayerGameLobby.cs | 25 +- .../GameLobby/PlayerLocationIndicator.cs | 2 +- .../Domain/Multiplayer/AllianceHolder.cs | 2 +- .../Multiplayer/CnCNet/CnCNetCommands.cs | 4 + .../Multiplayer/CnCNet/CnCNetLobbyCommands.cs | 19 + .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 67 +- .../Multiplayer/CnCNet/GameTunnelHandler.cs | 149 - .../Multiplayer/CnCNet/HostedCnCNetGame.cs | 2 +- .../Multiplayer/CnCNet/TunnelHandler.cs | 58 +- .../CnCNet/TunneledPlayerConnection.cs | 146 - .../Multiplayer/CnCNet/V3GameTunnelHandler.cs | 146 + .../Multiplayer/CnCNet/V3TunnelConnection.cs | 270 +- .../CnCNet/V3TunneledPlayerConnection.cs | 113 + .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 2 +- .../LAN/LANServerCommandHandler.cs | 4 +- .../LAN/ServerNoParamCommandHandler.cs | 10 +- .../LAN/ServerStringCommandHandler.cs | 10 +- .../Domain/Multiplayer/PlayerHouseInfo.cs | 21 +- DXMainClient/Domain/Multiplayer/PlayerInfo.cs | 20 +- DXMainClient/Online/Channel.cs | 5 + DXMainClient/Online/Connection.cs | 15 +- 32 files changed, 2503 insertions(+), 2227 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs delete mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs delete mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3TunneledPlayerConnection.cs diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 4f564082b..3d5f03504 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -53,14 +53,7 @@ protected ClientConfiguration() /// The object of the ClientConfiguration class. public static ClientConfiguration Instance { - get - { - if (_instance == null) - { - _instance = new ClientConfiguration(); - } - return _instance; - } + get { return _instance ??= new ClientConfiguration(); } } public void RefreshSettings() diff --git a/ClientCore/Extensions/TaskExtensions.cs b/ClientCore/Extensions/TaskExtensions.cs index 16bdc1776..6ad4f6c6e 100644 --- a/ClientCore/Extensions/TaskExtensions.cs +++ b/ClientCore/Extensions/TaskExtensions.cs @@ -6,7 +6,7 @@ namespace ClientCore.Extensions; public static class TaskExtensions { /// - /// Asynchronously awaits a and guarantees all exceptions are caught and handled when the is not directly awaited. + /// Runs a and guarantees all exceptions are caught and handled even when the is not directly awaited. /// /// The who's exceptions will be handled. /// Returns a that awaited and handled the original . @@ -23,7 +23,7 @@ public static async Task HandleTaskAsync(this Task task) } /// - /// Asynchronously awaits a and guarantees all exceptions are caught and handled when the is not directly awaited. + /// Runs a and guarantees all exceptions are caught and handled even when the is not directly awaited. /// /// The type of 's return value. /// The who's exceptions will be handled. @@ -43,7 +43,8 @@ public static async Task HandleTaskAsync(this Task task) } /// - /// Synchronously awaits a and guarantees all exceptions are caught and handled when the is not directly awaited. + /// Runs a and guarantees all exceptions are caught and handled even when the is not directly awaited. + /// Use this for 'fire and forget' tasks. /// /// The who's exceptions will be handled. public static void HandleTask(this Task task) diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index fc260194f..b4da0ceb9 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -65,6 +65,7 @@ public static class ProgramConstants public const string INI_NEWLINE_PATTERN = "@"; public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/master-list"; + public const string CNCNET_DYNAMIC_TUNNELS = "DYNAMIC"; public const int GAME_ID_MAX_LENGTH = 4; public static readonly Encoding LAN_ENCODING = Encoding.UTF8; @@ -104,9 +105,6 @@ public static string GetBaseResourcePath() return SafePath.CombineDirectoryPath(GamePath, BASE_RESOURCE_PATH); } - public const string GAME_INVITE_CTCP_COMMAND = "INVITE"; - public const string GAME_INVITATION_FAILED_CTCP_COMMAND = "INVITATION_FAILED"; - public static string GetAILevelName(int aiLevel) { if (aiLevel > -1 && aiLevel < AI_PLAYER_NAMES.Count) diff --git a/ClientCore/Settings/UserINISettings.cs b/ClientCore/Settings/UserINISettings.cs index 671c823fe..3774c683c 100644 --- a/ClientCore/Settings/UserINISettings.cs +++ b/ClientCore/Settings/UserINISettings.cs @@ -99,6 +99,9 @@ protected UserINISettings(IniFile iniFile) EnableMapSharing = new BoolSetting(iniFile, MULTIPLAYER, "EnableMapSharing", true); AlwaysDisplayTunnelList = new BoolSetting(iniFile, MULTIPLAYER, "AlwaysDisplayTunnelList", false); MapSortState = new IntSetting(iniFile, MULTIPLAYER, "MapSortState", (int)SortDirection.None); + UseLegacyTunnels = new BoolSetting(iniFile, MULTIPLAYER, "UseLegacyTunnels", false); + UseP2P = new BoolSetting(iniFile, MULTIPLAYER, "UseP2P", false); + UseDynamicTunnels = new BoolSetting(iniFile, MULTIPLAYER, "UseDynamicTunnels", true); CheckForUpdates = new BoolSetting(iniFile, OPTIONS, "CheckforUpdates", true); @@ -134,24 +137,40 @@ protected UserINISettings(IniFile iniFile) /*********/ public IntSetting IngameScreenWidth { get; private set; } + public IntSetting IngameScreenHeight { get; private set; } + public StringSetting ClientTheme { get; private set; } + public string ThemeFolderPath => ClientConfiguration.Instance.GetThemePath(ClientTheme); + public StringSetting Translation { get; private set; } + public string TranslationFolderPath => SafePath.CombineDirectoryPath( ClientConfiguration.Instance.TranslationsFolderPath, Translation); + public string TranslationThemeFolderPath => SafePath.CombineDirectoryPath( ClientConfiguration.Instance.TranslationsFolderPath, Translation, ClientConfiguration.Instance.GetThemePath(ClientTheme)); + public IntSetting DetailLevel { get; private set; } + public StringSetting Renderer { get; private set; } + public BoolSetting WindowedMode { get; private set; } + public BoolSetting BorderlessWindowedMode { get; private set; } + public BoolSetting BackBufferInVRAM { get; private set; } + public IntSetting ClientResolutionX { get; set; } + public IntSetting ClientResolutionY { get; set; } + public BoolSetting BorderlessWindowedClient { get; private set; } + public IntSetting ClientFPS { get; private set; } + public BoolSetting DisplayToggleableExtraTextures { get; private set; } /*********/ @@ -159,12 +178,19 @@ protected UserINISettings(IniFile iniFile) /*********/ public DoubleSetting ScoreVolume { get; private set; } + public DoubleSetting SoundVolume { get; private set; } + public DoubleSetting VoiceVolume { get; private set; } + public BoolSetting IsScoreShuffle { get; private set; } + public DoubleSetting ClientVolume { get; private set; } + public BoolSetting PlayMainMenuMusic { get; private set; } + public BoolSetting StopMusicOnMenu { get; private set; } + public BoolSetting MessageSound { get; private set; } /********/ @@ -172,8 +198,11 @@ protected UserINISettings(IniFile iniFile) /********/ public IntSetting ScrollRate { get; private set; } + public IntSetting DragDistance { get; private set; } + public IntSetting DoubleTapInterval { get; private set; } + public StringSetting Win8CompatMode { get; private set; } /************************/ @@ -183,15 +212,23 @@ protected UserINISettings(IniFile iniFile) public StringSetting PlayerName { get; private set; } public IntSetting ChatColor { get; private set; } + public IntSetting LANChatColor { get; private set; } + public BoolSetting PingUnofficialCnCNetTunnels { get; private set; } + public BoolSetting WritePathToRegistry { get; private set; } + public BoolSetting PlaySoundOnGameHosted { get; private set; } public BoolSetting SkipConnectDialog { get; private set; } + public BoolSetting PersistentMode { get; private set; } + public BoolSetting AutomaticCnCNetLogin { get; private set; } + public BoolSetting DiscordIntegration { get; private set; } + public BoolSetting AllowGameInvitesFromFriendsOnly { get; private set; } public BoolSetting NotifyOnUserListChange { get; private set; } @@ -206,6 +243,12 @@ protected UserINISettings(IniFile iniFile) public IntSetting MapSortState { get; private set; } + public BoolSetting UseLegacyTunnels { get; private set; } + + public BoolSetting UseP2P { get; private set; } + + public BoolSetting UseDynamicTunnels { get; private set; } + /*********************/ /* GAME LIST FILTERS */ /*********************/ @@ -229,7 +272,9 @@ protected UserINISettings(IniFile iniFile) public BoolSetting CheckForUpdates { get; private set; } public BoolSetting PrivacyPolicyAccepted { get; private set; } + public BoolSetting IsFirstRun { get; private set; } + public BoolSetting CustomComponentsDenied { get; private set; } public IntSetting Difficulty { get; private set; } diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 0c9a3f3f5..1c3269e22 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -619,13 +619,14 @@ private void SwitchMainMenuMusicFormat() #endif #if DX - wmaBackupMainMenuMusicFile.CopyTo(wmaMainMenuMusicFile.FullName, true); + if (!wmaMainMenuMusicFile.Exists) + wmaBackupMainMenuMusicFile.CopyTo(wmaMainMenuMusicFile.FullName); #elif GL FileInfo oggMainMenuMusicFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH, FormattableString.Invariant($"{ClientConfiguration.Instance.MainMenuMusicName}.ogg")); - if (oggMainMenuMusicFile.Exists) - oggMainMenuMusicFile.CopyTo(wmaMainMenuMusicFile.FullName, true); + if (oggMainMenuMusicFile.Exists && !wmaMainMenuMusicFile.Exists) + oggMainMenuMusicFile.CopyTo(wmaMainMenuMusicFile.FullName); #endif } diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 7ffff834d..9e0c83f8c 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -14,6 +14,7 @@ using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -893,7 +894,7 @@ private async Task JoinGameAsync(HostedCnCNetGame hg, string password) } else { - await gameLobby.SetUpAsync(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded, false); + await gameLobby.SetUpAsync(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded); gameChannel.UserAdded += gameChannel_UserAddedFunc; gameChannel.InvalidPasswordEntered += gameChannel_InvalidPasswordEntered_NewGameFunc; gameChannel.InviteOnlyErrorOnJoin += gameChannel_InviteOnlyErrorOnJoinFunc; @@ -1000,7 +1001,7 @@ private async Task Gcw_GameCreatedAsync(GameCreationEventArgs e) Channel gameChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, password); connectionManager.AddChannel(gameChannel); - await gameLobby.SetUpAsync(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword, false); + await gameLobby.SetUpAsync(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword); gameChannel.UserAdded += gameChannel_UserAddedFunc; await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.JOIN + " " + channelName + " " + password, QueuedMessageType.INSTANT_MESSAGE, 0)); @@ -1422,7 +1423,7 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr !updateDenied && channelUser.IsAdmin && !isInGameRoom && - e.Message.StartsWith("UPDATE ") && + e.Message.StartsWith(CnCNetCommands.UPDATE + " ") && e.Message.Length > 7) { string version = e.Message[7..]; @@ -1450,8 +1451,10 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr try { string revision = splitMessage[0]; + if (revision != ProgramConstants.CNCNET_PROTOCOL_REVISION) return; + string gameVersion = splitMessage[1]; int maxPlayers = Conversions.IntFromString(splitMessage[2], 0); string gameRoomChannelName = splitMessage[3]; @@ -1461,29 +1464,48 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr bool isClosed = Conversions.BooleanFromString(splitMessage[5].Substring(2, 1), true); bool isLoadedGame = Conversions.BooleanFromString(splitMessage[5].Substring(3, 1), false); bool isLadder = Conversions.BooleanFromString(splitMessage[5].Substring(4, 1), false); - string[] players = splitMessage[6].Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries); + string[] players = splitMessage[6].Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); string mapName = splitMessage[7]; string gameMode = splitMessage[8]; - - string[] tunnelAddressAndPort = splitMessage[9].Split(':'); - string tunnelAddress = tunnelAddressAndPort[0]; - int tunnelPort = int.Parse(tunnelAddressAndPort[1]); - + string tunnelHash = splitMessage[9]; string loadedGameId = splitMessage[10]; CnCNetGame cncnetGame = gameCollection.GameList.Find(g => g.GameBroadcastChannel == channel.ChannelName); - CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); - - if (tunnel == null) - return; - if (cncnetGame == null) return; - HostedCnCNetGame game = new HostedCnCNetGame(gameRoomChannelName, revision, gameVersion, maxPlayers, - gameRoomDisplayName, isCustomPassword, true, players, - e.UserName, mapName, gameMode); + CnCNetTunnel tunnel = null; + +#if DEBUG + if (tunnelHash.Contains(':')) + { + string[] tunnelAddressAndPort = splitMessage[9].Split(':'); + string tunnelAddress = tunnelAddressAndPort[0]; + int tunnelPort = int.Parse(tunnelAddressAndPort[1], CultureInfo.InvariantCulture); + + tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); + + if (tunnel == null) + return; + } + else + { +#endif + if (!ProgramConstants.CNCNET_DYNAMIC_TUNNELS.Equals(tunnelHash, StringComparison.OrdinalIgnoreCase)) + { + tunnel = tunnelHandler.Tunnels.Find(t => t.Hash.Equals(tunnelHash, StringComparison.OrdinalIgnoreCase)); + + if (tunnel == null) + return; + } +#if DEBUG + } +#endif + + var game = new HostedCnCNetGame(gameRoomChannelName, revision, gameVersion, maxPlayers, + gameRoomDisplayName, isCustomPassword, true, players, e.UserName, mapName, gameMode); + game.IsLoadedGame = isLoadedGame; game.MatchID = loadedGameId; game.LastRefreshTime = DateTime.Now; diff --git a/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs b/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs index 2ac774317..33024f77e 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs @@ -114,8 +114,7 @@ public void SetInfo(GenericHostedGame game) lblHost.Text = "Host:".L10N("Client:Main:GameInfoHost") + " " + Renderer.GetSafeString(game.HostName, lblHost.FontIndex); lblHost.Visible = true; - - lblPing.Text = game.Ping > 0 ? "Ping:".L10N("Client:Main:GameInfoPing") + " " + game.Ping.ToString() + " ms" : "Ping: Unknown".L10N("Client:Main:GameInfoPingUnknown"); + lblPing.Text = game.Ping > 0 ? "Ping:".L10N("Client:Main:GameInfoPing") + " " + game.Ping + " ms" : "Ping: Unknown".L10N("Client:Main:GameInfoPingUnknown"); lblPing.Visible = true; lblPlayers.Visible = true; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index a4678529f..3bcdf916d 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -19,7 +19,7 @@ namespace DTAClient.DXGUI.Multiplayer /// /// An abstract base class for a multiplayer game loading lobby. /// - public abstract class GameLoadingLobbyBase : XNAWindow, ISwitchable + internal abstract class GameLoadingLobbyBase : XNAWindow, ISwitchable { public GameLoadingLobbyBase(WindowManager windowManager, DiscordHandler discordHandler) : base(windowManager) { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index d89215d4b..e0b0ace6c 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -1,8 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; using ClientCore; using ClientCore.CnCNet5; +using ClientCore.Extensions; using ClientGUI; -using DTAClient.Domain.Multiplayer; using DTAClient.Domain; +using DTAClient.Domain.Multiplayer; +using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI.Generic; using DTAClient.DXGUI.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer.GameLobby.CommandHandlers; @@ -12,2149 +22,2318 @@ using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using ClientCore.Extensions; -using DTAClient.Domain.Multiplayer.CnCNet; -using ClientCore.Extensions; -namespace DTAClient.DXGUI.Multiplayer.GameLobby +namespace DTAClient.DXGUI.Multiplayer.GameLobby; + +internal sealed class CnCNetGameLobby : MultiplayerGameLobby { - internal sealed class CnCNetGameLobby : MultiplayerGameLobby + private const int HUMAN_PLAYER_OPTIONS_LENGTH = 3; + private const int AI_PLAYER_OPTIONS_LENGTH = 2; + private const double GAME_BROADCAST_INTERVAL = 30.0; + private const double GAME_BROADCAST_ACCELERATION = 10.0; + private const double INITIAL_GAME_BROADCAST_DELAY = 10.0; + private const double MAX_TIME_FOR_GAME_LAUNCH = 20.0; + private const int PRIORITY_START_GAME = 10; + + private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; + + private readonly TunnelHandler tunnelHandler; + private readonly CnCNetManager connectionManager; + private readonly string localGame; + private readonly List ctcpCommandHandlers; + private readonly GameCollection gameCollection; + private readonly CnCNetUserData cncnetUserData; + private readonly PrivateMessagingWindow pmWindow; + private readonly List tunnelPlayerIds = new(); + private readonly List hostUploadedMaps = new(); + private readonly List chatCommandDownloadedMaps = new(); + private readonly List<(string Name, CnCNetTunnel Tunnel)> playerTunnels = new(); + private readonly List<(string Sender, string TunnelPingsMessage)> tunnelPingsMessages = new(); + private readonly List<(List Names, V3GameTunnelHandler Tunnel)> dynamicV3GameTunnelHandlers = new(); + + private TunnelSelectionWindow tunnelSelectionWindow; + private XNAClientButton btnChangeTunnel; + private Channel channel; + private GlobalContextMenu globalContextMenu; + private string hostName; + private IRCColor chatColor; + private XNATimerControl gameBroadcastTimer; + private XNATimerControl gameStartTimer; + private int playerLimit; + private bool closed; + private bool isCustomPassword; + private bool[] isPlayerConnectedToTunnel; + private bool isStartingGame; + private string gameFilesHash; + private MapSharingConfirmationPanel mapSharingConfirmationPanel; + + /// + /// The SHA1 of the latest selected map. + /// Used for map sharing. + /// + private string lastMapHash; + + /// + /// The map name of the latest selected map. + /// Used for map sharing. + /// + private string lastMapName; + + /// + /// Set to true if host has selected invalid tunnel server. + /// + private bool tunnelErrorMode; + + private EventHandler channel_UserAddedFunc; + private EventHandler channel_UserQuitIRCFunc; + private EventHandler channel_UserLeftFunc; + private EventHandler channel_UserKickedFunc; + private EventHandler channel_UserListReceivedFunc; + private EventHandler connectionManager_ConnectionLostFunc; + private EventHandler connectionManager_DisconnectedFunc; + private EventHandler tunnelHandler_CurrentTunnelFunc; + private List<(int Ping, string Hash)> pinnedTunnels; + private string pinnedTunnelPingsMessage; + + public CnCNetGameLobby( + WindowManager windowManager, + TopBar topBar, + CnCNetManager connectionManager, + TunnelHandler tunnelHandler, + GameCollection gameCollection, + CnCNetUserData cncnetUserData, + MapLoader mapLoader, + DiscordHandler discordHandler, + PrivateMessagingWindow pmWindow) + : base(windowManager, "MultiplayerGameLobby", topBar, mapLoader, discordHandler) { - private const int HUMAN_PLAYER_OPTIONS_LENGTH = 3; - private const int AI_PLAYER_OPTIONS_LENGTH = 2; - - private const double GAME_BROADCAST_INTERVAL = 30.0; - private const double GAME_BROADCAST_ACCELERATION = 10.0; - private const double INITIAL_GAME_BROADCAST_DELAY = 10.0; + this.connectionManager = connectionManager; + localGame = ClientConfiguration.Instance.LocalGame; + this.tunnelHandler = tunnelHandler; + this.gameCollection = gameCollection; + this.cncnetUserData = cncnetUserData; + this.pmWindow = pmWindow; + + ctcpCommandHandlers = new() + { + new IntCommandHandler(CnCNetCommands.OPTIONS_REQUEST, (playerName, options) => HandleOptionsRequestAsync(playerName, options).HandleTask()), + new IntCommandHandler(CnCNetCommands.READY_REQUEST, (playerName, options) => HandleReadyRequestAsync(playerName, options).HandleTask()), + new StringCommandHandler(CnCNetCommands.PLAYER_OPTIONS, ApplyPlayerOptions), + new StringCommandHandler(CnCNetCommands.PLAYER_EXTRA_OPTIONS, ApplyPlayerExtraOptions), + new StringCommandHandler(CnCNetCommands.GAME_OPTIONS, (sender, message) => ApplyGameOptionsAsync(sender, message).HandleTask()), + new StringCommandHandler(CnCNetCommands.GAME_START_V2, (sender, message) => NonHostLaunchGameAsync(sender, message).HandleTask()), + new StringCommandHandler(CnCNetCommands.GAME_START_V3, HandleGameStartV3TunnelMessage), + new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_OK, playerName => HandlePlayerConnectedToTunnelAsync(playerName).HandleTask()), + new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_FAIL, HandleTunnelFail), + new NotificationHandler(CnCNetCommands.AI_SPECTATORS, HandleNotification, () => AISpectatorsNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.GET_READY_LOBBY, HandleNotification, () => GetReadyNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.INSUFFICIENT_PLAYERS, HandleNotification, () => InsufficientPlayersNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.TOO_MANY_PLAYERS, HandleNotification, () => TooManyPlayersNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.SHARED_COLORS, HandleNotification, () => SharedColorsNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.SHARED_STARTING_LOCATIONS, HandleNotification, () => SharedStartingLocationNotificationAsync().HandleTask()), + new NotificationHandler(CnCNetCommands.LOCK_GAME, HandleNotification, () => LockGameNotificationAsync().HandleTask()), + new IntNotificationHandler(CnCNetCommands.NOT_VERIFIED, HandleIntNotification, playerIndex => NotVerifiedNotificationAsync(playerIndex).HandleTask()), + new IntNotificationHandler(CnCNetCommands.STILL_IN_GAME, HandleIntNotification, playerIndex => StillInGameNotificationAsync(playerIndex).HandleTask()), + new StringCommandHandler(CnCNetCommands.MAP_SHARING_UPLOAD, HandleMapUploadRequest), + new StringCommandHandler(CnCNetCommands.MAP_SHARING_FAIL, HandleMapTransferFailMessage), + new StringCommandHandler(CnCNetCommands.MAP_SHARING_DOWNLOAD, HandleMapDownloadRequest), + new NoParamCommandHandler(CnCNetCommands.MAP_SHARING_DISABLED, HandleMapSharingBlockedMessage), + new NoParamCommandHandler(CnCNetCommands.RETURN, ReturnNotification), + new StringCommandHandler(CnCNetCommands.FILE_HASH, (sender, filesHash) => FileHashNotificationAsync(sender, filesHash).HandleTask()), + new StringCommandHandler(CnCNetCommands.CHEATER, CheaterNotification), + new StringCommandHandler(CnCNetCommands.DICE_ROLL, HandleDiceRollResult), + new NoParamCommandHandler(CnCNetCommands.CHEAT_DETECTED, HandleCheatDetectedMessage), + new IntCommandHandler(CnCNetCommands.TUNNEL_PING, HandleTunnelPing), + new StringCommandHandler(CnCNetCommands.CHANGE_TUNNEL_SERVER, (sender, tunnelAddressAndPort) => HandleTunnelServerChangeMessageAsync(sender, tunnelAddressAndPort).HandleTask()), + new StringCommandHandler(CnCNetCommands.PLAYER_TUNNEL_PINGS, HandleTunnelPingsMessage) + }; + + MapSharer.MapDownloadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapDownloadFailedAsync(e).HandleTask()); + MapSharer.MapDownloadComplete += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapDownloadCompleteAsync(e).HandleTask()); + MapSharer.MapUploadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapUploadFailedAsync(e).HandleTask()); + MapSharer.MapUploadComplete += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapUploadCompleteAsync(e).HandleTask()); + + AddChatBoxCommand(new( + CnCNetLobbyCommands.TUNNELINFO, + "View tunnel server information".L10N("Client:Main:TunnelInfo"), + false, + PrintTunnelServerInformation)); + AddChatBoxCommand(new( + CnCNetLobbyCommands.CHANGETUNNEL, + "Change the used CnCNet tunnel server (game host only)".L10N("Client:Main:ChangeTunnel"), + true, + _ => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")))); + AddChatBoxCommand(new( + CnCNetLobbyCommands.DOWNLOADMAP, + "Download a map from CNCNet's map server using a map ID and an optional filename.\nExample: \"/downloadmap MAPID [2] My Battle Map\"".L10N("Client:Main:DownloadMapCommandDescription"), + false, + DownloadMapByIdCommand)); + } - private const double MAX_TIME_FOR_GAME_LAUNCH = 20.0; + public event EventHandler GameLeft; - private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; + public override void Initialize() + { + IniNameOverride = nameof(CnCNetGameLobby); - #region Priorities + base.Initialize(); - private const int PRIORITY_START_GAME = 10; + btnChangeTunnel = FindChild(nameof(btnChangeTunnel)); - #endregion + btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick; - public CnCNetGameLobby( - WindowManager windowManager, - TopBar topBar, - CnCNetManager connectionManager, - TunnelHandler tunnelHandler, - GameCollection gameCollection, - CnCNetUserData cncnetUserData, - MapLoader mapLoader, - DiscordHandler discordHandler, - PrivateMessagingWindow pmWindow) - : base(windowManager, "MultiplayerGameLobby", topBar, mapLoader, discordHandler) + gameBroadcastTimer = new(WindowManager) { - this.connectionManager = connectionManager; - localGame = ClientConfiguration.Instance.LocalGame; - this.tunnelHandler = tunnelHandler; - this.gameCollection = gameCollection; - this.cncnetUserData = cncnetUserData; - this.pmWindow = pmWindow; - - ctcpCommandHandlers = new CommandHandlerBase[] - { - new IntCommandHandler(CnCNetCommands.OPTIONS_REQUEST, (playerName, options) => HandleOptionsRequestAsync(playerName, options).HandleTask()), - new IntCommandHandler(CnCNetCommands.READY_REQUEST, (playerName, options) => HandleReadyRequestAsync(playerName, options).HandleTask()), - new StringCommandHandler(CnCNetCommands.PLAYER_OPTIONS, ApplyPlayerOptions), - new StringCommandHandler(CnCNetCommands.PLAYER_EXTRA_OPTIONS, ApplyPlayerExtraOptions), - new StringCommandHandler(CnCNetCommands.GAME_OPTIONS, (sender, message) => ApplyGameOptionsAsync(sender, message).HandleTask()), - new StringCommandHandler(CnCNetCommands.GAME_START_V2, (sender, message) => NonHostLaunchGameAsync(sender, message).HandleTask()), - new StringCommandHandler(CnCNetCommands.GAME_START_V3, HandleGameStartV3TunnelMessage), - new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_OK, playerName => HandleTunnelConnectedAsync(playerName).HandleTask()), - new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_FAIL, HandleTunnelFail), - new NotificationHandler(CnCNetCommands.AI_SPECTATORS, HandleNotification, () => AISpectatorsNotificationAsync().HandleTask()), - new NotificationHandler(CnCNetCommands.GET_READY_LOBBY, HandleNotification, () => GetReadyNotificationAsync().HandleTask()), - new NotificationHandler(CnCNetCommands.INSUFFICIENT_PLAYERS, HandleNotification, () => InsufficientPlayersNotificationAsync().HandleTask()), - new NotificationHandler(CnCNetCommands.TOO_MANY_PLAYERS, HandleNotification, () => TooManyPlayersNotificationAsync().HandleTask()), - new NotificationHandler(CnCNetCommands.SHARED_COLORS, HandleNotification, () => SharedColorsNotificationAsync().HandleTask()), - new NotificationHandler(CnCNetCommands.SHARED_STARTING_LOCATIONS, HandleNotification, () => SharedStartingLocationNotificationAsync().HandleTask()), - new NotificationHandler(CnCNetCommands.LOCK_GAME, HandleNotification, () => LockGameNotificationAsync().HandleTask()), - new IntNotificationHandler(CnCNetCommands.NOT_VERIFIED, HandleIntNotification, playerIndex => NotVerifiedNotificationAsync(playerIndex).HandleTask()), - new IntNotificationHandler(CnCNetCommands.STILL_IN_GAME, HandleIntNotification, playerIndex => StillInGameNotificationAsync(playerIndex).HandleTask()), - new StringCommandHandler(CnCNetCommands.MAP_SHARING_UPLOAD, HandleMapUploadRequest), - new StringCommandHandler(CnCNetCommands.MAP_SHARING_FAIL, HandleMapTransferFailMessage), - new StringCommandHandler(CnCNetCommands.MAP_SHARING_DOWNLOAD, HandleMapDownloadRequest), - new NoParamCommandHandler(CnCNetCommands.MAP_SHARING_DISABLED, HandleMapSharingBlockedMessage), - new NoParamCommandHandler(CnCNetCommands.RETURN, ReturnNotification), - new IntCommandHandler(CnCNetCommands.TUNNEL_PING, HandleTunnelPing), - new StringCommandHandler(CnCNetCommands.FILE_HASH, (sender, filesHash) => FileHashNotificationAsync(sender, filesHash).HandleTask()), - new StringCommandHandler(CnCNetCommands.CHEATER, CheaterNotification), - new StringCommandHandler(CnCNetCommands.DICE_ROLL, HandleDiceRollResult), - new NoParamCommandHandler(CnCNetCommands.CHEAT_DETECTED, HandleCheatDetectedMessage), - new StringCommandHandler(CnCNetCommands.CHANGE_TUNNEL_SERVER, (sender, tunnelAddressAndPort) => HandleTunnelServerChangeMessageAsync(sender, tunnelAddressAndPort).HandleTask()) - }; + AutoReset = true, + Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL), + Enabled = false + }; + gameBroadcastTimer.TimeElapsed += (_, _) => BroadcastGameAsync().HandleTask(); - MapSharer.MapDownloadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapDownloadFailedAsync(e).HandleTask()); - MapSharer.MapDownloadComplete += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapDownloadCompleteAsync(e).HandleTask()); - MapSharer.MapUploadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapUploadFailedAsync(e).HandleTask()); - MapSharer.MapUploadComplete += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapUploadCompleteAsync(e).HandleTask()); - - AddChatBoxCommand(new( - "TUNNELINFO", - "View tunnel server information".L10N("Client:Main:TunnelInfoCommand"), - false, - PrintTunnelServerInformation)); - AddChatBoxCommand(new( - "CHANGETUNNEL", - "Change the used CnCNet tunnel server (game host only)".L10N("Client:Main:ChangeTunnelCommand"), - true, - _ => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")))); - AddChatBoxCommand(new( - "DOWNLOADMAP", - "Download a map from CNCNet's map server using a map ID and an optional filename.\nExample: \"/downloadmap MAPID [2] My Battle Map\"".L10N("Client:Main:DownloadMapCommandDescription"), - false, - DownloadMapByIdCommand)); - } - - public event EventHandler GameLeft; - - private readonly TunnelHandler tunnelHandler; - private TunnelSelectionWindow tunnelSelectionWindow; - private XNAClientButton btnChangeTunnel; - - private Channel channel; - private readonly CnCNetManager connectionManager; - private readonly string localGame; - - private readonly GameCollection gameCollection; - private readonly CnCNetUserData cncnetUserData; - private readonly PrivateMessagingWindow pmWindow; - private GlobalContextMenu globalContextMenu; - - private string hostName; - - private readonly CommandHandlerBase[] ctcpCommandHandlers; - - private IRCColor chatColor; - - private XNATimerControl gameBroadcastTimer; - private XNATimerControl gameStartTimer; - - private int playerLimit; + gameStartTimer = new(WindowManager) + { + AutoReset = false, + Interval = TimeSpan.FromSeconds(MAX_TIME_FOR_GAME_LAUNCH) + }; + gameStartTimer.TimeElapsed += GameStartTimer_TimeElapsed; - private bool closed; + tunnelSelectionWindow = new(WindowManager, tunnelHandler); - private bool isCustomPassword; - private bool isP2P; + tunnelSelectionWindow.Initialize(); - private readonly List tunnelPlayerIds = new(); - private bool[] isPlayerConnectedToTunnel; - private GameTunnelHandler gameTunnelHandler; - private bool isStartingGame; + tunnelSelectionWindow.DrawOrder = 1; + tunnelSelectionWindow.UpdateOrder = 1; - private string gameFilesHash; + DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow); + tunnelSelectionWindow.CenterOnParent(); + tunnelSelectionWindow.Disable(); - private readonly List hostUploadedMaps = new(); - private readonly List chatCommandDownloadedMaps = new(); - private List tunnels = new(); + tunnelSelectionWindow.TunnelSelected += (_, e) => TunnelSelectionWindow_TunnelSelectedAsync(e).HandleTask(); - private MapSharingConfirmationPanel mapSharingConfirmationPanel; + mapSharingConfirmationPanel = new(WindowManager); - /// - /// The SHA1 of the latest selected map. - /// Used for map sharing. - /// - private string lastMapSHA1; + MapPreviewBox.AddChild(mapSharingConfirmationPanel); - /// - /// The map name of the latest selected map. - /// Used for map sharing. - /// - private string lastMapName; + mapSharingConfirmationPanel.MapDownloadConfirmed += MapSharingConfirmationPanel_MapDownloadConfirmed; - private EventHandler channel_UserAddedFunc; - private EventHandler channel_UserQuitIRCFunc; - private EventHandler channel_UserLeftFunc; - private EventHandler channel_UserKickedFunc; - private EventHandler channel_UserListReceivedFunc; - private EventHandler connectionManager_ConnectionLostFunc; - private EventHandler connectionManager_DisconnectedFunc; - private EventHandler tunnelHandler_CurrentTunnelFunc; + WindowManager.AddAndInitializeControl(gameBroadcastTimer); - /// - /// Set to true if host has selected invalid tunnel server. - /// - private bool tunnelErrorMode; + globalContextMenu = new(WindowManager, connectionManager, cncnetUserData, pmWindow); - public override void Initialize() - { - IniNameOverride = nameof(CnCNetGameLobby); - base.Initialize(); + AddChild(globalContextMenu); + AddChild(gameStartTimer); - gameTunnelHandler = new(); - gameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - gameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + MultiplayerNameRightClicked += MultiplayerName_RightClick; - btnChangeTunnel = FindChild(nameof(btnChangeTunnel)); - btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick; + channel_UserAddedFunc = (_, e) => Channel_UserAddedAsync(e).HandleTask(); + channel_UserQuitIRCFunc = (_, e) => ChannelUserLeftAsync(e).HandleTask(); + channel_UserLeftFunc = (_, e) => ChannelUserLeftAsync(e).HandleTask(); + channel_UserKickedFunc = (_, e) => Channel_UserKickedAsync(e).HandleTask(); + channel_UserListReceivedFunc = (_, _) => Channel_UserListReceivedAsync().HandleTask(); + connectionManager_ConnectionLostFunc = (_, _) => HandleConnectionLossAsync().HandleTask(); + connectionManager_DisconnectedFunc = (_, _) => HandleConnectionLossAsync().HandleTask(); + tunnelHandler_CurrentTunnelFunc = (_, _) => UpdatePingAsync().HandleTask(); - gameBroadcastTimer = new(WindowManager); - gameBroadcastTimer.AutoReset = true; - gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL); - gameBroadcastTimer.Enabled = false; - gameBroadcastTimer.TimeElapsed += (_, _) => BroadcastGameAsync().HandleTask(); + PostInitialize(); + } - gameStartTimer = new(WindowManager); - gameStartTimer.AutoReset = false; - gameStartTimer.Interval = TimeSpan.FromSeconds(MAX_TIME_FOR_GAME_LAUNCH); - gameStartTimer.TimeElapsed += GameStartTimer_TimeElapsed; + private void GameStartTimer_TimeElapsed(object sender, EventArgs e) + { + string playerString = string.Empty; - tunnelSelectionWindow = new(WindowManager, tunnelHandler); - tunnelSelectionWindow.Initialize(); - tunnelSelectionWindow.DrawOrder = 1; - tunnelSelectionWindow.UpdateOrder = 1; - DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow); - tunnelSelectionWindow.CenterOnParent(); - tunnelSelectionWindow.Disable(); - tunnelSelectionWindow.TunnelSelected += (_, e) => TunnelSelectionWindow_TunnelSelectedAsync(e).HandleTask(); + for (int i = 0; i < Players.Count; i++) + { + if (!isPlayerConnectedToTunnel[i]) + { + if (playerString == string.Empty) + playerString = Players[i].Name; + else + playerString += ", " + Players[i].Name; + } + } - mapSharingConfirmationPanel = new(WindowManager); - MapPreviewBox.AddChild(mapSharingConfirmationPanel); - mapSharingConfirmationPanel.MapDownloadConfirmed += MapSharingConfirmationPanel_MapDownloadConfirmed; + AddNotice($"Some players ({playerString}) failed to connect within the time limit. Aborting game launch."); + AbortGameStart(); + } - WindowManager.AddAndInitializeControl(gameBroadcastTimer); + private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClickedEventArgs args) + { + globalContextMenu.Show( + new GlobalContextMenuData + { + PlayerName = args.PlayerName, + PreventJoinGame = true + }, + GetCursorPoint()); + } - globalContextMenu = new(WindowManager, connectionManager, cncnetUserData, pmWindow); - AddChild(globalContextMenu); - AddChild(gameStartTimer); + private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")); - MultiplayerNameRightClicked += MultiplayerName_RightClick; + public async Task SetUpAsync( + Channel channel, + bool isHost, + int playerLimit, + CnCNetTunnel tunnel, + string hostName, + bool isCustomPassword) + { + this.channel = channel; + this.hostName = hostName; + this.playerLimit = playerLimit; + this.isCustomPassword = isCustomPassword; + channel.MessageAdded += Channel_MessageAdded; + channel.CTCPReceived += Channel_CTCPReceived; + channel.UserKicked += channel_UserKickedFunc; + channel.UserQuitIRC += channel_UserQuitIRCFunc; + channel.UserLeft += channel_UserLeftFunc; + channel.UserAdded += channel_UserAddedFunc; + channel.UserNameChanged += Channel_UserNameChanged; + channel.UserListReceived += channel_UserListReceivedFunc; - channel_UserAddedFunc = (_, e) => Channel_UserAddedAsync(e).HandleTask(); - channel_UserQuitIRCFunc = (_, e) => Channel_UserQuitIRCAsync(e).HandleTask(); - channel_UserLeftFunc = (_, e) => Channel_UserLeftAsync(e).HandleTask(); - channel_UserKickedFunc = (_, e) => Channel_UserKickedAsync(e).HandleTask(); - channel_UserListReceivedFunc = (_, _) => Channel_UserListReceivedAsync().HandleTask(); - connectionManager_ConnectionLostFunc = (_, _) => HandleConnectionLossAsync().HandleTask(); - connectionManager_DisconnectedFunc = (_, _) => HandleConnectionLossAsync().HandleTask(); - tunnelHandler_CurrentTunnelFunc = (_, _) => UpdatePingAsync().HandleTask(); + if (isHost) + { + RandomSeed = new Random().Next(); - PostInitialize(); + await RefreshMapSelectionUIAsync(); + btnChangeTunnel.Enable(); } - - private void GameStartTimer_TimeElapsed(object sender, EventArgs e) + else { - string playerString = ""; + channel.ChannelModesChanged += Channel_ChannelModesChanged; - for (int i = 0; i < Players.Count; i++) - { - if (!isPlayerConnectedToTunnel[i]) - { - if (playerString == "") - playerString = Players[i].Name; - else - playerString += ", " + Players[i].Name; - } - } - - AddNotice($"Some players ({playerString}) failed to connect within the time limit. " + - $"Aborting game launch."); - AbortGameStart(); + AIPlayers.Clear(); } - private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClickedEventArgs args) + if (!UserINISettings.Instance.UseDynamicTunnels) { - globalContextMenu.Show(new GlobalContextMenuData - { - PlayerName = args.PlayerName, - PreventJoinGame = true - }, GetCursorPoint()); + tunnelHandler.CurrentTunnel = tunnel; + } + else + { + tunnelHandler.CurrentTunnel = tunnelHandler.Tunnels + .Where(q => q.PingInMs > -1 && !q.RequiresPassword && q.Clients < q.MaxClients - 8 && q.Version == Constants.TUNNEL_VERSION_3) + .MinBy(q => q.PingInMs); } - private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")); + tunnelHandler.CurrentTunnelPinged += tunnelHandler_CurrentTunnelFunc; + connectionManager.ConnectionLost += connectionManager_ConnectionLostFunc; + connectionManager.Disconnected += connectionManager_DisconnectedFunc; - public async Task SetUpAsync(Channel channel, bool isHost, int playerLimit, - CnCNetTunnel tunnel, string hostName, bool isCustomPassword, bool isP2P) - { - this.channel = channel; - channel.MessageAdded += Channel_MessageAdded; - channel.CTCPReceived += Channel_CTCPReceived; - channel.UserKicked += channel_UserKickedFunc; - channel.UserQuitIRC += channel_UserQuitIRCFunc; - channel.UserLeft += channel_UserLeftFunc; - channel.UserAdded += channel_UserAddedFunc; - channel.UserNameChanged += Channel_UserNameChanged; - channel.UserListReceived += channel_UserListReceivedFunc; + Refresh(isHost); + } - this.hostName = hostName; - this.playerLimit = playerLimit; - this.isCustomPassword = isCustomPassword; - this.isP2P = isP2P; + public async Task OnJoinedAsync() + { + var fhc = new FileHashCalculator(); - if (isHost) - { - RandomSeed = new Random().Next(); - await RefreshMapSelectionUIAsync(); - btnChangeTunnel.Enable(); - } - else - { - channel.ChannelModesChanged += Channel_ChannelModesChanged; - AIPlayers.Clear(); - btnChangeTunnel.Disable(); - } + fhc.CalculateHashes(GameModeMaps.GameModes); - tunnels = tunnelHandler.Tunnels; + gameFilesHash = fhc.GetCompleteHash(); + pinnedTunnels = tunnelHandler.Tunnels + .Where(q => !q.RequiresPassword && q.PingInMs > -1 && q.Clients < q.MaxClients - 8 && q.Version == Constants.TUNNEL_VERSION_3) + .OrderBy(q => q.PingInMs) + .ThenBy(q => q.Hash, StringComparer.OrdinalIgnoreCase) + .Select(q => (q.PingInMs, q.Hash)) + .ToList(); - tunnelHandler.CurrentTunnel = tunnel; - tunnelHandler.CurrentTunnelPinged += tunnelHandler_CurrentTunnelFunc; + IEnumerable tunnelPings = pinnedTunnels + .Take(10) + .Select(q => FormattableString.Invariant($"{q.Ping};{q.Hash}\t")); - connectionManager.ConnectionLost += connectionManager_ConnectionLostFunc; - connectionManager.Disconnected += connectionManager_DisconnectedFunc; + pinnedTunnelPingsMessage = string.Concat(tunnelPings); - Refresh(isHost); + foreach ((string sender, string tunnelPingsMessage) in tunnelPingsMessages) + { + HandleTunnelPingsMessage(sender, tunnelPingsMessage); } - public async Task OnJoinedAsync() + if (IsHost) { - FileHashCalculator fhc = new FileHashCalculator(); - fhc.CalculateHashes(GameModeMaps.GameModes); + await connectionManager.SendCustomMessageAsync(new( + FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} +klnNs {channel.Password} {playerLimit}"), + QueuedMessageType.SYSTEM_MESSAGE, + 50)); - gameFilesHash = fhc.GetCompleteHash(); + await connectionManager.SendCustomMessageAsync(new( + FormattableString.Invariant($"{IRCCommands.TOPIC} {channel.ChannelName} :{ProgramConstants.CNCNET_PROTOCOL_REVISION}:{localGame.ToLower()}"), + QueuedMessageType.SYSTEM_MESSAGE, + 50)); - if (IsHost) - { - await connectionManager.SendCustomMessageAsync(new( - string.Format(IRCCommands.MODE + " {0} +klnNs {1} {2}", channel.ChannelName, - channel.Password, playerLimit), - QueuedMessageType.SYSTEM_MESSAGE, 50)); - - await connectionManager.SendCustomMessageAsync(new( - string.Format(IRCCommands.TOPIC + " {0} :{1}", channel.ChannelName, - ProgramConstants.CNCNET_PROTOCOL_REVISION + ";" + localGame.ToLower()), - QueuedMessageType.SYSTEM_MESSAGE, 50)); - - gameBroadcastTimer.Enabled = true; - gameBroadcastTimer.Start(); - gameBroadcastTimer.SetTime(TimeSpan.FromSeconds(INITIAL_GAME_BROADCAST_DELAY)); - } - else - { - await channel.SendCTCPMessageAsync(CnCNetCommands.FILE_HASH + " " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); - } + gameBroadcastTimer.Enabled = true; - TopBar.AddPrimarySwitchable(this); - TopBar.SwitchToPrimary(); - WindowManager.SelectedControl = tbChatInput; - ResetAutoReadyCheckbox(); - await UpdatePingAsync(); - UpdateDiscordPresence(true); + gameBroadcastTimer.Start(); + gameBroadcastTimer.SetTime(TimeSpan.FromSeconds(INITIAL_GAME_BROADCAST_DELAY)); } - - private async Task UpdatePingAsync() + else { - if (tunnelHandler.CurrentTunnel == null) - return; + await channel.SendCTCPMessageAsync(CnCNetCommands.FILE_HASH + " " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); - await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_PING + " " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); + if (UserINISettings.Instance.UseDynamicTunnels) + await BroadcastPlayerTunnelPingsAsync(); - PlayerInfo pInfo = Players.Find(p => p.Name.Equals(ProgramConstants.PLAYERNAME)); - if (pInfo != null) + if (UserINISettings.Instance.UseP2P) { - pInfo.Ping = tunnelHandler.CurrentTunnel.PingInMs; - UpdatePlayerPingIndicator(pInfo); + // todo broadcast IPs so others can ping + // await channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_REQUEST + " " + x, QueuedMessageType.SYSTEM_MESSAGE, 10); + + // todo ping other players, if both sides can ping each other, add p2p ping as extra result to tunnel ping list + // await channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_PINGS + " " + x, QueuedMessageType.SYSTEM_MESSAGE, 10); } } - protected override void CopyPlayerDataToUI() - { - base.CopyPlayerDataToUI(); + TopBar.AddPrimarySwitchable(this); + TopBar.SwitchToPrimary(); + WindowManager.SelectedControl = tbChatInput; + ResetAutoReadyCheckbox(); + await UpdatePingAsync(); + UpdateDiscordPresence(true); + } - for (int i = AIPlayers.Count + Players.Count; i < MAX_PLAYER_COUNT; i++) - { - StatusIndicators[i].SwitchTexture( - i < playerLimit ? PlayerSlotState.Empty : PlayerSlotState.Unavailable); - } - } + private async Task UpdatePingAsync() + { + int ping; + + if (UserINISettings.Instance.UseDynamicTunnels) + ping = pinnedTunnels.Min(q => q.Ping); + else if (tunnelHandler.CurrentTunnel == null) + return; + else + ping = tunnelHandler.CurrentTunnel.PingInMs; - private void PrintTunnelServerInformation(string s) + await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_PING + " " + ping, QueuedMessageType.SYSTEM_MESSAGE, 10); + + PlayerInfo pInfo = FindLocalPlayer(); + + if (pInfo != null) { - if (tunnelHandler.CurrentTunnel == null) - { - AddNotice("Tunnel server unavailable!".L10N("Client:Main:TunnelUnavailable")); - } - else - { - AddNotice(string.Format("Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})".L10N("Client:Main:TunnelInfo"), - tunnelHandler.CurrentTunnel.Name, tunnelHandler.CurrentTunnel.Country, tunnelHandler.CurrentTunnel.Clients, tunnelHandler.CurrentTunnel.MaxClients, tunnelHandler.CurrentTunnel.Official - )); - } + pInfo.Ping = ping; + + UpdatePlayerPingIndicator(pInfo); } + } - private void ShowTunnelSelectionWindow(string description) - => tunnelSelectionWindow.Open(description, tunnelHandler.CurrentTunnel); + protected override void CopyPlayerDataToUI() + { + base.CopyPlayerDataToUI(); - private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) + for (int i = AIPlayers.Count + Players.Count; i < MAX_PLAYER_COUNT; i++) { - await channel.SendCTCPMessageAsync( - $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Address}:{e.Tunnel.Port}", - QueuedMessageType.SYSTEM_MESSAGE, - 10); - await HandleTunnelServerChangeAsync(e.Tunnel); + StatusIndicators[i].SwitchTexture( + i < playerLimit ? PlayerSlotState.Empty : PlayerSlotState.Unavailable); } + } - public void ChangeChatColor(IRCColor chatColor) + private void PrintTunnelServerInformation(string s) + { + if (tunnelHandler.CurrentTunnel == null) + { + AddNotice("Tunnel server unavailable!".L10N("Client:Main:TunnelUnavailable")); + } + else { - this.chatColor = chatColor; - tbChatInput.TextColor = chatColor.XnaColor; + AddNotice(string.Format("Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})".L10N("Client:Main:TunnelInfo"), + tunnelHandler.CurrentTunnel.Name, tunnelHandler.CurrentTunnel.Country, tunnelHandler.CurrentTunnel.Clients, tunnelHandler.CurrentTunnel.MaxClients, tunnelHandler.CurrentTunnel.Official)); } + } + + private void ShowTunnelSelectionWindow(string description) + => tunnelSelectionWindow.Open(description, tunnelHandler.CurrentTunnel); + + private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) + { + await channel.SendCTCPMessageAsync( + $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Address}:{e.Tunnel.Port}", + QueuedMessageType.SYSTEM_MESSAGE, + 10); + await HandleTunnelServerChangeAsync(e.Tunnel); + } - public override async Task ClearAsync() + public void ChangeChatColor(IRCColor chatColor) + { + this.chatColor = chatColor; + tbChatInput.TextColor = chatColor.XnaColor; + } + + public override async Task ClearAsync() + { + await base.ClearAsync(); + + if (channel != null) { - await base.ClearAsync(); + channel.MessageAdded -= Channel_MessageAdded; + channel.CTCPReceived -= Channel_CTCPReceived; + channel.UserKicked -= channel_UserKickedFunc; + channel.UserQuitIRC -= channel_UserQuitIRCFunc; + channel.UserLeft -= channel_UserLeftFunc; + channel.UserAdded -= channel_UserAddedFunc; + channel.UserNameChanged -= Channel_UserNameChanged; + channel.UserListReceived -= channel_UserListReceivedFunc; - if (channel != null) - { - channel.MessageAdded -= Channel_MessageAdded; - channel.CTCPReceived -= Channel_CTCPReceived; - channel.UserKicked -= channel_UserKickedFunc; - channel.UserQuitIRC -= channel_UserQuitIRCFunc; - channel.UserLeft -= channel_UserLeftFunc; - channel.UserAdded -= channel_UserAddedFunc; - channel.UserNameChanged -= Channel_UserNameChanged; - channel.UserListReceived -= channel_UserListReceivedFunc; - - if (!IsHost) - { - channel.ChannelModesChanged -= Channel_ChannelModesChanged; - } + if (!IsHost) + channel.ChannelModesChanged -= Channel_ChannelModesChanged; - connectionManager.RemoveChannel(channel); - } + connectionManager.RemoveChannel(channel); + } + + Disable(); + connectionManager.ConnectionLost -= connectionManager_ConnectionLostFunc; + connectionManager.Disconnected -= connectionManager_DisconnectedFunc; - Disable(); - connectionManager.ConnectionLost -= connectionManager_ConnectionLostFunc; - connectionManager.Disconnected -= connectionManager_DisconnectedFunc; + gameBroadcastTimer.Enabled = false; + closed = false; - gameBroadcastTimer.Enabled = false; - closed = false; + tbChatInput.Text = string.Empty; - tbChatInput.Text = string.Empty; + tunnelHandler.CurrentTunnel = null; + tunnelHandler.CurrentTunnelPinged -= tunnelHandler_CurrentTunnelFunc; - tunnelHandler.CurrentTunnel = null; - tunnelHandler.CurrentTunnelPinged -= tunnelHandler_CurrentTunnelFunc; + playerTunnels.Clear(); + tunnelPlayerIds.Clear(); + dynamicV3GameTunnelHandlers.Clear(); + pinnedTunnels.Clear(); + tunnelPingsMessages.Clear(); + pinnedTunnelPingsMessage = null; - GameLeft?.Invoke(this, EventArgs.Empty); + GameLeft?.Invoke(this, EventArgs.Empty); - TopBar.RemovePrimarySwitchable(this); - ResetDiscordPresence(); + TopBar.RemovePrimarySwitchable(this); + ResetDiscordPresence(); + } + + public async Task LeaveGameLobbyAsync() + { + if (IsHost) + { + closed = true; + await BroadcastGameAsync(); } - public async Task LeaveGameLobbyAsync() + await ClearAsync(); + await channel.LeaveAsync(); + } + + private async Task HandleConnectionLossAsync() + { + await ClearAsync(); + Disable(); + } + + private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) + { + Logger.Log("CnCNetGameLobby: Nickname change: " + e.OldUserName + " to " + e.User.Name); + + int index = Players.FindIndex(p => p.Name == e.OldUserName); + + if (index > -1) { - if (IsHost) - { - closed = true; - await BroadcastGameAsync(); - } + PlayerInfo player = Players[index]; - await ClearAsync(); - await channel.LeaveAsync(); + player.Name = e.User.Name; + ddPlayerNames[index].Items[0].Text = player.Name; + + AddNotice(string.Format("Player {0} changed their name to {1}".L10N("Client:Main:PlayerRename"), e.OldUserName, e.User.Name)); } + } + + protected override Task BtnLeaveGame_LeftClickAsync() + => LeaveGameLobbyAsync(); + + protected override void UpdateDiscordPresence(bool resetTimer = false) + { + if (discordHandler == null) + return; + + PlayerInfo player = FindLocalPlayer(); + + if (player == null || Map == null || GameMode == null) + return; + string side = string.Empty; + + if (ddPlayerSides.Length > Players.IndexOf(player)) + side = (string)ddPlayerSides[Players.IndexOf(player)].SelectedItem.Tag; + + string currentState = ProgramConstants.IsInGame ? "In Game" : "In Lobby"; // not UI strings + + discordHandler.UpdatePresence( + Map.UntranslatedName, + GameMode.UntranslatedUIName, + "Multiplayer", + currentState, + Players.Count, + playerLimit, + side, + channel.UIName, + IsHost, + isCustomPassword, + Locked, + resetTimer); + } + + private async Task ChannelUserLeftAsync(UserNameEventArgs e) + { + await RemovePlayerAsync(e.UserName); - private async Task HandleConnectionLossAsync() + if (e.UserName == hostName) { - await ClearAsync(); - Disable(); + connectionManager.MainChannel.AddMessage( + new(ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); + await BtnLeaveGame_LeftClickAsync(); + } + else + { + UpdateDiscordPresence(); } + } - private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) + private async Task Channel_UserKickedAsync(UserNameEventArgs e) + { + if (e.UserName == ProgramConstants.PLAYERNAME) { - Logger.Log("CnCNetGameLobby: Nickname change: " + e.OldUserName + " to " + e.User.Name); - int index = Players.FindIndex(p => p.Name == e.OldUserName); - if (index > -1) - { - PlayerInfo player = Players[index]; - player.Name = e.User.Name; - ddPlayerNames[index].Items[0].Text = player.Name; - AddNotice(string.Format("Player {0} changed their name to {1}".L10N("Client:Main:PlayerRename"), e.OldUserName, e.User.Name)); - } + connectionManager.MainChannel.AddMessage( + new(ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); + await ClearAsync(); + + Visible = false; + Enabled = false; + return; } - protected override Task BtnLeaveGame_LeftClickAsync() - => LeaveGameLobbyAsync(); + int index = Players.FindIndex(p => p.Name == e.UserName); - protected override void UpdateDiscordPresence(bool resetTimer = false) + if (index > -1) { - if (discordHandler == null) - return; + Players.RemoveAt(index); + CopyPlayerDataToUI(); + UpdateDiscordPresence(); + ClearReadyStatuses(); - PlayerInfo player = FindLocalPlayer(); - if (player == null || Map == null || GameMode == null) - return; - string side = ""; - if (ddPlayerSides.Length > Players.IndexOf(player)) - side = (string)ddPlayerSides[Players.IndexOf(player)].SelectedItem.Tag; - string currentState = ProgramConstants.IsInGame ? "In Game" : "In Lobby"; // not UI strings + (string Name, CnCNetTunnel Tunnel) playerTunnel = playerTunnels.SingleOrDefault(q => q.Name.Equals(e.UserName, StringComparison.OrdinalIgnoreCase)); - discordHandler.UpdatePresence( - Map.UntranslatedName, GameMode.UntranslatedUIName, "Multiplayer", - currentState, Players.Count, playerLimit, side, - channel.UIName, IsHost, isCustomPassword, Locked, resetTimer); + if (playerTunnel.Name is not null) + playerTunnels.Remove(playerTunnel); + + tunnelPlayerIds.Clear(); + dynamicV3GameTunnelHandlers.Clear(); } + } - private async Task Channel_UserQuitIRCAsync(UserNameEventArgs e) + private async Task Channel_UserListReceivedAsync() + { + if (!IsHost) { - await RemovePlayerAsync(e.UserName); - - if (e.UserName == hostName) + if (channel.Users.Find(hostName) == null) { - connectionManager.MainChannel.AddMessage(new( - ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); + connectionManager.MainChannel.AddMessage( + new(ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); await BtnLeaveGame_LeftClickAsync(); } - else - { - UpdateDiscordPresence(); - } } - private async Task Channel_UserLeftAsync(UserNameEventArgs e) - { - await RemovePlayerAsync(e.UserName); + UpdateDiscordPresence(); + } - if (e.UserName == hostName) - { - connectionManager.MainChannel.AddMessage(new( - ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); - await BtnLeaveGame_LeftClickAsync(); - } - else - { - UpdateDiscordPresence(); - } - } + private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) + { + var pInfo = new PlayerInfo(e.User.IRCUser.Name); - private async Task Channel_UserKickedAsync(UserNameEventArgs e) - { - if (e.UserName == ProgramConstants.PLAYERNAME) - { - connectionManager.MainChannel.AddMessage(new( - ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); - await ClearAsync(); - Visible = false; - Enabled = false; - return; - } + Players.Add(pInfo); - int index = Players.FindIndex(p => p.Name == e.UserName); + if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT && AIPlayers.Count > 0) + AIPlayers.RemoveAt(AIPlayers.Count - 1); - if (index > -1) - { - Players.RemoveAt(index); - CopyPlayerDataToUI(); - UpdateDiscordPresence(); - ClearReadyStatuses(); - } - } + if (UserINISettings.Instance.UseDynamicTunnels && pInfo != FindLocalPlayer()) + await BroadcastPlayerTunnelPingsAsync(); + + sndJoinSound.Play(); +#if WINFORMS + WindowManager.FlashWindow(); +#endif - private async Task Channel_UserListReceivedAsync() + if (!IsHost) { - if (!IsHost) - { - if (channel.Users.Find(hostName) == null) - { - connectionManager.MainChannel.AddMessage(new( - ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); - await BtnLeaveGame_LeftClickAsync(); - } - } + CopyPlayerDataToUI(); + return; + } + if (e.User.IRCUser.Name != ProgramConstants.PLAYERNAME) + { + // Changing the map applies forced settings (co-op sides etc.) to the + // new player, and it also sends an options broadcast message + await ChangeMapAsync(GameModeMap); + await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerExtraOptionsAsync(); UpdateDiscordPresence(); } + else + { + Players[0].Ready = true; + CopyPlayerDataToUI(); + } + + if (Players.Count >= playerLimit) + { + AddNotice("Player limit reached. The game room has been locked.".L10N("Client:Main:GameRoomNumberLimitReached")); + await LockGameAsync(); + } + } + + private async Task RemovePlayerAsync(string playerName) + { + AbortGameStart(); - private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) + PlayerInfo pInfo = Players.Find(p => p.Name == playerName); + + if (pInfo != null) { - PlayerInfo pInfo = new PlayerInfo(e.User.IRCUser.Name); - Players.Add(pInfo); + Players.Remove(pInfo); + CopyPlayerDataToUI(); - if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT && AIPlayers.Count > 0) - AIPlayers.RemoveAt(AIPlayers.Count - 1); + (string Name, CnCNetTunnel Tunnel) playerTunnel = playerTunnels.SingleOrDefault(q => q.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - sndJoinSound.Play(); -#if WINFORMS - WindowManager.FlashWindow(); -#endif + if (playerTunnel.Name is not null) + playerTunnels.Remove(playerTunnel); - if (!IsHost) - { - CopyPlayerDataToUI(); - return; - } + tunnelPlayerIds.Clear(); + dynamicV3GameTunnelHandlers.Clear(); - if (e.User.IRCUser.Name != ProgramConstants.PLAYERNAME) - { - // Changing the map applies forced settings (co-op sides etc.) to the - // new player, and it also sends an options broadcast message - await ChangeMapAsync(GameModeMap); + // This might not be necessary + if (IsHost) await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); - UpdateDiscordPresence(); - } - else - { - Players[0].Ready = true; - CopyPlayerDataToUI(); - } + } + + sndLeaveSound.Play(); + + if (IsHost && Locked && !ProgramConstants.IsInGame) + await UnlockGameAsync(true); + } + private void Channel_ChannelModesChanged(object sender, ChannelModeEventArgs e) + { + if (e.ModeString == "+i") + { if (Players.Count >= playerLimit) - { AddNotice("Player limit reached. The game room has been locked.".L10N("Client:Main:GameRoomNumberLimitReached")); - await LockGameAsync(); - } + else + AddNotice("The game host has locked the game room.".L10N("Client:Main:RoomLockedByHost")); + Locked = true; } - - private async Task RemovePlayerAsync(string playerName) + else if (e.ModeString == "-i") { - AbortGameStart(); + AddNotice("The game room has been unlocked.".L10N("Client:Main:GameRoomUnlocked")); + Locked = false; + } + } - PlayerInfo pInfo = Players.Find(p => p.Name == playerName); + private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e) + { + Logger.Log("CnCNetGameLobby_CTCPReceived"); - if (pInfo != null) + foreach (CommandHandlerBase cmdHandler in ctcpCommandHandlers) + { + if (cmdHandler.Handle(e.UserName, e.Message)) { - Players.Remove(pInfo); - - CopyPlayerDataToUI(); - - // This might not be necessary - if (IsHost) - await BroadcastPlayerOptionsAsync(); + UpdateDiscordPresence(); + return; } + } - sndLeaveSound.Play(); + Logger.Log("Unhandled CTCP command: " + e.Message + " from " + e.UserName); + } - if (IsHost && Locked && !ProgramConstants.IsInGame) - await UnlockGameAsync(true); + private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) + { + if (cncnetUserData.IsIgnored(e.Message.SenderIdent)) + { + lbChatMessages.AddMessage(new ChatMessage( + Color.Silver, + string.Format("Message blocked from {0}".L10N("Client:Main:MessageBlockedFromPlayer"), + e.Message.SenderName))); } - - private void Channel_ChannelModesChanged(object sender, ChannelModeEventArgs e) + else { - if (e.ModeString == "+i") - { - if (Players.Count >= playerLimit) - AddNotice("Player limit reached. The game room has been locked.".L10N("Client:Main:GameRoomNumberLimitReached")); - else - AddNotice("The game host has locked the game room.".L10N("Client:Main:RoomLockedByHost")); - Locked = true; - } - else if (e.ModeString == "-i") - { - AddNotice("The game room has been unlocked.".L10N("Client:Main:GameRoomUnlocked")); - Locked = false; - } + lbChatMessages.AddMessage(e.Message); + + if (e.Message.SenderName != null) + sndMessageSound.Play(); } + } - private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e) + /// + /// Starts the game for the game host. + /// + protected override async Task HostLaunchGameAsync() + { + if (Players.Count > 1) { - Logger.Log("CnCNetGameLobby_CTCPReceived"); + AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); - foreach (CommandHandlerBase cmdHandler in ctcpCommandHandlers) - { - if (cmdHandler.Handle(e.UserName, e.Message)) - { - UpdateDiscordPresence(); - return; - } - } + if (tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_2) + await HostLaunchGameV2TunnelAsync(); + else if (tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_3) + await HostLaunchGameV3TunnelAsync(); + else if (UserINISettings.Instance.UseDynamicTunnels) + await HostLaunchGameV3TunnelAsync(); + else + throw new InvalidOperationException("Unknown tunnel server version!"); - Logger.Log("Unhandled CTCP command: " + e.Message + " from " + e.UserName); + return; } - private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) - { - if (cncnetUserData.IsIgnored(e.Message.SenderIdent)) - { - lbChatMessages.AddMessage(new ChatMessage(Color.Silver, - string.Format("Message blocked from {0}".L10N("Client:Main:MessageBlockedFromPlayer"), e.Message.SenderName))); - } - else - { - lbChatMessages.AddMessage(e.Message); + Logger.Log("One player MP -- starting!"); + Players.ForEach(pInfo => pInfo.IsInGame = true); + CopyPlayerDataToUI(); + cncnetUserData.AddRecentPlayers(Players.Select(p => p.Name), channel.UIName); - if (e.Message.SenderName != null) - sndMessageSound.Play(); - } + await StartGameAsync(); + } + + private async Task HostLaunchGameV2TunnelAsync() + { + List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(Players.Count); + + if (playerPorts.Count < Players.Count) + { + ShowTunnelSelectionWindow(("An error occured while contacting " + + "the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); + AddNotice(("An error occured while contacting the specified CnCNet " + + "tunnel server. Please try using a different tunnel server " + + "(accessible by typing /CHANGETUNNEL in the chat box).").L10N("Client:Main:ConnectTunnelError2"), + ERROR_MESSAGE_COLOR); + return; } - /// - /// Starts the game for the game host. - /// - protected override async Task HostLaunchGameAsync() + StringBuilder sb = new StringBuilder(CnCNetCommands.GAME_START_V2).Append(' ').Append(UniqueGameID); + + for (int pId = 0; pId < Players.Count; pId++) { - if (Players.Count > 1) - { - if (isP2P) - throw new NotImplementedException("Peer-to-peer is not implemented yet."); + Players[pId].Port = playerPorts[pId]; - AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); + sb.Append(';') + .Append(Players[pId].Name) + .Append(';') + .Append("0.0.0.0:") + .Append(playerPorts[pId]); + } - if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) - { - await StartGame_V2TunnelAsync(); - } - else if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) - { - await StartGame_V3TunnelAsync(); - } - else - { - throw new InvalidOperationException("Unknown tunnel server version!"); - } + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + Players.ForEach(pInfo => pInfo.IsInGame = true); + await StartGameAsync(); + } - return; - } + private async Task HostLaunchGameV3TunnelAsync() + { + btnLaunchGame.InputEnabled = false; - Logger.Log("One player MP -- starting!"); + var random = new Random(); + uint randomNumber = (uint)random.Next(0, int.MaxValue - (MAX_PLAYER_COUNT / 2)) * (uint)random.Next(1, 3); + StringBuilder sb = new StringBuilder(CnCNetCommands.GAME_START_V3).Append(' ').Append(UniqueGameID); - Players.ForEach(pInfo => pInfo.IsInGame = true); - CopyPlayerDataToUI(); + tunnelPlayerIds.Clear(); - cncnetUserData.AddRecentPlayers(Players.Select(p => p.Name), channel.UIName); + for (int i = 0; i < Players.Count; i++) + { + uint id = randomNumber + (uint)i; - await StartGameAsync(); + sb.Append(';') + .Append(id); + tunnelPlayerIds.Add(id); } - private async Task StartGame_V2TunnelAsync() - { - List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(Players.Count); + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); - if (playerPorts.Count < Players.Count) - { - ShowTunnelSelectionWindow(("An error occured while contacting " + - "the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); - AddNotice(("An error occured while contacting the specified CnCNet " + - "tunnel server. Please try using a different tunnel server " + - "(accessible by typing /CHANGETUNNEL in the chat box).").L10N("Client:Main:ConnectTunnelError2"), ERROR_MESSAGE_COLOR); - return; - } + isStartingGame = true; - StringBuilder sb = new StringBuilder(CnCNetCommands.GAME_START_V2 + " "); - sb.Append(UniqueGameID); - for (int pId = 0; pId < Players.Count; pId++) - { - Players[pId].Port = playerPorts[pId]; - sb.Append(";"); - sb.Append(Players[pId].Name); - sb.Append(";"); - sb.Append("0.0.0.0:"); - sb.Append(playerPorts[pId]); - } - await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + ContactTunnel(); + } - Players.ForEach(pInfo => pInfo.IsInGame = true); + private void HandleGameStartV3TunnelMessage(string sender, string message) + { + if (sender != hostName) + return; - await StartGameAsync(); - } + string[] parts = message.Split(';'); - private async Task StartGame_V3TunnelAsync() - { - btnLaunchGame.InputEnabled = false; + if (parts.Length != Players.Count + 1) + return; - Random random = new Random(); - uint randomNumber = (uint)random.Next(0, int.MaxValue - (MAX_PLAYER_COUNT / 2)) * (uint)random.Next(1, 3); + UniqueGameID = Conversions.IntFromString(parts[0], -1); - StringBuilder sb = new StringBuilder(CnCNetCommands.GAME_START_V3 + " "); - sb.Append(UniqueGameID); - tunnelPlayerIds.Clear(); - for (int i = 0; i < Players.Count; i++) - { - uint id = randomNumber + (uint)i; - sb.Append(";"); - sb.Append(id); - tunnelPlayerIds.Add(id); - } - await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); - isStartingGame = true; + if (UniqueGameID < 0) + return; - ContactTunnel(); - } + tunnelPlayerIds.Clear(); - private void HandleGameStartV3TunnelMessage(string sender, string message) + for (int i = 1; i < parts.Length; i++) { - if (sender != hostName) + if (!uint.TryParse(parts[i], out uint id)) return; - string[] parts = message.Split(';'); + tunnelPlayerIds.Add(id); + } - if (parts.Length != Players.Count + 1) - return; + isStartingGame = true; - UniqueGameID = Conversions.IntFromString(parts[0], -1); - if (UniqueGameID < 0) - return; + ContactTunnel(); + } - tunnelPlayerIds.Clear(); + private void ContactTunnel() + { + isPlayerConnectedToTunnel = new bool[Players.Count]; - for (int i = 1; i < parts.Length; i++) - { - if (!uint.TryParse(parts[i], out uint id)) - return; + uint localId = tunnelPlayerIds[Players.FindIndex(p => p == FindLocalPlayer())]; - tunnelPlayerIds.Add(id); - } + dynamicV3GameTunnelHandlers.Clear(); - isStartingGame = true; - ContactTunnel(); - } + if (!UserINISettings.Instance.UseDynamicTunnels) + { + var dynamicV3GameTunnelHandler = new V3GameTunnelHandler(); - private void ContactTunnel() + dynamicV3GameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + dynamicV3GameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + + dynamicV3GameTunnelHandler.SetUp(tunnelHandler.CurrentTunnel, localId); + dynamicV3GameTunnelHandler.ConnectToTunnel(); + dynamicV3GameTunnelHandlers.Add(new(Players.Where(q => q != FindLocalPlayer()).Select(q => q.Name).ToList(), dynamicV3GameTunnelHandler)); + } + else { - isPlayerConnectedToTunnel = new bool[Players.Count]; - gameTunnelHandler.SetUp(tunnelHandler.CurrentTunnel, - tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); - gameTunnelHandler.ConnectToTunnel(); - // Abort starting the game if not everyone - // replies within the timer's limit - gameStartTimer.Start(); + foreach (IGrouping tunnelGrouping in playerTunnels.GroupBy(q => q.Tunnel)) + { + var dynamicV3GameTunnelHandler = new V3GameTunnelHandler(); + + dynamicV3GameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + dynamicV3GameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + + dynamicV3GameTunnelHandler.SetUp(tunnelGrouping.Key, localId); + dynamicV3GameTunnelHandler.ConnectToTunnel(); + dynamicV3GameTunnelHandlers.Add(new(tunnelGrouping.Select(q => q.Name).ToList(), dynamicV3GameTunnelHandler)); + } } - private async Task GameTunnelHandler_Connected_CallbackAsync() + // Abort starting the game if not everyone + // replies within the timer's limit + gameStartTimer.Start(); + } + + private async Task GameTunnelHandler_Connected_CallbackAsync() + { + if (UserINISettings.Instance.UseDynamicTunnels) { - isPlayerConnectedToTunnel[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)] = true; - await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_OK, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + if (dynamicV3GameTunnelHandlers.Any() && dynamicV3GameTunnelHandlers.All(q => q.Tunnel.IsConnected)) + isPlayerConnectedToTunnel[Players.FindIndex(p => p == FindLocalPlayer())] = true; } - - private async Task GameTunnelHandler_ConnectionFailed_CallbackAsync() + else { - await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_FAIL, QueuedMessageType.INSTANT_MESSAGE, 0); - HandleTunnelFail(ProgramConstants.PLAYERNAME); + isPlayerConnectedToTunnel[Players.FindIndex(p => p == FindLocalPlayer())] = true; } - private void HandleTunnelFail(string playerName) + await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_OK, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + } + + private async Task GameTunnelHandler_ConnectionFailed_CallbackAsync() + { + await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_FAIL, QueuedMessageType.INSTANT_MESSAGE, 0); + HandleTunnelFail(ProgramConstants.PLAYERNAME); + } + + private void HandleTunnelFail(string playerName) + { + Logger.Log(playerName + " failed to connect to tunnel - aborting game launch."); + AddNotice(playerName + " failed to connect to the tunnel server. Please " + + "retry or pick another tunnel server by type /CHANGETUNNEL to the chat input box."); + AbortGameStart(); + } + + private async Task HandlePlayerConnectedToTunnelAsync(string playerName) + { + if (!isStartingGame) + return; + + int index = Players.FindIndex(p => p.Name == playerName); + + if (index == -1) { - Logger.Log(playerName + " failed to connect to tunnel - aborting game launch."); - AddNotice(playerName + " failed to connect to the tunnel server. Please " + - "retry or pick another tunnel server by type /CHANGETUNNEL to the chat input box."); + Logger.Log("HandleTunnelConnected: Couldn't find player " + playerName + "!"); AbortGameStart(); + return; } - private async Task HandleTunnelConnectedAsync(string playerName) - { - if (!isStartingGame) - return; + isPlayerConnectedToTunnel[index] = true; - int index = Players.FindIndex(p => p.Name == playerName); - if (index == -1) - { - Logger.Log("HandleTunnelConnected: Couldn't find player " + playerName + "!"); - AbortGameStart(); - return; - } + if (isPlayerConnectedToTunnel.All(b => b)) + await HandleAllPlayersConnectedToTunnelAsync(); + } - isPlayerConnectedToTunnel[index] = true; + private async Task HandleAllPlayersConnectedToTunnelAsync() + { + Logger.Log("All players are connected to the tunnel, starting game!"); + AddNotice("All players have connected to the tunnel..."); - if (isPlayerConnectedToTunnel.All(b => b)) - { - Logger.Log("All players are connected to the tunnel, starting game!"); - AddNotice("All players have connected to the tunnel..."); - - // Remove our own ID from the list - List ids = new List(tunnelPlayerIds); - ids.Remove(tunnelPlayerIds[Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME)]); - List players = new List(Players); - int myIndex = Players.FindIndex(p => p.Name == ProgramConstants.PLAYERNAME); - players.RemoveAt(myIndex); - Tuple ports = gameTunnelHandler.CreatePlayerConnections(ids); - for (int i = 0; i < ports.Item1.Length; i++) - { - players[i].Port = ports.Item1[i]; - } + List playerPorts = new(); + + foreach (V3GameTunnelHandler dynamicV3GameTunnelHandler in dynamicV3GameTunnelHandlers.Select(q => q.Tunnel)) + { + var currentTunnelPlayers = Players.Where(q => dynamicV3GameTunnelHandlers.Single(r => r.Tunnel == dynamicV3GameTunnelHandler).Names.Contains(q.Name)).ToList(); + IEnumerable indexes = currentTunnelPlayers.Select(q => q.Index); + var playerIds = indexes.Select(q => tunnelPlayerIds[q]).ToList(); + List createdPlayerPorts = dynamicV3GameTunnelHandler.CreatePlayerConnections(playerIds); + int i = 0; - Players.Single(p => p.Name == ProgramConstants.PLAYERNAME).Port = ports.Item2; - gameStartTimer.Pause(); - btnLaunchGame.InputEnabled = true; - await StartGameAsync(); + foreach (PlayerInfo currentTunnelPlayer in currentTunnelPlayers) + { + currentTunnelPlayer.Port = createdPlayerPorts.Skip(i++).Take(1).Single(); } - } - private void AbortGameStart() - { - btnLaunchGame.InputEnabled = true; - gameTunnelHandler.Clear(); - gameStartTimer.Pause(); - isStartingGame = false; + playerPorts.AddRange(createdPlayerPorts); } - protected override string GetIPAddressForPlayer(PlayerInfo player) + int gamePort = V3GameTunnelHandler.GetFreePort(playerPorts); + + foreach (V3GameTunnelHandler dynamicV3GameTunnelHandler in dynamicV3GameTunnelHandlers.Select(q => q.Tunnel)) { - if (isP2P) - return IPAddress.Parse(player.IPAddress).MapToIPv4().ToString(); + dynamicV3GameTunnelHandler.StartPlayerConnections(gamePort); + } - if (tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) - return IPAddress.Loopback.MapToIPv4().ToString(); + FindLocalPlayer().Port = gamePort; - return base.GetIPAddressForPlayer(player); - } + gameStartTimer.Pause(); - protected override Task RequestPlayerOptionsAsync(int side, int color, int start, int team) - { - byte[] value = { - (byte)side, - (byte)color, - (byte)start, - (byte)team - }; + btnLaunchGame.InputEnabled = true; - int intValue = BitConverter.ToInt32(value, 0); + await StartGameAsync(); + } - return channel.SendCTCPMessageAsync( - FormattableString.Invariant($"{CnCNetCommands.OPTIONS_REQUEST} {intValue}"), - QueuedMessageType.GAME_SETTINGS_MESSAGE, 6); - } + private void AbortGameStart() + { + btnLaunchGame.InputEnabled = true; - protected override async Task RequestReadyStatusAsync() + foreach (V3GameTunnelHandler dynamicV3GameTunnelHandler in dynamicV3GameTunnelHandlers.Select(q => q.Tunnel)) { - if (Map == null || GameMode == null) - { - AddNotice(("The game host needs to select a different map or " + - "you will be unable to participate in the match.").L10N("Client:Main:HostMustReplaceMap")); + dynamicV3GameTunnelHandler.Clear(); + } - if (chkAutoReady.Checked) - await channel.SendCTCPMessageAsync(CnCNetCommands.READY_REQUEST + " 0", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); + gameStartTimer.Pause(); - return; - } + isStartingGame = false; + } - PlayerInfo pInfo = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); - int readyState = 0; + protected override string GetIPAddressForPlayer(PlayerInfo player) + { + if (UserINISettings.Instance.UseP2P) + return IPAddress.Parse(player.IPAddress).MapToIPv4().ToString(); - if (chkAutoReady.Checked) - readyState = 2; - else if (!pInfo.Ready) - readyState = 1; + if (UserINISettings.Instance.UseDynamicTunnels || tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) + return IPAddress.Loopback.MapToIPv4().ToString(); - await channel.SendCTCPMessageAsync($"{CnCNetCommands.READY_REQUEST} {readyState}", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); - } + return base.GetIPAddressForPlayer(player); + } - protected override void AddNotice(string message, Color color) => channel.AddMessage(new(color, message)); + protected override Task RequestPlayerOptionsAsync(int side, int color, int start, int team) + { + byte[] value = + { + (byte)side, + (byte)color, + (byte)start, + (byte)team + }; + int intValue = BitConverter.ToInt32(value, 0); + + return channel.SendCTCPMessageAsync( + FormattableString.Invariant($"{CnCNetCommands.OPTIONS_REQUEST} {intValue}"), + QueuedMessageType.GAME_SETTINGS_MESSAGE, + 6); + } - /// - /// Handles player option requests received from non-host players. - /// - private async Task HandleOptionsRequestAsync(string playerName, int options) + protected override async Task RequestReadyStatusAsync() + { + if (Map == null || GameMode == null) { - if (!IsHost) - return; + AddNotice(("The game host needs to select a different map or " + + "you will be unable to participate in the match.").L10N("Client:Main:HostMustReplaceMap")); - if (ProgramConstants.IsInGame) - return; - - PlayerInfo pInfo = Players.Find(p => p.Name == playerName); + if (chkAutoReady.Checked) + await channel.SendCTCPMessageAsync(CnCNetCommands.READY_REQUEST + " 0", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); - if (pInfo == null) - return; + return; + } - byte[] bytes = BitConverter.GetBytes(options); + PlayerInfo pInfo = FindLocalPlayer(); + int readyState = 0; - int side = bytes[0]; - int color = bytes[1]; - int start = bytes[2]; - int team = bytes[3]; + if (chkAutoReady.Checked) + readyState = 2; + else if (!pInfo.Ready) + readyState = 1; - if (side < 0 || side > SideCount + RandomSelectorCount) - return; + await channel.SendCTCPMessageAsync($"{CnCNetCommands.READY_REQUEST} {readyState}", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); + } - if (color < 0 || color > MPColors.Count) - return; + protected override void AddNotice(string message, Color color) => channel.AddMessage(new(color, message)); - var disallowedSides = GetDisallowedSides(); + /// + /// Handles player option requests received from non-host players. + /// + private async Task HandleOptionsRequestAsync(string playerName, int options) + { + if (!IsHost) + return; - if (side > 0 && side <= SideCount && disallowedSides[side - 1]) - return; + if (ProgramConstants.IsInGame) + return; - if (Map.CoopInfo != null) - { - if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) - return; + PlayerInfo pInfo = Players.Find(p => p.Name == playerName); - if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) - return; - } + if (pInfo == null) + return; - if (start < 0 || start > Map.MaxPlayers) - return; + byte[] bytes = BitConverter.GetBytes(options); + int side = bytes[0]; + int color = bytes[1]; + int start = bytes[2]; + int team = bytes[3]; - if (team < 0 || team > 4) - return; + if (side > SideCount + RandomSelectorCount) + return; - if (side != pInfo.SideId - || start != pInfo.StartingLocation - || team != pInfo.TeamId) - { - ClearReadyStatuses(); - } + if (color > MPColors.Count) + return; - pInfo.SideId = side; - pInfo.ColorId = color; - pInfo.StartingLocation = start; - pInfo.TeamId = team; + bool[] disallowedSides = GetDisallowedSides(); - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - } + if (side > 0 && side <= SideCount && disallowedSides[side - 1]) + return; - /// - /// Handles "I'm ready" messages received from non-host players. - /// - private async Task HandleReadyRequestAsync(string playerName, int readyStatus) + if (Map.CoopInfo != null) { - if (!IsHost) + if (Map.CoopInfo.DisallowedPlayerSides.Contains(side - 1) || side == SideCount + RandomSelectorCount) return; - PlayerInfo pInfo = Players.Find(p => p.Name == playerName); - - if (pInfo == null) + if (Map.CoopInfo.DisallowedPlayerColors.Contains(color - 1)) return; + } - pInfo.Ready = readyStatus > 0; - pInfo.AutoReady = readyStatus > 1; + if (start > Map.MaxPlayers) + return; - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - } + if (team > 4) + return; - /// - /// Broadcasts player options to non-host players. - /// - protected override Task BroadcastPlayerOptionsAsync() + if (side != pInfo.SideId + || start != pInfo.StartingLocation + || team != pInfo.TeamId) { - // Broadcast player options - StringBuilder sb = new StringBuilder(CnCNetCommands.PLAYER_OPTIONS + " "); - foreach (PlayerInfo pInfo in Players.Concat(AIPlayers)) - { - if (pInfo.IsAI) - sb.Append(pInfo.AILevel); - else - sb.Append(pInfo.Name); - sb.Append(";"); + ClearReadyStatuses(); + } - // Combine the options into one integer to save bandwidth in - // cases where the player uses default options (this is common for AI players) - // Will hopefully make GameSurge kicking people a bit less common - byte[] byteArray = new byte[] - { - (byte)pInfo.TeamId, - (byte)pInfo.StartingLocation, - (byte)pInfo.ColorId, - (byte)pInfo.SideId, - }; - - int value = BitConverter.ToInt32(byteArray, 0); - sb.Append(value); - sb.Append(";"); - if (!pInfo.IsAI) - { - if (pInfo.AutoReady && !pInfo.IsInGame) - sb.Append(2); - else - sb.Append(Convert.ToInt32(pInfo.Ready)); - sb.Append(';'); - } - } + pInfo.SideId = side; + pInfo.ColorId = color; + pInfo.StartingLocation = start; + pInfo.TeamId = team; - return channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_PLAYERS_MESSAGE, 11); - } + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + } - protected override async Task PlayerExtraOptions_OptionsChangedAsync() - { - await base.PlayerExtraOptions_OptionsChangedAsync(); - await BroadcastPlayerExtraOptionsAsync(); - } + /// + /// Handles "I'm ready" messages received from non-host players. + /// + private async Task HandleReadyRequestAsync(string playerName, int readyStatus) + { + if (!IsHost) + return; - protected override async Task BroadcastPlayerExtraOptionsAsync() - { - if (!IsHost) - return; + PlayerInfo pInfo = Players.Find(p => p.Name == playerName); - var playerExtraOptions = GetPlayerExtraOptions(); + if (pInfo == null) + return; - await channel.SendCTCPMessageAsync(playerExtraOptions.ToCncnetMessage(), QueuedMessageType.GAME_PLAYERS_EXTRA_MESSAGE, 11, true); - } + pInfo.Ready = readyStatus > 0; + pInfo.AutoReady = readyStatus > 1; + + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + } + + /// + /// Broadcasts player options to non-host players. + /// + protected override Task BroadcastPlayerOptionsAsync() + { + // Broadcast player options + var sb = new StringBuilder(CnCNetCommands.PLAYER_OPTIONS + " "); - /// - /// Handles player option messages received from the game host. - /// - private void ApplyPlayerOptions(string sender, string message) + foreach (PlayerInfo pInfo in Players.Concat(AIPlayers)) { - if (sender != hostName) - return; + if (pInfo.IsAI) + sb.Append(pInfo.AILevel); + else + sb.Append(pInfo.Name); - Players.Clear(); - AIPlayers.Clear(); + sb.Append(';'); - string[] parts = message.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - for (int i = 0; i < parts.Length;) + // Combine the options into one integer to save bandwidth in + // cases where the player uses default options (this is common for AI players) + // Will hopefully make GameSurge kicking people a bit less common + byte[] byteArray = new[] { - PlayerInfo pInfo = new PlayerInfo(); + (byte)pInfo.TeamId, + (byte)pInfo.StartingLocation, + (byte)pInfo.ColorId, + (byte)pInfo.SideId, + }; + int value = BitConverter.ToInt32(byteArray, 0); - string pName = parts[i]; - int converted = Conversions.IntFromString(pName, -1); + sb.Append(value); + sb.Append(';'); - if (converted > -1) - { - pInfo.IsAI = true; - pInfo.AILevel = converted; - pInfo.Name = AILevelToName(converted); - } + if (!pInfo.IsAI) + { + if (pInfo.AutoReady && !pInfo.IsInGame) + sb.Append(2); else - { - pInfo.Name = pName; - - // If we can't find the player from the channel user list, - // ignore the player - // They've either left the channel or got kicked before the - // player options message reached us - if (channel.Users.Find(pName) == null) - { - i += HUMAN_PLAYER_OPTIONS_LENGTH; - continue; - } - } - - if (parts.Length <= i + 1) - return; - - int playerOptions = Conversions.IntFromString(parts[i + 1], -1); - if (playerOptions == -1) - return; + sb.Append(Convert.ToInt32(pInfo.Ready)); - byte[] byteArray = BitConverter.GetBytes(playerOptions); + sb.Append(';'); + } + } - int team = byteArray[0]; - int start = byteArray[1]; - int color = byteArray[2]; - int side = byteArray[3]; + return channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_PLAYERS_MESSAGE, 11); + } - if (side < 0 || side > SideCount + RandomSelectorCount) - return; + protected override async Task PlayerExtraOptions_OptionsChangedAsync() + { + await base.PlayerExtraOptions_OptionsChangedAsync(); + await BroadcastPlayerExtraOptionsAsync(); + } - if (color < 0 || color > MPColors.Count) - return; + protected override async Task BroadcastPlayerExtraOptionsAsync() + { + if (!IsHost) + return; - if (start < 0 || start > MAX_PLAYER_COUNT) - return; + PlayerExtraOptions playerExtraOptions = GetPlayerExtraOptions(); - if (team < 0 || team > 4) - return; + await channel.SendCTCPMessageAsync(playerExtraOptions.ToCncnetMessage(), QueuedMessageType.GAME_PLAYERS_EXTRA_MESSAGE, 11, true); + } - pInfo.TeamId = byteArray[0]; - pInfo.StartingLocation = byteArray[1]; - pInfo.ColorId = byteArray[2]; - pInfo.SideId = byteArray[3]; + private Task BroadcastPlayerTunnelPingsAsync() + => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_TUNNEL_PINGS + " " + pinnedTunnelPingsMessage, QueuedMessageType.SYSTEM_MESSAGE, 10); - if (pInfo.IsAI) - { - pInfo.Ready = true; - AIPlayers.Add(pInfo); - i += AI_PLAYER_OPTIONS_LENGTH; - } - else - { - if (parts.Length <= i + 2) - return; + /// + /// Handles player option messages received from the game host. + /// + private void ApplyPlayerOptions(string sender, string message) + { + if (sender != hostName) + return; - int readyStatus = Conversions.IntFromString(parts[i + 2], -1); + Players.Clear(); + AIPlayers.Clear(); - if (readyStatus == -1) - return; + string[] parts = message.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - pInfo.Ready = readyStatus > 0; - pInfo.AutoReady = readyStatus > 1; + for (int i = 0; i < parts.Length;) + { + var pInfo = new PlayerInfo(); + string pName = parts[i]; + int converted = Conversions.IntFromString(pName, -1); - if (pInfo.Name == ProgramConstants.PLAYERNAME) - btnLaunchGame.Text = pInfo.Ready ? BTN_LAUNCH_NOT_READY : BTN_LAUNCH_READY; + if (converted > -1) + { + pInfo.IsAI = true; + pInfo.AILevel = converted; + pInfo.Name = AILevelToName(converted); + } + else + { + pInfo.Name = pName; - Players.Add(pInfo); + // If we can't find the player from the channel user list, + // ignore the player + // They've either left the channel or got kicked before the + // player options message reached us + if (channel.Users.Find(pName) == null) + { i += HUMAN_PLAYER_OPTIONS_LENGTH; + continue; } } - CopyPlayerDataToUI(); - } + if (parts.Length <= i + 1) + return; - /// - /// Broadcasts game options to non-host players - /// when the host has changed an option. - /// - protected override async Task OnGameOptionChangedAsync() - { - await base.OnGameOptionChangedAsync(); + int playerOptions = Conversions.IntFromString(parts[i + 1], -1); - if (!IsHost) + if (playerOptions == -1) return; - bool[] optionValues = new bool[CheckBoxes.Count]; - for (int i = 0; i < CheckBoxes.Count; i++) - optionValues[i] = CheckBoxes[i].Checked; + byte[] byteArray = BitConverter.GetBytes(playerOptions); + int team = byteArray[0]; + int start = byteArray[1]; + int color = byteArray[2]; + int side = byteArray[3]; - // Let's pack the booleans into bytes - List byteList = Conversions.BoolArrayIntoBytes(optionValues).ToList(); + if (side > SideCount + RandomSelectorCount) + return; - while (byteList.Count % 4 != 0) - byteList.Add(0); + if (color > MPColors.Count) + return; - int integerCount = byteList.Count / 4; - byte[] byteArray = byteList.ToArray(); + if (start > MAX_PLAYER_COUNT) + return; - ExtendedStringBuilder sb = new ExtendedStringBuilder(CnCNetCommands.GAME_OPTIONS + " ", true, ';'); + if (team > 4) + return; - for (int i = 0; i < integerCount; i++) - sb.Append(BitConverter.ToInt32(byteArray, i * 4)); + pInfo.TeamId = byteArray[0]; + pInfo.StartingLocation = byteArray[1]; + pInfo.ColorId = byteArray[2]; + pInfo.SideId = byteArray[3]; - // We don't gain much in most cases by packing the drop-down values - // (because they're bytes to begin with, and usually non-zero), - // so let's just transfer them as usual + if (pInfo.IsAI) + { + pInfo.Ready = true; - foreach (GameLobbyDropDown dd in DropDowns) - sb.Append(dd.SelectedIndex); + AIPlayers.Add(pInfo); - sb.Append(Convert.ToInt32(Map.Official)); - sb.Append(Map.SHA1); - sb.Append(GameMode.Name); - sb.Append(FrameSendRate); - sb.Append(MaxAhead); - sb.Append(ProtocolVersion); - sb.Append(RandomSeed); - sb.Append(Convert.ToInt32(RemoveStartingLocations)); - sb.Append(Map.UntranslatedName); + i += AI_PLAYER_OPTIONS_LENGTH; + } + else + { + if (parts.Length <= i + 2) + return; - await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); - } + int readyStatus = Conversions.IntFromString(parts[i + 2], -1); - /// - /// Handles game option messages received from the game host. - /// - private async Task ApplyGameOptionsAsync(string sender, string message) - { - if (sender != hostName) - return; + if (readyStatus == -1) + return; - string[] parts = message.Split(';'); + pInfo.Ready = readyStatus > 0; + pInfo.AutoReady = readyStatus > 1; - int checkBoxIntegerCount = (CheckBoxes.Count / 32) + 1; + if (pInfo == FindLocalPlayer()) + btnLaunchGame.Text = pInfo.Ready ? BTN_LAUNCH_NOT_READY : BTN_LAUNCH_READY; - int partIndex = checkBoxIntegerCount + DropDowns.Count; + Players.Add(pInfo); - if (parts.Length < partIndex + 6) - { - AddNotice(("The game host has sent an invalid game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); - return; + i += HUMAN_PLAYER_OPTIONS_LENGTH; } + } - string mapOfficial = parts[partIndex]; - bool isMapOfficial = Conversions.BooleanFromString(mapOfficial, true); - - string mapSHA1 = parts[partIndex + 1]; + CopyPlayerDataToUI(); + } - string gameMode = parts[partIndex + 2]; + /// + /// Broadcasts game options to non-host players + /// when the host has changed an option. + /// + protected override async Task OnGameOptionChangedAsync() + { + await base.OnGameOptionChangedAsync(); - int frameSendRate = Conversions.IntFromString(parts[partIndex + 3], FrameSendRate); - if (frameSendRate != FrameSendRate) - { - FrameSendRate = frameSendRate; - AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); - } + if (!IsHost) + return; - int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead); - if (maxAhead != MaxAhead) - { - MaxAhead = maxAhead; - AddNotice(string.Format("The game host has changed MaxAhead to {0}".L10N("Client:Main:HostChangeMaxAhead"), maxAhead)); - } + bool[] optionValues = new bool[CheckBoxes.Count]; + for (int i = 0; i < CheckBoxes.Count; i++) + optionValues[i] = CheckBoxes[i].Checked; - int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion); - if (protocolVersion != ProtocolVersion) - { - ProtocolVersion = protocolVersion; - AddNotice(string.Format("The game host has changed ProtocolVersion to {0}".L10N("Client:Main:HostChangeProtocolVersion"), protocolVersion)); - } + // Let's pack the booleans into bytes + var byteList = Conversions.BoolArrayIntoBytes(optionValues).ToList(); - string mapName = parts[partIndex + 8]; - GameModeMap currentGameModeMap = GameModeMap; + while (byteList.Count % 4 != 0) + byteList.Add(0); - lastMapSHA1 = mapSHA1; - lastMapName = mapName; + int integerCount = byteList.Count / 4; + byte[] byteArray = byteList.ToArray(); - GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); - if (GameModeMap == null) - { - await ChangeMapAsync(null); + var sb = new ExtendedStringBuilder(CnCNetCommands.GAME_OPTIONS + " ", true, ';'); - if (!isMapOfficial) - await RequestMapAsync(); - else - await ShowOfficialMapMissingMessageAsync(mapSHA1); - } - else if (GameModeMap != currentGameModeMap) - { - await ChangeMapAsync(GameModeMap); - } + for (int i = 0; i < integerCount; i++) + sb.Append(BitConverter.ToInt32(byteArray, i * 4)); - // By changing the game options after changing the map, we know which - // game options were changed by the map and which were changed by the game host + // We don't gain much in most cases by packing the drop-down values + // (because they're bytes to begin with, and usually non-zero), + // so let's just transfer them as usual - // If the map doesn't exist on the local installation, it's impossible - // to know which options were set by the host and which were set by the - // map, so we'll just assume that the host has set all the options. - // Very few (if any) custom maps force options, so it'll be correct nearly always + foreach (GameLobbyDropDown dd in DropDowns) + sb.Append(dd.SelectedIndex); - for (int i = 0; i < checkBoxIntegerCount; i++) - { - if (parts.Length <= i) - return; + sb.Append(Convert.ToInt32(Map.Official)); + sb.Append(Map.SHA1); + sb.Append(GameMode.Name); + sb.Append(FrameSendRate); + sb.Append(MaxAhead); + sb.Append(ProtocolVersion); + sb.Append(RandomSeed); + sb.Append(Convert.ToInt32(RemoveStartingLocations)); + sb.Append(Map.UntranslatedName); - int checkBoxStatusInt; - bool success = int.TryParse(parts[i], out checkBoxStatusInt); + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); + } - if (!success) - { - AddNotice(("Failed to parse check box options sent by game host!" + - "The game host's game version might be different from yours.").L10N("Client:Main:HostCheckBoxParseError"), Color.Red); - return; - } + /// + /// Handles game option messages received from the game host. + /// + private async Task ApplyGameOptionsAsync(string sender, string message) + { + if (sender != hostName) + return; - byte[] byteArray = BitConverter.GetBytes(checkBoxStatusInt); - bool[] boolArray = Conversions.BytesIntoBoolArray(byteArray); + string[] parts = message.Split(';'); + int checkBoxIntegerCount = (CheckBoxes.Count / 32) + 1; + int partIndex = checkBoxIntegerCount + DropDowns.Count; - for (int optionIndex = 0; optionIndex < boolArray.Length; optionIndex++) - { - int gameOptionIndex = i * 32 + optionIndex; + if (parts.Length < partIndex + 6) + { + AddNotice(("The game host has sent an invalid game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); + return; + } - if (gameOptionIndex >= CheckBoxes.Count) - break; + string mapOfficial = parts[partIndex]; + bool isMapOfficial = Conversions.BooleanFromString(mapOfficial, true); + string mapHash = parts[partIndex + 1]; + string gameMode = parts[partIndex + 2]; + int frameSendRate = Conversions.IntFromString(parts[partIndex + 3], FrameSendRate); - GameLobbyCheckBox checkBox = CheckBoxes[gameOptionIndex]; + if (frameSendRate != FrameSendRate) + { + FrameSendRate = frameSendRate; - if (checkBox.Checked != boolArray[optionIndex]) - { - if (boolArray[optionIndex]) - AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), checkBox.Text)); - else - AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), checkBox.Text)); - } + AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); + } - CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex]; - } - } + int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead); - for (int i = checkBoxIntegerCount; i < DropDowns.Count + checkBoxIntegerCount; i++) - { - if (parts.Length <= i) - { - AddNotice(("The game host has sent an invalid game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); - return; - } + if (maxAhead != MaxAhead) + { + MaxAhead = maxAhead; - int ddSelectedIndex; - bool success = int.TryParse(parts[i], out ddSelectedIndex); + AddNotice(string.Format("The game host has changed MaxAhead to {0}".L10N("Client:Main:HostChangeMaxAhead"), maxAhead)); + } - if (!success) - { - AddNotice(("Failed to parse drop down options sent by game host (2)! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalidTheSecondTime"), Color.Red); - return; - } + int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion); - GameLobbyDropDown dd = DropDowns[i - checkBoxIntegerCount]; + if (protocolVersion != ProtocolVersion) + { + ProtocolVersion = protocolVersion; - if (ddSelectedIndex < -1 || ddSelectedIndex >= dd.Items.Count) - continue; + AddNotice(string.Format("The game host has changed ProtocolVersion to {0}".L10N("Client:Main:HostChangeProtocolVersion"), protocolVersion)); + } - if (dd.SelectedIndex != ddSelectedIndex) - { - string ddName = dd.OptionName; - if (dd.OptionName == null) - ddName = dd.Name; + string mapName = parts[partIndex + 8]; + GameModeMap currentGameModeMap = GameModeMap; - AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); - } + lastMapHash = mapHash; + lastMapName = mapName; - DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex; - } + GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapHash); + + if (GameModeMap == null) + { + await ChangeMapAsync(null); - int randomSeed; - bool parseSuccess = int.TryParse(parts[partIndex + 6], out randomSeed); + if (!isMapOfficial) + await RequestMapAsync(); + else + await ShowOfficialMapMissingMessageAsync(mapHash); + } + else if (GameModeMap != currentGameModeMap) + { + await ChangeMapAsync(GameModeMap); + } + + // By changing the game options after changing the map, we know which + // game options were changed by the map and which were changed by the game host + + // If the map doesn't exist on the local installation, it's impossible + // to know which options were set by the host and which were set by the + // map, so we'll just assume that the host has set all the options. + // Very few (if any) custom maps force options, so it'll be correct nearly always - if (!parseSuccess) + for (int i = 0; i < checkBoxIntegerCount; i++) + { + if (parts.Length <= i) + return; + + bool success = int.TryParse(parts[i], out int checkBoxStatusInt); + + if (!success) { - AddNotice(("Failed to parse random seed from game options message! " + - "The game host's game version might be different from yours.").L10N("Client:Main:HostRandomSeedError"), Color.Red); + AddNotice(("Failed to parse check box options sent by game host!" + + "The game host's game version might be different from yours.").L10N("Client:Main:HostCheckBoxParseError"), Color.Red); + return; } - bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString(parts[partIndex + 7], - Convert.ToInt32(RemoveStartingLocations))); - SetRandomStartingLocations(removeStartingLocations); + byte[] byteArray = BitConverter.GetBytes(checkBoxStatusInt); + bool[] boolArray = Conversions.BytesIntoBoolArray(byteArray); + + for (int optionIndex = 0; optionIndex < boolArray.Length; optionIndex++) + { + int gameOptionIndex = (i * 32) + optionIndex; + + if (gameOptionIndex >= CheckBoxes.Count) + break; + + GameLobbyCheckBox checkBox = CheckBoxes[gameOptionIndex]; + + if (checkBox.Checked != boolArray[optionIndex]) + { + if (boolArray[optionIndex]) + AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), checkBox.Text)); + else + AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), checkBox.Text)); + } - RandomSeed = randomSeed; + CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex]; + } } - private async Task RequestMapAsync() + for (int i = checkBoxIntegerCount; i < DropDowns.Count + checkBoxIntegerCount; i++) { - if (UserINISettings.Instance.EnableMapSharing) + if (parts.Length <= i) { - AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist")); - mapSharingConfirmationPanel.ShowForMapDownload(); + AddNotice(("The game host has sent an invalid game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); + return; } - else + + bool success = int.TryParse(parts[i], out int ddSelectedIndex); + + if (!success) + { + AddNotice(("Failed to parse drop down options sent by game host (2)! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalidTheSecondTime"), Color.Red); + return; + } + + GameLobbyDropDown dd = DropDowns[i - checkBoxIntegerCount]; + + if (ddSelectedIndex < -1 || ddSelectedIndex >= dd.Items.Count) + continue; + + if (dd.SelectedIndex != ddSelectedIndex) { - AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + " " + - ("Because you've disabled map sharing, it cannot be transferred. The game host needs " + - "to change the map or you will be unable to participate in the match.").L10N("Client:Main:MapSharingDisabledNotice")); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DISABLED, QueuedMessageType.SYSTEM_MESSAGE, 9); + string ddName = dd.OptionName; + + if (dd.OptionName == null) + ddName = dd.Name; + + AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); } + + DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex; } - private Task ShowOfficialMapMissingMessageAsync(string sha1) + bool parseSuccess = int.TryParse(parts[partIndex + 6], out int randomSeed); + + if (!parseSuccess) { - AddNotice(("The game host has selected an official map that doesn't exist on your installation. " + - "This could mean that the game host has modified game files, or is running a different game version. " + - "They need to change the map or you will be unable to participate in the match.").L10N("Client:Main:OfficialMapNotExist")); - return channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + sha1, QueuedMessageType.SYSTEM_MESSAGE, 9); + AddNotice(("Failed to parse random seed from game options message! " + + "The game host's game version might be different from yours.").L10N("Client:Main:HostRandomSeedError"), Color.Red); } - private void MapSharingConfirmationPanel_MapDownloadConfirmed(object sender, EventArgs e) + bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString(parts[partIndex + 7], + Convert.ToInt32(RemoveStartingLocations))); + + SetRandomStartingLocations(removeStartingLocations); + + RandomSeed = randomSeed; + } + + private async Task RequestMapAsync() + { + if (UserINISettings.Instance.EnableMapSharing) { - Logger.Log("Map sharing confirmed."); - AddNotice("Attempting to download map.".L10N("Client:Main:DownloadingMap")); - mapSharingConfirmationPanel.SetDownloadingStatus(); - MapSharer.DownloadMap(lastMapSHA1, localGame, lastMapName); + AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist")); + mapSharingConfirmationPanel.ShowForMapDownload(); } - - protected override Task ChangeMapAsync(GameModeMap gameModeMap) + else { - mapSharingConfirmationPanel.Disable(); - return base.ChangeMapAsync(gameModeMap); + AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + " " + + ("Because you've disabled map sharing, it cannot be transferred. The game host needs " + + "to change the map or you will be unable to participate in the match.").L10N("Client:Main:MapSharingDisabledNotice")); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DISABLED, QueuedMessageType.SYSTEM_MESSAGE, 9); } + } - /// - /// Signals other players that the local player has returned from the game, - /// and unlocks the game as well as generates a new random seed as the game host. - /// - protected override async Task GameProcessExitedAsync() - { - await base.GameProcessExitedAsync(); - await channel.SendCTCPMessageAsync(CnCNetCommands.RETURN, QueuedMessageType.SYSTEM_MESSAGE, 20); - ReturnNotification(ProgramConstants.PLAYERNAME); + private Task ShowOfficialMapMissingMessageAsync(string sha1) + { + AddNotice(("The game host has selected an official map that doesn't exist on your installation. " + + "This could mean that the game host has modified game files, or is running a different game version. " + + "They need to change the map or you will be unable to participate in the match.").L10N("Client:Main:OfficialMapNotExist")); + return channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + sha1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } - if (IsHost) - { - RandomSeed = new Random().Next(); - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); + private void MapSharingConfirmationPanel_MapDownloadConfirmed(object sender, EventArgs e) + { + Logger.Log("Map sharing confirmed."); + AddNotice("Attempting to download map.".L10N("Client:Main:DownloadingMap")); + mapSharingConfirmationPanel.SetDownloadingStatus(); + MapSharer.DownloadMap(lastMapHash, localGame, lastMapName); + } - if (Players.Count < playerLimit) - await UnlockGameAsync(true); - } - } + protected override Task ChangeMapAsync(GameModeMap gameModeMap) + { + mapSharingConfirmationPanel.Disable(); + return base.ChangeMapAsync(gameModeMap); + } + + /// + /// Signals other players that the local player has returned from the game, + /// and unlocks the game as well as generates a new random seed as the game host. + /// + protected override async Task GameProcessExitedAsync() + { + await base.GameProcessExitedAsync(); + await channel.SendCTCPMessageAsync(CnCNetCommands.RETURN, QueuedMessageType.SYSTEM_MESSAGE, 20); + ReturnNotification(ProgramConstants.PLAYERNAME); - /// - /// Handles the "START" (game start) command sent by the game host. - /// - private async Task NonHostLaunchGameAsync(string sender, string message) + if (IsHost) { - if (tunnelHandler.CurrentTunnel.Version != Constants.TUNNEL_VERSION_2) - return; + RandomSeed = new Random().Next(); + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerExtraOptionsAsync(); - if (sender != hostName) - return; + if (Players.Count < playerLimit) + await UnlockGameAsync(true); + } + } - string[] parts = message.Split(';'); + /// + /// Handles the "START" (game start) command sent by the game host. + /// + private async Task NonHostLaunchGameAsync(string sender, string message) + { + if (tunnelHandler.CurrentTunnel.Version != Constants.TUNNEL_VERSION_2) + return; - if (parts.Length < 1) - return; + if (sender != hostName) + return; - UniqueGameID = Conversions.IntFromString(parts[0], -1); - if (UniqueGameID < 0) - return; + string[] parts = message.Split(';'); - var recentPlayers = new List(); + if (parts.Length < 1) + return; - for (int i = 1; i < parts.Length; i += 2) - { - if (parts.Length <= i + 1) - return; + UniqueGameID = Conversions.IntFromString(parts[0], -1); + if (UniqueGameID < 0) + return; - string pName = parts[i]; - string[] ipAndPort = parts[i + 1].Split(':'); + var recentPlayers = new List(); - if (ipAndPort.Length < 2) - return; + for (int i = 1; i < parts.Length; i += 2) + { + if (parts.Length <= i + 1) + return; - int port; - bool success = int.TryParse(ipAndPort[1], out port); + string pName = parts[i]; + string[] ipAndPort = parts[i + 1].Split(':'); - if (!success) - return; + if (ipAndPort.Length < 2) + return; - PlayerInfo pInfo = Players.Find(p => p.Name == pName); + bool success = int.TryParse(ipAndPort[1], out int port); - if (pInfo == null) - return; + if (!success) + return; - pInfo.Port = port; - recentPlayers.Add(pName); - } + PlayerInfo pInfo = Players.Find(p => p.Name == pName); + + if (pInfo == null) + return; - cncnetUserData.AddRecentPlayers(recentPlayers, channel.UIName); - await StartGameAsync(); + pInfo.Port = port; + recentPlayers.Add(pName); } - protected override async Task StartGameAsync() - { - AddNotice("Starting game...".L10N("Client:Main:StartingGame")); + cncnetUserData.AddRecentPlayers(recentPlayers, channel.UIName); + await StartGameAsync(); + } - isStartingGame = false; + protected override async Task StartGameAsync() + { + AddNotice("Starting game...".L10N("Client:Main:StartingGame")); - FileHashCalculator fhc = new FileHashCalculator(); - fhc.CalculateHashes(GameModeMaps.GameModes); + isStartingGame = false; - if (gameFilesHash != fhc.GetCompleteHash()) - { - Logger.Log("Game files modified during client session!"); - await channel.SendCTCPMessageAsync(CnCNetCommands.CHEAT_DETECTED, QueuedMessageType.INSTANT_MESSAGE, 0); - HandleCheatDetectedMessage(ProgramConstants.PLAYERNAME); - } + var fhc = new FileHashCalculator(); - await base.StartGameAsync(); - } + fhc.CalculateHashes(GameModeMaps.GameModes); - protected override void WriteSpawnIniAdditions(IniFile iniFile) + if (gameFilesHash != fhc.GetCompleteHash()) { - base.WriteSpawnIniAdditions(iniFile); + Logger.Log("Game files modified during client session!"); + await channel.SendCTCPMessageAsync(CnCNetCommands.CHEAT_DETECTED, QueuedMessageType.INSTANT_MESSAGE, 0); + HandleCheatDetectedMessage(ProgramConstants.PLAYERNAME); + } - if (!isP2P && tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) - { - iniFile.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); - iniFile.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); - } + await base.StartGameAsync(); + } - iniFile.SetIntValue("Settings", "GameID", UniqueGameID); - iniFile.SetBooleanValue("Settings", "Host", IsHost); + protected override void WriteSpawnIniAdditions(IniFile iniFile) + { + base.WriteSpawnIniAdditions(iniFile); - PlayerInfo localPlayer = FindLocalPlayer(); + if (!UserINISettings.Instance.UseP2P && !UserINISettings.Instance.UseDynamicTunnels && tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) + { + iniFile.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); + iniFile.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); + } - if (localPlayer == null) - return; + iniFile.SetIntValue("Settings", "GameID", UniqueGameID); + iniFile.SetBooleanValue("Settings", "Host", IsHost); - iniFile.SetIntValue("Settings", "Port", localPlayer.Port); - } + PlayerInfo localPlayer = FindLocalPlayer(); - protected override Task SendChatMessageAsync(string message) => channel.SendChatMessageAsync(message, chatColor); + if (localPlayer == null) + return; - #region Notifications + iniFile.SetIntValue("Settings", "Port", localPlayer.Port); + } - private void HandleNotification(string sender, Action handler) - { - if (sender != hostName) - return; + protected override Task SendChatMessageAsync(string message) => channel.SendChatMessageAsync(message, chatColor); - handler(); - } + private void HandleNotification(string sender, Action handler) + { + if (sender != hostName) + return; - private void HandleIntNotification(string sender, int parameter, Action handler) - { - if (sender != hostName) - return; + handler(); + } - handler(parameter); - } + private void HandleIntNotification(string sender, int parameter, Action handler) + { + if (sender != hostName) + return; - protected override async Task GetReadyNotificationAsync() - { - await base.GetReadyNotificationAsync(); + handler(parameter); + } + + protected override async Task GetReadyNotificationAsync() + { + await base.GetReadyNotificationAsync(); #if WINFORMS - WindowManager.FlashWindow(); + WindowManager.FlashWindow(); #endif - TopBar.SwitchToPrimary(); + TopBar.SwitchToPrimary(); - if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.GET_READY_LOBBY, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); - } + if (IsHost) + await channel.SendCTCPMessageAsync(CnCNetCommands.GET_READY_LOBBY, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); + } - protected override async Task AISpectatorsNotificationAsync() - { - await base.AISpectatorsNotificationAsync(); + protected override async Task AISpectatorsNotificationAsync() + { + await base.AISpectatorsNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.AI_SPECTATORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } + if (IsHost) + await channel.SendCTCPMessageAsync(CnCNetCommands.AI_SPECTATORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } - protected override async Task InsufficientPlayersNotificationAsync() - { - await base.InsufficientPlayersNotificationAsync(); + protected override async Task InsufficientPlayersNotificationAsync() + { + await base.InsufficientPlayersNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.INSUFFICIENT_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } + if (IsHost) + await channel.SendCTCPMessageAsync(CnCNetCommands.INSUFFICIENT_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } - protected override async Task TooManyPlayersNotificationAsync() - { - await base.TooManyPlayersNotificationAsync(); + protected override async Task TooManyPlayersNotificationAsync() + { + await base.TooManyPlayersNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.TOO_MANY_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } + if (IsHost) + await channel.SendCTCPMessageAsync(CnCNetCommands.TOO_MANY_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } - protected override async Task SharedColorsNotificationAsync() - { - await base.SharedColorsNotificationAsync(); + protected override async Task SharedColorsNotificationAsync() + { + await base.SharedColorsNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_COLORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } + if (IsHost) + await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_COLORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } - protected override async Task SharedStartingLocationNotificationAsync() - { - await base.SharedStartingLocationNotificationAsync(); + protected override async Task SharedStartingLocationNotificationAsync() + { + await base.SharedStartingLocationNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_STARTING_LOCATIONS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } + if (IsHost) + await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_STARTING_LOCATIONS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } - protected override async Task LockGameNotificationAsync() - { - await base.LockGameNotificationAsync(); + protected override async Task LockGameNotificationAsync() + { + await base.LockGameNotificationAsync(); - if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.LOCK_GAME, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } + if (IsHost) + await channel.SendCTCPMessageAsync(CnCNetCommands.LOCK_GAME, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } - protected override async Task NotVerifiedNotificationAsync(int playerIndex) - { - await base.NotVerifiedNotificationAsync(playerIndex); + protected override async Task NotVerifiedNotificationAsync(int playerIndex) + { + await base.NotVerifiedNotificationAsync(playerIndex); - if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.NOT_VERIFIED + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } + if (IsHost) + await channel.SendCTCPMessageAsync(CnCNetCommands.NOT_VERIFIED + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } - protected override async Task StillInGameNotificationAsync(int playerIndex) - { - await base.StillInGameNotificationAsync(playerIndex); + protected override async Task StillInGameNotificationAsync(int playerIndex) + { + await base.StillInGameNotificationAsync(playerIndex); - if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.STILL_IN_GAME + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); - } + if (IsHost) + await channel.SendCTCPMessageAsync(CnCNetCommands.STILL_IN_GAME + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + } - private void ReturnNotification(string sender) - { - AddNotice(string.Format("{0} has returned from the game.".L10N("Client:Main:PlayerReturned"), sender)); + private void ReturnNotification(string sender) + { + AddNotice(string.Format("{0} has returned from the game.".L10N("Client:Main:PlayerReturned"), sender)); - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo != null) - pInfo.IsInGame = false; + if (pInfo != null) + pInfo.IsInGame = false; - sndReturnSound.Play(); - CopyPlayerDataToUI(); - } + sndReturnSound.Play(); + CopyPlayerDataToUI(); + } + + private void HandleTunnelPing(string sender, int ping) + { + PlayerInfo pInfo = Players.Find(p => p.Name.Equals(sender)); - private void HandleTunnelPing(string sender, int ping) + if (pInfo != null) { - PlayerInfo pInfo = Players.Find(p => p.Name.Equals(sender)); - if (pInfo != null) - { - pInfo.Ping = ping; - UpdatePlayerPingIndicator(pInfo); - } + pInfo.Ping = ping; + + UpdatePlayerPingIndicator(pInfo); } + } - private async Task FileHashNotificationAsync(string sender, string filesHash) - { - if (!IsHost) - return; + private async Task FileHashNotificationAsync(string sender, string filesHash) + { + if (!IsHost) + return; - PlayerInfo pInfo = Players.Find(p => p.Name == sender); + PlayerInfo pInfo = Players.Find(p => p.Name == sender); - if (pInfo != null) - pInfo.Verified = true; + if (pInfo != null) + pInfo.Verified = true; - CopyPlayerDataToUI(); + CopyPlayerDataToUI(); - if (filesHash != gameFilesHash) - { - await channel.SendCTCPMessageAsync(CnCNetCommands.CHEATER + " " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10); - CheaterNotification(ProgramConstants.PLAYERNAME, sender); - } + if (filesHash != gameFilesHash) + { + await channel.SendCTCPMessageAsync(CnCNetCommands.CHEATER + " " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10); + CheaterNotification(ProgramConstants.PLAYERNAME, sender); } + } - private void CheaterNotification(string sender, string cheaterName) - { - if (sender != hostName) - return; + private void CheaterNotification(string sender, string cheaterName) + { + if (sender != hostName) + return; - AddNotice(string.Format("Player {0} has different files compared to the game host. Either {0} or the game host could be cheating.".L10N("Client:Main:DifferentFileCheating"), cheaterName), Color.Red); - } + AddNotice(string.Format("Player {0} has different files compared to the game host. Either {0} or the game host could be cheating.".L10N("Client:Main:DifferentFileCheating"), cheaterName), Color.Red); + } - protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) - { - string resultString = string.Join(",", results); - await channel.SendCTCPMessageAsync($"{CnCNetCommands.DICE_ROLL} {dieSides},{resultString}", QueuedMessageType.CHAT_MESSAGE, 0); - PrintDiceRollResult(ProgramConstants.PLAYERNAME, dieSides, results); - } + protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) + { + string resultString = string.Join(",", results); - #endregion + await channel.SendCTCPMessageAsync($"{CnCNetCommands.DICE_ROLL} {dieSides},{resultString}", QueuedMessageType.CHAT_MESSAGE, 0); + PrintDiceRollResult(ProgramConstants.PLAYERNAME, dieSides, results); + } - protected override async Task HandleLockGameButtonClickAsync() + protected override async Task HandleLockGameButtonClickAsync() + { + if (!Locked) { - if (!Locked) + AddNotice("You've locked the game room.".L10N("Client:Main:RoomLockedByYou")); + await LockGameAsync(); + } + else + { + if (Players.Count < playerLimit) { - AddNotice("You've locked the game room.".L10N("Client:Main:RoomLockedByYou")); - await LockGameAsync(); + AddNotice("You've unlocked the game room.".L10N("Client:Main:RoomUnockedByYou")); + await UnlockGameAsync(false); } else { - if (Players.Count < playerLimit) - { - AddNotice("You've unlocked the game room.".L10N("Client:Main:RoomUnockedByYou")); - await UnlockGameAsync(false); - } - else - AddNotice(string.Format( - "Cannot unlock game; the player limit ({0}) has been reached.".L10N("Client:Main:RoomCantUnlockAsLimit"), playerLimit)); + AddNotice(string.Format("Cannot unlock game; the player limit ({0}) has been reached.".L10N("Client:Main:RoomCantUnlockAsLimit"), playerLimit)); } } + } - protected override async Task LockGameAsync() - { - await connectionManager.SendCustomMessageAsync(new( - string.Format(IRCCommands.MODE + " {0} +i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); - - Locked = true; - btnLockGame.Text = "Unlock Game".L10N("Client:Main:UnlockGame"); - AccelerateGameBroadcasting(); - } + protected override async Task LockGameAsync() + { + await connectionManager.SendCustomMessageAsync( + new(string.Format(IRCCommands.MODE + " {0} +i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); - protected override async Task UnlockGameAsync(bool announce) - { - await connectionManager.SendCustomMessageAsync(new( - string.Format(IRCCommands.MODE + " {0} -i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); + Locked = true; + btnLockGame.Text = "Unlock Game".L10N("Client:Main:UnlockGame"); + AccelerateGameBroadcasting(); + } - Locked = false; - if (announce) - AddNotice("The game room has been unlocked.".L10N("Client:Main:GameRoomUnlocked")); - btnLockGame.Text = "Lock Game".L10N("Client:Main:LockGame"); - AccelerateGameBroadcasting(); - } + protected override async Task UnlockGameAsync(bool announce) + { + await connectionManager.SendCustomMessageAsync( + new(string.Format(IRCCommands.MODE + " {0} -i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); - protected override async Task KickPlayerAsync(int playerIndex) - { - if (playerIndex >= Players.Count) - return; + Locked = false; - var pInfo = Players[playerIndex]; + if (announce) + AddNotice("The game room has been unlocked.".L10N("Client:Main:GameRoomUnlocked")); - AddNotice(string.Format("Kicking {0} from the game...".L10N("Client:Main:KickPlayer"), pInfo.Name)); - await channel.SendKickMessageAsync(pInfo.Name, 8); - } + btnLockGame.Text = "Lock Game".L10N("Client:Main:LockGame"); + AccelerateGameBroadcasting(); + } - protected override async Task BanPlayerAsync(int playerIndex) - { - if (playerIndex >= Players.Count) - return; + protected override async Task KickPlayerAsync(int playerIndex) + { + if (playerIndex >= Players.Count) + return; - var pInfo = Players[playerIndex]; + PlayerInfo pInfo = Players[playerIndex]; - var user = connectionManager.UserList.Find(u => u.Name == pInfo.Name); + AddNotice(string.Format("Kicking {0} from the game...".L10N("Client:Main:KickPlayer"), pInfo.Name)); + await channel.SendKickMessageAsync(pInfo.Name, 8); + } - if (user != null) - { - AddNotice(string.Format("Banning and kicking {0} from the game...".L10N("Client:Main:BanAndKickPlayer"), pInfo.Name)); - await channel.SendBanMessageAsync(user.Hostname, 8); - await channel.SendKickMessageAsync(user.Name, 8); - } - } + protected override async Task BanPlayerAsync(int playerIndex) + { + if (playerIndex >= Players.Count) + return; - private void HandleCheatDetectedMessage(string sender) => - AddNotice(string.Format("{0} has modified game files during the client session. They are likely attempting to cheat!".L10N("Client:Main:PlayerModifyFileCheat"), sender), Color.Red); + PlayerInfo pInfo = Players[playerIndex]; + IRCUser user = connectionManager.UserList.Find(u => u.Name == pInfo.Name); - private async Task HandleTunnelServerChangeMessageAsync(string sender, string tunnelAddressAndPort) + if (user != null) { - if (sender != hostName) - return; + AddNotice(string.Format("Banning and kicking {0} from the game...".L10N("Client:Main:BanAndKickPlayer"), pInfo.Name)); + await channel.SendBanMessageAsync(user.Hostname, 8); + await channel.SendKickMessageAsync(user.Name, 8); + } + } - string[] split = tunnelAddressAndPort.Split(':'); - string tunnelAddress = split[0]; - int tunnelPort = int.Parse(split[1]); + private void HandleCheatDetectedMessage(string sender) => + AddNotice(string.Format("{0} has modified game files during the client session. They are likely attempting to cheat!".L10N("Client:Main:PlayerModifyFileCheat"), sender), Color.Red); - CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); - if (tunnel == null) - { - tunnelErrorMode = true; - AddNotice(("The game host has selected an invalid tunnel server! " + - "The game host needs to change the server or you will be unable " + - "to participate in the match.").L10N("Client:Main:HostInvalidTunnel"), - Color.Yellow); - UpdateLaunchGameButtonStatus(); - return; - } + private async Task HandleTunnelServerChangeMessageAsync(string sender, string tunnelAddressAndPort) + { + if (sender != hostName) + return; + string[] split = tunnelAddressAndPort.Split(':'); + string tunnelAddress = split[0]; + int tunnelPort = int.Parse(split[1], CultureInfo.InvariantCulture); + CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); - tunnelErrorMode = false; - await HandleTunnelServerChangeAsync(tunnel); + if (tunnel == null) + { + tunnelErrorMode = true; + AddNotice(("The game host has selected an invalid tunnel server! " + + "The game host needs to change the server or you will be unable " + + "to participate in the match.").L10N("Client:Main:HostInvalidTunnel"), + Color.Yellow); UpdateLaunchGameButtonStatus(); + return; } - /// - /// Changes the tunnel server used for the game. - /// - /// The new tunnel server to use. - private Task HandleTunnelServerChangeAsync(CnCNetTunnel tunnel) - { - tunnelHandler.CurrentTunnel = tunnel; - AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("Client:Main:HostChangeTunnel"), tunnel.Name)); - return UpdatePingAsync(); - } + tunnelErrorMode = false; + await HandleTunnelServerChangeAsync(tunnel); + UpdateLaunchGameButtonStatus(); + } - protected override bool UpdateLaunchGameButtonStatus() + private void HandleTunnelPingsMessage(string sender, string tunnelPingsMessage) + { + (string Name, CnCNetTunnel Tunnel) playerTunnelInfo = playerTunnels.SingleOrDefault(p => p.Name.Equals(sender, StringComparison.OrdinalIgnoreCase)); + + if (playerTunnelInfo.Tunnel is not null) + return; + + tunnelPingsMessages.Add((sender, tunnelPingsMessage)); + + if (!pinnedTunnels.Any()) + return; + + string[] tunnelPingsLines = tunnelPingsMessage.Split('\t', StringSplitOptions.RemoveEmptyEntries); + IEnumerable<(int Ping, string Hash)> tunnelPings = tunnelPingsLines.Select(q => { - btnLaunchGame.Enabled = base.UpdateLaunchGameButtonStatus() && !tunnelErrorMode; - return btnLaunchGame.Enabled; - } + string[] split = q.Split(';'); - #region CnCNet map sharing + return (int.Parse(split[0], CultureInfo.InvariantCulture), split[1]); + }); + IEnumerable<(int CombinedPing, string Hash)> combinedTunnelResults = tunnelPings + .Select(q => (CombinedPing: q.Ping + pinnedTunnels.SingleOrDefault(r => q.Hash.Equals(r.Hash, StringComparison.OrdinalIgnoreCase)).Ping, q.Hash)); + (int _, string hash) = combinedTunnelResults + .OrderBy(q => q.CombinedPing) + .ThenBy(q => q.Hash, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(); - private async Task MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) + if (hash is null) { - // If the host has already uploaded the map, we shouldn't request them to re-upload it - if (hostUploadedMaps.Contains(e.SHA1)) - { - AddNotice("Download of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:DownloadCustomMapFailed")); - mapSharingConfirmationPanel.SetFailedStatus(); + AddNotice(string.Format("No common tunnel server found for: {0}".L10N("Client:Main:NoCommonTunnel"), sender)); + } + else + { + CnCNetTunnel tunnel = tunnelHandler.Tunnels.Single(q => q.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - return; - } + playerTunnels.Add(new(sender, tunnel)); + AddNotice(string.Format("Dynamic tunnel server negotiated with {0}: {1} ({2}ms)".L10N("Client:Main:TunnelNegotiated"), sender, tunnel.Name, tunnel.PingInMs)); + } + } - if (chatCommandDownloadedMaps.Contains(e.SHA1)) - { - // Notify the user that their chat command map download failed. - // Do not notify other users with a CTCP message as this is irrelevant to them. - AddNotice("Downloading map via chat command has failed. Check the map ID and try again.".L10N("Client:Main:DownloadMapCommandFailedGeneric")); - mapSharingConfirmationPanel.SetFailedStatus(); - return; - } + /// + /// Changes the tunnel server used for the game. + /// + /// The new tunnel server to use. + private Task HandleTunnelServerChangeAsync(CnCNetTunnel tunnel) + { + tunnelHandler.CurrentTunnel = tunnel; - AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("Client:Main:RequestHostUploadMapToDB")); + AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("Client:Main:HostChangeTunnel"), tunnel.Name)); + return UpdatePingAsync(); + } - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_UPLOAD + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - } + protected override bool UpdateLaunchGameButtonStatus() + { + btnLaunchGame.Enabled = base.UpdateLaunchGameButtonStatus() && !tunnelErrorMode; + return btnLaunchGame.Enabled; + } - private async Task MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) + private async Task MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) + { + // If the host has already uploaded the map, we shouldn't request them to re-upload it + if (hostUploadedMaps.Contains(e.SHA1)) { - string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName); - Logger.Log("Map " + mapFileName + " downloaded, parsing."); - string mapPath = "Maps/Custom/" + mapFileName; - Map map = MapLoader.LoadCustomMap(mapPath, out string returnMessage); - if (map != null) - { - AddNotice(returnMessage); - if (lastMapSHA1 == e.SHA1) - { - GameModeMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == lastMapSHA1); - await ChangeMapAsync(GameModeMap); - } - } - else if (chatCommandDownloadedMaps.Contains(e.SHA1)) - { - // Somehow the user has managed to download an already existing sha1 hash. - // This special case prevents user confusion from the file successfully downloading but showing an error anyway. - AddNotice(returnMessage, Color.Yellow); - AddNotice("Map was downloaded, but a duplicate is already loaded from a different filename. This may cause strange behavior.".L10N("Client:Main:DownloadMapCommandDuplicateMapFileLoaded"), - Color.Yellow); - } - else - { - AddNotice(returnMessage, Color.Red); - AddNotice("Transfer of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:MapTransferFailed")); - mapSharingConfirmationPanel.SetFailedStatus(); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - } + AddNotice("Download of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:DownloadCustomMapFailed")); + mapSharingConfirmationPanel.SetFailedStatus(); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + return; } - private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) + if (chatCommandDownloadedMaps.Contains(e.SHA1)) { - Map map = e.Map; + // Notify the user that their chat command map download failed. + // Do not notify other users with a CTCP message as this is irrelevant to them. + AddNotice("Downloading map via chat command has failed. Check the map ID and try again.".L10N("Client:Main:DownloadMapCommandFailedGeneric")); + mapSharingConfirmationPanel.SetFailedStatus(); + return; + } - hostUploadedMaps.Add(map.SHA1); + AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("Client:Main:RequestHostUploadMapToDB")); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_UPLOAD + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } - AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("Client:Main:UpdateMapToDBFailed"), map.Name)); - if (map == Map) - { - AddNotice("You need to change the map or some players won't be able to participate in this match.".L10N("Client:Main:YouMustReplaceMap")); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - } - } + private async Task MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) + { + string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName); + Logger.Log("Map " + mapFileName + " downloaded, parsing."); + string mapPath = "Maps/Custom/" + mapFileName; + Map map = MapLoader.LoadCustomMap(mapPath, out string returnMessage); - private async Task MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) + if (map != null) { - hostUploadedMaps.Add(e.Map.SHA1); + AddNotice(returnMessage); - AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); - if (e.Map == Map) + if (lastMapHash == e.SHA1) { - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DOWNLOAD + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + GameModeMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == lastMapHash); + + await ChangeMapAsync(GameModeMap); } } - - /// - /// Handles a map upload request sent by a player. - /// - /// The sender of the request. - /// The SHA1 of the requested map. - private void HandleMapUploadRequest(string sender, string mapSHA1) + else if (chatCommandDownloadedMaps.Contains(e.SHA1)) { - if (hostUploadedMaps.Contains(mapSHA1)) - { - Logger.Log("HandleMapUploadRequest: Map " + mapSHA1 + " is already uploaded!"); - return; - } - - Map map = null; + // Somehow the user has managed to download an already existing sha1 hash. + // This special case prevents user confusion from the file successfully downloading but showing an error anyway. + AddNotice(returnMessage, Color.Yellow); + AddNotice( + "Map was downloaded, but a duplicate is already loaded from a different filename. This may cause strange behavior.".L10N("Client:Main:DownloadMapCommandDuplicateMapFileLoaded"), + Color.Yellow); + } + else + { + AddNotice(returnMessage, Color.Red); + AddNotice("Transfer of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:MapTransferFailed")); + mapSharingConfirmationPanel.SetFailedStatus(); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } + } - foreach (GameMode gm in GameModeMaps.GameModes) - { - map = gm.Maps.Find(m => m.SHA1 == mapSHA1); + private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) + { + Map map = e.Map; - if (map != null) - break; - } + hostUploadedMaps.Add(map.SHA1); + AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("Client:Main:UpdateMapToDBFailed"), map.Name)); - if (map == null) - { - Logger.Log("Unknown map upload request from " + sender + ": " + mapSHA1); - return; - } + if (map == Map) + { + AddNotice("You need to change the map or some players won't be able to participate in this match.".L10N("Client:Main:YouMustReplaceMap")); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } + } - if (map.Official) - { - Logger.Log("HandleMapUploadRequest: Map is official, so skip request"); + private async Task MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) + { + hostUploadedMaps.Add(e.Map.SHA1); + AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); - AddNotice(string.Format(("{0} doesn't have the map '{1}' on their local installation. " + - "The map needs to be changed or {0} is unable to participate in the match.").L10N("Client:Main:PlayerMissingMap"), - sender, map.Name)); + if (e.Map == Map) + { + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DOWNLOAD + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } + } - return; - } + /// + /// Handles a map upload request sent by a player. + /// + /// The sender of the request. + /// The SHA1 of the requested map. + private void HandleMapUploadRequest(string sender, string mapHash) + { + if (hostUploadedMaps.Contains(mapHash)) + { + Logger.Log("HandleMapUploadRequest: Map " + mapHash + " is already uploaded!"); + return; + } - if (!IsHost) - return; + Map map = null; - AddNotice(string.Format(("{0} doesn't have the map '{1}' on their local installation. " + - "Attempting to upload the map to the CnCNet map database.").L10N("Client:Main:UpdateMapToDBPrompt"), - sender, map.Name)); + foreach (GameMode gm in GameModeMaps.GameModes) + { + map = gm.Maps.Find(m => m.SHA1 == mapHash); - MapSharer.UploadMap(map, localGame); + if (map != null) + break; } - /// - /// Handles a map transfer failure message sent by either the player or the game host. - /// - private void HandleMapTransferFailMessage(string sender, string sha1) + if (map == null) { - if (sender == hostName) - { - AddNotice("The game host failed to upload the map to the CnCNet map database.".L10N("Client:Main:HostUpdateMapToDBFailed")); + Logger.Log("Unknown map upload request from " + sender + ": " + mapHash); + return; + } - hostUploadedMaps.Add(sha1); + if (map.Official) + { + Logger.Log("HandleMapUploadRequest: Map is official, so skip request"); + AddNotice( + string.Format(("{0} doesn't have the map '{1}' on their local installation. " + + "The map needs to be changed or {0} is unable to participate in the match.").L10N("Client:Main:PlayerMissingMap"), + sender, + map.Name)); - if (lastMapSHA1 == sha1 && Map == null) - { - AddNotice("The game host needs to change the map or you won't be able to participate in this match.".L10N("Client:Main:HostMustChangeMap")); - } + return; + } - return; - } + if (!IsHost) + return; - if (lastMapSHA1 == sha1) - { - if (!IsHost) - { - AddNotice(string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("Client:Main:PlayerDownloadMapFailed") + " " + - "The host needs to change the map or {0} won't be able to participate in this match.".L10N("Client:Main:HostNeedChangeMapForPlayer"), sender)); - } - else - { - AddNotice(string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("Client:Main:PlayerDownloadMapFailed") + " " + - "You need to change the map or {0} won't be able to participate in this match.".L10N("Client:Main:YouNeedChangeMapForPlayer"), sender)); - } - } - } + AddNotice( + string.Format(("{0} doesn't have the map '{1}' on their local installation. " + + "Attempting to upload the map to the CnCNet map database.").L10N("Client:Main:UpdateMapToDBPrompt"), + sender, + map.Name)); + MapSharer.UploadMap(map, localGame); + } - private void HandleMapDownloadRequest(string sender, string sha1) + /// + /// Handles a map transfer failure message sent by either the player or the game host. + /// + private void HandleMapTransferFailMessage(string sender, string sha1) + { + if (sender == hostName) { - if (sender != hostName) - return; - + AddNotice("The game host failed to upload the map to the CnCNet map database.".L10N("Client:Main:HostUpdateMapToDBFailed")); hostUploadedMaps.Add(sha1); - if (lastMapSHA1 == sha1 && Map == null) - { - Logger.Log("The game host has uploaded the map into the database. Re-attempting download..."); - MapSharer.DownloadMap(sha1, localGame, lastMapName); - } + if (lastMapHash == sha1 && Map == null) + AddNotice("The game host needs to change the map or you won't be able to participate in this match.".L10N("Client:Main:HostMustChangeMap")); + + return; } - private void HandleMapSharingBlockedMessage(string sender) - { - AddNotice(string.Format(("The selected map doesn't exist on {0}'s installation, and they " + - "have map sharing disabled in settings. The game host needs to change to a non-custom map or " + - "they will be unable to participate in this match.").L10N("Client:Main:PlayerMissingMapDisabledSharing"), sender)); - } - - /// - /// Download a map from CNCNet using a map hash ID. - /// - /// Users and testers can get map hash IDs from this URL template: - /// - /// - http://mapdb.cncnet.org/search.php?game=GAME_ID&search=MAP_NAME_SEARCH_STRING - /// - /// - /// - /// This is a string beginning with the sha1 hash map ID, and (optionally) the name to use as a local filename for the map file. - /// Every character after the first space will be treated as part of the map name. - /// - /// "?" characters are removed from the sha1 due to weird copy and paste behavior from the map search endpoint. - /// - private void DownloadMapByIdCommand(string parameters) - { - string sha1; - string mapName; - string message; - - // Make sure no spaces at the beginning or end of the string will mess up arg parsing. - parameters = parameters.Trim(); - // Check if the parameter's contain spaces. - // The presence of spaces indicates a user-specified map name. - int firstSpaceIndex = parameters.IndexOf(' '); - - if (firstSpaceIndex == -1) + if (lastMapHash == sha1) + { + if (!IsHost) { - // The user did not supply a map name. - sha1 = parameters; - mapName = "user_chat_command_download"; + AddNotice( + string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("Client:Main:PlayerDownloadMapFailed") + " " + + "The host needs to change the map or {0} won't be able to participate in this match.".L10N("Client:Main:HostNeedChangeMapForPlayer"), + sender)); } else { - // User supplied a map name. - sha1 = parameters[..firstSpaceIndex]; - mapName = parameters[(firstSpaceIndex + 1)..]; - mapName = mapName.Trim(); + AddNotice( + string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("Client:Main:PlayerDownloadMapFailed") + " " + + "You need to change the map or {0} won't be able to participate in this match.".L10N("Client:Main:YouNeedChangeMapForPlayer"), + sender)); } + } + } - // Remove erroneous "?". These sneak in when someone double-clicks a map ID and copies it from the cncnet search endpoint. - // There is some weird whitespace that gets copied to chat as a "?" at the end of the hash. It's hard to spot, so just hold the user's hand. - sha1 = sha1.Replace("?", ""); + private void HandleMapDownloadRequest(string sender, string sha1) + { + if (sender != hostName) + return; - // See if the user already has this map, with any filename, before attempting to download it. - GameModeMap loadedMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == sha1); + hostUploadedMaps.Add(sha1); - if (loadedMap != null) - { - message = String.Format( - "The map for ID \"{0}\" is already loaded from \"{1}.map\", delete the existing file before trying again.".L10N("Client:Main:DownloadMapCommandSha1AlreadyExists"), - sha1, - loadedMap.Map.BaseFilePath); - AddNotice(message, Color.Yellow); - Logger.Log(message); - return; - } + if (lastMapHash == sha1 && Map == null) + { + Logger.Log("The game host has uploaded the map into the database. Re-attempting download..."); + MapSharer.DownloadMap(sha1, localGame, lastMapName); + } + } - // Replace any characters that are not safe for filenames. - char replaceUnsafeCharactersWith = '-'; - // Use a hashset instead of an array for quick lookups in `invalidChars.Contains()`. - HashSet invalidChars = new HashSet(Path.GetInvalidFileNameChars()); - string safeMapName = new(mapName.Select(c => invalidChars.Contains(c) ? replaceUnsafeCharactersWith : c).ToArray()); + private void HandleMapSharingBlockedMessage(string sender) + { + AddNotice( + string.Format("The selected map doesn't exist on {0}'s installation, and they " + + "have map sharing disabled in settings. The game host needs to change to a non-custom map or " + + "they will be unable to participate in this match.".L10N("Client:Main:PlayerMissingMapDisabledSharing"), + sender)); + } - chatCommandDownloadedMaps.Add(sha1); + /// + /// Download a map from CNCNet using a map hash ID. + /// + /// Users and testers can get map hash IDs from this URL template: + /// + /// - https://mapdb.cncnet.org/search.php?game=GAME_ID&search=MAP_NAME_SEARCH_STRING. + /// + /// + /// + /// This is a string beginning with the sha1 hash map ID, and (optionally) the name to use as a local filename for the map file. + /// Every character after the first space will be treated as part of the map name. + /// + /// "?" characters are removed from the sha1 due to weird copy and paste behavior from the map search endpoint. + /// + private void DownloadMapByIdCommand(string parameters) + { + string sha1; + string mapName; + string message; - message = String.Format("Attempting to download map via chat command: sha1={0}, mapName={1}".L10N("Client:Main:DownloadMapCommandStartingDownload"), sha1, mapName); - Logger.Log(message); - AddNotice(message); + // Make sure no spaces at the beginning or end of the string will mess up arg parsing. + parameters = parameters.Trim(); - MapSharer.DownloadMap(sha1, localGame, safeMapName); - } + // Check if the parameter's contain spaces. + // The presence of spaces indicates a user-specified map name. + int firstSpaceIndex = parameters.IndexOf(' '); - #endregion + if (firstSpaceIndex == -1) + { + // The user did not supply a map name. + sha1 = parameters; + mapName = "user_chat_command_download"; + } + else + { + // User supplied a map name. + sha1 = parameters[..firstSpaceIndex]; + mapName = parameters[(firstSpaceIndex + 1)..]; + mapName = mapName.Trim(); + } - #region Game broadcasting logic + // Remove erroneous "?". These sneak in when someone double-clicks a map ID and copies it from the cncnet search endpoint. + // There is some weird whitespace that gets copied to chat as a "?" at the end of the hash. It's hard to spot, so just hold the user's hand. + sha1 = sha1.Replace("?", string.Empty); - /// - /// Lowers the time until the next game broadcasting message. - /// - private void AccelerateGameBroadcasting() => - gameBroadcastTimer.Accelerate(TimeSpan.FromSeconds(GAME_BROADCAST_ACCELERATION)); + // See if the user already has this map, with any filename, before attempting to download it. + GameModeMap loadedMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == sha1); - private async Task BroadcastGameAsync() + if (loadedMap != null) { - Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); + message = string.Format( + "The map for ID \"{0}\" is already loaded from \"{1}.map\", delete the existing file before trying again.".L10N("Client:Main:DownloadMapCommandSha1AlreadyExists"), + sha1, + loadedMap.Map.BaseFilePath); + AddNotice(message, Color.Yellow); + Logger.Log(message); + return; + } - if (broadcastChannel == null) - return; + // Replace any characters that are not safe for filenames. + char replaceUnsafeCharactersWith = '-'; - if (ProgramConstants.IsInGame && broadcastChannel.Users.Count > 500) - return; + // Use a hashset instead of an array for quick lookups in `invalidChars.Contains()`. + var invalidChars = new HashSet(Path.GetInvalidFileNameChars()); + string safeMapName = new(mapName.Select(c => invalidChars.Contains(c) ? replaceUnsafeCharactersWith : c).ToArray()); - if (GameMode == null || Map == null) - return; + chatCommandDownloadedMaps.Add(sha1); - StringBuilder sb = new StringBuilder(CnCNetCommands.GAME + " "); - sb.Append(ProgramConstants.CNCNET_PROTOCOL_REVISION); - sb.Append(";"); - sb.Append(ProgramConstants.GAME_VERSION); - sb.Append(";"); - sb.Append(playerLimit); - sb.Append(";"); - sb.Append(channel.ChannelName); - sb.Append(";"); - sb.Append(channel.UIName); - sb.Append(";"); - if (Locked) - sb.Append("1"); - else - sb.Append("0"); - sb.Append(Convert.ToInt32(isCustomPassword)); - sb.Append(Convert.ToInt32(closed)); - sb.Append("0"); // IsLoadedGame - sb.Append("0"); // IsLadder - sb.Append(";"); - foreach (PlayerInfo pInfo in Players) - { - sb.Append(pInfo.Name); - sb.Append(","); - } + message = string.Format("Attempting to download map via chat command: sha1={0}, mapName={1}".L10N("Client:Main:DownloadMapCommandStartingDownload"), sha1, mapName); - sb.Remove(sb.Length - 1, 1); - sb.Append(";"); - sb.Append(Map.UntranslatedName); - sb.Append(";"); - sb.Append(GameMode.UntranslatedUIName); - sb.Append(";"); - sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); - sb.Append(";"); - sb.Append(0); // LoadedGameId + Logger.Log(message); + AddNotice(message); - await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); - } + MapSharer.DownloadMap(sha1, localGame, safeMapName); + } - #endregion + /// + /// Lowers the time until the next game broadcasting message. + /// + private void AccelerateGameBroadcasting() => + gameBroadcastTimer.Accelerate(TimeSpan.FromSeconds(GAME_BROADCAST_ACCELERATION)); - public override string GetSwitchName() => "Game Lobby".L10N("Client:Main:GameLobby"); + private async Task BroadcastGameAsync() + { + Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); + + if (broadcastChannel == null) + return; + + if (ProgramConstants.IsInGame && broadcastChannel.Users.Count > 500) + return; + + if (GameMode == null || Map == null) + return; + + StringBuilder sb = new StringBuilder(CnCNetCommands.GAME + " ") + .Append(ProgramConstants.CNCNET_PROTOCOL_REVISION) + .Append(';') + .Append(ProgramConstants.GAME_VERSION) + .Append(';') + .Append(playerLimit) + .Append(';') + .Append(channel.ChannelName) + .Append(';') + .Append(channel.UIName) + .Append(';') + .Append(Locked ? '1' : '0') + .Append(Convert.ToInt32(isCustomPassword)) + .Append(Convert.ToInt32(closed)) + .Append('0') // IsLoadedGame + .Append('0') // IsLadder + .Append(';'); + + foreach (PlayerInfo pInfo in Players) + { + sb.Append(pInfo.Name); + sb.Append(','); + } + + sb.Remove(sb.Length - 1, 1) + .Append(';') + .Append(Map.UntranslatedName) + .Append(';') + .Append(GameMode.UntranslatedUIName) + .Append(';') + .Append(tunnelHandler.CurrentTunnel?.Hash ?? ProgramConstants.CNCNET_DYNAMIC_TUNNELS) + .Append(';') + .Append(0); // LoadedGameId + await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); } + + public override string GetSwitchName() => "Game Lobby".L10N("Client:Main:GameLobby"); } \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 45eb9ca72..c9d8bf771 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -27,7 +27,7 @@ namespace DTAClient.DXGUI.Multiplayer.GameLobby /// A generic base for all game lobbies (Skirmish, LAN and CnCNet). /// Contains the common logic for parsing game options and handling player info. /// - public abstract class GameLobbyBase : INItializableWindow + internal abstract class GameLobbyBase : INItializableWindow { protected const int MAX_PLAYER_COUNT = 8; protected const int PLAYER_OPTION_VERTICAL_MARGIN = 12; @@ -58,8 +58,8 @@ public GameLobbyBase( string iniName, MapLoader mapLoader, bool isMultiplayer, - DiscordHandler discordHandler - ) : base(windowManager) + DiscordHandler discordHandler) + : base(windowManager) { _iniSectionName = iniName; MapLoader = mapLoader; @@ -140,7 +140,7 @@ protected GameModeMap GameModeMap protected List Players = new List(); protected List AIPlayers = new List(); - protected virtual PlayerInfo FindLocalPlayer() => Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); + protected PlayerInfo FindLocalPlayer() => Players.Find(p => ProgramConstants.PLAYERNAME.Equals(p.Name, StringComparison.OrdinalIgnoreCase)); protected bool PlayerUpdatingInProgress { get; set; } @@ -945,7 +945,9 @@ private void EnablePlayerOptionDropDown(XNAClientDropDown clientDropDown, int pl { var pInfo = GetPlayerInfoForIndex(playerIndex); var allowOtherPlayerOptionsChange = AllowPlayerOptionsChange() && pInfo != null; + clientDropDown.AllowDropDown = enable && (allowOtherPlayerOptionsChange || pInfo?.Name == ProgramConstants.PLAYERNAME); + if (!clientDropDown.AllowDropDown) clientDropDown.SelectedIndex = clientDropDown.SelectedIndex > 0 ? 0 : clientDropDown.SelectedIndex; } @@ -1304,15 +1306,12 @@ private PlayerHouseInfo[] WriteSpawnIni() } var teamStartMappings = new List(0); + if (PlayerExtraOptionsPanel != null) - { teamStartMappings = PlayerExtraOptionsPanel.GetTeamStartMappings(); - } PlayerHouseInfo[] houseInfos = Randomize(teamStartMappings); - IniFile spawnIni = new IniFile(spawnerSettingsFile.FullName); - IniSection settings = new IniSection("Settings"); settings.SetStringValue("Name", ProgramConstants.PLAYERNAME); @@ -1325,17 +1324,22 @@ private PlayerHouseInfo[] WriteSpawnIni() settings.SetStringValue("MapID", Map.BaseFilePath); settings.SetIntValue("PlayerCount", Players.Count); - int myIndex = Players.FindIndex(c => c.Name == ProgramConstants.PLAYERNAME); + + int myIndex = Players.FindIndex(c => c == FindLocalPlayer()); + settings.SetIntValue("Side", houseInfos[myIndex].InternalSideIndex); settings.SetBooleanValue("IsSpectator", houseInfos[myIndex].IsSpectator); settings.SetIntValue("Color", houseInfos[myIndex].ColorIndex); settings.SetStringValue("CustomLoadScreen", LoadingScreenController.GetLoadScreenName(houseInfos[myIndex].InternalSideIndex.ToString())); settings.SetIntValue("AIPlayers", AIPlayers.Count); settings.SetIntValue("Seed", RandomSeed); + if (GetPvPTeamCount() > 1) settings.SetBooleanValue("CoachMode", true); + if (GetGameType() == GameType.Coop) settings.SetBooleanValue("AutoSurrender", false); + spawnIni.AddSection(settings); WriteSpawnIniAdditions(spawnIni); @@ -1346,24 +1350,20 @@ private PlayerHouseInfo[] WriteSpawnIni() dd.ApplySpawnIniCode(spawnIni); // Apply forced options from GameOptions.ini - List forcedKeys = GameOptionsIni.GetSectionKeys("ForcedSpawnIniOptions"); if (forcedKeys != null) { foreach (string key in forcedKeys) { - spawnIni.SetStringValue("Settings", key, - GameOptionsIni.GetStringValue("ForcedSpawnIniOptions", key, String.Empty)); + spawnIni.SetStringValue("Settings", key, GameOptionsIni.GetStringValue("ForcedSpawnIniOptions", key, String.Empty)); } } GameMode.ApplySpawnIniCode(spawnIni); // Forced options from the game mode - Map.ApplySpawnIniCode(spawnIni, Players.Count + AIPlayers.Count, - AIPlayers.Count, GameMode.CoopDifficultyLevel); // Forced options from the map + Map.ApplySpawnIniCode(spawnIni, Players.Count + AIPlayers.Count, AIPlayers.Count, GameMode.CoopDifficultyLevel); // Forced options from the map // Player options - int otherId = 1; for (int pId = 0; pId < Players.Count; pId++) @@ -1371,7 +1371,7 @@ private PlayerHouseInfo[] WriteSpawnIni() PlayerInfo pInfo = Players[pId]; PlayerHouseInfo pHouseInfo = houseInfos[pId]; - if (pInfo.Name == ProgramConstants.PLAYERNAME) + if (pInfo == FindLocalPlayer()) continue; string sectionName = "Other" + otherId; @@ -1404,7 +1404,6 @@ private PlayerHouseInfo[] WriteSpawnIni() for (int aiId = 0; aiId < AIPlayers.Count; aiId++) { int multiId = multiCmbIndexes.Count + aiId + 1; - string keyName = "Multi" + multiId; spawnIni.SetIntValue("HouseHandicaps", keyName, AIPlayers[aiId].HouseHandicapAILevel); @@ -1416,6 +1415,7 @@ private PlayerHouseInfo[] WriteSpawnIni() for (int multiId = 0; multiId < multiCmbIndexes.Count; multiId++) { int pIndex = multiCmbIndexes[multiId]; + if (houseInfos[pIndex].IsSpectator) spawnIni.SetBooleanValue("IsSpectator", "Multi" + (multiId + 1), true); } @@ -1432,8 +1432,8 @@ private PlayerHouseInfo[] WriteSpawnIni() if (startingWaypoint > -1) { int multiIndex = pId + 1; - spawnIni.SetIntValue("SpawnLocations", "Multi" + multiIndex, - startingWaypoint); + + spawnIni.SetIntValue("SpawnLocations", "Multi" + multiIndex, startingWaypoint); } } @@ -1444,8 +1444,8 @@ private PlayerHouseInfo[] WriteSpawnIni() if (startingWaypoint > -1) { int multiIndex = Players.Count + aiId + 1; - spawnIni.SetIntValue("SpawnLocations", "Multi" + multiIndex, - startingWaypoint); + + spawnIni.SetIntValue("SpawnLocations", "Multi" + multiIndex, startingWaypoint); } } @@ -1527,7 +1527,7 @@ private void InitializeMatchStatistics(PlayerHouseInfo[] houseInfos) for (int pId = 0; pId < Players.Count; pId++) { PlayerInfo pInfo = Players[pId]; - matchStatistics.AddPlayer(pInfo.Name, pInfo.Name == ProgramConstants.PLAYERNAME, + matchStatistics.AddPlayer(pInfo.Name, pInfo == FindLocalPlayer(), false, pInfo.SideId == SideCount + RandomSelectorCount, houseInfos[pId].SideIndex + 1, pInfo.TeamId, MPColors.FindIndex(c => c.GameColorIndex == houseInfos[pId].ColorIndex), 10); } @@ -1820,7 +1820,7 @@ protected virtual async Task CopyPlayerDataFromUIAsync(object sender) if ((bool)senderDropDown.Tag) ClearReadyStatuses(); - var oldSideId = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId; + var oldSideId = FindLocalPlayer()?.SideId; for (int pId = 0; pId < Players.Count; pId++) { @@ -1878,7 +1878,7 @@ protected virtual async Task CopyPlayerDataFromUIAsync(object sender) CopyPlayerDataToUI(); btnLaunchGame.SetRank(GetRank()); - if (oldSideId != Players.Find(p => p.Name == ProgramConstants.PLAYERNAME)?.SideId) + if (oldSideId != FindLocalPlayer()?.SideId) UpdateDiscordPresence(); } @@ -1942,7 +1942,7 @@ protected virtual void CopyPlayerDataToUI() ddPlayerName.SelectedIndex = 0; ddPlayerName.AllowDropDown = false; - bool allowPlayerOptionsChange = allowOptionsChange || pInfo.Name == ProgramConstants.PLAYERNAME; + bool allowPlayerOptionsChange = allowOptionsChange || pInfo == FindLocalPlayer(); ddPlayerSides[pId].SelectedIndex = pInfo.SideId; ddPlayerSides[pId].AllowDropDown = !playerExtraOptions.IsForceRandomSides && allowPlayerOptionsChange; @@ -2261,7 +2261,7 @@ protected int GetRank() } } - PlayerInfo localPlayer = Players.Find(p => p.Name == ProgramConstants.PLAYERNAME); + PlayerInfo localPlayer = FindLocalPlayer(); if (localPlayer == null) return RANK_NONE; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs index fd1f86ab5..a8854377e 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs @@ -32,7 +32,7 @@ public ExtraMapPreviewTexture(Texture2D texture, Point point, bool toggleable) /// /// The picture box for displaying the map preview. /// - public class MapPreviewBox : XNAPanel + internal sealed class MapPreviewBox : XNAPanel { private const int MAX_STARTING_LOCATIONS = 8; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 25898e40a..f15c1db7a 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using ClientCore.Extensions; using DTAClient.Domain; +using DTAClient.Domain.Multiplayer.CnCNet; using Microsoft.Xna.Framework.Graphics; using ClientCore.Extensions; @@ -40,22 +41,22 @@ public MultiplayerGameLobby( chatBoxCommands = new List { - new("HIDEMAPS", "Hide map list (game host only)".L10N("Client:Main:ChatboxCommandHideMapsHelp"), true, + new(CnCNetLobbyCommands.HIDEMAPS, "Hide map list (game host only)".L10N("Client:Main:ChatboxCommandHideMapsHelp"), true, s => HideMapList()), - new("SHOWMAPS", "Show map list (game host only)".L10N("Client:Main:ChatboxCommandShowMapsHelp"), true, + new(CnCNetLobbyCommands.SHOWMAPS, "Show map list (game host only)".L10N("Client:Main:ChatboxCommandShowMapsHelp"), true, s => ShowMapList()), - new("FRAMESENDRATE", "Change order lag / FrameSendRate (default 7) (game host only)".L10N("Client:Main:ChatboxCommandFrameSendRateHelp"), true, + new(CnCNetLobbyCommands.FRAMESENDRATE, "Change order lag / FrameSendRate (default 7) (game host only)".L10N("Client:Main:ChatboxCommandFrameSendRateHelp"), true, s => SetFrameSendRateAsync(s).HandleTask()), - new("MAXAHEAD", "Change MaxAhead (default 0) (game host only)".L10N("Client:Main:ChatboxCommandMaxAheadHelp"), true, + new(CnCNetLobbyCommands.MAXAHEAD, "Change MaxAhead (default 0) (game host only)".L10N("Client:Main:ChatboxCommandMaxAheadHelp"), true, s => SetMaxAheadAsync(s).HandleTask()), - new("PROTOCOLVERSION", "Change ProtocolVersion (default 2) (game host only)".L10N("Client:Main:ChatboxCommandProtocolVersionHelp"), true, + new(CnCNetLobbyCommands.PROTOCOLVERSION, "Change ProtocolVersion (default 2) (game host only)".L10N("Client:Main:ChatboxCommandProtocolVersionHelp"), true, s => SetProtocolVersionAsync(s).HandleTask()), - new("LOADMAP", "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("Client:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), - new("RANDOMSTARTS", "Enables completely random starting locations (Tiberian Sun based games only).".L10N("Client:Main:ChatboxCommandRandomStartsHelp"), true, + new(CnCNetLobbyCommands.LOADMAP, "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("Client:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), + new(CnCNetLobbyCommands.RANDOMSTARTS, "Enables completely random starting locations (Tiberian Sun based games only).".L10N("Client:Main:ChatboxCommandRandomStartsHelp"), true, s => SetStartingLocationClearanceAsync(s).HandleTask()), - new("ROLL", "Roll dice, for example /roll 3d6".L10N("Client:Main:ChatboxCommandRollHelp"), false, dieType => RollDiceCommandAsync(dieType).HandleTask()), - new("SAVEOPTIONS", "Save game option preset so it can be loaded later".L10N("Client:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), - new("LOADOPTIONS", "Load game option preset".L10N("Client:Main:ChatboxCommandLoadOptionsHelp"), true, presetName => HandleGameOptionPresetLoadCommandAsync(presetName).HandleTask()) + new(CnCNetLobbyCommands.ROLL, "Roll dice, for example /roll 3d6".L10N("Client:Main:ChatboxCommandRollHelp"), false, dieType => RollDiceCommandAsync(dieType).HandleTask()), + new(CnCNetLobbyCommands.SAVEOPTIONS, "Save game option preset so it can be loaded later".L10N("Client:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), + new(CnCNetLobbyCommands.LOADOPTIONS, "Load game option preset".L10N("Client:Main:ChatboxCommandLoadOptionsHelp"), true, presetName => HandleGameOptionPresetLoadCommandAsync(presetName).HandleTask()) }; chkAutoReady_CheckedChangedFunc = (_, _) => ChkAutoReady_CheckedChangedAsync().HandleTask(); @@ -152,8 +153,7 @@ public override void Initialize() { var indicatorPlayerReady = new XNAPlayerSlotIndicator(WindowManager); indicatorPlayerReady.Name = "playerStatusIndicator" + i; - indicatorPlayerReady.ClientRectangle = new Rectangle(statusIndicatorX, ddPlayerTeams[i].Y + statusIndicatorY, - 0, 0); + indicatorPlayerReady.ClientRectangle = new Rectangle(statusIndicatorX, ddPlayerTeams[i].Y + statusIndicatorY, 0, 0); PlayerOptionsPanel.AddChild(indicatorPlayerReady); @@ -615,7 +615,6 @@ protected void Refresh(bool isHost) UpdateMapPreviewBoxEnabledStatus(); PlayerExtraOptionsPanel?.SetIsHost(isHost); - //MapPreviewBox.EnableContextMenu = IsHost; btnLaunchGame.Text = IsHost ? BTN_LAUNCH_GAME : BTN_LAUNCH_READY; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/PlayerLocationIndicator.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/PlayerLocationIndicator.cs index 1640dd77a..8f6e1d323 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/PlayerLocationIndicator.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/PlayerLocationIndicator.cs @@ -14,7 +14,7 @@ namespace DTAClient.DXGUI.Multiplayer.GameLobby /// /// A player location indicator for the map preview. /// - public class PlayerLocationIndicator : XNAControl + internal sealed class PlayerLocationIndicator : XNAControl { const float TEXTURE_SCALE = 0.25f; diff --git a/DXMainClient/Domain/Multiplayer/AllianceHolder.cs b/DXMainClient/Domain/Multiplayer/AllianceHolder.cs index ba811338f..6a97ff5c7 100644 --- a/DXMainClient/Domain/Multiplayer/AllianceHolder.cs +++ b/DXMainClient/Domain/Multiplayer/AllianceHolder.cs @@ -6,7 +6,7 @@ namespace DTAClient.Domain.Multiplayer /// /// A helper class for setting up alliances in spawn.ini. /// - public static class AllianceHolder + internal static class AllianceHolder { public static void WriteInfoToSpawnIni( List players, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetCommands.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetCommands.cs index 4a0d6650c..76963670e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetCommands.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetCommands.cs @@ -42,4 +42,8 @@ internal static class CnCNetCommands public const string STILL_IN_GAME = "INGM"; public const string CHEATER = "MM"; public const string GAME = "GAME"; + public const string PLAYER_TUNNEL_PINGS = "TNLPINGS"; + public const string PLAYER_P2P_REQUEST = "P2PREQ"; + public const string PLAYER_P2P_PINGS = "P2PPINGS"; + public const string UPDATE = "UPDATE"; } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs new file mode 100644 index 000000000..6bc3c1c04 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs @@ -0,0 +1,19 @@ +#pragma warning disable SA1310 +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal static class CnCNetLobbyCommands +{ + public const string TUNNELINFO = "TUNNELINFO"; + public const string CHANGETUNNEL = "CHANGETUNNEL"; + public const string DOWNLOADMAP = "DOWNLOADMAP"; + public const string HIDEMAPS = "HIDEMAPS"; + public const string SHOWMAPS = "SHOWMAPS"; + public const string FRAMESENDRATE = "FRAMESENDRATE"; + public const string MAXAHEAD = "MAXAHEAD"; + public const string PROTOCOLVERSION = "PROTOCOLVERSION"; + public const string LOADMAP = "LOADMAP"; + public const string RANDOMSTARTS = "RANDOMSTARTS"; + public const string ROLL = "ROLL"; + public const string SAVEOPTIONS = "SAVEOPTIONS"; + public const string LOADOPTIONS = "LOADOPTIONS"; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 7453a5a33..f84505a01 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -1,5 +1,4 @@ -using Rampastring.Tools; -using System; +using System; using System.Buffers; using System.Collections.Generic; using System.Globalization; @@ -8,6 +7,7 @@ using System.Net.Sockets; using System.Threading.Tasks; using ClientCore; +using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -19,9 +19,13 @@ internal sealed class CnCNetTunnel private const int PING_PACKET_SEND_SIZE = 50; private const int PING_PACKET_RECEIVE_SIZE = 12; private const int PING_TIMEOUT = 1000; + private const int HASH_LENGTH = 10; + + private string ipAddress; + private string hash; /// - /// Parses a formatted string that contains the tunnel server's + /// Parses a formatted string that contains the tunnel server's /// information into a CnCNetTunnel instance. /// /// The string that contains the tunnel server's information. @@ -41,26 +45,39 @@ public static CnCNetTunnel Parse(string str) IPAddress secondaryIpAddress = string.IsNullOrWhiteSpace(secondaryAddress) ? null : IPAddress.Parse(secondaryAddress); if (Socket.OSSupportsIPv6 && primaryIpAddress.AddressFamily is AddressFamily.InterNetworkV6) + { tunnel.Address = primaryIpAddress.ToString(); + } else if (Socket.OSSupportsIPv6 && secondaryIpAddress?.AddressFamily is AddressFamily.InterNetworkV6) + { tunnel.Address = secondaryIpAddress.ToString(); + } else if (Socket.OSSupportsIPv4 && primaryIpAddress.AddressFamily is AddressFamily.InterNetwork) + { tunnel.Address = primaryIpAddress.ToString(); + } else if (Socket.OSSupportsIPv4 && secondaryIpAddress?.AddressFamily is AddressFamily.InterNetwork) + { tunnel.Address = secondaryIpAddress.ToString(); + } else + { throw new($"No supported IP address found ({nameof(Socket.OSSupportsIPv6)}={Socket.OSSupportsIPv6}," + $" {nameof(Socket.OSSupportsIPv4)}={Socket.OSSupportsIPv4}) for {str}."); + } tunnel.Port = int.Parse(addressAndPort[(addressAndPort.LastIndexOf(':') + 1)..], CultureInfo.InvariantCulture); tunnel.Country = parts[1]; tunnel.CountryCode = parts[2]; - tunnel.Name = parts[3] + " V" + version; + tunnel.Name = parts[3]; tunnel.RequiresPassword = parts[4] != "0"; tunnel.Clients = int.Parse(parts[5], CultureInfo.InvariantCulture); tunnel.MaxClients = int.Parse(parts[6], CultureInfo.InvariantCulture); + int status = int.Parse(parts[7], CultureInfo.InvariantCulture); + tunnel.Official = status == 2; + if (!tunnel.Official) tunnel.Recommended = status == 1; @@ -78,14 +95,14 @@ public static CnCNetTunnel Parse(string str) } } - private string _ipAddress; public string Address { - get => _ipAddress; + get => ipAddress; private set { - _ipAddress = value; - if (IPAddress.TryParse(_ipAddress, out IPAddress address)) + ipAddress = value; + + if (IPAddress.TryParse(ipAddress, out IPAddress address)) IPAddress = address; } } @@ -93,20 +110,41 @@ private set public IPAddress IPAddress { get; private set; } public int Port { get; private set; } + public string Country { get; private set; } + public string CountryCode { get; private set; } + public string Name { get; private set; } + public bool RequiresPassword { get; private set; } + public int Clients { get; private set; } + public int MaxClients { get; private set; } + public bool Official { get; private set; } + public bool Recommended { get; private set; } + public double Latitude { get; private set; } + public double Longitude { get; private set; } + public int Version { get; private set; } + public double Distance { get; private set; } + public int PingInMs { get; private set; } = -1; + public string Hash + { + get + { + return hash ??= Utilities.CalculateSHA1ForString(FormattableString.Invariant($"{Version}{CountryCode}{Name}{Official}{Recommended}"))[..HASH_LENGTH]; + } + } + /// /// Gets a list of player ports to use from a specific tunnel server. /// @@ -118,10 +156,9 @@ public async Task> GetPlayerPortInfoAsync(int playerCount) try { - Logger.Log($"Contacting tunnel at {Address}:{Port}"); - string addressString = $"{Uri.UriSchemeHttp}://{Address}:{Port}/request?clients={playerCount}"; - Logger.Log($"Downloading from {addressString}"); + + Logger.Log($"Contacting tunnel at {addressString}"); using var client = new HttpClient( new SocketsHttpHandler @@ -140,12 +177,12 @@ public async Task> GetPlayerPortInfoAsync(int playerCount) data = data.Replace("]", string.Empty); string[] portIDs = data.Split(','); - List playerPorts = new List(); + var playerPorts = new List(); - foreach (string _port in portIDs) + foreach (string port in portIDs) { - playerPorts.Add(Convert.ToInt32(_port)); - Logger.Log($"Added port {_port}"); + playerPorts.Add(Convert.ToInt32(port, CultureInfo.InvariantCulture)); + Logger.Log($"Added port {port}"); } return playerPorts; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs deleted file mode 100644 index e2aeac9a4..000000000 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameTunnelHandler.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.NetworkInformation; -using System.Threading; -using System.Threading.Tasks; -using ClientCore.Extensions; - -namespace DTAClient.Domain.Multiplayer.CnCNet -{ - internal sealed class GameTunnelHandler - { - public event EventHandler Connected; - public event EventHandler ConnectionFailed; - - private V3TunnelConnection tunnelConnection; - private Dictionary playerConnections = new(); - - private readonly SemaphoreSlim locker = new(1, 1); - - public void SetUp(CnCNetTunnel tunnel, uint ourSenderId) - { - tunnelConnection = new V3TunnelConnection(tunnel, this, ourSenderId); - tunnelConnection.Connected += TunnelConnection_Connected; - tunnelConnection.ConnectionFailed += TunnelConnection_ConnectionFailed; - tunnelConnection.ConnectionCut += TunnelConnection_ConnectionCut; - } - - public void ConnectToTunnel() - { - if (tunnelConnection == null) - throw new InvalidOperationException("GameTunnelHandler: Call SetUp before calling ConnectToTunnel."); - - tunnelConnection.ConnectAsync().HandleTask(); - } - - public Tuple CreatePlayerConnections(List playerIds) - { - int[] ports = new int[playerIds.Count]; - playerConnections = new Dictionary(); - - for (int i = 0; i < playerIds.Count; i++) - { - var playerConnection = new TunneledPlayerConnection(playerIds[i], this); - playerConnection.CreateSocket(); - ports[i] = playerConnection.PortNumber; - playerConnections.Add(playerIds[i], playerConnection); - } - - int gamePort = GetFreePort(ports); - - foreach (KeyValuePair playerConnection in playerConnections) - { - playerConnection.Value.StartAsync(gamePort).HandleTask(); - } - - return new Tuple(ports, gamePort); - } - - private static int GetFreePort(int[] playerPorts) - { - IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); - int[] usedPorts = endPoints.Select(q => q.Port).ToArray().Concat(playerPorts).ToArray(); - int selectedPort = 0; - - while (selectedPort == 0 || usedPorts.Contains(selectedPort)) - { - selectedPort = new Random().Next(1, 65535); - } - - return selectedPort; - } - - public void Clear() - { - locker.Wait(); - - try - { - foreach (var connection in playerConnections) - { - connection.Value.Stop(); - } - - playerConnections.Clear(); - - if (tunnelConnection == null) - return; - - tunnelConnection.CloseConnection(); - tunnelConnection.Connected -= TunnelConnection_Connected; - tunnelConnection.ConnectionFailed -= TunnelConnection_ConnectionFailed; - tunnelConnection.ConnectionCut -= TunnelConnection_ConnectionCut; - tunnelConnection = null; - } - finally - { - locker.Release(); - } - } - - public async Task PlayerConnection_PacketReceivedAsync(TunneledPlayerConnection sender, ReadOnlyMemory data) - { - await locker.WaitAsync(); - - try - { - if (tunnelConnection != null) - await tunnelConnection.SendDataAsync(data, sender.PlayerID); - } - finally - { - locker.Release(); - } - } - - public async Task TunnelConnection_MessageReceivedAsync(ReadOnlyMemory data, uint senderId) - { - await locker.WaitAsync(); - - try - { - if (playerConnections.TryGetValue(senderId, out TunneledPlayerConnection connection)) - await connection.SendPacketAsync(data); - } - finally - { - locker.Release(); - } - } - - private void TunnelConnection_Connected(object sender, EventArgs e) - { - Connected?.Invoke(this, EventArgs.Empty); - } - - private void TunnelConnection_ConnectionFailed(object sender, EventArgs e) - { - ConnectionFailed?.Invoke(this, EventArgs.Empty); - Clear(); - } - - private void TunnelConnection_ConnectionCut(object sender, EventArgs e) - { - Clear(); - } - } -} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs b/DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs index cdace2f6a..c79c7361f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/HostedCnCNetGame.cs @@ -27,7 +27,7 @@ public HostedCnCNetGame(string channelName, string revision, string gamever, int public string MatchID { get; set; } public CnCNetTunnel TunnelServer { get; set; } - public override int Ping => TunnelServer.PingInMs; + public override int Ping => TunnelServer?.PingInMs ?? 0; public override bool Equals(GenericHostedGame other) => other is HostedCnCNetGame hostedCnCNetGame diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 188647b65..643ca47b4 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -1,16 +1,16 @@ -using ClientCore; -using DTAClient.Online; -using Microsoft.Xna.Framework; -using Rampastring.Tools; -using Rampastring.XNAUI; -using System; +using System; using System.Collections.Generic; using System.IO; -using System.Net; -using System.Threading.Tasks; using System.Linq; +using System.Net; using System.Net.Http; +using System.Threading.Tasks; +using ClientCore; using ClientCore.Extensions; +using DTAClient.Online; +using Microsoft.Xna.Framework; +using Rampastring.Tools; +using Rampastring.XNAUI; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -23,14 +23,26 @@ internal sealed class TunnelHandler : GameComponent /// /// A reciprocal to the value which determines how frequent the full tunnel - /// refresh would be done instead of just pinging the current tunnel (1/N of + /// refresh would be done instead of just pinging the current tunnel (1/N of /// current tunnel ping refreshes would be substituted by a full list refresh). - /// Multiply by to get the interval + /// Multiply by to get the interval /// between full list refreshes. /// private const uint CYCLES_PER_TUNNEL_LIST_REFRESH = 6; - public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) : base(wm.Game) + private readonly WindowManager wm; + + private TimeSpan timeSinceTunnelRefresh = TimeSpan.MaxValue; + private uint skipCount; + + public event EventHandler TunnelsRefreshed; + + public event EventHandler CurrentTunnelPinged; + + public event Action TunnelPinged; + + public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) + : base(wm.Game) { this.wm = wm; @@ -44,16 +56,8 @@ public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) : base(w } public List Tunnels { get; private set; } = new(); - public CnCNetTunnel CurrentTunnel { get; set; } - - public event EventHandler TunnelsRefreshed; - public event EventHandler CurrentTunnelPinged; - public event Action TunnelPinged; - - private readonly WindowManager wm; - private TimeSpan timeSinceTunnelRefresh = TimeSpan.MaxValue; - private uint skipCount; + public CnCNetTunnel CurrentTunnel { get; set; } private void DoTunnelPinged(int index) { @@ -94,7 +98,8 @@ private void HandleRefreshedTunnels(List tunnels) if (CurrentTunnel != null) { - var updatedTunnel = Tunnels.Find(t => t.Address == CurrentTunnel.Address && t.Port == CurrentTunnel.Port); + CnCNetTunnel updatedTunnel = Tunnels.Find(t => t.Address == CurrentTunnel.Address && t.Port == CurrentTunnel.Port); + if (updatedTunnel != null) { // don't re-ping if the tunnel still exists in list, just update the tunnel instance and @@ -196,8 +201,13 @@ private static async Task> DoRefreshTunnelsAsync() if (tunnel.RequiresPassword) continue; - if (tunnel.Version != Constants.TUNNEL_VERSION_2 && - tunnel.Version != Constants.TUNNEL_VERSION_3) + if (tunnel.Version is not Constants.TUNNEL_VERSION_2 and not Constants.TUNNEL_VERSION_3) + continue; + + if (tunnel.Version is Constants.TUNNEL_VERSION_2 && !UserINISettings.Instance.UseLegacyTunnels) + continue; + + if (tunnel.Version is Constants.TUNNEL_VERSION_3 && UserINISettings.Instance.UseLegacyTunnels) continue; returnValue.Add(tunnel); @@ -249,7 +259,9 @@ public override void Update(GameTime gameTime) skipCount++; } else + { timeSinceTunnelRefresh += gameTime.ElapsedGameTime; + } base.Update(gameTime); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs deleted file mode 100644 index f3323f826..000000000 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunneledPlayerConnection.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System; -using System.Buffers; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; - -namespace DTAClient.Domain.Multiplayer.CnCNet -{ - /// - /// Captures packets sent by an UDP client (the game) to a specific address - /// and allows forwarding messages back to it. - /// - internal sealed class TunneledPlayerConnection - { - private const int Timeout = 60000; - private const uint IOC_IN = 0x80000000; - private const uint IOC_VENDOR = 0x18000000; - private const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; - - private readonly GameTunnelHandler gameTunnelHandler; - - public TunneledPlayerConnection(uint playerId, GameTunnelHandler gameTunnelHandler) - { - PlayerID = playerId; - this.gameTunnelHandler = gameTunnelHandler; - } - - public int PortNumber { get; private set; } - public uint PlayerID { get; } - - private bool aborted; - public bool Aborted - { - get - { - locker.Wait(); - - try - { - return aborted; - } - finally - { - locker.Release(); - } - } - private set - { - locker.Wait(); - - try - { - aborted = value; - } - finally - { - locker.Release(); - } - } - } - - private Socket socket; - private EndPoint endPoint; - private EndPoint remoteEndPoint; - - private readonly SemaphoreSlim locker = new(1, 1); - - public void Stop() - { - Aborted = true; - } - - /// - /// Creates a socket and sets the connection's port number. - /// - public void CreateSocket() - { - socket = new Socket(SocketType.Dgram, ProtocolType.Udp); - endPoint = new IPEndPoint(IPAddress.Loopback, 0); - - // Disable ICMP port not reachable exceptions, happens when the game is still loading and has not yet opened the socket. - if (OperatingSystem.IsWindows()) - socket.IOControl(unchecked((int)SIO_UDP_CONNRESET), new byte[] { 0 }, null); - - socket.Bind(endPoint); - - PortNumber = ((IPEndPoint)socket.LocalEndPoint).Port; - } - - public async Task StartAsync(int gamePort) - { - remoteEndPoint = new IPEndPoint(IPAddress.Loopback, gamePort); - - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(128); - Memory buffer = memoryOwner.Memory[..128]; - - socket.ReceiveTimeout = Timeout; - - try - { - while (true) - { - if (Aborted) - break; - - SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint); - Memory data = buffer[..socketReceiveFromResult.ReceivedBytes]; - - await gameTunnelHandler.PlayerConnection_PacketReceivedAsync(this, data); - } - } - catch (SocketException) - { - // Timeout - } - - await locker.WaitAsync(); - - try - { - aborted = true; - socket.Close(); - } - finally - { - locker.Release(); - } - } - - public async Task SendPacketAsync(ReadOnlyMemory packet) - { - await locker.WaitAsync(); - - try - { - if (!aborted) - await socket.SendToAsync(packet, SocketFlags.None, remoteEndPoint); - } - finally - { - locker.Release(); - } - } - } -} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs new file mode 100644 index 000000000..6e5f17e82 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Threading.Tasks; +using ClientCore.Extensions; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal sealed class V3GameTunnelHandler +{ + private readonly Dictionary playerConnections = new(); + + private V3TunnelConnection tunnelConnection; + + public event EventHandler Connected; + + public event EventHandler ConnectionFailed; + + public bool IsConnected { get; private set; } + + public static int GetFreePort(IEnumerable playerPorts) + { + IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); + int[] usedPorts = endPoints.Select(q => q.Port).ToArray().Concat(playerPorts).ToArray(); + int selectedPort = 0; + + while (selectedPort == 0 || usedPorts.Contains(selectedPort)) + { + selectedPort = new Random().Next(1, 65535); + } + + return selectedPort; + } + + public void SetUp(CnCNetTunnel tunnel, uint localId) + { + tunnelConnection = new V3TunnelConnection(tunnel, this, localId); + tunnelConnection.Connected += TunnelConnection_Connected; + tunnelConnection.ConnectionFailed += TunnelConnection_ConnectionFailed; + tunnelConnection.ConnectionCut += TunnelConnection_ConnectionCut; + } + + public List CreatePlayerConnections(List playerIds) + { + int[] ports = new int[playerIds.Count]; + + for (int i = 0; i < playerIds.Count; i++) + { + var playerConnection = new V3TunneledPlayerConnection(playerIds[i], this); + + playerConnection.CreateSocket(); + + ports[i] = playerConnection.PortNumber; + + playerConnections.Add(playerIds[i], playerConnection); + } + + return ports.ToList(); + } + + public void StartPlayerConnections(int gamePort) + { + foreach (KeyValuePair playerConnection in playerConnections) + { + playerConnection.Value.StartAsync(gamePort).HandleTask(); + } + } + + public void ConnectToTunnel() + { + if (tunnelConnection == null) + throw new InvalidOperationException("GameTunnelHandler: Call SetUp before calling ConnectToTunnel."); + + tunnelConnection.ConnectAsync().HandleTask(); + } + + public void Clear() + { + ClearPlayerConnections(); + + if (tunnelConnection == null) + return; + + tunnelConnection.CloseConnection(); + + tunnelConnection.Connected -= TunnelConnection_Connected; + tunnelConnection.ConnectionFailed -= TunnelConnection_ConnectionFailed; + tunnelConnection.ConnectionCut -= TunnelConnection_ConnectionCut; + + tunnelConnection = null; + } + + public async Task PlayerConnection_PacketReceivedAsync(V3TunneledPlayerConnection sender, ReadOnlyMemory data) + { + if (tunnelConnection != null) + await tunnelConnection.SendDataAsync(data, sender.PlayerId); + } + + public async Task TunnelConnection_MessageReceivedAsync(ReadOnlyMemory data, uint senderId) + { + V3TunneledPlayerConnection connection = GetPlayerConnection(senderId); + + if (connection is not null) + await connection.SendPacketAsync(data); + } + + private V3TunneledPlayerConnection GetPlayerConnection(uint senderId) + { + if (playerConnections.TryGetValue(senderId, out V3TunneledPlayerConnection connection)) + return connection; + + return null; + } + + private void ClearPlayerConnections() + { + foreach (KeyValuePair connection in playerConnections) + { + connection.Value.Stop(); + } + + playerConnections.Clear(); + } + + private void TunnelConnection_Connected(object sender, EventArgs e) + { + IsConnected = true; + + Connected?.Invoke(this, EventArgs.Empty); + } + + private void TunnelConnection_ConnectionFailed(object sender, EventArgs e) + { + IsConnected = false; + + ConnectionFailed?.Invoke(this, EventArgs.Empty); + Clear(); + } + + private void TunnelConnection_ConnectionCut(object sender, EventArgs e) + { + Clear(); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs index 1d88f17bd..a22341ca5 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs @@ -1,189 +1,167 @@ -using Rampastring.Tools; -using System; +using System; using System.Buffers; using System.Net; using System.Net.Sockets; -using System.Threading; using System.Threading.Tasks; using ClientCore; +using Rampastring.Tools; -namespace DTAClient.Domain.Multiplayer.CnCNet +namespace DTAClient.Domain.Multiplayer.CnCNet; + +/// +/// Handles connections to version 3 CnCNet tunnel servers. +/// +internal sealed class V3TunnelConnection { - /// - /// Handles connections to version 3 CnCNet tunnel servers. - /// - internal sealed class V3TunnelConnection + private readonly CnCNetTunnel tunnel; + private readonly V3GameTunnelHandler gameTunnelHandler; + private readonly uint localId; + + private Socket tunnelSocket; + private EndPoint tunnelEndPoint; + private bool aborted; + + public V3TunnelConnection(CnCNetTunnel tunnel, V3GameTunnelHandler gameTunnelHandler, uint localId) { - public V3TunnelConnection(CnCNetTunnel tunnel, GameTunnelHandler gameTunnelHandler, uint senderId) - { - this.tunnel = tunnel; - this.gameTunnelHandler = gameTunnelHandler; - SenderId = senderId; - } + this.tunnel = tunnel; + this.gameTunnelHandler = gameTunnelHandler; + this.localId = localId; + } + + public event EventHandler Connected; - public event EventHandler Connected; - public event EventHandler ConnectionFailed; - public event EventHandler ConnectionCut; + public event EventHandler ConnectionFailed; - public uint SenderId { get; } + public event EventHandler ConnectionCut; + + public async Task ConnectAsync() + { + Logger.Log("Attempting to establish connection to V3 tunnel server " + + $"{tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); - private bool aborted; - public bool Aborted + tunnelEndPoint = new IPEndPoint(tunnel.IPAddress, tunnel.Port); + tunnelSocket = new Socket(SocketType.Dgram, ProtocolType.Udp) { - get - { - locker.Wait(); + SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT, + ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT + }; - try - { - return aborted; - } - finally - { - locker.Release(); - } - } - private set - { - locker.Wait(); + try + { + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); + Memory buffer = memoryOwner.Memory[..50]; - try - { - aborted = value; - } - finally - { - locker.Release(); - } - } + if (!BitConverter.TryWriteBytes(buffer.Span[..4], localId)) + throw new Exception(); + + await tunnelSocket.SendToAsync(buffer, SocketFlags.None, tunnelEndPoint); + Logger.Log($"Connection to V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port}) established."); + Connected?.Invoke(this, EventArgs.Empty); + } + catch (SocketException ex) + { + ProgramConstants.LogException(ex, $"Failed to establish connection to V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port})."); + tunnelSocket.Close(); + ConnectionFailed?.Invoke(this, EventArgs.Empty); + return; } - private readonly CnCNetTunnel tunnel; - private readonly GameTunnelHandler gameTunnelHandler; - private Socket tunnelSocket; - private EndPoint tunnelEndPoint; + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; - private readonly SemaphoreSlim locker = new(1, 1); + await ReceiveLoopAsync(); + } - public async Task ConnectAsync() - { - Logger.Log($"Attempting to establish connection to V3 tunnel server " + - $"{tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); + public async Task SendDataAsync(ReadOnlyMemory data, uint receiverId) + { +#if DEBUG + Logger.Log($"Sending data {localId} -> {receiverId} from ({((IPEndPoint)tunnelSocket.LocalEndPoint).Address}:{((IPEndPoint)tunnelSocket.LocalEndPoint).Port}) to V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); - tunnelEndPoint = new IPEndPoint(tunnel.IPAddress, tunnel.Port); - tunnelSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); - tunnelSocket.SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; - tunnelSocket.ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT; +#endif + const int idsSize = sizeof(uint) * 2; + int bufferSize = data.Length + idsSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory packet = memoryOwner.Memory[..bufferSize]; - try - { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); - Memory buffer = memoryOwner.Memory[..50]; + if (!BitConverter.TryWriteBytes(packet.Span[..4], localId)) + throw new Exception(); - if (!BitConverter.TryWriteBytes(buffer.Span[..4], SenderId)) - throw new Exception(); + if (!BitConverter.TryWriteBytes(packet.Span[4..8], receiverId)) + throw new Exception(); - await tunnelSocket.SendToAsync(buffer, SocketFlags.None, tunnelEndPoint); + data.CopyTo(packet[8..]); - Logger.Log($"Connection to tunnel server established."); - Connected?.Invoke(this, EventArgs.Empty); - } - catch (SocketException ex) - { - ProgramConstants.LogException(ex, "Failed to establish connection to tunnel server."); - tunnelSocket.Close(); - ConnectionFailed?.Invoke(this, EventArgs.Empty); - return; - } + if (!aborted) + await tunnelSocket.SendToAsync(packet, SocketFlags.None, tunnelEndPoint); + } - tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; + public void CloseConnection() + { + Logger.Log("Closing connection to the tunnel server."); - await ReceiveLoopAsync(); - } + aborted = true; + } - private async Task ReceiveLoopAsync() + private async Task ReceiveLoopAsync() + { + try { - try + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); + +#if DEBUG + Logger.Log($"Start listening for V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port}) on ({((IPEndPoint)tunnelSocket.LocalEndPoint).Address}:{((IPEndPoint)tunnelSocket.LocalEndPoint).Port})"); + +#endif + while (true) { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); + if (aborted) + { + DoClose(); + Logger.Log("Exiting receive loop."); + return; + } + + Memory buffer = memoryOwner.Memory[..1024]; + SocketReceiveFromResult socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync(buffer, SocketFlags.None, tunnelEndPoint); - while (true) + if (socketReceiveFromResult.ReceivedBytes < 8) { - if (Aborted) - { - DoClose(); - Logger.Log("Exiting receive loop."); - return; - } - - Memory buffer = memoryOwner.Memory[..1024]; - SocketReceiveFromResult socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync(buffer, SocketFlags.None, tunnelEndPoint); - - if (socketReceiveFromResult.ReceivedBytes < 8) - { - Logger.Log("Invalid data packet from tunnel server"); - continue; - } - - Memory data = buffer[8..socketReceiveFromResult.ReceivedBytes]; - uint senderId = BitConverter.ToUInt32(buffer[..4].Span); - - await gameTunnelHandler.TunnelConnection_MessageReceivedAsync(data, senderId); + Logger.Log("Invalid data packet from tunnel server"); + continue; } - } - catch (SocketException ex) - { - ProgramConstants.LogException(ex, "Socket exception in V3 tunnel receive loop."); - DoClose(); - ConnectionCut?.Invoke(this, EventArgs.Empty); - } - } - public void CloseConnection() - { - Logger.Log("Closing connection to the tunnel server."); - Aborted = true; - } + Memory data = buffer[8..socketReceiveFromResult.ReceivedBytes]; + uint senderId = BitConverter.ToUInt32(buffer[..4].Span); + uint receiverId = BitConverter.ToUInt32(buffer[4..8].Span); - private void DoClose() - { - Aborted = true; +#if DEBUG + Logger.Log($"Received {senderId} -> {receiverId} from V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port}) on ({((IPEndPoint)tunnelSocket.LocalEndPoint).Address}:{((IPEndPoint)tunnelSocket.LocalEndPoint).Port})"); - if (tunnelSocket != null) - { - tunnelSocket.Close(); - tunnelSocket = null; - } +#endif + if (receiverId != localId) + { + Logger.Log($"Invalid target (received: {receiverId}, expected: {localId}) from V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); + continue; + } - Logger.Log("Connection to tunnel server closed."); + await gameTunnelHandler.TunnelConnection_MessageReceivedAsync(data, senderId); + } } - - public async Task SendDataAsync(ReadOnlyMemory data, uint receiverId) + catch (SocketException ex) { - const int idsSize = sizeof(uint) * 2; - int bufferSize = data.Length + idsSize; - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); - Memory packet = memoryOwner.Memory[..bufferSize]; - - if (!BitConverter.TryWriteBytes(packet.Span[..4], SenderId)) - throw new Exception(); + ProgramConstants.LogException(ex, "Socket exception in V3 tunnel receive loop."); + DoClose(); + ConnectionCut?.Invoke(this, EventArgs.Empty); + } + } - if (!BitConverter.TryWriteBytes(packet.Span[4..8], receiverId)) - throw new Exception(); + private void DoClose() + { + aborted = true; - data.CopyTo(packet[8..]); + tunnelSocket?.Close(); - await locker.WaitAsync(); + tunnelSocket = null; - try - { - if (!aborted) - await tunnelSocket.SendToAsync(packet, SocketFlags.None, tunnelEndPoint); - } - finally - { - locker.Release(); - } - } + Logger.Log("Connection to tunnel server closed."); } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunneledPlayerConnection.cs new file mode 100644 index 000000000..18a7887a0 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunneledPlayerConnection.cs @@ -0,0 +1,113 @@ +using System; +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +#if DEBUG +using Rampastring.Tools; +#endif + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +/// +/// Captures packets sent by an UDP client (the game) to a specific address +/// and allows forwarding messages back to it. +/// +internal sealed class V3TunneledPlayerConnection +{ + private const int Timeout = 60000; + private const uint IOC_IN = 0x80000000; + private const uint IOC_VENDOR = 0x18000000; + private const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; + + private readonly V3GameTunnelHandler gameTunnelHandler; + + private Socket socket; + private EndPoint endPoint; + private EndPoint remoteEndPoint; + private bool aborted; + + public V3TunneledPlayerConnection(uint playerId, V3GameTunnelHandler gameTunnelHandler) + { + PlayerId = playerId; + this.gameTunnelHandler = gameTunnelHandler; + } + + public int PortNumber { get; private set; } + + public uint PlayerId { get; } + + public void Stop() + { + aborted = true; + } + + /// + /// Creates a socket and sets the connection's port number. + /// + public void CreateSocket() + { + socket = new Socket(SocketType.Dgram, ProtocolType.Udp); + endPoint = new IPEndPoint(IPAddress.Loopback, 0); + + // Disable ICMP port not reachable exceptions, happens when the game is still loading and has not yet opened the socket. + if (OperatingSystem.IsWindows()) + socket.IOControl(unchecked((int)SIO_UDP_CONNRESET), new byte[] { 0 }, null); + + socket.Bind(endPoint); + + PortNumber = ((IPEndPoint)socket.LocalEndPoint).Port; + } + + public async Task StartAsync(int gamePort) + { + remoteEndPoint = new IPEndPoint(IPAddress.Loopback, gamePort); + + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(128); + Memory buffer = memoryOwner.Memory[..128]; + + socket.ReceiveTimeout = Timeout; + +#if DEBUG + Logger.Log($"Start listening for local game {((IPEndPoint)remoteEndPoint).Address}:{((IPEndPoint)remoteEndPoint).Port} on ({((IPEndPoint)socket.LocalEndPoint).Address}:{((IPEndPoint)socket.LocalEndPoint).Port})"); + +#endif + try + { + while (true) + { + if (aborted) + break; + + SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint); + Memory data = buffer[..socketReceiveFromResult.ReceivedBytes]; + +#if DEBUG + Logger.Log($"Received data from local game {((IPEndPoint)socketReceiveFromResult.RemoteEndPoint).Address}:{((IPEndPoint)socketReceiveFromResult.RemoteEndPoint).Port} on ({((IPEndPoint)socket.LocalEndPoint).Address}:{((IPEndPoint)socket.LocalEndPoint).Port})"); + +#endif + + await gameTunnelHandler.PlayerConnection_PacketReceivedAsync(this, data); + } + } + catch (SocketException) + { + } + + aborted = true; + + socket.Close(); + } + + public async Task SendPacketAsync(ReadOnlyMemory packet) + { + if (aborted) + return; + +#if DEBUG + Logger.Log($"Sending data from ({((IPEndPoint)socket.LocalEndPoint).Address}:{((IPEndPoint)socket.LocalEndPoint).Port}) to local game {((IPEndPoint)remoteEndPoint).Address}:{((IPEndPoint)remoteEndPoint).Port}"); + +#endif + await socket.SendToAsync(packet, SocketFlags.None, remoteEndPoint); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index afb23ef6c..ded2fbaef 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -11,7 +11,7 @@ namespace DTAClient.Domain.Multiplayer.LAN { - public class LANPlayerInfo : PlayerInfo + internal sealed class LANPlayerInfo : PlayerInfo { public LANPlayerInfo(Encoding encoding) { diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANServerCommandHandler.cs b/DXMainClient/Domain/Multiplayer/LAN/LANServerCommandHandler.cs index b4a33a7cf..4d56a450e 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANServerCommandHandler.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANServerCommandHandler.cs @@ -1,6 +1,6 @@ namespace DTAClient.Domain.Multiplayer.LAN { - public abstract class LANServerCommandHandler + internal abstract class LANServerCommandHandler { public LANServerCommandHandler(string commandName) { @@ -11,4 +11,4 @@ public LANServerCommandHandler(string commandName) public abstract bool Handle(LANPlayerInfo pInfo, string message); } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/LAN/ServerNoParamCommandHandler.cs b/DXMainClient/Domain/Multiplayer/LAN/ServerNoParamCommandHandler.cs index 9146d8cca..083a7ad77 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/ServerNoParamCommandHandler.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/ServerNoParamCommandHandler.cs @@ -2,15 +2,15 @@ namespace DTAClient.Domain.Multiplayer.LAN { - public class ServerNoParamCommandHandler : LANServerCommandHandler + internal sealed class ServerNoParamCommandHandler : LANServerCommandHandler { - public ServerNoParamCommandHandler(string commandName, - Action handler) : base(commandName) + public ServerNoParamCommandHandler(string commandName, Action handler) + : base(commandName) { this.handler = handler; } - Action handler; + private Action handler; public override bool Handle(LANPlayerInfo pInfo, string message) { @@ -23,4 +23,4 @@ public override bool Handle(LANPlayerInfo pInfo, string message) return false; } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/LAN/ServerStringCommandHandler.cs b/DXMainClient/Domain/Multiplayer/LAN/ServerStringCommandHandler.cs index d69ef71f3..536d0f4cf 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/ServerStringCommandHandler.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/ServerStringCommandHandler.cs @@ -2,10 +2,9 @@ namespace DTAClient.Domain.Multiplayer.LAN { - public class ServerStringCommandHandler : LANServerCommandHandler + internal sealed class ServerStringCommandHandler : LANServerCommandHandler { - public ServerStringCommandHandler(string commandName, - Action handler) + public ServerStringCommandHandler(string commandName, Action handler) : base(commandName) { this.handler = handler; @@ -15,12 +14,11 @@ public ServerStringCommandHandler(string commandName, public override bool Handle(LANPlayerInfo pInfo, string message) { - if (!message.StartsWith(CommandName) || - message.Length <= CommandName.Length + 1) + if (!message.StartsWith(CommandName) || message.Length <= CommandName.Length + 1) return false; handler(pInfo, message); return true; } } -} +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/PlayerHouseInfo.cs b/DXMainClient/Domain/Multiplayer/PlayerHouseInfo.cs index a7493be58..994371fcf 100644 --- a/DXMainClient/Domain/Multiplayer/PlayerHouseInfo.cs +++ b/DXMainClient/Domain/Multiplayer/PlayerHouseInfo.cs @@ -4,7 +4,7 @@ namespace DTAClient.Domain.Multiplayer { - public class PlayerHouseInfo + internal sealed class PlayerHouseInfo { public int SideIndex { get; set; } @@ -18,7 +18,7 @@ public int InternalSideIndex { if (IsSpectator && !string.IsNullOrEmpty(ClientConfiguration.Instance.SpectatorInternalSideIndex)) return int.Parse(ClientConfiguration.Instance.SpectatorInternalSideIndex); - + if (!string.IsNullOrEmpty(ClientConfiguration.Instance.InternalSideIndices)) return Array.ConvertAll(ClientConfiguration.Instance.InternalSideIndices.Split(','), int.Parse)[SideIndex]; @@ -40,8 +40,13 @@ public int InternalSideIndex /// The number of sides in the game. /// Random number generator. /// A bool array that determines which side indexes are disallowed by game options. - public void RandomizeSide(PlayerInfo pInfo, int sideCount, Random random, - bool[] disallowedSideArray, List randomSelectors, int randomCount) + public void RandomizeSide( + PlayerInfo pInfo, + int sideCount, + Random random, + bool[] disallowedSideArray, + List randomSelectors, + int randomCount) { if (pInfo.SideId == 0 || pInfo.SideId == sideCount + randomCount) { @@ -62,7 +67,7 @@ public void RandomizeSide(PlayerInfo pInfo, int sideCount, Random random, int[] randomsides = randomSelectors[pInfo.SideId - 1]; int count = randomsides.Length; int sideId; - + do sideId = randomsides[random.Next(0, count)]; while (disallowedSideArray[sideId]); @@ -81,7 +86,7 @@ public void RandomizeSide(PlayerInfo pInfo, int sideCount, Random random, /// The list of available (un-used) colors. /// The list of all multiplayer colors. /// Random number generator. - public void RandomizeColor(PlayerInfo pInfo, List freeColors, + public void RandomizeColor(PlayerInfo pInfo, List freeColors, List mpColors, Random random) { if (pInfo.ColorId == 0) @@ -114,9 +119,9 @@ public void RandomizeColor(PlayerInfo pInfo, List freeColors, /// True if the player's starting location index exceeds the map's number of starting waypoints, /// otherwise false. public void RandomizeStart( - PlayerInfo pInfo, + PlayerInfo pInfo, Random random, - List freeStartingLocations, + List freeStartingLocations, List takenStartingLocations, bool overrideGameRandomLocations ) diff --git a/DXMainClient/Domain/Multiplayer/PlayerInfo.cs b/DXMainClient/Domain/Multiplayer/PlayerInfo.cs index a4ff6f809..7f5bbbeba 100644 --- a/DXMainClient/Domain/Multiplayer/PlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/PlayerInfo.cs @@ -1,14 +1,16 @@ -using Rampastring.Tools; -using System; +using System; +using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer { /// /// A player in the game lobby. /// - public class PlayerInfo + internal class PlayerInfo { - public PlayerInfo() { } + public PlayerInfo() + { + } public PlayerInfo(string name) { @@ -25,17 +27,27 @@ public PlayerInfo(string name, int sideId, int startingLocation, int colorId, in } public string Name { get; set; } + public int SideId { get; set; } + public int StartingLocation { get; set; } + public int ColorId { get; set; } + public int TeamId { get; set; } + public bool Ready { get; set; } + public bool AutoReady { get; set; } + public bool IsAI { get; set; } public bool IsInGame { get; set; } + public virtual string IPAddress { get; set; } = System.Net.IPAddress.Any.ToString(); + public int Port { get; set; } + public bool Verified { get; set; } public int Index { get; set; } diff --git a/DXMainClient/Online/Channel.cs b/DXMainClient/Online/Channel.cs index 27d89137b..73aab3fe5 100644 --- a/DXMainClient/Online/Channel.cs +++ b/DXMainClient/Online/Channel.cs @@ -323,6 +323,11 @@ public Task JoinAsync() return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, IRCCommands.JOIN + " " + ChannelName + " " + Password); } + public Task RequestUserInfoAsync() + { + return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, "WHO " + ChannelName); + } + public async Task LeaveAsync() { // Wait a random amount of time before joining to prevent join/part floods diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index e0da78d66..bb4c538e0 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -42,7 +42,7 @@ public Connection(IConnectionManager connectionManager) new("Burstfire.UK.EU.GameSurge.net", "GameSurge London, UK", new[] { 6667, 6668, 7000 }), new("VortexServers.IL.US.GameSurge.net", "GameSurge Chicago, IL", new[] { 6660, 6666, 6667, 6668, 6669 }), new("Gameservers.NJ.US.GameSurge.net", "GameSurge Newark, NJ", new[] { 6665, 6666, 6667, 6668, 6669, 7000, 8080 }), - new("Krypt.CA.US.GameSurge.net", "GameSurge Santa Ana, CA",new[] { 6666, 6667, 6668, 6669 }), + new("Krypt.CA.US.GameSurge.net", "GameSurge Santa Ana, CA", new[] { 6666, 6667, 6668, 6669 }), new("NuclearFallout.WA.US.GameSurge.net", "GameSurge Seattle, WA", new[] { 6667, 5960 }), new("Stockholm.SE.EU.GameSurge.net", "GameSurge Stockholm, Sweden", new[] { 6660, 6666, 6667, 6668, 6669 }), new("Prothid.NY.US.GameSurge.Net", "GameSurge NYC, NY", new[] { 5960, 6660, 6666, 6667, 6668, 6669 }), @@ -156,11 +156,13 @@ private async Task ConnectToServerAsync(CancellationToken cancellationToken) try { - await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(server.Host), port), + await client.ConnectAsync( + new IPEndPoint(IPAddress.Parse(server.Host), port), new CancellationTokenSource(TimeSpan.FromSeconds(3)).Token); } catch (OperationCanceledException) - { } + { + } if (!client.Connected) { @@ -168,7 +170,7 @@ await client.ConnectAsync(new IPEndPoint(IPAddress.Parse(server.Host), port), continue; // Start all over again, using the next port } - Logger.Log("Succesfully connected to " + server.Host + " on port " + port); + Logger.Log("Successfully connected to " + server.Host + " on port " + port); _isConnected = true; _attemptingConnection = false; @@ -257,8 +259,9 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) // A message has been successfully received string msg = encoding.GetString(message.Span[..bytesRead]); +#if !DEBUG Logger.Log("Message received: " + msg); - +#endif await HandleMessageAsync(msg); timer.Interval = 30000; } @@ -497,7 +500,9 @@ private async Task PerformCommandAsync(string message) ParseIrcMessage(message, out string prefix, out string command, out List parameters); string paramString = string.Empty; foreach (string param in parameters) { paramString = paramString + param + ","; } +#if !DEBUG Logger.Log("RMP: " + prefix + " " + command + " " + paramString); +#endif try { From 198ae1832e6395ee6b1028f22fc302859622e23b Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 29 Nov 2022 17:41:09 +0100 Subject: [PATCH 044/109] Add chat command for dynamic tunnels --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 95 ++++++++++++++----- .../Multiplayer/CnCNet/CnCNetLobbyCommands.cs | 1 + 2 files changed, 71 insertions(+), 25 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index e0b0ace6c..b62686021 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -34,6 +34,7 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private const double INITIAL_GAME_BROADCAST_DELAY = 10.0; private const double MAX_TIME_FOR_GAME_LAUNCH = 20.0; private const int PRIORITY_START_GAME = 10; + private const int PINNED_DYNAMIC_TUNNELS = 10; private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; @@ -94,6 +95,7 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private EventHandler tunnelHandler_CurrentTunnelFunc; private List<(int Ping, string Hash)> pinnedTunnels; private string pinnedTunnelPingsMessage; + private bool dynamicTunnelsEnabled; public CnCNetGameLobby( WindowManager windowManager, @@ -168,6 +170,11 @@ public CnCNetGameLobby( "Download a map from CNCNet's map server using a map ID and an optional filename.\nExample: \"/downloadmap MAPID [2] My Battle Map\"".L10N("Client:Main:DownloadMapCommandDescription"), false, DownloadMapByIdCommand)); + AddChatBoxCommand(new( + CnCNetLobbyCommands.DYNAMICTUNNELS, + "Toggle dynamic CnCNet tunnel servers on/off (game host only)".L10N("UI:Main:ChangeDynamicTunnels"), + true, + _ => ToggleDynamicTunnelsAsync().HandleTask())); } public event EventHandler GameLeft; @@ -281,6 +288,7 @@ public async Task SetUpAsync( this.hostName = hostName; this.playerLimit = playerLimit; this.isCustomPassword = isCustomPassword; + dynamicTunnelsEnabled = UserINISettings.Instance.UseDynamicTunnels; channel.MessageAdded += Channel_MessageAdded; channel.CTCPReceived += Channel_CTCPReceived; channel.UserKicked += channel_UserKickedFunc; @@ -304,7 +312,7 @@ public async Task SetUpAsync( AIPlayers.Clear(); } - if (!UserINISettings.Instance.UseDynamicTunnels) + if (!dynamicTunnelsEnabled) { tunnelHandler.CurrentTunnel = tunnel; } @@ -333,11 +341,11 @@ public async Task OnJoinedAsync() .Where(q => !q.RequiresPassword && q.PingInMs > -1 && q.Clients < q.MaxClients - 8 && q.Version == Constants.TUNNEL_VERSION_3) .OrderBy(q => q.PingInMs) .ThenBy(q => q.Hash, StringComparer.OrdinalIgnoreCase) + .Take(PINNED_DYNAMIC_TUNNELS) .Select(q => (q.PingInMs, q.Hash)) .ToList(); IEnumerable tunnelPings = pinnedTunnels - .Take(10) .Select(q => FormattableString.Invariant($"{q.Ping};{q.Hash}\t")); pinnedTunnelPingsMessage = string.Concat(tunnelPings); @@ -368,7 +376,7 @@ await connectionManager.SendCustomMessageAsync(new( { await channel.SendCTCPMessageAsync(CnCNetCommands.FILE_HASH + " " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); - if (UserINISettings.Instance.UseDynamicTunnels) + if (dynamicTunnelsEnabled) await BroadcastPlayerTunnelPingsAsync(); if (UserINISettings.Instance.UseP2P) @@ -393,7 +401,7 @@ private async Task UpdatePingAsync() { int ping; - if (UserINISettings.Instance.UseDynamicTunnels) + if (dynamicTunnelsEnabled) ping = pinnedTunnels.Min(q => q.Ping); else if (tunnelHandler.CurrentTunnel == null) return; @@ -425,7 +433,11 @@ protected override void CopyPlayerDataToUI() private void PrintTunnelServerInformation(string s) { - if (tunnelHandler.CurrentTunnel == null) + if (dynamicTunnelsEnabled) + { + AddNotice("Dynamic tunnels enabled".L10N("Client:Main:DynamicTunnelsEnabled")); + } + else if (tunnelHandler.CurrentTunnel is null) { AddNotice("Tunnel server unavailable!".L10N("Client:Main:TunnelUnavailable")); } @@ -641,7 +653,7 @@ private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT && AIPlayers.Count > 0) AIPlayers.RemoveAt(AIPlayers.Count - 1); - if (UserINISettings.Instance.UseDynamicTunnels && pInfo != FindLocalPlayer()) + if (dynamicTunnelsEnabled && pInfo != FindLocalPlayer()) await BroadcastPlayerTunnelPingsAsync(); sndJoinSound.Play(); @@ -771,7 +783,7 @@ protected override async Task HostLaunchGameAsync() await HostLaunchGameV2TunnelAsync(); else if (tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_3) await HostLaunchGameV3TunnelAsync(); - else if (UserINISettings.Instance.UseDynamicTunnels) + else if (dynamicTunnelsEnabled) await HostLaunchGameV3TunnelAsync(); else throw new InvalidOperationException("Unknown tunnel server version!"); @@ -884,29 +896,29 @@ private void ContactTunnel() dynamicV3GameTunnelHandlers.Clear(); - if (!UserINISettings.Instance.UseDynamicTunnels) + if (!dynamicTunnelsEnabled) { - var dynamicV3GameTunnelHandler = new V3GameTunnelHandler(); + var gameTunnelHandler = new V3GameTunnelHandler(); - dynamicV3GameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - dynamicV3GameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + gameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + gameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - dynamicV3GameTunnelHandler.SetUp(tunnelHandler.CurrentTunnel, localId); - dynamicV3GameTunnelHandler.ConnectToTunnel(); - dynamicV3GameTunnelHandlers.Add(new(Players.Where(q => q != FindLocalPlayer()).Select(q => q.Name).ToList(), dynamicV3GameTunnelHandler)); + gameTunnelHandler.SetUp(tunnelHandler.CurrentTunnel, localId); + gameTunnelHandler.ConnectToTunnel(); + dynamicV3GameTunnelHandlers.Add(new(Players.Where(q => q != FindLocalPlayer()).Select(q => q.Name).ToList(), gameTunnelHandler)); } else { foreach (IGrouping tunnelGrouping in playerTunnels.GroupBy(q => q.Tunnel)) { - var dynamicV3GameTunnelHandler = new V3GameTunnelHandler(); + var gameTunnelHandler = new V3GameTunnelHandler(); - dynamicV3GameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - dynamicV3GameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + gameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + gameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - dynamicV3GameTunnelHandler.SetUp(tunnelGrouping.Key, localId); - dynamicV3GameTunnelHandler.ConnectToTunnel(); - dynamicV3GameTunnelHandlers.Add(new(tunnelGrouping.Select(q => q.Name).ToList(), dynamicV3GameTunnelHandler)); + gameTunnelHandler.SetUp(tunnelGrouping.Key, localId); + gameTunnelHandler.ConnectToTunnel(); + dynamicV3GameTunnelHandlers.Add(new(tunnelGrouping.Select(q => q.Name).ToList(), gameTunnelHandler)); } } @@ -917,7 +929,7 @@ private void ContactTunnel() private async Task GameTunnelHandler_Connected_CallbackAsync() { - if (UserINISettings.Instance.UseDynamicTunnels) + if (dynamicTunnelsEnabled) { if (dynamicV3GameTunnelHandlers.Any() && dynamicV3GameTunnelHandlers.All(q => q.Tunnel.IsConnected)) isPlayerConnectedToTunnel[Players.FindIndex(p => p == FindLocalPlayer())] = true; @@ -1022,7 +1034,7 @@ protected override string GetIPAddressForPlayer(PlayerInfo player) if (UserINISettings.Instance.UseP2P) return IPAddress.Parse(player.IPAddress).MapToIPv4().ToString(); - if (UserINISettings.Instance.UseDynamicTunnels || tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) + if (dynamicTunnelsEnabled || tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) return IPAddress.Loopback.MapToIPv4().ToString(); return base.GetIPAddressForPlayer(player); @@ -1336,6 +1348,7 @@ protected override async Task OnGameOptionChangedAsync() return; bool[] optionValues = new bool[CheckBoxes.Count]; + for (int i = 0; i < CheckBoxes.Count; i++) optionValues[i] = CheckBoxes[i].Checked; @@ -1347,7 +1360,6 @@ protected override async Task OnGameOptionChangedAsync() int integerCount = byteList.Count / 4; byte[] byteArray = byteList.ToArray(); - var sb = new ExtendedStringBuilder(CnCNetCommands.GAME_OPTIONS + " ", true, ';'); for (int i = 0; i < integerCount; i++) @@ -1356,7 +1368,6 @@ protected override async Task OnGameOptionChangedAsync() // We don't gain much in most cases by packing the drop-down values // (because they're bytes to begin with, and usually non-zero), // so let's just transfer them as usual - foreach (GameLobbyDropDown dd in DropDowns) sb.Append(dd.SelectedIndex); @@ -1369,10 +1380,17 @@ protected override async Task OnGameOptionChangedAsync() sb.Append(RandomSeed); sb.Append(Convert.ToInt32(RemoveStartingLocations)); sb.Append(Map.UntranslatedName); + sb.Append(Convert.ToInt32(dynamicTunnelsEnabled)); // todo get from UI await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); } + private async Task ToggleDynamicTunnelsAsync() + { + await ChangeDynamicTunnelsSettingAsync(!dynamicTunnelsEnabled); + await OnGameOptionChangedAsync(); + } + /// /// Handles game option messages received from the game host. /// @@ -1385,7 +1403,7 @@ private async Task ApplyGameOptionsAsync(string sender, string message) int checkBoxIntegerCount = (CheckBoxes.Count / 32) + 1; int partIndex = checkBoxIntegerCount + DropDowns.Count; - if (parts.Length < partIndex + 6) + if (parts.Length < partIndex + 10) { AddNotice(("The game host has sent an invalid game options message! " + "The game host's game version might be different from yours.").L10N("Client:Main:HostGameOptionInvalid"), Color.Red); @@ -1541,6 +1559,32 @@ private async Task ApplyGameOptionsAsync(string sender, string message) SetRandomStartingLocations(removeStartingLocations); RandomSeed = randomSeed; + + bool newDynamicTunnelsSetting = Conversions.BooleanFromString(parts[partIndex + 9], true); + + if (newDynamicTunnelsSetting != dynamicTunnelsEnabled) + { + if (newDynamicTunnelsSetting) + AddNotice(string.Format("The game host has enabled Dynamic Tunnels".L10N("UI:Main:HostEnableDynamicTunnels"))); + else + AddNotice(string.Format("The game host has disabled Dynamic Tunnels".L10N("UI:Main:HostDisableDynamicTunnels"))); + + await ChangeDynamicTunnelsSettingAsync(newDynamicTunnelsSetting); + } + } + + private async Task ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsEnabledValue) + { + dynamicTunnelsEnabled = newDynamicTunnelsEnabledValue; + + if (newDynamicTunnelsEnabledValue) + { + tunnelHandler.CurrentTunnel = tunnelHandler.Tunnels + .Where(q => q.PingInMs > -1 && !q.RequiresPassword && q.Clients < q.MaxClients - 8 && q.Version == Constants.TUNNEL_VERSION_3) + .MinBy(q => q.PingInMs); + + await BroadcastPlayerTunnelPingsAsync(); + } } private async Task RequestMapAsync() @@ -1972,6 +2016,7 @@ private void HandleTunnelPingsMessage(string sender, string tunnelPingsMessage) return (int.Parse(split[0], CultureInfo.InvariantCulture), split[1]); }); IEnumerable<(int CombinedPing, string Hash)> combinedTunnelResults = tunnelPings + .Where(q => pinnedTunnels.Select(r => r.Hash).Contains(q.Hash)) .Select(q => (CombinedPing: q.Ping + pinnedTunnels.SingleOrDefault(r => q.Hash.Equals(r.Hash, StringComparison.OrdinalIgnoreCase)).Ping, q.Hash)); (int _, string hash) = combinedTunnelResults .OrderBy(q => q.CombinedPing) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs index 6bc3c1c04..02a43c457 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs @@ -16,4 +16,5 @@ internal static class CnCNetLobbyCommands public const string ROLL = "ROLL"; public const string SAVEOPTIONS = "SAVEOPTIONS"; public const string LOADOPTIONS = "LOADOPTIONS"; + public const string DYNAMICTUNNELS = "DYNAMICTUNNELS"; } \ No newline at end of file From b42bbedd75651902aa3121e6b3f73411cd88cfdb Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 29 Nov 2022 18:54:11 +0100 Subject: [PATCH 045/109] Tunnel switching and matching --- .../CnCNet/CnCNetGameLoadingLobby.cs | 11 +++--- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 22 ++---------- .../DXGUI/Multiplayer/CnCNet/TunnelListBox.cs | 25 +++++++------ .../CnCNet/TunnelSelectionWindow.cs | 6 ++-- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 36 +++++++++---------- .../Multiplayer/CnCNet/TunnelHandler.cs | 5 +-- 6 files changed, 43 insertions(+), 62 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index a9af05314..ef11dff9f 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -363,7 +363,7 @@ private void ShowTunnelSelectionWindow(string description) private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) { await channel.SendCTCPMessageAsync( - $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Address}:{e.Tunnel.Port}", + $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Hash}", QueuedMessageType.SYSTEM_MESSAGE, 10); HandleTunnelServerChange(e.Tunnel); @@ -541,16 +541,13 @@ private async Task HandlePlayerReadyRequestAsync(string sender, int readyStatus) await BroadcastOptionsAsync(); } - private void HandleTunnelServerChangeMessage(string sender, string tunnelAddressAndPort) + private void HandleTunnelServerChangeMessage(string sender, string hash) { if (sender != hostName) return; - string[] split = tunnelAddressAndPort.Split(':'); - string tunnelAddress = split[0]; - int tunnelPort = int.Parse(split[1]); + CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)); - CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); if (tunnel == null) { AddNotice(("The game host has selected an invalid tunnel server! " + @@ -673,7 +670,7 @@ private async Task BroadcastGameAsync() sb.Append(";"); sb.Append((string)lblGameModeValue.Tag); sb.Append(";"); - sb.Append(tunnelHandler.CurrentTunnel.Address + ":" + tunnelHandler.CurrentTunnel.Port); + sb.Append(tunnelHandler.CurrentTunnel?.Hash ?? ProgramConstants.CNCNET_DYNAMIC_TUNNELS); sb.Append(";"); sb.Append(0); // LoadedGameId diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 9e0c83f8c..6fee62082 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -1477,31 +1477,13 @@ private void GameBroadcastChannel_CTCPReceived(object sender, ChannelCTCPEventAr CnCNetTunnel tunnel = null; -#if DEBUG - if (tunnelHash.Contains(':')) + if (!ProgramConstants.CNCNET_DYNAMIC_TUNNELS.Equals(tunnelHash, StringComparison.OrdinalIgnoreCase)) { - string[] tunnelAddressAndPort = splitMessage[9].Split(':'); - string tunnelAddress = tunnelAddressAndPort[0]; - int tunnelPort = int.Parse(tunnelAddressAndPort[1], CultureInfo.InvariantCulture); - - tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); + tunnel = tunnelHandler.Tunnels.Find(t => t.Hash.Equals(tunnelHash, StringComparison.OrdinalIgnoreCase)); if (tunnel == null) return; } - else - { -#endif - if (!ProgramConstants.CNCNET_DYNAMIC_TUNNELS.Equals(tunnelHash, StringComparison.OrdinalIgnoreCase)) - { - tunnel = tunnelHandler.Tunnels.Find(t => t.Hash.Equals(tunnelHash, StringComparison.OrdinalIgnoreCase)); - - if (tunnel == null) - return; - } -#if DEBUG - } -#endif var game = new HostedCnCNetGame(gameRoomChannelName, revision, gameVersion, maxPlayers, gameRoomDisplayName, isCustomPassword, true, players, e.UserName, mapName, gameMode); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs index 1944f047a..c399f597e 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelListBox.cs @@ -14,7 +14,8 @@ namespace DTAClient.DXGUI.Multiplayer.CnCNet /// class TunnelListBox : XNAMultiColumnListBox { - public TunnelListBox(WindowManager windowManager, TunnelHandler tunnelHandler) : base(windowManager) + public TunnelListBox(WindowManager windowManager, TunnelHandler tunnelHandler) + : base(windowManager) { this.tunnelHandler = tunnelHandler; @@ -45,7 +46,7 @@ public TunnelListBox(WindowManager windowManager, TunnelHandler tunnelHandler) : private int lowestTunnelRating = int.MaxValue; private bool isManuallySelectedTunnel; - private string manuallySelectedTunnelAddress; + private string manuallySelectedTunnelHash; /// /// Selects a tunnel from the list with the given address. @@ -58,17 +59,17 @@ public void SelectTunnel(CnCNetTunnel cnCNetTunnel) { SelectedIndex = index; isManuallySelectedTunnel = true; - manuallySelectedTunnelAddress = cnCNetTunnel.Address; + manuallySelectedTunnelHash = cnCNetTunnel.Hash; } } /// /// Gets whether or not a tunnel from the list with the given address is selected. /// - /// The address of the tunnel server + /// The hash of the tunnel server /// True if tunnel with given address is selected, otherwise false. - public bool IsTunnelSelected(string address) => - tunnelHandler.Tunnels.FindIndex(t => t.Address == address) == SelectedIndex; + public bool IsTunnelSelected(string hash) => + tunnelHandler.Tunnels.FindIndex(t => t.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)) == SelectedIndex; private void TunnelHandler_TunnelsRefreshed(object sender, EventArgs e) { @@ -112,7 +113,7 @@ private void TunnelHandler_TunnelsRefreshed(object sender, EventArgs e) } else { - int manuallySelectedIndex = tunnelHandler.Tunnels.FindIndex(t => t.Address == manuallySelectedTunnelAddress); + int manuallySelectedIndex = tunnelHandler.Tunnels.FindIndex(t => t.Hash.Equals(manuallySelectedTunnelHash, StringComparison.OrdinalIgnoreCase)); if (manuallySelectedIndex == -1) { @@ -120,7 +121,9 @@ private void TunnelHandler_TunnelsRefreshed(object sender, EventArgs e) isManuallySelectedTunnel = false; } else + { SelectedIndex = manuallySelectedIndex; + } } } @@ -133,7 +136,9 @@ private void TunnelHandler_TunnelPinged(int tunnelIndex) CnCNetTunnel tunnel = tunnelHandler.Tunnels[tunnelIndex]; if (tunnel.PingInMs == -1) + { lbItem.Text = "Unknown".L10N("Client:Main:UnknownPing"); + } else { lbItem.Text = tunnel.PingInMs + " ms"; @@ -151,7 +156,7 @@ private void TunnelHandler_TunnelPinged(int tunnelIndex) } } - private int GetTunnelRating(CnCNetTunnel tunnel) + private static int GetTunnelRating(CnCNetTunnel tunnel) { double usageRatio = (double)tunnel.Clients / tunnel.MaxClients; @@ -169,7 +174,7 @@ private void TunnelListBox_SelectedIndexChanged(object sender, EventArgs e) return; isManuallySelectedTunnel = true; - manuallySelectedTunnelAddress = tunnelHandler.Tunnels[SelectedIndex].Address; + manuallySelectedTunnelHash = tunnelHandler.Tunnels[SelectedIndex].Hash; } } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs index 108ccc38b..be8a0c552 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/TunnelSelectionWindow.cs @@ -24,7 +24,7 @@ public TunnelSelectionWindow(WindowManager windowManager, TunnelHandler tunnelHa private XNALabel lblDescription; private XNAClientButton btnApply; - private string originalTunnelAddress; + private string originalTunnelHash; public override void Initialize() { @@ -90,7 +90,7 @@ private void BtnApply_LeftClick(object sender, EventArgs e) private void BtnCancel_LeftClick(object sender, EventArgs e) => Disable(); private void LbTunnelList_SelectedIndexChanged(object sender, EventArgs e) => - btnApply.AllowClick = !lbTunnelList.IsTunnelSelected(originalTunnelAddress) && lbTunnelList.IsValidIndexSelected(); + btnApply.AllowClick = !lbTunnelList.IsTunnelSelected(originalTunnelHash) && lbTunnelList.IsValidIndexSelected(); /// /// Sets the window's description and selects the tunnel server @@ -101,7 +101,7 @@ private void LbTunnelList_SelectedIndexChanged(object sender, EventArgs e) => public void Open(string description, CnCNetTunnel cnCNetTunnel) { lblDescription.Text = description; - originalTunnelAddress = cnCNetTunnel.Address; + originalTunnelHash = cnCNetTunnel.Hash; if (cnCNetTunnel is not null) lbTunnelList.SelectTunnel(cnCNetTunnel); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index b62686021..a9f20b6af 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -67,6 +67,7 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private bool isStartingGame; private string gameFilesHash; private MapSharingConfirmationPanel mapSharingConfirmationPanel; + private CnCNetTunnel initialTunnel; /// /// The SHA1 of the latest selected map. @@ -146,7 +147,7 @@ public CnCNetGameLobby( new StringCommandHandler(CnCNetCommands.DICE_ROLL, HandleDiceRollResult), new NoParamCommandHandler(CnCNetCommands.CHEAT_DETECTED, HandleCheatDetectedMessage), new IntCommandHandler(CnCNetCommands.TUNNEL_PING, HandleTunnelPing), - new StringCommandHandler(CnCNetCommands.CHANGE_TUNNEL_SERVER, (sender, tunnelAddressAndPort) => HandleTunnelServerChangeMessageAsync(sender, tunnelAddressAndPort).HandleTask()), + new StringCommandHandler(CnCNetCommands.CHANGE_TUNNEL_SERVER, (sender, hash) => HandleTunnelServerChangeMessageAsync(sender, hash).HandleTask()), new StringCommandHandler(CnCNetCommands.PLAYER_TUNNEL_PINGS, HandleTunnelPingsMessage) }; @@ -312,9 +313,11 @@ public async Task SetUpAsync( AIPlayers.Clear(); } + initialTunnel = tunnel; + if (!dynamicTunnelsEnabled) { - tunnelHandler.CurrentTunnel = tunnel; + tunnelHandler.CurrentTunnel = initialTunnel; } else { @@ -350,11 +353,6 @@ public async Task OnJoinedAsync() pinnedTunnelPingsMessage = string.Concat(tunnelPings); - foreach ((string sender, string tunnelPingsMessage) in tunnelPingsMessages) - { - HandleTunnelPingsMessage(sender, tunnelPingsMessage); - } - if (IsHost) { await connectionManager.SendCustomMessageAsync(new( @@ -454,7 +452,7 @@ private void ShowTunnelSelectionWindow(string description) private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) { await channel.SendCTCPMessageAsync( - $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Address}:{e.Tunnel.Port}", + $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Hash}", QueuedMessageType.SYSTEM_MESSAGE, 10); await HandleTunnelServerChangeAsync(e.Tunnel); @@ -1389,6 +1387,9 @@ private async Task ToggleDynamicTunnelsAsync() { await ChangeDynamicTunnelsSettingAsync(!dynamicTunnelsEnabled); await OnGameOptionChangedAsync(); + + if (!dynamicTunnelsEnabled) + await TunnelSelectionWindow_TunnelSelectedAsync(new TunnelEventArgs(initialTunnel)); } /// @@ -1563,20 +1564,18 @@ private async Task ApplyGameOptionsAsync(string sender, string message) bool newDynamicTunnelsSetting = Conversions.BooleanFromString(parts[partIndex + 9], true); if (newDynamicTunnelsSetting != dynamicTunnelsEnabled) - { - if (newDynamicTunnelsSetting) - AddNotice(string.Format("The game host has enabled Dynamic Tunnels".L10N("UI:Main:HostEnableDynamicTunnels"))); - else - AddNotice(string.Format("The game host has disabled Dynamic Tunnels".L10N("UI:Main:HostDisableDynamicTunnels"))); - await ChangeDynamicTunnelsSettingAsync(newDynamicTunnelsSetting); - } } private async Task ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsEnabledValue) { dynamicTunnelsEnabled = newDynamicTunnelsEnabledValue; + if (newDynamicTunnelsEnabledValue) + AddNotice(string.Format("The game host has enabled Dynamic Tunnels".L10N("UI:Main:HostEnableDynamicTunnels"))); + else + AddNotice(string.Format("The game host has disabled Dynamic Tunnels".L10N("UI:Main:HostDisableDynamicTunnels"))); + if (newDynamicTunnelsEnabledValue) { tunnelHandler.CurrentTunnel = tunnelHandler.Tunnels @@ -1970,15 +1969,12 @@ protected override async Task BanPlayerAsync(int playerIndex) private void HandleCheatDetectedMessage(string sender) => AddNotice(string.Format("{0} has modified game files during the client session. They are likely attempting to cheat!".L10N("Client:Main:PlayerModifyFileCheat"), sender), Color.Red); - private async Task HandleTunnelServerChangeMessageAsync(string sender, string tunnelAddressAndPort) + private async Task HandleTunnelServerChangeMessageAsync(string sender, string hash) { if (sender != hostName) return; - string[] split = tunnelAddressAndPort.Split(':'); - string tunnelAddress = split[0]; - int tunnelPort = int.Parse(split[1], CultureInfo.InvariantCulture); - CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); + CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)); if (tunnel == null) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 643ca47b4..84da6d9a3 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -98,7 +98,7 @@ private void HandleRefreshedTunnels(List tunnels) if (CurrentTunnel != null) { - CnCNetTunnel updatedTunnel = Tunnels.Find(t => t.Address == CurrentTunnel.Address && t.Port == CurrentTunnel.Port); + CnCNetTunnel updatedTunnel = Tunnels.Find(t => t.Hash.Equals(CurrentTunnel.Hash, StringComparison.OrdinalIgnoreCase)); if (updatedTunnel != null) { @@ -131,7 +131,8 @@ private async Task PingCurrentTunnelAsync(bool checkTunnelList = false) if (checkTunnelList) { - int tunnelIndex = Tunnels.FindIndex(t => t.Address == CurrentTunnel.Address && t.Port == CurrentTunnel.Port); + int tunnelIndex = Tunnels.FindIndex(t => t.Hash.Equals(CurrentTunnel.Hash)); + if (tunnelIndex > -1) DoTunnelPinged(tunnelIndex); } From 6ad7f951a1c815c0eaffdcfedc4e2160bb5ded02 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 2 Dec 2022 14:52:38 +0100 Subject: [PATCH 046/109] Fix LAN lobbies broadcast/discovery with multiple network interfaces --- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 129 ++++++++++----------- 1 file changed, 60 insertions(+), 69 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index db2a65cd2..174009209 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -1,5 +1,18 @@ -using ClientCore; +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ClientCore; using ClientCore.CnCNet5; +using ClientCore.Extensions; using ClientGUI; using DTAClient.Domain; using DTAClient.Domain.LAN; @@ -13,25 +26,13 @@ using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; -using System; -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using ClientCore.Extensions; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Rectangle = Microsoft.Xna.Framework.Rectangle; namespace DTAClient.DXGUI.Multiplayer { - class LANLobby : XNAWindow + internal sealed class LANLobby : XNAWindow { private const double ALIVE_MESSAGE_INTERVAL = 5.0; private const double INACTIVITY_REMOVE_TIME = 10.0; @@ -40,8 +41,8 @@ public LANLobby( WindowManager windowManager, GameCollection gameCollection, MapLoader mapLoader, - DiscordHandler discordHandler - ) : base(windowManager) + DiscordHandler discordHandler) + : base(windowManager) { this.gameCollection = gameCollection; this.mapLoader = mapLoader; @@ -50,52 +51,34 @@ DiscordHandler discordHandler public event EventHandler Exited; - XNAListBox lbPlayerList; - ChatListBox lbChatMessages; - GameListBox lbGameList; - - XNAClientButton btnMainMenu; - XNAClientButton btnNewGame; - XNAClientButton btnJoinGame; - - XNAChatTextBox tbChatInput; - - XNALabel lblColor; - - XNAClientDropDown ddColor; - - LANGameCreationWindow gameCreationWindow; - - LANGameLobby lanGameLobby; - - LANGameLoadingLobby lanGameLoadingLobby; - - Texture2D unknownGameIcon; - - LANColor[] chatColors; - - string localGame; - int localGameIndex; - - GameCollection gameCollection; - + private XNAListBox lbPlayerList; + private ChatListBox lbChatMessages; + private GameListBox lbGameList; + private XNAClientButton btnMainMenu; + private XNAClientButton btnNewGame; + private XNAClientButton btnJoinGame; + private XNAChatTextBox tbChatInput; + private XNALabel lblColor; + private XNAClientDropDown ddColor; + private LANGameCreationWindow gameCreationWindow; + private LANGameLobby lanGameLobby; + private LANGameLoadingLobby lanGameLoadingLobby; + private Texture2D unknownGameIcon; + private LANColor[] chatColors; + private string localGame; + private int localGameIndex; + private GameCollection gameCollection; private List gameModes => mapLoader.GameModes; - - Socket socket; - IPEndPoint endPoint; - Encoding encoding; - - List players = new List(); - - TimeSpan timeSinceAliveMessage = TimeSpan.Zero; - - MapLoader mapLoader; - - DiscordHandler discordHandler; - - bool initSuccess; - + private Socket socket; + private IPEndPoint endPoint; + private Encoding encoding; + private List players = new List(); + private TimeSpan timeSinceAliveMessage = TimeSpan.Zero; + private MapLoader mapLoader; + private DiscordHandler discordHandler; + private bool initSuccess; private CancellationTokenSource cancellationTokenSource; + private IPAddress lanIpV4BroadcastIpAddress; public override void Initialize() { @@ -306,21 +289,29 @@ public async Task OpenAsync() players.Clear(); lbPlayerList.Clear(); lbGameList.ClearGames(); + cancellationTokenSource?.Dispose(); Visible = true; Enabled = true; - - cancellationTokenSource?.Dispose(); cancellationTokenSource = new CancellationTokenSource(); Logger.Log("Creating LAN socket."); + IEnumerable lanIpAddresses = NetworkHelper.GetUniCastIpAddresses(); + UnicastIPAddressInformation lanIpV4Address = lanIpAddresses.FirstOrDefault(q => q.Address.AddressFamily is AddressFamily.InterNetwork); + + lanIpV4BroadcastIpAddress = NetworkHelper.GetIpV4BroadcastAddress(lanIpV4Address); + try { - socket = new Socket(SocketType.Dgram, ProtocolType.Udp); - socket.EnableBroadcast = true; - socket.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_LOBBY_PORT)); - endPoint = new IPEndPoint(IPAddress.Broadcast, ProgramConstants.LAN_LOBBY_PORT); + socket = new Socket(SocketType.Dgram, ProtocolType.Udp) + { + EnableBroadcast = true + }; + + socket.Bind(new IPEndPoint(lanIpV4Address.Address, ProgramConstants.LAN_LOBBY_PORT)); + + endPoint = new IPEndPoint(lanIpV4BroadcastIpAddress, ProgramConstants.LAN_LOBBY_PORT); initSuccess = true; } catch (SocketException ex) @@ -333,13 +324,13 @@ public async Task OpenAsync() lbChatMessages.AddMessage(new ChatMessage(Color.Red, $"Also make sure that no other application is listening to traffic on UDP ports" + $" {ProgramConstants.LAN_LOBBY_PORT} - {ProgramConstants.LAN_INGAME_PORT}.".L10N("Client:Main:SocketFailure3"))); + initSuccess = false; return; } Logger.Log("Starting listener."); ListenAsync(cancellationTokenSource.Token).HandleTask(); - await SendAliveAsync(cancellationTokenSource.Token); } @@ -373,7 +364,7 @@ private async Task ListenAsync(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { - EndPoint ep = new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT); + EndPoint ep = new IPEndPoint(lanIpV4BroadcastIpAddress, ProgramConstants.LAN_LOBBY_PORT); Memory buffer = memoryOwner.Memory[..4096]; SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep, cancellationToken); var iep = (IPEndPoint)socketReceiveFromResult.RemoteEndPoint; @@ -639,7 +630,7 @@ public override void Update(GameTime gameTime) timeSinceAliveMessage += gameTime.ElapsedGameTime; if (timeSinceAliveMessage > TimeSpan.FromSeconds(ALIVE_MESSAGE_INTERVAL)) - Task.Run(() => SendAliveAsync(cancellationTokenSource?.Token ?? default).HandleTaskAsync()).Wait(); + Task.Run(() => SendAliveAsync(cancellationTokenSource?.Token ?? default).HandleTask()).Wait(); base.Update(gameTime); } From 3946539cc98d15e1478e9d68cab2d349a1bc9a67 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 3 Dec 2022 17:11:08 +0100 Subject: [PATCH 047/109] Support P2P and UPnP port forwarding --- BuildScripts/Common.ps1 | 4 +- ClientCore/Extensions/TaskExtensions.cs | 93 ++- .../PreprocessorBackgroundTask.cs | 2 +- DXMainClient/DXGUI/Generic/LoadingScreen.cs | 11 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 3 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 4 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 566 +++++++++++++----- .../Multiplayer/GameLobby/GameLobbyBase.cs | 7 +- .../Multiplayer/GameLobby/LANGameLobby.cs | 20 +- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 32 +- DXMainClient/DXMainClient.csproj | 1 + .../Multiplayer/CnCNet/CnCNetLobbyCommands.cs | 1 + .../CnCNet/GameDataReceivedEventArgs.cs | 16 + .../UPNP/Models/AddAnyPortMappingRequest.cs | 14 + .../UPNP/Models/AddAnyPortMappingResponse.cs | 7 + .../CnCNet/UPNP/Models/AddPinholeRequest.cs | 12 + .../CnCNet/UPNP/Models/AddPinholeResponse.cs | 7 + .../UPNP/Models/AddPortMappingRequest.cs | 14 + .../UPNP/Models/AddPortMappingResponse.cs | 6 + .../CnCNet/UPNP/Models/AddressType.cs | 12 + .../UPNP/Models/DeletePinholeRequest.cs | 7 + .../UPNP/Models/DeletePinholeResponse.cs | 6 + .../UPNP/Models/DeletePortMappingRequestV1.cs | 9 + .../UPNP/Models/DeletePortMappingRequestV2.cs | 9 + .../Models/DeletePortMappingResponseV1.cs | 6 + .../Models/DeletePortMappingResponseV2.cs | 6 + .../Multiplayer/CnCNet/UPNP/Models/Device.cs | 20 + .../Models/GetExternalIPAddressRequestV1.cs | 6 + .../Models/GetExternalIPAddressRequestV2.cs | 6 + .../Models/GetExternalIPAddressResponseV1.cs | 7 + .../Models/GetExternalIPAddressResponseV2.cs | 7 + .../UPNP/Models/GetFirewallStatusRequest.cs | 6 + .../UPNP/Models/GetFirewallStatusResponse.cs | 8 + .../UPNP/Models/GetNatRsipStatusRequestV1.cs | 6 + .../UPNP/Models/GetNatRsipStatusRequestV2.cs | 6 + .../UPNP/Models/GetNatRsipStatusResponseV1.cs | 8 + .../UPNP/Models/GetNatRsipStatusResponseV2.cs | 8 + .../CnCNet/UPNP/Models/IconListItem.cs | 11 + .../UPNP/Models/InternetGatewayDevice.cs | 301 ++++++++++ .../Models/InternetGatewayDeviceResponse.cs | 5 + .../CnCNet/UPNP/Models/ServiceListItem.cs | 11 + .../CnCNet/UPNP/Models/SpecVersion.cs | 8 + .../CnCNet/UPNP/Models/SystemVersion.cs | 12 + .../CnCNet/UPNP/Models/UPnPDescription.cs | 9 + .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 304 ++++++++++ .../Multiplayer/CnCNet/V3GameTunnelHandler.cs | 146 ++--- .../CnCNet/V3LocalPlayerConnection.cs | 119 ++++ .../CnCNet/V3RemotePlayerConnection.cs | 226 +++++++ .../Multiplayer/CnCNet/V3TunnelConnection.cs | 167 ------ .../CnCNet/V3TunneledPlayerConnection.cs | 113 ---- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 8 +- DXMainClient/Domain/Multiplayer/MapLoader.cs | 2 +- .../Domain/Multiplayer/NetworkHelper.cs | 97 +++ DXMainClient/Domain/Multiplayer/PlayerInfo.cs | 3 +- DXMainClient/Online/Connection.cs | 66 +- DXMainClient/Startup.cs | 18 +- build/AfterPublish.targets | 6 + 57 files changed, 1973 insertions(+), 622 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/GameDataReceivedEventArgs.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingResponse.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeResponse.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingResponse.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeRequest.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeResponse.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV1.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV2.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV1.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV2.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Device.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV1.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV2.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV1.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV2.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusRequest.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusResponse.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV1.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV2.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV1.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV2.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/IconListItem.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/ServiceListItem.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SpecVersion.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SystemVersion.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPDescription.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs delete mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs delete mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3TunneledPlayerConnection.cs create mode 100644 DXMainClient/Domain/Multiplayer/NetworkHelper.cs diff --git a/BuildScripts/Common.ps1 b/BuildScripts/Common.ps1 index 1396219bd..d8f70aae2 100644 --- a/BuildScripts/Common.ps1 +++ b/BuildScripts/Common.ps1 @@ -14,10 +14,10 @@ $EngineMap = @{ function Build-Project($Configuration, $Game, $Engine, $Framework) { $Output = Join-Path $CompiledRoot $Game $Output Resources Binaries ($EngineMap[$Engine]) if ($Engine -EQ 'WindowsXNA') { - dotnet publish $ProjectPath --configuration=$Configuration -property:GAME=$Game -property:ENGINE=$Engine --framework=$Framework --output=$Output --arch=x86 + dotnet publish $ProjectPath -c $Configuration -p:GAME=$Game -p:ENGINE=$Engine -f $Framework -o $Output -p:SatelliteResourceLanguages=en -a x86 } else { - dotnet publish $ProjectPath --configuration=$Configuration -property:GAME=$Game -property:ENGINE=$Engine --framework=$Framework --output=$Output + dotnet publish $ProjectPath -c $Configuration -p:GAME=$Game -p:ENGINE=$Engine -f $Framework -o $Output -p:SatelliteResourceLanguages=en } if ($LASTEXITCODE) { throw "Build failed for $Game $Engine $Framework $Configuration" diff --git a/ClientCore/Extensions/TaskExtensions.cs b/ClientCore/Extensions/TaskExtensions.cs index 6ad4f6c6e..21571d544 100644 --- a/ClientCore/Extensions/TaskExtensions.cs +++ b/ClientCore/Extensions/TaskExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace ClientCore.Extensions; @@ -10,7 +11,7 @@ public static class TaskExtensions /// /// The who's exceptions will be handled. /// Returns a that awaited and handled the original . - public static async Task HandleTaskAsync(this Task task) + public static async Task HandleTask(this Task task) { try { @@ -28,7 +29,7 @@ public static async Task HandleTaskAsync(this Task task) /// The type of 's return value. /// The who's exceptions will be handled. /// Returns a that awaited and handled the original . - public static async Task HandleTaskAsync(this Task task) + public static async Task HandleTask(this Task task) { try { @@ -43,12 +44,86 @@ public static async Task HandleTaskAsync(this Task task) } /// - /// Runs a and guarantees all exceptions are caught and handled even when the is not directly awaited. - /// Use this for 'fire and forget' tasks. + /// Executes a list of tasks and waits for all of them to complete and throws an containing all exceptions from all tasks. + /// When using only the first thrown exception from a single may be observed. /// - /// The who's exceptions will be handled. - public static void HandleTask(this Task task) -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - => task.HandleTaskAsync(); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + /// The type of 's return value. + /// The list of s who's exceptions will be handled. + /// Returns a that awaited and handled the original . + public static async Task WhenAllSafe(IEnumerable> tasks) + { + var whenAllTask = Task.WhenAll(tasks); + + try + { + return await whenAllTask; + } + catch + { + if (whenAllTask.Exception is null) + throw; + + throw whenAllTask.Exception; + } + } + + /// + /// Executes a list of tasks and waits for all of them to complete and throws an containing all exceptions from all tasks. + /// When using only the first thrown exception from a single may be observed. + /// + /// The list of s who's exceptions will be handled. + /// Returns a that awaited and handled the original . + public static async Task WhenAllSafe(IEnumerable tasks) + { + var whenAllTask = Task.WhenAll(tasks); + + try + { + await whenAllTask; + } + catch + { + if (whenAllTask.Exception is null) + throw; + + throw whenAllTask.Exception; + } + } + + /// + /// Runs a and guarantees all exceptions are caught and handled even when the is not directly awaited. + /// + /// The who's exceptions will be handled. + /// Returns a that awaited and handled the original . + public static async ValueTask HandleTask(this ValueTask task) + { + try + { + await task; + } + catch (Exception ex) + { + ProgramConstants.HandleException(ex); + } + } + + /// + /// Runs a and guarantees all exceptions are caught and handled even when the is not directly awaited. + /// + /// The type of 's return value. + /// The who's exceptions will be handled. + /// Returns a that awaited and handled the original . + public static async ValueTask HandleTask(this ValueTask task) + { + try + { + return await task; + } + catch (Exception ex) + { + ProgramConstants.HandleException(ex); + } + + return default; + } } \ No newline at end of file diff --git a/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs b/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs index 127c56f08..cbed66aa3 100644 --- a/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs +++ b/ClientCore/INIProcessing/PreprocessorBackgroundTask.cs @@ -34,7 +34,7 @@ public static PreprocessorBackgroundTask Instance public void Run() { - task = Task.Run(CheckFiles).HandleTaskAsync(); + task = Task.Run(CheckFiles).HandleTask(); } private static void CheckFiles() diff --git a/DXMainClient/DXGUI/Generic/LoadingScreen.cs b/DXMainClient/DXGUI/Generic/LoadingScreen.cs index 340e9c0d2..bc81ce424 100644 --- a/DXMainClient/DXGUI/Generic/LoadingScreen.cs +++ b/DXMainClient/DXGUI/Generic/LoadingScreen.cs @@ -6,9 +6,6 @@ using ClientGUI; using ClientUpdater; using DTAClient.Domain.Multiplayer; -using DTAClient.DXGUI.Multiplayer; -using DTAClient.DXGUI.Multiplayer.CnCNet; -using DTAClient.DXGUI.Multiplayer.GameLobby; using DTAClient.Online; using Microsoft.Extensions.DependencyInjection; using Microsoft.Xna.Framework; @@ -32,11 +29,7 @@ MapLoader mapLoader } private MapLoader mapLoader; - - private PrivateMessagingPanel privateMessagingPanel; - private bool visibleSpriteCursor; - private Task updaterInitTask; private Task mapLoadTask; private readonly CnCNetManager cncnetManager; @@ -56,9 +49,9 @@ public override void Initialize() bool initUpdater = !ClientConfiguration.Instance.ModMode; if (initUpdater) - updaterInitTask = Task.Run(InitUpdater).HandleTaskAsync(); + updaterInitTask = Task.Run(InitUpdater).HandleTask(); - mapLoadTask = mapLoader.LoadMapsAsync().HandleTaskAsync(); + mapLoadTask = mapLoader.LoadMapsAsync().HandleTask(); if (Cursor.Visible) { diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index ef11dff9f..68d39e6ee 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -15,6 +15,7 @@ using Rampastring.XNAUI.XNAControls; using System; using System.Collections.Generic; +using System.Net; using System.Text; using System.Threading.Tasks; using ClientCore.Extensions; @@ -595,7 +596,7 @@ protected override async Task HostStartGameAsync() Players[pId].Port = playerPorts[pId]; sb.Append(Players[pId].Name); sb.Append(";"); - sb.Append("0.0.0.0:"); + sb.Append($"{IPAddress.Any}:"); sb.Append(playerPorts[pId]); sb.Append(";"); } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index 3bcdf916d..bb08aa437 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -321,7 +321,7 @@ protected void LoadGame() if (otherPlayer == null) continue; - spawnIni.SetStringValue("Other" + i, "Ip", otherPlayer.IPAddress); + spawnIni.SetStringValue("Other" + i, "Ip", otherPlayer.IPAddress.ToString()); spawnIni.SetIntValue("Other" + i, "Port", otherPlayer.Port); } @@ -331,7 +331,7 @@ protected void LoadGame() FileInfo spawnMapFileInfo = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI); spawnMapFileInfo.Delete(); - using StreamWriter spawnMapStreamWriter = new StreamWriter(spawnMapFileInfo.FullName); + using var spawnMapStreamWriter = new StreamWriter(spawnMapFileInfo.FullName); spawnMapStreamWriter.WriteLine("[Map]"); spawnMapStreamWriter.WriteLine("Size=0,0,50,50"); spawnMapStreamWriter.WriteLine("LocalSize=0,0,50,50"); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index a9f20b6af..033f83a01 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -4,7 +4,10 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; using System.Text; +using System.Threading; using System.Threading.Tasks; using ClientCore; using ClientCore.CnCNet5; @@ -35,6 +38,7 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private const double MAX_TIME_FOR_GAME_LAUNCH = 20.0; private const int PRIORITY_START_GAME = 10; private const int PINNED_DYNAMIC_TUNNELS = 10; + private const int P2P_PING_TIMEOUT = 1000; private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; @@ -45,12 +49,12 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private readonly GameCollection gameCollection; private readonly CnCNetUserData cncnetUserData; private readonly PrivateMessagingWindow pmWindow; - private readonly List tunnelPlayerIds = new(); + private readonly List gamePlayerIds = new(); private readonly List hostUploadedMaps = new(); private readonly List chatCommandDownloadedMaps = new(); - private readonly List<(string Name, CnCNetTunnel Tunnel)> playerTunnels = new(); - private readonly List<(string Sender, string TunnelPingsMessage)> tunnelPingsMessages = new(); - private readonly List<(List Names, V3GameTunnelHandler Tunnel)> dynamicV3GameTunnelHandlers = new(); + private readonly List<(string RemotePlayerName, CnCNetTunnel Tunnel, int CombinedPing)> playerTunnels = new(); + private readonly List<(List RemotePlayerNames, V3GameTunnelHandler Tunnel)> v3GameTunnelHandlers = new(); + private readonly List<(string RemotePlayerName, ushort[] RemotePorts, List<(IPAddress RemoteIpAddress, long Ping)> LocalPingResults, List<(IPAddress RemoteIpAddress, long Ping)> RemotePingResults, bool Enabled)> p2pPlayers = new(); private TunnelSelectionWindow tunnelSelectionWindow; private XNAClientButton btnChangeTunnel; @@ -63,11 +67,16 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private int playerLimit; private bool closed; private bool isCustomPassword; - private bool[] isPlayerConnectedToTunnel; + private bool[] isPlayerConnected; private bool isStartingGame; private string gameFilesHash; private MapSharingConfirmationPanel mapSharingConfirmationPanel; private CnCNetTunnel initialTunnel; + private IPAddress publicIpV4Address; + private IPAddress publicIpV6Address; + private List p2pPorts = new(); + private List p2pIpV6PortIds = new(); + private CancellationTokenSource gameStartCancellationTokenSource; /// /// The SHA1 of the latest selected map. @@ -97,6 +106,8 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private List<(int Ping, string Hash)> pinnedTunnels; private string pinnedTunnelPingsMessage; private bool dynamicTunnelsEnabled; + private bool p2pEnabled; + private InternetGatewayDevice internetGatewayDevice; public CnCNetGameLobby( WindowManager windowManager, @@ -123,9 +134,9 @@ public CnCNetGameLobby( new IntCommandHandler(CnCNetCommands.READY_REQUEST, (playerName, options) => HandleReadyRequestAsync(playerName, options).HandleTask()), new StringCommandHandler(CnCNetCommands.PLAYER_OPTIONS, ApplyPlayerOptions), new StringCommandHandler(CnCNetCommands.PLAYER_EXTRA_OPTIONS, ApplyPlayerExtraOptions), - new StringCommandHandler(CnCNetCommands.GAME_OPTIONS, (sender, message) => ApplyGameOptionsAsync(sender, message).HandleTask()), - new StringCommandHandler(CnCNetCommands.GAME_START_V2, (sender, message) => NonHostLaunchGameAsync(sender, message).HandleTask()), - new StringCommandHandler(CnCNetCommands.GAME_START_V3, HandleGameStartV3TunnelMessage), + new StringCommandHandler(CnCNetCommands.GAME_OPTIONS, (playerName, message) => ApplyGameOptionsAsync(playerName, message).HandleTask()), + new StringCommandHandler(CnCNetCommands.GAME_START_V2, (playerName, message) => ClientLaunchGameV2Async(playerName, message).HandleTask()), + new StringCommandHandler(CnCNetCommands.GAME_START_V3, ClientLaunchGameV3Async), new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_OK, playerName => HandlePlayerConnectedToTunnelAsync(playerName).HandleTask()), new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_FAIL, HandleTunnelFail), new NotificationHandler(CnCNetCommands.AI_SPECTATORS, HandleNotification, () => AISpectatorsNotificationAsync().HandleTask()), @@ -142,13 +153,15 @@ public CnCNetGameLobby( new StringCommandHandler(CnCNetCommands.MAP_SHARING_DOWNLOAD, HandleMapDownloadRequest), new NoParamCommandHandler(CnCNetCommands.MAP_SHARING_DISABLED, HandleMapSharingBlockedMessage), new NoParamCommandHandler(CnCNetCommands.RETURN, ReturnNotification), - new StringCommandHandler(CnCNetCommands.FILE_HASH, (sender, filesHash) => FileHashNotificationAsync(sender, filesHash).HandleTask()), + new StringCommandHandler(CnCNetCommands.FILE_HASH, (playerName, filesHash) => FileHashNotificationAsync(playerName, filesHash).HandleTask()), new StringCommandHandler(CnCNetCommands.CHEATER, CheaterNotification), new StringCommandHandler(CnCNetCommands.DICE_ROLL, HandleDiceRollResult), new NoParamCommandHandler(CnCNetCommands.CHEAT_DETECTED, HandleCheatDetectedMessage), new IntCommandHandler(CnCNetCommands.TUNNEL_PING, HandleTunnelPing), - new StringCommandHandler(CnCNetCommands.CHANGE_TUNNEL_SERVER, (sender, hash) => HandleTunnelServerChangeMessageAsync(sender, hash).HandleTask()), - new StringCommandHandler(CnCNetCommands.PLAYER_TUNNEL_PINGS, HandleTunnelPingsMessage) + new StringCommandHandler(CnCNetCommands.CHANGE_TUNNEL_SERVER, (playerName, hash) => HandleTunnelServerChangeMessageAsync(playerName, hash).HandleTask()), + new StringCommandHandler(CnCNetCommands.PLAYER_TUNNEL_PINGS, HandleTunnelPingsMessage), + new StringCommandHandler(CnCNetCommands.PLAYER_P2P_REQUEST, (playerName, p2pRequestMessage) => HandleP2PRequestMessageAsync(playerName, p2pRequestMessage).HandleTask()), + new StringCommandHandler(CnCNetCommands.PLAYER_P2P_PINGS, HandleP2PPingsMessage) }; MapSharer.MapDownloadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapDownloadFailedAsync(e).HandleTask()); @@ -173,9 +186,14 @@ public CnCNetGameLobby( DownloadMapByIdCommand)); AddChatBoxCommand(new( CnCNetLobbyCommands.DYNAMICTUNNELS, - "Toggle dynamic CnCNet tunnel servers on/off (game host only)".L10N("UI:Main:ChangeDynamicTunnels"), + "Toggle dynamic CnCNet tunnel servers on/off (game host only)".L10N("Client:Main:ChangeDynamicTunnels"), true, _ => ToggleDynamicTunnelsAsync().HandleTask())); + AddChatBoxCommand(new( + CnCNetLobbyCommands.P2P, + "Toggle P2P connections on/off".L10N("Client:Main:ChangeP2P"), + false, + _ => ToggleP2PAsync().HandleTask())); } public event EventHandler GameLeft; @@ -251,7 +269,7 @@ private void GameStartTimer_TimeElapsed(object sender, EventArgs e) for (int i = 0; i < Players.Count; i++) { - if (!isPlayerConnectedToTunnel[i]) + if (!isPlayerConnected[i]) { if (playerString == string.Empty) playerString = Players[i].Name; @@ -260,7 +278,7 @@ private void GameStartTimer_TimeElapsed(object sender, EventArgs e) } } - AddNotice($"Some players ({playerString}) failed to connect within the time limit. Aborting game launch."); + AddNotice(string.Format(CultureInfo.InvariantCulture, "Some players ({0}) failed to connect within the time limit. Aborting game launch.", playerString)); AbortGameStart(); } @@ -275,7 +293,8 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick GetCursorPoint()); } - private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")); + private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) + => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")); public async Task SetUpAsync( Channel channel, @@ -290,6 +309,7 @@ public async Task SetUpAsync( this.playerLimit = playerLimit; this.isCustomPassword = isCustomPassword; dynamicTunnelsEnabled = UserINISettings.Instance.UseDynamicTunnels; + p2pEnabled = UserINISettings.Instance.UseP2P; channel.MessageAdded += Channel_MessageAdded; channel.CTCPReceived += Channel_CTCPReceived; channel.UserKicked += channel_UserKickedFunc; @@ -375,16 +395,10 @@ await connectionManager.SendCustomMessageAsync(new( await channel.SendCTCPMessageAsync(CnCNetCommands.FILE_HASH + " " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); if (dynamicTunnelsEnabled) - await BroadcastPlayerTunnelPingsAsync(); + BroadcastPlayerTunnelPingsAsync().HandleTask(); - if (UserINISettings.Instance.UseP2P) - { - // todo broadcast IPs so others can ping - // await channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_REQUEST + " " + x, QueuedMessageType.SYSTEM_MESSAGE, 10); - - // todo ping other players, if both sides can ping each other, add p2p ping as extra result to tunnel ping list - // await channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_PINGS + " " + x, QueuedMessageType.SYSTEM_MESSAGE, 10); - } + if (p2pEnabled) + BroadcastPlayerP2PRequestAsync().HandleTask(); } TopBar.AddPrimarySwitchable(this); @@ -400,7 +414,7 @@ private async Task UpdatePingAsync() int ping; if (dynamicTunnelsEnabled) - ping = pinnedTunnels.Min(q => q.Ping); + ping = pinnedTunnels?.Min(q => q.Ping) ?? -1; else if (tunnelHandler.CurrentTunnel == null) return; else @@ -441,8 +455,9 @@ private void PrintTunnelServerInformation(string s) } else { - AddNotice(string.Format("Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})".L10N("Client:Main:TunnelInfo"), - tunnelHandler.CurrentTunnel.Name, tunnelHandler.CurrentTunnel.Country, tunnelHandler.CurrentTunnel.Clients, tunnelHandler.CurrentTunnel.MaxClients, tunnelHandler.CurrentTunnel.Official)); + AddNotice(string.Format(CultureInfo.CurrentCulture, + "Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})".L10N("Client:Main:TunnelInfo"), + tunnelHandler.CurrentTunnel.Name, tunnelHandler.CurrentTunnel.Country, tunnelHandler.CurrentTunnel.Clients, tunnelHandler.CurrentTunnel.MaxClients, tunnelHandler.CurrentTunnel.Official)); } } @@ -486,28 +501,62 @@ public override async Task ClearAsync() } Disable(); + connectionManager.ConnectionLost -= connectionManager_ConnectionLostFunc; connectionManager.Disconnected -= connectionManager_DisconnectedFunc; - gameBroadcastTimer.Enabled = false; closed = false; - tbChatInput.Text = string.Empty; - - tunnelHandler.CurrentTunnel = null; tunnelHandler.CurrentTunnelPinged -= tunnelHandler_CurrentTunnelFunc; + tunnelHandler.CurrentTunnel = null; + pinnedTunnelPingsMessage = null; + gameStartCancellationTokenSource?.Cancel(); + v3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); + v3GameTunnelHandlers.Clear(); playerTunnels.Clear(); - tunnelPlayerIds.Clear(); - dynamicV3GameTunnelHandlers.Clear(); + gamePlayerIds.Clear(); pinnedTunnels.Clear(); - tunnelPingsMessages.Clear(); - pinnedTunnelPingsMessage = null; - + p2pPlayers.Clear(); GameLeft?.Invoke(this, EventArgs.Empty); - TopBar.RemovePrimarySwitchable(this); ResetDiscordPresence(); + CloseP2PPortsAsync().HandleTask(); + } + + private async Task CloseP2PPortsAsync() + { + try + { + foreach (ushort p2pPort in p2pPorts) + { + await internetGatewayDevice.CloseIpV4PortAsync(p2pPort); + } + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, "Could not close P2P IPV4 ports."); + } + finally + { + p2pPorts.Clear(); + } + + try + { + foreach (ushort p2pIpV6PortId in p2pIpV6PortIds) + { + await internetGatewayDevice.CloseIpV6PortAsync(p2pIpV6PortId); + } + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, "Could not close P2P IPV6 ports."); + } + finally + { + p2pIpV6PortIds.Clear(); + } } public async Task LeaveGameLobbyAsync() @@ -532,7 +581,7 @@ private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) { Logger.Log("CnCNetGameLobby: Nickname change: " + e.OldUserName + " to " + e.User.Name); - int index = Players.FindIndex(p => p.Name == e.OldUserName); + int index = Players.FindIndex(p => p.Name.Equals(e.OldUserName, StringComparison.OrdinalIgnoreCase)); if (index > -1) { @@ -541,7 +590,7 @@ private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) player.Name = e.User.Name; ddPlayerNames[index].Items[0].Text = player.Name; - AddNotice(string.Format("Player {0} changed their name to {1}".L10N("Client:Main:PlayerRename"), e.OldUserName, e.User.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} changed their name to {1}".L10N("Client:Main:PlayerRename"), e.OldUserName, e.User.Name)); } } @@ -583,7 +632,7 @@ private async Task ChannelUserLeftAsync(UserNameEventArgs e) { await RemovePlayerAsync(e.UserName); - if (e.UserName == hostName) + if (e.UserName.Equals(hostName, StringComparison.OrdinalIgnoreCase)) { connectionManager.MainChannel.AddMessage( new(ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); @@ -608,7 +657,7 @@ private async Task Channel_UserKickedAsync(UserNameEventArgs e) return; } - int index = Players.FindIndex(p => p.Name == e.UserName); + int index = Players.FindIndex(p => p.Name.Equals(e.UserName, StringComparison.OrdinalIgnoreCase)); if (index > -1) { @@ -617,13 +666,10 @@ private async Task Channel_UserKickedAsync(UserNameEventArgs e) UpdateDiscordPresence(); ClearReadyStatuses(); - (string Name, CnCNetTunnel Tunnel) playerTunnel = playerTunnels.SingleOrDefault(q => q.Name.Equals(e.UserName, StringComparison.OrdinalIgnoreCase)); + (string Name, CnCNetTunnel Tunnel, int CombinedPing) playerTunnel = playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(e.UserName, StringComparison.OrdinalIgnoreCase)); if (playerTunnel.Name is not null) playerTunnels.Remove(playerTunnel); - - tunnelPlayerIds.Clear(); - dynamicV3GameTunnelHandlers.Clear(); } } @@ -631,7 +677,7 @@ private async Task Channel_UserListReceivedAsync() { if (!IsHost) { - if (channel.Users.Find(hostName) == null) + if (channel.Users.Find(hostName) is null) { connectionManager.MainChannel.AddMessage( new(ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); @@ -652,7 +698,10 @@ private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) AIPlayers.RemoveAt(AIPlayers.Count - 1); if (dynamicTunnelsEnabled && pInfo != FindLocalPlayer()) - await BroadcastPlayerTunnelPingsAsync(); + BroadcastPlayerTunnelPingsAsync().HandleTask(); + + if (p2pEnabled && pInfo != FindLocalPlayer()) + BroadcastPlayerP2PRequestAsync().HandleTask(); sndJoinSound.Play(); #if WINFORMS @@ -691,21 +740,18 @@ private async Task RemovePlayerAsync(string playerName) { AbortGameStart(); - PlayerInfo pInfo = Players.Find(p => p.Name == playerName); + PlayerInfo pInfo = Players.Find(p => p.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)); if (pInfo != null) { Players.Remove(pInfo); CopyPlayerDataToUI(); - (string Name, CnCNetTunnel Tunnel) playerTunnel = playerTunnels.SingleOrDefault(q => q.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)); + (string Name, CnCNetTunnel Tunnel, int CombinedPing) playerTunnel = playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); if (playerTunnel.Name is not null) playerTunnels.Remove(playerTunnel); - tunnelPlayerIds.Clear(); - dynamicV3GameTunnelHandlers.Clear(); - // This might not be necessary if (IsHost) await BroadcastPlayerOptionsAsync(); @@ -775,14 +821,12 @@ protected override async Task HostLaunchGameAsync() { if (Players.Count > 1) { - AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); + AddNotice("Contacting remote hosts...".L10N("Client:Main:ConnectingTunnel")); if (tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_2) - await HostLaunchGameV2TunnelAsync(); - else if (tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_3) - await HostLaunchGameV3TunnelAsync(); - else if (dynamicTunnelsEnabled) - await HostLaunchGameV3TunnelAsync(); + await HostLaunchGameV2Async(); + else if (dynamicTunnelsEnabled || tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_3) + await HostLaunchGameV3Async(); else throw new InvalidOperationException("Unknown tunnel server version!"); @@ -797,7 +841,7 @@ protected override async Task HostLaunchGameAsync() await StartGameAsync(); } - private async Task HostLaunchGameV2TunnelAsync() + private async Task HostLaunchGameV2Async() { List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(Players.Count); @@ -805,14 +849,23 @@ private async Task HostLaunchGameV2TunnelAsync() { ShowTunnelSelectionWindow(("An error occured while contacting " + "the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); - AddNotice(("An error occured while contacting the specified CnCNet " + + AddNotice(string.Format(CultureInfo.InvariantCulture, "An error occured while contacting the specified CnCNet " + "tunnel server. Please try using a different tunnel server " + - "(accessible by typing /CHANGETUNNEL in the chat box).").L10N("Client:Main:ConnectTunnelError2"), + "(accessible by typing /{0} in the chat box).".L10N("Client:Main:ConnectTunnelError2"), CnCNetLobbyCommands.CHANGETUNNEL), ERROR_MESSAGE_COLOR); return; } - StringBuilder sb = new StringBuilder(CnCNetCommands.GAME_START_V2).Append(' ').Append(UniqueGameID); + string playerPortsV2String = SetGamePlayerPortsV2(playerPorts); + + await channel.SendCTCPMessageAsync($"{CnCNetCommands.GAME_START_V2} {UniqueGameID} {playerPortsV2String}", QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + Players.ForEach(pInfo => pInfo.IsInGame = true); + await StartGameAsync(); + } + + private string SetGamePlayerPortsV2(IReadOnlyList playerPorts) + { + var sb = new StringBuilder(); for (int pId = 0; pId < Players.Count; pId++) { @@ -821,24 +874,33 @@ private async Task HostLaunchGameV2TunnelAsync() sb.Append(';') .Append(Players[pId].Name) .Append(';') - .Append("0.0.0.0:") + .Append($"{IPAddress.Any}:") .Append(playerPorts[pId]); } - await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); - Players.ForEach(pInfo => pInfo.IsInGame = true); - await StartGameAsync(); + return sb.ToString(); } - private async Task HostLaunchGameV3TunnelAsync() + private async Task HostLaunchGameV3Async() { btnLaunchGame.InputEnabled = false; + string gamePlayerIdsString = HostGenerateGamePlayerIds(); + + await channel.SendCTCPMessageAsync($"{CnCNetCommands.GAME_START_V3} {UniqueGameID}{gamePlayerIdsString}", QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + + isStartingGame = true; + + StartV3ConnectionListeners(); + } + + private string HostGenerateGamePlayerIds() + { var random = new Random(); uint randomNumber = (uint)random.Next(0, int.MaxValue - (MAX_PLAYER_COUNT / 2)) * (uint)random.Next(1, 3); - StringBuilder sb = new StringBuilder(CnCNetCommands.GAME_START_V3).Append(' ').Append(UniqueGameID); + var sb = new StringBuilder(); - tunnelPlayerIds.Clear(); + gamePlayerIds.Clear(); for (int i = 0; i < Players.Count; i++) { @@ -846,19 +908,15 @@ private async Task HostLaunchGameV3TunnelAsync() sb.Append(';') .Append(id); - tunnelPlayerIds.Add(id); + gamePlayerIds.Add(id); } - await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); - - isStartingGame = true; - - ContactTunnel(); + return sb.ToString(); } - private void HandleGameStartV3TunnelMessage(string sender, string message) + private void ClientLaunchGameV3Async(string sender, string message) { - if (sender != hostName) + if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; string[] parts = message.Split(';'); @@ -871,52 +929,82 @@ private void HandleGameStartV3TunnelMessage(string sender, string message) if (UniqueGameID < 0) return; - tunnelPlayerIds.Clear(); + gamePlayerIds.Clear(); for (int i = 1; i < parts.Length; i++) { if (!uint.TryParse(parts[i], out uint id)) return; - tunnelPlayerIds.Add(id); + gamePlayerIds.Add(id); } isStartingGame = true; - ContactTunnel(); + StartV3ConnectionListeners(); } - private void ContactTunnel() + private void StartV3ConnectionListeners() { - isPlayerConnectedToTunnel = new bool[Players.Count]; + isPlayerConnected = new bool[Players.Count]; + + uint gameLocalPlayerId = gamePlayerIds[Players.FindIndex(p => p == FindLocalPlayer())]; - uint localId = tunnelPlayerIds[Players.FindIndex(p => p == FindLocalPlayer())]; + v3GameTunnelHandlers.Clear(); + gameStartCancellationTokenSource?.Dispose(); - dynamicV3GameTunnelHandlers.Clear(); + gameStartCancellationTokenSource = new CancellationTokenSource(); if (!dynamicTunnelsEnabled) { var gameTunnelHandler = new V3GameTunnelHandler(); - gameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - gameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + gameTunnelHandler.RaiseConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + gameTunnelHandler.RaiseConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - gameTunnelHandler.SetUp(tunnelHandler.CurrentTunnel, localId); + gameTunnelHandler.SetUp(new IPEndPoint(tunnelHandler.CurrentTunnel.IPAddress, tunnelHandler.CurrentTunnel.Port), 0, gameLocalPlayerId, gameStartCancellationTokenSource.Token); gameTunnelHandler.ConnectToTunnel(); - dynamicV3GameTunnelHandlers.Add(new(Players.Where(q => q != FindLocalPlayer()).Select(q => q.Name).ToList(), gameTunnelHandler)); + v3GameTunnelHandlers.Add(new(Players.Where(q => q != FindLocalPlayer()).Select(q => q.Name).ToList(), gameTunnelHandler)); } else { - foreach (IGrouping tunnelGrouping in playerTunnels.GroupBy(q => q.Tunnel)) + List p2pPlayerTunnels = new(); + + if (p2pEnabled) + { + foreach (var (remotePlayerName, remotePorts, localPingResults, remotePingResults, _) in p2pPlayers.Where(q => q.RemotePingResults.Any() && q.Enabled)) + { + IEnumerable<(IPAddress IpAddress, long CombinedPing)> combinedPingResults = localPingResults.Select(q => (q.RemoteIpAddress, q.Ping + remotePingResults.SingleOrDefault(r => r.RemoteIpAddress.Equals(q.RemoteIpAddress)).Ping)); + (IPAddress ipAddress, long combinedPing) = combinedPingResults.OrderByDescending(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).MinBy(q => q.CombinedPing); + + if (combinedPing < playerTunnels.Single(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).CombinedPing) + { + int index = Players.Where(q => q != FindLocalPlayer()).OrderBy(q => q.Name).ToList().FindIndex(q => q.Name.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)); + ushort localPort = p2pPorts[6 - index]; + ushort remotePort = remotePorts[6 - index]; + var p2pLocalTunnelHandler = new V3GameTunnelHandler(); + + p2pLocalTunnelHandler.RaiseConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + p2pLocalTunnelHandler.RaiseConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + + p2pLocalTunnelHandler.SetUp(new IPEndPoint(ipAddress, remotePort), localPort, gameLocalPlayerId, gameStartCancellationTokenSource.Token); + p2pLocalTunnelHandler.ConnectToTunnel(); + v3GameTunnelHandlers.Add(new(new List { remotePlayerName }, p2pLocalTunnelHandler)); + p2pPlayerTunnels.Add(remotePlayerName); + } + } + } + + foreach (IGrouping tunnelGrouping in playerTunnels.Where(q => !p2pPlayerTunnels.Contains(q.RemotePlayerName, StringComparer.OrdinalIgnoreCase)).GroupBy(q => q.Tunnel)) { var gameTunnelHandler = new V3GameTunnelHandler(); - gameTunnelHandler.Connected += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - gameTunnelHandler.ConnectionFailed += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + gameTunnelHandler.RaiseConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + gameTunnelHandler.RaiseConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - gameTunnelHandler.SetUp(tunnelGrouping.Key, localId); + gameTunnelHandler.SetUp(new IPEndPoint(tunnelGrouping.Key.IPAddress, tunnelGrouping.Key.Port), 0, gameLocalPlayerId, gameStartCancellationTokenSource.Token); gameTunnelHandler.ConnectToTunnel(); - dynamicV3GameTunnelHandlers.Add(new(tunnelGrouping.Select(q => q.Name).ToList(), gameTunnelHandler)); + v3GameTunnelHandlers.Add(new(tunnelGrouping.Select(q => q.Name).ToList(), gameTunnelHandler)); } } @@ -929,17 +1017,22 @@ private async Task GameTunnelHandler_Connected_CallbackAsync() { if (dynamicTunnelsEnabled) { - if (dynamicV3GameTunnelHandlers.Any() && dynamicV3GameTunnelHandlers.All(q => q.Tunnel.IsConnected)) - isPlayerConnectedToTunnel[Players.FindIndex(p => p == FindLocalPlayer())] = true; + if (v3GameTunnelHandlers.Any() && v3GameTunnelHandlers.TrueForAll(q => q.Tunnel.IsConnected)) + SetLocalPlayerConnected(); } else { - isPlayerConnectedToTunnel[Players.FindIndex(p => p == FindLocalPlayer())] = true; + SetLocalPlayerConnected(); } await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_OK, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); } + private void SetLocalPlayerConnected() + { + isPlayerConnected[Players.FindIndex(p => p == FindLocalPlayer())] = true; + } + private async Task GameTunnelHandler_ConnectionFailed_CallbackAsync() { await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_FAIL, QueuedMessageType.INSTANT_MESSAGE, 0); @@ -948,9 +1041,9 @@ private async Task GameTunnelHandler_ConnectionFailed_CallbackAsync() private void HandleTunnelFail(string playerName) { - Logger.Log(playerName + " failed to connect to tunnel - aborting game launch."); - AddNotice(playerName + " failed to connect to the tunnel server. Please " + - "retry or pick another tunnel server by type /CHANGETUNNEL to the chat input box."); + Logger.Log(playerName + " failed to connect - aborting game launch."); + AddNotice(string.Format(CultureInfo.InvariantCulture, "{0} failed to connect. Please retry, disable P2P or pick " + + "another tunnel server by typing /{1} in the chat input box.".L10N("Client:Main:PlayerConnectFailed"), playerName, CnCNetLobbyCommands.CHANGETUNNEL)); AbortGameStart(); } @@ -959,7 +1052,7 @@ private async Task HandlePlayerConnectedToTunnelAsync(string playerName) if (!isStartingGame) return; - int index = Players.FindIndex(p => p.Name == playerName); + int index = Players.FindIndex(p => p.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)); if (index == -1) { @@ -968,25 +1061,25 @@ private async Task HandlePlayerConnectedToTunnelAsync(string playerName) return; } - isPlayerConnectedToTunnel[index] = true; + isPlayerConnected[index] = true; - if (isPlayerConnectedToTunnel.All(b => b)) - await HandleAllPlayersConnectedToTunnelAsync(); + if (isPlayerConnected.All(b => b)) + await LaunchGameV3Async(); } - private async Task HandleAllPlayersConnectedToTunnelAsync() + private async Task LaunchGameV3Async() { - Logger.Log("All players are connected to the tunnel, starting game!"); - AddNotice("All players have connected to the tunnel..."); + Logger.Log("All players are connected, starting game!"); + AddNotice("All players have connected...".L10N("Client:Main:PlayersConnected")); - List playerPorts = new(); + List playerPorts = new(); - foreach (V3GameTunnelHandler dynamicV3GameTunnelHandler in dynamicV3GameTunnelHandlers.Select(q => q.Tunnel)) + foreach (V3GameTunnelHandler dynamicV3GameTunnelHandler in v3GameTunnelHandlers.Select(q => q.Tunnel)) { - var currentTunnelPlayers = Players.Where(q => dynamicV3GameTunnelHandlers.Single(r => r.Tunnel == dynamicV3GameTunnelHandler).Names.Contains(q.Name)).ToList(); + var currentTunnelPlayers = Players.Where(q => v3GameTunnelHandlers.Single(r => r.Tunnel == dynamicV3GameTunnelHandler).RemotePlayerNames.Contains(q.Name)).ToList(); IEnumerable indexes = currentTunnelPlayers.Select(q => q.Index); - var playerIds = indexes.Select(q => tunnelPlayerIds[q]).ToList(); - List createdPlayerPorts = dynamicV3GameTunnelHandler.CreatePlayerConnections(playerIds); + var playerIds = indexes.Select(q => gamePlayerIds[q]).ToList(); + List createdPlayerPorts = dynamicV3GameTunnelHandler.CreatePlayerConnections(playerIds); int i = 0; foreach (PlayerInfo currentTunnelPlayer in currentTunnelPlayers) @@ -997,11 +1090,13 @@ private async Task HandleAllPlayersConnectedToTunnelAsync() playerPorts.AddRange(createdPlayerPorts); } - int gamePort = V3GameTunnelHandler.GetFreePort(playerPorts); + playerPorts.AddRange(p2pPorts); + + ushort gamePort = NetworkHelper.GetFreeUdpPort(playerPorts); - foreach (V3GameTunnelHandler dynamicV3GameTunnelHandler in dynamicV3GameTunnelHandlers.Select(q => q.Tunnel)) + foreach (V3GameTunnelHandler v3GameTunnelHandler in v3GameTunnelHandlers.Select(q => q.Tunnel)) { - dynamicV3GameTunnelHandler.StartPlayerConnections(gamePort); + v3GameTunnelHandler.StartPlayerConnections(gamePort); } FindLocalPlayer().Port = gamePort; @@ -1017,23 +1112,17 @@ private void AbortGameStart() { btnLaunchGame.InputEnabled = true; - foreach (V3GameTunnelHandler dynamicV3GameTunnelHandler in dynamicV3GameTunnelHandlers.Select(q => q.Tunnel)) - { - dynamicV3GameTunnelHandler.Clear(); - } - + gameStartCancellationTokenSource?.Cancel(); + v3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); gameStartTimer.Pause(); isStartingGame = false; } - protected override string GetIPAddressForPlayer(PlayerInfo player) + protected override IPAddress GetIPAddressForPlayer(PlayerInfo player) { - if (UserINISettings.Instance.UseP2P) - return IPAddress.Parse(player.IPAddress).MapToIPv4().ToString(); - - if (dynamicTunnelsEnabled || tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) - return IPAddress.Loopback.MapToIPv4().ToString(); + if (p2pEnabled || dynamicTunnelsEnabled || tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) + return IPAddress.Loopback.MapToIPv4(); return base.GetIPAddressForPlayer(player); } @@ -1230,12 +1319,36 @@ protected override async Task BroadcastPlayerExtraOptionsAsync() private Task BroadcastPlayerTunnelPingsAsync() => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_TUNNEL_PINGS + " " + pinnedTunnelPingsMessage, QueuedMessageType.SYSTEM_MESSAGE, 10); + private async Task BroadcastPlayerP2PRequestAsync() + { + if (!p2pPorts.Any()) + { + try + { + (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync(internetGatewayDevice); + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, "Could not open UPnP P2P ports."); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Could not open UPnP P2P ports".L10N("Client:Main:UPnPP2PFailed"))); + + return; + } + } + + if ((publicIpV4Address is not null || publicIpV6Address is not null) && p2pPorts.Any()) + await SendPlayerP2PRequestAsync(); + } + + private Task SendPlayerP2PRequestAsync() + => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_REQUEST + $" {publicIpV4Address};{publicIpV6Address};{(!p2pPorts.Any() ? null : p2pPorts.Select(q => q.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}", QueuedMessageType.SYSTEM_MESSAGE, 10); + /// /// Handles player option messages received from the game host. /// private void ApplyPlayerOptions(string sender, string message) { - if (sender != hostName) + if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; Players.Clear(); @@ -1392,12 +1505,34 @@ private async Task ToggleDynamicTunnelsAsync() await TunnelSelectionWindow_TunnelSelectedAsync(new TunnelEventArgs(initialTunnel)); } + private async Task ToggleP2PAsync() + { + p2pEnabled = !p2pEnabled; + + if (p2pEnabled) + { + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} enabled P2P".L10N("Client:Main:P2PEnabled"), FindLocalPlayer().Name)); + await BroadcastPlayerP2PRequestAsync(); + + return; + } + + await CloseP2PPortsAsync(); + + internetGatewayDevice = null; + publicIpV4Address = null; + publicIpV6Address = null; + + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled P2P".L10N("Client:Main:P2PDisabled"), FindLocalPlayer().Name)); + await SendPlayerP2PRequestAsync(); + } + /// /// Handles game option messages received from the game host. /// private async Task ApplyGameOptionsAsync(string sender, string message) { - if (sender != hostName) + if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; string[] parts = message.Split(';'); @@ -1421,7 +1556,7 @@ private async Task ApplyGameOptionsAsync(string sender, string message) { FrameSendRate = frameSendRate; - AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has changed FrameSendRate (order lag) to {0}".L10N("Client:Main:HostChangeFrameSendRate"), frameSendRate)); } int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead); @@ -1430,7 +1565,7 @@ private async Task ApplyGameOptionsAsync(string sender, string message) { MaxAhead = maxAhead; - AddNotice(string.Format("The game host has changed MaxAhead to {0}".L10N("Client:Main:HostChangeMaxAhead"), maxAhead)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has changed MaxAhead to {0}".L10N("Client:Main:HostChangeMaxAhead"), maxAhead)); } int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion); @@ -1439,7 +1574,7 @@ private async Task ApplyGameOptionsAsync(string sender, string message) { ProtocolVersion = protocolVersion; - AddNotice(string.Format("The game host has changed ProtocolVersion to {0}".L10N("Client:Main:HostChangeProtocolVersion"), protocolVersion)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has changed ProtocolVersion to {0}".L10N("Client:Main:HostChangeProtocolVersion"), protocolVersion)); } string mapName = parts[partIndex + 8]; @@ -1501,9 +1636,9 @@ private async Task ApplyGameOptionsAsync(string sender, string message) if (checkBox.Checked != boolArray[optionIndex]) { if (boolArray[optionIndex]) - AddNotice(string.Format("The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), checkBox.Text)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has enabled {0}".L10N("Client:Main:HostEnableOption"), checkBox.Text)); else - AddNotice(string.Format("The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), checkBox.Text)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has disabled {0}".L10N("Client:Main:HostDisableOption"), checkBox.Text)); } CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex]; @@ -1540,7 +1675,7 @@ private async Task ApplyGameOptionsAsync(string sender, string message) if (dd.OptionName == null) ddName = dd.Name; - AddNotice(string.Format("The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has set {0} to {1}".L10N("Client:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); } DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex; @@ -1572,9 +1707,9 @@ private async Task ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsEnable dynamicTunnelsEnabled = newDynamicTunnelsEnabledValue; if (newDynamicTunnelsEnabledValue) - AddNotice(string.Format("The game host has enabled Dynamic Tunnels".L10N("UI:Main:HostEnableDynamicTunnels"))); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has enabled Dynamic Tunnels".L10N("Client:Main:HostEnableDynamicTunnels"))); else - AddNotice(string.Format("The game host has disabled Dynamic Tunnels".L10N("UI:Main:HostDisableDynamicTunnels"))); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has disabled Dynamic Tunnels".L10N("Client:Main:HostDisableDynamicTunnels"))); if (newDynamicTunnelsEnabledValue) { @@ -1632,6 +1767,9 @@ protected override async Task GameProcessExitedAsync() { await base.GameProcessExitedAsync(); await channel.SendCTCPMessageAsync(CnCNetCommands.RETURN, QueuedMessageType.SYSTEM_MESSAGE, 20); + gameStartCancellationTokenSource.Cancel(); + v3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); + v3GameTunnelHandlers.Clear(); ReturnNotification(ProgramConstants.PLAYERNAME); if (IsHost) @@ -1651,12 +1789,12 @@ protected override async Task GameProcessExitedAsync() /// /// Handles the "START" (game start) command sent by the game host. /// - private async Task NonHostLaunchGameAsync(string sender, string message) + private async Task ClientLaunchGameV2Async(string sender, string message) { if (tunnelHandler.CurrentTunnel.Version != Constants.TUNNEL_VERSION_2) return; - if (sender != hostName) + if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; string[] parts = message.Split(';'); @@ -1723,7 +1861,7 @@ protected override void WriteSpawnIniAdditions(IniFile iniFile) { base.WriteSpawnIniAdditions(iniFile); - if (!UserINISettings.Instance.UseP2P && !UserINISettings.Instance.UseDynamicTunnels && tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) + if (!p2pEnabled && !UserINISettings.Instance.UseDynamicTunnels && tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) { iniFile.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); iniFile.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); @@ -1744,7 +1882,7 @@ protected override void WriteSpawnIniAdditions(IniFile iniFile) private void HandleNotification(string sender, Action handler) { - if (sender != hostName) + if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; handler(); @@ -1752,7 +1890,7 @@ private void HandleNotification(string sender, Action handler) private void HandleIntNotification(string sender, int parameter, Action handler) { - if (sender != hostName) + if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; handler(parameter); @@ -1836,7 +1974,7 @@ protected override async Task StillInGameNotificationAsync(int playerIndex) private void ReturnNotification(string sender) { - AddNotice(string.Format("{0} has returned from the game.".L10N("Client:Main:PlayerReturned"), sender)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} has returned from the game.".L10N("Client:Main:PlayerReturned"), sender)); PlayerInfo pInfo = Players.Find(p => p.Name == sender); @@ -1849,7 +1987,7 @@ private void ReturnNotification(string sender) private void HandleTunnelPing(string sender, int ping) { - PlayerInfo pInfo = Players.Find(p => p.Name.Equals(sender)); + PlayerInfo pInfo = Players.Find(p => p.Name.Equals(sender, StringComparison.OrdinalIgnoreCase)); if (pInfo != null) { @@ -1880,10 +2018,10 @@ private async Task FileHashNotificationAsync(string sender, string filesHash) private void CheaterNotification(string sender, string cheaterName) { - if (sender != hostName) + if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; - AddNotice(string.Format("Player {0} has different files compared to the game host. Either {0} or the game host could be cheating.".L10N("Client:Main:DifferentFileCheating"), cheaterName), Color.Red); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} has different files compared to the game host. Either {0} or the game host could be cheating.".L10N("Client:Main:DifferentFileCheating"), cheaterName), Color.Red); } protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) @@ -1910,7 +2048,7 @@ protected override async Task HandleLockGameButtonClickAsync() } else { - AddNotice(string.Format("Cannot unlock game; the player limit ({0}) has been reached.".L10N("Client:Main:RoomCantUnlockAsLimit"), playerLimit)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Cannot unlock game; the player limit ({0}) has been reached.".L10N("Client:Main:RoomCantUnlockAsLimit"), playerLimit)); } } } @@ -1946,7 +2084,7 @@ protected override async Task KickPlayerAsync(int playerIndex) PlayerInfo pInfo = Players[playerIndex]; - AddNotice(string.Format("Kicking {0} from the game...".L10N("Client:Main:KickPlayer"), pInfo.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Kicking {0} from the game...".L10N("Client:Main:KickPlayer"), pInfo.Name)); await channel.SendKickMessageAsync(pInfo.Name, 8); } @@ -1960,18 +2098,18 @@ protected override async Task BanPlayerAsync(int playerIndex) if (user != null) { - AddNotice(string.Format("Banning and kicking {0} from the game...".L10N("Client:Main:BanAndKickPlayer"), pInfo.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Banning and kicking {0} from the game...".L10N("Client:Main:BanAndKickPlayer"), pInfo.Name)); await channel.SendBanMessageAsync(user.Hostname, 8); await channel.SendKickMessageAsync(user.Name, 8); } } private void HandleCheatDetectedMessage(string sender) => - AddNotice(string.Format("{0} has modified game files during the client session. They are likely attempting to cheat!".L10N("Client:Main:PlayerModifyFileCheat"), sender), Color.Red); + AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} has modified game files during the client session. They are likely attempting to cheat!".L10N("Client:Main:PlayerModifyFileCheat"), sender), Color.Red); private async Task HandleTunnelServerChangeMessageAsync(string sender, string hash) { - if (sender != hostName) + if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)); @@ -1992,15 +2130,8 @@ private async Task HandleTunnelServerChangeMessageAsync(string sender, string ha UpdateLaunchGameButtonStatus(); } - private void HandleTunnelPingsMessage(string sender, string tunnelPingsMessage) + private void HandleTunnelPingsMessage(string playerName, string tunnelPingsMessage) { - (string Name, CnCNetTunnel Tunnel) playerTunnelInfo = playerTunnels.SingleOrDefault(p => p.Name.Equals(sender, StringComparison.OrdinalIgnoreCase)); - - if (playerTunnelInfo.Tunnel is not null) - return; - - tunnelPingsMessages.Add((sender, tunnelPingsMessage)); - if (!pinnedTunnels.Any()) return; @@ -2014,24 +2145,127 @@ private void HandleTunnelPingsMessage(string sender, string tunnelPingsMessage) IEnumerable<(int CombinedPing, string Hash)> combinedTunnelResults = tunnelPings .Where(q => pinnedTunnels.Select(r => r.Hash).Contains(q.Hash)) .Select(q => (CombinedPing: q.Ping + pinnedTunnels.SingleOrDefault(r => q.Hash.Equals(r.Hash, StringComparison.OrdinalIgnoreCase)).Ping, q.Hash)); - (int _, string hash) = combinedTunnelResults + (int combinedPing, string hash) = combinedTunnelResults .OrderBy(q => q.CombinedPing) .ThenBy(q => q.Hash, StringComparer.OrdinalIgnoreCase) .FirstOrDefault(); if (hash is null) { - AddNotice(string.Format("No common tunnel server found for: {0}".L10N("Client:Main:NoCommonTunnel"), sender)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "No common tunnel server found for: {0}".L10N("Client:Main:NoCommonTunnel"), playerName)); } else { CnCNetTunnel tunnel = tunnelHandler.Tunnels.Single(q => q.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)); - playerTunnels.Add(new(sender, tunnel)); - AddNotice(string.Format("Dynamic tunnel server negotiated with {0}: {1} ({2}ms)".L10N("Client:Main:TunnelNegotiated"), sender, tunnel.Name, tunnel.PingInMs)); + if (playerTunnels.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) + { + int index = playerTunnels.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); + + playerTunnels.RemoveAt(index); + } + + playerTunnels.Add(new(playerName, tunnel, combinedPing)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Dynamic tunnel server negotiated with {0}: {1} ({2}ms)".L10N("Client:Main:TunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); } } + private async Task HandleP2PRequestMessageAsync(string playerName, string p2pRequestMessage) + { + if (!p2pEnabled || !p2pPorts.Any()) + return; + + List<(IPAddress IpAddress, long Ping)> localPingResults = new(); + string[] splitLines = p2pRequestMessage.Split(';'); + using var ping = new Ping(); + + if (IPAddress.TryParse(splitLines[0], out IPAddress parsedIpV4Address)) + { + PingReply pingResult = await ping.SendPingAsync(parsedIpV4Address, P2P_PING_TIMEOUT); + + if (pingResult.Status is IPStatus.Success) + localPingResults.Add((parsedIpV4Address, pingResult.RoundtripTime)); + } + + if (IPAddress.TryParse(splitLines[1], out IPAddress parsedIpV6Address)) + { + PingReply pingResult = await ping.SendPingAsync(parsedIpV6Address, P2P_PING_TIMEOUT); + + if (pingResult.Status is IPStatus.Success) + localPingResults.Add((parsedIpV6Address, pingResult.RoundtripTime)); + } + + if (parsedIpV4Address is null && parsedIpV6Address is null) + { + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled P2P".L10N("Client:Main:P2PDisabled"), playerName)); + + (string RemotePlayerName, ushort[] RemotePorts, List<(IPAddress RemoteIpAddress, long Ping)> LocalPingResults, List<(IPAddress RemoteIpAddress, long Ping)> RemotePingResults, bool Enabled) p2pPlayer; + + if (p2pPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) + { + p2pPlayer = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); + + p2pPlayers.RemoveAt(p2pPlayers.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + p2pPlayers.Add((p2pPlayer.RemotePlayerName, p2pPlayer.RemotePorts, p2pPlayer.LocalPingResults, p2pPlayer.RemotePingResults, false)); + } + + return; + } + + ushort[] remotePlayerPorts = splitLines[2].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); + List<(IPAddress RemoteIpAddress, long Ping)> remotePingResults = new(); + + if (p2pPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) + { + remotePingResults = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)).RemotePingResults; + + p2pPlayers.RemoveAt(p2pPlayers.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + } + + p2pPlayers.Add((playerName, remotePlayerPorts, localPingResults, remotePingResults, true)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} allows P2P: ({1}ms)".L10N("Client:Main:P2PAllowed"), playerName, localPingResults.Min(q => q.Ping))); + await channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_PINGS + $" {playerName}-{localPingResults.Select(q => $"{q.IpAddress};{q.Ping}\t").Aggregate((q, r) => $"{q}{r}")}", QueuedMessageType.SYSTEM_MESSAGE, 10); + } + + private void HandleP2PPingsMessage(string playerName, string p2pPingsMessage) + { + if (!p2pEnabled) + return; + + string[] splitLines = p2pPingsMessage.Split('-'); + string pingPlayerName = splitLines[0]; + + if (!FindLocalPlayer().Name.Equals(pingPlayerName, StringComparison.OrdinalIgnoreCase)) + return; + + var p2pPlayer = p2pPlayers.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); + + if (p2pPlayer.RemotePlayerName is null) + { + BroadcastPlayerP2PRequestAsync().HandleTask(); + + return; + } + + string[] pingResults = splitLines[1].Split('\t', StringSplitOptions.RemoveEmptyEntries); + List<(IPAddress IpAddress, long Ping)> playerPings = new(); + + foreach (string pingResult in pingResults) + { + string[] ipAddressPingResult = pingResult.Split(';'); + + if (IPAddress.TryParse(ipAddressPingResult[0], out IPAddress ipV4Address)) + playerPings.Add((ipV4Address, long.Parse(ipAddressPingResult[1], CultureInfo.InvariantCulture))); + } + + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} P2P enabled".L10N("Client:Main:P2PEnabled"), playerName)); + + p2pPlayer.RemotePingResults = playerPings; + + p2pPlayers.RemoveAt(p2pPlayers.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + p2pPlayers.Add(p2pPlayer); + } + /// /// Changes the tunnel server used for the game. /// @@ -2040,7 +2274,7 @@ private Task HandleTunnelServerChangeAsync(CnCNetTunnel tunnel) { tunnelHandler.CurrentTunnel = tunnel; - AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("Client:Main:HostChangeTunnel"), tunnel.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has changed the tunnel server to: {0}".L10N("Client:Main:HostChangeTunnel"), tunnel.Name)); return UpdatePingAsync(); } @@ -2115,7 +2349,7 @@ private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) Map map = e.Map; hostUploadedMaps.Add(map.SHA1); - AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("Client:Main:UpdateMapToDBFailed"), map.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Uploading map {0} to the CnCNet map database failed.".L10N("Client:Main:UpdateMapToDBFailed"), map.Name)); if (map == Map) { @@ -2127,7 +2361,7 @@ private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) private async Task MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) { hostUploadedMaps.Add(e.Map.SHA1); - AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); if (e.Map == Map) { @@ -2192,7 +2426,7 @@ private void HandleMapUploadRequest(string sender, string mapHash) /// private void HandleMapTransferFailMessage(string sender, string sha1) { - if (sender == hostName) + if (sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) { AddNotice("The game host failed to upload the map to the CnCNet map database.".L10N("Client:Main:HostUpdateMapToDBFailed")); hostUploadedMaps.Add(sha1); @@ -2224,7 +2458,7 @@ private void HandleMapTransferFailMessage(string sender, string sha1) private void HandleMapDownloadRequest(string sender, string sha1) { - if (sender != hostName) + if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; hostUploadedMaps.Add(sha1); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index c9d8bf771..3081884e5 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1380,7 +1380,7 @@ private PlayerHouseInfo[] WriteSpawnIni() spawnIni.SetIntValue(sectionName, "Side", pHouseInfo.InternalSideIndex); spawnIni.SetBooleanValue(sectionName, "IsSpectator", pHouseInfo.IsSpectator); spawnIni.SetIntValue(sectionName, "Color", pHouseInfo.ColorIndex); - spawnIni.SetStringValue(sectionName, "Ip", GetIPAddressForPlayer(pInfo)); + spawnIni.SetStringValue(sectionName, "Ip", GetIPAddressForPlayer(pInfo).ToString()); spawnIni.SetIntValue(sectionName, "Port", pInfo.Port); otherId++; @@ -1493,7 +1493,10 @@ protected bool IsPlayerSpectator(PlayerInfo pInfo) return false; } - protected virtual string GetIPAddressForPlayer(PlayerInfo player) => IPAddress.Any.MapToIPv4().ToString(); + /// + /// Returns the IPv4 address used to connect to the local game. + /// + protected virtual IPAddress GetIPAddressForPlayer(PlayerInfo player) => IPAddress.Any; /// /// Override this in a derived class to write game lobby specific code to diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index a1412649c..00504a0e7 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -198,7 +198,7 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) { listener = new Socket(SocketType.Stream, ProtocolType.Tcp); - listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); + listener.Bind(new IPEndPoint(IPAddress.IPv6Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); listener.Listen(); while (!cancellationToken.IsCancellationRequested) @@ -542,10 +542,10 @@ protected override async Task BroadcastPlayerExtraOptionsAsync() protected override Task HostLaunchGameAsync() => BroadcastMessageAsync(LANCommands.LAUNCH_GAME + " " + UniqueGameID); - protected override string GetIPAddressForPlayer(PlayerInfo player) + protected override IPAddress GetIPAddressForPlayer(PlayerInfo player) { var lpInfo = (LANPlayerInfo)player; - return IPAddress.Parse(lpInfo.IPAddress).MapToIPv4().ToString(); + return lpInfo.IPAddress.MapToIPv4(); } protected override Task RequestPlayerOptionsAsync(int side, int color, int start, int team) @@ -736,14 +736,14 @@ public override void Update(GameTime gameTime) for (int i = 1; i < Players.Count; i++) { LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i]; - if (!Task.Run(() => lpInfo.UpdateAsync(gameTime).HandleTaskAsync()).Result) + if (!Task.Run(() => lpInfo.UpdateAsync(gameTime).HandleTask()).Result) { CleanUpPlayer(lpInfo); Players.RemoveAt(i); AddNotice(string.Format("{0} - connection timed out".L10N("Client:Main:PlayerTimeout"), lpInfo.Name)); CopyPlayerDataToUI(); - Task.Run(() => BroadcastPlayerOptionsAsync().HandleTaskAsync()).Wait(); - Task.Run(() => BroadcastPlayerExtraOptionsAsync().HandleTaskAsync()).Wait(); + Task.Run(() => BroadcastPlayerOptionsAsync().HandleTask()).Wait(); + Task.Run(() => BroadcastPlayerExtraOptionsAsync().HandleTask()).Wait(); UpdateDiscordPresence(); i--; } @@ -762,7 +762,7 @@ public override void Update(GameTime gameTime) timeSinceLastReceivedCommand += gameTime.ElapsedGameTime; if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT)) - Task.Run(() => BtnLeaveGame_LeftClickAsync().HandleTaskAsync()).Wait(); + Task.Run(() => BtnLeaveGame_LeftClickAsync().HandleTask()).Wait(); } base.Update(gameTime); @@ -927,7 +927,7 @@ private void HandlePlayerOptionsBroadcast(string data) int start = Conversions.IntFromString(parts[baseIndex + 3], -1); int team = Conversions.IntFromString(parts[baseIndex + 4], -1); int readyStatus = Conversions.IntFromString(parts[baseIndex + 5], -1); - string ipAddress = parts[baseIndex + 6]; + var ipAddress = IPAddress.Parse(parts[baseIndex + 6]); int aiLevel = Conversions.IntFromString(parts[baseIndex + 7], -1); if (side < 0 || side > SideCount + RandomSelectorCount) @@ -942,8 +942,8 @@ private void HandlePlayerOptionsBroadcast(string data) if (team < 0 || team > 4) return; - if (IPAddress.IsLoopback(IPAddress.Parse(ipAddress))) - ipAddress = hostEndPoint.Address.MapToIPv4().ToString(); + if (IPAddress.IsLoopback(ipAddress)) + ipAddress = hostEndPoint.Address.MapToIPv4(); bool isAi = aiLevel > -1; if (aiLevel > 2) diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index d11fd604c..5b1ff3141 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -69,7 +69,7 @@ private async Task WindowManager_GameClosingAsync() private readonly LANColor[] chatColors; private readonly MapLoader mapLoader; - private int chatColorIndex; + private const int chatColorIndex = 0; private readonly Encoding encoding; private readonly LANServerCommandHandler[] hostCommandHandlers; @@ -156,7 +156,7 @@ public async Task PostJoinAsync() private async Task ListenForClientsAsync(CancellationToken cancellationToken) { listener = new Socket(SocketType.Stream, ProtocolType.Tcp); - listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); + listener.Bind(new IPEndPoint(IPAddress.IPv6Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); listener.Listen(); while (!cancellationToken.IsCancellationRequested) @@ -524,7 +524,7 @@ private void Client_HandleOptionsMessage(string data) const int PLAYER_INFO_PARTS = 3; int pCount = (parts.Length - 1) / PLAYER_INFO_PARTS; - if (pCount * PLAYER_INFO_PARTS + 1 != parts.Length) + if ((pCount * PLAYER_INFO_PARTS) + 1 != parts.Length) return; int savedGameIndex = Conversions.IntFromString(parts[0], -1); @@ -539,20 +539,18 @@ private void Client_HandleOptionsMessage(string data) for (int i = 0; i < pCount; i++) { - int baseIndex = 1 + i * PLAYER_INFO_PARTS; - string pName = parts[baseIndex]; - bool ready = Conversions.IntFromString(parts[baseIndex + 1], -1) > 0; - string ipAddress = parts[baseIndex + 2]; - - LANPlayerInfo pInfo = new LANPlayerInfo(encoding); - pInfo.Name = pName; - pInfo.Ready = ready; - pInfo.IPAddress = ipAddress; - Players.Add(pInfo); + int baseIndex = 1 + (i * PLAYER_INFO_PARTS); + + Players.Add(new LANPlayerInfo(encoding) + { + Name = parts[baseIndex], + Ready = Conversions.IntFromString(parts[baseIndex + 1], -1) > 0, + IPAddress = IPAddress.Parse(parts[baseIndex + 2]) + }); } if (Players.Count > 0) // Set IP of host - Players[0].IPAddress = ((IPEndPoint)client.RemoteEndPoint).Address.ToString(); + Players[0].IPAddress = ((IPEndPoint)client.RemoteEndPoint).Address; CopyPlayerDataToUI(); } @@ -620,13 +618,13 @@ public override void Update(GameTime gameTime) for (int i = 1; i < Players.Count; i++) { LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i]; - if (!Task.Run(() => lpInfo.UpdateAsync(gameTime).HandleTaskAsync()).Result) + if (!Task.Run(() => lpInfo.UpdateAsync(gameTime).HandleTask()).Result) { CleanUpPlayer(lpInfo); Players.RemoveAt(i); AddNotice(string.Format("{0} - connection timed out".L10N("Client:Main:PlayerTimeout"), lpInfo.Name)); CopyPlayerDataToUI(); - Task.Run(() => BroadcastOptionsAsync().HandleTaskAsync()).Wait(); + Task.Run(() => BroadcastOptionsAsync().HandleTask()).Wait(); UpdateDiscordPresence(); i--; } @@ -645,7 +643,7 @@ public override void Update(GameTime gameTime) timeSinceLastReceivedCommand += gameTime.ElapsedGameTime; if (timeSinceLastReceivedCommand > TimeSpan.FromSeconds(DROPOUT_TIMEOUT)) - Task.Run(() => LeaveGameAsync().HandleTaskAsync()).Wait(); + Task.Run(() => LeaveGameAsync().HandleTask()).Wait(); } base.Update(gameTime); diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index 5eded4e66..1f571622d 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -41,6 +41,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs index 02a43c457..cd07bd70b 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs @@ -17,4 +17,5 @@ internal static class CnCNetLobbyCommands public const string SAVEOPTIONS = "SAVEOPTIONS"; public const string LOADOPTIONS = "LOADOPTIONS"; public const string DYNAMICTUNNELS = "DYNAMICTUNNELS"; + public const string P2P = "P2P"; } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameDataReceivedEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameDataReceivedEventArgs.cs new file mode 100644 index 000000000..539d966ac --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameDataReceivedEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal sealed class GameDataReceivedEventArgs : EventArgs +{ + public GameDataReceivedEventArgs(uint playerId, ReadOnlyMemory gameData) + { + PlayerId = playerId; + GameData = gameData; + } + + public uint PlayerId { get; } + + public ReadOnlyMemory GameData { get; } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs new file mode 100644 index 000000000..bb630e2cf --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs @@ -0,0 +1,14 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "AddAnyPortMapping", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +internal readonly record struct AddAnyPortMappingRequest( + [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string + [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, + [property: MessageBodyMember(Name = "NewProtocol")] string Protocol, // TCP or UDP + [property: MessageBodyMember(Name = "NewInternalPort")] ushort InternalPort, + [property: MessageBodyMember(Name = "NewInternalClient")] string InternalClient, // “x.x.x.x” or empty string + [property: MessageBodyMember(Name = "NewEnabled")] byte Enabled, // bool + [property: MessageBodyMember(Name = "NewPortMappingDescription")] string PortMappingDescription, + [property: MessageBodyMember(Name = "NewLeaseDuration")] uint LeaseDuration); // seconds \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingResponse.cs new file mode 100644 index 000000000..4adf0ec30 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingResponse.cs @@ -0,0 +1,7 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "AddAnyPortMappingResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +internal readonly record struct AddAnyPortMappingResponse( + [property: MessageBodyMember(Name = "NewReservedPort")] ushort ReservedPort); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs new file mode 100644 index 000000000..ab90d0d56 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs @@ -0,0 +1,12 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "AddPinhole", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +internal readonly record struct AddPinholeRequest( + [property: MessageBodyMember(Name = "RemoteHost")] string RemoteHost, + [property: MessageBodyMember(Name = "RemotePort")] ushort RemotePort, // 0 = wildcard + [property: MessageBodyMember(Name = "InternalClient")] string InternalClient, + [property: MessageBodyMember(Name = "InternalPort")] ushort InternalPort, // 0 = wildcard + [property: MessageBodyMember(Name = "Protocol")] ushort Protocol, // 17 = UDP + [property: MessageBodyMember(Name = "LeaseTime")] uint LeaseTime); // 1-86400 \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeResponse.cs new file mode 100644 index 000000000..beaed5dc1 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeResponse.cs @@ -0,0 +1,7 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "AddPinholeResponse", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +internal readonly record struct AddPinholeResponse( + [property: MessageBodyMember(Name = "UniqueID")] ushort UniqueId); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs new file mode 100644 index 000000000..c48bb968e --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs @@ -0,0 +1,14 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "AddPortMapping", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +internal readonly record struct AddPortMappingRequest( + [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string + [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, + [property: MessageBodyMember(Name = "NewProtocol")] string Protocol, // TCP or UDP + [property: MessageBodyMember(Name = "NewInternalPort")] ushort InternalPort, + [property: MessageBodyMember(Name = "NewInternalClient")] string InternalClient, // “x.x.x.x” or empty string + [property: MessageBodyMember(Name = "NewEnabled")] byte Enabled, // bool + [property: MessageBodyMember(Name = "NewPortMappingDescription")] string PortMappingDescription, + [property: MessageBodyMember(Name = "NewLeaseDuration")] uint LeaseDuration); // seconds \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingResponse.cs new file mode 100644 index 000000000..c4089f610 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingResponse.cs @@ -0,0 +1,6 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "AddPortMappingResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +internal readonly record struct AddPortMappingResponse; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs new file mode 100644 index 000000000..8438c8181 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs @@ -0,0 +1,12 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal enum AddressType +{ + Unknown, + + IpV4SiteLocal, + + IpV6LinkLocal, + + IpV6SiteLocal +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeRequest.cs new file mode 100644 index 000000000..f510904c7 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeRequest.cs @@ -0,0 +1,7 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "DeletePinhole", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +internal readonly record struct DeletePinholeRequest( + [property: MessageBodyMember(Name = "UniqueID")] ushort UniqueId); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeResponse.cs new file mode 100644 index 000000000..5cb4e07c4 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeResponse.cs @@ -0,0 +1,6 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "DeletePinholeResponse", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +internal readonly record struct DeletePinholeResponse; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV1.cs new file mode 100644 index 000000000..43f8c5720 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV1.cs @@ -0,0 +1,9 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "DeletePortMapping", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +internal readonly record struct DeletePortMappingRequestV1( + [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string + [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, + [property: MessageBodyMember(Name = "NewProtocol")] string Protocol); // TCP or UDP \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV2.cs new file mode 100644 index 000000000..18921c22e --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV2.cs @@ -0,0 +1,9 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "DeletePortMapping", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +internal readonly record struct DeletePortMappingRequestV2( + [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string + [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, + [property: MessageBodyMember(Name = "NewProtocol")] string Protocol); // TCP or UDP \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV1.cs new file mode 100644 index 000000000..dd5a29e31 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV1.cs @@ -0,0 +1,6 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "DeletePortMappingResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +internal readonly record struct DeletePortMappingResponseV1; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV2.cs new file mode 100644 index 000000000..86cf4b035 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV2.cs @@ -0,0 +1,6 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "DeletePortMappingResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +internal readonly record struct DeletePortMappingResponseV2; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Device.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Device.cs new file mode 100644 index 000000000..05f324a39 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Device.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[DataContract(Name = "device", Namespace = "urn:schemas-upnp-org:device-1-0")] +internal readonly record struct Device( + [property: DataMember(Name = "deviceType", Order = 0)] string DeviceType, + [property: DataMember(Name = "friendlyName", Order = 1)] string FriendlyName, + [property: DataMember(Name = "manufacturer", Order = 2)] string Manufacturer, + [property: DataMember(Name = "manufacturerURL", Order = 3)] string ManufacturerUrl, + [property: DataMember(Name = "modelDescription", Order = 4)] string ModelDescription, + [property: DataMember(Name = "modelName", Order = 5)] string ModelName, + [property: DataMember(Name = "modelNumber", Order = 6)] string ModelNumber, + [property: DataMember(Name = "modelURL", Order = 7)] string ModelUrl, + [property: DataMember(Name = "UDN", Order = 8)] string UniqueDeviceName, + [property: DataMember(Name = "UPC", Order = 9)] string Upc, + [property: DataMember(Name = "iconList", Order = 10)] IconListItem[] IconList, + [property: DataMember(Name = "serviceList", Order = 11)] ServiceListItem[] ServiceList, + [property: DataMember(Name = "deviceList", Order = 12)] Device[] DeviceList, + [property: DataMember(Name = "presentationURL", Order = 13)] string PresentationUrl); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV1.cs new file mode 100644 index 000000000..6ae834a0f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV1.cs @@ -0,0 +1,6 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "GetExternalIPAddress", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +internal readonly record struct GetExternalIPAddressRequestV1; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV2.cs new file mode 100644 index 000000000..e1ef1e4d0 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV2.cs @@ -0,0 +1,6 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "GetExternalIPAddress", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +internal readonly record struct GetExternalIPAddressRequestV2; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV1.cs new file mode 100644 index 000000000..5ffe3d71a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV1.cs @@ -0,0 +1,7 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "GetExternalIPAddressResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +internal readonly record struct GetExternalIPAddressResponseV1( + [property: MessageBodyMember(Name = "NewExternalIPAddress")] string ExternalIPAddress); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV2.cs new file mode 100644 index 000000000..0107f106d --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV2.cs @@ -0,0 +1,7 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "GetExternalIPAddressResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +internal readonly record struct GetExternalIPAddressResponseV2( + [property: MessageBodyMember(Name = "NewExternalIPAddress")] string ExternalIPAddress); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusRequest.cs new file mode 100644 index 000000000..b1b554361 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusRequest.cs @@ -0,0 +1,6 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "GetFirewallStatus", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +internal readonly record struct GetFirewallStatusRequest; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusResponse.cs new file mode 100644 index 000000000..75d7a6c12 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusResponse.cs @@ -0,0 +1,8 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "GetFirewallStatusResponse", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +internal readonly record struct GetFirewallStatusResponse( + [property: MessageBodyMember(Name = "FirewallEnabled")] bool FirewallEnabled, + [property: MessageBodyMember(Name = "InboundPinholeAllowed")] bool InboundPinholeAllowed); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV1.cs new file mode 100644 index 000000000..b2277f812 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV1.cs @@ -0,0 +1,6 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "GetNatRsipStatusRequest", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +public readonly record struct GetNatRsipStatusRequestV1; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV2.cs new file mode 100644 index 000000000..e863b356a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV2.cs @@ -0,0 +1,6 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "GetNatRsipStatusRequest", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +public readonly record struct GetNatRsipStatusRequestV2; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV1.cs new file mode 100644 index 000000000..69a074bbf --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV1.cs @@ -0,0 +1,8 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "GetNatRsipStatusResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +public readonly record struct GetNatRsipStatusResponseV1( + [property: MessageBodyMember(Name = "NewRSIPAvailable")] bool RsipAvailable, + [property: MessageBodyMember(Name = "NewNATEnabled")] bool NatEnabled); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV2.cs new file mode 100644 index 000000000..1a821fe0a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV2.cs @@ -0,0 +1,8 @@ +using System.ServiceModel; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[MessageContract(WrapperName = "GetNatRsipStatusResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +public readonly record struct GetNatRsipStatusResponseV2( + [property: MessageBodyMember(Name = "NewRSIPAvailable")] bool RsipAvailable, + [property: MessageBodyMember(Name = "NewNATEnabled")] bool NatEnabled); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/IconListItem.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/IconListItem.cs new file mode 100644 index 000000000..c1acc6869 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/IconListItem.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[DataContract(Name = "icon", Namespace = "urn:schemas-upnp-org:device-1-0")] +internal readonly record struct IconListItem( + [property: DataMember(Name = "mimetype", Order = 0)] string Mimetype, + [property: DataMember(Name = "width", Order = 1)] int Width, + [property: DataMember(Name = "height", Order = 2)] int Height, + [property: DataMember(Name = "depth", Order = 3)] int Depth, + [property: DataMember(Name = "url", Order = 4)] string Url); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs new file mode 100644 index 000000000..4bd0d3f4f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Security; +using System.Net.Sockets; +using System.ServiceModel; +using System.ServiceModel.Description; +using System.Text; +using System.Xml; +using System.ServiceModel.Channels; +using ClientCore; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal sealed record InternetGatewayDevice(IEnumerable Locations, string Server, string CacheControl, string Ext, string SearchTarget, string UniqueServiceName, UPnPDescription UPnPDescription, Uri PreferredLocation) +{ + private const int ReceiveTimeout = 10000; + private const string UPnPWanConnectionDevice = "urn:schemas-upnp-org:device:WANConnectionDevice"; + private const string UPnPWanDevice = "urn:schemas-upnp-org:device:WANDevice"; + private const string UPnPService = "urn:schemas-upnp-org:service"; + private const string UPnPWanIpConnection = "WANIPConnection"; + + private static readonly HttpClient HttpClient; + + public const string UPnPInternetGatewayDevice = "urn:schemas-upnp-org:device:InternetGatewayDevice"; + + static InternetGatewayDevice() + { + HttpClient = new HttpClient( + new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + SslOptions = new() + { + RemoteCertificateValidationCallback = (_, _, _, sslPolicyErrors) => (sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) == 0, + } + }, true) + { + Timeout = TimeSpan.FromMilliseconds(ReceiveTimeout), + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher + }; + } + + public async ValueTask OpenIpV4PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken = default) + { + Logger.Log($"Opening IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + + int uPnPVersion = GetDeviceUPnPVersion(); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}", AddressFamily.InterNetwork); + + switch (uPnPVersion) + { + case 2: + string addAnyPortMappingAction = $"\"{service.ServiceType}#AddAnyPortMapping\""; + var addAnyPortMappingRequest = new AddAnyPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, "CnCNet", 0); + AddAnyPortMappingResponse addAnyPortMappingResponse = await ExecuteSoapAction( + serviceUri, addAnyPortMappingAction, serviceType, addAnyPortMappingRequest, cancellationToken); + + port = addAnyPortMappingResponse.ReservedPort; + + break; + case 1: + string addPortMappingAction = $"\"{service.ServiceType}#AddPortMapping\""; + var addPortMappingRequest = new AddPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, "CnCNet", 0); + + await ExecuteSoapAction( + serviceUri, addPortMappingAction, serviceType, addPortMappingRequest, cancellationToken); + + break; + default: + throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); + } + + Logger.Log($"Opened IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + + return port; + } + + public async ValueTask CloseIpV4PortAsync(ushort port, CancellationToken cancellationToken = default) + { + Logger.Log($"Deleting IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + + int uPnPVersion = GetDeviceUPnPVersion(); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}"); + string serviceAction = $"\"{service.ServiceType}#DeletePortMapping\""; + + switch (uPnPVersion) + { + case 2: + var deletePortMappingRequestV2 = new DeletePortMappingRequestV2(string.Empty, port, "UDP"); + + await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, deletePortMappingRequestV2, cancellationToken); + + break; + case 1: + var deletePortMappingRequestV1 = new DeletePortMappingRequestV1(string.Empty, port, "UDP"); + + await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, deletePortMappingRequestV1, cancellationToken); + + break; + default: + throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); + } + + Logger.Log($"Deleted IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + } + + public async ValueTask GetExternalIpV4AddressAsync(CancellationToken cancellationToken = default) + { + Logger.Log($"Requesting external IP address from UPnP device {UPnPDescription.Device.FriendlyName}."); + + int uPnPVersion = GetDeviceUPnPVersion(); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}"); + string serviceAction = $"\"{service.ServiceType}#GetExternalIPAddress\""; + IPAddress ipAddress; + + switch (uPnPVersion) + { + case 2: + GetExternalIPAddressResponseV2 getExternalIpAddressResponseV2 = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, default, cancellationToken); + + ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV2.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV2.ExternalIPAddress); + + break; + case 1: + GetExternalIPAddressResponseV1 getExternalIpAddressResponseV1 = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, default, cancellationToken); + + ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV1.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV1.ExternalIPAddress); + break; + default: + throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); + } + + Logger.Log($"Received external IP address {ipAddress} from UPnP device {UPnPDescription.Device.FriendlyName}."); + + return ipAddress; + } + + public async ValueTask GetNatRsipStatusAsync(CancellationToken cancellationToken = default) + { + Logger.Log($"Checking NAT status on UPnP device {UPnPDescription.Device.FriendlyName}."); + + int uPnPVersion = GetDeviceUPnPVersion(); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}"); + string serviceAction = $"\"{service.ServiceType}#GetNatRsipStatus\""; + bool natEnabled; + + switch (uPnPVersion) + { + case 2: + GetNatRsipStatusResponseV2 getNatRsipStatusResponseV2 = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, default, cancellationToken); + + natEnabled = getNatRsipStatusResponseV2.NatEnabled; + + break; + case 1: + GetNatRsipStatusResponseV1 getNatRsipStatusResponseV1 = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, default, cancellationToken); + + natEnabled = getNatRsipStatusResponseV1.NatEnabled; + break; + default: + throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); + } + + Logger.Log($"Received NAT status {natEnabled} on UPnP device {UPnPDescription.Device.FriendlyName}."); + + return natEnabled; + } + + public async ValueTask<(bool FirewallEnabled, bool InboundPinholeAllowed)> GetIpV6FirewallStatusAsync(CancellationToken cancellationToken = default) + { + Logger.Log($"Checking IPV6 firewall status on UPnP device {UPnPDescription.Device.FriendlyName}."); + + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1"); + string serviceAction = $"\"{service.ServiceType}#GetFirewallStatus\""; + GetFirewallStatusResponse response = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, default, cancellationToken); + + Logger.Log($"Received IPV6 firewall status {response.FirewallEnabled} and port mapping allowed {response.InboundPinholeAllowed} on UPnP device {UPnPDescription.Device.FriendlyName}."); + + return (response.FirewallEnabled, response.InboundPinholeAllowed); + } + + public async Task OpenIpV6PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken = default) + { + Logger.Log($"Opening IPV6 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1"); + string serviceAction = $"\"{service.ServiceType}#AddPinhole\""; + var request = new AddPinholeRequest(string.Empty, port, ipAddress.ToString(), port, 17, 86400); + AddPinholeResponse response = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, request, cancellationToken); + + Logger.Log($"Opened IPV6 UDP port {port} with ID {response.UniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + + return response.UniqueId; + } + + public async ValueTask CloseIpV6PortAsync(ushort uniqueId, CancellationToken cancellationToken = default) + { + Logger.Log($"Opening IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1"); + string serviceAction = $"\"{service.ServiceType}#DeletePinhole\""; + var request = new DeletePinholeRequest(uniqueId); + await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, request, cancellationToken); + + Logger.Log($"Opened IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + } + + private static async ValueTask ExecuteSoapAction(string serviceUri, string soapAction, string defaultNamespace, TRequest request, CancellationToken cancellationToken) + { + HttpClient.DefaultRequestHeaders.Remove("SOAPAction"); + HttpClient.DefaultRequestHeaders.Add("SOAPAction", soapAction); + + var xmlSerializerFormatAttribute = new XmlSerializerFormatAttribute + { + Style = OperationFormatStyle.Rpc, + Use = OperationFormatUse.Encoded + }; + var requestTypedMessageConverter = TypedMessageConverter.Create(typeof(TRequest), soapAction, defaultNamespace, xmlSerializerFormatAttribute); + using var requestMessage = requestTypedMessageConverter.ToMessage(request); + await using var requestStream = new MemoryStream(); + await using var writer = XmlWriter.Create( + requestStream, + new() + { + OmitXmlDeclaration = true, + Async = true, + Encoding = new UTF8Encoding(false) + }); + requestMessage.WriteMessage(writer); + await writer.FlushAsync(); + + requestStream.Position = 0L; + + using var content = new StreamContent(requestStream); + + content.Headers.ContentType = MediaTypeHeaderValue.Parse("text/xml"); + + using HttpResponseMessage httpResponseMessage = await HttpClient.PostAsync(serviceUri, content, cancellationToken); + await using Stream stream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken); + + try + { + httpResponseMessage.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) + { + using var reader = new StreamReader(stream); + string error = await reader.ReadToEndAsync(CancellationToken.None); + + ProgramConstants.LogException(ex, $"UPNP error {ex.StatusCode}:{error}."); + + throw; + } + + using var envelopeReader = XmlDictionaryReader.CreateTextReader(stream, new()); + using var responseMessage = Message.CreateMessage(envelopeReader, int.MaxValue, MessageVersion.Soap11WSAddressingAugust2004); + var responseTypedMessageConverter = TypedMessageConverter.Create(typeof(TResponse), null, defaultNamespace, xmlSerializerFormatAttribute); + + return (TResponse)responseTypedMessageConverter.FromMessage(responseMessage); + } + + private (ServiceListItem WanIpConnectionService, string ServiceUri, string ServiceType) GetSoapActionParameters(string wanConnectionDeviceService, AddressFamily? addressFamily = null) + { + Uri location = PreferredLocation; + + if (addressFamily is AddressFamily.InterNetwork && Locations.Any(q => q.HostNameType is UriHostNameType.IPv4)) + location = Locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv4); + + int uPnPVersion = GetDeviceUPnPVersion(); + Device wanDevice = UPnPDescription.Device.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPWanDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); + Device wanConnectionDevice = wanDevice.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPWanConnectionDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); + string serviceType = $"{UPnPService}:{wanConnectionDeviceService}"; + ServiceListItem wanIpConnectionService = wanConnectionDevice.ServiceList.Single(q => q.ServiceType.Equals(serviceType, StringComparison.OrdinalIgnoreCase)); + string serviceUri = FormattableString.Invariant($"{location.Scheme}://{location.Authority}{wanIpConnectionService.ControlUrl}"); + + return new(wanIpConnectionService, serviceUri, serviceType); + } + + private int GetDeviceUPnPVersion() + { + return $"{UPnPInternetGatewayDevice}:2".Equals(UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase) ? 2 + : ($"{UPnPInternetGatewayDevice}:1".Equals(UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase) ? 1 : 0); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs new file mode 100644 index 000000000..3f33396e1 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs @@ -0,0 +1,5 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal readonly record struct InternetGatewayDeviceResponse(Uri Location, string Server, string CacheControl, string Ext, string SearchTarget, string Usn); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/ServiceListItem.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/ServiceListItem.cs new file mode 100644 index 000000000..56d6523a9 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/ServiceListItem.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[DataContract(Name = "service", Namespace = "urn:schemas-upnp-org:device-1-0")] +internal readonly record struct ServiceListItem( + [property: DataMember(Name = "serviceType", Order = 0)] string ServiceType, + [property: DataMember(Name = "serviceId", Order = 1)] string ServiceId, + [property: DataMember(Name = "controlURL", Order = 2)] string ControlUrl, + [property: DataMember(Name = "eventSubURL", Order = 3)] string EventSubUrl, + [property: DataMember(Name = "SCPDURL", Order = 4)] string ScpdUrl); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SpecVersion.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SpecVersion.cs new file mode 100644 index 000000000..674c6477c --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SpecVersion.cs @@ -0,0 +1,8 @@ +using System.Runtime.Serialization; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[DataContract(Name = "specVersion", Namespace = "urn:schemas-upnp-org:device-1-0")] +internal readonly record struct SpecVersion( + [property: DataMember(Name = "major", Order = 0)] int Major, + [property: DataMember(Name = "minor", Order = 1)] int Minor); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SystemVersion.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SystemVersion.cs new file mode 100644 index 000000000..10b8836e3 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SystemVersion.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[DataContract(Name = "systemVersion", Namespace = "urn:schemas-upnp-org:device-1-0")] +internal readonly record struct SystemVersion( + [property: DataMember(Name = "HW", Order = 0)] int Hw, + [property: DataMember(Name = "Major", Order = 1)] int Major, + [property: DataMember(Name = "Minor", Order = 2)] int Minor, + [property: DataMember(Name = "Patch", Order = 3)] int Patch, + [property: DataMember(Name = "Buildnumber", Order = 4)] int BuildNumber, + [property: DataMember(Name = "Display", Order = 5)] string Display); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPDescription.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPDescription.cs new file mode 100644 index 000000000..fa9e41474 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPDescription.cs @@ -0,0 +1,9 @@ +using System.Runtime.Serialization; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +[DataContract(Name = "root", Namespace = "urn:schemas-upnp-org:device-1-0")] +internal readonly record struct UPnPDescription( + [property: DataMember(Name = "specVersion", Order = 0)] SpecVersion SpecVersion, + [property: DataMember(Name = "systemVersion", Order = 1)] SystemVersion SystemVersion, + [property: DataMember(Name = "device", Order = 2)] Device Device); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs new file mode 100644 index 000000000..c5e1a144f --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -0,0 +1,304 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Net; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Text; +using System.Xml; +using ClientCore; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal static class UPnPHandler +{ + private const string InternetGatewayDeviceDeviceType = "upnp:rootdevice"; + private const int UPnPMultiCastPort = 1900; + private const int ReceiveTimeout = 2000; + private const int SendCount = 3; + + private static IReadOnlyDictionary SsdpMultiCastAddresses => new Dictionary + { + [AddressType.IpV4SiteLocal] = IPAddress.Parse("239.255.255.250"), + [AddressType.IpV6LinkLocal] = IPAddress.Parse("[FF02::C]"), + [AddressType.IpV6SiteLocal] = IPAddress.Parse("[FF05::C]") + }.AsReadOnly(); + + public static async ValueTask<(InternetGatewayDevice InternetGatewayDevice, List P2pPorts, List P2pIpV6PortIds, IPAddress ipV6Address, IPAddress ipV4Address)> SetupPortsAsync(InternetGatewayDevice internetGatewayDevice) + { + var p2pPorts = new List(); + var p2pIpV6PortIds = new List(); + IPAddress routerPublicIpV4Address = null; + bool? routerNatEnabled = null; + bool natDetected = false; + + if (internetGatewayDevice is null) + { + var internetGatewayDevices = (await GetInternetGatewayDevicesAsync()).ToList(); + + internetGatewayDevice = GetInternetGatewayDevice(internetGatewayDevices, 2); + internetGatewayDevice ??= GetInternetGatewayDevice(internetGatewayDevices, 1); + } + + if (internetGatewayDevice is not null) + { + routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(); + routerPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(); + } + + var publicIpAddresses = NetworkHelper.GetPublicIpAddresses().ToList(); + IPAddress publicIpV4Address = publicIpAddresses.FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetwork); + + if ((routerNatEnabled ?? false) || (publicIpV4Address is not null && routerPublicIpV4Address is not null && !publicIpV4Address.Equals(routerPublicIpV4Address))) + natDetected = true; + + publicIpV4Address ??= routerPublicIpV4Address; + + List p2pReservedPorts = new(); + + for (int i = 0; i < 7; i++) + { + p2pReservedPorts.Add(NetworkHelper.GetFreeUdpPort(Array.Empty())); + } + + var privateIpV4Addresses = NetworkHelper.GetPrivateIpAddresses().Where(q => q.AddressFamily is AddressFamily.InterNetwork).ToList(); + IPAddress privateIpV4Address = null; + + try + { + privateIpV4Address = privateIpV4Addresses.FirstOrDefault(); + + if (natDetected && privateIpV4Address is not null) + { + foreach (int p2PReservedPort in p2pReservedPorts) + { + p2pPorts.Add(await internetGatewayDevice.OpenIpV4PortAsync(privateIpV4Address, (ushort)p2PReservedPort)); + } + + p2pReservedPorts = p2pPorts; + } + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, $"Could not open P2P IPV4 ports for {privateIpV4Address} -> {publicIpV4Address}."); + } + + IPAddress publicIpV6Address = null; + + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var publicIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses().Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); + + (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundPublicIpV6Address = publicIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); + + if (foundPublicIpV6Address.IpAddress is null) + { + foundPublicIpV6Address = publicIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); + } + + publicIpV6Address = foundPublicIpV6Address.IpAddress; + } + else + { + publicIpV6Address = NetworkHelper.GetPublicIpAddresses() + .FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); + } + + if (publicIpV6Address is not null && internetGatewayDevice is not null) + { + (bool firewallEnabled, bool inboundPinholeAllowed) = await internetGatewayDevice.GetIpV6FirewallStatusAsync(); + + if (firewallEnabled && inboundPinholeAllowed) + { + foreach (int p2pReservedPort in p2pReservedPorts) + { + p2pIpV6PortIds.Add(await internetGatewayDevice.OpenIpV6PortAsync(publicIpV6Address, (ushort)p2pReservedPort)); + } + } + } + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, $"Could not open P2P IPV6 ports for {publicIpV6Address}."); + } + + return (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address); + } + + private static async ValueTask> GetInternetGatewayDevicesAsync(CancellationToken cancellationToken = default) + { + IEnumerable rawDeviceResponses = await GetRawDeviceResponses(cancellationToken); + IEnumerable> formattedDeviceResponses = GetFormattedDeviceResponses(rawDeviceResponses); + IEnumerable> groupedInternetGatewayDeviceResponses = GetGroupedInternetGatewayDeviceResponses(formattedDeviceResponses); + + return await ClientCore.Extensions.TaskExtensions.WhenAllSafe(groupedInternetGatewayDeviceResponses.Select(q => GetInternetGatewayDeviceAsync(q, cancellationToken))); + } + + private static InternetGatewayDevice GetInternetGatewayDevice(List internetGatewayDevices, ushort uPnPVersion) + => internetGatewayDevices.SingleOrDefault(q => $"{InternetGatewayDevice.UPnPInternetGatewayDevice}:{uPnPVersion}".Equals(q.UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase)); + + private static IEnumerable> GetGroupedInternetGatewayDeviceResponses(IEnumerable> formattedDeviceResponses) + { + return formattedDeviceResponses + .Select(q => new InternetGatewayDeviceResponse(new(q["LOCATION"]), q["SERVER"], q["CACHE-CONTROL"], q["EXT"], q["ST"], q["USN"])) + .GroupBy(q => q.Usn); + } + + private static Uri GetPreferredLocation(IReadOnlyCollection locations) + { + return locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6) ?? locations.First(q => q.HostNameType is UriHostNameType.IPv4); + } + + private static IEnumerable> GetFormattedDeviceResponses(IEnumerable responses) + { + return responses.Select(q => q.Split(Environment.NewLine)).Select(q => q.Where(r => r.Contains(':', StringComparison.OrdinalIgnoreCase)).ToDictionary( + s => s[..s.IndexOf(':', StringComparison.OrdinalIgnoreCase)], + s => + { + string value = s[s.IndexOf(':', StringComparison.OrdinalIgnoreCase)..]; + + if (value.EndsWith(":", StringComparison.OrdinalIgnoreCase)) + return value.Replace(":", null, StringComparison.OrdinalIgnoreCase); + + return value.Replace(": ", null, StringComparison.OrdinalIgnoreCase); + }, + StringComparer.OrdinalIgnoreCase)); + } + + private static async Task> SearchDevicesAsync(IPAddress localAddress, CancellationToken cancellationToken) + { + var responses = new List(); + AddressType addressType = GetAddressType(localAddress); + + if (addressType is AddressType.Unknown) + return responses; + + using var socket = new Socket(localAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + + socket.ExclusiveAddressUse = true; + + socket.Bind(new IPEndPoint(localAddress, 0)); + + var multiCastIpEndPoint = new IPEndPoint(SsdpMultiCastAddresses[addressType], UPnPMultiCastPort); + string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {multiCastIpEndPoint}\r\nST: {InternetGatewayDeviceDeviceType}\r\nMAN: \"ssdp:discover\"\r\nMX: 3\r\n\r\n"); + var buffer = new ArraySegment(Encoding.UTF8.GetBytes(request)); + + for (int i = 0; i < SendCount; i++) + { + _ = await socket.SendToAsync(buffer, SocketFlags.None, multiCastIpEndPoint); + } + + await ReceiveAsync(socket, new(new byte[4096]), responses, ReceiveTimeout, cancellationToken); + + return responses; + } + + private static AddressType GetAddressType(IPAddress localAddress) + { + if (localAddress.AddressFamily == AddressFamily.InterNetwork) + return AddressType.IpV4SiteLocal; + + if (localAddress.IsIPv6LinkLocal) + return AddressType.IpV6LinkLocal; + + if (localAddress.IsIPv6SiteLocal) + return AddressType.IpV6SiteLocal; + + return AddressType.Unknown; + } + + private static async ValueTask ReceiveAsync(Socket socket, ArraySegment buffer, ICollection responses, int receiveTimeout, CancellationToken cancellationToken) + { + using var timeoutCancellationTokenSource = new CancellationTokenSource(receiveTimeout); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); + + while (!linkedCancellationTokenSource.IsCancellationRequested) + { + try + { + int bytesReceived = await socket.ReceiveAsync(buffer, SocketFlags.None, linkedCancellationTokenSource.Token); + + if (bytesReceived > 0) + responses.Add(Encoding.UTF8.GetString(buffer.Take(bytesReceived).ToArray())); + } + catch (OperationCanceledException) + { + } + } + } + + private static async ValueTask GetUPnPDescription(Uri uri, CancellationToken cancellationToken) + { + using var client = new HttpClient( + new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + AutomaticDecompression = DecompressionMethods.All + }, true) + { + Timeout = TimeSpan.FromMilliseconds(ReceiveTimeout), + DefaultRequestVersion = HttpVersion.Version11, + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher + }; + + await using Stream uPnPDescription = await client.GetStreamAsync(uri, cancellationToken); + using var xmlTextReader = new XmlTextReader(uPnPDescription); + + return (UPnPDescription)new DataContractSerializer(typeof(UPnPDescription)).ReadObject(xmlTextReader); + } + + private static async ValueTask> GetRawDeviceResponses(CancellationToken cancellationToken) + { + IEnumerable localAddresses = NetworkHelper.GetLocalAddresses(); + IEnumerable[] localAddressesDeviceResponses = await ClientCore.Extensions.TaskExtensions.WhenAllSafe(localAddresses.Select(q => SearchDevicesAsync(q, cancellationToken))); + + return localAddressesDeviceResponses.Where(q => q.Any()).SelectMany(q => q).Distinct(); + } + + private static async Task GetInternetGatewayDeviceAsync(IGrouping internetGatewayDeviceResponses, CancellationToken cancellationToken) + { + Uri[] locations = internetGatewayDeviceResponses.Select(r => r.Location).ToArray(); + Uri location = GetPreferredLocation(locations); + UPnPDescription uPnPDescription = default; + + try + { + uPnPDescription = await GetUPnPDescription(location, cancellationToken); + } + catch (OperationCanceledException) + { + if (location.HostNameType is UriHostNameType.IPv6 && locations.Any(q => q.HostNameType is UriHostNameType.IPv4)) + { + try + { + location = locations.SingleOrDefault(q => q.HostNameType is UriHostNameType.IPv4); + + uPnPDescription = await GetUPnPDescription(location, cancellationToken); + } + catch (OperationCanceledException) + { + } + } + } + + return new( + internetGatewayDeviceResponses.Select(r => r.Location).Distinct(), + internetGatewayDeviceResponses.Select(r => r.Server).Distinct().Single(), + internetGatewayDeviceResponses.Select(r => r.CacheControl).Distinct().Single(), + internetGatewayDeviceResponses.Select(r => r.Ext).Distinct().Single(), + internetGatewayDeviceResponses.Select(r => r.SearchTarget).Distinct().Single(), + internetGatewayDeviceResponses.Key, + uPnPDescription, + location); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs index 6e5f17e82..175b37c00 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs @@ -2,55 +2,53 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Net.NetworkInformation; +using System.Threading; using System.Threading.Tasks; using ClientCore.Extensions; namespace DTAClient.Domain.Multiplayer.CnCNet; -internal sealed class V3GameTunnelHandler +/// +/// Manages connections between one or more s representing local game players and a representing a remote host. +/// +internal sealed class V3GameTunnelHandler : IDisposable { - private readonly Dictionary playerConnections = new(); + private readonly Dictionary playerConnections = new(); - private V3TunnelConnection tunnelConnection; + private CancellationToken cancellationToken; + private V3RemotePlayerConnection remoteHostConnection; + private EventHandler gameDataReceivedFunc; - public event EventHandler Connected; + public event EventHandler RaiseConnectedEvent; - public event EventHandler ConnectionFailed; + public event EventHandler RaiseConnectionFailedEvent; public bool IsConnected { get; private set; } - public static int GetFreePort(IEnumerable playerPorts) + public void SetUp(IPEndPoint remoteIpEndPoint, ushort localPort, uint gameLocalPlayerId, CancellationToken cancellationToken) { - IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); - int[] usedPorts = endPoints.Select(q => q.Port).ToArray().Concat(playerPorts).ToArray(); - int selectedPort = 0; + this.cancellationToken = cancellationToken; + remoteHostConnection = new V3RemotePlayerConnection(); + gameDataReceivedFunc = (_, e) => RemoteHostConnection_MessageReceivedAsync(e).HandleTask(); - while (selectedPort == 0 || usedPorts.Contains(selectedPort)) - { - selectedPort = new Random().Next(1, 65535); - } + remoteHostConnection.RaiseConnectedEvent += RemoteHostConnection_Connected; + remoteHostConnection.RaiseConnectionFailedEvent += RemoteHostConnection_ConnectionFailed; + remoteHostConnection.RaiseConnectionCutEvent += RemoteHostConnection_ConnectionCut; + remoteHostConnection.RaiseGameDataReceivedEvent += gameDataReceivedFunc; - return selectedPort; + remoteHostConnection.SetUp(remoteIpEndPoint, localPort, gameLocalPlayerId, cancellationToken); } - public void SetUp(CnCNetTunnel tunnel, uint localId) + public List CreatePlayerConnections(List playerIds) { - tunnelConnection = new V3TunnelConnection(tunnel, this, localId); - tunnelConnection.Connected += TunnelConnection_Connected; - tunnelConnection.ConnectionFailed += TunnelConnection_ConnectionFailed; - tunnelConnection.ConnectionCut += TunnelConnection_ConnectionCut; - } - - public List CreatePlayerConnections(List playerIds) - { - int[] ports = new int[playerIds.Count]; + ushort[] ports = new ushort[playerIds.Count]; for (int i = 0; i < playerIds.Count; i++) { - var playerConnection = new V3TunneledPlayerConnection(playerIds[i], this); + var playerConnection = new V3LocalPlayerConnection(); - playerConnection.CreateSocket(); + playerConnection.RaiseGameDataReceivedEvent += (_, e) => PlayerConnection_PacketReceivedAsync(e).HandleTask(); + playerConnection.Setup(playerIds[i], cancellationToken); ports[i] = playerConnection.PortNumber; @@ -62,85 +60,89 @@ public List CreatePlayerConnections(List playerIds) public void StartPlayerConnections(int gamePort) { - foreach (KeyValuePair playerConnection in playerConnections) + foreach (KeyValuePair playerConnection in playerConnections) { - playerConnection.Value.StartAsync(gamePort).HandleTask(); + playerConnection.Value.StartConnectionAsync(gamePort).HandleTask(); } } public void ConnectToTunnel() { - if (tunnelConnection == null) - throw new InvalidOperationException("GameTunnelHandler: Call SetUp before calling ConnectToTunnel."); + if (remoteHostConnection == null) + throw new InvalidOperationException($"Call SetUp before calling {nameof(ConnectToTunnel)}."); - tunnelConnection.ConnectAsync().HandleTask(); + remoteHostConnection.StartConnectionAsync().HandleTask(); } - public void Clear() - { - ClearPlayerConnections(); + /// + /// Forwards local game data to the remote host. + /// + private ValueTask PlayerConnection_PacketReceivedAsync(GameDataReceivedEventArgs e) + => remoteHostConnection?.SendDataAsync(e.GameData, e.PlayerId) ?? ValueTask.CompletedTask; - if (tunnelConnection == null) - return; - - tunnelConnection.CloseConnection(); - - tunnelConnection.Connected -= TunnelConnection_Connected; - tunnelConnection.ConnectionFailed -= TunnelConnection_ConnectionFailed; - tunnelConnection.ConnectionCut -= TunnelConnection_ConnectionCut; + /// + /// Forwards remote player data to the local game. + /// + private ValueTask RemoteHostConnection_MessageReceivedAsync(GameDataReceivedEventArgs e) + { + V3LocalPlayerConnection localPlayerConnection = GetLocalPlayerConnection(e.PlayerId); - tunnelConnection = null; + return localPlayerConnection?.SendDataAsync(e.GameData) ?? ValueTask.CompletedTask; } - public async Task PlayerConnection_PacketReceivedAsync(V3TunneledPlayerConnection sender, ReadOnlyMemory data) + public void Dispose() { - if (tunnelConnection != null) - await tunnelConnection.SendDataAsync(data, sender.PlayerId); - } + foreach (KeyValuePair remotePlayerGameConnection in playerConnections) + { + remotePlayerGameConnection.Value.Dispose(); + } - public async Task TunnelConnection_MessageReceivedAsync(ReadOnlyMemory data, uint senderId) - { - V3TunneledPlayerConnection connection = GetPlayerConnection(senderId); + playerConnections.Clear(); + + if (remoteHostConnection == null) + return; - if (connection is not null) - await connection.SendPacketAsync(data); + remoteHostConnection.RaiseConnectedEvent -= RemoteHostConnection_Connected; + remoteHostConnection.RaiseConnectionFailedEvent -= RemoteHostConnection_ConnectionFailed; + remoteHostConnection.RaiseConnectionCutEvent -= RemoteHostConnection_ConnectionCut; + remoteHostConnection.RaiseGameDataReceivedEvent -= gameDataReceivedFunc; + + remoteHostConnection.Dispose(); } - private V3TunneledPlayerConnection GetPlayerConnection(uint senderId) + private V3LocalPlayerConnection GetLocalPlayerConnection(uint senderId) + => playerConnections.TryGetValue(senderId, out V3LocalPlayerConnection connection) ? connection : null; + + private void RemoteHostConnection_Connected(object sender, EventArgs e) { - if (playerConnections.TryGetValue(senderId, out V3TunneledPlayerConnection connection)) - return connection; + IsConnected = true; - return null; + OnRaiseConnectedEvent(EventArgs.Empty); } - private void ClearPlayerConnections() + private void RemoteHostConnection_ConnectionFailed(object sender, EventArgs e) { - foreach (KeyValuePair connection in playerConnections) - { - connection.Value.Stop(); - } + IsConnected = false; - playerConnections.Clear(); + OnRaiseConnectionFailedEvent(EventArgs.Empty); } - private void TunnelConnection_Connected(object sender, EventArgs e) + private void OnRaiseConnectedEvent(EventArgs e) { - IsConnected = true; + EventHandler raiseEvent = RaiseConnectedEvent; - Connected?.Invoke(this, EventArgs.Empty); + raiseEvent?.Invoke(this, e); } - private void TunnelConnection_ConnectionFailed(object sender, EventArgs e) + private void OnRaiseConnectionFailedEvent(EventArgs e) { - IsConnected = false; + EventHandler raiseEvent = RaiseConnectionFailedEvent; - ConnectionFailed?.Invoke(this, EventArgs.Empty); - Clear(); + raiseEvent?.Invoke(this, e); } - private void TunnelConnection_ConnectionCut(object sender, EventArgs e) + private void RemoteHostConnection_ConnectionCut(object sender, EventArgs e) { - Clear(); + Dispose(); } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs new file mode 100644 index 000000000..857ea38f5 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs @@ -0,0 +1,119 @@ +using System; +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +/// +/// Manages a player connection between the local game and this application. +/// +internal sealed class V3LocalPlayerConnection : IDisposable +{ + private const int Timeout = 60000; + private const uint IOC_IN = 0x80000000; + private const uint IOC_VENDOR = 0x18000000; + private const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; + + private Socket localGameSocket; + private EndPoint remotePlayerEndPoint; + private CancellationToken cancellationToken; + private uint playerId; + + public ushort PortNumber { get; private set; } + + public void Setup(uint playerId, CancellationToken cancellationToken) + { + this.cancellationToken = cancellationToken; + this.playerId = playerId; + localGameSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); + + // Disable ICMP port not reachable exceptions, happens when the game is still loading and has not yet opened the socket. + if (OperatingSystem.IsWindows()) + localGameSocket.IOControl(unchecked((int)SIO_UDP_CONNRESET), new byte[] { 0 }, null); + + localGameSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + + PortNumber = (ushort)((IPEndPoint)localGameSocket.LocalEndPoint).Port; + } + + public event EventHandler RaiseGameDataReceivedEvent; + + /// + /// Starts listening for local game player data and forwards it to the tunnel. + /// + /// The game UDP port to listen on. + public async ValueTask StartConnectionAsync(int gamePort) + { + remotePlayerEndPoint = new IPEndPoint(IPAddress.Loopback, gamePort); + + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(128); + Memory buffer = memoryOwner.Memory[..128]; + + localGameSocket.ReceiveTimeout = Timeout; + +#if DEBUG + Logger.Log($"Start listening for local game {remotePlayerEndPoint} on {localGameSocket.LocalEndPoint}."); +#else + Logger.Log($"Start listening for local game player {playerId}."); +#endif + try + { + while (!cancellationToken.IsCancellationRequested) + { + SocketReceiveFromResult socketReceiveFromResult = await localGameSocket.ReceiveFromAsync(buffer, SocketFlags.None, remotePlayerEndPoint, cancellationToken); + Memory data = buffer[..socketReceiveFromResult.ReceivedBytes]; + +#if DEBUG + Logger.Log($"Received data from local game {socketReceiveFromResult.RemoteEndPoint} on {localGameSocket.LocalEndPoint}."); +#endif + OnRaiseGameDataReceivedEvent(new(playerId, data)); + } + } + catch (SocketException) + { + } + catch (OperationCanceledException) + { + } + } + + /// + /// Sends tunnel data to the local game. + /// + /// The data to send to the game. + public async ValueTask SendDataAsync(ReadOnlyMemory data) + { +#if DEBUG + Logger.Log($"Sending data from {localGameSocket.LocalEndPoint} to local game {remotePlayerEndPoint}."); + +#endif + try + { + await localGameSocket.SendToAsync(data, SocketFlags.None, remotePlayerEndPoint, cancellationToken); + } + catch (OperationCanceledException) + { + } + } + + public void Dispose() + { +#if DEBUG + Logger.Log($"Connection to local game {localGameSocket.RemoteEndPoint} closed."); +#else + Logger.Log($"Connection to local game for player {playerId} closed."); +#endif + localGameSocket.Dispose(); + } + + private void OnRaiseGameDataReceivedEvent(GameDataReceivedEventArgs e) + { + EventHandler raiseEvent = RaiseGameDataReceivedEvent; + + raiseEvent?.Invoke(this, e); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs new file mode 100644 index 000000000..e9a75e6fb --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -0,0 +1,226 @@ +using System; +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using ClientCore; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +/// +/// Manages a player connection between a remote host and this application. +/// +internal sealed class V3RemotePlayerConnection : IDisposable +{ + private uint gameLocalPlayerId; + private CancellationToken cancellationToken; + private Socket tunnelSocket; + private IPEndPoint remoteEndPoint; + private ushort localPort; + + public void SetUp(IPEndPoint remoteEndPoint, ushort localPort, uint gameLocalPlayerId, CancellationToken cancellationToken) + { + this.cancellationToken = cancellationToken; + this.gameLocalPlayerId = gameLocalPlayerId; + this.remoteEndPoint = remoteEndPoint; + this.localPort = localPort; + } + + public event EventHandler RaiseConnectedEvent; + + public event EventHandler RaiseConnectionFailedEvent; + + public event EventHandler RaiseConnectionCutEvent; + + public event EventHandler RaiseGameDataReceivedEvent; + + /// + /// Starts listening for remote player data and forwards it to the local game. + /// + public async ValueTask StartConnectionAsync() + { +#if DEBUG + Logger.Log($"Attempting to establish a connection from port {localPort} to {remoteEndPoint})."); +#else + Logger.Log($"Attempting to establish a connection using {localPort})."); +#endif + + tunnelSocket = new Socket(SocketType.Dgram, ProtocolType.Udp) + { + SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT, + ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT + }; + + tunnelSocket.Bind(new IPEndPoint(IPAddress.IPv6Any, localPort)); + + try + { + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); + Memory buffer = memoryOwner.Memory[..50]; + + if (!BitConverter.TryWriteBytes(buffer.Span[..4], gameLocalPlayerId)) + throw new Exception(); + + await tunnelSocket.SendToAsync(buffer, SocketFlags.None, remoteEndPoint, cancellationToken); +#if DEBUG + Logger.Log($"Connection from {tunnelSocket.LocalEndPoint} to {remoteEndPoint} established."); +#else + Logger.Log($"Connection using {localPort} established."); +#endif + OnRaiseConnectedEvent(EventArgs.Empty); + } + catch (SocketException ex) + { +#if DEBUG + ProgramConstants.LogException(ex, $"Failed to establish connection from port {localPort} to {remoteEndPoint}."); +#else + ProgramConstants.LogException(ex, $"Failed to establish connection using {localPort}."); +#endif + OnRaiseConnectionFailedEvent(EventArgs.Empty); + return; + } + catch (OperationCanceledException) + { + return; + } + + tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; + + await ReceiveLoopAsync(); + } + + /// + /// Sends local game player data to the remote host. + /// + /// The data to send to the game. + /// The id of the player that receives the data. + public async ValueTask SendDataAsync(ReadOnlyMemory data, uint receiverId) + { +#if DEBUG + Logger.Log($"Sending data {gameLocalPlayerId} -> {receiverId} from {tunnelSocket.LocalEndPoint} to {remoteEndPoint}."); + +#endif + const int idsSize = sizeof(uint) * 2; + int bufferSize = data.Length + idsSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory packet = memoryOwner.Memory[..bufferSize]; + + if (!BitConverter.TryWriteBytes(packet.Span[..4], gameLocalPlayerId)) + throw new Exception(); + + if (!BitConverter.TryWriteBytes(packet.Span[4..8], receiverId)) + throw new Exception(); + + data.CopyTo(packet[8..]); + + try + { + await tunnelSocket.SendToAsync(packet, SocketFlags.None, remoteEndPoint, cancellationToken); + } + catch (OperationCanceledException) + { + } + } + + private async ValueTask ReceiveLoopAsync() + { + try + { + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); + +#if DEBUG + Logger.Log($"Start listening for {remoteEndPoint} on {tunnelSocket.LocalEndPoint}."); +#else + Logger.Log($"Start listening on {localPort}."); +#endif + + while (!cancellationToken.IsCancellationRequested) + { + Memory buffer = memoryOwner.Memory[..1024]; + SocketReceiveFromResult socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint, cancellationToken); + + if (socketReceiveFromResult.ReceivedBytes < 8) + { +#if DEBUG + Logger.Log($"Invalid data packet from {remoteEndPoint}"); +#else + Logger.Log($"Invalid data packet on {localPort}"); +#endif + continue; + } + + Memory data = buffer[8..socketReceiveFromResult.ReceivedBytes]; + uint senderId = BitConverter.ToUInt32(buffer[..4].Span); + uint receiverId = BitConverter.ToUInt32(buffer[4..8].Span); + +#if DEBUG + Logger.Log($"Received {senderId} -> {receiverId} from {remoteEndPoint} on {tunnelSocket.LocalEndPoint}."); + +#endif + if (receiverId != gameLocalPlayerId) + { +#if DEBUG + Logger.Log($"Invalid target (received: {receiverId}, expected: {gameLocalPlayerId}) from {remoteEndPoint}."); +#else + Logger.Log($"Invalid target (received: {receiverId}, expected: {gameLocalPlayerId}) on port {localPort}."); +#endif + continue; + } + + OnRaiseGameDataReceivedEvent(new(senderId, data)); + } + } + catch (SocketException ex) + { +#if DEBUG + ProgramConstants.LogException(ex, $"Socket exception in {remoteEndPoint} receive loop."); +#else + ProgramConstants.LogException(ex, $"Socket exception on port {localPort} receive loop."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); + } + catch (OperationCanceledException) + { + } + } + + public void Dispose() + { +#if DEBUG + Logger.Log($"Connection to remote host {remoteEndPoint} closed."); +#else + Logger.Log($"Connection to remote host on port {localPort} closed."); +#endif + tunnelSocket?.Dispose(); + } + + private void OnRaiseConnectedEvent(EventArgs e) + { + EventHandler raiseEvent = RaiseConnectedEvent; + + raiseEvent?.Invoke(this, e); + } + + private void OnRaiseConnectionFailedEvent(EventArgs e) + { + EventHandler raiseEvent = RaiseConnectionFailedEvent; + + raiseEvent?.Invoke(this, e); + } + + private void OnRaiseConnectionCutEvent(EventArgs e) + { + EventHandler raiseEvent = RaiseConnectionCutEvent; + + raiseEvent?.Invoke(this, e); + } + + private void OnRaiseGameDataReceivedEvent(GameDataReceivedEventArgs e) + { + EventHandler raiseEvent = RaiseGameDataReceivedEvent; + + raiseEvent?.Invoke(this, e); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs deleted file mode 100644 index a22341ca5..000000000 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunnelConnection.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Buffers; -using System.Net; -using System.Net.Sockets; -using System.Threading.Tasks; -using ClientCore; -using Rampastring.Tools; - -namespace DTAClient.Domain.Multiplayer.CnCNet; - -/// -/// Handles connections to version 3 CnCNet tunnel servers. -/// -internal sealed class V3TunnelConnection -{ - private readonly CnCNetTunnel tunnel; - private readonly V3GameTunnelHandler gameTunnelHandler; - private readonly uint localId; - - private Socket tunnelSocket; - private EndPoint tunnelEndPoint; - private bool aborted; - - public V3TunnelConnection(CnCNetTunnel tunnel, V3GameTunnelHandler gameTunnelHandler, uint localId) - { - this.tunnel = tunnel; - this.gameTunnelHandler = gameTunnelHandler; - this.localId = localId; - } - - public event EventHandler Connected; - - public event EventHandler ConnectionFailed; - - public event EventHandler ConnectionCut; - - public async Task ConnectAsync() - { - Logger.Log("Attempting to establish connection to V3 tunnel server " + - $"{tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); - - tunnelEndPoint = new IPEndPoint(tunnel.IPAddress, tunnel.Port); - tunnelSocket = new Socket(SocketType.Dgram, ProtocolType.Udp) - { - SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT, - ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT - }; - - try - { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); - Memory buffer = memoryOwner.Memory[..50]; - - if (!BitConverter.TryWriteBytes(buffer.Span[..4], localId)) - throw new Exception(); - - await tunnelSocket.SendToAsync(buffer, SocketFlags.None, tunnelEndPoint); - Logger.Log($"Connection to V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port}) established."); - Connected?.Invoke(this, EventArgs.Empty); - } - catch (SocketException ex) - { - ProgramConstants.LogException(ex, $"Failed to establish connection to V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port})."); - tunnelSocket.Close(); - ConnectionFailed?.Invoke(this, EventArgs.Empty); - return; - } - - tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; - - await ReceiveLoopAsync(); - } - - public async Task SendDataAsync(ReadOnlyMemory data, uint receiverId) - { -#if DEBUG - Logger.Log($"Sending data {localId} -> {receiverId} from ({((IPEndPoint)tunnelSocket.LocalEndPoint).Address}:{((IPEndPoint)tunnelSocket.LocalEndPoint).Port}) to V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); - -#endif - const int idsSize = sizeof(uint) * 2; - int bufferSize = data.Length + idsSize; - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); - Memory packet = memoryOwner.Memory[..bufferSize]; - - if (!BitConverter.TryWriteBytes(packet.Span[..4], localId)) - throw new Exception(); - - if (!BitConverter.TryWriteBytes(packet.Span[4..8], receiverId)) - throw new Exception(); - - data.CopyTo(packet[8..]); - - if (!aborted) - await tunnelSocket.SendToAsync(packet, SocketFlags.None, tunnelEndPoint); - } - - public void CloseConnection() - { - Logger.Log("Closing connection to the tunnel server."); - - aborted = true; - } - - private async Task ReceiveLoopAsync() - { - try - { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); - -#if DEBUG - Logger.Log($"Start listening for V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port}) on ({((IPEndPoint)tunnelSocket.LocalEndPoint).Address}:{((IPEndPoint)tunnelSocket.LocalEndPoint).Port})"); - -#endif - while (true) - { - if (aborted) - { - DoClose(); - Logger.Log("Exiting receive loop."); - return; - } - - Memory buffer = memoryOwner.Memory[..1024]; - SocketReceiveFromResult socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync(buffer, SocketFlags.None, tunnelEndPoint); - - if (socketReceiveFromResult.ReceivedBytes < 8) - { - Logger.Log("Invalid data packet from tunnel server"); - continue; - } - - Memory data = buffer[8..socketReceiveFromResult.ReceivedBytes]; - uint senderId = BitConverter.ToUInt32(buffer[..4].Span); - uint receiverId = BitConverter.ToUInt32(buffer[4..8].Span); - -#if DEBUG - Logger.Log($"Received {senderId} -> {receiverId} from V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port}) on ({((IPEndPoint)tunnelSocket.LocalEndPoint).Address}:{((IPEndPoint)tunnelSocket.LocalEndPoint).Port})"); - -#endif - if (receiverId != localId) - { - Logger.Log($"Invalid target (received: {receiverId}, expected: {localId}) from V3 tunnel server {tunnel.Name} ({tunnel.Address}:{tunnel.Port})"); - continue; - } - - await gameTunnelHandler.TunnelConnection_MessageReceivedAsync(data, senderId); - } - } - catch (SocketException ex) - { - ProgramConstants.LogException(ex, "Socket exception in V3 tunnel receive loop."); - DoClose(); - ConnectionCut?.Invoke(this, EventArgs.Empty); - } - } - - private void DoClose() - { - aborted = true; - - tunnelSocket?.Close(); - - tunnelSocket = null; - - Logger.Log("Connection to tunnel server closed."); - } -} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunneledPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3TunneledPlayerConnection.cs deleted file mode 100644 index 18a7887a0..000000000 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3TunneledPlayerConnection.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Buffers; -using System.Net; -using System.Net.Sockets; -using System.Threading.Tasks; -#if DEBUG -using Rampastring.Tools; -#endif - -namespace DTAClient.Domain.Multiplayer.CnCNet; - -/// -/// Captures packets sent by an UDP client (the game) to a specific address -/// and allows forwarding messages back to it. -/// -internal sealed class V3TunneledPlayerConnection -{ - private const int Timeout = 60000; - private const uint IOC_IN = 0x80000000; - private const uint IOC_VENDOR = 0x18000000; - private const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; - - private readonly V3GameTunnelHandler gameTunnelHandler; - - private Socket socket; - private EndPoint endPoint; - private EndPoint remoteEndPoint; - private bool aborted; - - public V3TunneledPlayerConnection(uint playerId, V3GameTunnelHandler gameTunnelHandler) - { - PlayerId = playerId; - this.gameTunnelHandler = gameTunnelHandler; - } - - public int PortNumber { get; private set; } - - public uint PlayerId { get; } - - public void Stop() - { - aborted = true; - } - - /// - /// Creates a socket and sets the connection's port number. - /// - public void CreateSocket() - { - socket = new Socket(SocketType.Dgram, ProtocolType.Udp); - endPoint = new IPEndPoint(IPAddress.Loopback, 0); - - // Disable ICMP port not reachable exceptions, happens when the game is still loading and has not yet opened the socket. - if (OperatingSystem.IsWindows()) - socket.IOControl(unchecked((int)SIO_UDP_CONNRESET), new byte[] { 0 }, null); - - socket.Bind(endPoint); - - PortNumber = ((IPEndPoint)socket.LocalEndPoint).Port; - } - - public async Task StartAsync(int gamePort) - { - remoteEndPoint = new IPEndPoint(IPAddress.Loopback, gamePort); - - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(128); - Memory buffer = memoryOwner.Memory[..128]; - - socket.ReceiveTimeout = Timeout; - -#if DEBUG - Logger.Log($"Start listening for local game {((IPEndPoint)remoteEndPoint).Address}:{((IPEndPoint)remoteEndPoint).Port} on ({((IPEndPoint)socket.LocalEndPoint).Address}:{((IPEndPoint)socket.LocalEndPoint).Port})"); - -#endif - try - { - while (true) - { - if (aborted) - break; - - SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint); - Memory data = buffer[..socketReceiveFromResult.ReceivedBytes]; - -#if DEBUG - Logger.Log($"Received data from local game {((IPEndPoint)socketReceiveFromResult.RemoteEndPoint).Address}:{((IPEndPoint)socketReceiveFromResult.RemoteEndPoint).Port} on ({((IPEndPoint)socket.LocalEndPoint).Address}:{((IPEndPoint)socket.LocalEndPoint).Port})"); - -#endif - - await gameTunnelHandler.PlayerConnection_PacketReceivedAsync(this, data); - } - } - catch (SocketException) - { - } - - aborted = true; - - socket.Close(); - } - - public async Task SendPacketAsync(ReadOnlyMemory packet) - { - if (aborted) - return; - -#if DEBUG - Logger.Log($"Sending data from ({((IPEndPoint)socket.LocalEndPoint).Address}:{((IPEndPoint)socket.LocalEndPoint).Port}) to local game {((IPEndPoint)remoteEndPoint).Address}:{((IPEndPoint)remoteEndPoint).Port}"); - -#endif - await socket.SendToAsync(packet, SocketFlags.None, remoteEndPoint); - } -} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index ded2fbaef..e80743d55 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -34,8 +34,6 @@ public LANPlayerInfo(Encoding encoding) private string overMessage = string.Empty; - private CancellationTokenSource cancellationTokenSource; - public void SetClient(Socket client) { if (TcpClient != null) @@ -57,7 +55,7 @@ public async Task UpdateAsync(GameTime gameTime) if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT) || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)) - await SendMessageAsync(LANCommands.PING, cancellationTokenSource?.Token ?? default); + await SendMessageAsync(LANCommands.PING, default); if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT)) return false; @@ -65,12 +63,12 @@ public async Task UpdateAsync(GameTime gameTime) return true; } - public override string IPAddress + public override IPAddress IPAddress { get { if (TcpClient != null) - return ((IPEndPoint)TcpClient.RemoteEndPoint).Address.ToString(); + return ((IPEndPoint)TcpClient.RemoteEndPoint).Address.MapToIPv4(); return base.IPAddress; } diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index 6dfeb4b34..61af448bc 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -173,7 +173,7 @@ private async Task LoadCustomMapsAsync() })); } - await Task.WhenAll(tasks.ToArray()); + await ClientCore.Extensions.TaskExtensions.WhenAllSafe(tasks.ToArray()); // remove cached maps that no longer exist locally foreach (var missingSHA in customMapCache.Keys.Where(cachedSHA => !localMapSHAs.Contains(cachedSHA))) diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs new file mode 100644 index 000000000..9e3d9722e --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Runtime.Versioning; + +namespace DTAClient.Domain.Multiplayer; + +internal sealed class NetworkHelper +{ + private static readonly IReadOnlyCollection SupportedAddressFamilies = new[] + { + AddressFamily.InterNetwork, + AddressFamily.InterNetworkV6 + }.AsReadOnly(); + + [SupportedOSPlatform("windows")] + public static IEnumerable<(IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin)> GetWindowsPublicIpAddresses() + => GetUniCastIpAddresses() + .Where(q => !IsPrivateIpAddress(q.Address)) + .Select(q => (q.Address, q.PrefixOrigin, q.SuffixOrigin)); + + public static IEnumerable GetLocalAddresses() + => GetUniCastIpAddresses() + .Select(q => q.Address); + + public static IEnumerable GetPublicIpAddresses() + => GetLocalAddresses() + .Where(q => !IsPrivateIpAddress(q)); + + public static IEnumerable GetPrivateIpAddresses() + => GetLocalAddresses() + .Where(IsPrivateIpAddress); + + public static IEnumerable GetUniCastIpAddresses() + => NetworkInterface.GetAllNetworkInterfaces() + .Where(q => q.OperationalStatus is OperationalStatus.Up) + .Select(q => q.GetIPProperties()) + .Where(q => q.GatewayAddresses.Any()) + .SelectMany(q => q.UnicastAddresses) + .Where(q => SupportedAddressFamilies.Contains(q.Address.AddressFamily)); + + public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unicastIPAddressInformation) + { + uint ipAddress = BitConverter.ToUInt32(unicastIPAddressInformation.Address.GetAddressBytes(), 0); + uint ipMaskV4 = BitConverter.ToUInt32(unicastIPAddressInformation.IPv4Mask.GetAddressBytes(), 0); + uint broadCastIpAddress = ipAddress | ~ipMaskV4; + + return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); + } + + /// + /// Returns a free UDP port number above 1023. + /// + /// List of UDP port numbers which are additionally excluded. + /// A free UDP port number on the current system. + public static ushort GetFreeUdpPort(IEnumerable excludedPorts) + { + IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); + ushort[] activePorts = endPoints.Select(q => (ushort)q.Port).ToArray().Concat(excludedPorts).ToArray(); + ushort selectedPort = 0; + + while (selectedPort == 0 || activePorts.Contains(selectedPort)) + { + selectedPort = (ushort)new Random().Next(1024, IPEndPoint.MaxPort); + } + + return selectedPort; + } + + private static bool IsPrivateIpAddress(IPAddress ipAddress) + => ipAddress.AddressFamily switch + { + AddressFamily.InterNetworkV6 => ipAddress.IsIPv6SiteLocal + || ipAddress.IsIPv6UniqueLocal + || ipAddress.IsIPv6LinkLocal, + AddressFamily.InterNetwork => IsInRange("10.0.0.0", "10.255.255.255", ipAddress) + || IsInRange("172.16.0.0", "172.31.255.255", ipAddress) + || IsInRange("172.16.0.0", "172.31.255.255", ipAddress) + || IsInRange("192.168.0.0", "192.168.255.255", ipAddress) + || IsInRange("169.254.0.0", "169.254.255.255", ipAddress) + || IsInRange("127.0.0.0", "127.255.255.255", ipAddress) + || IsInRange("0.0.0.0", "0.255.255.255", ipAddress), + _ => throw new ArgumentOutOfRangeException(nameof(ipAddress.AddressFamily), ipAddress.AddressFamily, null), + }; + + private static bool IsInRange(string startIpAddress, string endIpAddress, IPAddress address) + { + uint ipStart = BitConverter.ToUInt32(IPAddress.Parse(startIpAddress).GetAddressBytes().Reverse().ToArray(), 0); + uint ipEnd = BitConverter.ToUInt32(IPAddress.Parse(endIpAddress).GetAddressBytes().Reverse().ToArray(), 0); + uint ip = BitConverter.ToUInt32(address.GetAddressBytes().Reverse().ToArray(), 0); + + return ip >= ipStart && ip <= ipEnd; + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/PlayerInfo.cs b/DXMainClient/Domain/Multiplayer/PlayerInfo.cs index 7f5bbbeba..52e066d79 100644 --- a/DXMainClient/Domain/Multiplayer/PlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/PlayerInfo.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer @@ -44,7 +45,7 @@ public PlayerInfo(string name, int sideId, int startingLocation, int colorId, in public bool IsInGame { get; set; } - public virtual string IPAddress { get; set; } = System.Net.IPAddress.Any.ToString(); + public virtual IPAddress IPAddress { get; set; } = System.Net.IPAddress.Any; public int Port { get; set; } diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index bb4c538e0..3492c1fde 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -50,23 +50,11 @@ public Connection(IConnectionManager connectionManager) new("irc.gamesurge.net", "GameSurge", new[] { 6667 }) }.AsReadOnly(); - bool _isConnected; - public bool IsConnected - { - get { return _isConnected; } - } + public bool IsConnected { get; private set; } - bool _attemptingConnection; - public bool AttemptingConnection - { - get { return _attemptingConnection; } - } + public bool AttemptingConnection { get; private set; } - Random _rng = new(); - public Random Rng - { - get { return _rng; } - } + public Random Rng { get; } = new(); private List MessageQueue = new(); private TimeSpan MessageQueueDelay; @@ -96,7 +84,8 @@ public Random Rng private static string systemId; private static readonly object idLocker = new(); - private CancellationTokenSource cancellationTokenSource; + private CancellationTokenSource connectionCancellationTokenSource; + private CancellationTokenSource sendQueueCancellationTokenSource; public static void SetId(string id) { @@ -112,22 +101,23 @@ public static void SetId(string id) /// public void ConnectAsync() { - if (_isConnected) - throw new InvalidOperationException("The client is already connected!"); + if (IsConnected) + throw new InvalidOperationException("The client is already connected!".L10N("Client:Main:ClientAlreadyConnected")); - if (_attemptingConnection) + if (AttemptingConnection) return; // Maybe we should throw in this case as well? welcomeMessageReceived = false; connectionCut = false; - _attemptingConnection = true; + AttemptingConnection = true; MessageQueueDelay = TimeSpan.FromMilliseconds(ClientConfiguration.Instance.SendSleep); - cancellationTokenSource?.Dispose(); - cancellationTokenSource = new CancellationTokenSource(); + connectionCancellationTokenSource?.Dispose(); - ConnectToServerAsync(cancellationTokenSource.Token).HandleTask(); + connectionCancellationTokenSource = new CancellationTokenSource(); + + ConnectToServerAsync(connectionCancellationTokenSource.Token).HandleTask(); } /// @@ -172,12 +162,15 @@ await client.ConnectAsync( Logger.Log("Successfully connected to " + server.Host + " on port " + port); - _isConnected = true; - _attemptingConnection = false; + IsConnected = true; + AttemptingConnection = false; connectionManager.OnConnected(); + sendQueueCancellationTokenSource?.Dispose(); + + sendQueueCancellationTokenSource = new CancellationTokenSource(); - RunSendQueueAsync(cancellationToken).HandleTask(); + RunSendQueueAsync(sendQueueCancellationTokenSource.Token).HandleTask(); socket?.Dispose(); socket = client; @@ -200,7 +193,7 @@ await client.ConnectAsync( Logger.Log("Connecting to CnCNet failed!"); // Clear the failed server list in case connecting to all servers has failed failedServerIPs.Clear(); - _attemptingConnection = false; + AttemptingConnection = false; connectionManager.OnConnectAttemptFailed(); } catch (OperationCanceledException) @@ -240,11 +233,13 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) catch (Exception ex) { ProgramConstants.LogException(ex, "Disconnected from CnCNet due to a socket error."); + errorTimes++; if (errorTimes > MAX_RECONNECT_COUNT) { const string errorMessage = "Disconnected from CnCNet after reaching the maximum number of connection retries."; + Logger.Log(errorMessage); failedServerIPs.Add(currentConnectedServerIP); connectionManager.OnConnectionLost(errorMessage.L10N("Client:Main:ClientDisconnectedAfterRetries")); @@ -263,24 +258,31 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) Logger.Log("Message received: " + msg); #endif await HandleMessageAsync(msg); + timer.Interval = 30000; } if (cancellationToken.IsCancellationRequested) { connectionManager.OnDisconnected(); + connectionCut = false; // This disconnect is intentional } timer.Enabled = false; + timer.Dispose(); - _isConnected = false; + IsConnected = false; if (connectionCut) { + sendQueueCancellationTokenSource.Cancel(); + while (!sendQueueExited) + { await Task.Delay(100, cancellationToken); + } reconnectCount++; @@ -312,7 +314,7 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) private async Task> GetServerListSortedByLatencyAsync() { // Resolve the hostnames. - IEnumerable<(IPAddress IpAddress, string Name, int[] Ports)>[] servers = await Task.WhenAll(Servers.Select(ResolveServerAsync)); + IEnumerable<(IPAddress IpAddress, string Name, int[] Ports)>[] servers = await ClientCore.Extensions.TaskExtensions.WhenAllSafe(Servers.Select(ResolveServerAsync)); // Group the tuples by IPAddress to merge duplicate servers. IEnumerable> serverInfosGroupedByIPAddress = servers @@ -353,7 +355,7 @@ private async Task> GetServerListSortedByLatencyAsync() } (Server Server, IPAddress IpAddress, long Result)[] serverAndLatencyResults = - await Task.WhenAll(serverInfos.Where(q => !failedServerIPs.Contains(q.IpAddress.ToString())).Select(PingServerAsync)); + await ClientCore.Extensions.TaskExtensions.WhenAllSafe(serverInfos.Where(q => !failedServerIPs.Contains(q.IpAddress.ToString())).Select(PingServerAsync)); // Sort the servers by AddressFamily & latency. (Server Server, IPAddress IpAddress, long Result)[] sortedServerAndLatencyResults = serverAndLatencyResults @@ -451,7 +453,7 @@ private async Task> GetServerListSortedByLatencyAsync() public async Task DisconnectAsync() { await SendMessageAsync(IRCCommands.QUIT); - cancellationTokenSource.Cancel(); + connectionCancellationTokenSource.Cancel(); socket.Close(); } @@ -980,7 +982,7 @@ private bool ReplaceMessage(QueuedMessage qm) /// The message to queue. public async Task QueueMessageAsync(QueuedMessage qm) { - if (!_isConnected) + if (!IsConnected) return; if (qm.Replace && ReplaceMessage(qm)) diff --git a/DXMainClient/Startup.cs b/DXMainClient/Startup.cs index 099409890..b5b892847 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -310,11 +310,8 @@ private static void CheckSystemSpecifications() /// private static async Task GenerateOnlineIdAsync() { -#if !WINFORMS if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { -#endif -#pragma warning disable format try { await Task.CompletedTask; @@ -333,7 +330,7 @@ private static async Task GenerateOnlineIdAsync() foreach (ManagementObject mo in moc.Cast()) mbid = (string)mo["SerialNumber"]; - string sid = new SecurityIdentifier((byte[])new DirectoryEntry(string.Format("WinNT://{0},Computer", Environment.MachineName)).Children.Cast().First().InvokeGet("objectSID"), 0).AccountDomainSid.Value; + string sid = new SecurityIdentifier((byte[])new DirectoryEntry(FormattableString.Invariant($"WinNT://{Environment.MachineName},Computer")).Children.Cast().First().InvokeGet("objectSID"), 0).AccountDomainSid.Value; Connection.SetId(cpuid + mbid + sid); using RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\" + ClientConfiguration.Instance.InstallationPathRegKey); @@ -342,14 +339,14 @@ private static async Task GenerateOnlineIdAsync() catch (Exception ex) { ProgramConstants.LogException(ex); - Random rn = new Random(); - + var rn = new Random(); using RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\" + ClientConfiguration.Instance.InstallationPathRegKey); - string str = rn.Next(int.MaxValue - 1).ToString(); + string str = rn.Next(int.MaxValue - 1).ToString(CultureInfo.InvariantCulture); try { - Object o = key.GetValue("Ident"); + object o = key.GetValue("Ident"); + if (o == null) key.SetValue("Ident", str); else @@ -362,8 +359,6 @@ private static async Task GenerateOnlineIdAsync() Connection.SetId(str); } -#pragma warning restore format -#if !WINFORMS } else { @@ -376,10 +371,9 @@ private static async Task GenerateOnlineIdAsync() catch (Exception ex) { ProgramConstants.LogException(ex); - Connection.SetId(new Random().Next(int.MaxValue - 1).ToString()); + Connection.SetId(new Random().Next(int.MaxValue - 1).ToString(CultureInfo.InvariantCulture)); } } -#endif } /// diff --git a/build/AfterPublish.targets b/build/AfterPublish.targets index 7efa27848..ff9e97dbf 100644 --- a/build/AfterPublish.targets +++ b/build/AfterPublish.targets @@ -32,6 +32,7 @@ + @@ -54,6 +55,7 @@ + @@ -63,6 +65,10 @@ + + + + \ No newline at end of file From fe5bbc2e911bb5c6d44234334b676a4051b0d3be Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 3 Dec 2022 17:38:18 +0100 Subject: [PATCH 048/109] Fix after rebase --- DXMainClient/Online/Connection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index 3492c1fde..8b2d63401 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -386,7 +386,7 @@ private async Task> GetServerListSortedByLatencyAsync() int candidateCount = sortedServerAndLatencyResults.Length; int closerCount = sortedServerAndLatencyResults.Count( - serverAndLatencyResult => serverAndLatencyResult.Item2 <= MAXIMUM_LATENCY); + serverAndLatencyResult => serverAndLatencyResult.Result <= MAXIMUM_LATENCY); Logger.Log($"Lobby servers: {candidateCount} available, {closerCount} fast."); connectionManager.OnServerLatencyTested(candidateCount, closerCount); From eb4a6fb64069867eaa5e42bb4d47c717b86e6d1b Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 3 Dec 2022 22:06:30 +0100 Subject: [PATCH 049/109] Cleanup --- .../DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs | 9 ++++++++- .../CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs | 2 +- .../Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs | 2 +- .../CnCNet/UPNP/Models/AddPortMappingRequest.cs | 2 +- .../CnCNet/UPNP/Models/InternetGatewayDevice.cs | 9 ++++++--- .../Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 9 +-------- 6 files changed, 18 insertions(+), 15 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 033f83a01..747c92596 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -1323,9 +1323,16 @@ private async Task BroadcastPlayerP2PRequestAsync() { if (!p2pPorts.Any()) { + List p2pReservedPorts = new(); + + for (int i = 0; i < 7; i++) + { + p2pReservedPorts.Add(NetworkHelper.GetFreeUdpPort(Array.Empty())); + } + try { - (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync(internetGatewayDevice); + (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync(internetGatewayDevice, p2pReservedPorts); } catch (Exception ex) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs index bb630e2cf..afa8346ef 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs @@ -11,4 +11,4 @@ internal readonly record struct AddAnyPortMappingRequest( [property: MessageBodyMember(Name = "NewInternalClient")] string InternalClient, // “x.x.x.x” or empty string [property: MessageBodyMember(Name = "NewEnabled")] byte Enabled, // bool [property: MessageBodyMember(Name = "NewPortMappingDescription")] string PortMappingDescription, - [property: MessageBodyMember(Name = "NewLeaseDuration")] uint LeaseDuration); // seconds \ No newline at end of file + [property: MessageBodyMember(Name = "NewLeaseDuration")] uint LeaseDuration); // in seconds, 1-604800 \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs index ab90d0d56..26e89b6a2 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs @@ -9,4 +9,4 @@ internal readonly record struct AddPinholeRequest( [property: MessageBodyMember(Name = "InternalClient")] string InternalClient, [property: MessageBodyMember(Name = "InternalPort")] ushort InternalPort, // 0 = wildcard [property: MessageBodyMember(Name = "Protocol")] ushort Protocol, // 17 = UDP - [property: MessageBodyMember(Name = "LeaseTime")] uint LeaseTime); // 1-86400 \ No newline at end of file + [property: MessageBodyMember(Name = "LeaseTime")] uint LeaseTime); // in seconds, 1-86400 \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs index c48bb968e..df80b1b7e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs @@ -11,4 +11,4 @@ internal readonly record struct AddPortMappingRequest( [property: MessageBodyMember(Name = "NewInternalClient")] string InternalClient, // “x.x.x.x” or empty string [property: MessageBodyMember(Name = "NewEnabled")] byte Enabled, // bool [property: MessageBodyMember(Name = "NewPortMappingDescription")] string PortMappingDescription, - [property: MessageBodyMember(Name = "NewLeaseDuration")] uint LeaseDuration); // seconds \ No newline at end of file + [property: MessageBodyMember(Name = "NewLeaseDuration")] uint LeaseDuration); // in seconds, 1-604800 \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs index 4bd0d3f4f..02efacc9a 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs @@ -26,6 +26,9 @@ internal sealed record InternetGatewayDevice(IEnumerable Locations, string private const string UPnPWanDevice = "urn:schemas-upnp-org:device:WANDevice"; private const string UPnPService = "urn:schemas-upnp-org:service"; private const string UPnPWanIpConnection = "WANIPConnection"; + private const uint IpLeaseTimeInSeconds = 4 * 60 * 60; + private const ushort IanaUdpProtocolNumber = 17; + private const string PortMappingDescription = "CnCNet"; private static readonly HttpClient HttpClient; @@ -59,7 +62,7 @@ public async ValueTask OpenIpV4PortAsync(IPAddress ipAddress, ushort por { case 2: string addAnyPortMappingAction = $"\"{service.ServiceType}#AddAnyPortMapping\""; - var addAnyPortMappingRequest = new AddAnyPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, "CnCNet", 0); + var addAnyPortMappingRequest = new AddAnyPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, PortMappingDescription, IpLeaseTimeInSeconds); AddAnyPortMappingResponse addAnyPortMappingResponse = await ExecuteSoapAction( serviceUri, addAnyPortMappingAction, serviceType, addAnyPortMappingRequest, cancellationToken); @@ -68,7 +71,7 @@ public async ValueTask OpenIpV4PortAsync(IPAddress ipAddress, ushort por break; case 1: string addPortMappingAction = $"\"{service.ServiceType}#AddPortMapping\""; - var addPortMappingRequest = new AddPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, "CnCNet", 0); + var addPortMappingRequest = new AddPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, PortMappingDescription, IpLeaseTimeInSeconds); await ExecuteSoapAction( serviceUri, addPortMappingAction, serviceType, addPortMappingRequest, cancellationToken); @@ -200,7 +203,7 @@ public async Task OpenIpV6PortAsync(IPAddress ipAddress, ushort port, Ca (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1"); string serviceAction = $"\"{service.ServiceType}#AddPinhole\""; - var request = new AddPinholeRequest(string.Empty, port, ipAddress.ToString(), port, 17, 86400); + var request = new AddPinholeRequest(string.Empty, port, ipAddress.ToString(), port, IanaUdpProtocolNumber, IpLeaseTimeInSeconds); AddPinholeResponse response = await ExecuteSoapAction( serviceUri, serviceAction, serviceType, request, cancellationToken); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index c5e1a144f..65ae0580d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -30,7 +30,7 @@ internal static class UPnPHandler [AddressType.IpV6SiteLocal] = IPAddress.Parse("[FF05::C]") }.AsReadOnly(); - public static async ValueTask<(InternetGatewayDevice InternetGatewayDevice, List P2pPorts, List P2pIpV6PortIds, IPAddress ipV6Address, IPAddress ipV4Address)> SetupPortsAsync(InternetGatewayDevice internetGatewayDevice) + public static async ValueTask<(InternetGatewayDevice InternetGatewayDevice, List P2pPorts, List P2pIpV6PortIds, IPAddress ipV6Address, IPAddress ipV4Address)> SetupPortsAsync(InternetGatewayDevice internetGatewayDevice, IEnumerable p2pReservedPorts) { var p2pPorts = new List(); var p2pIpV6PortIds = new List(); @@ -60,13 +60,6 @@ internal static class UPnPHandler publicIpV4Address ??= routerPublicIpV4Address; - List p2pReservedPorts = new(); - - for (int i = 0; i < 7; i++) - { - p2pReservedPorts.Add(NetworkHelper.GetFreeUdpPort(Array.Empty())); - } - var privateIpV4Addresses = NetworkHelper.GetPrivateIpAddresses().Where(q => q.AddressFamily is AddressFamily.InterNetwork).ToList(); IPAddress privateIpV4Address = null; From 04a8f07196837e1211155872fc0a61e307072b69 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 3 Dec 2022 23:03:04 +0100 Subject: [PATCH 050/109] Rebase fix --- DXMainClient/Domain/Multiplayer/Map.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index 37a117ede..eb33015b5 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -375,7 +375,7 @@ public bool SetInfoFromMpMapsINI(IniFile iniFile) CoopInfo.SetHouseInfos(section); } - if (MainClientConstants.USE_ISOMETRIC_CELLS) + if (ProgramConstants.USE_ISOMETRIC_CELLS) { localSize = section.GetStringValue("LocalSize", "0,0,0,0").Split(','); actualSize = section.GetStringValue("Size", "0,0,0,0").Split(','); @@ -471,7 +471,7 @@ public List GetStartingLocationPreviewCoords(Point previewSize) foreach (string waypoint in waypoints) { - if (MainClientConstants.USE_ISOMETRIC_CELLS) + if (ProgramConstants.USE_ISOMETRIC_CELLS) startingLocations.Add(GetIsometricWaypointCoords(waypoint, actualSize, localSize, previewSize)); else startingLocations.Add(GetTDRAWaypointCoords(waypoint, x, y, width, height, previewSize)); @@ -483,7 +483,7 @@ public List GetStartingLocationPreviewCoords(Point previewSize) public Point MapPointToMapPreviewPoint(Point mapPoint, Point previewSize, int level) { - if (MainClientConstants.USE_ISOMETRIC_CELLS) + if (ProgramConstants.USE_ISOMETRIC_CELLS) return GetIsoTilePixelCoord(mapPoint.X, mapPoint.Y, actualSize, localSize, previewSize, level); return GetTDRACellPixelCoord(mapPoint.X, mapPoint.Y, x, y, width, height, previewSize); @@ -598,7 +598,7 @@ public bool SetInfoFromCustomMap() localSize = iniFile.GetStringValue("Map", "LocalSize", "0,0,0,0").Split(','); actualSize = iniFile.GetStringValue("Map", "Size", "0,0,0,0").Split(','); - if (MainClientConstants.USE_ISOMETRIC_CELLS) + if (ProgramConstants.USE_ISOMETRIC_CELLS) { localSize = iniFile.GetStringValue("Map", "LocalSize", "0,0,0,0").Split(','); actualSize = iniFile.GetStringValue("Map", "Size", "0,0,0,0").Split(','); @@ -823,7 +823,7 @@ private static string HouseAllyIndexToString(int index) public string GetSizeString() { - if (MainClientConstants.USE_ISOMETRIC_CELLS) + if (ProgramConstants.USE_ISOMETRIC_CELLS) { if (actualSize == null || actualSize.Length < 4) return "Not available"; @@ -844,8 +844,8 @@ private static Point GetTDRAWaypointCoords(string waypoint, int x, int y, int wi return new Point(0, 0); // https://modenc.renegadeprojects.com/Waypoints - int waypointX = waypointCoordsInt % MainClientConstants.TDRA_WAYPOINT_COEFFICIENT; - int waypointY = waypointCoordsInt / MainClientConstants.TDRA_WAYPOINT_COEFFICIENT; + int waypointX = waypointCoordsInt % ProgramConstants.TDRA_WAYPOINT_COEFFICIENT; + int waypointY = waypointCoordsInt / ProgramConstants.TDRA_WAYPOINT_COEFFICIENT; return GetTDRACellPixelCoord(waypointX, waypointY, x, y, width, height, previewSizePoint); } From 5a1072be00ca040ba1dfa82ba8c39de96568f3e1 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 6 Dec 2022 22:05:28 +0100 Subject: [PATCH 051/109] Update P2P connection handling, use ValueTasks --- DXMainClient/DXGUI/Generic/MainMenu.cs | 6 +- DXMainClient/DXGUI/Generic/TopBar.cs | 2 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 45 ++- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 48 +-- .../Multiplayer/CnCNet/GlobalContextMenu.cs | 4 +- .../CnCNet/PrivateMessagingWindow.cs | 2 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 30 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 307 +++++++++--------- .../Multiplayer/GameLobby/GameLobbyBase.cs | 50 +-- .../Multiplayer/GameLobby/LANGameLobby.cs | 97 +++--- .../GameLobby/MultiplayerGameLobby.cs | 88 ++--- .../Multiplayer/GameLobby/SkirmishLobby.cs | 8 +- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 49 +-- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 30 +- .../CnCNet/CnCNetPlayerCountTask.cs | 33 +- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 8 +- .../Domain/Multiplayer/CnCNet/Constants.cs | 1 - .../Multiplayer/CnCNet/GameDataException.cs | 7 + .../Multiplayer/CnCNet/IRCChannelModes.cs | 14 + .../Domain/Multiplayer/CnCNet/MapSharer.cs | 11 +- .../Multiplayer/CnCNet/TunnelHandler.cs | 8 +- .../UPNP/Models/InternetGatewayDevice.cs | 10 +- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 63 ++-- .../Multiplayer/CnCNet/V3GameTunnelHandler.cs | 126 +++---- .../CnCNet/V3LocalPlayerConnection.cs | 131 ++++++-- .../CnCNet/V3RemotePlayerConnection.cs | 203 ++++++++---- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 8 +- DXMainClient/Domain/Multiplayer/MapLoader.cs | 5 +- .../Domain/Multiplayer/NetworkHelper.cs | 30 +- DXMainClient/Domain/Multiplayer/P2PPlayer.cs | 11 + DXMainClient/Domain/Multiplayer/PlayerInfo.cs | 4 +- DXMainClient/Online/Channel.cs | 22 +- DXMainClient/Online/CnCNetManager.cs | 14 +- DXMainClient/Online/Connection.cs | 115 ++++--- DXMainClient/Startup.cs | 2 +- 35 files changed, 893 insertions(+), 699 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/GameDataException.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/IRCChannelModes.cs create mode 100644 DXMainClient/Domain/Multiplayer/P2PPlayer.cs diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 1c3269e22..08f5b9ad4 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -528,9 +528,9 @@ private void CnCNetInfoController_CnCNetGameCountUpdated(object sender, PlayerCo } /// - /// Attemps to "clean" the client session in a nice way if the user closes the game. + /// Attempts to "clean" the client session in a nice way if the user closes the game. /// - private async Task CleanAsync() + private async ValueTask CleanAsync() { Updater.FileIdentifiersUpdated -= Updater_FileIdentifiersUpdated; @@ -832,7 +832,7 @@ private void BtnNewCampaign_LeftClick(object sender, EventArgs e) private void BtnLoadGame_LeftClick(object sender, EventArgs e) => innerPanel.Show(innerPanel.GameLoadingWindow); - private async Task BtnLan_LeftClickAsync() + private async ValueTask BtnLan_LeftClickAsync() { await lanLobby.OpenAsync(); diff --git a/DXMainClient/DXGUI/Generic/TopBar.cs b/DXMainClient/DXGUI/Generic/TopBar.cs index 7ffae4692..0ae31b86c 100644 --- a/DXMainClient/DXGUI/Generic/TopBar.cs +++ b/DXMainClient/DXGUI/Generic/TopBar.cs @@ -290,7 +290,7 @@ private void ConnectionEvent(string text) downTime = TimeSpan.FromSeconds(DOWN_TIME_WAIT_SECONDS - EVENT_DOWN_TIME_WAIT_SECONDS); } - private async Task BtnLogout_LeftClickAsync() + private async ValueTask BtnLogout_LeftClickAsync() { await connectionManager.DisconnectAsync(); LogoutEvent?.Invoke(this, null); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index 68d39e6ee..75a69e89d 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -165,7 +165,7 @@ private void TunnelHandler_CurrentTunnelPinged(object sender, EventArgs e) /// /// Clears event subscriptions and leaves the channel. /// - public async Task ClearAsync() + public async ValueTask ClearAsync() { gameBroadcastTimer.Enabled = false; @@ -211,7 +211,7 @@ private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e) /// /// Called when the local user has joined the game channel. /// - public async Task OnJoinedAsync() + public async ValueTask OnJoinedAsync() { FileHashCalculator fhc = new FileHashCalculator(); fhc.CalculateHashes(gameModes); @@ -219,8 +219,7 @@ public async Task OnJoinedAsync() if (IsHost) { await connectionManager.SendCustomMessageAsync(new QueuedMessage( - string.Format(IRCCommands.MODE + " {0} +klnNs {1} {2}", channel.ChannelName, - channel.Password, SGPlayers.Count), + FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} +{IRCChannelModes.DEFAULT} {channel.Password} {SGPlayers.Count}"), QueuedMessageType.SYSTEM_MESSAGE, 50)); await connectionManager.SendCustomMessageAsync(new QueuedMessage( @@ -252,7 +251,7 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage( UpdateDiscordPresence(true); } - private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) + private async ValueTask Channel_UserAddedAsync(ChannelUserEventArgs e) { PlayerInfo pInfo = new PlayerInfo(); pInfo.Name = e.User.IRCUser.Name; @@ -266,13 +265,13 @@ private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) UpdateDiscordPresence(); } - private async Task OnPlayerLeftAsync(UserNameEventArgs e) + private async ValueTask OnPlayerLeftAsync(UserNameEventArgs e) { await RemovePlayerAsync(e.UserName); UpdateDiscordPresence(); } - private async Task RemovePlayerAsync(string playerName) + private async ValueTask RemovePlayerAsync(string playerName) { int index = Players.FindIndex(p => p.Name == playerName); @@ -304,7 +303,7 @@ private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) protected override void AddNotice(string message, Color color) => channel.AddMessage(new ChatMessage(color, message)); - protected override async Task BroadcastOptionsAsync() + protected override async ValueTask BroadcastOptionsAsync() { if (!IsHost) return; @@ -327,17 +326,17 @@ protected override async Task BroadcastOptionsAsync() await channel.SendCTCPMessageAsync(message.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 10); } - protected override Task SendChatMessageAsync(string message) + protected override ValueTask SendChatMessageAsync(string message) { sndMessageSound.Play(); return channel.SendChatMessageAsync(message, chatColor); } - protected override Task RequestReadyStatusAsync() => + protected override ValueTask RequestReadyStatusAsync() => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_READY + " 1", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 10); - protected override async Task GetReadyNotificationAsync() + protected override async ValueTask GetReadyNotificationAsync() { await base.GetReadyNotificationAsync(); @@ -347,7 +346,7 @@ protected override async Task GetReadyNotificationAsync() await channel.SendCTCPMessageAsync(CnCNetCommands.GET_READY, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); } - protected override async Task NotAllPresentNotificationAsync() + protected override async ValueTask NotAllPresentNotificationAsync() { await base.NotAllPresentNotificationAsync(); @@ -361,7 +360,7 @@ await channel.SendCTCPMessageAsync(CnCNetCommands.NOT_ALL_PLAYERS_PRESENT, private void ShowTunnelSelectionWindow(string description) => tunnelSelectionWindow.Open(description, tunnelHandler.CurrentTunnel); - private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) + private async ValueTask TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) { await channel.SendCTCPMessageAsync( $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Hash}", @@ -372,7 +371,7 @@ await channel.SendCTCPMessageAsync( #region CTCP Handlers - private async Task HandleGetReadyNotificationAsync(string sender) + private async ValueTask HandleGetReadyNotificationAsync(string sender) { if (sender != hostName) return; @@ -380,7 +379,7 @@ private async Task HandleGetReadyNotificationAsync(string sender) await GetReadyNotificationAsync(); } - private async Task HandleNotAllPresentNotificationAsync(string sender) + private async ValueTask HandleNotAllPresentNotificationAsync(string sender) { if (sender != hostName) return; @@ -388,7 +387,7 @@ private async Task HandleNotAllPresentNotificationAsync(string sender) await NotAllPresentNotificationAsync(); } - private async Task HandleFileHashCommandAsync(string sender, string fileHash) + private async ValueTask HandleFileHashCommandAsync(string sender, string fileHash) { if (!IsHost) return; @@ -406,7 +405,7 @@ private async Task HandleFileHashCommandAsync(string sender, string fileHash) } } - private async Task HandleCheaterNotificationAsync(string sender, string cheaterName) + private async ValueTask HandleCheaterNotificationAsync(string sender, string cheaterName) { if (sender != hostName) return; @@ -428,7 +427,7 @@ private void HandleTunnelPing(string sender, int pingInMs) /// /// Handles an options broadcast sent by the game host. /// - private async Task HandleOptionsMessageAsync(string sender, string data) + private async ValueTask HandleOptionsMessageAsync(string sender, string data) { if (sender != hostName) return; @@ -527,7 +526,7 @@ private void HandleStartGameCommand(string sender, string data) LoadGame(); } - private async Task HandlePlayerReadyRequestAsync(string sender, int readyStatus) + private async ValueTask HandlePlayerReadyRequestAsync(string sender, int readyStatus) { PlayerInfo pInfo = Players.Find(p => p.Name == sender); @@ -575,7 +574,7 @@ private void HandleTunnelServerChange(CnCNetTunnel tunnel) #endregion - protected override async Task HostStartGameAsync() + protected override async ValueTask HostStartGameAsync() { AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(SGPlayers.Count); @@ -618,13 +617,13 @@ protected override void WriteSpawnIniAdditions(IniFile spawnIni) base.WriteSpawnIniAdditions(spawnIni); } - protected override async Task HandleGameProcessExitedAsync() + protected override async ValueTask HandleGameProcessExitedAsync() { await base.HandleGameProcessExitedAsync(); await ClearAsync(); } - protected override Task LeaveGameAsync() => ClearAsync(); + protected override ValueTask LeaveGameAsync() => ClearAsync(); public void ChangeChatColor(IRCColor chatColor) { @@ -632,7 +631,7 @@ public void ChangeChatColor(IRCColor chatColor) tbChatInput.TextColor = chatColor.XnaColor; } - private async Task BroadcastGameAsync() + private async ValueTask BroadcastGameAsync() { Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 6fee62082..38f2dfc85 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -592,7 +592,7 @@ private void PostUIInit() /// Displays a message when the IRC server has informed that the local user /// has been banned from a channel that they're attempting to join. /// - private async Task ConnectionManager_BannedFromChannelAsync(ChannelEventArgs e) + private async ValueTask ConnectionManager_BannedFromChannelAsync(ChannelEventArgs e) { var game = lbGameList.HostedGames.Find(hg => ((HostedCnCNetGame)hg).ChannelName == e.ChannelName); @@ -617,16 +617,16 @@ private async Task ConnectionManager_BannedFromChannelAsync(ChannelEventArgs e) } } - private Task SharedUILogic_GameProcessStartedAsync() + private ValueTask SharedUILogic_GameProcessStartedAsync() => connectionManager.SendCustomMessageAsync(new QueuedMessage( IRCCommands.AWAY + " " + (char)58 + "In-game", QueuedMessageType.SYSTEM_MESSAGE, 0)); - private Task SharedUILogic_GameProcessExitedAsync() + private ValueTask SharedUILogic_GameProcessExitedAsync() => connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.AWAY, QueuedMessageType.SYSTEM_MESSAGE, 0)); - private async Task Instance_SettingsSavedAsync() + private async ValueTask Instance_SettingsSavedAsync() { if (!connectionManager.IsConnected) return; @@ -801,7 +801,7 @@ private string GetJoinGameError(HostedCnCNetGame hg) return GetJoinGameErrorBase(); } - private async Task JoinSelectedGameAsync() + private async ValueTask JoinSelectedGameAsync() { var listedGame = (HostedCnCNetGame)lbGameList.SelectedItem?.Tag; if (listedGame == null) @@ -810,7 +810,7 @@ private async Task JoinSelectedGameAsync() await JoinGameByIndexAsync(hostedGameIndex, string.Empty); } - private async Task JoinGameByIndexAsync(int gameIndex, string password) + private async ValueTask JoinGameByIndexAsync(int gameIndex, string password) { string error = GetJoinGameErrorByIndex(gameIndex); if (!string.IsNullOrEmpty(error)) @@ -828,7 +828,7 @@ private async Task JoinGameByIndexAsync(int gameIndex, string password) /// The game to join. /// The password to join with. /// The message view/list to write error messages to. - private async Task JoinGameAsync(HostedCnCNetGame hg, string password, IMessageView messageView) + private async ValueTask JoinGameAsync(HostedCnCNetGame hg, string password, IMessageView messageView) { string error = GetJoinGameError(hg); if (!string.IsNullOrEmpty(error)) @@ -876,7 +876,7 @@ private async Task JoinGameAsync(HostedCnCNetGame hg, string password, IMe return true; } - private async Task JoinGameAsync(HostedCnCNetGame hg, string password) + private async ValueTask JoinGameAsync(HostedCnCNetGame hg, string password) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName))); @@ -906,13 +906,13 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.JOI QueuedMessageType.INSTANT_MESSAGE, 0)); } - private async Task GameChannel_TargetChangeTooFastAsync(object sender, MessageEventArgs e) + private async ValueTask GameChannel_TargetChangeTooFastAsync(object sender, MessageEventArgs e) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, e.Message)); await ClearGameJoinAttemptAsync((Channel)sender); } - private async Task OnGameLocked(object sender) + private async ValueTask OnGameLocked(object sender) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "The selected game is locked!".L10N("Client:Main:GameLocked"))); var channel = (Channel)sender; @@ -936,13 +936,13 @@ private HostedCnCNetGame FindGameByChannelName(string channelName) return (HostedCnCNetGame)game; } - private async Task GameChannel_InvalidPasswordEntered_NewGameAsync(object sender) + private async ValueTask GameChannel_InvalidPasswordEntered_NewGameAsync(object sender) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "Incorrect password!".L10N("Client:Main:PasswordWrong"))); await ClearGameJoinAttemptAsync((Channel)sender); } - private async Task GameChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) + private async ValueTask GameChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) { Channel gameChannel = (Channel)sender; @@ -955,7 +955,7 @@ private async Task GameChannel_UserAddedAsync(object sender, ChannelUserEventArg } } - private async Task ClearGameJoinAttemptAsync(Channel channel) + private async ValueTask ClearGameJoinAttemptAsync(Channel channel) { ClearGameChannelEvents(channel); await gameLobby.ClearAsync(); @@ -985,7 +985,7 @@ private void BtnNewGame_LeftClick(object sender, EventArgs e) gcw.Refresh(); } - private async Task Gcw_GameCreatedAsync(GameCreationEventArgs e) + private async ValueTask Gcw_GameCreatedAsync(GameCreationEventArgs e) { if (gameLobby.Enabled || gameLoadingLobby.Enabled) return; @@ -1014,7 +1014,7 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.JOI pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); } - private async Task Gcw_LoadedGameCreatedAsync(GameCreationEventArgs e) + private async ValueTask Gcw_LoadedGameCreatedAsync(GameCreationEventArgs e) { if (gameLobby.Enabled || gameLoadingLobby.Enabled) return; @@ -1036,7 +1036,7 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.JOI pmWindow.SetInviteChannelInfo(channelName, e.GameRoomName, string.IsNullOrEmpty(e.Password) ? string.Empty : e.Password); } - private async Task GameChannel_InvalidPasswordEntered_LoadedGameAsync(object sender) + private async ValueTask GameChannel_InvalidPasswordEntered_LoadedGameAsync(object sender) { var channel = (Channel)sender; channel.UserAdded -= gameLoadingChannel_UserAddedFunc; @@ -1045,7 +1045,7 @@ private async Task GameChannel_InvalidPasswordEntered_LoadedGameAsync(object sen isJoiningGame = false; } - private async Task GameLoadingChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) + private async ValueTask GameLoadingChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) { Channel gameLoadingChannel = (Channel)sender; @@ -1077,7 +1077,7 @@ private string RandomizeChannelName() private void Gcw_Cancelled(object sender, EventArgs e) => gameCreationPanel.Hide(); - private async Task TbChatInput_EnterPressedAsync() + private async ValueTask TbChatInput_EnterPressedAsync() { if (string.IsNullOrEmpty(tbChatInput.Text)) return; @@ -1129,7 +1129,7 @@ private void ConnectionManager_Disconnected(object sender, EventArgs e) gameCheckCancellation.Cancel(); } - private async Task ConnectionManager_WelcomeMessageReceivedAsync() + private async ValueTask ConnectionManager_WelcomeMessageReceivedAsync() { btnNewGame.AllowClick = true; btnJoinGame.AllowClick = true; @@ -1176,7 +1176,7 @@ private void ConnectionManager_PrivateCTCPReceived(object sender, PrivateCTCPEve Logger.Log("Unhandled private CTCP command: " + e.Message + " from " + e.Sender); } - private async Task HandleGameInviteCommandAsync(string sender, string argumentsString) + private async ValueTask HandleGameInviteCommandAsync(string sender, string argumentsString) { // arguments are semicolon-delimited var arguments = argumentsString.Split(';'); @@ -1245,7 +1245,7 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.PRI sndGameInviteReceived.Play(); } - private async Task AffirmativeClickedActionAsync(string channelName, string password, string sender, UserChannelPair invitationIdentity) + private async ValueTask AffirmativeClickedActionAsync(string channelName, string password, string sender, UserChannelPair invitationIdentity) { // if we're currently in a game lobby, first leave that channel if (isInGameRoom) @@ -1279,7 +1279,7 @@ private void HandleGameInvitationFailedNotification(string sender) } } - private async Task DdCurrentChannel_SelectedIndexChangedAsync() + private async ValueTask DdCurrentChannel_SelectedIndexChangedAsync() { if (currentChatChannel != null) { @@ -1545,7 +1545,7 @@ private void UpdateMessageBox_YesClicked(XNAMessageBox messageBox) => private void UpdateMessageBox_NoClicked(XNAMessageBox messageBox) => updateDenied = true; - private async Task BtnLogout_LeftClickAsync() + private async ValueTask BtnLogout_LeftClickAsync() { if (isInGameRoom) { @@ -1662,7 +1662,7 @@ private HostedCnCNetGame GetHostedGameForUser(IRCUser user) /// /// The user to join. /// The message view/list to write error messages to. - private async Task JoinUserAsync(IRCUser user, IMessageView messageView) + private async ValueTask JoinUserAsync(IRCUser user, IMessageView messageView) { if (user == null) { diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs index 0c27c1c17..72badfeca 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs @@ -106,7 +106,7 @@ public override void Initialize() AddItem(openLinkItem); } - private async Task InviteAsync() + private async ValueTask InviteAsync() { // note it's assumed that if the channel name is specified, the game name must be also if (string.IsNullOrEmpty(contextMenuData.inviteChannelName) || ProgramConstants.IsInGame) @@ -187,7 +187,7 @@ private void CopyLink(string link) } } - private async Task GetIrcUserIdentAsync(Action callback) + private async ValueTask GetIrcUserIdentAsync(Action callback) { var ircUser = GetIrcUser(); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs index 8bdeafffe..7a021adc9 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs @@ -517,7 +517,7 @@ private void ShowNotification(IRCUser ircUser, string message) private int FindItemIndexForName(string userName) => lbUserList.Items.FindIndex(MatchItemForName(userName)); - private async Task TbMessageInput_EnterPressedAsync() + private async ValueTask TbMessageInput_EnterPressedAsync() { if (string.IsNullOrEmpty(tbMessageInput.Text)) return; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index bb08aa437..c0cc69bfa 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -219,12 +219,12 @@ public override void Initialize() /// private void ResetDiscordPresence() => discordHandler.UpdatePresence(); - protected virtual Task LeaveGameAsync() + protected virtual ValueTask LeaveGameAsync() { GameLeft?.Invoke(this, EventArgs.Empty); ResetDiscordPresence(); - return Task.CompletedTask; + return ValueTask.CompletedTask; } private void fsw_Created(object sender, FileSystemEventArgs e) => @@ -240,7 +240,7 @@ private void HandleFSWEvent(FileSystemEventArgs e) } } - private async Task BtnLoadGame_LeftClickAsync() + private async ValueTask BtnLoadGame_LeftClickAsync() { if (!IsHost) { @@ -263,9 +263,9 @@ private async Task BtnLoadGame_LeftClickAsync() await HostStartGameAsync(); } - protected abstract Task RequestReadyStatusAsync(); + protected abstract ValueTask RequestReadyStatusAsync(); - protected virtual Task GetReadyNotificationAsync() + protected virtual ValueTask GetReadyNotificationAsync() { AddNotice("The game host wants to load the game but cannot because not all players are ready!".L10N("Client:Main:GetReadyPlease")); @@ -275,16 +275,16 @@ protected virtual Task GetReadyNotificationAsync() WindowManager.FlashWindow(); #endif - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected virtual Task NotAllPresentNotificationAsync() + protected virtual ValueTask NotAllPresentNotificationAsync() { AddNotice("You cannot load the game before all players are present.".L10N("Client:Main:NotAllPresent")); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected abstract Task HostStartGameAsync(); + protected abstract ValueTask HostStartGameAsync(); protected void LoadGame() { @@ -348,7 +348,7 @@ protected void LoadGame() private void SharedUILogic_GameProcessExited() => AddCallback(() => HandleGameProcessExitedAsync().HandleTask()); - protected virtual Task HandleGameProcessExitedAsync() + protected virtual ValueTask HandleGameProcessExitedAsync() { fsw.EnableRaisingEvents = false; @@ -371,7 +371,7 @@ protected virtual Task HandleGameProcessExitedAsync() UpdateDiscordPresence(true); - return Task.CompletedTask; + return ValueTask.CompletedTask; } protected virtual void WriteSpawnIniAdditions(IniFile spawnIni) @@ -479,7 +479,7 @@ protected void CopyPlayerDataToUI() } } - private async Task DdSavedGame_SelectedIndexChangedAsync() + private async ValueTask DdSavedGame_SelectedIndexChangedAsync() { if (!IsHost) return; @@ -494,7 +494,7 @@ private async Task DdSavedGame_SelectedIndexChangedAsync() UpdateDiscordPresence(); } - private async Task TbChatInput_EnterPressedAsync() + private async ValueTask TbChatInput_EnterPressedAsync() { if (string.IsNullOrEmpty(tbChatInput.Text)) return; @@ -507,9 +507,9 @@ private async Task TbChatInput_EnterPressedAsync() /// Override in a derived class to broadcast player ready statuses and the selected /// saved game to players. /// - protected abstract Task BroadcastOptionsAsync(); + protected abstract ValueTask BroadcastOptionsAsync(); - protected abstract Task SendChatMessageAsync(string message); + protected abstract ValueTask SendChatMessageAsync(string message); public override void Draw(GameTime gameTime) { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 747c92596..b7c73b439 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -39,6 +39,7 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private const int PRIORITY_START_GAME = 10; private const int PINNED_DYNAMIC_TUNNELS = 10; private const int P2P_PING_TIMEOUT = 1000; + private const ushort MAX_REMOTE_PLAYERS = 7; private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; @@ -54,7 +55,7 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private readonly List chatCommandDownloadedMaps = new(); private readonly List<(string RemotePlayerName, CnCNetTunnel Tunnel, int CombinedPing)> playerTunnels = new(); private readonly List<(List RemotePlayerNames, V3GameTunnelHandler Tunnel)> v3GameTunnelHandlers = new(); - private readonly List<(string RemotePlayerName, ushort[] RemotePorts, List<(IPAddress RemoteIpAddress, long Ping)> LocalPingResults, List<(IPAddress RemoteIpAddress, long Ping)> RemotePingResults, bool Enabled)> p2pPlayers = new(); + private readonly List p2pPlayers = new(); private TunnelSelectionWindow tunnelSelectionWindow; private XNAClientButton btnChangeTunnel; @@ -296,7 +297,7 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("Client:Main:SelectTunnelServer")); - public async Task SetUpAsync( + public async ValueTask SetUpAsync( Channel channel, bool isHost, int playerLimit, @@ -353,7 +354,7 @@ public async Task SetUpAsync( Refresh(isHost); } - public async Task OnJoinedAsync() + public async ValueTask OnJoinedAsync() { var fhc = new FileHashCalculator(); @@ -376,7 +377,7 @@ public async Task OnJoinedAsync() if (IsHost) { await connectionManager.SendCustomMessageAsync(new( - FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} +klnNs {channel.Password} {playerLimit}"), + FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} +{IRCChannelModes.DEFAULT} {channel.Password} {playerLimit}"), QueuedMessageType.SYSTEM_MESSAGE, 50)); @@ -409,7 +410,7 @@ await connectionManager.SendCustomMessageAsync(new( UpdateDiscordPresence(true); } - private async Task UpdatePingAsync() + private async ValueTask UpdatePingAsync() { int ping; @@ -464,7 +465,7 @@ private void PrintTunnelServerInformation(string s) private void ShowTunnelSelectionWindow(string description) => tunnelSelectionWindow.Open(description, tunnelHandler.CurrentTunnel); - private async Task TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) + private async ValueTask TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArgs e) { await channel.SendCTCPMessageAsync( $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Hash}", @@ -479,7 +480,7 @@ public void ChangeChatColor(IRCColor chatColor) tbChatInput.TextColor = chatColor.XnaColor; } - public override async Task ClearAsync() + public override async ValueTask ClearAsync() { await base.ClearAsync(); @@ -524,14 +525,12 @@ public override async Task ClearAsync() CloseP2PPortsAsync().HandleTask(); } - private async Task CloseP2PPortsAsync() + private async ValueTask CloseP2PPortsAsync() { try { foreach (ushort p2pPort in p2pPorts) - { await internetGatewayDevice.CloseIpV4PortAsync(p2pPort); - } } catch (Exception ex) { @@ -545,9 +544,7 @@ private async Task CloseP2PPortsAsync() try { foreach (ushort p2pIpV6PortId in p2pIpV6PortIds) - { await internetGatewayDevice.CloseIpV6PortAsync(p2pIpV6PortId); - } } catch (Exception ex) { @@ -559,7 +556,7 @@ private async Task CloseP2PPortsAsync() } } - public async Task LeaveGameLobbyAsync() + public async ValueTask LeaveGameLobbyAsync() { if (IsHost) { @@ -571,7 +568,7 @@ public async Task LeaveGameLobbyAsync() await channel.LeaveAsync(); } - private async Task HandleConnectionLossAsync() + private async ValueTask HandleConnectionLossAsync() { await ClearAsync(); Disable(); @@ -594,7 +591,7 @@ private void Channel_UserNameChanged(object sender, UserNameChangedEventArgs e) } } - protected override Task BtnLeaveGame_LeftClickAsync() + protected override ValueTask BtnLeaveGame_LeftClickAsync() => LeaveGameLobbyAsync(); protected override void UpdateDiscordPresence(bool resetTimer = false) @@ -628,7 +625,7 @@ protected override void UpdateDiscordPresence(bool resetTimer = false) resetTimer); } - private async Task ChannelUserLeftAsync(UserNameEventArgs e) + private async ValueTask ChannelUserLeftAsync(UserNameEventArgs e) { await RemovePlayerAsync(e.UserName); @@ -644,7 +641,7 @@ private async Task ChannelUserLeftAsync(UserNameEventArgs e) } } - private async Task Channel_UserKickedAsync(UserNameEventArgs e) + private async ValueTask Channel_UserKickedAsync(UserNameEventArgs e) { if (e.UserName == ProgramConstants.PLAYERNAME) { @@ -673,7 +670,7 @@ private async Task Channel_UserKickedAsync(UserNameEventArgs e) } } - private async Task Channel_UserListReceivedAsync() + private async ValueTask Channel_UserListReceivedAsync() { if (!IsHost) { @@ -688,7 +685,7 @@ private async Task Channel_UserListReceivedAsync() UpdateDiscordPresence(); } - private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) + private async ValueTask Channel_UserAddedAsync(ChannelUserEventArgs e) { var pInfo = new PlayerInfo(e.User.IRCUser.Name); @@ -736,7 +733,7 @@ private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) } } - private async Task RemovePlayerAsync(string playerName) + private async ValueTask RemovePlayerAsync(string playerName) { AbortGameStart(); @@ -817,7 +814,7 @@ private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) /// /// Starts the game for the game host. /// - protected override async Task HostLaunchGameAsync() + protected override async ValueTask HostLaunchGameAsync() { if (Players.Count > 1) { @@ -841,7 +838,7 @@ protected override async Task HostLaunchGameAsync() await StartGameAsync(); } - private async Task HostLaunchGameV2Async() + private async ValueTask HostLaunchGameV2Async() { List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(Players.Count); @@ -881,7 +878,7 @@ private string SetGamePlayerPortsV2(IReadOnlyList playerPorts) return sb.ToString(); } - private async Task HostLaunchGameV3Async() + private async ValueTask HostLaunchGameV3Async() { btnLaunchGame.InputEnabled = false; @@ -953,16 +950,16 @@ private void StartV3ConnectionListeners() v3GameTunnelHandlers.Clear(); gameStartCancellationTokenSource?.Dispose(); - gameStartCancellationTokenSource = new CancellationTokenSource(); + gameStartCancellationTokenSource = new(); if (!dynamicTunnelsEnabled) { var gameTunnelHandler = new V3GameTunnelHandler(); - gameTunnelHandler.RaiseConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - gameTunnelHandler.RaiseConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - gameTunnelHandler.SetUp(new IPEndPoint(tunnelHandler.CurrentTunnel.IPAddress, tunnelHandler.CurrentTunnel.Port), 0, gameLocalPlayerId, gameStartCancellationTokenSource.Token); + gameTunnelHandler.SetUp(new(tunnelHandler.CurrentTunnel.IPAddress, tunnelHandler.CurrentTunnel.Port), 0, gameLocalPlayerId, gameStartCancellationTokenSource.Token); gameTunnelHandler.ConnectToTunnel(); v3GameTunnelHandlers.Add(new(Players.Where(q => q != FindLocalPlayer()).Select(q => q.Name).ToList(), gameTunnelHandler)); } @@ -975,21 +972,24 @@ private void StartV3ConnectionListeners() foreach (var (remotePlayerName, remotePorts, localPingResults, remotePingResults, _) in p2pPlayers.Where(q => q.RemotePingResults.Any() && q.Enabled)) { IEnumerable<(IPAddress IpAddress, long CombinedPing)> combinedPingResults = localPingResults.Select(q => (q.RemoteIpAddress, q.Ping + remotePingResults.SingleOrDefault(r => r.RemoteIpAddress.Equals(q.RemoteIpAddress)).Ping)); - (IPAddress ipAddress, long combinedPing) = combinedPingResults.OrderByDescending(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).MinBy(q => q.CombinedPing); + (IPAddress ipAddress, long combinedPing) = combinedPingResults.OrderBy(q => q.CombinedPing).ThenByDescending(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).First(); if (combinedPing < playerTunnels.Single(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).CombinedPing) { - int index = Players.Where(q => q != FindLocalPlayer()).OrderBy(q => q.Name).ToList().FindIndex(q => q.Name.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)); - ushort localPort = p2pPorts[6 - index]; - ushort remotePort = remotePorts[6 - index]; + var allPlayerNames = Players.Select(q => q.Name).OrderBy(q => q, StringComparer.OrdinalIgnoreCase).ToList(); + string localPlayerName = FindLocalPlayer().Name; + var remotePlayerNames = allPlayerNames.Where(q => !q.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); + var tunnelClientPlayerNames = allPlayerNames.Where(q => !q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); + ushort localPort = p2pPorts[6 - remotePlayerNames.FindIndex(q => q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase))]; + ushort remotePort = remotePorts[6 - tunnelClientPlayerNames.FindIndex(q => q.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase))]; var p2pLocalTunnelHandler = new V3GameTunnelHandler(); - p2pLocalTunnelHandler.RaiseConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - p2pLocalTunnelHandler.RaiseConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + p2pLocalTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + p2pLocalTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - p2pLocalTunnelHandler.SetUp(new IPEndPoint(ipAddress, remotePort), localPort, gameLocalPlayerId, gameStartCancellationTokenSource.Token); + p2pLocalTunnelHandler.SetUp(new(ipAddress, remotePort), localPort, gameLocalPlayerId, gameStartCancellationTokenSource.Token); p2pLocalTunnelHandler.ConnectToTunnel(); - v3GameTunnelHandlers.Add(new(new List { remotePlayerName }, p2pLocalTunnelHandler)); + v3GameTunnelHandlers.Add(new(new() { remotePlayerName }, p2pLocalTunnelHandler)); p2pPlayerTunnels.Add(remotePlayerName); } } @@ -999,10 +999,10 @@ private void StartV3ConnectionListeners() { var gameTunnelHandler = new V3GameTunnelHandler(); - gameTunnelHandler.RaiseConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - gameTunnelHandler.RaiseConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - gameTunnelHandler.SetUp(new IPEndPoint(tunnelGrouping.Key.IPAddress, tunnelGrouping.Key.Port), 0, gameLocalPlayerId, gameStartCancellationTokenSource.Token); + gameTunnelHandler.SetUp(new(tunnelGrouping.Key.IPAddress, tunnelGrouping.Key.Port), 0, gameLocalPlayerId, gameStartCancellationTokenSource.Token); gameTunnelHandler.ConnectToTunnel(); v3GameTunnelHandlers.Add(new(tunnelGrouping.Select(q => q.Name).ToList(), gameTunnelHandler)); } @@ -1013,11 +1013,11 @@ private void StartV3ConnectionListeners() gameStartTimer.Start(); } - private async Task GameTunnelHandler_Connected_CallbackAsync() + private async ValueTask GameTunnelHandler_Connected_CallbackAsync() { if (dynamicTunnelsEnabled) { - if (v3GameTunnelHandlers.Any() && v3GameTunnelHandlers.TrueForAll(q => q.Tunnel.IsConnected)) + if (v3GameTunnelHandlers.Any() && v3GameTunnelHandlers.TrueForAll(q => q.Tunnel.ConnectSucceeded)) SetLocalPlayerConnected(); } else @@ -1033,7 +1033,7 @@ private void SetLocalPlayerConnected() isPlayerConnected[Players.FindIndex(p => p == FindLocalPlayer())] = true; } - private async Task GameTunnelHandler_ConnectionFailed_CallbackAsync() + private async ValueTask GameTunnelHandler_ConnectionFailed_CallbackAsync() { await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_FAIL, QueuedMessageType.INSTANT_MESSAGE, 0); HandleTunnelFail(ProgramConstants.PLAYERNAME); @@ -1047,7 +1047,7 @@ private void HandleTunnelFail(string playerName) AbortGameStart(); } - private async Task HandlePlayerConnectedToTunnelAsync(string playerName) + private async ValueTask HandlePlayerConnectedToTunnelAsync(string playerName) { if (!isStartingGame) return; @@ -1067,37 +1067,31 @@ private async Task HandlePlayerConnectedToTunnelAsync(string playerName) await LaunchGameV3Async(); } - private async Task LaunchGameV3Async() + private async ValueTask LaunchGameV3Async() { Logger.Log("All players are connected, starting game!"); AddNotice("All players have connected...".L10N("Client:Main:PlayersConnected")); - List playerPorts = new(); + List usedPorts = new(p2pPorts); - foreach (V3GameTunnelHandler dynamicV3GameTunnelHandler in v3GameTunnelHandlers.Select(q => q.Tunnel)) + foreach ((List remotePlayerNames, V3GameTunnelHandler v3GameTunnelHandler) in v3GameTunnelHandlers) { - var currentTunnelPlayers = Players.Where(q => v3GameTunnelHandlers.Single(r => r.Tunnel == dynamicV3GameTunnelHandler).RemotePlayerNames.Contains(q.Name)).ToList(); + var currentTunnelPlayers = Players.Where(q => remotePlayerNames.Contains(q.Name)).ToList(); IEnumerable indexes = currentTunnelPlayers.Select(q => q.Index); var playerIds = indexes.Select(q => gamePlayerIds[q]).ToList(); - List createdPlayerPorts = dynamicV3GameTunnelHandler.CreatePlayerConnections(playerIds); + List createdLocalPlayerPorts = v3GameTunnelHandler.CreatePlayerConnections(playerIds).ToList(); int i = 0; foreach (PlayerInfo currentTunnelPlayer in currentTunnelPlayers) - { - currentTunnelPlayer.Port = createdPlayerPorts.Skip(i++).Take(1).Single(); - } + currentTunnelPlayer.Port = createdLocalPlayerPorts.Skip(i++).Take(1).Single(); - playerPorts.AddRange(createdPlayerPorts); + usedPorts.AddRange(createdLocalPlayerPorts); } - playerPorts.AddRange(p2pPorts); - - ushort gamePort = NetworkHelper.GetFreeUdpPort(playerPorts); - foreach (V3GameTunnelHandler v3GameTunnelHandler in v3GameTunnelHandlers.Select(q => q.Tunnel)) - { - v3GameTunnelHandler.StartPlayerConnections(gamePort); - } + v3GameTunnelHandler.StartPlayerConnections(); + + int gamePort = NetworkHelper.GetFreeUdpPorts(usedPorts, 1).Single(); FindLocalPlayer().Port = gamePort; @@ -1127,7 +1121,7 @@ protected override IPAddress GetIPAddressForPlayer(PlayerInfo player) return base.GetIPAddressForPlayer(player); } - protected override Task RequestPlayerOptionsAsync(int side, int color, int start, int team) + protected override ValueTask RequestPlayerOptionsAsync(int side, int color, int start, int team) { byte[] value = { @@ -1144,7 +1138,7 @@ protected override Task RequestPlayerOptionsAsync(int side, int color, int start 6); } - protected override async Task RequestReadyStatusAsync() + protected override async ValueTask RequestReadyStatusAsync() { if (Map == null || GameMode == null) { @@ -1173,7 +1167,7 @@ protected override async Task RequestReadyStatusAsync() /// /// Handles player option requests received from non-host players. /// - private async Task HandleOptionsRequestAsync(string playerName, int options) + private async ValueTask HandleOptionsRequestAsync(string playerName, int options) { if (!IsHost) return; @@ -1237,7 +1231,7 @@ private async Task HandleOptionsRequestAsync(string playerName, int options) /// /// Handles "I'm ready" messages received from non-host players. /// - private async Task HandleReadyRequestAsync(string playerName, int readyStatus) + private async ValueTask HandleReadyRequestAsync(string playerName, int readyStatus) { if (!IsHost) return; @@ -1257,7 +1251,7 @@ private async Task HandleReadyRequestAsync(string playerName, int readyStatus) /// /// Broadcasts player options to non-host players. /// - protected override Task BroadcastPlayerOptionsAsync() + protected override ValueTask BroadcastPlayerOptionsAsync() { // Broadcast player options var sb = new StringBuilder(CnCNetCommands.PLAYER_OPTIONS + " "); @@ -1300,13 +1294,13 @@ protected override Task BroadcastPlayerOptionsAsync() return channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_PLAYERS_MESSAGE, 11); } - protected override async Task PlayerExtraOptions_OptionsChangedAsync() + protected override async ValueTask PlayerExtraOptions_OptionsChangedAsync() { await base.PlayerExtraOptions_OptionsChangedAsync(); await BroadcastPlayerExtraOptionsAsync(); } - protected override async Task BroadcastPlayerExtraOptionsAsync() + protected override async ValueTask BroadcastPlayerExtraOptionsAsync() { if (!IsHost) return; @@ -1316,19 +1310,14 @@ protected override async Task BroadcastPlayerExtraOptionsAsync() await channel.SendCTCPMessageAsync(playerExtraOptions.ToCncnetMessage(), QueuedMessageType.GAME_PLAYERS_EXTRA_MESSAGE, 11, true); } - private Task BroadcastPlayerTunnelPingsAsync() + private ValueTask BroadcastPlayerTunnelPingsAsync() => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_TUNNEL_PINGS + " " + pinnedTunnelPingsMessage, QueuedMessageType.SYSTEM_MESSAGE, 10); - private async Task BroadcastPlayerP2PRequestAsync() + private async ValueTask BroadcastPlayerP2PRequestAsync() { if (!p2pPorts.Any()) { - List p2pReservedPorts = new(); - - for (int i = 0; i < 7; i++) - { - p2pReservedPorts.Add(NetworkHelper.GetFreeUdpPort(Array.Empty())); - } + IEnumerable p2pReservedPorts = NetworkHelper.GetFreeUdpPorts(Array.Empty(), MAX_REMOTE_PLAYERS); try { @@ -1337,7 +1326,7 @@ private async Task BroadcastPlayerP2PRequestAsync() catch (Exception ex) { ProgramConstants.LogException(ex, "Could not open UPnP P2P ports."); - AddNotice(string.Format(CultureInfo.CurrentCulture, "Could not open UPnP P2P ports".L10N("Client:Main:UPnPP2PFailed"))); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Could not open P2P ports. Check that UPnP port mapping is enabled for this device on your router/modem.".L10N("Client:Main:UPnPP2PFailed")), Color.Orange); return; } @@ -1347,7 +1336,7 @@ private async Task BroadcastPlayerP2PRequestAsync() await SendPlayerP2PRequestAsync(); } - private Task SendPlayerP2PRequestAsync() + private ValueTask SendPlayerP2PRequestAsync() => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_REQUEST + $" {publicIpV4Address};{publicIpV6Address};{(!p2pPorts.Any() ? null : p2pPorts.Select(q => q.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}", QueuedMessageType.SYSTEM_MESSAGE, 10); /// @@ -1458,7 +1447,7 @@ private void ApplyPlayerOptions(string sender, string message) /// Broadcasts game options to non-host players /// when the host has changed an option. /// - protected override async Task OnGameOptionChangedAsync() + protected override async ValueTask OnGameOptionChangedAsync() { await base.OnGameOptionChangedAsync(); @@ -1498,21 +1487,21 @@ protected override async Task OnGameOptionChangedAsync() sb.Append(RandomSeed); sb.Append(Convert.ToInt32(RemoveStartingLocations)); sb.Append(Map.UntranslatedName); - sb.Append(Convert.ToInt32(dynamicTunnelsEnabled)); // todo get from UI + sb.Append(Convert.ToInt32(dynamicTunnelsEnabled)); await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); } - private async Task ToggleDynamicTunnelsAsync() + private async ValueTask ToggleDynamicTunnelsAsync() { await ChangeDynamicTunnelsSettingAsync(!dynamicTunnelsEnabled); await OnGameOptionChangedAsync(); if (!dynamicTunnelsEnabled) - await TunnelSelectionWindow_TunnelSelectedAsync(new TunnelEventArgs(initialTunnel)); + await TunnelSelectionWindow_TunnelSelectedAsync(new(initialTunnel)); } - private async Task ToggleP2PAsync() + private async ValueTask ToggleP2PAsync() { p2pEnabled = !p2pEnabled; @@ -1537,7 +1526,7 @@ private async Task ToggleP2PAsync() /// /// Handles game option messages received from the game host. /// - private async Task ApplyGameOptionsAsync(string sender, string message) + private async ValueTask ApplyGameOptionsAsync(string sender, string message) { if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; @@ -1709,7 +1698,7 @@ private async Task ApplyGameOptionsAsync(string sender, string message) await ChangeDynamicTunnelsSettingAsync(newDynamicTunnelsSetting); } - private async Task ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsEnabledValue) + private async ValueTask ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsEnabledValue) { dynamicTunnelsEnabled = newDynamicTunnelsEnabledValue; @@ -1728,7 +1717,7 @@ private async Task ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsEnable } } - private async Task RequestMapAsync() + private async ValueTask RequestMapAsync() { if (UserINISettings.Instance.EnableMapSharing) { @@ -1744,7 +1733,7 @@ private async Task RequestMapAsync() } } - private Task ShowOfficialMapMissingMessageAsync(string sha1) + private ValueTask ShowOfficialMapMissingMessageAsync(string sha1) { AddNotice(("The game host has selected an official map that doesn't exist on your installation. " + "This could mean that the game host has modified game files, or is running a different game version. " + @@ -1760,7 +1749,7 @@ private void MapSharingConfirmationPanel_MapDownloadConfirmed(object sender, Eve MapSharer.DownloadMap(lastMapHash, localGame, lastMapName); } - protected override Task ChangeMapAsync(GameModeMap gameModeMap) + protected override ValueTask ChangeMapAsync(GameModeMap gameModeMap) { mapSharingConfirmationPanel.Disable(); return base.ChangeMapAsync(gameModeMap); @@ -1770,7 +1759,7 @@ protected override Task ChangeMapAsync(GameModeMap gameModeMap) /// Signals other players that the local player has returned from the game, /// and unlocks the game as well as generates a new random seed as the game host. /// - protected override async Task GameProcessExitedAsync() + protected override async ValueTask GameProcessExitedAsync() { await base.GameProcessExitedAsync(); await channel.SendCTCPMessageAsync(CnCNetCommands.RETURN, QueuedMessageType.SYSTEM_MESSAGE, 20); @@ -1796,7 +1785,7 @@ protected override async Task GameProcessExitedAsync() /// /// Handles the "START" (game start) command sent by the game host. /// - private async Task ClientLaunchGameV2Async(string sender, string message) + private async ValueTask ClientLaunchGameV2Async(string sender, string message) { if (tunnelHandler.CurrentTunnel.Version != Constants.TUNNEL_VERSION_2) return; @@ -1844,7 +1833,7 @@ private async Task ClientLaunchGameV2Async(string sender, string message) await StartGameAsync(); } - protected override async Task StartGameAsync() + protected override async ValueTask StartGameAsync() { AddNotice("Starting game...".L10N("Client:Main:StartingGame")); @@ -1868,7 +1857,7 @@ protected override void WriteSpawnIniAdditions(IniFile iniFile) { base.WriteSpawnIniAdditions(iniFile); - if (!p2pEnabled && !UserINISettings.Instance.UseDynamicTunnels && tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_2) + if (tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_2) { iniFile.SetStringValue("Tunnel", "Ip", tunnelHandler.CurrentTunnel.Address); iniFile.SetIntValue("Tunnel", "Port", tunnelHandler.CurrentTunnel.Port); @@ -1885,7 +1874,7 @@ protected override void WriteSpawnIniAdditions(IniFile iniFile) iniFile.SetIntValue("Settings", "Port", localPlayer.Port); } - protected override Task SendChatMessageAsync(string message) => channel.SendChatMessageAsync(message, chatColor); + protected override ValueTask SendChatMessageAsync(string message) => channel.SendChatMessageAsync(message, chatColor); private void HandleNotification(string sender, Action handler) { @@ -1903,7 +1892,7 @@ private void HandleIntNotification(string sender, int parameter, Action han handler(parameter); } - protected override async Task GetReadyNotificationAsync() + protected override async ValueTask GetReadyNotificationAsync() { await base.GetReadyNotificationAsync(); #if WINFORMS @@ -1915,7 +1904,7 @@ protected override async Task GetReadyNotificationAsync() await channel.SendCTCPMessageAsync(CnCNetCommands.GET_READY_LOBBY, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); } - protected override async Task AISpectatorsNotificationAsync() + protected override async ValueTask AISpectatorsNotificationAsync() { await base.AISpectatorsNotificationAsync(); @@ -1923,7 +1912,7 @@ protected override async Task AISpectatorsNotificationAsync() await channel.SendCTCPMessageAsync(CnCNetCommands.AI_SPECTATORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } - protected override async Task InsufficientPlayersNotificationAsync() + protected override async ValueTask InsufficientPlayersNotificationAsync() { await base.InsufficientPlayersNotificationAsync(); @@ -1931,7 +1920,7 @@ protected override async Task InsufficientPlayersNotificationAsync() await channel.SendCTCPMessageAsync(CnCNetCommands.INSUFFICIENT_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } - protected override async Task TooManyPlayersNotificationAsync() + protected override async ValueTask TooManyPlayersNotificationAsync() { await base.TooManyPlayersNotificationAsync(); @@ -1939,7 +1928,7 @@ protected override async Task TooManyPlayersNotificationAsync() await channel.SendCTCPMessageAsync(CnCNetCommands.TOO_MANY_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } - protected override async Task SharedColorsNotificationAsync() + protected override async ValueTask SharedColorsNotificationAsync() { await base.SharedColorsNotificationAsync(); @@ -1947,7 +1936,7 @@ protected override async Task SharedColorsNotificationAsync() await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_COLORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } - protected override async Task SharedStartingLocationNotificationAsync() + protected override async ValueTask SharedStartingLocationNotificationAsync() { await base.SharedStartingLocationNotificationAsync(); @@ -1955,7 +1944,7 @@ protected override async Task SharedStartingLocationNotificationAsync() await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_STARTING_LOCATIONS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } - protected override async Task LockGameNotificationAsync() + protected override async ValueTask LockGameNotificationAsync() { await base.LockGameNotificationAsync(); @@ -1963,7 +1952,7 @@ protected override async Task LockGameNotificationAsync() await channel.SendCTCPMessageAsync(CnCNetCommands.LOCK_GAME, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } - protected override async Task NotVerifiedNotificationAsync(int playerIndex) + protected override async ValueTask NotVerifiedNotificationAsync(int playerIndex) { await base.NotVerifiedNotificationAsync(playerIndex); @@ -1971,7 +1960,7 @@ protected override async Task NotVerifiedNotificationAsync(int playerIndex) await channel.SendCTCPMessageAsync(CnCNetCommands.NOT_VERIFIED + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); } - protected override async Task StillInGameNotificationAsync(int playerIndex) + protected override async ValueTask StillInGameNotificationAsync(int playerIndex) { await base.StillInGameNotificationAsync(playerIndex); @@ -2004,7 +1993,7 @@ private void HandleTunnelPing(string sender, int ping) } } - private async Task FileHashNotificationAsync(string sender, string filesHash) + private async ValueTask FileHashNotificationAsync(string sender, string filesHash) { if (!IsHost) return; @@ -2031,7 +2020,7 @@ private void CheaterNotification(string sender, string cheaterName) AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} has different files compared to the game host. Either {0} or the game host could be cheating.".L10N("Client:Main:DifferentFileCheating"), cheaterName), Color.Red); } - protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) + protected override async ValueTask BroadcastDiceRollAsync(int dieSides, int[] results) { string resultString = string.Join(",", results); @@ -2039,7 +2028,7 @@ protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results PrintDiceRollResult(ProgramConstants.PLAYERNAME, dieSides, results); } - protected override async Task HandleLockGameButtonClickAsync() + protected override async ValueTask HandleLockGameButtonClickAsync() { if (!Locked) { @@ -2060,20 +2049,20 @@ protected override async Task HandleLockGameButtonClickAsync() } } - protected override async Task LockGameAsync() + protected override async ValueTask LockGameAsync() { await connectionManager.SendCustomMessageAsync( - new(string.Format(IRCCommands.MODE + " {0} +i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); + new(FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} +{IRCChannelModes.INVITE_ONLY}"), QueuedMessageType.INSTANT_MESSAGE, -1)); Locked = true; btnLockGame.Text = "Unlock Game".L10N("Client:Main:UnlockGame"); AccelerateGameBroadcasting(); } - protected override async Task UnlockGameAsync(bool announce) + protected override async ValueTask UnlockGameAsync(bool announce) { await connectionManager.SendCustomMessageAsync( - new(string.Format(IRCCommands.MODE + " {0} -i", channel.ChannelName), QueuedMessageType.INSTANT_MESSAGE, -1)); + new(FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} -{IRCChannelModes.INVITE_ONLY}"), QueuedMessageType.INSTANT_MESSAGE, -1)); Locked = false; @@ -2084,7 +2073,7 @@ await connectionManager.SendCustomMessageAsync( AccelerateGameBroadcasting(); } - protected override async Task KickPlayerAsync(int playerIndex) + protected override async ValueTask KickPlayerAsync(int playerIndex) { if (playerIndex >= Players.Count) return; @@ -2095,7 +2084,7 @@ protected override async Task KickPlayerAsync(int playerIndex) await channel.SendKickMessageAsync(pInfo.Name, 8); } - protected override async Task BanPlayerAsync(int playerIndex) + protected override async ValueTask BanPlayerAsync(int playerIndex) { if (playerIndex >= Players.Count) return; @@ -2114,7 +2103,7 @@ protected override async Task BanPlayerAsync(int playerIndex) private void HandleCheatDetectedMessage(string sender) => AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} has modified game files during the client session. They are likely attempting to cheat!".L10N("Client:Main:PlayerModifyFileCheat"), sender), Color.Red); - private async Task HandleTunnelServerChangeMessageAsync(string sender, string hash) + private async ValueTask HandleTunnelServerChangeMessageAsync(string sender, string hash) { if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; @@ -2159,7 +2148,7 @@ private void HandleTunnelPingsMessage(string playerName, string tunnelPingsMessa if (hash is null) { - AddNotice(string.Format(CultureInfo.CurrentCulture, "No common tunnel server found for: {0}".L10N("Client:Main:NoCommonTunnel"), playerName)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "No common tunnel found for: {0}".L10N("Client:Main:NoCommonTunnel"), playerName)); } else { @@ -2173,13 +2162,13 @@ private void HandleTunnelPingsMessage(string playerName, string tunnelPingsMessa } playerTunnels.Add(new(playerName, tunnel, combinedPing)); - AddNotice(string.Format(CultureInfo.CurrentCulture, "Dynamic tunnel server negotiated with {0}: {1} ({2}ms)".L10N("Client:Main:TunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} dynamic tunnel: {1} ({2}ms)".L10N("Client:Main:TunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); } } - private async Task HandleP2PRequestMessageAsync(string playerName, string p2pRequestMessage) + private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p2pRequestMessage) { - if (!p2pEnabled || !p2pPorts.Any()) + if (!p2pEnabled) return; List<(IPAddress IpAddress, long Ping)> localPingResults = new(); @@ -2202,36 +2191,38 @@ private async Task HandleP2PRequestMessageAsync(string playerName, string p2pReq localPingResults.Add((parsedIpV6Address, pingResult.RoundtripTime)); } - if (parsedIpV4Address is null && parsedIpV6Address is null) - { - AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled P2P".L10N("Client:Main:P2PDisabled"), playerName)); - - (string RemotePlayerName, ushort[] RemotePorts, List<(IPAddress RemoteIpAddress, long Ping)> LocalPingResults, List<(IPAddress RemoteIpAddress, long Ping)> RemotePingResults, bool Enabled) p2pPlayer; - - if (p2pPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) - { - p2pPlayer = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - - p2pPlayers.RemoveAt(p2pPlayers.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); - p2pPlayers.Add((p2pPlayer.RemotePlayerName, p2pPlayer.RemotePorts, p2pPlayer.LocalPingResults, p2pPlayer.RemotePingResults, false)); - } + bool remotePlayerP2PEnabled = false; + ushort[] remotePlayerPorts = Array.Empty(); + P2PPlayer remoteP2PPlayer; - return; + if (parsedIpV4Address is not null || parsedIpV6Address is not null) + { + remotePlayerP2PEnabled = true; + remotePlayerPorts = splitLines[2].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); } - ushort[] remotePlayerPorts = splitLines[2].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); - List<(IPAddress RemoteIpAddress, long Ping)> remotePingResults = new(); - if (p2pPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) { - remotePingResults = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)).RemotePingResults; + remoteP2PPlayer = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); p2pPlayers.RemoveAt(p2pPlayers.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); } + else + { + remoteP2PPlayer = new(playerName, Array.Empty(), new(), new(), false); + } - p2pPlayers.Add((playerName, remotePlayerPorts, localPingResults, remotePingResults, true)); - AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} allows P2P: ({1}ms)".L10N("Client:Main:P2PAllowed"), playerName, localPingResults.Min(q => q.Ping))); - await channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_PINGS + $" {playerName}-{localPingResults.Select(q => $"{q.IpAddress};{q.Ping}\t").Aggregate((q, r) => $"{q}{r}")}", QueuedMessageType.SYSTEM_MESSAGE, 10); + p2pPlayers.Add(remoteP2PPlayer with { LocalPingResults = localPingResults, RemotePorts = remotePlayerPorts, Enabled = remotePlayerP2PEnabled }); + + if (remotePlayerP2PEnabled) + { + ShowP2PPlayerStatus(playerName); + await channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_PINGS + $" {playerName}-{localPingResults.Select(q => $"{q.IpAddress};{q.Ping}\t").Aggregate((q, r) => $"{q}{r}")}", QueuedMessageType.SYSTEM_MESSAGE, 10); + } + else + { + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled P2P".L10N("Client:Main:P2PDisabled"), playerName)); + } } private void HandleP2PPingsMessage(string playerName, string p2pPingsMessage) @@ -2245,15 +2236,6 @@ private void HandleP2PPingsMessage(string playerName, string p2pPingsMessage) if (!FindLocalPlayer().Name.Equals(pingPlayerName, StringComparison.OrdinalIgnoreCase)) return; - var p2pPlayer = p2pPlayers.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - - if (p2pPlayer.RemotePlayerName is null) - { - BroadcastPlayerP2PRequestAsync().HandleTask(); - - return; - } - string[] pingResults = splitLines[1].Split('\t', StringSplitOptions.RemoveEmptyEntries); List<(IPAddress IpAddress, long Ping)> playerPings = new(); @@ -2265,19 +2247,38 @@ private void HandleP2PPingsMessage(string playerName, string p2pPingsMessage) playerPings.Add((ipV4Address, long.Parse(ipAddressPingResult[1], CultureInfo.InvariantCulture))); } - AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} P2P enabled".L10N("Client:Main:P2PEnabled"), playerName)); + P2PPlayer p2pPlayer; - p2pPlayer.RemotePingResults = playerPings; + if (p2pPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) + { + p2pPlayer = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); + + p2pPlayers.RemoveAt(p2pPlayers.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + } + else + { + p2pPlayer = new(playerName, Array.Empty(), new(), new(), false); + } + + p2pPlayers.Add(p2pPlayer with { RemotePingResults = playerPings }); + + if (!p2pPlayer.RemotePingResults.Any()) + ShowP2PPlayerStatus(playerName); + } + + private void ShowP2PPlayerStatus(string playerName) + { + P2PPlayer p2pPlayer = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - p2pPlayers.RemoveAt(p2pPlayers.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); - p2pPlayers.Add(p2pPlayer); + if (p2pPlayer.RemotePingResults.Any() && p2pPlayer.LocalPingResults.Any()) + AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} supports P2P ({1}ms)".L10N("Client:Main:PlayerP2PSupported"), playerName, p2pPlayer.LocalPingResults.Min(q => q.Ping))); } /// /// Changes the tunnel server used for the game. /// /// The new tunnel server to use. - private Task HandleTunnelServerChangeAsync(CnCNetTunnel tunnel) + private ValueTask HandleTunnelServerChangeAsync(CnCNetTunnel tunnel) { tunnelHandler.CurrentTunnel = tunnel; @@ -2291,7 +2292,7 @@ protected override bool UpdateLaunchGameButtonStatus() return btnLaunchGame.Enabled; } - private async Task MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) + private async ValueTask MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) { // If the host has already uploaded the map, we shouldn't request them to re-upload it if (hostUploadedMaps.Contains(e.SHA1)) @@ -2315,7 +2316,7 @@ private async Task MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_UPLOAD + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } - private async Task MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) + private async ValueTask MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) { string mapFileName = MapSharer.GetMapFileName(e.SHA1, e.MapName); Logger.Log("Map " + mapFileName + " downloaded, parsing."); @@ -2351,7 +2352,7 @@ private async Task MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) } } - private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) + private async ValueTask MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) { Map map = e.Map; @@ -2365,7 +2366,7 @@ private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) } } - private async Task MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) + private async ValueTask MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) { hostUploadedMaps.Add(e.Map.SHA1); AddNotice(string.Format(CultureInfo.CurrentCulture, "Uploading map {0} to the CnCNet map database complete.".L10N("Client:Main:UpdateMapToDBSuccess"), e.Map.Name)); @@ -2568,7 +2569,7 @@ private void DownloadMapByIdCommand(string parameters) private void AccelerateGameBroadcasting() => gameBroadcastTimer.Accelerate(TimeSpan.FromSeconds(GAME_BROADCAST_ACCELERATION)); - private async Task BroadcastGameAsync() + private async ValueTask BroadcastGameAsync() { Channel broadcastChannel = connectionManager.FindChannel(gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGame)); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 3081884e5..94f1ae013 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -400,7 +400,7 @@ protected void HandleGameOptionPresetSaveCommand(string presetName) protected void HandleGameOptionPresetLoadCommand(GameOptionPresetEventArgs e) => HandleGameOptionPresetLoadCommandAsync(e.PresetName).HandleTask(); - protected async Task HandleGameOptionPresetLoadCommandAsync(string presetName) + protected async ValueTask HandleGameOptionPresetLoadCommandAsync(string presetName) { if (await LoadGameOptionPresetAsync(presetName)) AddNotice("Game option preset loaded succesfully.".L10N("Client:Main:PresetLoaded")); @@ -414,7 +414,7 @@ protected async Task HandleGameOptionPresetLoadCommandAsync(string presetName) private void TbMapSearch_InputReceived(object sender, EventArgs e) => ListMaps(); - private async Task Dropdown_SelectedIndexChangedAsync(object sender) + private async ValueTask Dropdown_SelectedIndexChangedAsync(object sender) { if (disableGameOptionUpdateBroadcast) return; @@ -424,7 +424,7 @@ private async Task Dropdown_SelectedIndexChangedAsync(object sender) await OnGameOptionChangedAsync(); } - private async Task ChkBox_CheckedChangedAsync(object sender) + private async ValueTask ChkBox_CheckedChangedAsync(object sender) { if (disableGameOptionUpdateBroadcast) return; @@ -434,16 +434,16 @@ private async Task ChkBox_CheckedChangedAsync(object sender) await OnGameOptionChangedAsync(); } - protected virtual Task OnGameOptionChangedAsync() + protected virtual ValueTask OnGameOptionChangedAsync() { CheckDisallowedSides(); btnLaunchGame.SetRank(GetRank()); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected async Task DdGameModeMapFilter_SelectedIndexChangedAsync() + protected async ValueTask DdGameModeMapFilter_SelectedIndexChangedAsync() { gameModeMapFilter = ddGameModeMapFilter.SelectedItem.Tag as GameModeMapFilter; @@ -643,7 +643,7 @@ protected void RefreshForFavoriteMapRemoved() lbGameModeMapList.SelectedIndex = 0; // the map was removed while viewing favorites } - private async Task DeleteSelectedMapAsync() + private async ValueTask DeleteSelectedMapAsync() { try { @@ -672,7 +672,7 @@ private async Task DeleteSelectedMapAsync() } } - private async Task LbGameModeMapList_SelectedIndexChangedAsync() + private async ValueTask LbGameModeMapList_SelectedIndexChangedAsync() { if (lbGameModeMapList.SelectedIndex < 0 || lbGameModeMapList.SelectedIndex >= lbGameModeMapList.ItemCount) { @@ -703,7 +703,7 @@ private void LbGameModeMapList_HoveredIndexChanged(object sender, EventArgs e) mapListTooltip.Text = string.Empty; } - private async Task PickRandomMapAsync() + private async ValueTask PickRandomMapAsync() { int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + AIPlayers.Count; @@ -735,7 +735,7 @@ private List GetMapList(int playerCount) /// Refreshes the map selection UI to match the currently selected map /// and game mode. /// - protected async Task RefreshMapSelectionUIAsync() + protected async ValueTask RefreshMapSelectionUIAsync() { if (GameMode == null) return; @@ -919,7 +919,7 @@ private XNALabel GeneratePlayerOptionCaption(string name, string text, int x, in return label; } - protected virtual Task PlayerExtraOptions_OptionsChangedAsync() + protected virtual ValueTask PlayerExtraOptions_OptionsChangedAsync() { var playerExtraOptions = GetPlayerExtraOptions(); @@ -938,7 +938,7 @@ protected virtual Task PlayerExtraOptions_OptionsChangedAsync() UpdateMapPreviewBoxEnabledStatus(); RefreshBtnPlayerExtraOptionsOpenTexture(); - return Task.CompletedTask; + return ValueTask.CompletedTask; } private void EnablePlayerOptionDropDown(XNAClientDropDown clientDropDown, int playerIndex, bool enable) @@ -1007,9 +1007,9 @@ private void GetRandomSelectors(List selectorNames, List selector } } - protected abstract Task BtnLaunchGame_LeftClickAsync(); + protected abstract ValueTask BtnLaunchGame_LeftClickAsync(); - protected abstract Task BtnLeaveGame_LeftClickAsync(); + protected abstract ValueTask BtnLeaveGame_LeftClickAsync(); /// /// Updates Discord Rich Presence with actual information. @@ -1779,7 +1779,7 @@ private void ManipulateStartingLocations(IniFile mapIni, PlayerHouseInfo[] house /// Writes spawn.ini, writes the map file, initializes statistics and /// starts the game process. /// - protected virtual Task StartGameAsync() + protected virtual ValueTask StartGameAsync() { PlayerHouseInfo[] houseInfos = WriteSpawnIni(); InitializeMatchStatistics(houseInfos); @@ -1790,12 +1790,12 @@ protected virtual Task StartGameAsync() GameProcessLogic.StartGameProcess(WindowManager); UpdateDiscordPresence(true); - return Task.CompletedTask; + return ValueTask.CompletedTask; } private void GameProcessExited_Callback() => AddCallback(() => GameProcessExitedAsync().HandleTask()); - protected virtual Task GameProcessExitedAsync() + protected virtual ValueTask GameProcessExitedAsync() { GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; @@ -1807,14 +1807,14 @@ protected virtual Task GameProcessExitedAsync() CopyPlayerDataToUI(); UpdateDiscordPresence(true); - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// /// "Copies" player information from the UI to internal memory, /// applying users' player options changes. /// - protected virtual async Task CopyPlayerDataFromUIAsync(object sender) + protected virtual async ValueTask CopyPlayerDataFromUIAsync(object sender) { if (PlayerUpdatingInProgress) return; @@ -2040,27 +2040,27 @@ protected virtual void CopyPlayerDataToUI() /// Override this in a derived class to kick players. /// /// The index of the player that should be kicked. - protected virtual Task KickPlayerAsync(int playerIndex) + protected virtual ValueTask KickPlayerAsync(int playerIndex) { // Do nothing by default - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// /// Override this in a derived class to ban players. /// /// The index of the player that should be banned. - protected virtual Task BanPlayerAsync(int playerIndex) + protected virtual ValueTask BanPlayerAsync(int playerIndex) { // Do nothing by default - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// /// Changes the current map and game mode. /// /// The new game mode map. - protected virtual async Task ChangeMapAsync(GameModeMap gameModeMap) + protected virtual async ValueTask ChangeMapAsync(GameModeMap gameModeMap) { GameModeMap = gameModeMap; @@ -2436,7 +2436,7 @@ protected string AddGameOptionPreset(string name) return null; } - public async Task LoadGameOptionPresetAsync(string name) + public async ValueTask LoadGameOptionPresetAsync(string name) { GameOptionPreset preset = GameOptionPresets.Instance.GetPreset(name); if (preset == null) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index 00504a0e7..ebcc57ded 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -71,7 +71,7 @@ public LANGameLobby( WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync().HandleTask(); } - private async Task WindowManager_GameClosingAsync() + private async ValueTask WindowManager_GameClosingAsync() { if (client is { Connected: true }) await ClearAsync(); @@ -126,7 +126,7 @@ public override void Initialize() PostInitialize(); } - public async Task SetUpAsync(bool isHost, IPEndPoint hostEndPoint, Socket client) + public async ValueTask SetUpAsync(bool isHost, IPEndPoint hostEndPoint, Socket client) { Refresh(isHost); @@ -160,7 +160,7 @@ public async Task SetUpAsync(bool isHost, IPEndPoint hostEndPoint, Socket client WindowManager.SelectedControl = tbChatInput; } - private async Task SendHostPlayerJoinedMessageAsync(CancellationToken cancellationToken) + private async ValueTask SendHostPlayerJoinedMessageAsync(CancellationToken cancellationToken) { try { @@ -184,7 +184,7 @@ private async Task SendHostPlayerJoinedMessageAsync(CancellationToken cancellati } } - public async Task PostJoinAsync() + public async ValueTask PostJoinAsync() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(GameModeMaps.GameModes); @@ -194,7 +194,7 @@ public async Task PostJoinAsync() #region Server code - private async Task ListenForClientsAsync(CancellationToken cancellationToken) + private async ValueTask ListenForClientsAsync(CancellationToken cancellationToken) { listener = new Socket(SocketType.Stream, ProtocolType.Tcp); @@ -224,6 +224,7 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) if (Players.Count >= MAX_PLAYER_COUNT) { Logger.Log("Dropping client because of player limit."); + client.Shutdown(SocketShutdown.Both); client.Close(); continue; } @@ -231,6 +232,7 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) if (Locked) { Logger.Log("Dropping client because the game room is locked."); + client.Shutdown(SocketShutdown.Both); client.Close(); continue; } @@ -242,7 +244,7 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) } } - private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) + private async ValueTask HandleClientConnectionAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); @@ -293,11 +295,11 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio break; } - if (lpInfo.TcpClient.Connected) - lpInfo.TcpClient.Close(); + lpInfo.TcpClient.Shutdown(SocketShutdown.Both); + lpInfo.TcpClient.Close(); } - private async Task AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) + private async ValueTask AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { if (Players.Find(p => p.Name == lpInfo.Name) != null || Players.Count >= MAX_PLAYER_COUNT || Locked) @@ -321,7 +323,7 @@ private async Task AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancel UpdateDiscordPresence(); } - private async Task LpInfo_ConnectionLostAsync(object sender) + private async ValueTask LpInfo_ConnectionLostAsync(object sender) { var lpInfo = (LANPlayerInfo)sender; CleanUpPlayer(lpInfo); @@ -360,12 +362,13 @@ private void CleanUpPlayer(LANPlayerInfo lpInfo) { lpInfo.MessageReceived -= LpInfo_MessageReceived; lpInfo.ConnectionLost -= lpInfo_ConnectionLostFunc; + lpInfo.TcpClient.Shutdown(SocketShutdown.Both); lpInfo.TcpClient.Close(); } #endregion - private async Task HandleServerCommunicationAsync(CancellationToken cancellationToken) + private async ValueTask HandleServerCommunicationAsync(CancellationToken cancellationToken) { if (!client.Connected) return; @@ -442,7 +445,7 @@ private void HandleMessageFromServer(string message) Logger.Log("Unknown LAN command from the server: " + message); } - protected override async Task BtnLeaveGame_LeftClickAsync() + protected override async ValueTask BtnLeaveGame_LeftClickAsync() { await ClearAsync(); GameLeft?.Invoke(this, EventArgs.Empty); @@ -468,7 +471,7 @@ protected override void UpdateDiscordPresence(bool resetTimer = false) "LAN Game", IsHost, false, Locked, resetTimer); } - public override async Task ClearAsync() + public override async ValueTask ClearAsync() { await base.ClearAsync(); @@ -477,7 +480,10 @@ public override async Task ClearAsync() await BroadcastMessageAsync(LANCommands.PLAYER_QUIT_COMMAND); Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p)); Players.Clear(); - cancellationTokenSource.Cancel(); + + if (listener.Connected) + listener.Shutdown(SocketShutdown.Both); + listener.Close(); } else @@ -485,12 +491,9 @@ public override async Task ClearAsync() await SendMessageToHostAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationTokenSource?.Token ?? default); } - if (client.Connected) - { - cancellationTokenSource.Cancel(); - client.Close(); - } - + cancellationTokenSource.Cancel(); + client.Shutdown(SocketShutdown.Both); + client.Close(); ResetDiscordPresence(); } @@ -505,7 +508,7 @@ public void SetChatColorIndex(int colorIndex) protected override void AddNotice(string message, Color color) => lbChatMessages.AddMessage(null, message, color); - protected override async Task BroadcastPlayerOptionsAsync() + protected override async ValueTask BroadcastPlayerOptionsAsync() { if (!IsHost) return; @@ -533,14 +536,14 @@ protected override async Task BroadcastPlayerOptionsAsync() await BroadcastMessageAsync(sb.ToString()); } - protected override async Task BroadcastPlayerExtraOptionsAsync() + protected override async ValueTask BroadcastPlayerExtraOptionsAsync() { var playerExtraOptions = GetPlayerExtraOptions(); await BroadcastMessageAsync(playerExtraOptions.ToLanMessage(), true); } - protected override Task HostLaunchGameAsync() => BroadcastMessageAsync(LANCommands.LAUNCH_GAME + " " + UniqueGameID); + protected override ValueTask HostLaunchGameAsync() => BroadcastMessageAsync(LANCommands.LAUNCH_GAME + " " + UniqueGameID); protected override IPAddress GetIPAddressForPlayer(PlayerInfo player) { @@ -548,7 +551,7 @@ protected override IPAddress GetIPAddressForPlayer(PlayerInfo player) return lpInfo.IPAddress.MapToIPv4(); } - protected override Task RequestPlayerOptionsAsync(int side, int color, int start, int team) + protected override ValueTask RequestPlayerOptionsAsync(int side, int color, int start, int team) { var sb = new ExtendedStringBuilder(LANCommands.PLAYER_OPTIONS_REQUEST + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; @@ -559,12 +562,12 @@ protected override Task RequestPlayerOptionsAsync(int side, int color, int start return SendMessageToHostAsync(sb.ToString(), cancellationTokenSource?.Token ?? default); } - protected override Task RequestReadyStatusAsync() + protected override ValueTask RequestReadyStatusAsync() { return SendMessageToHostAsync(LANCommands.PLAYER_READY_REQUEST + " " + Convert.ToInt32(chkAutoReady.Checked), cancellationTokenSource?.Token ?? default); } - protected override Task SendChatMessageAsync(string message) + protected override ValueTask SendChatMessageAsync(string message) { var sb = new ExtendedStringBuilder(LANCommands.CHAT_LOBBY_COMMAND + " ", true); sb.Separator = ProgramConstants.LAN_DATA_SEPARATOR; @@ -573,7 +576,7 @@ protected override Task SendChatMessageAsync(string message) return SendMessageToHostAsync(sb.ToString(), cancellationTokenSource?.Token ?? default); } - protected override async Task OnGameOptionChangedAsync() + protected override async ValueTask OnGameOptionChangedAsync() { await base.OnGameOptionChangedAsync(); @@ -601,7 +604,7 @@ protected override async Task OnGameOptionChangedAsync() await BroadcastMessageAsync(sb.ToString()); } - protected override async Task GetReadyNotificationAsync() + protected override async ValueTask GetReadyNotificationAsync() { await base.GetReadyNotificationAsync(); #if WINFORMS @@ -627,7 +630,7 @@ protected override void UpdatePlayerPingIndicator(PlayerInfo pInfo) /// /// The command to send. /// If true, only send this to other players. Otherwise, even the sender will receive their message. - private async Task BroadcastMessageAsync(string message, bool otherPlayersOnly = false) + private async ValueTask BroadcastMessageAsync(string message, bool otherPlayersOnly = false) { if (!IsHost) return; @@ -639,13 +642,13 @@ private async Task BroadcastMessageAsync(string message, bool otherPlayersOnly = } } - protected override async Task PlayerExtraOptions_OptionsChangedAsync() + protected override async ValueTask PlayerExtraOptions_OptionsChangedAsync() { await base.PlayerExtraOptions_OptionsChangedAsync(); await BroadcastPlayerExtraOptionsAsync(); } - private async Task SendMessageToHostAsync(string message, CancellationToken cancellationToken) + private async ValueTask SendMessageToHostAsync(string message, CancellationToken cancellationToken) { if (!client.Connected) return; @@ -673,7 +676,7 @@ private async Task SendMessageToHostAsync(string message, CancellationToken canc } } - protected override Task UnlockGameAsync(bool manual) + protected override ValueTask UnlockGameAsync(bool manual) { Locked = false; @@ -682,10 +685,10 @@ protected override Task UnlockGameAsync(bool manual) if (manual) AddNotice("You've unlocked the game room.".L10N("Client:Main:RoomUnockedByYou")); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected override Task LockGameAsync() + protected override ValueTask LockGameAsync() { Locked = true; @@ -694,10 +697,10 @@ protected override Task LockGameAsync() if (Locked) AddNotice("You've locked the game room.".L10N("Client:Main:RoomLockedByYou")); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected override async Task GameProcessExitedAsync() + protected override async ValueTask GameProcessExitedAsync() { await base.GameProcessExitedAsync(); await SendMessageToHostAsync(LANCommands.RETURN, cancellationTokenSource?.Token ?? default); @@ -793,7 +796,7 @@ private void BroadcastGame() #region Command Handlers - private async Task GameHost_HandleChatCommandAsync(string sender, string data) + private async ValueTask GameHost_HandleChatCommandAsync(string sender, string data) { string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); @@ -826,7 +829,7 @@ private void Player_HandleChatCommand(string data) chatColors[colorIndex].XNAColor, DateTime.Now, parts[2])); } - private Task GameHost_HandleReturnCommandAsync(string sender) + private ValueTask GameHost_HandleReturnCommandAsync(string sender) => BroadcastMessageAsync(LANCommands.RETURN + ProgramConstants.LAN_DATA_SEPARATOR + sender); private void Player_HandleReturnCommand(string sender) @@ -834,13 +837,13 @@ private void Player_HandleReturnCommand(string sender) ReturnNotification(sender); } - private async Task HandleGetReadyCommandAsync() + private async ValueTask HandleGetReadyCommandAsync() { if (!IsHost) await GetReadyNotificationAsync(); } - private async Task HandlePlayerOptionsRequestAsync(string sender, string data) + private async ValueTask HandlePlayerOptionsRequestAsync(string sender, string data) { if (!IsHost) return; @@ -981,7 +984,7 @@ private void HandlePlayerOptionsBroadcast(string data) UpdateDiscordPresence(); } - private async Task HandlePlayerQuitAsync(string sender) + private async ValueTask HandlePlayerQuitAsync(string sender) { PlayerInfo pInfo = Players.Find(p => p.Name == sender); @@ -996,7 +999,7 @@ private async Task HandlePlayerQuitAsync(string sender) UpdateDiscordPresence(); } - private async Task HandleGameOptionsMessageAsync(string data) + private async ValueTask HandleGameOptionsMessageAsync(string data) { if (IsHost) return; @@ -1083,7 +1086,7 @@ private async Task HandleGameOptionsMessageAsync(string data) } } - private async Task GameHost_HandleReadyRequestAsync(string sender, string autoReady) + private async ValueTask GameHost_HandleReadyRequestAsync(string sender, string autoReady) { PlayerInfo pInfo = Players.Find(p => p.Name == sender); @@ -1096,7 +1099,7 @@ private async Task GameHost_HandleReadyRequestAsync(string sender, string autoRe await BroadcastPlayerOptionsAsync(); } - private async Task HandleGameLaunchCommandAsync(string gameId) + private async ValueTask HandleGameLaunchCommandAsync(string gameId) { Players.ForEach(pInfo => pInfo.IsInGame = true); UniqueGameID = Conversions.IntFromString(gameId, -1); @@ -1109,16 +1112,16 @@ private async Task HandleGameLaunchCommandAsync(string gameId) } - private Task HandlePingAsync() + private ValueTask HandlePingAsync() => SendMessageToHostAsync(LANCommands.PING, cancellationTokenSource?.Token ?? default); - protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) + protected override async ValueTask BroadcastDiceRollAsync(int dieSides, int[] results) { string resultString = string.Join(",", results); await SendMessageToHostAsync($"{LANCommands.DICE_ROLL} {dieSides},{resultString}", cancellationTokenSource?.Token ?? default); } - private Task Host_HandleDiceRollAsync(string sender, string result) + private ValueTask Host_HandleDiceRollAsync(string sender, string result) => BroadcastMessageAsync($"{LANCommands.DICE_ROLL} {sender}{ProgramConstants.LAN_DATA_SEPARATOR}{result}"); private void Client_HandleDiceRoll(string data) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index f15c1db7a..0e64fc7ef 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -262,7 +262,7 @@ private void FSWEvent(FileSystemEventArgs e) } } - protected override Task StartGameAsync() + protected override ValueTask StartGameAsync() { if (fsw != null) fsw.EnableRaisingEvents = true; @@ -273,7 +273,7 @@ protected override Task StartGameAsync() return base.StartGameAsync(); } - protected override async Task GameProcessExitedAsync() + protected override async ValueTask GameProcessExitedAsync() { gameSaved = false; @@ -317,7 +317,7 @@ private void GenerateGameID() } } - protected virtual async Task HandleLockGameButtonClickAsync() + protected virtual async ValueTask HandleLockGameButtonClickAsync() { if (Locked) await UnlockGameAsync(true); @@ -325,11 +325,11 @@ protected virtual async Task HandleLockGameButtonClickAsync() await LockGameAsync(); } - protected abstract Task LockGameAsync(); + protected abstract ValueTask LockGameAsync(); - protected abstract Task UnlockGameAsync(bool announce); + protected abstract ValueTask UnlockGameAsync(bool announce); - private async Task TbChatInput_EnterPressedAsync() + private async ValueTask TbChatInput_EnterPressedAsync() { if (string.IsNullOrEmpty(tbChatInput.Text)) return; @@ -385,7 +385,7 @@ private async Task TbChatInput_EnterPressedAsync() tbChatInput.Text = string.Empty; } - private async Task ChkAutoReady_CheckedChangedAsync() + private async ValueTask ChkAutoReady_CheckedChangedAsync() { UpdateLaunchGameButtonStatus(); await RequestReadyStatusAsync(); @@ -399,7 +399,7 @@ protected void ResetAutoReadyCheckbox() UpdateLaunchGameButtonStatus(); } - private async Task SetFrameSendRateAsync(string value) + private async ValueTask SetFrameSendRateAsync(string value) { bool success = int.TryParse(value, out int intValue); @@ -417,7 +417,7 @@ private async Task SetFrameSendRateAsync(string value) ClearReadyStatuses(); } - private async Task SetMaxAheadAsync(string value) + private async ValueTask SetMaxAheadAsync(string value) { bool success = int.TryParse(value, out int intValue); @@ -434,7 +434,7 @@ private async Task SetMaxAheadAsync(string value) ClearReadyStatuses(); } - private async Task SetProtocolVersionAsync(string value) + private async ValueTask SetProtocolVersionAsync(string value) { bool success = int.TryParse(value, out int intValue); @@ -457,7 +457,7 @@ private async Task SetProtocolVersionAsync(string value) ClearReadyStatuses(); } - private async Task SetStartingLocationClearanceAsync(string value) + private async ValueTask SetStartingLocationClearanceAsync(string value) { bool removeStartingLocations = Conversions.BooleanFromString(value, RemoveStartingLocations); @@ -488,7 +488,7 @@ protected void SetRandomStartingLocations(bool newValue) /// Handles the dice rolling command. /// /// The parameters given for the command by the user. - private async Task RollDiceCommandAsync(string dieType) + private async ValueTask RollDiceCommandAsync(string dieType) { int dieSides = 6; int dieCount = 1; @@ -551,7 +551,7 @@ private void LoadCustomMap(string mapName) /// /// The number of sides in the dice. /// The results of the dice roll. - protected abstract Task BroadcastDiceRollAsync(int dieSides, int[] results); + protected abstract ValueTask BroadcastDiceRollAsync(int dieSides, int[] results); /// /// Parses and lists the results of rolling dice. @@ -601,7 +601,7 @@ protected void PrintDiceRollResult(string senderName, int dieSides, int[] result )); } - protected abstract Task SendChatMessageAsync(string message); + protected abstract ValueTask SendChatMessageAsync(string message); /// /// Changes the game lobby's UI depending on whether the local player is the host. @@ -739,7 +739,7 @@ private void MapPreviewBox_LocalStartingLocationSelected(object sender, LocalSta ddPlayerStarts[mTopIndex].SelectedIndex = e.StartingLocationIndex; } - private async Task MapPreviewBox_StartingLocationAppliedAsync() + private async ValueTask MapPreviewBox_StartingLocationAppliedAsync() { ClearReadyStatuses(); CopyPlayerDataToUI(); @@ -752,7 +752,7 @@ private async Task MapPreviewBox_StartingLocationAppliedAsync() /// launches the game if it's allowed. If the local player isn't the game host, /// sends a ready request. /// - protected override async Task BtnLaunchGame_LeftClickAsync() + protected override async ValueTask BtnLaunchGame_LeftClickAsync() { if (!IsHost) { @@ -889,43 +889,43 @@ protected override async Task BtnLaunchGame_LeftClickAsync() await HostLaunchGameAsync(); } - protected virtual Task LockGameNotificationAsync() + protected virtual ValueTask LockGameNotificationAsync() { AddNotice("You need to lock the game room before launching the game.".L10N("Client:Main:LockGameNotification")); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected virtual Task SharedColorsNotificationAsync() + protected virtual ValueTask SharedColorsNotificationAsync() { AddNotice("Multiple human players cannot share the same color.".L10N("Client:Main:SharedColorsNotification")); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected virtual Task AISpectatorsNotificationAsync() + protected virtual ValueTask AISpectatorsNotificationAsync() { AddNotice("AI players don't enjoy spectating matches. They want some action!".L10N("Client:Main:AISpectatorsNotification")); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected virtual Task SharedStartingLocationNotificationAsync() + protected virtual ValueTask SharedStartingLocationNotificationAsync() { AddNotice("Multiple players cannot share the same starting location on this map.".L10N("Client:Main:SharedStartingLocationNotification")); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected virtual Task NotVerifiedNotificationAsync(int playerIndex) + protected virtual ValueTask NotVerifiedNotificationAsync(int playerIndex) { if (playerIndex > -1 && playerIndex < Players.Count) AddNotice(string.Format("Unable to launch game. Player {0} hasn't been verified.".L10N("Client:Main:NotVerifiedNotification"), Players[playerIndex].Name)); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected virtual Task StillInGameNotificationAsync(int playerIndex) + protected virtual ValueTask StillInGameNotificationAsync(int playerIndex) { if (playerIndex > -1 && playerIndex < Players.Count) { @@ -933,19 +933,19 @@ protected virtual Task StillInGameNotificationAsync(int playerIndex) Players[playerIndex].Name)); } - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected virtual Task GetReadyNotificationAsync() + protected virtual ValueTask GetReadyNotificationAsync() { AddNotice("The host wants to start the game but cannot because not all players are ready!".L10N("Client:Main:GetReadyNotification")); if (!IsHost && !Players.Find(p => p.Name == ProgramConstants.PLAYERNAME).Ready) sndGetReadySound.Play(); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected virtual Task InsufficientPlayersNotificationAsync() + protected virtual ValueTask InsufficientPlayersNotificationAsync() { if (GameMode != null && GameMode.MinPlayersOverride > -1) AddNotice(String.Format("Unable to launch game: {0} cannot be played with fewer than {1} players".L10N("Client:Main:InsufficientPlayersNotification1"), @@ -954,29 +954,29 @@ protected virtual Task InsufficientPlayersNotificationAsync() AddNotice(String.Format("Unable to launch game: this map cannot be played with fewer than {0} players.".L10N("Client:Main:InsufficientPlayersNotification2"), Map.MinPlayers)); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected virtual Task TooManyPlayersNotificationAsync() + protected virtual ValueTask TooManyPlayersNotificationAsync() { if (Map != null) AddNotice(String.Format("Unable to launch game: this map cannot be played with more than {0} players.".L10N("Client:Main:TooManyPlayersNotification"), Map.MaxPlayers)); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - public virtual Task ClearAsync() + public virtual ValueTask ClearAsync() { if (!IsHost) AIPlayers.Clear(); Players.Clear(); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - protected override async Task OnGameOptionChangedAsync() + protected override async ValueTask OnGameOptionChangedAsync() { await base.OnGameOptionChangedAsync(); @@ -984,9 +984,9 @@ protected override async Task OnGameOptionChangedAsync() CopyPlayerDataToUI(); } - protected abstract Task HostLaunchGameAsync(); + protected abstract ValueTask HostLaunchGameAsync(); - protected override async Task CopyPlayerDataFromUIAsync(object sender) + protected override async ValueTask CopyPlayerDataFromUIAsync(object sender) { if (PlayerUpdatingInProgress) return; @@ -1115,13 +1115,13 @@ private Texture2D GetTextureForPing(int ping) } } - protected abstract Task BroadcastPlayerOptionsAsync(); + protected abstract ValueTask BroadcastPlayerOptionsAsync(); - protected abstract Task BroadcastPlayerExtraOptionsAsync(); + protected abstract ValueTask BroadcastPlayerExtraOptionsAsync(); - protected abstract Task RequestPlayerOptionsAsync(int side, int color, int start, int team); + protected abstract ValueTask RequestPlayerOptionsAsync(int side, int color, int start, int team); - protected abstract Task RequestReadyStatusAsync(); + protected abstract ValueTask RequestReadyStatusAsync(); // this public as it is used by the main lobby to notify the user of invitation failure public void AddWarning(string message) @@ -1131,7 +1131,7 @@ public void AddWarning(string message) protected override bool AllowPlayerOptionsChange() => IsHost; - protected override async Task ChangeMapAsync(GameModeMap gameModeMap) + protected override async ValueTask ChangeMapAsync(GameModeMap gameModeMap) { await base.ChangeMapAsync(gameModeMap); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs index e35785378..a1e1e98f9 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -166,7 +166,7 @@ private string CheckGameValidity() return null; } - protected override async Task BtnLaunchGame_LeftClickAsync() + protected override async ValueTask BtnLaunchGame_LeftClickAsync() { string error = CheckGameValidity(); @@ -180,7 +180,7 @@ protected override async Task BtnLaunchGame_LeftClickAsync() XNAMessageBox.Show(WindowManager, "Cannot launch game".L10N("Client:Main:LaunchGameErrorTitle"), error); } - protected override Task BtnLeaveGame_LeftClickAsync() + protected override ValueTask BtnLeaveGame_LeftClickAsync() { Enabled = false; Visible = false; @@ -190,7 +190,7 @@ protected override Task BtnLeaveGame_LeftClickAsync() topBar.RemovePrimarySwitchable(this); ResetDiscordPresence(); - return Task.CompletedTask; + return ValueTask.CompletedTask; } private void PlayerSideChanged(object sender, EventArgs e) @@ -228,7 +228,7 @@ protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap) return StatisticsManager.Instance.GetSkirmishRankForDefaultMap(gameModeMap.Map.UntranslatedName, gameModeMap.Map.MaxPlayers); } - protected override async Task GameProcessExitedAsync() + protected override async ValueTask GameProcessExitedAsync() { await base.GameProcessExitedAsync(); await DdGameModeMapFilter_SelectedIndexChangedAsync(); // Refresh ranks diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index 5b1ff3141..7b2f8714e 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -56,7 +56,7 @@ public LANGameLoadingLobby( WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync().HandleTask(); } - private async Task WindowManager_GameClosingAsync() + private async ValueTask WindowManager_GameClosingAsync() { if (client is { Connected: true }) await ClearAsync(); @@ -93,7 +93,7 @@ private async Task WindowManager_GameClosingAsync() private CancellationTokenSource cancellationTokenSource; - public async Task SetUpAsync(bool isHost, Socket client, int loadedGameId) + public async ValueTask SetUpAsync(bool isHost, Socket client, int loadedGameId) { Refresh(isHost); @@ -143,7 +143,7 @@ public async Task SetUpAsync(bool isHost, Socket client, int loadedGameId) WindowManager.SelectedControl = tbChatInput; } - public async Task PostJoinAsync() + public async ValueTask PostJoinAsync() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(gameModes); @@ -153,7 +153,7 @@ public async Task PostJoinAsync() #region Server code - private async Task ListenForClientsAsync(CancellationToken cancellationToken) + private async ValueTask ListenForClientsAsync(CancellationToken cancellationToken) { listener = new Socket(SocketType.Stream, ProtocolType.Tcp); listener.Bind(new IPEndPoint(IPAddress.IPv6Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); @@ -186,7 +186,7 @@ private async Task ListenForClientsAsync(CancellationToken cancellationToken) } } - private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) + private async ValueTask HandleClientConnectionAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); @@ -239,16 +239,17 @@ private async Task HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancellatio break; } - if (lpInfo.TcpClient.Connected) - lpInfo.TcpClient.Close(); + lpInfo.TcpClient.Shutdown(SocketShutdown.Both); + lpInfo.TcpClient.Close(); } - private async Task AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) + private async ValueTask AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { if (Players.Find(p => p.Name == lpInfo.Name) != null || Players.Count >= SGPlayers.Count || SGPlayers.Find(p => p.Name == lpInfo.Name) == null) { + lpInfo.TcpClient.Shutdown(SocketShutdown.Both); lpInfo.TcpClient.Close(); return; } @@ -271,7 +272,7 @@ private async Task AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken cancel UpdateDiscordPresence(); } - private async Task LpInfo_ConnectionLostAsync(object sender) + private async ValueTask LpInfo_ConnectionLostAsync(object sender) { var lpInfo = (LANPlayerInfo)sender; CleanUpPlayer(lpInfo); @@ -307,12 +308,13 @@ private void HandleClientMessage(string data, LANPlayerInfo lpInfo) private void CleanUpPlayer(LANPlayerInfo lpInfo) { lpInfo.MessageReceived -= LpInfo_MessageReceived; + lpInfo.TcpClient.Shutdown(SocketShutdown.Both); lpInfo.TcpClient.Close(); } #endregion - private async Task HandleServerCommunicationAsync(CancellationToken cancellationToken) + private async ValueTask HandleServerCommunicationAsync(CancellationToken cancellationToken) { if (!client.Connected) return; @@ -389,14 +391,14 @@ private void HandleMessageFromServer(string message) Logger.Log("Unknown LAN command from the server: " + message); } - protected override async Task LeaveGameAsync() + protected override async ValueTask LeaveGameAsync() { await ClearAsync(); Disable(); await base.LeaveGameAsync(); } - private async Task ClearAsync() + private async ValueTask ClearAsync() { if (IsHost) { @@ -411,9 +413,8 @@ private async Task ClearAsync() } cancellationTokenSource.Cancel(); - - if (client.Connected) - client.Close(); + client.Shutdown(SocketShutdown.Both); + client.Close(); } protected override void AddNotice(string message, Color color) @@ -421,7 +422,7 @@ protected override void AddNotice(string message, Color color) lbChatMessages.AddMessage(null, message, color); } - protected override async Task BroadcastOptionsAsync() + protected override async ValueTask BroadcastOptionsAsync() { if (Players.Count > 0) Players[0].Ready = true; @@ -441,13 +442,13 @@ protected override async Task BroadcastOptionsAsync() await BroadcastMessageAsync(sb.ToString(), cancellationTokenSource?.Token ?? default); } - protected override Task HostStartGameAsync() + protected override ValueTask HostStartGameAsync() => BroadcastMessageAsync(LANCommands.GAME_START, cancellationTokenSource?.Token ?? default); - protected override Task RequestReadyStatusAsync() + protected override ValueTask RequestReadyStatusAsync() => SendMessageToHostAsync(LANCommands.READY_STATUS, cancellationTokenSource?.Token ?? default); - protected override async Task SendChatMessageAsync(string message) + protected override async ValueTask SendChatMessageAsync(string message) { await SendMessageToHostAsync(LANCommands.CHAT_GAME_LOADING_COMMAND + " " + chatColorIndex + ProgramConstants.LAN_DATA_SEPARATOR + message, cancellationTokenSource?.Token ?? default); @@ -457,7 +458,7 @@ await SendMessageToHostAsync(LANCommands.CHAT_GAME_LOADING_COMMAND + " " + chatC #region Server's command handlers - private async Task Server_HandleChatMessageAsync(LANPlayerInfo sender, string data) + private async ValueTask Server_HandleChatMessageAsync(LANPlayerInfo sender, string data) { string[] parts = data.Split(ProgramConstants.LAN_DATA_SEPARATOR); @@ -481,7 +482,7 @@ private void Server_HandleFileHashMessage(LANPlayerInfo sender, string hash) sender.Verified = true; } - private async Task Server_HandleReadyRequestAsync(LANPlayerInfo sender) + private async ValueTask Server_HandleReadyRequestAsync(LANPlayerInfo sender) { if (sender.Ready) return; @@ -568,7 +569,7 @@ private void Client_HandleStartCommand() /// Broadcasts a command to all players in the game as the game host. /// /// The command to send. - private async Task BroadcastMessageAsync(string message, CancellationToken cancellationToken) + private async ValueTask BroadcastMessageAsync(string message, CancellationToken cancellationToken) { if (!IsHost) return; @@ -580,7 +581,7 @@ private async Task BroadcastMessageAsync(string message, CancellationToken cance } } - private async Task SendMessageToHostAsync(string message, CancellationToken cancellationToken) + private async ValueTask SendMessageToHostAsync(string message, CancellationToken cancellationToken) { if (!client.Connected) return; @@ -669,7 +670,7 @@ private void BroadcastGame() GameBroadcast?.Invoke(this, new GameBroadcastEventArgs(sb.ToString())); } - protected override async Task HandleGameProcessExitedAsync() + protected override async ValueTask HandleGameProcessExitedAsync() { await base.HandleGameProcessExitedAsync(); await LeaveGameAsync(); diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 174009209..ab450fa4c 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -68,7 +68,6 @@ public LANLobby( private string localGame; private int localGameIndex; private GameCollection gameCollection; - private List gameModes => mapLoader.GameModes; private Socket socket; private IPEndPoint endPoint; private Encoding encoding; @@ -243,27 +242,26 @@ public override void Initialize() WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync(cancellationTokenSource?.Token ?? default).HandleTask(); } - private async Task WindowManager_GameClosingAsync(CancellationToken cancellationToken) + private async ValueTask WindowManager_GameClosingAsync(CancellationToken cancellationToken) { if (socket == null) return; if (socket.IsBound) - { await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationToken); - cancellationTokenSource.Cancel(); - socket.Close(); - } + + cancellationTokenSource.Cancel(); + socket.Close(); } - private async Task GameCreationWindow_LoadGameAsync(GameLoadEventArgs e) + private async ValueTask GameCreationWindow_LoadGameAsync(GameLoadEventArgs e) { await lanGameLoadingLobby.SetUpAsync(true, null, e.LoadedGameID); lanGameLoadingLobby.Enable(); } - private async Task GameCreationWindow_NewGameAsync() + private async ValueTask GameCreationWindow_NewGameAsync() { await lanGameLobby.SetUpAsync(true, new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), null); @@ -284,7 +282,7 @@ private void DdColor_SelectedIndexChanged(object sender, EventArgs e) UserINISettings.Instance.SaveSettings(); } - public async Task OpenAsync() + public async ValueTask OpenAsync() { players.Clear(); lbPlayerList.Clear(); @@ -334,7 +332,7 @@ public async Task OpenAsync() await SendAliveAsync(cancellationTokenSource.Token); } - private async Task SendMessageAsync(string message, CancellationToken cancellationToken) + private async ValueTask SendMessageAsync(string message, CancellationToken cancellationToken) { try { @@ -356,7 +354,7 @@ private async Task SendMessageAsync(string message, CancellationToken cancellati } } - private async Task ListenAsync(CancellationToken cancellationToken) + private async ValueTask ListenAsync(CancellationToken cancellationToken) { try { @@ -473,7 +471,7 @@ private void HandleNetworkMessage(string data, IPEndPoint endPoint) } } - private async Task SendAliveAsync(CancellationToken cancellationToken) + private async ValueTask SendAliveAsync(CancellationToken cancellationToken) { StringBuilder sb = new StringBuilder(LANCommands.ALIVE + " "); sb.Append(localGameIndex); @@ -483,7 +481,7 @@ private async Task SendAliveAsync(CancellationToken cancellationToken) timeSinceAliveMessage = TimeSpan.Zero; } - private async Task TbChatInput_EnterPressedAsync(CancellationToken cancellationToken) + private async ValueTask TbChatInput_EnterPressedAsync(CancellationToken cancellationToken) { if (string.IsNullOrEmpty(tbChatInput.Text)) return; @@ -500,7 +498,7 @@ private async Task TbChatInput_EnterPressedAsync(CancellationToken cancellationT tbChatInput.Text = string.Empty; } - private async Task JoinGameAsync() + private async ValueTask JoinGameAsync() { if (lbGameList.SelectedIndex < 0 || lbGameList.SelectedIndex >= lbGameList.Items.Count) return; @@ -596,7 +594,7 @@ private async Task JoinGameAsync() } } - private async Task BtnMainMenu_LeftClickAsync() + private async ValueTask BtnMainMenu_LeftClickAsync() { Visible = false; Enabled = false; @@ -606,7 +604,7 @@ private async Task BtnMainMenu_LeftClickAsync() Exited?.Invoke(this, EventArgs.Empty); } - private async Task BtnNewGame_LeftClickAsync() + private async ValueTask BtnNewGame_LeftClickAsync() { if (!ClientConfiguration.Instance.DisableMultiplayerGameLoading) gameCreationWindow.Open(); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index b6e1857e1..2e3fcad0d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -1,9 +1,10 @@ -using ClientCore; -using System; +using System; +using System.Globalization; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using ClientCore; using ClientCore.Extensions; namespace DTAClient.Domain.Multiplayer.CnCNet @@ -13,7 +14,8 @@ namespace DTAClient.Domain.Multiplayer.CnCNet /// public static class CnCNetPlayerCountTask { - private static int REFRESH_INTERVAL = 60000; // 1 minute + private const int REFRESH_INTERVAL = 60000; + private const int REFRESH_TIMEOUT = 10000; internal static event EventHandler CnCNetGameCountUpdated; @@ -26,14 +28,16 @@ public static void InitializeService(CancellationTokenSource cts) RunServiceAsync(cts.Token).HandleTask(); } - private static async Task RunServiceAsync(CancellationToken cancellationToken) + private static async ValueTask RunServiceAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { - CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(await GetCnCNetPlayerCountAsync())); - try { + using var timeoutCancellationTokenSource = new CancellationTokenSource(REFRESH_TIMEOUT); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); + + CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(await GetCnCNetPlayerCountAsync(linkedCancellationTokenSource.Token))); await Task.Delay(REFRESH_INTERVAL, cancellationToken); } catch (OperationCanceledException) @@ -43,7 +47,7 @@ private static async Task RunServiceAsync(CancellationToken cancellationToken) } } - private static async Task GetCnCNetPlayerCountAsync() + private static async ValueTask GetCnCNetPlayerCountAsync(CancellationToken cancellationToken) { try { @@ -55,16 +59,15 @@ private static async Task GetCnCNetPlayerCountAsync() }, true) { - Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT), DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; - string info = await client.GetStringAsync($"{Uri.UriSchemeHttps}://api.cncnet.org/status"); + string info = await client.GetStringAsync($"{Uri.UriSchemeHttps}://api.cncnet.org/status", cancellationToken); - info = info.Replace("{", String.Empty); - info = info.Replace("}", String.Empty); - info = info.Replace("\"", String.Empty); - string[] values = info.Split(new char[] { ',' }); + info = info.Replace("{", string.Empty); + info = info.Replace("}", string.Empty); + info = info.Replace("\"", string.Empty); + string[] values = info.Split(new[] { ',' }); int numGames = -1; @@ -72,7 +75,7 @@ private static async Task GetCnCNetPlayerCountAsync() { if (value.Contains(cncnetLiveStatusIdentifier)) { - numGames = Convert.ToInt32(value[(cncnetLiveStatusIdentifier.Length + 1)..]); + numGames = Convert.ToInt32(value[(cncnetLiveStatusIdentifier.Length + 1)..], CultureInfo.InvariantCulture); return numGames; } } @@ -87,7 +90,7 @@ private static async Task GetCnCNetPlayerCountAsync() } } - internal class PlayerCountEventArgs : EventArgs + internal sealed class PlayerCountEventArgs : EventArgs { public PlayerCountEventArgs(int playerCount) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index f84505a01..27994d1ab 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -17,7 +17,6 @@ namespace DTAClient.Domain.Multiplayer.CnCNet internal sealed class CnCNetTunnel { private const int PING_PACKET_SEND_SIZE = 50; - private const int PING_PACKET_RECEIVE_SIZE = 12; private const int PING_TIMEOUT = 1000; private const int HASH_LENGTH = 10; @@ -149,7 +148,7 @@ public string Hash /// Gets a list of player ports to use from a specific tunnel server. /// /// A list of player ports to use. - public async Task> GetPlayerPortInfoAsync(int playerCount) + public async ValueTask> GetPlayerPortInfoAsync(int playerCount) { if (Version != Constants.TUNNEL_VERSION_2) throw new InvalidOperationException($"{nameof(GetPlayerPortInfoAsync)} only works with version {Constants.TUNNEL_VERSION_2} tunnels."); @@ -195,7 +194,7 @@ public async Task> GetPlayerPortInfoAsync(int playerCount) return new List(); } - public async Task UpdatePingAsync() + public async ValueTask UpdatePingAsync() { using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); @@ -210,9 +209,6 @@ public async Task UpdatePingAsync() long ticks = DateTime.Now.Ticks; await socket.SendToAsync(buffer, SocketFlags.None, ep); - - buffer = buffer[..PING_PACKET_RECEIVE_SIZE]; - await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep); ticks = DateTime.Now.Ticks - ticks; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs index c38002d12..c13e80653 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs @@ -3,7 +3,6 @@ internal static class Constants { internal const int TUNNEL_CONNECTION_TIMEOUT = 10000; // In milliseconds - internal const int TUNNEL_RECEIVE_TIMEOUT = 30000; internal const int TUNNEL_VERSION_2 = 2; internal const int TUNNEL_VERSION_3 = 3; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameDataException.cs b/DXMainClient/Domain/Multiplayer/CnCNet/GameDataException.cs new file mode 100644 index 000000000..2da0d51c2 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/GameDataException.cs @@ -0,0 +1,7 @@ +using System; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal sealed class GameDataException : Exception +{ +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/IRCChannelModes.cs b/DXMainClient/Domain/Multiplayer/CnCNet/IRCChannelModes.cs new file mode 100644 index 000000000..b11fa6b0a --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/IRCChannelModes.cs @@ -0,0 +1,14 @@ +#pragma warning disable SA1310 +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal static class IRCChannelModes +{ + public const char BAN = 'b'; + public const char INVITE_ONLY = 'i'; + public const char CHANNEL_KEY = 'k'; + public const char CHANNEL_LIMIT = 'l'; + public const char NO_EXTERNAL_MESSAGES = 'n'; + public const char NO_NICKNAME_CHANGE = 'N'; + public const char SECRET_CHANNEL = 's'; + public static string DEFAULT = $"{CHANNEL_KEY}{CHANNEL_LIMIT}{NO_EXTERNAL_MESSAGES}{NO_NICKNAME_CHANGE}{SECRET_CHANNEL}"; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs index 653bd0303..12358ea45 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs @@ -61,7 +61,7 @@ public static void UploadMap(Map map, string myGame) } } - private static async Task UploadAsync(Map map, string myGameId) + private static async ValueTask UploadAsync(Map map, string myGameId) { MapUploadStarted?.Invoke(null, new MapEventArgs(map)); @@ -102,7 +102,7 @@ private static async Task UploadAsync(Map map, string myGameId) } } - private static async Task<(string Message, bool Success)> MapUploadAsync(Map map, string gameName) + private static async ValueTask<(string Message, bool Success)> MapUploadAsync(Map map, string gameName) { using MemoryStream zipStream = CreateZipFile(map.CompleteFilePath); @@ -132,10 +132,9 @@ private static async Task UploadAsync(Map map, string myGameId) } } - private static async Task UploadFilesAsync(List files, NameValueCollection values) + private static async ValueTask UploadFilesAsync(List files, NameValueCollection values) { using HttpClient client = GetHttpClient(); - var multipartFormDataContent = new MultipartFormDataContent(); // Write the values @@ -208,7 +207,7 @@ public static void DownloadMap(string sha1, string myGame, string mapName) } } - private static async Task DownloadAsync(string sha1, string myGameId, string mapName) + private static async ValueTask DownloadAsync(string sha1, string myGameId, string mapName) { Logger.Log("MapSharer: Preparing to download map " + sha1 + " with name: " + mapName); @@ -250,7 +249,7 @@ private static async Task DownloadAsync(string sha1, string myGameId, string map public static string GetMapFileName(string sha1, string mapName) => FormattableString.Invariant($"{mapName}_{sha1}"); - private static async Task<(string Error, bool Success)> DownloadMainAsync(string sha1, string myGame, string mapName) + private static async ValueTask<(string Error, bool Success)> DownloadMainAsync(string sha1, string myGame, string mapName) { string customMapsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Maps", "Custom"); string mapFileName = GetMapFileName(sha1, mapName); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 84da6d9a3..14c66e2b9 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -77,7 +77,7 @@ private void DoCurrentTunnelPinged() private void ConnectionManager_Disconnected(object sender, EventArgs e) => Enabled = false; - private async Task RefreshTunnelsAsync() + private async ValueTask RefreshTunnelsAsync() { List tunnels = await DoRefreshTunnelsAsync(); wm.AddCallback(() => HandleRefreshedTunnels(tunnels)); @@ -115,7 +115,7 @@ private void HandleRefreshedTunnels(List tunnels) } } - private async Task PingListTunnelAsync(int index) + private async ValueTask PingListTunnelAsync(int index) { await Tunnels[index].UpdatePingAsync(); DoTunnelPinged(index); @@ -124,7 +124,7 @@ private async Task PingListTunnelAsync(int index) private void PingCurrentTunnel(bool checkTunnelList = false) => PingCurrentTunnelAsync(checkTunnelList).HandleTask(); - private async Task PingCurrentTunnelAsync(bool checkTunnelList = false) + private async ValueTask PingCurrentTunnelAsync(bool checkTunnelList = false) { await CurrentTunnel.UpdatePingAsync(); DoCurrentTunnelPinged(); @@ -142,7 +142,7 @@ private async Task PingCurrentTunnelAsync(bool checkTunnelList = false) /// Downloads and parses the list of CnCNet tunnels. /// /// A list of tunnel servers. - private static async Task> DoRefreshTunnelsAsync() + private static async ValueTask> DoRefreshTunnelsAsync() { FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); var returnValue = new List(); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs index 02efacc9a..94aa0a639 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs @@ -51,7 +51,7 @@ static InternetGatewayDevice() }; } - public async ValueTask OpenIpV4PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken = default) + public async ValueTask OpenIpV4PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) { Logger.Log($"Opening IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); @@ -117,7 +117,7 @@ await ExecuteSoapAction Logger.Log($"Deleted IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); } - public async ValueTask GetExternalIpV4AddressAsync(CancellationToken cancellationToken = default) + public async ValueTask GetExternalIpV4AddressAsync(CancellationToken cancellationToken) { Logger.Log($"Requesting external IP address from UPnP device {UPnPDescription.Device.FriendlyName}."); @@ -150,7 +150,7 @@ public async ValueTask GetExternalIpV4AddressAsync(CancellationToken return ipAddress; } - public async ValueTask GetNatRsipStatusAsync(CancellationToken cancellationToken = default) + public async ValueTask GetNatRsipStatusAsync(CancellationToken cancellationToken) { Logger.Log($"Checking NAT status on UPnP device {UPnPDescription.Device.FriendlyName}."); @@ -183,7 +183,7 @@ public async ValueTask GetNatRsipStatusAsync(CancellationToken cancellatio return natEnabled; } - public async ValueTask<(bool FirewallEnabled, bool InboundPinholeAllowed)> GetIpV6FirewallStatusAsync(CancellationToken cancellationToken = default) + public async ValueTask<(bool FirewallEnabled, bool InboundPinholeAllowed)> GetIpV6FirewallStatusAsync(CancellationToken cancellationToken) { Logger.Log($"Checking IPV6 firewall status on UPnP device {UPnPDescription.Device.FriendlyName}."); @@ -197,7 +197,7 @@ public async ValueTask GetNatRsipStatusAsync(CancellationToken cancellatio return (response.FirewallEnabled, response.InboundPinholeAllowed); } - public async Task OpenIpV6PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken = default) + public async ValueTask OpenIpV6PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) { Logger.Log($"Opening IPV6 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 65ae0580d..eaab5ae17 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; @@ -30,7 +31,8 @@ internal static class UPnPHandler [AddressType.IpV6SiteLocal] = IPAddress.Parse("[FF05::C]") }.AsReadOnly(); - public static async ValueTask<(InternetGatewayDevice InternetGatewayDevice, List P2pPorts, List P2pIpV6PortIds, IPAddress ipV6Address, IPAddress ipV4Address)> SetupPortsAsync(InternetGatewayDevice internetGatewayDevice, IEnumerable p2pReservedPorts) + public static async ValueTask<(InternetGatewayDevice InternetGatewayDevice, List P2pPorts, List P2pIpV6PortIds, IPAddress ipV6Address, IPAddress ipV4Address)> SetupPortsAsync( + InternetGatewayDevice internetGatewayDevice, IEnumerable p2pReservedPorts, CancellationToken cancellationToken = default) { var p2pPorts = new List(); var p2pIpV6PortIds = new List(); @@ -40,7 +42,7 @@ internal static class UPnPHandler if (internetGatewayDevice is null) { - var internetGatewayDevices = (await GetInternetGatewayDevicesAsync()).ToList(); + var internetGatewayDevices = (await GetInternetGatewayDevicesAsync(cancellationToken)).ToList(); internetGatewayDevice = GetInternetGatewayDevice(internetGatewayDevices, 2); internetGatewayDevice ??= GetInternetGatewayDevice(internetGatewayDevices, 1); @@ -48,8 +50,8 @@ internal static class UPnPHandler if (internetGatewayDevice is not null) { - routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(); - routerPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(); + routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(cancellationToken); + routerPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken); } var publicIpAddresses = NetworkHelper.GetPublicIpAddresses().ToList(); @@ -71,7 +73,7 @@ internal static class UPnPHandler { foreach (int p2PReservedPort in p2pReservedPorts) { - p2pPorts.Add(await internetGatewayDevice.OpenIpV4PortAsync(privateIpV4Address, (ushort)p2PReservedPort)); + p2pPorts.Add(await internetGatewayDevice.OpenIpV4PortAsync(privateIpV4Address, (ushort)p2PReservedPort, cancellationToken)); } p2pReservedPorts = p2pPorts; @@ -109,13 +111,13 @@ internal static class UPnPHandler if (publicIpV6Address is not null && internetGatewayDevice is not null) { - (bool firewallEnabled, bool inboundPinholeAllowed) = await internetGatewayDevice.GetIpV6FirewallStatusAsync(); + (bool firewallEnabled, bool inboundPinholeAllowed) = await internetGatewayDevice.GetIpV6FirewallStatusAsync(cancellationToken); if (firewallEnabled && inboundPinholeAllowed) { foreach (int p2pReservedPort in p2pReservedPorts) { - p2pIpV6PortIds.Add(await internetGatewayDevice.OpenIpV6PortAsync(publicIpV6Address, (ushort)p2pReservedPort)); + p2pIpV6PortIds.Add(await internetGatewayDevice.OpenIpV6PortAsync(publicIpV6Address, (ushort)p2pReservedPort, cancellationToken)); } } } @@ -128,7 +130,7 @@ internal static class UPnPHandler return (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address); } - private static async ValueTask> GetInternetGatewayDevicesAsync(CancellationToken cancellationToken = default) + private static async ValueTask> GetInternetGatewayDevicesAsync(CancellationToken cancellationToken) { IEnumerable rawDeviceResponses = await GetRawDeviceResponses(cancellationToken); IEnumerable> formattedDeviceResponses = GetFormattedDeviceResponses(rawDeviceResponses); @@ -176,23 +178,37 @@ private static async Task> SearchDevicesAsync(IPAddress loca if (addressType is AddressType.Unknown) return responses; - using var socket = new Socket(localAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + var socket = new Socket(localAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); - socket.ExclusiveAddressUse = true; + try + { + socket.ExclusiveAddressUse = true; + + socket.Bind(new IPEndPoint(localAddress, 0)); - socket.Bind(new IPEndPoint(localAddress, 0)); + var multiCastIpEndPoint = new IPEndPoint(SsdpMultiCastAddresses[addressType], UPnPMultiCastPort); + string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {multiCastIpEndPoint}\r\nST: {InternetGatewayDeviceDeviceType}\r\nMAN: \"ssdp:discover\"\r\nMX: 3\r\n\r\n"); + const int charSize = sizeof(char); + int bufferSize = request.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; + int bytes = Encoding.UTF8.GetBytes(request.AsSpan(), buffer.Span); - var multiCastIpEndPoint = new IPEndPoint(SsdpMultiCastAddresses[addressType], UPnPMultiCastPort); - string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {multiCastIpEndPoint}\r\nST: {InternetGatewayDeviceDeviceType}\r\nMAN: \"ssdp:discover\"\r\nMX: 3\r\n\r\n"); - var buffer = new ArraySegment(Encoding.UTF8.GetBytes(request)); + buffer = buffer[..bytes]; - for (int i = 0; i < SendCount; i++) + for (int i = 0; i < SendCount; i++) + { + await socket.SendToAsync(buffer, SocketFlags.None, multiCastIpEndPoint, cancellationToken); + await Task.Delay(100, cancellationToken); + } + + await ReceiveAsync(socket, responses, cancellationToken); + } + finally { - _ = await socket.SendToAsync(buffer, SocketFlags.None, multiCastIpEndPoint); + socket.Close(); } - await ReceiveAsync(socket, new(new byte[4096]), responses, ReceiveTimeout, cancellationToken); - return responses; } @@ -210,19 +226,21 @@ private static AddressType GetAddressType(IPAddress localAddress) return AddressType.Unknown; } - private static async ValueTask ReceiveAsync(Socket socket, ArraySegment buffer, ICollection responses, int receiveTimeout, CancellationToken cancellationToken) + private static async ValueTask ReceiveAsync(Socket socket, ICollection responses, CancellationToken cancellationToken) { - using var timeoutCancellationTokenSource = new CancellationTokenSource(receiveTimeout); + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); + using var timeoutCancellationTokenSource = new CancellationTokenSource(ReceiveTimeout); using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); while (!linkedCancellationTokenSource.IsCancellationRequested) { + Memory buffer = memoryOwner.Memory[..4096]; + try { int bytesReceived = await socket.ReceiveAsync(buffer, SocketFlags.None, linkedCancellationTokenSource.Token); - if (bytesReceived > 0) - responses.Add(Encoding.UTF8.GetString(buffer.Take(bytesReceived).ToArray())); + responses.Add(Encoding.UTF8.GetString(buffer.Span[..bytesReceived])); } catch (OperationCanceledException) { @@ -240,7 +258,6 @@ private static async ValueTask GetUPnPDescription(Uri uri, Canc }, true) { Timeout = TimeSpan.FromMilliseconds(ReceiveTimeout), - DefaultRequestVersion = HttpVersion.Version11, DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs index 175b37c00..e3e055053 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -13,57 +12,60 @@ namespace DTAClient.Domain.Multiplayer.CnCNet; /// internal sealed class V3GameTunnelHandler : IDisposable { - private readonly Dictionary playerConnections = new(); + private readonly Dictionary localGamePlayerConnections = new(); + private readonly CancellationTokenSource connectionErrorCancellationTokenSource = new(); - private CancellationToken cancellationToken; private V3RemotePlayerConnection remoteHostConnection; - private EventHandler gameDataReceivedFunc; + private EventHandler remoteHostGameDataReceivedFunc; + private EventHandler localGameGameDataReceivedFunc; - public event EventHandler RaiseConnectedEvent; + /// + /// Occurs when the connection to the remote host succeeded. + /// + public event EventHandler RaiseRemoteHostConnectedEvent; - public event EventHandler RaiseConnectionFailedEvent; + /// + /// Occurs when the connection to the remote host could not be made. + /// + public event EventHandler RaiseRemoteHostConnectionFailedEvent; - public bool IsConnected { get; private set; } + public bool ConnectSucceeded { get; private set; } public void SetUp(IPEndPoint remoteIpEndPoint, ushort localPort, uint gameLocalPlayerId, CancellationToken cancellationToken) { - this.cancellationToken = cancellationToken; + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(connectionErrorCancellationTokenSource.Token, cancellationToken); + remoteHostConnection = new V3RemotePlayerConnection(); - gameDataReceivedFunc = (_, e) => RemoteHostConnection_MessageReceivedAsync(e).HandleTask(); + remoteHostGameDataReceivedFunc = (_, e) => RemoteHostConnection_MessageReceivedAsync(e).HandleTask(); + localGameGameDataReceivedFunc = (_, e) => PlayerConnection_PacketReceivedAsync(e).HandleTask(); remoteHostConnection.RaiseConnectedEvent += RemoteHostConnection_Connected; remoteHostConnection.RaiseConnectionFailedEvent += RemoteHostConnection_ConnectionFailed; - remoteHostConnection.RaiseConnectionCutEvent += RemoteHostConnection_ConnectionCut; - remoteHostConnection.RaiseGameDataReceivedEvent += gameDataReceivedFunc; + remoteHostConnection.RaiseConnectionCutEvent += Connection_ConnectionCut; + remoteHostConnection.RaiseGameDataReceivedEvent += remoteHostGameDataReceivedFunc; remoteHostConnection.SetUp(remoteIpEndPoint, localPort, gameLocalPlayerId, cancellationToken); } - public List CreatePlayerConnections(List playerIds) + public IEnumerable CreatePlayerConnections(List playerIds) { - ushort[] ports = new ushort[playerIds.Count]; - - for (int i = 0; i < playerIds.Count; i++) + foreach (uint playerId in playerIds) { - var playerConnection = new V3LocalPlayerConnection(); + var localGamePlayerConnection = new V3LocalPlayerConnection(); - playerConnection.RaiseGameDataReceivedEvent += (_, e) => PlayerConnection_PacketReceivedAsync(e).HandleTask(); - playerConnection.Setup(playerIds[i], cancellationToken); + localGamePlayerConnection.RaiseConnectionCutEvent += Connection_ConnectionCut; + localGamePlayerConnection.RaiseGameDataReceivedEvent += localGameGameDataReceivedFunc; - ports[i] = playerConnection.PortNumber; + localGamePlayerConnections.Add(playerId, localGamePlayerConnection); - playerConnections.Add(playerIds[i], playerConnection); + yield return localGamePlayerConnection.Setup(playerId, connectionErrorCancellationTokenSource.Token); } - - return ports.ToList(); } - public void StartPlayerConnections(int gamePort) + public void StartPlayerConnections() { - foreach (KeyValuePair playerConnection in playerConnections) - { - playerConnection.Value.StartConnectionAsync(gamePort).HandleTask(); - } + foreach (KeyValuePair playerConnection in localGamePlayerConnections) + playerConnection.Value.StartConnectionAsync().HandleTask(); } public void ConnectToTunnel() @@ -74,75 +76,73 @@ public void ConnectToTunnel() remoteHostConnection.StartConnectionAsync().HandleTask(); } - /// - /// Forwards local game data to the remote host. - /// - private ValueTask PlayerConnection_PacketReceivedAsync(GameDataReceivedEventArgs e) - => remoteHostConnection?.SendDataAsync(e.GameData, e.PlayerId) ?? ValueTask.CompletedTask; - - /// - /// Forwards remote player data to the local game. - /// - private ValueTask RemoteHostConnection_MessageReceivedAsync(GameDataReceivedEventArgs e) + public void Dispose() { - V3LocalPlayerConnection localPlayerConnection = GetLocalPlayerConnection(e.PlayerId); + if (!connectionErrorCancellationTokenSource.IsCancellationRequested) + connectionErrorCancellationTokenSource.Cancel(); - return localPlayerConnection?.SendDataAsync(e.GameData) ?? ValueTask.CompletedTask; - } + connectionErrorCancellationTokenSource.Dispose(); - public void Dispose() - { - foreach (KeyValuePair remotePlayerGameConnection in playerConnections) + foreach (KeyValuePair localGamePlayerConnection in localGamePlayerConnections) { - remotePlayerGameConnection.Value.Dispose(); + localGamePlayerConnection.Value.RaiseConnectionCutEvent -= Connection_ConnectionCut; + localGamePlayerConnection.Value.RaiseGameDataReceivedEvent -= localGameGameDataReceivedFunc; + + localGamePlayerConnection.Value.Dispose(); } - playerConnections.Clear(); + localGamePlayerConnections.Clear(); if (remoteHostConnection == null) return; remoteHostConnection.RaiseConnectedEvent -= RemoteHostConnection_Connected; remoteHostConnection.RaiseConnectionFailedEvent -= RemoteHostConnection_ConnectionFailed; - remoteHostConnection.RaiseConnectionCutEvent -= RemoteHostConnection_ConnectionCut; - remoteHostConnection.RaiseGameDataReceivedEvent -= gameDataReceivedFunc; + remoteHostConnection.RaiseConnectionCutEvent -= Connection_ConnectionCut; + remoteHostConnection.RaiseGameDataReceivedEvent -= remoteHostGameDataReceivedFunc; remoteHostConnection.Dispose(); } + /// + /// Forwards local game data to the remote host. + /// + private ValueTask PlayerConnection_PacketReceivedAsync(GameDataReceivedEventArgs e) + => remoteHostConnection?.SendDataAsync(e.GameData, e.PlayerId) ?? ValueTask.CompletedTask; + + /// + /// Forwards remote player data to the local game. + /// + private ValueTask RemoteHostConnection_MessageReceivedAsync(GameDataReceivedEventArgs e) + => GetLocalPlayerConnection(e.PlayerId)?.SendDataAsync(e.GameData) ?? ValueTask.CompletedTask; + private V3LocalPlayerConnection GetLocalPlayerConnection(uint senderId) - => playerConnections.TryGetValue(senderId, out V3LocalPlayerConnection connection) ? connection : null; + => localGamePlayerConnections.TryGetValue(senderId, out V3LocalPlayerConnection connection) ? connection : null; private void RemoteHostConnection_Connected(object sender, EventArgs e) { - IsConnected = true; + ConnectSucceeded = true; - OnRaiseConnectedEvent(EventArgs.Empty); + OnRaiseRemoteHostConnectedEvent(EventArgs.Empty); } private void RemoteHostConnection_ConnectionFailed(object sender, EventArgs e) - { - IsConnected = false; - - OnRaiseConnectionFailedEvent(EventArgs.Empty); - } + => OnRaiseRemoteHostConnectionFailedEvent(EventArgs.Empty); - private void OnRaiseConnectedEvent(EventArgs e) + private void OnRaiseRemoteHostConnectedEvent(EventArgs e) { - EventHandler raiseEvent = RaiseConnectedEvent; + EventHandler raiseEvent = RaiseRemoteHostConnectedEvent; raiseEvent?.Invoke(this, e); } - private void OnRaiseConnectionFailedEvent(EventArgs e) + private void OnRaiseRemoteHostConnectionFailedEvent(EventArgs e) { - EventHandler raiseEvent = RaiseConnectionFailedEvent; + EventHandler raiseEvent = RaiseRemoteHostConnectionFailedEvent; raiseEvent?.Invoke(this, e); } - private void RemoteHostConnection_ConnectionCut(object sender, EventArgs e) - { - Dispose(); - } + private void Connection_ConnectionCut(object sender, EventArgs e) + => Dispose(); } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs index 857ea38f5..5a30582ec 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs @@ -4,6 +4,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using ClientCore; using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet; @@ -13,19 +14,24 @@ namespace DTAClient.Domain.Multiplayer.CnCNet; /// internal sealed class V3LocalPlayerConnection : IDisposable { - private const int Timeout = 60000; private const uint IOC_IN = 0x80000000; private const uint IOC_VENDOR = 0x18000000; private const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; + private const int SendTimeout = 10000; + private const int GameStartReceiveTimeout = 60000; + private const int ReceiveTimeout = 10000; private Socket localGameSocket; private EndPoint remotePlayerEndPoint; private CancellationToken cancellationToken; private uint playerId; - public ushort PortNumber { get; private set; } - - public void Setup(uint playerId, CancellationToken cancellationToken) + /// + /// Creates a local game socket and returns the port. + /// + /// The id of the player for which to create the local game socket. + /// The port of the created socket. + public ushort Setup(uint playerId, CancellationToken cancellationToken) { this.cancellationToken = cancellationToken; this.playerId = playerId; @@ -37,47 +43,83 @@ public void Setup(uint playerId, CancellationToken cancellationToken) localGameSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - PortNumber = (ushort)((IPEndPoint)localGameSocket.LocalEndPoint).Port; + return (ushort)((IPEndPoint)localGameSocket.LocalEndPoint).Port; } + /// + /// Occurs when the connection to the local game was lost. + /// + public event EventHandler RaiseConnectionCutEvent; + + /// + /// Occurs when game data from the local game was received. + /// public event EventHandler RaiseGameDataReceivedEvent; /// /// Starts listening for local game player data and forwards it to the tunnel. /// - /// The game UDP port to listen on. - public async ValueTask StartConnectionAsync(int gamePort) + public async ValueTask StartConnectionAsync() { - remotePlayerEndPoint = new IPEndPoint(IPAddress.Loopback, gamePort); + remotePlayerEndPoint = new IPEndPoint(IPAddress.Loopback, 0); using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(128); Memory buffer = memoryOwner.Memory[..128]; - - localGameSocket.ReceiveTimeout = Timeout; + int receiveTimeout = GameStartReceiveTimeout; #if DEBUG - Logger.Log($"Start listening for local game {remotePlayerEndPoint} on {localGameSocket.LocalEndPoint}."); + Logger.Log($"Start listening for local game {remotePlayerEndPoint} on {localGameSocket.LocalEndPoint} for player {playerId}."); #else - Logger.Log($"Start listening for local game player {playerId}."); + Logger.Log($"Start listening for local game for player {playerId}."); #endif - try + + while (!cancellationToken.IsCancellationRequested) { - while (!cancellationToken.IsCancellationRequested) + using var timeoutCancellationTokenSource = new CancellationTokenSource(receiveTimeout); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); + Memory data; + + try { - SocketReceiveFromResult socketReceiveFromResult = await localGameSocket.ReceiveFromAsync(buffer, SocketFlags.None, remotePlayerEndPoint, cancellationToken); - Memory data = buffer[..socketReceiveFromResult.ReceivedBytes]; + SocketReceiveFromResult socketReceiveFromResult = await localGameSocket.ReceiveFromAsync(buffer, SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token); + + remotePlayerEndPoint = socketReceiveFromResult.RemoteEndPoint; + data = buffer[..socketReceiveFromResult.ReceivedBytes]; #if DEBUG - Logger.Log($"Received data from local game {socketReceiveFromResult.RemoteEndPoint} on {localGameSocket.LocalEndPoint}."); + Logger.Log($"Received data from local game {socketReceiveFromResult.RemoteEndPoint} on {localGameSocket.LocalEndPoint} for player {playerId}."); #endif - OnRaiseGameDataReceivedEvent(new(playerId, data)); } - } - catch (SocketException) - { - } - catch (OperationCanceledException) - { + catch (SocketException ex) + { +#if DEBUG + ProgramConstants.LogException(ex, $"Socket exception in {remotePlayerEndPoint} receive loop for player {playerId}."); +#else + ProgramConstants.LogException(ex, $"Socket exception in receive loop for player {playerId}."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); + + return; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + return; + } + catch (OperationCanceledException) + { +#if DEBUG + Logger.Log($"Local game connection {localGameSocket.LocalEndPoint} timed out for player {playerId} when receiving data."); +#else + Logger.Log($"Local game connection timed out for player {playerId} when receiving data."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); + + return; + } + + receiveTimeout = ReceiveTimeout; + + OnRaiseGameDataReceivedEvent(new(playerId, data)); } } @@ -88,26 +130,57 @@ public async ValueTask StartConnectionAsync(int gamePort) public async ValueTask SendDataAsync(ReadOnlyMemory data) { #if DEBUG - Logger.Log($"Sending data from {localGameSocket.LocalEndPoint} to local game {remotePlayerEndPoint}."); + Logger.Log($"Sending data from {localGameSocket.LocalEndPoint} to local game {remotePlayerEndPoint} for player {playerId}."); #endif + if (remotePlayerEndPoint is null) + return; + + using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); + try { - await localGameSocket.SendToAsync(data, SocketFlags.None, remotePlayerEndPoint, cancellationToken); + await localGameSocket.SendToAsync(data, SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token); + } + catch (SocketException ex) + { +#if DEBUG + ProgramConstants.LogException(ex, $"Socket exception sending data to {remotePlayerEndPoint} for player {playerId}."); +#else + ProgramConstants.LogException(ex, $"Socket exception sending data for player {playerId}."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { } catch (OperationCanceledException) { +#if DEBUG + Logger.Log($"Local game connection {localGameSocket.LocalEndPoint} timed out for player {playerId} when sending data."); +#else + Logger.Log($"Local game connection timed out for player {playerId} when sending data."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); } } public void Dispose() { #if DEBUG - Logger.Log($"Connection to local game {localGameSocket.RemoteEndPoint} closed."); + Logger.Log($"Connection to local game {remotePlayerEndPoint} closed for player {playerId}."); #else - Logger.Log($"Connection to local game for player {playerId} closed."); + Logger.Log($"Connection to local game closed for player {playerId}."); #endif - localGameSocket.Dispose(); + localGameSocket.Close(); + } + + private void OnRaiseConnectionCutEvent(EventArgs e) + { + EventHandler raiseEvent = RaiseConnectionCutEvent; + + raiseEvent?.Invoke(this, e); } private void OnRaiseGameDataReceivedEvent(GameDataReceivedEventArgs e) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index e9a75e6fb..e8665833f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -14,6 +14,10 @@ namespace DTAClient.Domain.Multiplayer.CnCNet; /// internal sealed class V3RemotePlayerConnection : IDisposable { + private const int SendTimeout = 10000; + private const int GameStartReceiveTimeout = 60000; + private const int ReceiveTimeout = 60000; + private uint gameLocalPlayerId; private CancellationToken cancellationToken; private Socket tunnelSocket; @@ -28,12 +32,24 @@ public void SetUp(IPEndPoint remoteEndPoint, ushort localPort, uint gameLocalPla this.localPort = localPort; } + /// + /// Occurs when the connection to the remote host succeeded. + /// public event EventHandler RaiseConnectedEvent; + /// + /// Occurs when the connection to the remote host could not be made. + /// public event EventHandler RaiseConnectionFailedEvent; + /// + /// Occurs when the connection to the remote host was lost. + /// public event EventHandler RaiseConnectionCutEvent; + /// + /// Occurs when game data from the remote host was received. + /// public event EventHandler RaiseGameDataReceivedEvent; /// @@ -47,29 +63,22 @@ public async ValueTask StartConnectionAsync() Logger.Log($"Attempting to establish a connection using {localPort})."); #endif - tunnelSocket = new Socket(SocketType.Dgram, ProtocolType.Udp) - { - SendTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT, - ReceiveTimeout = Constants.TUNNEL_CONNECTION_TIMEOUT - }; + tunnelSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); tunnelSocket.Bind(new IPEndPoint(IPAddress.IPv6Any, localPort)); - try - { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); - Memory buffer = memoryOwner.Memory[..50]; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); + Memory buffer = memoryOwner.Memory[..50]; - if (!BitConverter.TryWriteBytes(buffer.Span[..4], gameLocalPlayerId)) - throw new Exception(); + if (!BitConverter.TryWriteBytes(buffer.Span[..4], gameLocalPlayerId)) + throw new GameDataException(); - await tunnelSocket.SendToAsync(buffer, SocketFlags.None, remoteEndPoint, cancellationToken); -#if DEBUG - Logger.Log($"Connection from {tunnelSocket.LocalEndPoint} to {remoteEndPoint} established."); -#else - Logger.Log($"Connection using {localPort} established."); -#endif - OnRaiseConnectedEvent(EventArgs.Empty); + using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); + + try + { + await tunnelSocket.SendToAsync(buffer, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token); } catch (SocketException ex) { @@ -79,15 +88,31 @@ public async ValueTask StartConnectionAsync() ProgramConstants.LogException(ex, $"Failed to establish connection using {localPort}."); #endif OnRaiseConnectionFailedEvent(EventArgs.Empty); + return; } - catch (OperationCanceledException) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { return; } + catch (OperationCanceledException) + { +#if DEBUG + Logger.Log($"Failed to establish connection (time out) from port {localPort} to {remoteEndPoint}."); +#else + Logger.Log($"Failed to establish connection (time out) using {localPort}."); +#endif + OnRaiseConnectionFailedEvent(EventArgs.Empty); - tunnelSocket.ReceiveTimeout = Constants.TUNNEL_RECEIVE_TIMEOUT; + return; + } +#if DEBUG + Logger.Log($"Connection from {tunnelSocket.LocalEndPoint} to {remoteEndPoint} established."); +#else + Logger.Log($"Connection using {localPort} established."); +#endif + OnRaiseConnectedEvent(EventArgs.Empty); await ReceiveLoopAsync(); } @@ -108,92 +133,134 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data, uint receiverId) Memory packet = memoryOwner.Memory[..bufferSize]; if (!BitConverter.TryWriteBytes(packet.Span[..4], gameLocalPlayerId)) - throw new Exception(); + throw new GameDataException(); if (!BitConverter.TryWriteBytes(packet.Span[4..8], receiverId)) - throw new Exception(); + throw new GameDataException(); data.CopyTo(packet[8..]); + using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); + try { - await tunnelSocket.SendToAsync(packet, SocketFlags.None, remoteEndPoint, cancellationToken); + await tunnelSocket.SendToAsync(packet, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token); + } + catch (SocketException ex) + { +#if DEBUG + ProgramConstants.LogException(ex, $"Socket exception sending data to {remoteEndPoint}."); +#else + ProgramConstants.LogException(ex, $"Socket exception sending data from port {localPort}."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { } catch (OperationCanceledException) { +#if DEBUG + Logger.Log($"Remote host connection {remoteEndPoint} timed out when sending data."); +#else + Logger.Log($"Remote host connection from port {localPort} timed out when sending data."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); } } - private async ValueTask ReceiveLoopAsync() + public void Dispose() { - try - { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); - #if DEBUG - Logger.Log($"Start listening for {remoteEndPoint} on {tunnelSocket.LocalEndPoint}."); + Logger.Log($"Connection to remote host {remoteEndPoint} closed."); #else - Logger.Log($"Start listening on {localPort}."); + Logger.Log($"Connection to remote host on port {localPort} closed."); #endif + tunnelSocket?.Close(); + } - while (!cancellationToken.IsCancellationRequested) - { - Memory buffer = memoryOwner.Memory[..1024]; - SocketReceiveFromResult socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint, cancellationToken); + private async ValueTask ReceiveLoopAsync() + { + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); + int receiveTimeout = GameStartReceiveTimeout; - if (socketReceiveFromResult.ReceivedBytes < 8) - { #if DEBUG - Logger.Log($"Invalid data packet from {remoteEndPoint}"); + Logger.Log($"Start listening for {remoteEndPoint} on {tunnelSocket.LocalEndPoint}."); #else - Logger.Log($"Invalid data packet on {localPort}"); + Logger.Log($"Start listening on {localPort}."); #endif - continue; - } - Memory data = buffer[8..socketReceiveFromResult.ReceivedBytes]; - uint senderId = BitConverter.ToUInt32(buffer[..4].Span); - uint receiverId = BitConverter.ToUInt32(buffer[4..8].Span); + while (!cancellationToken.IsCancellationRequested) + { + Memory buffer = memoryOwner.Memory[..1024]; + SocketReceiveFromResult socketReceiveFromResult; + using var timeoutCancellationTokenSource = new CancellationTokenSource(receiveTimeout); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); + try + { + socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token); + } + catch (SocketException ex) + { #if DEBUG - Logger.Log($"Received {senderId} -> {receiverId} from {remoteEndPoint} on {tunnelSocket.LocalEndPoint}."); - + ProgramConstants.LogException(ex, $"Socket exception in {remoteEndPoint} receive loop."); +#else + ProgramConstants.LogException(ex, $"Socket exception on port {localPort} receive loop."); #endif - if (receiverId != gameLocalPlayerId) - { + OnRaiseConnectionCutEvent(EventArgs.Empty); + + return; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + return; + } + catch (OperationCanceledException) + { #if DEBUG - Logger.Log($"Invalid target (received: {receiverId}, expected: {gameLocalPlayerId}) from {remoteEndPoint}."); + Logger.Log($"Remote host connection {remoteEndPoint} timed out when receiving data."); #else - Logger.Log($"Invalid target (received: {receiverId}, expected: {gameLocalPlayerId}) on port {localPort}."); + Logger.Log($"Remote host connection on port {localPort} timed out when receiving data."); #endif - continue; - } + OnRaiseConnectionCutEvent(EventArgs.Empty); - OnRaiseGameDataReceivedEvent(new(senderId, data)); + return; } - } - catch (SocketException ex) - { + + receiveTimeout = ReceiveTimeout; + + if (socketReceiveFromResult.ReceivedBytes < 8) + { #if DEBUG - ProgramConstants.LogException(ex, $"Socket exception in {remoteEndPoint} receive loop."); + Logger.Log($"Invalid data packet from {socketReceiveFromResult.RemoteEndPoint}"); #else - ProgramConstants.LogException(ex, $"Socket exception on port {localPort} receive loop."); + Logger.Log($"Invalid data packet on {localPort}"); #endif - OnRaiseConnectionCutEvent(EventArgs.Empty); - } - catch (OperationCanceledException) - { - } - } + continue; + } + + Memory data = buffer[8..socketReceiveFromResult.ReceivedBytes]; + uint senderId = BitConverter.ToUInt32(buffer[..4].Span); + uint receiverId = BitConverter.ToUInt32(buffer[4..8].Span); - public void Dispose() - { #if DEBUG - Logger.Log($"Connection to remote host {remoteEndPoint} closed."); + Logger.Log($"Received {senderId} -> {receiverId} from {socketReceiveFromResult.RemoteEndPoint} on {tunnelSocket.LocalEndPoint}."); + +#endif + if (receiverId != gameLocalPlayerId) + { +#if DEBUG + Logger.Log($"Invalid target (received: {receiverId}, expected: {gameLocalPlayerId}) from {socketReceiveFromResult.RemoteEndPoint}."); #else - Logger.Log($"Connection to remote host on port {localPort} closed."); + Logger.Log($"Invalid target (received: {receiverId}, expected: {gameLocalPlayerId}) on port {localPort}."); #endif - tunnelSocket?.Dispose(); + continue; + } + + OnRaiseGameDataReceivedEvent(new(senderId, data)); + } } private void OnRaiseConnectedEvent(EventArgs e) diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index e80743d55..046d8e890 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -83,7 +83,7 @@ public override IPAddress IPAddress /// Sends a message to the player over the network. /// /// The message to send. - public async Task SendMessageAsync(string message, CancellationToken cancellationToken) + public async ValueTask SendMessageAsync(string message, CancellationToken cancellationToken) { message += ProgramConstants.LAN_MESSAGE_SEPARATOR; @@ -114,9 +114,9 @@ public override string ToString() => Name + " (" + IPAddress + ")"; /// - /// Starts receiving messages from the player asynchronously. + /// Starts receiving messages from the player. /// - public async Task StartReceiveLoopAsync(CancellationToken cancellationToken) + public async ValueTask StartReceiveLoopAsync(CancellationToken cancellationToken) { using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); @@ -147,7 +147,7 @@ public async Task StartReceiveLoopAsync(CancellationToken cancellationToken) msg = overMessage + msg; - List commands = new List(); + var commands = new List(); while (true) { diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index 61af448bc..a831532d7 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -140,7 +140,7 @@ private void LoadGameModeAliases(IniFile mpMapsIni) } } - private async Task LoadCustomMapsAsync() + private async ValueTask LoadCustomMapsAsync() { DirectoryInfo customMapsDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, CUSTOM_MAPS_DIRECTORY); @@ -153,7 +153,6 @@ private async Task LoadCustomMapsAsync() IEnumerable mapFiles = customMapsDirectory.EnumerateFiles($"*{MAP_FILE_EXTENSION}"); ConcurrentDictionary customMapCache = await LoadCustomMapCacheAsync(); var localMapSHAs = new List(); - var tasks = new List(); foreach (FileInfo mapFile in mapFiles) @@ -210,7 +209,7 @@ private void CacheCustomMaps(ConcurrentDictionary customMaps) /// Load previously cached custom maps /// /// - private async Task> LoadCustomMapCacheAsync() + private async ValueTask> LoadCustomMapCacheAsync() { try { diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 9e3d9722e..8a9462a79 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -8,7 +8,7 @@ namespace DTAClient.Domain.Multiplayer; -internal sealed class NetworkHelper +internal static class NetworkHelper { private static readonly IReadOnlyCollection SupportedAddressFamilies = new[] { @@ -42,10 +42,10 @@ public static IEnumerable GetUniCastIpAddresses() .SelectMany(q => q.UnicastAddresses) .Where(q => SupportedAddressFamilies.Contains(q.Address.AddressFamily)); - public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unicastIPAddressInformation) + public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unicastIpAddressInformation) { - uint ipAddress = BitConverter.ToUInt32(unicastIPAddressInformation.Address.GetAddressBytes(), 0); - uint ipMaskV4 = BitConverter.ToUInt32(unicastIPAddressInformation.IPv4Mask.GetAddressBytes(), 0); + uint ipAddress = BitConverter.ToUInt32(unicastIpAddressInformation.Address.GetAddressBytes(), 0); + uint ipMaskV4 = BitConverter.ToUInt32(unicastIpAddressInformation.IPv4Mask.GetAddressBytes(), 0); uint broadCastIpAddress = ipAddress | ~ipMaskV4; return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); @@ -55,19 +55,27 @@ public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unic /// Returns a free UDP port number above 1023. /// /// List of UDP port numbers which are additionally excluded. + /// The number of free ports to return. /// A free UDP port number on the current system. - public static ushort GetFreeUdpPort(IEnumerable excludedPorts) + public static IEnumerable GetFreeUdpPorts(IEnumerable excludedPorts, ushort numberOfPorts) { IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); - ushort[] activePorts = endPoints.Select(q => (ushort)q.Port).ToArray().Concat(excludedPorts).ToArray(); - ushort selectedPort = 0; + List activePorts = endPoints.Select(q => (ushort)q.Port).ToArray().Concat(excludedPorts).ToList(); + ushort foundPortCount = 0; - while (selectedPort == 0 || activePorts.Contains(selectedPort)) + while (foundPortCount != numberOfPorts) { - selectedPort = (ushort)new Random().Next(1024, IPEndPoint.MaxPort); - } + ushort foundPort = (ushort)new Random().Next(1024, IPEndPoint.MaxPort); + + if (!activePorts.Contains(foundPort)) + { + activePorts.Add(foundPort); - return selectedPort; + foundPortCount++; + + yield return foundPort; + } + } } private static bool IsPrivateIpAddress(IPAddress ipAddress) diff --git a/DXMainClient/Domain/Multiplayer/P2PPlayer.cs b/DXMainClient/Domain/Multiplayer/P2PPlayer.cs new file mode 100644 index 000000000..136c0f8d5 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/P2PPlayer.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Net; + +namespace DTAClient.Domain.Multiplayer; + +internal readonly record struct P2PPlayer( + string RemotePlayerName, + ushort[] RemotePorts, + List<(IPAddress RemoteIpAddress, long Ping)> LocalPingResults, + List<(IPAddress RemoteIpAddress, long Ping)> RemotePingResults, + bool Enabled); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/PlayerInfo.cs b/DXMainClient/Domain/Multiplayer/PlayerInfo.cs index 52e066d79..d7fda33bf 100644 --- a/DXMainClient/Domain/Multiplayer/PlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/PlayerInfo.cs @@ -45,7 +45,7 @@ public PlayerInfo(string name, int sideId, int startingLocation, int colorId, in public bool IsInGame { get; set; } - public virtual IPAddress IPAddress { get; set; } = System.Net.IPAddress.Any; + public virtual IPAddress IPAddress { get; set; } = IPAddress.Any; public int Port { get; set; } @@ -86,7 +86,7 @@ public override string ToString() } /// - /// Creates a PlayerInfo instance from a string in a format that matches the + /// Creates a PlayerInfo instance from a string in a format that matches the /// string given by the ToString() method. /// /// The string. diff --git a/DXMainClient/Online/Channel.cs b/DXMainClient/Online/Channel.cs index 73aab3fe5..40a3f5ece 100644 --- a/DXMainClient/Online/Channel.cs +++ b/DXMainClient/Online/Channel.cs @@ -114,7 +114,7 @@ public void AddUser(ChannelUser user) UserAdded?.Invoke(this, new ChannelUserEventArgs(user)); } - public async Task OnUserJoinedAsync(ChannelUser user) + public async ValueTask OnUserJoinedAsync(ChannelUser user) { await Task.CompletedTask; AddUser(user); @@ -257,7 +257,7 @@ public void AddMessage(ChatMessage message) MessageAdded?.Invoke(this, new IRCMessageEventArgs(message)); } - public Task SendChatMessageAsync(string message, IRCColor color) + public ValueTask SendChatMessageAsync(string message, IRCColor color) { AddMessage(new ChatMessage(ProgramConstants.PLAYERNAME, color.XnaColor, DateTime.Now, message)); @@ -274,7 +274,7 @@ public Task SendChatMessageAsync(string message, IRCColor color) /// This can be used to help prevent flooding for multiple options that are changed quickly. It allows for a single message /// for multiple changes. /// - public Task SendCTCPMessageAsync(string message, QueuedMessageType qmType, int priority, bool replace = false) + public ValueTask SendCTCPMessageAsync(string message, QueuedMessageType qmType, int priority, bool replace = false) { char CTCPChar1 = (char)58; char CTCPChar2 = (char)01; @@ -288,7 +288,7 @@ public Task SendCTCPMessageAsync(string message, QueuedMessageType qmType, int p /// /// The name of the user that should be kicked. /// The priority of the message in the send queue. - public Task SendKickMessageAsync(string userName, int priority) + public ValueTask SendKickMessageAsync(string userName, int priority) { return connection.QueueMessageAsync(QueuedMessageType.INSTANT_MESSAGE, priority, IRCCommands.KICK + " " + ChannelName + " " + userName); } @@ -298,13 +298,15 @@ public Task SendKickMessageAsync(string userName, int priority) /// /// The host that should be banned. /// The priority of the message in the send queue. - public Task SendBanMessageAsync(string host, int priority) + public ValueTask SendBanMessageAsync(string host, int priority) { - return connection.QueueMessageAsync(QueuedMessageType.INSTANT_MESSAGE, priority, - string.Format(IRCCommands.MODE + " {0} +b *!*@{1}", ChannelName, host)); + return connection.QueueMessageAsync( + QueuedMessageType.INSTANT_MESSAGE, + priority, + FormattableString.Invariant($"{IRCCommands.MODE} {ChannelName} +{IRCChannelModes.BAN} *!*@{host}")); } - public Task JoinAsync() + public ValueTask JoinAsync() { // Wait a random amount of time before joining to prevent join/part floods if (Persistent) @@ -323,12 +325,12 @@ public Task JoinAsync() return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, IRCCommands.JOIN + " " + ChannelName + " " + Password); } - public Task RequestUserInfoAsync() + public ValueTask RequestUserInfoAsync() { return connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, "WHO " + ChannelName); } - public async Task LeaveAsync() + public async ValueTask LeaveAsync() { // Wait a random amount of time before joining to prevent join/part floods if (Persistent) diff --git a/DXMainClient/Online/CnCNetManager.cs b/DXMainClient/Online/CnCNetManager.cs index 621806ce3..b53a6ff36 100644 --- a/DXMainClient/Online/CnCNetManager.cs +++ b/DXMainClient/Online/CnCNetManager.cs @@ -154,12 +154,12 @@ public void SetMainChannel(Channel channel) MainChannel = channel; } - public Task SendCustomMessageAsync(QueuedMessage qm) + public ValueTask SendCustomMessageAsync(QueuedMessage qm) { return connection.QueueMessageAsync(qm); } - public Task SendWhoIsMessageAsync(string nick) + public ValueTask SendWhoIsMessageAsync(string nick) { return SendCustomMessageAsync(new QueuedMessage($"{IRCCommands.WHOIS} {nick}", QueuedMessageType.WHOIS_MESSAGE, 0)); } @@ -441,10 +441,8 @@ private void DoConnectionLost(string reason) /// /// Disconnects from CnCNet. /// - public async Task DisconnectAsync() - { - await connection.DisconnectAsync(); - } + public ValueTask DisconnectAsync() + => connection.DisconnectAsync(); /// /// Connects to CnCNet. @@ -549,7 +547,7 @@ public void OnUserJoinedChannel(string channelName, string host, string userName wm.AddCallback(() => DoUserJoinedChannelAsync(channelName, host, userName, ident).HandleTask()); } - private async Task DoUserJoinedChannelAsync(string channelName, string host, string userName, string userAddress) + private async ValueTask DoUserJoinedChannelAsync(string channelName, string host, string userName, string userAddress) { Channel channel = FindChannel(channelName); @@ -840,7 +838,7 @@ public void OnNameAlreadyInUse() /// IRC user. Adds additional underscores to the name or replaces existing /// characters with underscores. /// - private async Task DoNameAlreadyInUseAsync() + private async ValueTask DoNameAlreadyInUseAsync() { var charList = ProgramConstants.PLAYERNAME.ToList(); int maxNameLength = ClientConfiguration.Instance.MaxNameLength; diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index 8b2d63401..1ad177385 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -26,6 +26,9 @@ internal sealed class Connection private const int RECONNECT_WAIT_DELAY = 4000; private const int ID_LENGTH = 9; private const int MAXIMUM_LATENCY = 400; + private const int SendTimeout = 1000; + private const int ConnectTimeout = 3000; + private const int PingInterval = 120000; public Connection(IConnectionManager connectionManager) { @@ -50,18 +53,18 @@ public Connection(IConnectionManager connectionManager) new("irc.gamesurge.net", "GameSurge", new[] { 6667 }) }.AsReadOnly(); - public bool IsConnected { get; private set; } + private bool IsConnected { get; set; } public bool AttemptingConnection { get; private set; } public Random Rng { get; } = new(); - private List MessageQueue = new(); - private TimeSpan MessageQueueDelay; + private readonly List messageQueue = new(); + private TimeSpan messageQueueDelay; private Socket socket; - volatile int reconnectCount; + private volatile int reconnectCount; private volatile bool connectionCut; private volatile bool welcomeMessageReceived; @@ -69,8 +72,6 @@ public Connection(IConnectionManager connectionManager) private string overMessage; - private readonly Encoding encoding = Encoding.UTF8; - /// /// A list of server IPs that have dropped our connection. /// The client skips these servers when attempting to re-connect, to @@ -111,7 +112,7 @@ public void ConnectAsync() connectionCut = false; AttemptingConnection = true; - MessageQueueDelay = TimeSpan.FromMilliseconds(ClientConfiguration.Instance.SendSleep); + messageQueueDelay = TimeSpan.FromMilliseconds(ClientConfiguration.Instance.SendSleep); connectionCancellationTokenSource?.Dispose(); @@ -123,7 +124,7 @@ public void ConnectAsync() /// /// Attempts to connect to CnCNet. /// - private async Task ConnectToServerAsync(CancellationToken cancellationToken) + private async ValueTask ConnectToServerAsync(CancellationToken cancellationToken) { try { @@ -137,10 +138,9 @@ private async Task ConnectToServerAsync(CancellationToken cancellationToken) { connectionManager.OnAttemptedServerChanged(server.Name); - var client = new Socket(SocketType.Stream, ProtocolType.Tcp) - { - ReceiveTimeout = 1000 - }; + var client = new Socket(SocketType.Stream, ProtocolType.Tcp); + using var timeoutCancellationTokenSource = new CancellationTokenSource(ConnectTimeout); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); Logger.Log("Attempting connection to " + server.Host + ":" + port); @@ -148,13 +148,9 @@ private async Task ConnectToServerAsync(CancellationToken cancellationToken) { await client.ConnectAsync( new IPEndPoint(IPAddress.Parse(server.Host), port), - new CancellationTokenSource(TimeSpan.FromSeconds(3)).Token); + linkedCancellationTokenSource.Token); } - catch (OperationCanceledException) - { - } - - if (!client.Connected) + catch (OperationCanceledException) when (timeoutCancellationTokenSource.Token.IsCancellationRequested) { Logger.Log("Connecting to " + server.Host + " port " + port + " timed out!"); continue; // Start all over again, using the next port @@ -172,7 +168,10 @@ await client.ConnectAsync( RunSendQueueAsync(sendQueueCancellationTokenSource.Token).HandleTask(); - socket?.Dispose(); + if (socket?.Connected ?? false) + socket.Shutdown(SocketShutdown.Both); + + socket?.Close(); socket = client; currentConnectedServerIP = server.Host; @@ -201,7 +200,7 @@ await client.ConnectAsync( } } - private async Task HandleCommAsync(CancellationToken cancellationToken) + private async ValueTask HandleCommAsync(CancellationToken cancellationToken) { int errorTimes = 0; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); @@ -209,7 +208,7 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) await RegisterAsync(); - var timer = new System.Timers.Timer(120000) + var timer = new System.Timers.Timer(PingInterval) { Enabled = true }; @@ -252,7 +251,7 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) errorTimes = 0; // A message has been successfully received - string msg = encoding.GetString(message.Span[..bytesRead]); + string msg = Encoding.UTF8.GetString(message.Span[..bytesRead]); #if !DEBUG Logger.Log("Message received: " + msg); @@ -311,7 +310,7 @@ private async Task HandleCommAsync(CancellationToken cancellationToken) /// Servers that did not respond to ICMP messages in time will be placed at the end of the list. /// /// A list of Lobby servers sorted by latency. - private async Task> GetServerListSortedByLatencyAsync() + private async ValueTask> GetServerListSortedByLatencyAsync() { // Resolve the hostnames. IEnumerable<(IPAddress IpAddress, string Name, int[] Ports)>[] servers = await ClientCore.Extensions.TaskExtensions.WhenAllSafe(Servers.Select(ResolveServerAsync)); @@ -450,10 +449,11 @@ private async Task> GetServerListSortedByLatencyAsync() return Array.Empty<(IPAddress IpAddress, string Name, int[] Ports)>(); } - public async Task DisconnectAsync() + public async ValueTask DisconnectAsync() { await SendMessageAsync(IRCCommands.QUIT); connectionCancellationTokenSource.Cancel(); + socket.Shutdown(SocketShutdown.Both); socket.Close(); } @@ -464,7 +464,7 @@ public async Task DisconnectAsync() /// message, and handles it accordingly. /// /// The message. - private async Task HandleMessageAsync(string message) + private async ValueTask HandleMessageAsync(string message) { string msg = overMessage + message; overMessage = ""; @@ -496,7 +496,7 @@ private async Task HandleMessageAsync(string message) /// /// Handles a specific command received from the IRC server. /// - private async Task PerformCommandAsync(string message) + private async ValueTask PerformCommandAsync(string message) { message = message.Replace("\r", string.Empty); ParseIrcMessage(message, out string prefix, out string command, out List parameters); @@ -807,7 +807,7 @@ private void ParseIrcMessage(string message, out string prefix, out string comma #region Sending commands - private async Task RunSendQueueAsync(CancellationToken cancellationToken) + private async ValueTask RunSendQueueAsync(CancellationToken cancellationToken) { try { @@ -819,9 +819,9 @@ private async Task RunSendQueueAsync(CancellationToken cancellationToken) try { - for (int i = 0; i < MessageQueue.Count; i++) + for (int i = 0; i < messageQueue.Count; i++) { - QueuedMessage qm = MessageQueue[i]; + QueuedMessage qm = messageQueue[i]; if (qm.Delay > 0) { if (qm.SendAt < DateTime.Now) @@ -830,14 +830,14 @@ private async Task RunSendQueueAsync(CancellationToken cancellationToken) Logger.Log("Delayed message sent: " + qm.ID); - MessageQueue.RemoveAt(i); + messageQueue.RemoveAt(i); break; } } else { message = qm.Command; - MessageQueue.RemoveAt(i); + messageQueue.RemoveAt(i); break; } } @@ -854,7 +854,7 @@ private async Task RunSendQueueAsync(CancellationToken cancellationToken) } await SendMessageAsync(message); - await Task.Delay(MessageQueueDelay, cancellationToken); + await Task.Delay(messageQueueDelay, cancellationToken); } } catch (OperationCanceledException) @@ -866,7 +866,7 @@ private async Task RunSendQueueAsync(CancellationToken cancellationToken) try { - MessageQueue.Clear(); + messageQueue.Clear(); } finally { @@ -880,41 +880,38 @@ private async Task RunSendQueueAsync(CancellationToken cancellationToken) /// /// Sends a PING message to the server to indicate that we're still connected. /// - private Task AutoPingAsync() + private ValueTask AutoPingAsync() => SendMessageAsync(IRCCommands.PING_LAG + new Random().Next(100000, 999999)); /// /// Registers the user. /// - private async Task RegisterAsync() + private async ValueTask RegisterAsync() { if (welcomeMessageReceived) return; Logger.Log("Registering."); - var defaultGame = ClientConfiguration.Instance.LocalGame; - - string realname = ProgramConstants.GAME_VERSION + " " + defaultGame + " CnCNet"; - - await SendMessageAsync(string.Format(IRCCommands.USER + " {0} 0 * :{1}", defaultGame + "." + - systemId, realname)); + string defaultGame = ClientConfiguration.Instance.LocalGame; + string realName = ProgramConstants.GAME_VERSION + " " + defaultGame + " CnCNet"; + await SendMessageAsync(FormattableString.Invariant($"{IRCCommands.USER} {defaultGame}.{systemId} 0 * :{realName}")); await SendMessageAsync(IRCCommands.NICK + " " + ProgramConstants.PLAYERNAME); } - public Task ChangeNicknameAsync() + public ValueTask ChangeNicknameAsync() { return SendMessageAsync(IRCCommands.NICK + " " + ProgramConstants.PLAYERNAME); } - public Task QueueMessageAsync(QueuedMessageType type, int priority, string message, bool replace = false) + public ValueTask QueueMessageAsync(QueuedMessageType type, int priority, string message, bool replace = false) { QueuedMessage qm = new QueuedMessage(message, type, priority, replace); return QueueMessageAsync(qm); } - public async Task QueueMessageAsync(QueuedMessageType type, int priority, int delay, string message) + public async ValueTask QueueMessageAsync(QueuedMessageType type, int priority, int delay, string message) { QueuedMessage qm = new QueuedMessage(message, type, priority, delay); await QueueMessageAsync(qm); @@ -925,7 +922,7 @@ public async Task QueueMessageAsync(QueuedMessageType type, int priority, int de /// Send a message to the CnCNet server. /// /// The message to send. - private async Task SendMessageAsync(string message) + private async ValueTask SendMessageAsync(string message) { if (!socket?.Connected ?? false) return; @@ -936,13 +933,15 @@ private async Task SendMessageAsync(string message) int bufferSize = message.Length * charSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); Memory buffer = memoryOwner.Memory[..bufferSize]; - int bytes = encoding.GetBytes((message + "\r\n").AsSpan(), buffer.Span); + int bytes = Encoding.UTF8.GetBytes((message + "\r\n").AsSpan(), buffer.Span); buffer = buffer[..bytes]; + using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); + try { - await socket.SendAsync(buffer, SocketFlags.None, CancellationToken.None); + await socket.SendAsync(buffer, SocketFlags.None, timeoutCancellationTokenSource.Token); } catch (IOException ex) { @@ -963,11 +962,11 @@ private bool ReplaceMessage(QueuedMessage qm) try { - var previousMessageIndex = MessageQueue.FindIndex(m => m.MessageType == qm.MessageType); + var previousMessageIndex = messageQueue.FindIndex(m => m.MessageType == qm.MessageType); if (previousMessageIndex == -1) return false; - MessageQueue[previousMessageIndex] = qm; + messageQueue[previousMessageIndex] = qm; return true; } finally @@ -980,7 +979,7 @@ private bool ReplaceMessage(QueuedMessage qm) /// Adds a message to the send queue. /// /// The message to queue. - public async Task QueueMessageAsync(QueuedMessage qm) + public async ValueTask QueueMessageAsync(QueuedMessage qm) { if (!IsConnected) return; @@ -1012,13 +1011,13 @@ public async Task QueueMessageAsync(QueuedMessage qm) await SendMessageAsync(qm.Command); break; default: - int placeInQueue = MessageQueue.FindIndex(m => m.Priority < qm.Priority); + int placeInQueue = messageQueue.FindIndex(m => m.Priority < qm.Priority); if (ProgramConstants.LOG_LEVEL > 1) Logger.Log("QM Undefined: " + qm.Command + " " + placeInQueue); if (placeInQueue == -1) - MessageQueue.Add(qm); + messageQueue.Add(qm); else - MessageQueue.Insert(placeInQueue, qm); + messageQueue.Insert(placeInQueue, qm); break; } } @@ -1035,7 +1034,7 @@ public async Task QueueMessageAsync(QueuedMessage qm) /// The message to queue. private void AddSpecialQueuedMessage(QueuedMessage qm) { - int broadcastingMessageIndex = MessageQueue.FindIndex(m => m.MessageType == qm.MessageType); + int broadcastingMessageIndex = messageQueue.FindIndex(m => m.MessageType == qm.MessageType); qm.ID = NextQueueID++; @@ -1043,17 +1042,17 @@ private void AddSpecialQueuedMessage(QueuedMessage qm) { if (ProgramConstants.LOG_LEVEL > 1) Logger.Log("QM Replace: " + qm.Command + " " + broadcastingMessageIndex); - MessageQueue[broadcastingMessageIndex] = qm; + messageQueue[broadcastingMessageIndex] = qm; } else { - int placeInQueue = MessageQueue.FindIndex(m => m.Priority < qm.Priority); + int placeInQueue = messageQueue.FindIndex(m => m.Priority < qm.Priority); if (ProgramConstants.LOG_LEVEL > 1) Logger.Log("QM: " + qm.Command + " " + placeInQueue); if (placeInQueue == -1) - MessageQueue.Add(qm); + messageQueue.Add(qm); else - MessageQueue.Insert(placeInQueue, qm); + messageQueue.Insert(placeInQueue, qm); } } diff --git a/DXMainClient/Startup.cs b/DXMainClient/Startup.cs index b5b892847..dde0ed263 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -308,7 +308,7 @@ private static void CheckSystemSpecifications() /// /// Generate an ID for online play. /// - private static async Task GenerateOnlineIdAsync() + private static async ValueTask GenerateOnlineIdAsync() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { From 949144b0af0f301477b91aa9bb8b1f856f00fd5a Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 7 Dec 2022 00:25:28 +0100 Subject: [PATCH 052/109] CnCNetGameLobby Dispose --- ClientCore/Extensions/TaskExtensions.cs | 23 +------------------ .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 20 ++++++++++++++-- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/ClientCore/Extensions/TaskExtensions.cs b/ClientCore/Extensions/TaskExtensions.cs index 21571d544..1094a7263 100644 --- a/ClientCore/Extensions/TaskExtensions.cs +++ b/ClientCore/Extensions/TaskExtensions.cs @@ -94,8 +94,7 @@ public static async Task WhenAllSafe(IEnumerable tasks) /// Runs a and guarantees all exceptions are caught and handled even when the is not directly awaited. /// /// The who's exceptions will be handled. - /// Returns a that awaited and handled the original . - public static async ValueTask HandleTask(this ValueTask task) + public static async void HandleTask(this ValueTask task) { try { @@ -106,24 +105,4 @@ public static async ValueTask HandleTask(this ValueTask task) ProgramConstants.HandleException(ex); } } - - /// - /// Runs a and guarantees all exceptions are caught and handled even when the is not directly awaited. - /// - /// The type of 's return value. - /// The who's exceptions will be handled. - /// Returns a that awaited and handled the original . - public static async ValueTask HandleTask(this ValueTask task) - { - try - { - return await task; - } - catch (Exception ex) - { - ProgramConstants.HandleException(ex); - } - - return default; - } } \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index b7c73b439..fd169b6d4 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -78,6 +78,7 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private List p2pPorts = new(); private List p2pIpV6PortIds = new(); private CancellationTokenSource gameStartCancellationTokenSource; + private bool disposed; /// /// The SHA1 of the latest selected map. @@ -192,9 +193,11 @@ public CnCNetGameLobby( _ => ToggleDynamicTunnelsAsync().HandleTask())); AddChatBoxCommand(new( CnCNetLobbyCommands.P2P, - "Toggle P2P connections on/off".L10N("Client:Main:ChangeP2P"), + "Toggle P2P connections on/off, your IP will be public to players in the lobby".L10N("Client:Main:ChangeP2P"), false, _ => ToggleP2PAsync().HandleTask())); + + WindowManager.GameClosing += (_, _) => Dispose(true); } public event EventHandler GameLeft; @@ -264,6 +267,19 @@ public override void Initialize() PostInitialize(); } + protected override void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + ClearAsync().HandleTask(); + + disposed = true; + } + + base.Dispose(disposing); + } + private void GameStartTimer_TimeElapsed(object sender, EventArgs e) { string playerString = string.Empty; @@ -517,7 +533,7 @@ public override async ValueTask ClearAsync() v3GameTunnelHandlers.Clear(); playerTunnels.Clear(); gamePlayerIds.Clear(); - pinnedTunnels.Clear(); + pinnedTunnels?.Clear(); p2pPlayers.Clear(); GameLeft?.Invoke(this, EventArgs.Empty); TopBar.RemovePrimarySwitchable(this); From 7c8734aa94d01c3fa309e57a11f44db755217fd8 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 7 Dec 2022 10:45:11 +0100 Subject: [PATCH 053/109] Use Tasks instead of Threads --- ClientGUI/GameProcessLogic.cs | 6 +- .../DXGUI/Generic/CampaignSelector.cs | 23 ++---- .../DXGUI/Generic/GameLoadingWindow.cs | 10 ++- DXMainClient/DXGUI/Generic/MainMenu.cs | 25 +++--- .../CnCNet/CnCNetGameLoadingLobby.cs | 8 +- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 7 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 4 +- .../Multiplayer/GameLobby/GameLobbyBase.cs | 6 +- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 6 +- DXMainClient/Online/CnCNetGameCheck.cs | 76 +++++++------------ 10 files changed, 70 insertions(+), 101 deletions(-) diff --git a/ClientGUI/GameProcessLogic.cs b/ClientGUI/GameProcessLogic.cs index a1e0965c6..203dc4855 100644 --- a/ClientGUI/GameProcessLogic.cs +++ b/ClientGUI/GameProcessLogic.cs @@ -5,7 +5,7 @@ using ClientCore; using Rampastring.Tools; using ClientCore.INIProcessing; -using System.Threading; +using System.Threading.Tasks; using Rampastring.XNAUI; namespace ClientGUI @@ -27,7 +27,7 @@ public static class GameProcessLogic /// /// Starts the main game process. /// - public static void StartGameProcess(WindowManager windowManager) + public static async ValueTask StartGameProcessAsync(WindowManager windowManager) { Logger.Log("About to launch main game executable."); @@ -36,7 +36,7 @@ public static void StartGameProcess(WindowManager windowManager) int waitTimes = 0; while (PreprocessorBackgroundTask.Instance.IsRunning) { - Thread.Sleep(1000); + await Task.Delay(1000); waitTimes++; if (waitTimes > 10) { diff --git a/DXMainClient/DXGUI/Generic/CampaignSelector.cs b/DXMainClient/DXGUI/Generic/CampaignSelector.cs index c758551a2..c2c3d8518 100644 --- a/DXMainClient/DXGUI/Generic/CampaignSelector.cs +++ b/DXMainClient/DXGUI/Generic/CampaignSelector.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using DTAClient.Domain; using System.IO; +using System.Threading.Tasks; +using ClientCore.Extensions; using ClientGUI; using Rampastring.XNAUI.XNAControls; using Rampastring.XNAUI; @@ -149,7 +151,7 @@ public override void Initialize() btnLaunch.ClientRectangle = new Rectangle(12, Height - 35, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLaunch.Text = "Launch".L10N("Client:Main:ButtonLaunch"); btnLaunch.AllowClick = false; - btnLaunch.LeftClick += BtnLaunch_LeftClick; + btnLaunch.LeftClick += (_, _) => BtnLaunch_LeftClickAsync().HandleTask(); var btnCancel = new XNAClientButton(WindowManager); btnCancel.Name = "btnCancel"; @@ -186,7 +188,7 @@ public override void Initialize() AddChild(dp); dp.CenterOnParent(); cheaterWindow.CenterOnParent(); - cheaterWindow.YesClicked += CheaterWindow_YesClicked; + cheaterWindow.YesClicked += (_, _) => LaunchMissionAsync(missionToLaunch).HandleTask(); cheaterWindow.Disable(); } @@ -224,7 +226,7 @@ private void BtnCancel_LeftClick(object sender, EventArgs e) Enabled = false; } - private void BtnLaunch_LeftClick(object sender, EventArgs e) + private async ValueTask BtnLaunch_LeftClickAsync() { int selectedMissionId = lbCampaignList.SelectedIndex; @@ -239,7 +241,7 @@ private void BtnLaunch_LeftClick(object sender, EventArgs e) return; } - LaunchMission(mission); + await LaunchMissionAsync(mission); } private bool AreFilesModified() @@ -253,19 +255,10 @@ private bool AreFilesModified() return false; } - /// - /// Called when the user wants to proceed to the mission despite having - /// being called a cheater. - /// - private void CheaterWindow_YesClicked(object sender, EventArgs e) - { - LaunchMission(missionToLaunch); - } - /// /// Starts a singleplayer mission. /// - private void LaunchMission(Mission mission) + private async ValueTask LaunchMissionAsync(Mission mission) { bool copyMapsToSpawnmapINI = ClientConfiguration.Instance.CopyMissionsToSpawnmapINI; @@ -322,7 +315,7 @@ private void LaunchMission(Mission mission) discordHandler.UpdatePresence(mission.UntranslatedGUIName, difficultyName, mission.IconPath, true); GameProcessLogic.GameProcessExited += GameProcessExited_Callback; - GameProcessLogic.StartGameProcess(WindowManager); + await GameProcessLogic.StartGameProcessAsync(WindowManager); } private int GetComputerDifficulty() => diff --git a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs index 74cf92c18..5c565038c 100644 --- a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.DXGUI.Generic { @@ -55,7 +57,7 @@ public override void Initialize() btnLaunch.ClientRectangle = new Rectangle(125, 345, 110, 23); btnLaunch.Text = "Load".L10N("Client:Main:ButtonLoad"); btnLaunch.AllowClick = false; - btnLaunch.LeftClick += BtnLaunch_LeftClick; + btnLaunch.LeftClick += (_, _) => BtnLaunch_LeftClickAsync().HandleTask(); btnDelete = new XNAClientButton(WindowManager); btnDelete.Name = nameof(btnDelete); @@ -99,7 +101,7 @@ private void BtnCancel_LeftClick(object sender, EventArgs e) Enabled = false; } - private void BtnLaunch_LeftClick(object sender, EventArgs e) + private async ValueTask BtnLaunch_LeftClickAsync() { SavedGame sg = savedGames[lbSaveGameList.SelectedIndex]; Logger.Log("Loading saved game " + sg.FileName); @@ -137,7 +139,7 @@ private void BtnLaunch_LeftClick(object sender, EventArgs e) Enabled = false; GameProcessLogic.GameProcessExited += GameProcessExited_Callback; - GameProcessLogic.StartGameProcess(WindowManager); + await GameProcessLogic.StartGameProcessAsync(WindowManager); } private void BtnDelete_LeftClick(object sender, EventArgs e) @@ -217,4 +219,4 @@ private void ParseSaveGame(string fileName) savedGames.Add(sg); } } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 08f5b9ad4..14b100c2b 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -229,7 +229,7 @@ public override void Initialize() btnExit.IdleTexture = AssetLoader.LoadTexture("MainMenu/exitgame.png"); btnExit.HoverTexture = AssetLoader.LoadTexture("MainMenu/exitgame_c.png"); btnExit.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); - btnExit.LeftClick += BtnExit_LeftClick; + btnExit.LeftClick += (_, _) => BtnExit_LeftClickAsync().HandleTask(); XNALabel lblCnCNetStatus = new XNALabel(WindowManager); lblCnCNetStatus.Name = nameof(lblCnCNetStatus); @@ -316,10 +316,8 @@ public override void Initialize() GameProcessLogic.GameProcessStarted += SharedUILogic_GameProcessStarted; GameProcessLogic.GameProcessStarting += SharedUILogic_GameProcessStarting; - UserINISettings.Instance.SettingsSaved += SettingsSaved; - - Updater.Restart += Updater_Restart; + Updater.Restart += (_, _) => WindowManager.AddCallback(() => ExitClientAsync().HandleTask()); SetButtonHotkeys(true); } @@ -383,9 +381,6 @@ private void SharedUILogic_GameProcessStarting() } } - private void Updater_Restart(object sender, EventArgs e) => - WindowManager.AddCallback(ExitClient); - /// /// Applies configuration changes (music playback and volume) /// when settings are saved. @@ -868,12 +863,12 @@ private void BtnCredits_LeftClick(object sender, EventArgs e) private void BtnExtras_LeftClick(object sender, EventArgs e) => innerPanel.Show(innerPanel.ExtrasWindow); - private void BtnExit_LeftClick(object sender, EventArgs e) + private ValueTask BtnExit_LeftClickAsync() { #if WINFORMS WindowManager.HideWindow(); #endif - FadeMusicExit(); + return FadeMusicExitAsync(); } private void SharedUILogic_GameProcessExited() => @@ -969,11 +964,11 @@ private void FadeMusic(GameTime gameTime) /// /// Exits the client. Quickly fades the music if it's playing. /// - private void FadeMusicExit() + private async ValueTask FadeMusicExitAsync() { if (!isMediaPlayerAvailable || themeSong == null) { - ExitClient(); + await ExitClientAsync(); return; } @@ -982,22 +977,22 @@ private void FadeMusicExit() if (MediaPlayer.Volume > step) { MediaPlayer.Volume -= step; - AddCallback(FadeMusicExit); + AddCallback(() => FadeMusicExitAsync().HandleTask()); } else { MediaPlayer.Stop(); - ExitClient(); + await ExitClientAsync(); } } - private void ExitClient() + private async ValueTask ExitClientAsync() { Logger.Log("Exiting."); WindowManager.CloseGame(); themeSong?.Dispose(); #if !XNA - Thread.Sleep(1000); + await Task.Delay(1000); Environment.Exit(0); #endif } diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index 75a69e89d..74644be91 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -53,7 +53,7 @@ public CnCNetGameLoadingLobby( new IntCommandHandler(CnCNetCommands.TUNNEL_PING, HandleTunnelPing), new StringCommandHandler(CnCNetCommands.OPTIONS, (sender, data) => HandleOptionsMessageAsync(sender, data).HandleTask()), new NoParamCommandHandler(CnCNetCommands.INVALID_SAVED_GAME_INDEX, HandleInvalidSaveIndexCommand), - new StringCommandHandler(CnCNetCommands.START_GAME, HandleStartGameCommand), + new StringCommandHandler(CnCNetCommands.START_GAME, (sender, data) => HandleStartGameCommandAsync(sender, data).HandleTask()), new IntCommandHandler(CnCNetCommands.PLAYER_READY, (sender, readyStatus) => HandlePlayerReadyRequestAsync(sender, readyStatus).HandleTask()), new StringCommandHandler(CnCNetCommands.CHANGE_TUNNEL_SERVER, HandleTunnelServerChangeMessage) }; @@ -489,7 +489,7 @@ private void HandleInvalidSaveIndexCommand(string sender) CopyPlayerDataToUI(); } - private void HandleStartGameCommand(string sender, string data) + private async ValueTask HandleStartGameCommandAsync(string sender, string data) { if (sender != hostName) return; @@ -523,7 +523,7 @@ private void HandleStartGameCommand(string sender, string data) pInfo.Port = port; } - LoadGame(); + await LoadGameAsync(); } private async ValueTask HandlePlayerReadyRequestAsync(string sender, int readyStatus) @@ -606,7 +606,7 @@ protected override async ValueTask HostStartGameAsync() started = true; - LoadGame(); + await LoadGameAsync(); } protected override void WriteSpawnIniAdditions(IniFile spawnIni) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 38f2dfc85..3cf96742e 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -1125,8 +1125,8 @@ private void ConnectionManager_Disconnected(object sender, EventArgs e) ddCurrentChannel.SelectedIndex = gameIndex; } - if (gameCheckCancellation != null) - gameCheckCancellation.Cancel(); + gameCheckCancellation?.Cancel(); + gameCheckCancellation?.Dispose(); } private async ValueTask ConnectionManager_WelcomeMessageReceivedAsync() @@ -1161,8 +1161,7 @@ private async ValueTask ConnectionManager_WelcomeMessageReceivedAsync() } gameCheckCancellation = new CancellationTokenSource(); - CnCNetGameCheck gameCheck = new CnCNetGameCheck(); - gameCheck.InitializeService(gameCheckCancellation); + CnCNetGameCheck.RunServiceAsync(gameCheckCancellation.Token).HandleTask(); } private void ConnectionManager_PrivateCTCPReceived(object sender, PrivateCTCPEventArgs e) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index c0cc69bfa..baab0cad6 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -286,7 +286,7 @@ protected virtual ValueTask NotAllPresentNotificationAsync() protected abstract ValueTask HostStartGameAsync(); - protected void LoadGame() + protected async ValueTask LoadGameAsync() { FileInfo spawnFileInfo = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS); @@ -340,7 +340,7 @@ protected void LoadGame() gameLoadTime = DateTime.Now; GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited; - GameProcessLogic.StartGameProcess(WindowManager); + await GameProcessLogic.StartGameProcessAsync(WindowManager); fsw.EnableRaisingEvents = true; UpdateDiscordPresence(true); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 94f1ae013..9e9487fce 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1779,7 +1779,7 @@ private void ManipulateStartingLocations(IniFile mapIni, PlayerHouseInfo[] house /// Writes spawn.ini, writes the map file, initializes statistics and /// starts the game process. /// - protected virtual ValueTask StartGameAsync() + protected virtual async ValueTask StartGameAsync() { PlayerHouseInfo[] houseInfos = WriteSpawnIni(); InitializeMatchStatistics(houseInfos); @@ -1787,10 +1787,8 @@ protected virtual ValueTask StartGameAsync() GameProcessLogic.GameProcessExited += GameProcessExited_Callback; - GameProcessLogic.StartGameProcess(WindowManager); + await GameProcessLogic.StartGameProcessAsync(WindowManager); UpdateDiscordPresence(true); - - return ValueTask.CompletedTask; } private void GameProcessExited_Callback() => AddCallback(() => GameProcessExitedAsync().HandleTask()); diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index 7b2f8714e..3c92c5053 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -50,7 +50,7 @@ public LANGameLoadingLobby( { new ClientStringCommandHandler(LANCommands.CHAT_GAME_LOADING_COMMAND, Client_HandleChatMessage), new ClientStringCommandHandler(LANCommands.OPTIONS, Client_HandleOptionsMessage), - new ClientNoParamCommandHandler(LANCommands.GAME_START, Client_HandleStartCommand) + new ClientNoParamCommandHandler(LANCommands.GAME_START, () => Client_HandleStartCommandAsync().HandleTask()) }; WindowManager.GameClosing += (_, _) => WindowManager_GameClosingAsync().HandleTask(); @@ -556,11 +556,11 @@ private void Client_HandleOptionsMessage(string data) CopyPlayerDataToUI(); } - private void Client_HandleStartCommand() + private ValueTask Client_HandleStartCommandAsync() { started = true; - LoadGame(); + return LoadGameAsync(); } #endregion diff --git a/DXMainClient/Online/CnCNetGameCheck.cs b/DXMainClient/Online/CnCNetGameCheck.cs index 952b69aec..f878b20d0 100644 --- a/DXMainClient/Online/CnCNetGameCheck.cs +++ b/DXMainClient/Online/CnCNetGameCheck.cs @@ -1,48 +1,43 @@ using System; using ClientCore; using System.Diagnostics; +using System.IO; using System.Threading; +using System.Threading.Tasks; namespace DTAClient.Online { - public class CnCNetGameCheck + internal static class CnCNetGameCheck { - private static int REFRESH_INTERVAL = 15000; // 15 seconds + private const int REFRESH_INTERVAL = 15000; // 15 seconds - public void InitializeService(CancellationTokenSource cts) + public static async ValueTask RunServiceAsync(CancellationToken cancellationToken) { - ThreadPool.QueueUserWorkItem(new WaitCallback(RunService), cts); - } - - private void RunService(object tokenObj) - { - var waitHandle = ((CancellationTokenSource)tokenObj).Token.WaitHandle; - - while (true) + while (!cancellationToken.IsCancellationRequested) { - if (waitHandle.WaitOne(REFRESH_INTERVAL)) + try { - // Cancellation signaled - return; + await Task.Delay(REFRESH_INTERVAL, cancellationToken); + + CheatEngineWatchEvent(); } - else + catch (OperationCanceledException) { - CheatEngineWatchEvent(); } } } - private void CheatEngineWatchEvent() + private static void CheatEngineWatchEvent() { Process[] processlist = Process.GetProcesses(); + foreach (Process process in processlist) { try { if (process.ProcessName.Contains("cheatengine") || process.MainWindowTitle.ToLower().Contains("cheat engine") || - process.MainWindowHandle.ToString().ToLower().Contains("cheat engine") - ) + process.MainWindowHandle.ToString().ToLower().Contains("cheat engine")) { KillGameInstance(); } @@ -56,38 +51,25 @@ private void CheatEngineWatchEvent() } } - private void KillGameInstance() + private static void KillGameInstance() { - try - { - string gameExecutableName = ClientConfiguration.Instance.GetOperatingSystemVersion() == OSVersion.UNIX ? - ClientConfiguration.Instance.UnixGameExecutableName : - ClientConfiguration.Instance.GetGameExecutableName(); + string gameExecutableName = ClientConfiguration.Instance.GetOperatingSystemVersion() == OSVersion.UNIX ? + ClientConfiguration.Instance.UnixGameExecutableName : + ClientConfiguration.Instance.GetGameExecutableName(); - gameExecutableName = gameExecutableName.Replace(".exe", ""); - - Process[] processlist = Process.GetProcesses(); - foreach (Process process in processlist) + foreach (Process process in Process.GetProcessesByName(Path.GetFileNameWithoutExtension(gameExecutableName))) + { + try { - try - { - if (process.ProcessName.Contains(gameExecutableName)) - { - process.Kill(); - } - } - catch (Exception ex) - { - ProgramConstants.LogException(ex); - } - - process.Dispose(); + process.Kill(); } - } - catch (Exception ex) - { - ProgramConstants.LogException(ex); + catch (Exception ex) + { + ProgramConstants.LogException(ex); + } + + process.Dispose(); } } } -} +} \ No newline at end of file From e0102003f236c6fca4c74a3ac96cd05449ff7a4c Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 7 Dec 2022 11:31:03 +0100 Subject: [PATCH 054/109] Use Tasks instead of Threads --- ClientCore/SavedGameManager.cs | 11 ++++++----- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 8 +++----- .../Multiplayer/GameLobby/MultiplayerGameLobby.cs | 8 +++----- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/ClientCore/SavedGameManager.cs b/ClientCore/SavedGameManager.cs index 8b6a8951c..855d147eb 100644 --- a/ClientCore/SavedGameManager.cs +++ b/ClientCore/SavedGameManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using Rampastring.Tools; namespace ClientCore @@ -10,7 +11,7 @@ namespace ClientCore /// public static class SavedGameManager { - private static bool saveRenameInProgress = false; + private static bool saveRenameInProgress; public static int GetSaveGameCount() { @@ -88,7 +89,7 @@ public static bool InitSavedGames() return true; } - public static void RenameSavedGame() + public static async ValueTask RenameSavedGameAsync() { Logger.Log("Renaming saved game."); @@ -149,7 +150,7 @@ public static void RenameSavedGame() return; } - System.Threading.Thread.Sleep(250); + await Task.Delay(250); } saveRenameInProgress = false; @@ -157,7 +158,7 @@ public static void RenameSavedGame() Logger.Log("Saved game SAVEGAME.NET succesfully renamed to " + Path.GetFileName(sgPath)); } - public static bool EraseSavedGames() + private static bool EraseSavedGames() { Logger.Log("Erasing previous MP saved games."); @@ -178,4 +179,4 @@ public static bool EraseSavedGames() return true; } } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index baab0cad6..fa4948ba2 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -228,16 +228,14 @@ protected virtual ValueTask LeaveGameAsync() } private void fsw_Created(object sender, FileSystemEventArgs e) => - AddCallback(() => HandleFSWEvent(e)); + AddCallback(() => HandleFSWEventAsync(e).HandleTask()); - private void HandleFSWEvent(FileSystemEventArgs e) + private static async ValueTask HandleFSWEventAsync(FileSystemEventArgs e) { Logger.Log("FSW Event: " + e.FullPath); if (Path.GetFileName(e.FullPath) == "SAVEGAME.NET") - { - SavedGameManager.RenameSavedGame(); - } + await SavedGameManager.RenameSavedGameAsync(); } private async ValueTask BtnLoadGame_LeftClickAsync() diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 0e64fc7ef..775261813 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -238,11 +238,9 @@ protected void PostInitialize() } private void fsw_Created(object sender, FileSystemEventArgs e) - { - AddCallback(() => FSWEvent(e)); - } + => AddCallback(() => FSWEventAsync(e).HandleTask()); - private void FSWEvent(FileSystemEventArgs e) + private async ValueTask FSWEventAsync(FileSystemEventArgs e) { Logger.Log("FSW Event: " + e.FullPath); @@ -258,7 +256,7 @@ private void FSWEvent(FileSystemEventArgs e) gameSaved = true; - SavedGameManager.RenameSavedGame(); + await SavedGameManager.RenameSavedGameAsync(); } } From 3e87d9bff30e7dd7d110dc733c3c8748b2011769 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 7 Dec 2022 22:32:33 +0100 Subject: [PATCH 055/109] Update P2P handling --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 18 ++-- .../Multiplayer/GameLobby/LANGameLobby.cs | 6 +- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 15 +-- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 22 ++-- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 100 +++++++++++------- .../CnCNet/V3LocalPlayerConnection.cs | 15 ++- .../CnCNet/V3RemotePlayerConnection.cs | 20 +++- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 17 +-- .../Domain/Multiplayer/NetworkHelper.cs | 36 +++++++ 9 files changed, 163 insertions(+), 86 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index fd169b6d4..5040e64a2 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net; using System.Net.NetworkInformation; -using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -987,8 +986,13 @@ private void StartV3ConnectionListeners() { foreach (var (remotePlayerName, remotePorts, localPingResults, remotePingResults, _) in p2pPlayers.Where(q => q.RemotePingResults.Any() && q.Enabled)) { - IEnumerable<(IPAddress IpAddress, long CombinedPing)> combinedPingResults = localPingResults.Select(q => (q.RemoteIpAddress, q.Ping + remotePingResults.SingleOrDefault(r => r.RemoteIpAddress.Equals(q.RemoteIpAddress)).Ping)); - (IPAddress ipAddress, long combinedPing) = combinedPingResults.OrderBy(q => q.CombinedPing).ThenByDescending(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).First(); + (IPAddress selectedRemoteIpAddress, long combinedPing) = localPingResults + .Where(q => q.RemoteIpAddress is not null && remotePingResults + .Where(r => r.RemoteIpAddress is not null) + .Select(r => r.RemoteIpAddress.AddressFamily) + .Contains(q.RemoteIpAddress.AddressFamily)) + .Select(q => (q.RemoteIpAddress, q.Ping + remotePingResults.Single(r => r.RemoteIpAddress.AddressFamily == q.RemoteIpAddress.AddressFamily).Ping)) + .MaxBy(q => q.RemoteIpAddress.AddressFamily); if (combinedPing < playerTunnels.Single(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).CombinedPing) { @@ -1003,7 +1007,7 @@ private void StartV3ConnectionListeners() p2pLocalTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); p2pLocalTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - p2pLocalTunnelHandler.SetUp(new(ipAddress, remotePort), localPort, gameLocalPlayerId, gameStartCancellationTokenSource.Token); + p2pLocalTunnelHandler.SetUp(new(selectedRemoteIpAddress, remotePort), localPort, gameLocalPlayerId, gameStartCancellationTokenSource.Token); p2pLocalTunnelHandler.ConnectToTunnel(); v3GameTunnelHandlers.Add(new(new() { remotePlayerName }, p2pLocalTunnelHandler)); p2pPlayerTunnels.Add(remotePlayerName); @@ -1333,11 +1337,11 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() { if (!p2pPorts.Any()) { - IEnumerable p2pReservedPorts = NetworkHelper.GetFreeUdpPorts(Array.Empty(), MAX_REMOTE_PLAYERS); + p2pPorts = NetworkHelper.GetFreeUdpPorts(Array.Empty(), MAX_REMOTE_PLAYERS).ToList(); try { - (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync(internetGatewayDevice, p2pReservedPorts); + (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync(internetGatewayDevice, p2pPorts); } catch (Exception ex) { @@ -1348,7 +1352,7 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() } } - if ((publicIpV4Address is not null || publicIpV6Address is not null) && p2pPorts.Any()) + if (publicIpV4Address is not null || publicIpV6Address is not null) await SendPlayerP2PRequestAsync(); } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index ebcc57ded..c3d357193 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -198,7 +198,7 @@ private async ValueTask ListenForClientsAsync(CancellationToken cancellationToke { listener = new Socket(SocketType.Stream, ProtocolType.Tcp); - listener.Bind(new IPEndPoint(IPAddress.IPv6Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); + listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); listener.Listen(); while (!cancellationToken.IsCancellationRequested) @@ -256,7 +256,7 @@ private async ValueTask HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancel try { message = memoryOwner.Memory[..1024]; - bytesRead = await lpInfo.TcpClient.ReceiveAsync(message, SocketFlags.None, cancellationToken); + bytesRead = await lpInfo.TcpClient.ReceiveAsync(message, cancellationToken); } catch (OperationCanceledException) { @@ -383,7 +383,7 @@ private async ValueTask HandleServerCommunicationAsync(CancellationToken cancell try { message = memoryOwner.Memory[..1024]; - bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); + bytesRead = await client.ReceiveAsync(message, cancellationToken); } catch (OperationCanceledException) { diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index 3c92c5053..71c0b13f3 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -156,7 +156,7 @@ public async ValueTask PostJoinAsync() private async ValueTask ListenForClientsAsync(CancellationToken cancellationToken) { listener = new Socket(SocketType.Stream, ProtocolType.Tcp); - listener.Bind(new IPEndPoint(IPAddress.IPv6Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); + listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); listener.Listen(); while (!cancellationToken.IsCancellationRequested) @@ -188,7 +188,7 @@ private async ValueTask ListenForClientsAsync(CancellationToken cancellationToke private async ValueTask HandleClientConnectionAsync(LANPlayerInfo lpInfo, CancellationToken cancellationToken) { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); while (!cancellationToken.IsCancellationRequested) { @@ -197,7 +197,7 @@ private async ValueTask HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancel try { - message = memoryOwner.Memory[..1024]; + message = memoryOwner.Memory[..4096]; bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); } catch (OperationCanceledException) @@ -308,7 +308,10 @@ private void HandleClientMessage(string data, LANPlayerInfo lpInfo) private void CleanUpPlayer(LANPlayerInfo lpInfo) { lpInfo.MessageReceived -= LpInfo_MessageReceived; - lpInfo.TcpClient.Shutdown(SocketShutdown.Both); + + if (lpInfo.TcpClient.Connected) + lpInfo.TcpClient.Shutdown(SocketShutdown.Both); + lpInfo.TcpClient.Close(); } @@ -319,7 +322,7 @@ private async ValueTask HandleServerCommunicationAsync(CancellationToken cancell if (!client.Connected) return; - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); while (!cancellationToken.IsCancellationRequested) { @@ -328,7 +331,7 @@ private async ValueTask HandleServerCommunicationAsync(CancellationToken cancell try { - message = memoryOwner.Memory[..1024]; + message = memoryOwner.Memory[..4096]; bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); } catch (OperationCanceledException) diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index ab450fa4c..8969597ae 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -334,19 +334,19 @@ public async ValueTask OpenAsync() private async ValueTask SendMessageAsync(string message, CancellationToken cancellationToken) { - try - { - if (!initSuccess) - return; + if (!initSuccess) + return; - const int charSize = sizeof(char); - int bufferSize = message.Length * charSize; - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); - Memory buffer = memoryOwner.Memory[..bufferSize]; - int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + const int charSize = sizeof(char); + int bufferSize = message.Length * charSize; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); + Memory buffer = memoryOwner.Memory[..bufferSize]; + int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); - buffer = buffer[..bytes]; + buffer = buffer[..bytes]; + try + { await socket.SendToAsync(buffer, SocketFlags.None, endPoint, cancellationToken); } catch (OperationCanceledException) @@ -505,7 +505,7 @@ private async ValueTask JoinGameAsync() HostedLANGame hg = (HostedLANGame)lbGameList.Items[lbGameList.SelectedIndex].Tag; - if (hg.Game.InternalName.ToUpper() != localGame.ToUpper()) + if (!hg.Game.InternalName.Equals(localGame, StringComparison.OrdinalIgnoreCase)) { lbChatMessages.AddMessage( string.Format("The selected game is for {0}!".L10N("Client:Main:GameIsOfPurpose"), gameCollection.GetGameNameFromInternalName(hg.Game.InternalName))); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index eaab5ae17..35bfa8b60 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -14,6 +14,7 @@ using System.Text; using System.Xml; using ClientCore; +using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet; @@ -37,9 +38,11 @@ internal static class UPnPHandler var p2pPorts = new List(); var p2pIpV6PortIds = new List(); IPAddress routerPublicIpV4Address = null; - bool? routerNatEnabled = null; + bool routerNatEnabled = false; bool natDetected = false; + Logger.Log("Starting P2P Setup."); + if (internetGatewayDevice is null) { var internetGatewayDevices = (await GetInternetGatewayDevicesAsync(cancellationToken)).ToList(); @@ -50,26 +53,38 @@ internal static class UPnPHandler if (internetGatewayDevice is not null) { + Logger.Log("Found NAT device."); + routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(cancellationToken); routerPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken); } + if (routerPublicIpV4Address == null) + { + Logger.Log("Using IPV4 detection."); + + routerPublicIpV4Address = await NetworkHelper.DetectPublicIpV4Address(cancellationToken); + } + var publicIpAddresses = NetworkHelper.GetPublicIpAddresses().ToList(); IPAddress publicIpV4Address = publicIpAddresses.FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetwork); - if ((routerNatEnabled ?? false) || (publicIpV4Address is not null && routerPublicIpV4Address is not null && !publicIpV4Address.Equals(routerPublicIpV4Address))) + if (routerNatEnabled || (publicIpV4Address is not null && routerPublicIpV4Address is not null && !publicIpV4Address.Equals(routerPublicIpV4Address))) natDetected = true; publicIpV4Address ??= routerPublicIpV4Address; + if (publicIpV4Address is not null) + Logger.Log("Public IPV4 detected."); + var privateIpV4Addresses = NetworkHelper.GetPrivateIpAddresses().Where(q => q.AddressFamily is AddressFamily.InterNetwork).ToList(); - IPAddress privateIpV4Address = null; + IPAddress privateIpV4Address = privateIpV4Addresses.FirstOrDefault(); - try + if (natDetected && privateIpV4Address is not null && publicIpV4Address is not null) { - privateIpV4Address = privateIpV4Addresses.FirstOrDefault(); + Logger.Log("Using IPV4 port mapping."); - if (natDetected && privateIpV4Address is not null) + try { foreach (int p2PReservedPort in p2pReservedPorts) { @@ -78,54 +93,61 @@ internal static class UPnPHandler p2pReservedPorts = p2pPorts; } - } - catch (Exception ex) - { - ProgramConstants.LogException(ex, $"Could not open P2P IPV4 ports for {privateIpV4Address} -> {publicIpV4Address}."); + catch (Exception ex) + { + ProgramConstants.LogException(ex, $"Could not open P2P IPV4 ports for {privateIpV4Address} -> {publicIpV4Address}."); + } } - IPAddress publicIpV6Address = null; + IPAddress publicIpV6Address; - try + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var publicIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses().Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); + var publicIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses().Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); - (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundPublicIpV6Address = publicIpV6Addresses - .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); + (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundPublicIpV6Address = publicIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); - if (foundPublicIpV6Address.IpAddress is null) - { - foundPublicIpV6Address = publicIpV6Addresses - .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); - } - - publicIpV6Address = foundPublicIpV6Address.IpAddress; - } - else + if (foundPublicIpV6Address.IpAddress is null) { - publicIpV6Address = NetworkHelper.GetPublicIpAddresses() - .FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); + foundPublicIpV6Address = publicIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); } - if (publicIpV6Address is not null && internetGatewayDevice is not null) - { - (bool firewallEnabled, bool inboundPinholeAllowed) = await internetGatewayDevice.GetIpV6FirewallStatusAsync(cancellationToken); + publicIpV6Address = foundPublicIpV6Address.IpAddress; + } + else + { + publicIpV6Address = NetworkHelper.GetPublicIpAddresses() + .FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); + } + + if (publicIpV6Address is not null) + { + Logger.Log("Public IPV6 detected."); - if (firewallEnabled && inboundPinholeAllowed) + if (internetGatewayDevice is not null) + { + try { - foreach (int p2pReservedPort in p2pReservedPorts) + (bool firewallEnabled, bool inboundPinholeAllowed) = await internetGatewayDevice.GetIpV6FirewallStatusAsync(cancellationToken); + + if (firewallEnabled && inboundPinholeAllowed) { - p2pIpV6PortIds.Add(await internetGatewayDevice.OpenIpV6PortAsync(publicIpV6Address, (ushort)p2pReservedPort, cancellationToken)); + Logger.Log("Configuring IPV6 firewall."); + + foreach (ushort p2pReservedPort in p2pReservedPorts) + { + p2pIpV6PortIds.Add(await internetGatewayDevice.OpenIpV6PortAsync(publicIpV6Address, p2pReservedPort, cancellationToken)); + } } } + catch (Exception ex) + { + ProgramConstants.LogException(ex, $"Could not open P2P IPV6 ports for {publicIpV6Address}."); + } } } - catch (Exception ex) - { - ProgramConstants.LogException(ex, $"Could not open P2P IPV6 ports for {publicIpV6Address}."); - } return (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address); } @@ -291,7 +313,7 @@ private static async Task GetInternetGatewayDeviceAsync(I { try { - location = locations.SingleOrDefault(q => q.HostNameType is UriHostNameType.IPv4); + location = locations.First(q => q.HostNameType is UriHostNameType.IPv4); uPnPDescription = await GetUPnPDescription(location, cancellationToken); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs index 5a30582ec..f7d2c182f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs @@ -20,6 +20,8 @@ internal sealed class V3LocalPlayerConnection : IDisposable private const int SendTimeout = 10000; private const int GameStartReceiveTimeout = 60000; private const int ReceiveTimeout = 10000; + private const int MinimumPacketSize = 8; + private const int MaximumPacketSize = 1024; private Socket localGameSocket; private EndPoint remotePlayerEndPoint; @@ -63,8 +65,8 @@ public async ValueTask StartConnectionAsync() { remotePlayerEndPoint = new IPEndPoint(IPAddress.Loopback, 0); - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(128); - Memory buffer = memoryOwner.Memory[..128]; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(MaximumPacketSize); + Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; int receiveTimeout = GameStartReceiveTimeout; #if DEBUG @@ -101,6 +103,10 @@ public async ValueTask StartConnectionAsync() return; } + catch (ObjectDisposedException) + { + return; + } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { return; @@ -133,7 +139,7 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data) Logger.Log($"Sending data from {localGameSocket.LocalEndPoint} to local game {remotePlayerEndPoint} for player {playerId}."); #endif - if (remotePlayerEndPoint is null) + if (remotePlayerEndPoint is null || data.Length < MinimumPacketSize) return; using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); @@ -152,6 +158,9 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data) #endif OnRaiseConnectionCutEvent(EventArgs.Empty); } + catch (ObjectDisposedException) + { + } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index e8665833f..5290e82d8 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -17,6 +17,8 @@ internal sealed class V3RemotePlayerConnection : IDisposable private const int SendTimeout = 10000; private const int GameStartReceiveTimeout = 60000; private const int ReceiveTimeout = 60000; + private const int MinimumPacketSize = 8; + private const int MaximumPacketSize = 1024; private uint gameLocalPlayerId; private CancellationToken cancellationToken; @@ -67,8 +69,8 @@ public async ValueTask StartConnectionAsync() tunnelSocket.Bind(new IPEndPoint(IPAddress.IPv6Any, localPort)); - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(50); - Memory buffer = memoryOwner.Memory[..50]; + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(MaximumPacketSize); + Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; if (!BitConverter.TryWriteBytes(buffer.Span[..4], gameLocalPlayerId)) throw new GameDataException(); @@ -156,6 +158,9 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data, uint receiverId) #endif OnRaiseConnectionCutEvent(EventArgs.Empty); } + catch (ObjectDisposedException) + { + } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } @@ -182,7 +187,7 @@ public void Dispose() private async ValueTask ReceiveLoopAsync() { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(MaximumPacketSize); int receiveTimeout = GameStartReceiveTimeout; #if DEBUG @@ -193,7 +198,7 @@ private async ValueTask ReceiveLoopAsync() while (!cancellationToken.IsCancellationRequested) { - Memory buffer = memoryOwner.Memory[..1024]; + Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; SocketReceiveFromResult socketReceiveFromResult; using var timeoutCancellationTokenSource = new CancellationTokenSource(receiveTimeout); using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); @@ -213,6 +218,10 @@ private async ValueTask ReceiveLoopAsync() return; } + catch (ObjectDisposedException) + { + return; + } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { return; @@ -231,7 +240,7 @@ private async ValueTask ReceiveLoopAsync() receiveTimeout = ReceiveTimeout; - if (socketReceiveFromResult.ReceivedBytes < 8) + if (socketReceiveFromResult.ReceivedBytes < MinimumPacketSize) { #if DEBUG Logger.Log($"Invalid data packet from {socketReceiveFromResult.RemoteEndPoint}"); @@ -256,6 +265,7 @@ private async ValueTask ReceiveLoopAsync() #else Logger.Log($"Invalid target (received: {receiverId}, expected: {gameLocalPlayerId}) on port {localPort}."); #endif + continue; } diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index 046d8e890..b5b793a57 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -97,7 +97,7 @@ public async ValueTask SendMessageAsync(string message, CancellationToken cancel try { - await TcpClient.SendAsync(buffer, SocketFlags.None, cancellationToken); + await TcpClient.SendAsync(buffer, cancellationToken); } catch (OperationCanceledException) { @@ -118,16 +118,16 @@ public override string ToString() /// public async ValueTask StartReceiveLoopAsync(CancellationToken cancellationToken) { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); while (!cancellationToken.IsCancellationRequested) { int bytesRead; - Memory message = memoryOwner.Memory[..1024]; + Memory message = memoryOwner.Memory[..4096]; try { - bytesRead = await TcpClient.ReceiveAsync(message, SocketFlags.None, cancellationToken); + bytesRead = await TcpClient.ReceiveAsync(message, cancellationToken); } catch (OperationCanceledException) { @@ -147,8 +147,6 @@ public async ValueTask StartReceiveLoopAsync(CancellationToken cancellationToken msg = overMessage + msg; - var commands = new List(); - while (true) { int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR); @@ -159,15 +157,10 @@ public async ValueTask StartReceiveLoopAsync(CancellationToken cancellationToken break; } - commands.Add(msg[..index]); + MessageReceived?.Invoke(this, new NetworkMessageEventArgs(msg[..index])); msg = msg[(index + 1)..]; } - foreach (string cmd in commands) - { - MessageReceived?.Invoke(this, new NetworkMessageEventArgs(cmd)); - } - continue; } diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 8a9462a79..0d6585a1c 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -5,11 +5,16 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; namespace DTAClient.Domain.Multiplayer; internal static class NetworkHelper { + private const string PingHost = "cncnet.org"; + private const int PingTimeout = 10000; + private static readonly IReadOnlyCollection SupportedAddressFamilies = new[] { AddressFamily.InterNetwork, @@ -51,6 +56,37 @@ public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unic return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); } + public static async Task DetectPublicIpV4Address(CancellationToken cancellationToken) + { + IPAddress[] ipAddresses = await Dns.GetHostAddressesAsync(PingHost, cancellationToken).ConfigureAwait(false); + using var ping = new Ping(); + + foreach (IPAddress ipAddress in ipAddresses.Where(q => q.AddressFamily is AddressFamily.InterNetwork)) + { + PingReply pingReply = await ping.SendPingAsync(ipAddress, PingTimeout).ConfigureAwait(false); + + if (pingReply.Status is not IPStatus.Success) + continue; + + IPAddress pingIpAddress = null; + int ttl = 1; + + while (!ipAddress.Equals(pingIpAddress)) + { + pingReply = await ping.SendPingAsync(ipAddress, PingTimeout, Array.Empty(), new(ttl++, false)).ConfigureAwait(false); + pingIpAddress = pingReply.Address; + + if (ipAddress.Equals(pingIpAddress)) + break; + + if (!IsPrivateIpAddress(pingReply.Address)) + return pingReply.Address; + } + } + + return null; + } + /// /// Returns a free UDP port number above 1023. /// From 352a1d97bf182d4ac89cc8d95b78df9f65d8f9a2 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 11 Dec 2022 00:09:10 +0100 Subject: [PATCH 056/109] Handle remote player disconnects --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 2 +- ...dEventArgs.cs => DataReceivedEventArgs.cs} | 4 +- .../Multiplayer/CnCNet/V3GameTunnelHandler.cs | 58 +++++++++++-------- .../CnCNet/V3LocalPlayerConnection.cs | 16 ++--- .../CnCNet/V3RemotePlayerConnection.cs | 20 +++---- 5 files changed, 56 insertions(+), 44 deletions(-) rename DXMainClient/Domain/Multiplayer/CnCNet/{GameDataReceivedEventArgs.cs => DataReceivedEventArgs.cs} (61%) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 5040e64a2..d37327fea 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -1783,7 +1783,7 @@ protected override async ValueTask GameProcessExitedAsync() { await base.GameProcessExitedAsync(); await channel.SendCTCPMessageAsync(CnCNetCommands.RETURN, QueuedMessageType.SYSTEM_MESSAGE, 20); - gameStartCancellationTokenSource.Cancel(); + gameStartCancellationTokenSource?.Cancel(); v3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); v3GameTunnelHandlers.Clear(); ReturnNotification(ProgramConstants.PLAYERNAME); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/GameDataReceivedEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs similarity index 61% rename from DXMainClient/Domain/Multiplayer/CnCNet/GameDataReceivedEventArgs.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs index 539d966ac..1e3c22662 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/GameDataReceivedEventArgs.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs @@ -2,9 +2,9 @@ namespace DTAClient.Domain.Multiplayer.CnCNet; -internal sealed class GameDataReceivedEventArgs : EventArgs +internal sealed class DataReceivedEventArgs : EventArgs { - public GameDataReceivedEventArgs(uint playerId, ReadOnlyMemory gameData) + public DataReceivedEventArgs(uint playerId, ReadOnlyMemory gameData) { PlayerId = playerId; GameData = gameData; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs index e3e055053..b1bbab69e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -12,12 +13,12 @@ namespace DTAClient.Domain.Multiplayer.CnCNet; /// internal sealed class V3GameTunnelHandler : IDisposable { - private readonly Dictionary localGamePlayerConnections = new(); + private readonly Dictionary localGameConnections = new(); private readonly CancellationTokenSource connectionErrorCancellationTokenSource = new(); private V3RemotePlayerConnection remoteHostConnection; - private EventHandler remoteHostGameDataReceivedFunc; - private EventHandler localGameGameDataReceivedFunc; + private EventHandler remoteHostConnectionDataReceivedFunc; + private EventHandler localGameConnectionDataReceivedFunc; /// /// Occurs when the connection to the remote host succeeded. @@ -36,13 +37,13 @@ public void SetUp(IPEndPoint remoteIpEndPoint, ushort localPort, uint gameLocalP using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(connectionErrorCancellationTokenSource.Token, cancellationToken); remoteHostConnection = new V3RemotePlayerConnection(); - remoteHostGameDataReceivedFunc = (_, e) => RemoteHostConnection_MessageReceivedAsync(e).HandleTask(); - localGameGameDataReceivedFunc = (_, e) => PlayerConnection_PacketReceivedAsync(e).HandleTask(); + remoteHostConnectionDataReceivedFunc = (_, e) => RemoteHostConnection_DataReceivedAsync(e).HandleTask(); + localGameConnectionDataReceivedFunc = (_, e) => LocalGameConnection_DataReceivedAsync(e).HandleTask(); remoteHostConnection.RaiseConnectedEvent += RemoteHostConnection_Connected; remoteHostConnection.RaiseConnectionFailedEvent += RemoteHostConnection_ConnectionFailed; - remoteHostConnection.RaiseConnectionCutEvent += Connection_ConnectionCut; - remoteHostConnection.RaiseGameDataReceivedEvent += remoteHostGameDataReceivedFunc; + remoteHostConnection.RaiseConnectionCutEvent += RemoteHostConnection_ConnectionCut; + remoteHostConnection.RaiseDataReceivedEvent += remoteHostConnectionDataReceivedFunc; remoteHostConnection.SetUp(remoteIpEndPoint, localPort, gameLocalPlayerId, cancellationToken); } @@ -51,20 +52,20 @@ public IEnumerable CreatePlayerConnections(List playerIds) { foreach (uint playerId in playerIds) { - var localGamePlayerConnection = new V3LocalPlayerConnection(); + var localPlayerConnection = new V3LocalPlayerConnection(); - localGamePlayerConnection.RaiseConnectionCutEvent += Connection_ConnectionCut; - localGamePlayerConnection.RaiseGameDataReceivedEvent += localGameGameDataReceivedFunc; + localPlayerConnection.RaiseConnectionCutEvent += LocalGameConnection_ConnectionCut; + localPlayerConnection.RaiseDataReceivedEvent += localGameConnectionDataReceivedFunc; - localGamePlayerConnections.Add(playerId, localGamePlayerConnection); + localGameConnections.Add(playerId, localPlayerConnection); - yield return localGamePlayerConnection.Setup(playerId, connectionErrorCancellationTokenSource.Token); + yield return localPlayerConnection.Setup(playerId, connectionErrorCancellationTokenSource.Token); } } public void StartPlayerConnections() { - foreach (KeyValuePair playerConnection in localGamePlayerConnections) + foreach (KeyValuePair playerConnection in localGameConnections) playerConnection.Value.StartConnectionAsync().HandleTask(); } @@ -83,41 +84,52 @@ public void Dispose() connectionErrorCancellationTokenSource.Dispose(); - foreach (KeyValuePair localGamePlayerConnection in localGamePlayerConnections) + foreach (KeyValuePair localGamePlayerConnection in localGameConnections) { - localGamePlayerConnection.Value.RaiseConnectionCutEvent -= Connection_ConnectionCut; - localGamePlayerConnection.Value.RaiseGameDataReceivedEvent -= localGameGameDataReceivedFunc; + localGamePlayerConnection.Value.RaiseConnectionCutEvent -= LocalGameConnection_ConnectionCut; + localGamePlayerConnection.Value.RaiseDataReceivedEvent -= localGameConnectionDataReceivedFunc; localGamePlayerConnection.Value.Dispose(); } - localGamePlayerConnections.Clear(); + localGameConnections.Clear(); if (remoteHostConnection == null) return; remoteHostConnection.RaiseConnectedEvent -= RemoteHostConnection_Connected; remoteHostConnection.RaiseConnectionFailedEvent -= RemoteHostConnection_ConnectionFailed; - remoteHostConnection.RaiseConnectionCutEvent -= Connection_ConnectionCut; - remoteHostConnection.RaiseGameDataReceivedEvent -= remoteHostGameDataReceivedFunc; + remoteHostConnection.RaiseConnectionCutEvent -= RemoteHostConnection_ConnectionCut; + remoteHostConnection.RaiseDataReceivedEvent -= remoteHostConnectionDataReceivedFunc; remoteHostConnection.Dispose(); } + private void LocalGameConnection_ConnectionCut(object sender, EventArgs e) + { + var localGamePlayerConnection = sender as V3LocalPlayerConnection; + + localGameConnections.Remove(localGameConnections.Single(q => q.Value == localGamePlayerConnection).Key); + + localGamePlayerConnection.RaiseConnectionCutEvent -= LocalGameConnection_ConnectionCut; + localGamePlayerConnection.RaiseDataReceivedEvent -= localGameConnectionDataReceivedFunc; + localGamePlayerConnection.Dispose(); + } + /// /// Forwards local game data to the remote host. /// - private ValueTask PlayerConnection_PacketReceivedAsync(GameDataReceivedEventArgs e) + private ValueTask LocalGameConnection_DataReceivedAsync(DataReceivedEventArgs e) => remoteHostConnection?.SendDataAsync(e.GameData, e.PlayerId) ?? ValueTask.CompletedTask; /// /// Forwards remote player data to the local game. /// - private ValueTask RemoteHostConnection_MessageReceivedAsync(GameDataReceivedEventArgs e) + private ValueTask RemoteHostConnection_DataReceivedAsync(DataReceivedEventArgs e) => GetLocalPlayerConnection(e.PlayerId)?.SendDataAsync(e.GameData) ?? ValueTask.CompletedTask; private V3LocalPlayerConnection GetLocalPlayerConnection(uint senderId) - => localGamePlayerConnections.TryGetValue(senderId, out V3LocalPlayerConnection connection) ? connection : null; + => localGameConnections.TryGetValue(senderId, out V3LocalPlayerConnection connection) ? connection : null; private void RemoteHostConnection_Connected(object sender, EventArgs e) { @@ -143,6 +155,6 @@ private void OnRaiseRemoteHostConnectionFailedEvent(EventArgs e) raiseEvent?.Invoke(this, e); } - private void Connection_ConnectionCut(object sender, EventArgs e) + private void RemoteHostConnection_ConnectionCut(object sender, EventArgs e) => Dispose(); } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs index f7d2c182f..be930c646 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs @@ -56,7 +56,7 @@ public ushort Setup(uint playerId, CancellationToken cancellationToken) /// /// Occurs when game data from the local game was received. /// - public event EventHandler RaiseGameDataReceivedEvent; + public event EventHandler RaiseDataReceivedEvent; /// /// Starts listening for local game player data and forwards it to the tunnel. @@ -125,7 +125,7 @@ public async ValueTask StartConnectionAsync() receiveTimeout = ReceiveTimeout; - OnRaiseGameDataReceivedEvent(new(playerId, data)); + OnRaiseDataReceivedEvent(new(playerId, data)); } } @@ -135,10 +135,6 @@ public async ValueTask StartConnectionAsync() /// The data to send to the game. public async ValueTask SendDataAsync(ReadOnlyMemory data) { -#if DEBUG - Logger.Log($"Sending data from {localGameSocket.LocalEndPoint} to local game {remotePlayerEndPoint} for player {playerId}."); - -#endif if (remotePlayerEndPoint is null || data.Length < MinimumPacketSize) return; @@ -147,6 +143,10 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data) try { +#if DEBUG + Logger.Log($"Sending data from {localGameSocket.LocalEndPoint} to local game {remotePlayerEndPoint} for player {playerId}."); + +#endif await localGameSocket.SendToAsync(data, SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token); } catch (SocketException ex) @@ -192,9 +192,9 @@ private void OnRaiseConnectionCutEvent(EventArgs e) raiseEvent?.Invoke(this, e); } - private void OnRaiseGameDataReceivedEvent(GameDataReceivedEventArgs e) + private void OnRaiseDataReceivedEvent(DataReceivedEventArgs e) { - EventHandler raiseEvent = RaiseGameDataReceivedEvent; + EventHandler raiseEvent = RaiseDataReceivedEvent; raiseEvent?.Invoke(this, e); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index 5290e82d8..c8adfa008 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -15,8 +15,8 @@ namespace DTAClient.Domain.Multiplayer.CnCNet; internal sealed class V3RemotePlayerConnection : IDisposable { private const int SendTimeout = 10000; - private const int GameStartReceiveTimeout = 60000; - private const int ReceiveTimeout = 60000; + private const int GameStartReceiveTimeout = 1200000; + private const int ReceiveTimeout = 1200000; private const int MinimumPacketSize = 8; private const int MaximumPacketSize = 1024; @@ -52,7 +52,7 @@ public void SetUp(IPEndPoint remoteEndPoint, ushort localPort, uint gameLocalPla /// /// Occurs when game data from the remote host was received. /// - public event EventHandler RaiseGameDataReceivedEvent; + public event EventHandler RaiseDataReceivedEvent; /// /// Starts listening for remote player data and forwards it to the local game. @@ -125,10 +125,6 @@ public async ValueTask StartConnectionAsync() /// The id of the player that receives the data. public async ValueTask SendDataAsync(ReadOnlyMemory data, uint receiverId) { -#if DEBUG - Logger.Log($"Sending data {gameLocalPlayerId} -> {receiverId} from {tunnelSocket.LocalEndPoint} to {remoteEndPoint}."); - -#endif const int idsSize = sizeof(uint) * 2; int bufferSize = data.Length + idsSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); @@ -147,6 +143,10 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data, uint receiverId) try { +#if DEBUG + Logger.Log($"Sending data {gameLocalPlayerId} -> {receiverId} from {tunnelSocket.LocalEndPoint} to {remoteEndPoint}."); + +#endif await tunnelSocket.SendToAsync(packet, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token); } catch (SocketException ex) @@ -269,7 +269,7 @@ private async ValueTask ReceiveLoopAsync() continue; } - OnRaiseGameDataReceivedEvent(new(senderId, data)); + OnRaiseDataReceivedEvent(new(senderId, data)); } } @@ -294,9 +294,9 @@ private void OnRaiseConnectionCutEvent(EventArgs e) raiseEvent?.Invoke(this, e); } - private void OnRaiseGameDataReceivedEvent(GameDataReceivedEventArgs e) + private void OnRaiseDataReceivedEvent(DataReceivedEventArgs e) { - EventHandler raiseEvent = RaiseGameDataReceivedEvent; + EventHandler raiseEvent = RaiseDataReceivedEvent; raiseEvent?.Invoke(this, e); } From 60e34364856baf56c18e9d6b2c83cd14ee54cf53 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 11 Dec 2022 00:51:10 +0100 Subject: [PATCH 057/109] Handle P2P direct public IP to public IP connections --- .../Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 35bfa8b60..ad5aa7628 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -33,7 +33,7 @@ internal static class UPnPHandler }.AsReadOnly(); public static async ValueTask<(InternetGatewayDevice InternetGatewayDevice, List P2pPorts, List P2pIpV6PortIds, IPAddress ipV6Address, IPAddress ipV4Address)> SetupPortsAsync( - InternetGatewayDevice internetGatewayDevice, IEnumerable p2pReservedPorts, CancellationToken cancellationToken = default) + InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, CancellationToken cancellationToken = default) { var p2pPorts = new List(); var p2pIpV6PortIds = new List(); @@ -95,9 +95,13 @@ internal static class UPnPHandler } catch (Exception ex) { - ProgramConstants.LogException(ex, $"Could not open P2P IPV4 ports for {privateIpV4Address} -> {publicIpV4Address}."); + ProgramConstants.LogException(ex, $"Could not open P2P IPV4 router ports for {privateIpV4Address} -> {publicIpV4Address}."); } } + else + { + p2pPorts = p2pReservedPorts; + } IPAddress publicIpV6Address; @@ -144,7 +148,7 @@ internal static class UPnPHandler } catch (Exception ex) { - ProgramConstants.LogException(ex, $"Could not open P2P IPV6 ports for {publicIpV6Address}."); + ProgramConstants.LogException(ex, $"Could not open P2P IPV6 router ports for {publicIpV6Address}."); } } } From 670629d34049c5cd31b4ac3c97bbfaca045d2691 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 11 Dec 2022 02:15:07 +0100 Subject: [PATCH 058/109] P2P ping cleanup --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 37 ++++++++++--------- .../Domain/Multiplayer/NetworkHelper.cs | 30 ++++++++++++++- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index d37327fea..c00506fa6 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Net; -using System.Net.NetworkInformation; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -677,12 +676,21 @@ private async ValueTask Channel_UserKickedAsync(UserNameEventArgs e) CopyPlayerDataToUI(); UpdateDiscordPresence(); ClearReadyStatuses(); + RemoveV3Player(e.UserName); + } + } - (string Name, CnCNetTunnel Tunnel, int CombinedPing) playerTunnel = playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(e.UserName, StringComparison.OrdinalIgnoreCase)); + private void RemoveV3Player(string playerName) + { + (string Name, CnCNetTunnel Tunnel, int CombinedPing) playerTunnel = playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - if (playerTunnel.Name is not null) - playerTunnels.Remove(playerTunnel); - } + if (playerTunnel.Name is not null) + playerTunnels.Remove(playerTunnel); + + P2PPlayer p2pPlayer = p2pPlayers.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); + + if (p2pPlayer.RemotePlayerName is not null) + p2pPlayers.Remove(p2pPlayer); } private async ValueTask Channel_UserListReceivedAsync() @@ -758,11 +766,7 @@ private async ValueTask RemovePlayerAsync(string playerName) { Players.Remove(pInfo); CopyPlayerDataToUI(); - - (string Name, CnCNetTunnel Tunnel, int CombinedPing) playerTunnel = playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - - if (playerTunnel.Name is not null) - playerTunnels.Remove(playerTunnel); + RemoveV3Player(playerName); // This might not be necessary if (IsHost) @@ -2193,22 +2197,21 @@ private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p List<(IPAddress IpAddress, long Ping)> localPingResults = new(); string[] splitLines = p2pRequestMessage.Split(';'); - using var ping = new Ping(); if (IPAddress.TryParse(splitLines[0], out IPAddress parsedIpV4Address)) { - PingReply pingResult = await ping.SendPingAsync(parsedIpV4Address, P2P_PING_TIMEOUT); + long? pingResult = await NetworkHelper.PingAsync(parsedIpV4Address); - if (pingResult.Status is IPStatus.Success) - localPingResults.Add((parsedIpV4Address, pingResult.RoundtripTime)); + if (pingResult is not null) + localPingResults.Add((parsedIpV4Address, pingResult.Value)); } if (IPAddress.TryParse(splitLines[1], out IPAddress parsedIpV6Address)) { - PingReply pingResult = await ping.SendPingAsync(parsedIpV6Address, P2P_PING_TIMEOUT); + long? pingResult = await NetworkHelper.PingAsync(parsedIpV6Address); - if (pingResult.Status is IPStatus.Success) - localPingResults.Add((parsedIpV6Address, pingResult.RoundtripTime)); + if (pingResult is not null) + localPingResults.Add((parsedIpV6Address, pingResult.Value)); } bool remotePlayerP2PEnabled = false; diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 0d6585a1c..3ba730edb 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -7,13 +7,14 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using ClientCore; namespace DTAClient.Domain.Multiplayer; internal static class NetworkHelper { private const string PingHost = "cncnet.org"; - private const int PingTimeout = 10000; + private const int PingTimeout = 1000; private static readonly IReadOnlyCollection SupportedAddressFamilies = new[] { @@ -56,7 +57,7 @@ public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unic return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); } - public static async Task DetectPublicIpV4Address(CancellationToken cancellationToken) + public static async ValueTask DetectPublicIpV4Address(CancellationToken cancellationToken) { IPAddress[] ipAddresses = await Dns.GetHostAddressesAsync(PingHost, cancellationToken).ConfigureAwait(false); using var ping = new Ping(); @@ -87,6 +88,31 @@ public static async Task DetectPublicIpV4Address(CancellationToken ca return null; } + public static async ValueTask PingAsync(IPAddress ipAddress) + { + if ((ipAddress.AddressFamily is AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) + || (ipAddress.AddressFamily is AddressFamily.InterNetwork && !Socket.OSSupportsIPv4)) + { + return null; + } + + using var ping = new Ping(); + + try + { + PingReply pingResult = await ping.SendPingAsync(ipAddress, PingTimeout); + + if (pingResult.Status is IPStatus.Success) + return pingResult.RoundtripTime; + } + catch (PingException ex) + { + ProgramConstants.LogException(ex); + } + + return null; + } + /// /// Returns a free UDP port number above 1023. /// From 8670570cf2d5ccb62147b7778621f466ba64b67b Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 11 Dec 2022 17:20:51 +0100 Subject: [PATCH 059/109] Add P2P STUN --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 20 +++-- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 7 ++ .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 74 +++++++++++++++---- .../Domain/Multiplayer/NetworkHelper.cs | 63 +++++++++++++++- 4 files changed, 140 insertions(+), 24 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index c00506fa6..f46040ab5 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -428,8 +428,8 @@ private async ValueTask UpdatePingAsync() { int ping; - if (dynamicTunnelsEnabled) - ping = pinnedTunnels?.Min(q => q.Ping) ?? -1; + if (dynamicTunnelsEnabled && (pinnedTunnels?.Any() ?? false)) + ping = pinnedTunnels.Min(q => q.Ping); else if (tunnelHandler.CurrentTunnel == null) return; else @@ -543,8 +543,11 @@ private async ValueTask CloseP2PPortsAsync() { try { - foreach (ushort p2pPort in p2pPorts) - await internetGatewayDevice.CloseIpV4PortAsync(p2pPort); + if (internetGatewayDevice is not null) + { + foreach (ushort p2pPort in p2pPorts) + await internetGatewayDevice.CloseIpV4PortAsync(p2pPort); + } } catch (Exception ex) { @@ -557,8 +560,11 @@ private async ValueTask CloseP2PPortsAsync() try { - foreach (ushort p2pIpV6PortId in p2pIpV6PortIds) - await internetGatewayDevice.CloseIpV6PortAsync(p2pIpV6PortId); + if (internetGatewayDevice is not null) + { + foreach (ushort p2pIpV6PortId in p2pIpV6PortIds) + await internetGatewayDevice.CloseIpV6PortAsync(p2pIpV6PortId); + } } catch (Exception ex) { @@ -1345,7 +1351,7 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() try { - (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync(internetGatewayDevice, p2pPorts); + (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync(internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? initialTunnel.IPAddresses); } catch (Exception ex) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 27994d1ab..b0cc5a990 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -65,6 +65,11 @@ public static CnCNetTunnel Parse(string str) $" {nameof(Socket.OSSupportsIPv4)}={Socket.OSSupportsIPv4}) for {str}."); } + tunnel.IPAddresses = new List { primaryIpAddress }; + + if (secondaryIpAddress is not null) + tunnel.IPAddresses.Add(secondaryIpAddress); + tunnel.Port = int.Parse(addressAndPort[(addressAndPort.LastIndexOf(':') + 1)..], CultureInfo.InvariantCulture); tunnel.Country = parts[1]; tunnel.CountryCode = parts[2]; @@ -108,6 +113,8 @@ private set public IPAddress IPAddress { get; private set; } + public List IPAddresses { get; private set; } + public int Port { get; private set; } public string Country { get; private set; } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index ad5aa7628..f17f967fb 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -33,7 +33,7 @@ internal static class UPnPHandler }.AsReadOnly(); public static async ValueTask<(InternetGatewayDevice InternetGatewayDevice, List P2pPorts, List P2pIpV6PortIds, IPAddress ipV6Address, IPAddress ipV4Address)> SetupPortsAsync( - InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, CancellationToken cancellationToken = default) + InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, List stunServerIpAddresses, CancellationToken cancellationToken = default) { var p2pPorts = new List(); var p2pIpV6PortIds = new List(); @@ -59,11 +59,33 @@ internal static class UPnPHandler routerPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken); } + if (stunServerIpAddresses.Any(q => q.AddressFamily is AddressFamily.InterNetwork)) + { + if (routerPublicIpV4Address == null) + { + Logger.Log("Using IPV4 STUN."); + + foreach (ushort p2pReservedPort in p2pReservedPorts) + { + IPEndPoint publicIpV4Endpoint = await NetworkHelper.PerformStunAsync(stunServerIpAddresses.Single(q => q.AddressFamily is AddressFamily.InterNetwork), p2pReservedPort, cancellationToken); + + if (publicIpV4Endpoint is null) + break; + + routerPublicIpV4Address ??= publicIpV4Endpoint.Address; + } + } + } + else + { + Logger.Log($"STUN server {stunServerIpAddresses.First()} has no IPV4 address."); + } + if (routerPublicIpV4Address == null) { - Logger.Log("Using IPV4 detection."); + Logger.Log("Using IPV4 trace detection."); - routerPublicIpV4Address = await NetworkHelper.DetectPublicIpV4Address(cancellationToken); + routerPublicIpV4Address = await NetworkHelper.TracePublicIpV4Address(cancellationToken); } var publicIpAddresses = NetworkHelper.GetPublicIpAddresses().ToList(); @@ -103,27 +125,47 @@ internal static class UPnPHandler p2pPorts = p2pReservedPorts; } - IPAddress publicIpV6Address; + IPAddress publicIpV6Address = null; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (stunServerIpAddresses.Any(q => q.AddressFamily is AddressFamily.InterNetworkV6)) { - var publicIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses().Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); + foreach (ushort p2pReservedPort in p2pReservedPorts) + { + IPEndPoint publicIpV6Endpoint = await NetworkHelper.PerformStunAsync(stunServerIpAddresses.Single(q => q.AddressFamily is AddressFamily.InterNetworkV6), p2pReservedPort, cancellationToken); - (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundPublicIpV6Address = publicIpV6Addresses - .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); + if (publicIpV6Endpoint is null) + break; - if (foundPublicIpV6Address.IpAddress is null) - { - foundPublicIpV6Address = publicIpV6Addresses - .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); + publicIpV6Address ??= publicIpV6Endpoint.Address; } - - publicIpV6Address = foundPublicIpV6Address.IpAddress; } else { - publicIpV6Address = NetworkHelper.GetPublicIpAddresses() - .FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); + Logger.Log($"STUN server {stunServerIpAddresses.First()} has no IPV6 address."); + } + + if (publicIpV6Address is null) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var publicIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses().Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); + + (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundPublicIpV6Address = publicIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); + + if (foundPublicIpV6Address.IpAddress is null) + { + foundPublicIpV6Address = publicIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); + } + + publicIpV6Address = foundPublicIpV6Address.IpAddress; + } + else + { + publicIpV6Address = NetworkHelper.GetPublicIpAddresses() + .FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); + } } if (publicIpV6Address is not null) diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 3ba730edb..df5092e2c 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Net; @@ -57,7 +58,7 @@ public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unic return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); } - public static async ValueTask DetectPublicIpV4Address(CancellationToken cancellationToken) + public static async ValueTask TracePublicIpV4Address(CancellationToken cancellationToken) { IPAddress[] ipAddresses = await Dns.GetHostAddressesAsync(PingHost, cancellationToken).ConfigureAwait(false); using var ping = new Ping(); @@ -113,6 +114,66 @@ public static async ValueTask DetectPublicIpV4Address(CancellationTok return null; } + public static async ValueTask PerformStunAsync(IPAddress stunServerIpAddress, ushort localPort, CancellationToken cancellationToken) + { + const short stunId = 26262; + const int stunPort1 = 3478; + const int stunPort2 = 8054; + const int stunSize = 48; + int[] stunPorts = { stunPort1, stunPort2 }; + using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); + short stunIdNetworkOrder = IPAddress.HostToNetworkOrder(stunId); + byte[] stunIdNetworkOrderBytes = BitConverter.GetBytes(stunIdNetworkOrder); + IPEndPoint stunServerIpEndPoint = null; + using IMemoryOwner receiveMemoryOwner = MemoryPool.Shared.Rent(stunSize); + Memory buffer = receiveMemoryOwner.Memory[..stunSize]; + int addressBytes = stunServerIpAddress.GetAddressBytes().Length; + const int portBytes = sizeof(ushort); + + stunIdNetworkOrderBytes.CopyTo(buffer.Span); + + socket.Bind(new IPEndPoint(IPAddress.IPv6Any, localPort)); + + foreach (int stunPort in stunPorts) + { + try + { + using var timeoutCancellationTokenSource = new CancellationTokenSource(PingTimeout); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); + + stunServerIpEndPoint = new IPEndPoint(stunServerIpAddress, stunPort); + + await socket.SendToAsync(buffer, stunServerIpEndPoint, linkedCancellationTokenSource.Token); + + SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, stunServerIpEndPoint, linkedCancellationTokenSource.Token); + + buffer = buffer[..socketReceiveFromResult.ReceivedBytes]; + + // de-obfuscate + for (int i = 0; i < addressBytes + portBytes; i++) + buffer.Span[i] ^= 0x20; + + ReadOnlyMemory publicIpAddressBytes = buffer[..addressBytes]; + var publicIpAddress = new IPAddress(publicIpAddressBytes.Span); + ReadOnlyMemory publicPortBytes = buffer[addressBytes..(addressBytes + portBytes)]; + short publicPortNetworkOrder = BitConverter.ToInt16(publicPortBytes.Span); + short publicPortHostOrder = IPAddress.NetworkToHostOrder(publicPortNetworkOrder); + ushort publicPort = (ushort)publicPortHostOrder; + + if (publicPort != localPort) + throw new($"STUN ports mismatch, expected {localPort}, received {publicPort}."); + + return new IPEndPoint(publicIpAddress, publicPort); + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, $"STUN server {stunServerIpEndPoint} failed."); + } + } + + return null; + } + /// /// Returns a free UDP port number above 1023. /// From a87b2021603f6364e9b8dd50c64fb452f6d213be Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 11 Dec 2022 21:23:31 +0100 Subject: [PATCH 060/109] P2P STUN port mapping and keep alive --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 84 ++++++++--- .../CnCNet/CnCNetPlayerCountTask.cs | 1 - .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 139 ++++++++++++------ .../Domain/Multiplayer/NetworkHelper.cs | 23 ++- DXMainClient/Domain/Multiplayer/P2PPlayer.cs | 3 +- 5 files changed, 181 insertions(+), 69 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index f46040ab5..08809fd4a 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -36,7 +37,6 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private const double MAX_TIME_FOR_GAME_LAUNCH = 20.0; private const int PRIORITY_START_GAME = 10; private const int PINNED_DYNAMIC_TUNNELS = 10; - private const int P2P_PING_TIMEOUT = 1000; private const ushort MAX_REMOTE_PLAYERS = 7; private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; @@ -73,9 +73,11 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private CnCNetTunnel initialTunnel; private IPAddress publicIpV4Address; private IPAddress publicIpV6Address; - private List p2pPorts = new(); + private List<(ushort InternalPort, ushort ExternalPort)> ipV6P2PPorts = new(); + private List<(ushort InternalPort, ushort ExternalPort)> ipV4P2PPorts = new(); private List p2pIpV6PortIds = new(); private CancellationTokenSource gameStartCancellationTokenSource; + private CancellationTokenSource stunCancellationTokenSource; private bool disposed; /// @@ -527,6 +529,7 @@ public override async ValueTask ClearAsync() pinnedTunnelPingsMessage = null; gameStartCancellationTokenSource?.Cancel(); + stunCancellationTokenSource?.Cancel(); v3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); v3GameTunnelHandlers.Clear(); playerTunnels.Clear(); @@ -545,7 +548,7 @@ private async ValueTask CloseP2PPortsAsync() { if (internetGatewayDevice is not null) { - foreach (ushort p2pPort in p2pPorts) + foreach (ushort p2pPort in ipV4P2PPorts.Select(q => q.InternalPort)) await internetGatewayDevice.CloseIpV4PortAsync(p2pPort); } } @@ -555,7 +558,7 @@ private async ValueTask CloseP2PPortsAsync() } finally { - p2pPorts.Clear(); + ipV4P2PPorts.Clear(); } try @@ -572,6 +575,7 @@ private async ValueTask CloseP2PPortsAsync() } finally { + ipV6P2PPorts.Clear(); p2pIpV6PortIds.Clear(); } } @@ -994,7 +998,7 @@ private void StartV3ConnectionListeners() if (p2pEnabled) { - foreach (var (remotePlayerName, remotePorts, localPingResults, remotePingResults, _) in p2pPlayers.Where(q => q.RemotePingResults.Any() && q.Enabled)) + foreach (var (remotePlayerName, remoteIpV6Ports, remoteIpV4Ports, localPingResults, remotePingResults, _) in p2pPlayers.Where(q => q.RemotePingResults.Any() && q.Enabled)) { (IPAddress selectedRemoteIpAddress, long combinedPing) = localPingResults .Where(q => q.RemoteIpAddress is not null && remotePingResults @@ -1006,11 +1010,25 @@ private void StartV3ConnectionListeners() if (combinedPing < playerTunnels.Single(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).CombinedPing) { + ushort[] localPorts; + ushort[] remotePorts; + + if (selectedRemoteIpAddress.AddressFamily is AddressFamily.InterNetworkV6) + { + localPorts = ipV6P2PPorts.Select(q => q.InternalPort).ToArray(); + remotePorts = remoteIpV6Ports; + } + else + { + localPorts = ipV4P2PPorts.Select(q => q.InternalPort).ToArray(); + remotePorts = remoteIpV4Ports; + } + var allPlayerNames = Players.Select(q => q.Name).OrderBy(q => q, StringComparer.OrdinalIgnoreCase).ToList(); string localPlayerName = FindLocalPlayer().Name; var remotePlayerNames = allPlayerNames.Where(q => !q.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); var tunnelClientPlayerNames = allPlayerNames.Where(q => !q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); - ushort localPort = p2pPorts[6 - remotePlayerNames.FindIndex(q => q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase))]; + ushort localPort = localPorts[6 - remotePlayerNames.FindIndex(q => q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase))]; ushort remotePort = remotePorts[6 - tunnelClientPlayerNames.FindIndex(q => q.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase))]; var p2pLocalTunnelHandler = new V3GameTunnelHandler(); @@ -1102,7 +1120,7 @@ private async ValueTask LaunchGameV3Async() Logger.Log("All players are connected, starting game!"); AddNotice("All players have connected...".L10N("Client:Main:PlayersConnected")); - List usedPorts = new(p2pPorts); + List usedPorts = new(ipV4P2PPorts.Select(q => q.InternalPort).Concat(ipV6P2PPorts.Select(q => q.InternalPort)).Distinct()); foreach ((List remotePlayerNames, V3GameTunnelHandler v3GameTunnelHandler) in v3GameTunnelHandlers) { @@ -1126,6 +1144,7 @@ private async ValueTask LaunchGameV3Async() FindLocalPlayer().Port = gamePort; gameStartTimer.Pause(); + stunCancellationTokenSource?.Cancel(); btnLaunchGame.InputEnabled = true; @@ -1345,18 +1364,27 @@ private ValueTask BroadcastPlayerTunnelPingsAsync() private async ValueTask BroadcastPlayerP2PRequestAsync() { - if (!p2pPorts.Any()) + if (!ipV6P2PPorts.Any() && !ipV4P2PPorts.Any()) { - p2pPorts = NetworkHelper.GetFreeUdpPorts(Array.Empty(), MAX_REMOTE_PLAYERS).ToList(); + var p2pPorts = NetworkHelper.GetFreeUdpPorts(Array.Empty(), MAX_REMOTE_PLAYERS).ToList(); + + stunCancellationTokenSource?.Cancel(); + stunCancellationTokenSource?.Dispose(); + + stunCancellationTokenSource = new CancellationTokenSource(); try { - (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync(internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? initialTunnel.IPAddresses); + (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync( + internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? initialTunnel.IPAddresses, stunCancellationTokenSource.Token); } catch (Exception ex) { ProgramConstants.LogException(ex, "Could not open UPnP P2P ports."); - AddNotice(string.Format(CultureInfo.CurrentCulture, "Could not open P2P ports. Check that UPnP port mapping is enabled for this device on your router/modem.".L10N("Client:Main:UPnPP2PFailed")), Color.Orange); + AddNotice(string.Format( + CultureInfo.CurrentCulture, + "Could not open P2P ports. Check that UPnP port mapping is enabled for this device on your router/modem.".L10N("Client:Main:UPnPP2PFailed")), + Color.Orange); return; } @@ -1367,7 +1395,14 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() } private ValueTask SendPlayerP2PRequestAsync() - => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_REQUEST + $" {publicIpV4Address};{publicIpV6Address};{(!p2pPorts.Any() ? null : p2pPorts.Select(q => q.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}", QueuedMessageType.SYSTEM_MESSAGE, 10); + { + return channel.SendCTCPMessageAsync( + CnCNetCommands.PLAYER_P2P_REQUEST + + $" {publicIpV4Address}\t{(!ipV4P2PPorts.Any() ? null : ipV4P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}" + + $";{publicIpV6Address}\t{(!ipV6P2PPorts.Any() ? null : ipV6P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}", + QueuedMessageType.SYSTEM_MESSAGE, + 10); + } /// /// Handles player option messages received from the game host. @@ -2203,8 +2238,10 @@ private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p List<(IPAddress IpAddress, long Ping)> localPingResults = new(); string[] splitLines = p2pRequestMessage.Split(';'); + string[] ipV4splitLines = splitLines[0].Split('\t'); + string[] ipV6splitLines = splitLines[1].Split('\t'); - if (IPAddress.TryParse(splitLines[0], out IPAddress parsedIpV4Address)) + if (IPAddress.TryParse(ipV4splitLines[0], out IPAddress parsedIpV4Address)) { long? pingResult = await NetworkHelper.PingAsync(parsedIpV4Address); @@ -2212,7 +2249,7 @@ private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p localPingResults.Add((parsedIpV4Address, pingResult.Value)); } - if (IPAddress.TryParse(splitLines[1], out IPAddress parsedIpV6Address)) + if (IPAddress.TryParse(ipV6splitLines[0], out IPAddress parsedIpV6Address)) { long? pingResult = await NetworkHelper.PingAsync(parsedIpV6Address); @@ -2221,13 +2258,20 @@ private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p } bool remotePlayerP2PEnabled = false; - ushort[] remotePlayerPorts = Array.Empty(); + ushort[] remotePlayerIpV4Ports = Array.Empty(); + ushort[] remotePlayerIpV6Ports = Array.Empty(); P2PPlayer remoteP2PPlayer; - if (parsedIpV4Address is not null || parsedIpV6Address is not null) + if (parsedIpV4Address is not null) + { + remotePlayerP2PEnabled = true; + remotePlayerIpV4Ports = ipV4splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); + } + + if (parsedIpV6Address is not null) { remotePlayerP2PEnabled = true; - remotePlayerPorts = splitLines[2].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); + remotePlayerIpV6Ports = ipV6splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); } if (p2pPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) @@ -2238,10 +2282,10 @@ private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p } else { - remoteP2PPlayer = new(playerName, Array.Empty(), new(), new(), false); + remoteP2PPlayer = new(playerName, Array.Empty(), Array.Empty(), new(), new(), false); } - p2pPlayers.Add(remoteP2PPlayer with { LocalPingResults = localPingResults, RemotePorts = remotePlayerPorts, Enabled = remotePlayerP2PEnabled }); + p2pPlayers.Add(remoteP2PPlayer with { LocalPingResults = localPingResults, RemoteIpV6Ports = remotePlayerIpV6Ports, RemoteIpV4Ports = remotePlayerIpV4Ports, Enabled = remotePlayerP2PEnabled }); if (remotePlayerP2PEnabled) { @@ -2286,7 +2330,7 @@ private void HandleP2PPingsMessage(string playerName, string p2pPingsMessage) } else { - p2pPlayer = new(playerName, Array.Empty(), new(), new(), false); + p2pPlayer = new(playerName, Array.Empty(), Array.Empty(), new(), new(), false); } p2pPlayers.Add(p2pPlayer with { RemotePingResults = playerPings }); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index 2e3fcad0d..0aee30c4d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -42,7 +42,6 @@ private static async ValueTask RunServiceAsync(CancellationToken cancellationTok } catch (OperationCanceledException) { - break; } } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index f17f967fb..223f9fb0f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -14,6 +14,7 @@ using System.Text; using System.Xml; using ClientCore; +using ClientCore.Extensions; using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet; @@ -32,15 +33,9 @@ internal static class UPnPHandler [AddressType.IpV6SiteLocal] = IPAddress.Parse("[FF05::C]") }.AsReadOnly(); - public static async ValueTask<(InternetGatewayDevice InternetGatewayDevice, List P2pPorts, List P2pIpV6PortIds, IPAddress ipV6Address, IPAddress ipV4Address)> SetupPortsAsync( - InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, List stunServerIpAddresses, CancellationToken cancellationToken = default) + public static async ValueTask<(InternetGatewayDevice InternetGatewayDevice, List<(ushort InternalPort, ushort ExternalPort)> IpV6P2PPorts, List<(ushort InternalPort, ushort ExternalPort)> IpV4P2PPorts, List P2PIpV6PortIds, IPAddress ipV6Address, IPAddress ipV4Address)> SetupPortsAsync( + InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, List stunServerIpAddresses, CancellationToken cancellationToken) { - var p2pPorts = new List(); - var p2pIpV6PortIds = new List(); - IPAddress routerPublicIpV4Address = null; - bool routerNatEnabled = false; - bool natDetected = false; - Logger.Log("Starting P2P Setup."); if (internetGatewayDevice is null) @@ -51,58 +46,77 @@ internal static class UPnPHandler internetGatewayDevice ??= GetInternetGatewayDevice(internetGatewayDevices, 1); } + IPAddress detectedPublicIpV4Address = null; + bool routerNatEnabled = false; + if (internetGatewayDevice is not null) { Logger.Log("Found NAT device."); routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(cancellationToken); - routerPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken); + detectedPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken); } + var ipV4StunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); + if (stunServerIpAddresses.Any(q => q.AddressFamily is AddressFamily.InterNetwork)) { - if (routerPublicIpV4Address == null) + IPAddress stunServerIpAddress = stunServerIpAddresses.Single(q => q.AddressFamily is AddressFamily.InterNetwork); + + if (detectedPublicIpV4Address == null) { Logger.Log("Using IPV4 STUN."); foreach (ushort p2pReservedPort in p2pReservedPorts) { - IPEndPoint publicIpV4Endpoint = await NetworkHelper.PerformStunAsync(stunServerIpAddresses.Single(q => q.AddressFamily is AddressFamily.InterNetwork), p2pReservedPort, cancellationToken); + IPEndPoint publicIpV4Endpoint = await NetworkHelper.PerformStunAsync(stunServerIpAddress, p2pReservedPort, cancellationToken); if (publicIpV4Endpoint is null) + { + Logger.Log("IPV4 STUN failed."); break; + } - routerPublicIpV4Address ??= publicIpV4Endpoint.Address; + detectedPublicIpV4Address ??= publicIpV4Endpoint.Address; + + if (p2pReservedPort != publicIpV4Endpoint.Port) + ipV4StunPortMapping.Add(new(p2pReservedPort, (ushort)publicIpV4Endpoint.Port)); } } + + if (ipV4StunPortMapping.Any()) + { + NetworkHelper.KeepStunAliveAsync( + stunServerIpAddress, + ipV4StunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); + } } else { Logger.Log($"STUN server {stunServerIpAddresses.First()} has no IPV4 address."); } - if (routerPublicIpV4Address == null) + if (detectedPublicIpV4Address == null) { Logger.Log("Using IPV4 trace detection."); - routerPublicIpV4Address = await NetworkHelper.TracePublicIpV4Address(cancellationToken); + detectedPublicIpV4Address = await NetworkHelper.TracePublicIpV4Address(cancellationToken); } var publicIpAddresses = NetworkHelper.GetPublicIpAddresses().ToList(); IPAddress publicIpV4Address = publicIpAddresses.FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetwork); + bool natDetected = routerNatEnabled || (publicIpV4Address is not null && detectedPublicIpV4Address is not null && !publicIpV4Address.Equals(detectedPublicIpV4Address)); - if (routerNatEnabled || (publicIpV4Address is not null && routerPublicIpV4Address is not null && !publicIpV4Address.Equals(routerPublicIpV4Address))) - natDetected = true; - - publicIpV4Address ??= routerPublicIpV4Address; + publicIpV4Address ??= detectedPublicIpV4Address; if (publicIpV4Address is not null) Logger.Log("Public IPV4 detected."); var privateIpV4Addresses = NetworkHelper.GetPrivateIpAddresses().Where(q => q.AddressFamily is AddressFamily.InterNetwork).ToList(); IPAddress privateIpV4Address = privateIpV4Addresses.FirstOrDefault(); + var ipV4P2PPorts = new List<(ushort InternalPort, ushort ExternalPort)>(); - if (natDetected && privateIpV4Address is not null && publicIpV4Address is not null) + if (natDetected && routerNatEnabled && privateIpV4Address is not null && publicIpV4Address is not null) { Logger.Log("Using IPV4 port mapping."); @@ -110,33 +124,57 @@ internal static class UPnPHandler { foreach (int p2PReservedPort in p2pReservedPorts) { - p2pPorts.Add(await internetGatewayDevice.OpenIpV4PortAsync(privateIpV4Address, (ushort)p2PReservedPort, cancellationToken)); + ushort openedPort = await internetGatewayDevice.OpenIpV4PortAsync(privateIpV4Address, (ushort)p2PReservedPort, cancellationToken); + + ipV4P2PPorts.Add((openedPort, openedPort)); } - p2pReservedPorts = p2pPorts; + p2pReservedPorts = ipV4P2PPorts.Select(q => q.InternalPort).ToList(); } catch (Exception ex) { ProgramConstants.LogException(ex, $"Could not open P2P IPV4 router ports for {privateIpV4Address} -> {publicIpV4Address}."); } } + else if (ipV4StunPortMapping.Any()) + { + ipV4P2PPorts = ipV4StunPortMapping; + } else { - p2pPorts = p2pReservedPorts; + ipV4P2PPorts = p2pReservedPorts.Select(q => (q, q)).ToList(); } - IPAddress publicIpV6Address = null; + IPAddress detectedPublicIpV6Address = null; + var ipV6StunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); if (stunServerIpAddresses.Any(q => q.AddressFamily is AddressFamily.InterNetworkV6)) { + Logger.Log("Using IPV6 STUN."); + + IPAddress stunServerIpAddress = stunServerIpAddresses.Single(q => q.AddressFamily is AddressFamily.InterNetworkV6); + foreach (ushort p2pReservedPort in p2pReservedPorts) { - IPEndPoint publicIpV6Endpoint = await NetworkHelper.PerformStunAsync(stunServerIpAddresses.Single(q => q.AddressFamily is AddressFamily.InterNetworkV6), p2pReservedPort, cancellationToken); + IPEndPoint publicIpV6Endpoint = await NetworkHelper.PerformStunAsync(stunServerIpAddress, p2pReservedPort, cancellationToken); if (publicIpV6Endpoint is null) + { + Logger.Log("IPV6 STUN failed."); break; + } - publicIpV6Address ??= publicIpV6Endpoint.Address; + detectedPublicIpV6Address ??= publicIpV6Endpoint.Address; + + if (p2pReservedPort != publicIpV6Endpoint.Port) + ipV6StunPortMapping.Add(new(p2pReservedPort, (ushort)publicIpV6Endpoint.Port)); + } + + if (ipV6StunPortMapping.Any()) + { + NetworkHelper.KeepStunAliveAsync( + stunServerIpAddress, + ipV6StunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); } } else @@ -144,31 +182,33 @@ internal static class UPnPHandler Logger.Log($"STUN server {stunServerIpAddresses.First()} has no IPV6 address."); } - if (publicIpV6Address is null) + IPAddress publicIpV6Address; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var publicIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses().Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); + var publicIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses().Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); - (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundPublicIpV6Address = publicIpV6Addresses - .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); + (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundPublicIpV6Address = publicIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); - if (foundPublicIpV6Address.IpAddress is null) - { - foundPublicIpV6Address = publicIpV6Addresses - .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); - } - - publicIpV6Address = foundPublicIpV6Address.IpAddress; - } - else + if (foundPublicIpV6Address.IpAddress is null) { - publicIpV6Address = NetworkHelper.GetPublicIpAddresses() - .FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); + foundPublicIpV6Address = publicIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); } + + publicIpV6Address = foundPublicIpV6Address.IpAddress; + } + else + { + publicIpV6Address = NetworkHelper.GetPublicIpAddresses() + .FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); } - if (publicIpV6Address is not null) + var ipV6P2PPorts = new List<(ushort InternalPort, ushort ExternalPort)>(); + var p2pIpV6PortIds = new List(); + + if (detectedPublicIpV6Address is not null || publicIpV6Address is not null) { Logger.Log("Public IPV6 detected."); @@ -193,9 +233,20 @@ internal static class UPnPHandler ProgramConstants.LogException(ex, $"Could not open P2P IPV6 router ports for {publicIpV6Address}."); } } + + if (detectedPublicIpV6Address is not null && publicIpV6Address is not null && !detectedPublicIpV6Address.Equals(publicIpV6Address)) + { + publicIpV6Address = detectedPublicIpV6Address; + + ipV6P2PPorts = ipV6StunPortMapping; + } + else + { + ipV6P2PPorts = p2pReservedPorts.Select(q => (q, q)).ToList(); + } } - return (internetGatewayDevice, p2pPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address); + return (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address); } private static async ValueTask> GetInternetGatewayDevicesAsync(CancellationToken cancellationToken) diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index df5092e2c..c0fe4bd9f 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -160,9 +160,6 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI short publicPortHostOrder = IPAddress.NetworkToHostOrder(publicPortNetworkOrder); ushort publicPort = (ushort)publicPortHostOrder; - if (publicPort != localPort) - throw new($"STUN ports mismatch, expected {localPort}, received {publicPort}."); - return new IPEndPoint(publicIpAddress, publicPort); } catch (Exception ex) @@ -174,6 +171,26 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI return null; } + public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List localPorts, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + foreach (ushort localPort in localPorts) + { + await PerformStunAsync(stunServerIpAddress, localPort, cancellationToken); + await Task.Delay(100, cancellationToken); + } + + await Task.Delay(5000, cancellationToken); + } + catch (TaskCanceledException) + { + } + } + } + /// /// Returns a free UDP port number above 1023. /// diff --git a/DXMainClient/Domain/Multiplayer/P2PPlayer.cs b/DXMainClient/Domain/Multiplayer/P2PPlayer.cs index 136c0f8d5..78eee8b43 100644 --- a/DXMainClient/Domain/Multiplayer/P2PPlayer.cs +++ b/DXMainClient/Domain/Multiplayer/P2PPlayer.cs @@ -5,7 +5,8 @@ namespace DTAClient.Domain.Multiplayer; internal readonly record struct P2PPlayer( string RemotePlayerName, - ushort[] RemotePorts, + ushort[] RemoteIpV6Ports, + ushort[] RemoteIpV4Ports, List<(IPAddress RemoteIpAddress, long Ping)> LocalPingResults, List<(IPAddress RemoteIpAddress, long Ping)> RemotePingResults, bool Enabled); \ No newline at end of file From 6722a88d1757131ac543e6bae80a65e11af173b9 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 12 Dec 2022 18:41:15 +0100 Subject: [PATCH 061/109] Cleanup --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 58 ++++++------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 08809fd4a..908fa9ed7 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -667,7 +667,7 @@ private async ValueTask ChannelUserLeftAsync(UserNameEventArgs e) private async ValueTask Channel_UserKickedAsync(UserNameEventArgs e) { - if (e.UserName == ProgramConstants.PLAYERNAME) + if (e.UserName.Equals(ProgramConstants.PLAYERNAME, StringComparison.OrdinalIgnoreCase)) { connectionManager.MainChannel.AddMessage( new(ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); @@ -678,29 +678,17 @@ private async ValueTask Channel_UserKickedAsync(UserNameEventArgs e) return; } - int index = Players.FindIndex(p => p.Name.Equals(e.UserName, StringComparison.OrdinalIgnoreCase)); - - if (index > -1) - { - Players.RemoveAt(index); - CopyPlayerDataToUI(); - UpdateDiscordPresence(); - ClearReadyStatuses(); - RemoveV3Player(e.UserName); - } + Players.Remove(Players.SingleOrDefault(p => p.Name.Equals(e.UserName, StringComparison.OrdinalIgnoreCase))); + CopyPlayerDataToUI(); + UpdateDiscordPresence(); + ClearReadyStatuses(); + RemoveV3Player(e.UserName); } private void RemoveV3Player(string playerName) { - (string Name, CnCNetTunnel Tunnel, int CombinedPing) playerTunnel = playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - - if (playerTunnel.Name is not null) - playerTunnels.Remove(playerTunnel); - - P2PPlayer p2pPlayer = p2pPlayers.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - - if (p2pPlayer.RemotePlayerName is not null) - p2pPlayers.Remove(p2pPlayer); + playerTunnels.Remove(playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + p2pPlayers.Remove(p2pPlayers.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); } private async ValueTask Channel_UserListReceivedAsync() @@ -769,19 +757,13 @@ private async ValueTask Channel_UserAddedAsync(ChannelUserEventArgs e) private async ValueTask RemovePlayerAsync(string playerName) { AbortGameStart(); + Players.Remove(Players.SingleOrDefault(p => p.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + CopyPlayerDataToUI(); + RemoveV3Player(playerName); - PlayerInfo pInfo = Players.Find(p => p.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - - if (pInfo != null) - { - Players.Remove(pInfo); - CopyPlayerDataToUI(); - RemoveV3Player(playerName); - - // This might not be necessary - if (IsHost) - await BroadcastPlayerOptionsAsync(); - } + // This might not be necessary + if (IsHost) + await BroadcastPlayerOptionsAsync(); sndLeaveSound.Play(); @@ -2219,13 +2201,7 @@ private void HandleTunnelPingsMessage(string playerName, string tunnelPingsMessa { CnCNetTunnel tunnel = tunnelHandler.Tunnels.Single(q => q.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)); - if (playerTunnels.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) - { - int index = playerTunnels.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - - playerTunnels.RemoveAt(index); - } - + playerTunnels.Remove(playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); playerTunnels.Add(new(playerName, tunnel, combinedPing)); AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} dynamic tunnel: {1} ({2}ms)".L10N("Client:Main:TunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); } @@ -2278,7 +2254,7 @@ private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p { remoteP2PPlayer = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - p2pPlayers.RemoveAt(p2pPlayers.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + p2pPlayers.Remove(remoteP2PPlayer); } else { @@ -2326,7 +2302,7 @@ private void HandleP2PPingsMessage(string playerName, string p2pPingsMessage) { p2pPlayer = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - p2pPlayers.RemoveAt(p2pPlayers.FindIndex(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + p2pPlayers.Remove(p2pPlayer); } else { From e7b4f2c972212ea209ab285c4d845ad1f0467336 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 13 Dec 2022 14:15:44 +0100 Subject: [PATCH 062/109] Refactor --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 448 ++++-------------- .../Multiplayer/CnCNet/V3ConnectionState.cs | 362 ++++++++++++++ 2 files changed, 463 insertions(+), 347 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 908fa9ed7..8ba03c491 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Net; -using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -36,8 +35,6 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private const double INITIAL_GAME_BROADCAST_DELAY = 10.0; private const double MAX_TIME_FOR_GAME_LAUNCH = 20.0; private const int PRIORITY_START_GAME = 10; - private const int PINNED_DYNAMIC_TUNNELS = 10; - private const ushort MAX_REMOTE_PLAYERS = 7; private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; @@ -51,9 +48,6 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private readonly List gamePlayerIds = new(); private readonly List hostUploadedMaps = new(); private readonly List chatCommandDownloadedMaps = new(); - private readonly List<(string RemotePlayerName, CnCNetTunnel Tunnel, int CombinedPing)> playerTunnels = new(); - private readonly List<(List RemotePlayerNames, V3GameTunnelHandler Tunnel)> v3GameTunnelHandlers = new(); - private readonly List p2pPlayers = new(); private TunnelSelectionWindow tunnelSelectionWindow; private XNAClientButton btnChangeTunnel; @@ -70,15 +64,17 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby private bool isStartingGame; private string gameFilesHash; private MapSharingConfirmationPanel mapSharingConfirmationPanel; - private CnCNetTunnel initialTunnel; - private IPAddress publicIpV4Address; - private IPAddress publicIpV6Address; - private List<(ushort InternalPort, ushort ExternalPort)> ipV6P2PPorts = new(); - private List<(ushort InternalPort, ushort ExternalPort)> ipV4P2PPorts = new(); - private List p2pIpV6PortIds = new(); private CancellationTokenSource gameStartCancellationTokenSource; - private CancellationTokenSource stunCancellationTokenSource; + private EventHandler channel_UserAddedFunc; + private EventHandler channel_UserQuitIRCFunc; + private EventHandler channel_UserLeftFunc; + private EventHandler channel_UserKickedFunc; + private EventHandler channel_UserListReceivedFunc; + private EventHandler connectionManager_ConnectionLostFunc; + private EventHandler connectionManager_DisconnectedFunc; + private EventHandler tunnelHandler_CurrentTunnelFunc; private bool disposed; + private V3ConnectionState v3ConnectionState; /// /// The SHA1 of the latest selected map. @@ -97,20 +93,6 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby /// private bool tunnelErrorMode; - private EventHandler channel_UserAddedFunc; - private EventHandler channel_UserQuitIRCFunc; - private EventHandler channel_UserLeftFunc; - private EventHandler channel_UserKickedFunc; - private EventHandler channel_UserListReceivedFunc; - private EventHandler connectionManager_ConnectionLostFunc; - private EventHandler connectionManager_DisconnectedFunc; - private EventHandler tunnelHandler_CurrentTunnelFunc; - private List<(int Ping, string Hash)> pinnedTunnels; - private string pinnedTunnelPingsMessage; - private bool dynamicTunnelsEnabled; - private bool p2pEnabled; - private InternetGatewayDevice internetGatewayDevice; - public CnCNetGameLobby( WindowManager windowManager, TopBar topBar, @@ -264,6 +246,8 @@ public override void Initialize() connectionManager_DisconnectedFunc = (_, _) => HandleConnectionLossAsync().HandleTask(); tunnelHandler_CurrentTunnelFunc = (_, _) => UpdatePingAsync().HandleTask(); + v3ConnectionState = new(tunnelHandler); + PostInitialize(); } @@ -325,8 +309,8 @@ public async ValueTask SetUpAsync( this.hostName = hostName; this.playerLimit = playerLimit; this.isCustomPassword = isCustomPassword; - dynamicTunnelsEnabled = UserINISettings.Instance.UseDynamicTunnels; - p2pEnabled = UserINISettings.Instance.UseP2P; + v3ConnectionState.DynamicTunnelsEnabled = UserINISettings.Instance.UseDynamicTunnels; + v3ConnectionState.P2PEnabled = UserINISettings.Instance.UseP2P; channel.MessageAdded += Channel_MessageAdded; channel.CTCPReceived += Channel_CTCPReceived; channel.UserKicked += channel_UserKickedFunc; @@ -350,18 +334,7 @@ public async ValueTask SetUpAsync( AIPlayers.Clear(); } - initialTunnel = tunnel; - - if (!dynamicTunnelsEnabled) - { - tunnelHandler.CurrentTunnel = initialTunnel; - } - else - { - tunnelHandler.CurrentTunnel = tunnelHandler.Tunnels - .Where(q => q.PingInMs > -1 && !q.RequiresPassword && q.Clients < q.MaxClients - 8 && q.Version == Constants.TUNNEL_VERSION_3) - .MinBy(q => q.PingInMs); - } + v3ConnectionState.Setup(tunnel); tunnelHandler.CurrentTunnelPinged += tunnelHandler_CurrentTunnelFunc; connectionManager.ConnectionLost += connectionManager_ConnectionLostFunc; @@ -377,18 +350,8 @@ public async ValueTask OnJoinedAsync() fhc.CalculateHashes(GameModeMaps.GameModes); gameFilesHash = fhc.GetCompleteHash(); - pinnedTunnels = tunnelHandler.Tunnels - .Where(q => !q.RequiresPassword && q.PingInMs > -1 && q.Clients < q.MaxClients - 8 && q.Version == Constants.TUNNEL_VERSION_3) - .OrderBy(q => q.PingInMs) - .ThenBy(q => q.Hash, StringComparer.OrdinalIgnoreCase) - .Take(PINNED_DYNAMIC_TUNNELS) - .Select(q => (q.PingInMs, q.Hash)) - .ToList(); - IEnumerable tunnelPings = pinnedTunnels - .Select(q => FormattableString.Invariant($"{q.Ping};{q.Hash}\t")); - - pinnedTunnelPingsMessage = string.Concat(tunnelPings); + v3ConnectionState.PinTunnels(); if (IsHost) { @@ -411,10 +374,10 @@ await connectionManager.SendCustomMessageAsync(new( { await channel.SendCTCPMessageAsync(CnCNetCommands.FILE_HASH + " " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); - if (dynamicTunnelsEnabled) + if (v3ConnectionState.DynamicTunnelsEnabled) BroadcastPlayerTunnelPingsAsync().HandleTask(); - if (p2pEnabled) + if (v3ConnectionState.P2PEnabled) BroadcastPlayerP2PRequestAsync().HandleTask(); } @@ -430,8 +393,8 @@ private async ValueTask UpdatePingAsync() { int ping; - if (dynamicTunnelsEnabled && (pinnedTunnels?.Any() ?? false)) - ping = pinnedTunnels.Min(q => q.Ping); + if (v3ConnectionState.DynamicTunnelsEnabled && v3ConnectionState.PinnedTunnels.Any()) + ping = v3ConnectionState.PinnedTunnels.Min(q => q.Ping); else if (tunnelHandler.CurrentTunnel == null) return; else @@ -462,7 +425,7 @@ protected override void CopyPlayerDataToUI() private void PrintTunnelServerInformation(string s) { - if (dynamicTunnelsEnabled) + if (v3ConnectionState.DynamicTunnelsEnabled) { AddNotice("Dynamic tunnels enabled".L10N("Client:Main:DynamicTunnelsEnabled")); } @@ -474,7 +437,11 @@ private void PrintTunnelServerInformation(string s) { AddNotice(string.Format(CultureInfo.CurrentCulture, "Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})".L10N("Client:Main:TunnelInfo"), - tunnelHandler.CurrentTunnel.Name, tunnelHandler.CurrentTunnel.Country, tunnelHandler.CurrentTunnel.Clients, tunnelHandler.CurrentTunnel.MaxClients, tunnelHandler.CurrentTunnel.Official)); + tunnelHandler.CurrentTunnel.Name, + tunnelHandler.CurrentTunnel.Country, + tunnelHandler.CurrentTunnel.Clients, + tunnelHandler.CurrentTunnel.MaxClients, + tunnelHandler.CurrentTunnel.Official)); } } @@ -526,58 +493,13 @@ public override async ValueTask ClearAsync() tbChatInput.Text = string.Empty; tunnelHandler.CurrentTunnelPinged -= tunnelHandler_CurrentTunnelFunc; tunnelHandler.CurrentTunnel = null; - pinnedTunnelPingsMessage = null; gameStartCancellationTokenSource?.Cancel(); - stunCancellationTokenSource?.Cancel(); - v3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); - v3GameTunnelHandlers.Clear(); - playerTunnels.Clear(); + v3ConnectionState.DisposeAsync().HandleTask(); gamePlayerIds.Clear(); - pinnedTunnels?.Clear(); - p2pPlayers.Clear(); GameLeft?.Invoke(this, EventArgs.Empty); TopBar.RemovePrimarySwitchable(this); ResetDiscordPresence(); - CloseP2PPortsAsync().HandleTask(); - } - - private async ValueTask CloseP2PPortsAsync() - { - try - { - if (internetGatewayDevice is not null) - { - foreach (ushort p2pPort in ipV4P2PPorts.Select(q => q.InternalPort)) - await internetGatewayDevice.CloseIpV4PortAsync(p2pPort); - } - } - catch (Exception ex) - { - ProgramConstants.LogException(ex, "Could not close P2P IPV4 ports."); - } - finally - { - ipV4P2PPorts.Clear(); - } - - try - { - if (internetGatewayDevice is not null) - { - foreach (ushort p2pIpV6PortId in p2pIpV6PortIds) - await internetGatewayDevice.CloseIpV6PortAsync(p2pIpV6PortId); - } - } - catch (Exception ex) - { - ProgramConstants.LogException(ex, "Could not close P2P IPV6 ports."); - } - finally - { - ipV6P2PPorts.Clear(); - p2pIpV6PortIds.Clear(); - } } public async ValueTask LeaveGameLobbyAsync() @@ -682,13 +604,7 @@ private async ValueTask Channel_UserKickedAsync(UserNameEventArgs e) CopyPlayerDataToUI(); UpdateDiscordPresence(); ClearReadyStatuses(); - RemoveV3Player(e.UserName); - } - - private void RemoveV3Player(string playerName) - { - playerTunnels.Remove(playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); - p2pPlayers.Remove(p2pPlayers.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + v3ConnectionState.RemoveV3Player(e.UserName); } private async ValueTask Channel_UserListReceivedAsync() @@ -715,10 +631,10 @@ private async ValueTask Channel_UserAddedAsync(ChannelUserEventArgs e) if (Players.Count + AIPlayers.Count > MAX_PLAYER_COUNT && AIPlayers.Count > 0) AIPlayers.RemoveAt(AIPlayers.Count - 1); - if (dynamicTunnelsEnabled && pInfo != FindLocalPlayer()) + if (v3ConnectionState.DynamicTunnelsEnabled && pInfo != FindLocalPlayer()) BroadcastPlayerTunnelPingsAsync().HandleTask(); - if (p2pEnabled && pInfo != FindLocalPlayer()) + if (v3ConnectionState.P2PEnabled && pInfo != FindLocalPlayer()) BroadcastPlayerP2PRequestAsync().HandleTask(); sndJoinSound.Play(); @@ -759,7 +675,7 @@ private async ValueTask RemovePlayerAsync(string playerName) AbortGameStart(); Players.Remove(Players.SingleOrDefault(p => p.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase))); CopyPlayerDataToUI(); - RemoveV3Player(playerName); + v3ConnectionState.RemoveV3Player(playerName); // This might not be necessary if (IsHost) @@ -810,8 +726,10 @@ private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) { lbChatMessages.AddMessage(new ChatMessage( Color.Silver, - string.Format("Message blocked from {0}".L10N("Client:Main:MessageBlockedFromPlayer"), - e.Message.SenderName))); + string.Format( + CultureInfo.CurrentCulture, + "Message blocked from {0}".L10N("Client:Main:MessageBlockedFromPlayer"), + e.Message.SenderName))); } else { @@ -833,7 +751,7 @@ protected override async ValueTask HostLaunchGameAsync() if (tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_2) await HostLaunchGameV2Async(); - else if (dynamicTunnelsEnabled || tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_3) + else if (v3ConnectionState.DynamicTunnelsEnabled || tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_3) await HostLaunchGameV3Async(); else throw new InvalidOperationException("Unknown tunnel server version!"); @@ -857,7 +775,7 @@ private async ValueTask HostLaunchGameV2Async() { ShowTunnelSelectionWindow(("An error occured while contacting " + "the CnCNet tunnel server.\nTry picking a different tunnel server:").L10N("Client:Main:ConnectTunnelError1")); - AddNotice(string.Format(CultureInfo.InvariantCulture, "An error occured while contacting the specified CnCNet " + + AddNotice(string.Format(CultureInfo.CurrentCulture, "An error occured while contacting the specified CnCNet " + "tunnel server. Please try using a different tunnel server " + "(accessible by typing /{0} in the chat box).".L10N("Client:Main:ConnectTunnelError2"), CnCNetLobbyCommands.CHANGETUNNEL), ERROR_MESSAGE_COLOR); @@ -958,85 +876,20 @@ private void StartV3ConnectionListeners() uint gameLocalPlayerId = gamePlayerIds[Players.FindIndex(p => p == FindLocalPlayer())]; - v3GameTunnelHandlers.Clear(); gameStartCancellationTokenSource?.Dispose(); gameStartCancellationTokenSource = new(); - if (!dynamicTunnelsEnabled) - { - var gameTunnelHandler = new V3GameTunnelHandler(); - - gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - - gameTunnelHandler.SetUp(new(tunnelHandler.CurrentTunnel.IPAddress, tunnelHandler.CurrentTunnel.Port), 0, gameLocalPlayerId, gameStartCancellationTokenSource.Token); - gameTunnelHandler.ConnectToTunnel(); - v3GameTunnelHandlers.Add(new(Players.Where(q => q != FindLocalPlayer()).Select(q => q.Name).ToList(), gameTunnelHandler)); - } - else - { - List p2pPlayerTunnels = new(); - - if (p2pEnabled) - { - foreach (var (remotePlayerName, remoteIpV6Ports, remoteIpV4Ports, localPingResults, remotePingResults, _) in p2pPlayers.Where(q => q.RemotePingResults.Any() && q.Enabled)) - { - (IPAddress selectedRemoteIpAddress, long combinedPing) = localPingResults - .Where(q => q.RemoteIpAddress is not null && remotePingResults - .Where(r => r.RemoteIpAddress is not null) - .Select(r => r.RemoteIpAddress.AddressFamily) - .Contains(q.RemoteIpAddress.AddressFamily)) - .Select(q => (q.RemoteIpAddress, q.Ping + remotePingResults.Single(r => r.RemoteIpAddress.AddressFamily == q.RemoteIpAddress.AddressFamily).Ping)) - .MaxBy(q => q.RemoteIpAddress.AddressFamily); - - if (combinedPing < playerTunnels.Single(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).CombinedPing) - { - ushort[] localPorts; - ushort[] remotePorts; - - if (selectedRemoteIpAddress.AddressFamily is AddressFamily.InterNetworkV6) - { - localPorts = ipV6P2PPorts.Select(q => q.InternalPort).ToArray(); - remotePorts = remoteIpV6Ports; - } - else - { - localPorts = ipV4P2PPorts.Select(q => q.InternalPort).ToArray(); - remotePorts = remoteIpV4Ports; - } - - var allPlayerNames = Players.Select(q => q.Name).OrderBy(q => q, StringComparer.OrdinalIgnoreCase).ToList(); - string localPlayerName = FindLocalPlayer().Name; - var remotePlayerNames = allPlayerNames.Where(q => !q.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); - var tunnelClientPlayerNames = allPlayerNames.Where(q => !q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); - ushort localPort = localPorts[6 - remotePlayerNames.FindIndex(q => q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase))]; - ushort remotePort = remotePorts[6 - tunnelClientPlayerNames.FindIndex(q => q.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase))]; - var p2pLocalTunnelHandler = new V3GameTunnelHandler(); - - p2pLocalTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - p2pLocalTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - - p2pLocalTunnelHandler.SetUp(new(selectedRemoteIpAddress, remotePort), localPort, gameLocalPlayerId, gameStartCancellationTokenSource.Token); - p2pLocalTunnelHandler.ConnectToTunnel(); - v3GameTunnelHandlers.Add(new(new() { remotePlayerName }, p2pLocalTunnelHandler)); - p2pPlayerTunnels.Add(remotePlayerName); - } - } - } - - foreach (IGrouping tunnelGrouping in playerTunnels.Where(q => !p2pPlayerTunnels.Contains(q.RemotePlayerName, StringComparer.OrdinalIgnoreCase)).GroupBy(q => q.Tunnel)) - { - var gameTunnelHandler = new V3GameTunnelHandler(); - - gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); - gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); + void RemoteHostConnectedAction() => AddCallback(() => GameTunnelHandler_Connected_CallbackAsync().HandleTask()); + void RemoteHostConnectionFailedAction() => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); - gameTunnelHandler.SetUp(new(tunnelGrouping.Key.IPAddress, tunnelGrouping.Key.Port), 0, gameLocalPlayerId, gameStartCancellationTokenSource.Token); - gameTunnelHandler.ConnectToTunnel(); - v3GameTunnelHandlers.Add(new(tunnelGrouping.Select(q => q.Name).ToList(), gameTunnelHandler)); - } - } + v3ConnectionState.StartV3ConnectionListeners( + gameLocalPlayerId, + FindLocalPlayer().Name, + Players, + RemoteHostConnectedAction, + RemoteHostConnectionFailedAction, + gameStartCancellationTokenSource.Token); // Abort starting the game if not everyone // replies within the timer's limit @@ -1045,9 +898,9 @@ private void StartV3ConnectionListeners() private async ValueTask GameTunnelHandler_Connected_CallbackAsync() { - if (dynamicTunnelsEnabled) + if (v3ConnectionState.DynamicTunnelsEnabled) { - if (v3GameTunnelHandlers.Any() && v3GameTunnelHandlers.TrueForAll(q => q.Tunnel.ConnectSucceeded)) + if (v3ConnectionState.V3GameTunnelHandlers.Any() && v3ConnectionState.V3GameTunnelHandlers.TrueForAll(q => q.Tunnel.ConnectSucceeded)) SetLocalPlayerConnected(); } else @@ -1072,7 +925,7 @@ private async ValueTask GameTunnelHandler_ConnectionFailed_CallbackAsync() private void HandleTunnelFail(string playerName) { Logger.Log(playerName + " failed to connect - aborting game launch."); - AddNotice(string.Format(CultureInfo.InvariantCulture, "{0} failed to connect. Please retry, disable P2P or pick " + + AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} failed to connect. Please retry, disable P2P or pick " + "another tunnel server by typing /{1} in the chat input box.".L10N("Client:Main:PlayerConnectFailed"), playerName, CnCNetLobbyCommands.CHANGETUNNEL)); AbortGameStart(); } @@ -1102,9 +955,9 @@ private async ValueTask LaunchGameV3Async() Logger.Log("All players are connected, starting game!"); AddNotice("All players have connected...".L10N("Client:Main:PlayersConnected")); - List usedPorts = new(ipV4P2PPorts.Select(q => q.InternalPort).Concat(ipV6P2PPorts.Select(q => q.InternalPort)).Distinct()); + List usedPorts = new(v3ConnectionState.IpV4P2PPorts.Select(q => q.InternalPort).Concat(v3ConnectionState.IpV6P2PPorts.Select(q => q.InternalPort)).Distinct()); - foreach ((List remotePlayerNames, V3GameTunnelHandler v3GameTunnelHandler) in v3GameTunnelHandlers) + foreach ((List remotePlayerNames, V3GameTunnelHandler v3GameTunnelHandler) in v3ConnectionState.V3GameTunnelHandlers) { var currentTunnelPlayers = Players.Where(q => remotePlayerNames.Contains(q.Name)).ToList(); IEnumerable indexes = currentTunnelPlayers.Select(q => q.Index); @@ -1118,7 +971,7 @@ private async ValueTask LaunchGameV3Async() usedPorts.AddRange(createdLocalPlayerPorts); } - foreach (V3GameTunnelHandler v3GameTunnelHandler in v3GameTunnelHandlers.Select(q => q.Tunnel)) + foreach (V3GameTunnelHandler v3GameTunnelHandler in v3ConnectionState.V3GameTunnelHandlers.Select(q => q.Tunnel)) v3GameTunnelHandler.StartPlayerConnections(); int gamePort = NetworkHelper.GetFreeUdpPorts(usedPorts, 1).Single(); @@ -1126,7 +979,7 @@ private async ValueTask LaunchGameV3Async() FindLocalPlayer().Port = gamePort; gameStartTimer.Pause(); - stunCancellationTokenSource?.Cancel(); + v3ConnectionState.StunCancellationTokenSource?.Cancel(); btnLaunchGame.InputEnabled = true; @@ -1138,7 +991,7 @@ private void AbortGameStart() btnLaunchGame.InputEnabled = true; gameStartCancellationTokenSource?.Cancel(); - v3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); + v3ConnectionState.V3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); gameStartTimer.Pause(); isStartingGame = false; @@ -1146,7 +999,7 @@ private void AbortGameStart() protected override IPAddress GetIPAddressForPlayer(PlayerInfo player) { - if (p2pEnabled || dynamicTunnelsEnabled || tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) + if (v3ConnectionState.P2PEnabled || v3ConnectionState.DynamicTunnelsEnabled || tunnelHandler.CurrentTunnel.Version == Constants.TUNNEL_VERSION_3) return IPAddress.Loopback.MapToIPv4(); return base.GetIPAddressForPlayer(player); @@ -1342,49 +1195,33 @@ protected override async ValueTask BroadcastPlayerExtraOptionsAsync() } private ValueTask BroadcastPlayerTunnelPingsAsync() - => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_TUNNEL_PINGS + " " + pinnedTunnelPingsMessage, QueuedMessageType.SYSTEM_MESSAGE, 10); + => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_TUNNEL_PINGS + " " + v3ConnectionState.PinnedTunnelPingsMessage, QueuedMessageType.SYSTEM_MESSAGE, 10); private async ValueTask BroadcastPlayerP2PRequestAsync() { - if (!ipV6P2PPorts.Any() && !ipV4P2PPorts.Any()) - { - var p2pPorts = NetworkHelper.GetFreeUdpPorts(Array.Empty(), MAX_REMOTE_PLAYERS).ToList(); - - stunCancellationTokenSource?.Cancel(); - stunCancellationTokenSource?.Dispose(); + bool p2pSetupSucceeded; - stunCancellationTokenSource = new CancellationTokenSource(); - - try - { - (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync( - internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? initialTunnel.IPAddresses, stunCancellationTokenSource.Token); - } - catch (Exception ex) - { - ProgramConstants.LogException(ex, "Could not open UPnP P2P ports."); - AddNotice(string.Format( - CultureInfo.CurrentCulture, - "Could not open P2P ports. Check that UPnP port mapping is enabled for this device on your router/modem.".L10N("Client:Main:UPnPP2PFailed")), - Color.Orange); + try + { + p2pSetupSucceeded = await v3ConnectionState.HandlePlayerP2PRequestAsync(); + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, "Could not open UPnP P2P ports."); + AddNotice(string.Format( + CultureInfo.CurrentCulture, + "Could not open P2P ports. Check that UPnP port mapping is enabled for this device on your router/modem.".L10N("Client:Main:UPnPP2PFailed")), + Color.Orange); - return; - } + return; } - if (publicIpV4Address is not null || publicIpV6Address is not null) + if (p2pSetupSucceeded) await SendPlayerP2PRequestAsync(); } private ValueTask SendPlayerP2PRequestAsync() - { - return channel.SendCTCPMessageAsync( - CnCNetCommands.PLAYER_P2P_REQUEST + - $" {publicIpV4Address}\t{(!ipV4P2PPorts.Any() ? null : ipV4P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}" + - $";{publicIpV6Address}\t{(!ipV6P2PPorts.Any() ? null : ipV6P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}", - QueuedMessageType.SYSTEM_MESSAGE, - 10); - } + => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_REQUEST + v3ConnectionState.GetP2PRequestCommand(), QueuedMessageType.SYSTEM_MESSAGE, 10); /// /// Handles player option messages received from the game host. @@ -1534,23 +1371,23 @@ protected override async ValueTask OnGameOptionChangedAsync() sb.Append(RandomSeed); sb.Append(Convert.ToInt32(RemoveStartingLocations)); sb.Append(Map.UntranslatedName); - sb.Append(Convert.ToInt32(dynamicTunnelsEnabled)); + sb.Append(Convert.ToInt32(v3ConnectionState.DynamicTunnelsEnabled)); await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); } private async ValueTask ToggleDynamicTunnelsAsync() { - await ChangeDynamicTunnelsSettingAsync(!dynamicTunnelsEnabled); + await ChangeDynamicTunnelsSettingAsync(!v3ConnectionState.DynamicTunnelsEnabled); await OnGameOptionChangedAsync(); - if (!dynamicTunnelsEnabled) - await TunnelSelectionWindow_TunnelSelectedAsync(new(initialTunnel)); + if (!v3ConnectionState.DynamicTunnelsEnabled) + await TunnelSelectionWindow_TunnelSelectedAsync(new(v3ConnectionState.InitialTunnel)); } private async ValueTask ToggleP2PAsync() { - p2pEnabled = !p2pEnabled; + bool p2pEnabled = await v3ConnectionState.ToggleP2PAsync(); if (p2pEnabled) { @@ -1560,12 +1397,6 @@ private async ValueTask ToggleP2PAsync() return; } - await CloseP2PPortsAsync(); - - internetGatewayDevice = null; - publicIpV4Address = null; - publicIpV6Address = null; - AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled P2P".L10N("Client:Main:P2PDisabled"), FindLocalPlayer().Name)); await SendPlayerP2PRequestAsync(); } @@ -1741,13 +1572,13 @@ private async ValueTask ApplyGameOptionsAsync(string sender, string message) bool newDynamicTunnelsSetting = Conversions.BooleanFromString(parts[partIndex + 9], true); - if (newDynamicTunnelsSetting != dynamicTunnelsEnabled) + if (newDynamicTunnelsSetting != v3ConnectionState.DynamicTunnelsEnabled) await ChangeDynamicTunnelsSettingAsync(newDynamicTunnelsSetting); } private async ValueTask ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsEnabledValue) { - dynamicTunnelsEnabled = newDynamicTunnelsEnabledValue; + v3ConnectionState.DynamicTunnelsEnabled = newDynamicTunnelsEnabledValue; if (newDynamicTunnelsEnabledValue) AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has enabled Dynamic Tunnels".L10N("Client:Main:HostEnableDynamicTunnels"))); @@ -1811,8 +1642,8 @@ protected override async ValueTask GameProcessExitedAsync() await base.GameProcessExitedAsync(); await channel.SendCTCPMessageAsync(CnCNetCommands.RETURN, QueuedMessageType.SYSTEM_MESSAGE, 20); gameStartCancellationTokenSource?.Cancel(); - v3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); - v3GameTunnelHandlers.Clear(); + v3ConnectionState.V3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); + v3ConnectionState.V3GameTunnelHandlers.Clear(); ReturnNotification(ProgramConstants.PLAYERNAME); if (IsHost) @@ -2175,7 +2006,7 @@ private async ValueTask HandleTunnelServerChangeMessageAsync(string sender, stri private void HandleTunnelPingsMessage(string playerName, string tunnelPingsMessage) { - if (!pinnedTunnels.Any()) + if (!v3ConnectionState.PinnedTunnels.Any()) return; string[] tunnelPingsLines = tunnelPingsMessage.Split('\t', StringSplitOptions.RemoveEmptyEntries); @@ -2186,8 +2017,8 @@ private void HandleTunnelPingsMessage(string playerName, string tunnelPingsMessa return (int.Parse(split[0], CultureInfo.InvariantCulture), split[1]); }); IEnumerable<(int CombinedPing, string Hash)> combinedTunnelResults = tunnelPings - .Where(q => pinnedTunnels.Select(r => r.Hash).Contains(q.Hash)) - .Select(q => (CombinedPing: q.Ping + pinnedTunnels.SingleOrDefault(r => q.Hash.Equals(r.Hash, StringComparison.OrdinalIgnoreCase)).Ping, q.Hash)); + .Where(q => v3ConnectionState.PinnedTunnels.Select(r => r.Hash).Contains(q.Hash)) + .Select(q => (CombinedPing: q.Ping + v3ConnectionState.PinnedTunnels.SingleOrDefault(r => q.Hash.Equals(r.Hash, StringComparison.OrdinalIgnoreCase)).Ping, q.Hash)); (int combinedPing, string hash) = combinedTunnelResults .OrderBy(q => q.CombinedPing) .ThenBy(q => q.Hash, StringComparer.OrdinalIgnoreCase) @@ -2201,72 +2032,23 @@ private void HandleTunnelPingsMessage(string playerName, string tunnelPingsMessa { CnCNetTunnel tunnel = tunnelHandler.Tunnels.Single(q => q.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)); - playerTunnels.Remove(playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); - playerTunnels.Add(new(playerName, tunnel, combinedPing)); + v3ConnectionState.PlayerTunnels.Remove(v3ConnectionState.PlayerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + v3ConnectionState.PlayerTunnels.Add(new(playerName, tunnel, combinedPing)); AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} dynamic tunnel: {1} ({2}ms)".L10N("Client:Main:TunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); } } private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p2pRequestMessage) { - if (!p2pEnabled) + if (!v3ConnectionState.P2PEnabled) return; - List<(IPAddress IpAddress, long Ping)> localPingResults = new(); - string[] splitLines = p2pRequestMessage.Split(';'); - string[] ipV4splitLines = splitLines[0].Split('\t'); - string[] ipV6splitLines = splitLines[1].Split('\t'); - - if (IPAddress.TryParse(ipV4splitLines[0], out IPAddress parsedIpV4Address)) - { - long? pingResult = await NetworkHelper.PingAsync(parsedIpV4Address); - - if (pingResult is not null) - localPingResults.Add((parsedIpV4Address, pingResult.Value)); - } - - if (IPAddress.TryParse(ipV6splitLines[0], out IPAddress parsedIpV6Address)) - { - long? pingResult = await NetworkHelper.PingAsync(parsedIpV6Address); - - if (pingResult is not null) - localPingResults.Add((parsedIpV6Address, pingResult.Value)); - } - - bool remotePlayerP2PEnabled = false; - ushort[] remotePlayerIpV4Ports = Array.Empty(); - ushort[] remotePlayerIpV6Ports = Array.Empty(); - P2PPlayer remoteP2PPlayer; - - if (parsedIpV4Address is not null) - { - remotePlayerP2PEnabled = true; - remotePlayerIpV4Ports = ipV4splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); - } - - if (parsedIpV6Address is not null) - { - remotePlayerP2PEnabled = true; - remotePlayerIpV6Ports = ipV6splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); - } - - if (p2pPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) - { - remoteP2PPlayer = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - - p2pPlayers.Remove(remoteP2PPlayer); - } - else - { - remoteP2PPlayer = new(playerName, Array.Empty(), Array.Empty(), new(), new(), false); - } - - p2pPlayers.Add(remoteP2PPlayer with { LocalPingResults = localPingResults, RemoteIpV6Ports = remotePlayerIpV6Ports, RemoteIpV4Ports = remotePlayerIpV4Ports, Enabled = remotePlayerP2PEnabled }); + bool remotePlayerP2PEnabled = await v3ConnectionState.PingRemotePlayer(playerName, p2pRequestMessage); if (remotePlayerP2PEnabled) { ShowP2PPlayerStatus(playerName); - await channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_PINGS + $" {playerName}-{localPingResults.Select(q => $"{q.IpAddress};{q.Ping}\t").Aggregate((q, r) => $"{q}{r}")}", QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_PINGS + v3ConnectionState.GetP2PPingCommand(playerName), QueuedMessageType.SYSTEM_MESSAGE, 10); } else { @@ -2276,48 +2058,15 @@ private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p private void HandleP2PPingsMessage(string playerName, string p2pPingsMessage) { - if (!p2pEnabled) - return; - - string[] splitLines = p2pPingsMessage.Split('-'); - string pingPlayerName = splitLines[0]; - - if (!FindLocalPlayer().Name.Equals(pingPlayerName, StringComparison.OrdinalIgnoreCase)) - return; - - string[] pingResults = splitLines[1].Split('\t', StringSplitOptions.RemoveEmptyEntries); - List<(IPAddress IpAddress, long Ping)> playerPings = new(); - - foreach (string pingResult in pingResults) - { - string[] ipAddressPingResult = pingResult.Split(';'); - - if (IPAddress.TryParse(ipAddressPingResult[0], out IPAddress ipV4Address)) - playerPings.Add((ipV4Address, long.Parse(ipAddressPingResult[1], CultureInfo.InvariantCulture))); - } - - P2PPlayer p2pPlayer; - - if (p2pPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) - { - p2pPlayer = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); - - p2pPlayers.Remove(p2pPlayer); - } - else - { - p2pPlayer = new(playerName, Array.Empty(), Array.Empty(), new(), new(), false); - } - - p2pPlayers.Add(p2pPlayer with { RemotePingResults = playerPings }); + bool shouldUpdatePlayerStatus = v3ConnectionState.UpdateRemotePingResults(playerName, p2pPingsMessage, FindLocalPlayer().Name); - if (!p2pPlayer.RemotePingResults.Any()) + if (shouldUpdatePlayerStatus) ShowP2PPlayerStatus(playerName); } private void ShowP2PPlayerStatus(string playerName) { - P2PPlayer p2pPlayer = p2pPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); + P2PPlayer p2pPlayer = v3ConnectionState.P2PPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); if (p2pPlayer.RemotePingResults.Any() && p2pPlayer.LocalPingResults.Any()) AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} supports P2P ({1}ms)".L10N("Client:Main:PlayerP2PSupported"), playerName, p2pPlayer.LocalPingResults.Min(q => q.Ping))); @@ -2459,7 +2208,7 @@ private void HandleMapUploadRequest(string sender, string mapHash) { Logger.Log("HandleMapUploadRequest: Map is official, so skip request"); AddNotice( - string.Format(("{0} doesn't have the map '{1}' on their local installation. " + + string.Format(CultureInfo.CurrentCulture, ("{0} doesn't have the map '{1}' on their local installation. " + "The map needs to be changed or {0} is unable to participate in the match.").L10N("Client:Main:PlayerMissingMap"), sender, map.Name)); @@ -2471,7 +2220,7 @@ private void HandleMapUploadRequest(string sender, string mapHash) return; AddNotice( - string.Format(("{0} doesn't have the map '{1}' on their local installation. " + + string.Format(CultureInfo.CurrentCulture, ("{0} doesn't have the map '{1}' on their local installation. " + "Attempting to upload the map to the CnCNet map database.").L10N("Client:Main:UpdateMapToDBPrompt"), sender, map.Name)); @@ -2499,14 +2248,14 @@ private void HandleMapTransferFailMessage(string sender, string sha1) if (!IsHost) { AddNotice( - string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("Client:Main:PlayerDownloadMapFailed") + " " + + string.Format(CultureInfo.CurrentCulture, "{0} has failed to download the map from the CnCNet map database.".L10N("Client:Main:PlayerDownloadMapFailed") + " " + "The host needs to change the map or {0} won't be able to participate in this match.".L10N("Client:Main:HostNeedChangeMapForPlayer"), sender)); } else { AddNotice( - string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("Client:Main:PlayerDownloadMapFailed") + " " + + string.Format(CultureInfo.CurrentCulture, "{0} has failed to download the map from the CnCNet map database.".L10N("Client:Main:PlayerDownloadMapFailed") + " " + "You need to change the map or {0} won't be able to participate in this match.".L10N("Client:Main:YouNeedChangeMapForPlayer"), sender)); } @@ -2530,7 +2279,7 @@ private void HandleMapDownloadRequest(string sender, string sha1) private void HandleMapSharingBlockedMessage(string sender) { AddNotice( - string.Format("The selected map doesn't exist on {0}'s installation, and they " + + string.Format(CultureInfo.CurrentCulture, "The selected map doesn't exist on {0}'s installation, and they " + "have map sharing disabled in settings. The game host needs to change to a non-custom map or " + "they will be unable to participate in this match.".L10N("Client:Main:PlayerMissingMapDisabledSharing"), sender)); @@ -2587,6 +2336,7 @@ private void DownloadMapByIdCommand(string parameters) if (loadedMap != null) { message = string.Format( + CultureInfo.CurrentCulture, "The map for ID \"{0}\" is already loaded from \"{1}.map\", delete the existing file before trying again.".L10N("Client:Main:DownloadMapCommandSha1AlreadyExists"), sha1, loadedMap.Map.BaseFilePath); @@ -2604,7 +2354,11 @@ private void DownloadMapByIdCommand(string parameters) chatCommandDownloadedMaps.Add(sha1); - message = string.Format("Attempting to download map via chat command: sha1={0}, mapName={1}".L10N("Client:Main:DownloadMapCommandStartingDownload"), sha1, mapName); + message = string.Format( + CultureInfo.CurrentCulture, + "Attempting to download map via chat command: sha1={0}, mapName={1}".L10N("Client:Main:DownloadMapCommandStartingDownload"), + sha1, + mapName); Logger.Log(message); AddNotice(message); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs new file mode 100644 index 000000000..ced227b16 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using ClientCore; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal sealed class V3ConnectionState : IAsyncDisposable +{ + private const ushort MAX_REMOTE_PLAYERS = 7; + private const int PINNED_DYNAMIC_TUNNELS = 10; + + private readonly TunnelHandler tunnelHandler; + + private IPAddress publicIpV4Address; + private IPAddress publicIpV6Address; + private List p2pIpV6PortIds = new(); + private InternetGatewayDevice internetGatewayDevice; + + public List<(ushort InternalPort, ushort ExternalPort)> IpV6P2PPorts { get; private set; } = new(); + + public List<(ushort InternalPort, ushort ExternalPort)> IpV4P2PPorts { get; private set; } = new(); + + public List<(int Ping, string Hash)> PinnedTunnels { get; private set; } = new(); + + public string PinnedTunnelPingsMessage { get; private set; } + + public bool DynamicTunnelsEnabled { get; set; } + + public bool P2PEnabled { get; set; } + + public CnCNetTunnel InitialTunnel { get; private set; } + + public CancellationTokenSource StunCancellationTokenSource { get; private set; } + + public List<(string RemotePlayerName, CnCNetTunnel Tunnel, int CombinedPing)> PlayerTunnels { get; } = new(); + + public List<(List RemotePlayerNames, V3GameTunnelHandler Tunnel)> V3GameTunnelHandlers { get; } = new(); + + public List P2PPlayers { get; } = new(); + + public V3ConnectionState(TunnelHandler tunnelHandler) + { + this.tunnelHandler = tunnelHandler; + } + + public void Setup(CnCNetTunnel tunnel) + { + InitialTunnel = tunnel; + + if (!DynamicTunnelsEnabled) + { + tunnelHandler.CurrentTunnel = InitialTunnel; + } + else + { + tunnelHandler.CurrentTunnel = GetEligibleTunnels() + .MinBy(q => q.PingInMs); + } + } + + public void PinTunnels() + { + PinnedTunnels = GetEligibleTunnels() + .OrderBy(q => q.PingInMs) + .ThenBy(q => q.Hash, StringComparer.OrdinalIgnoreCase) + .Take(PINNED_DYNAMIC_TUNNELS) + .Select(q => (q.PingInMs, q.Hash)) + .ToList(); + + IEnumerable tunnelPings = PinnedTunnels + .Select(q => FormattableString.Invariant($"{q.Ping};{q.Hash}\t")); + + PinnedTunnelPingsMessage = string.Concat(tunnelPings); + } + + public async ValueTask HandlePlayerP2PRequestAsync() + { + if (!IpV6P2PPorts.Any() && !IpV4P2PPorts.Any()) + { + var p2pPorts = NetworkHelper.GetFreeUdpPorts(Array.Empty(), MAX_REMOTE_PLAYERS).ToList(); + + StunCancellationTokenSource?.Cancel(); + StunCancellationTokenSource?.Dispose(); + + StunCancellationTokenSource = new(); + + (internetGatewayDevice, IpV6P2PPorts, IpV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync( + internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? InitialTunnel.IPAddresses, StunCancellationTokenSource.Token); + } + + return publicIpV4Address is not null || publicIpV6Address is not null; + } + + public void RemoveV3Player(string playerName) + { + PlayerTunnels.Remove(PlayerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + P2PPlayers.Remove(P2PPlayers.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + } + + public string GetP2PRequestCommand() + => $" {publicIpV4Address}\t{(!IpV4P2PPorts.Any() ? null : IpV4P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}" + + $";{publicIpV6Address}\t{(!IpV6P2PPorts.Any() ? null : IpV6P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}"; + + public string GetP2PPingCommand(string playerName) + => $" {playerName}-{P2PPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)).LocalPingResults.Select(q => $"{q.RemoteIpAddress};{q.Ping}\t").Aggregate((q, r) => $"{q}{r}")}"; + + public async ValueTask ToggleP2PAsync() + { + P2PEnabled = !P2PEnabled; + + if (P2PEnabled) + return true; + + await CloseP2PPortsAsync(); + + internetGatewayDevice = null; + publicIpV4Address = null; + publicIpV6Address = null; + + return false; + } + + public async ValueTask PingRemotePlayer(string playerName, string p2pRequestMessage) + { + List<(IPAddress RemoteIpAddress, long Ping)> localPingResults = new(); + string[] splitLines = p2pRequestMessage.Split(';'); + string[] ipV4splitLines = splitLines[0].Split('\t'); + string[] ipV6splitLines = splitLines[1].Split('\t'); + + if (IPAddress.TryParse(ipV4splitLines[0], out IPAddress parsedIpV4Address)) + { + long? pingResult = await NetworkHelper.PingAsync(parsedIpV4Address); + + if (pingResult is not null) + localPingResults.Add((parsedIpV4Address, pingResult.Value)); + } + + if (IPAddress.TryParse(ipV6splitLines[0], out IPAddress parsedIpV6Address)) + { + long? pingResult = await NetworkHelper.PingAsync(parsedIpV6Address); + + if (pingResult is not null) + localPingResults.Add((parsedIpV6Address, pingResult.Value)); + } + + bool remotePlayerP2PEnabled = false; + ushort[] remotePlayerIpV4Ports = Array.Empty(); + ushort[] remotePlayerIpV6Ports = Array.Empty(); + P2PPlayer remoteP2PPlayer; + + if (parsedIpV4Address is not null) + { + remotePlayerP2PEnabled = true; + remotePlayerIpV4Ports = ipV4splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); + } + + if (parsedIpV6Address is not null) + { + remotePlayerP2PEnabled = true; + remotePlayerIpV6Ports = ipV6splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); + } + + if (P2PPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) + { + remoteP2PPlayer = P2PPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); + + P2PPlayers.Remove(remoteP2PPlayer); + } + else + { + remoteP2PPlayer = new(playerName, Array.Empty(), Array.Empty(), new(), new(), false); + } + + P2PPlayers.Add(remoteP2PPlayer with { LocalPingResults = localPingResults, RemoteIpV6Ports = remotePlayerIpV6Ports, RemoteIpV4Ports = remotePlayerIpV4Ports, Enabled = remotePlayerP2PEnabled }); + + return remotePlayerP2PEnabled; + } + + public bool UpdateRemotePingResults(string senderName, string p2pPingsMessage, string localPlayerName) + { + if (!P2PEnabled) + return false; + + string[] splitLines = p2pPingsMessage.Split('-'); + string pingPlayerName = splitLines[0]; + + if (!localPlayerName.Equals(pingPlayerName, StringComparison.OrdinalIgnoreCase)) + return false; + + string[] pingResults = splitLines[1].Split('\t', StringSplitOptions.RemoveEmptyEntries); + List<(IPAddress IpAddress, long Ping)> playerPings = new(); + + foreach (string pingResult in pingResults) + { + string[] ipAddressPingResult = pingResult.Split(';'); + + if (IPAddress.TryParse(ipAddressPingResult[0], out IPAddress ipV4Address)) + playerPings.Add((ipV4Address, long.Parse(ipAddressPingResult[1], CultureInfo.InvariantCulture))); + } + + P2PPlayer p2pPlayer; + + if (P2PPlayers.Any(q => q.RemotePlayerName.Equals(senderName, StringComparison.OrdinalIgnoreCase))) + { + p2pPlayer = P2PPlayers.Single(q => q.RemotePlayerName.Equals(senderName, StringComparison.OrdinalIgnoreCase)); + + P2PPlayers.Remove(p2pPlayer); + } + else + { + p2pPlayer = new(senderName, Array.Empty(), Array.Empty(), new(), new(), false); + } + + P2PPlayers.Add(p2pPlayer with { RemotePingResults = playerPings }); + + return !p2pPlayer.RemotePingResults.Any(); + } + + public void StartV3ConnectionListeners( + uint gameLocalPlayerId, + string localPlayerName, + List players, + Action remoteHostConnectedAction, + Action remoteHostConnectionFailedAction, + CancellationToken cancellationToken) + { + V3GameTunnelHandlers.Clear(); + + if (!DynamicTunnelsEnabled) + { + var gameTunnelHandler = new V3GameTunnelHandler(); + + gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); + gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); + + gameTunnelHandler.SetUp(new(tunnelHandler.CurrentTunnel.IPAddress, tunnelHandler.CurrentTunnel.Port), 0, gameLocalPlayerId, cancellationToken); + gameTunnelHandler.ConnectToTunnel(); + V3GameTunnelHandlers.Add(new(players.Where(q => !q.Name.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase)).Select(q => q.Name).ToList(), gameTunnelHandler)); + } + else + { + List p2pPlayerTunnels = new(); + + if (P2PEnabled) + { + foreach (var (remotePlayerName, remoteIpV6Ports, remoteIpV4Ports, localPingResults, remotePingResults, _) in P2PPlayers.Where(q => q.RemotePingResults.Any() && q.Enabled)) + { + (IPAddress selectedRemoteIpAddress, long combinedPing) = localPingResults + .Where(q => q.RemoteIpAddress is not null && remotePingResults + .Where(r => r.RemoteIpAddress is not null) + .Select(r => r.RemoteIpAddress.AddressFamily) + .Contains(q.RemoteIpAddress.AddressFamily)) + .Select(q => (q.RemoteIpAddress, q.Ping + remotePingResults.Single(r => r.RemoteIpAddress.AddressFamily == q.RemoteIpAddress.AddressFamily).Ping)) + .MaxBy(q => q.RemoteIpAddress.AddressFamily); + + if (combinedPing < PlayerTunnels.Single(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).CombinedPing) + { + ushort[] localPorts; + ushort[] remotePorts; + + if (selectedRemoteIpAddress.AddressFamily is AddressFamily.InterNetworkV6) + { + localPorts = IpV6P2PPorts.Select(q => q.InternalPort).ToArray(); + remotePorts = remoteIpV6Ports; + } + else + { + localPorts = IpV4P2PPorts.Select(q => q.InternalPort).ToArray(); + remotePorts = remoteIpV4Ports; + } + + var allPlayerNames = players.Select(q => q.Name).OrderBy(q => q, StringComparer.OrdinalIgnoreCase).ToList(); + var remotePlayerNames = allPlayerNames.Where(q => !q.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); + var tunnelClientPlayerNames = allPlayerNames.Where(q => !q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); + ushort localPort = localPorts[6 - remotePlayerNames.FindIndex(q => q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase))]; + ushort remotePort = remotePorts[6 - tunnelClientPlayerNames.FindIndex(q => q.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase))]; + var p2pLocalTunnelHandler = new V3GameTunnelHandler(); + + p2pLocalTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); + p2pLocalTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); + + p2pLocalTunnelHandler.SetUp(new(selectedRemoteIpAddress, remotePort), localPort, gameLocalPlayerId, cancellationToken); + p2pLocalTunnelHandler.ConnectToTunnel(); + V3GameTunnelHandlers.Add(new(new() { remotePlayerName }, p2pLocalTunnelHandler)); + p2pPlayerTunnels.Add(remotePlayerName); + } + } + } + + foreach (IGrouping tunnelGrouping in PlayerTunnels.Where(q => !p2pPlayerTunnels.Contains(q.RemotePlayerName, StringComparer.OrdinalIgnoreCase)).GroupBy(q => q.Tunnel)) + { + var gameTunnelHandler = new V3GameTunnelHandler(); + + gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); + gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); + + gameTunnelHandler.SetUp(new(tunnelGrouping.Key.IPAddress, tunnelGrouping.Key.Port), 0, gameLocalPlayerId, cancellationToken); + gameTunnelHandler.ConnectToTunnel(); + V3GameTunnelHandlers.Add(new(tunnelGrouping.Select(q => q.Name).ToList(), gameTunnelHandler)); + } + } + } + + public async ValueTask DisposeAsync() + { + PinnedTunnelPingsMessage = null; + StunCancellationTokenSource?.Cancel(); + V3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); + V3GameTunnelHandlers.Clear(); + PlayerTunnels.Clear(); + P2PPlayers.Clear(); + PinnedTunnels?.Clear(); + await CloseP2PPortsAsync(); + } + + private IEnumerable GetEligibleTunnels() + => tunnelHandler.Tunnels.Where(q => !q.RequiresPassword && q.PingInMs > -1 && q.Clients < q.MaxClients - 8 && q.Version is Constants.TUNNEL_VERSION_3); + + private async ValueTask CloseP2PPortsAsync() + { + try + { + if (internetGatewayDevice is not null) + { + foreach (ushort p2pPort in IpV4P2PPorts.Select(q => q.InternalPort)) + await internetGatewayDevice.CloseIpV4PortAsync(p2pPort); + } + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, "Could not close P2P IPV4 ports."); + } + finally + { + IpV4P2PPorts.Clear(); + } + + try + { + if (internetGatewayDevice is not null) + { + foreach (ushort p2pIpV6PortId in p2pIpV6PortIds) + await internetGatewayDevice.CloseIpV6PortAsync(p2pIpV6PortId); + } + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, "Could not close P2P IPV6 ports."); + } + finally + { + IpV6P2PPorts.Clear(); + p2pIpV6PortIds.Clear(); + } + } +} \ No newline at end of file From d1e55a80e27cbf7085d31e8faf3910c273eef9e0 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 13 Dec 2022 22:21:54 +0100 Subject: [PATCH 063/109] HttpClient handling --- DXMainClient/DXGUI/GameClass.cs | 1 - .../CnCNet/CnCNetPlayerCountTask.cs | 15 +-------- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 14 +-------- .../Domain/Multiplayer/CnCNet/Constants.cs | 28 ++++++++++++----- .../Domain/Multiplayer/CnCNet/MapSharer.cs | 25 ++------------- .../Multiplayer/CnCNet/TunnelHandler.cs | 17 ++-------- .../UPNP/Models/InternetGatewayDevice.cs | 31 ++++++++----------- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 29 ++++++++++------- 8 files changed, 58 insertions(+), 102 deletions(-) diff --git a/DXMainClient/DXGUI/GameClass.cs b/DXMainClient/DXGUI/GameClass.cs index f64232e18..47f6731f3 100644 --- a/DXMainClient/DXGUI/GameClass.cs +++ b/DXMainClient/DXGUI/GameClass.cs @@ -10,7 +10,6 @@ using Rampastring.Tools; using Rampastring.XNAUI; using System; -using ClientGUI; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index 0aee30c4d..80dfee02e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -1,7 +1,5 @@ using System; using System.Globalization; -using System.Net; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using ClientCore; @@ -50,18 +48,7 @@ private static async ValueTask GetCnCNetPlayerCountAsync(CancellationToken { try { - using var client = new HttpClient( - new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(15), - AutomaticDecompression = DecompressionMethods.All - }, - true) - { - DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher - }; - - string info = await client.GetStringAsync($"{Uri.UriSchemeHttps}://api.cncnet.org/status", cancellationToken); + string info = await Constants.CnCNetHttpClient.GetStringAsync($"{Uri.UriSchemeHttps}://api.cncnet.org/status", cancellationToken); info = info.Replace("{", string.Empty); info = info.Replace("}", string.Empty); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index b0cc5a990..19054efb9 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.Net; -using System.Net.Http; using System.Net.Sockets; using System.Threading.Tasks; using ClientCore; @@ -166,18 +165,7 @@ public async ValueTask> GetPlayerPortInfoAsync(int playerCount) Logger.Log($"Contacting tunnel at {addressString}"); - using var client = new HttpClient( - new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(15), - AutomaticDecompression = DecompressionMethods.All - }, - true) - { - Timeout = TimeSpan.FromMilliseconds(Constants.TUNNEL_CONNECTION_TIMEOUT), - DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher - }; - string data = await client.GetStringAsync(addressString); + string data = await Constants.CnCNetHttpClient.GetStringAsync(addressString); data = data.Replace("[", string.Empty); data = data.Replace("]", string.Empty); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs index c13e80653..28b90ea0e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Constants.cs @@ -1,10 +1,24 @@ -namespace DTAClient.Domain.Multiplayer.CnCNet +using System; +using System.Net; +using System.Net.Http; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal static class Constants { - internal static class Constants - { - internal const int TUNNEL_CONNECTION_TIMEOUT = 10000; // In milliseconds + public static HttpClient CnCNetHttpClient + => new( + new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + AutomaticDecompression = DecompressionMethods.All + }, + true) + { + Timeout = TimeSpan.FromSeconds(10), + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher + }; - internal const int TUNNEL_VERSION_2 = 2; - internal const int TUNNEL_VERSION_3 = 3; - } + internal const int TUNNEL_VERSION_2 = 2; + internal const int TUNNEL_VERSION_3 = 3; } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs index 12358ea45..b2b221891 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs @@ -4,7 +4,6 @@ using System.IO; using System.IO.Compression; using System.Linq; -using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; @@ -134,7 +133,6 @@ private static async ValueTask UploadAsync(Map map, string myGameId) private static async ValueTask UploadFilesAsync(List files, NameValueCollection values) { - using HttpClient client = GetHttpClient(); var multipartFormDataContent = new MultipartFormDataContent(); // Write the values @@ -153,27 +151,11 @@ private static async ValueTask UploadFilesAsync(List files multipartFormDataContent.Add(streamContent, file.Name, file.Filename); } - HttpResponseMessage httpResponseMessage = await client.PostAsync("upload", multipartFormDataContent); + HttpResponseMessage httpResponseMessage = await Constants.CnCNetHttpClient.PostAsync($"{MAPDB_URL}upload", multipartFormDataContent); return await httpResponseMessage.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); } - private static HttpClient GetHttpClient() - { - return new HttpClient( - new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(15), - AutomaticDecompression = DecompressionMethods.All - }, - true) - { - Timeout = TimeSpan.FromMilliseconds(10000), - BaseAddress = new Uri(MAPDB_URL), - DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher - }; - } - private static MemoryStream CreateZipFile(string file) { var zipStream = new MemoryStream(1024); @@ -254,14 +236,13 @@ public static string GetMapFileName(string sha1, string mapName) string customMapsDirectory = SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "Maps", "Custom"); string mapFileName = GetMapFileName(sha1, mapName); string newFile = SafePath.CombineFilePath(customMapsDirectory, FormattableString.Invariant($"{mapFileName}.map")); - using HttpClient client = GetHttpClient(); Stream stream; try { - string address = FormattableString.Invariant($"{myGame}/{sha1}.zip"); + string address = FormattableString.Invariant($"{MAPDB_URL}{myGame}/{sha1}.zip"); Logger.Log($"MapSharer: Downloading URL: {MAPDB_URL}{address})"); - stream = await client.GetStreamAsync(address); + stream = await Constants.CnCNetHttpClient.GetStreamAsync(address); } catch (Exception ex) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 14c66e2b9..d0117467d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Net.Http; using System.Threading.Tasks; using ClientCore; @@ -146,32 +145,20 @@ private static async ValueTask> DoRefreshTunnelsAsync() { FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); var returnValue = new List(); - using var client = new HttpClient( - new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(15), - AutomaticDecompression = DecompressionMethods.All - }, - true) - { - Timeout = TimeSpan.FromSeconds(100), - DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher - }; - string data; Logger.Log("Fetching tunnel server info."); try { - data = await client.GetStringAsync(ProgramConstants.CNCNET_TUNNEL_LIST_URL); + data = await Constants.CnCNetHttpClient.GetStringAsync(ProgramConstants.CNCNET_TUNNEL_LIST_URL); } catch (HttpRequestException ex) { ProgramConstants.LogException(ex, "Error when downloading tunnel server info. Retrying."); try { - data = await client.GetStringAsync(ProgramConstants.CNCNET_TUNNEL_LIST_URL); + data = await Constants.CnCNetHttpClient.GetStringAsync(ProgramConstants.CNCNET_TUNNEL_LIST_URL); } catch (HttpRequestException ex1) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs index 94aa0a639..6e7db9712 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs @@ -30,27 +30,22 @@ internal sealed record InternetGatewayDevice(IEnumerable Locations, string private const ushort IanaUdpProtocolNumber = 17; private const string PortMappingDescription = "CnCNet"; - private static readonly HttpClient HttpClient; + private static readonly HttpClient HttpClient = new( + new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + SslOptions = new() + { + RemoteCertificateValidationCallback = (_, _, _, sslPolicyErrors) => (sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) == 0, + } + }, true) + { + Timeout = TimeSpan.FromMilliseconds(ReceiveTimeout), + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher + }; public const string UPnPInternetGatewayDevice = "urn:schemas-upnp-org:device:InternetGatewayDevice"; - static InternetGatewayDevice() - { - HttpClient = new HttpClient( - new SocketsHttpHandler - { - AutomaticDecompression = DecompressionMethods.All, - SslOptions = new() - { - RemoteCertificateValidationCallback = (_, _, _, sslPolicyErrors) => (sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) == 0, - } - }, true) - { - Timeout = TimeSpan.FromMilliseconds(ReceiveTimeout), - DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher - }; - } - public async ValueTask OpenIpV4PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) { Logger.Log($"Opening IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 223f9fb0f..20d6dcfc2 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -8,6 +8,7 @@ using System.Net; using System.Net.Http; using System.Net.NetworkInformation; +using System.Net.Security; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Runtime.Serialization; @@ -26,6 +27,21 @@ internal static class UPnPHandler private const int ReceiveTimeout = 2000; private const int SendCount = 3; + private static readonly HttpClient HttpClient = new( + new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + SslOptions = new() + { + RemoteCertificateValidationCallback = (_, _, _, sslPolicyErrors) => (sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) == 0, + } + }, + true) + { + Timeout = TimeSpan.FromMilliseconds(ReceiveTimeout), + DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher + }; + private static IReadOnlyDictionary SsdpMultiCastAddresses => new Dictionary { [AddressType.IpV4SiteLocal] = IPAddress.Parse("239.255.255.250"), @@ -369,18 +385,7 @@ private static async ValueTask ReceiveAsync(Socket socket, ICollection r private static async ValueTask GetUPnPDescription(Uri uri, CancellationToken cancellationToken) { - using var client = new HttpClient( - new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(15), - AutomaticDecompression = DecompressionMethods.All - }, true) - { - Timeout = TimeSpan.FromMilliseconds(ReceiveTimeout), - DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher - }; - - await using Stream uPnPDescription = await client.GetStreamAsync(uri, cancellationToken); + await using Stream uPnPDescription = await HttpClient.GetStreamAsync(uri, cancellationToken); using var xmlTextReader = new XmlTextReader(uPnPDescription); return (UPnPDescription)new DataContractSerializer(typeof(UPnPDescription)).ReadObject(xmlTextReader); From 2b0d57559d0b6e31c958d2d89b622af1fdbf3302 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 14 Dec 2022 17:36:41 +0100 Subject: [PATCH 064/109] Apply ConfigureAwait() --- ClientCore/Extensions/TaskExtensions.cs | 25 +- ClientCore/SavedGameManager.cs | 2 +- .../GameParsers/LogFileStatisticsParser.cs | 4 +- ClientGUI/GameProcessLogic.cs | 2 +- .../DXGUI/Generic/CampaignSelector.cs | 64 ++--- .../DXGUI/Generic/GameLoadingWindow.cs | 42 ++-- DXMainClient/DXGUI/Generic/MainMenu.cs | 12 +- .../DXGUI/Generic/StatisticsWindow.cs | 2 +- DXMainClient/DXGUI/Generic/TopBar.cs | 12 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 59 ++--- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 62 ++--- .../Multiplayer/CnCNet/GlobalContextMenu.cs | 4 +- .../CnCNet/PrivateMessagingWindow.cs | 2 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 30 +-- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 218 +++++++++--------- .../Multiplayer/GameLobby/GameLobbyBase.cs | 28 +-- .../Multiplayer/GameLobby/LANGameLobby.cs | 105 +++++---- .../Multiplayer/GameLobby/MapPreviewBox.cs | 13 +- .../GameLobby/MultiplayerGameLobby.cs | 100 +++----- .../Multiplayer/GameLobby/SkirmishLobby.cs | 8 +- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 48 ++-- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 35 +-- .../CnCNet/CnCNetPlayerCountTask.cs | 6 +- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 6 +- .../Domain/Multiplayer/CnCNet/MapSharer.cs | 14 +- .../Multiplayer/CnCNet/TunnelHandler.cs | 14 +- .../{Models => }/InternetGatewayDevice.cs | 121 ++++++---- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 102 +++++--- .../Multiplayer/CnCNet/V3ConnectionState.cs | 15 +- .../CnCNet/V3LocalPlayerConnection.cs | 6 +- .../CnCNet/V3RemotePlayerConnection.cs | 9 +- .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 14 +- DXMainClient/Domain/Multiplayer/Map.cs | 11 +- DXMainClient/Domain/Multiplayer/MapLoader.cs | 27 ++- .../Domain/Multiplayer/MapPreviewExtractor.cs | 46 ++-- .../Domain/Multiplayer/NetworkHelper.cs | 18 +- DXMainClient/Domain/SavedGame.cs | 2 +- DXMainClient/Online/Channel.cs | 8 +- DXMainClient/Online/CnCNetGameCheck.cs | 2 +- DXMainClient/Online/CnCNetManager.cs | 8 +- DXMainClient/Online/Connection.cs | 63 ++--- DXMainClient/Startup.cs | 18 +- 42 files changed, 731 insertions(+), 656 deletions(-) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/{Models => }/InternetGatewayDevice.cs (78%) diff --git a/ClientCore/Extensions/TaskExtensions.cs b/ClientCore/Extensions/TaskExtensions.cs index 1094a7263..192941f78 100644 --- a/ClientCore/Extensions/TaskExtensions.cs +++ b/ClientCore/Extensions/TaskExtensions.cs @@ -10,12 +10,13 @@ public static class TaskExtensions /// Runs a and guarantees all exceptions are caught and handled even when the is not directly awaited. /// /// The who's exceptions will be handled. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. /// Returns a that awaited and handled the original . - public static async Task HandleTask(this Task task) + public static async Task HandleTask(this Task task, bool continueOnCapturedContext = false) { try { - await task; + await task.ConfigureAwait(continueOnCapturedContext); } catch (Exception ex) { @@ -28,12 +29,13 @@ public static async Task HandleTask(this Task task) /// /// The type of 's return value. /// The who's exceptions will be handled. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. /// Returns a that awaited and handled the original . - public static async Task HandleTask(this Task task) + public static async Task HandleTask(this Task task, bool continueOnCapturedContext = false) { try { - return await task; + return await task.ConfigureAwait(continueOnCapturedContext); } catch (Exception ex) { @@ -49,14 +51,15 @@ public static async Task HandleTask(this Task task) /// /// The type of 's return value. /// The list of s who's exceptions will be handled. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. /// Returns a that awaited and handled the original . - public static async Task WhenAllSafe(IEnumerable> tasks) + public static async Task WhenAllSafe(IEnumerable> tasks, bool continueOnCapturedContext = false) { var whenAllTask = Task.WhenAll(tasks); try { - return await whenAllTask; + return await whenAllTask.ConfigureAwait(continueOnCapturedContext); } catch { @@ -72,14 +75,15 @@ public static async Task WhenAllSafe(IEnumerable> tasks) /// When using only the first thrown exception from a single may be observed. /// /// The list of s who's exceptions will be handled. + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. /// Returns a that awaited and handled the original . - public static async Task WhenAllSafe(IEnumerable tasks) + public static async Task WhenAllSafe(IEnumerable tasks, bool continueOnCapturedContext = false) { var whenAllTask = Task.WhenAll(tasks); try { - await whenAllTask; + await whenAllTask.ConfigureAwait(continueOnCapturedContext); } catch { @@ -94,11 +98,12 @@ public static async Task WhenAllSafe(IEnumerable tasks) /// Runs a and guarantees all exceptions are caught and handled even when the is not directly awaited. /// /// The who's exceptions will be handled. - public static async void HandleTask(this ValueTask task) + /// true to attempt to marshal the continuation back to the original context captured; otherwise, false. + public static async void HandleTask(this ValueTask task, bool continueOnCapturedContext = false) { try { - await task; + await task.ConfigureAwait(continueOnCapturedContext); } catch (Exception ex) { diff --git a/ClientCore/SavedGameManager.cs b/ClientCore/SavedGameManager.cs index 855d147eb..d5bc99d84 100644 --- a/ClientCore/SavedGameManager.cs +++ b/ClientCore/SavedGameManager.cs @@ -150,7 +150,7 @@ public static async ValueTask RenameSavedGameAsync() return; } - await Task.Delay(250); + await Task.Delay(250).ConfigureAwait(false); } saveRenameInProgress = false; diff --git a/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs b/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs index 5067e130e..99e7b6c2f 100644 --- a/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs +++ b/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs @@ -67,7 +67,7 @@ protected override void ParseStatistics(string gamepath) // The player has been taken over by an AI during the match Logger.Log("Losing take-over AI found"); takeoverAIs.Add(new PlayerStatistics("Computer", false, true, false, 0, 10, 255, 1)); - currentPlayer = takeoverAIs[takeoverAIs.Count - 1]; + currentPlayer = takeoverAIs[^1]; } if (currentPlayer != null) @@ -91,7 +91,7 @@ protected override void ParseStatistics(string gamepath) // The player has been taken over by an AI during the match Logger.Log("Winning take-over AI found"); takeoverAIs.Add(new PlayerStatistics("Computer", false, true, false, 0, 10, 255, 1)); - currentPlayer = takeoverAIs[takeoverAIs.Count - 1]; + currentPlayer = takeoverAIs[^1]; } if (currentPlayer != null) diff --git a/ClientGUI/GameProcessLogic.cs b/ClientGUI/GameProcessLogic.cs index 203dc4855..d7e1e66d5 100644 --- a/ClientGUI/GameProcessLogic.cs +++ b/ClientGUI/GameProcessLogic.cs @@ -36,7 +36,7 @@ public static async ValueTask StartGameProcessAsync(WindowManager windowManager) int waitTimes = 0; while (PreprocessorBackgroundTask.Instance.IsRunning) { - await Task.Delay(1000); + await Task.Delay(1000).ConfigureAwait(false); waitTimes++; if (waitTimes > 10) { diff --git a/DXMainClient/DXGUI/Generic/CampaignSelector.cs b/DXMainClient/DXGUI/Generic/CampaignSelector.cs index c2c3d8518..6b568bece 100644 --- a/DXMainClient/DXGUI/Generic/CampaignSelector.cs +++ b/DXMainClient/DXGUI/Generic/CampaignSelector.cs @@ -241,7 +241,7 @@ private async ValueTask BtnLaunch_LeftClickAsync() return; } - await LaunchMissionAsync(mission); + await LaunchMissionAsync(mission).ConfigureAwait(false); } private bool AreFilesModified() @@ -263,43 +263,47 @@ private async ValueTask LaunchMissionAsync(Mission mission) bool copyMapsToSpawnmapINI = ClientConfiguration.Instance.CopyMissionsToSpawnmapINI; Logger.Log($"About to write {ProgramConstants.SPAWNER_SETTINGS}."); - using var spawnStreamWriter = new StreamWriter(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS)); - spawnStreamWriter.WriteLine("; Generated by DTA Client"); - spawnStreamWriter.WriteLine("[Settings]"); - if (copyMapsToSpawnmapINI) - spawnStreamWriter.WriteLine($"Scenario={ProgramConstants.SPAWNMAP_INI}"); - else - spawnStreamWriter.WriteLine("Scenario=" + mission.Scenario); + var spawnStreamWriter = new StreamWriter(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS)); - // No one wants to play missions on Fastest, so we'll change it to Faster - if (UserINISettings.Instance.GameSpeed == 0) - UserINISettings.Instance.GameSpeed.Value = 1; + await using (spawnStreamWriter.ConfigureAwait(false)) + { + await spawnStreamWriter.WriteLineAsync("; Generated by DTA Client").ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("[Settings]").ConfigureAwait(false); + if (copyMapsToSpawnmapINI) + await spawnStreamWriter.WriteLineAsync($"Scenario={ProgramConstants.SPAWNMAP_INI}").ConfigureAwait(false); + else + await spawnStreamWriter.WriteLineAsync("Scenario=" + mission.Scenario).ConfigureAwait(false); - spawnStreamWriter.WriteLine("CampaignID=" + mission.Index); - spawnStreamWriter.WriteLine("GameSpeed=" + UserINISettings.Instance.GameSpeed); + // No one wants to play missions on Fastest, so we'll change it to Faster + if (UserINISettings.Instance.GameSpeed == 0) + UserINISettings.Instance.GameSpeed.Value = 1; + + await spawnStreamWriter.WriteLineAsync("CampaignID=" + mission.Index).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("GameSpeed=" + UserINISettings.Instance.GameSpeed).ConfigureAwait(false); #if YR || ARES - spawnStreamWriter.WriteLine("Ra2Mode=" + !mission.RequiredAddon); + await spawnStreamWriter.WriteLineAsync("Ra2Mode=" + !mission.RequiredAddon).ConfigureAwait(false); #else - spawnStreamWriter.WriteLine("Firestorm=" + mission.RequiredAddon); + await spawnStreamWriter.WriteLineAsync("Firestorm=" + mission.RequiredAddon).ConfigureAwait(false); #endif - spawnStreamWriter.WriteLine("CustomLoadScreen=" + LoadingScreenController.GetLoadScreenName(mission.Side.ToString())); - spawnStreamWriter.WriteLine("IsSinglePlayer=Yes"); - spawnStreamWriter.WriteLine("SidebarHack=" + ClientConfiguration.Instance.SidebarHack); - spawnStreamWriter.WriteLine("Side=" + mission.Side); - spawnStreamWriter.WriteLine("BuildOffAlly=" + mission.BuildOffAlly); - - UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value; - - spawnStreamWriter.WriteLine("DifficultyModeHuman=" + (mission.PlayerAlwaysOnNormalDifficulty ? "1" : trbDifficultySelector.Value.ToString())); - spawnStreamWriter.WriteLine("DifficultyModeComputer=" + GetComputerDifficulty()); + await spawnStreamWriter.WriteLineAsync("Firestorm=" + mission.RequiredAddon).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("CustomLoadScreen=" + LoadingScreenController.GetLoadScreenName(mission.Side.ToString())).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("IsSinglePlayer=Yes").ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("SidebarHack=" + ClientConfiguration.Instance.SidebarHack).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("Side=" + mission.Side).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("BuildOffAlly=" + mission.BuildOffAlly).ConfigureAwait(false); + + UserINISettings.Instance.Difficulty.Value = trbDifficultySelector.Value; + + await spawnStreamWriter.WriteLineAsync("DifficultyModeHuman=" + (mission.PlayerAlwaysOnNormalDifficulty ? "1" : trbDifficultySelector.Value.ToString())).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("DifficultyModeComputer=" + GetComputerDifficulty()).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync().ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync().ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync().ConfigureAwait(false); + } var difficultyIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, DifficultyIniPaths[trbDifficultySelector.Value])); string difficultyName = DifficultyNames[trbDifficultySelector.Value]; - spawnStreamWriter.WriteLine(); - spawnStreamWriter.WriteLine(); - spawnStreamWriter.WriteLine(); - if (copyMapsToSpawnmapINI) { var mapIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, mission.Scenario)); @@ -315,7 +319,7 @@ private async ValueTask LaunchMissionAsync(Mission mission) discordHandler.UpdatePresence(mission.UntranslatedGUIName, difficultyName, mission.IconPath, true); GameProcessLogic.GameProcessExited += GameProcessExited_Callback; - await GameProcessLogic.StartGameProcessAsync(WindowManager); + await GameProcessLogic.StartGameProcessAsync(WindowManager).ConfigureAwait(false); } private int GetComputerDifficulty() => diff --git a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs index 5c565038c..6c1051609 100644 --- a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs @@ -111,35 +111,43 @@ private async ValueTask BtnLaunch_LeftClickAsync() if (spawnerSettingsFile.Exists) spawnerSettingsFile.Delete(); - using StreamWriter spawnStreamWriter = new StreamWriter(spawnerSettingsFile.FullName); - spawnStreamWriter.WriteLine("; generated by DTA Client"); - spawnStreamWriter.WriteLine("[Settings]"); - spawnStreamWriter.WriteLine($"Scenario={ProgramConstants.SPAWNMAP_INI}"); - spawnStreamWriter.WriteLine("SaveGameName=" + sg.FileName); - spawnStreamWriter.WriteLine("LoadSaveGame=Yes"); - spawnStreamWriter.WriteLine("SidebarHack=" + ClientConfiguration.Instance.SidebarHack); - spawnStreamWriter.WriteLine("CustomLoadScreen=" + LoadingScreenController.GetLoadScreenName("g")); - spawnStreamWriter.WriteLine("Firestorm=No"); - spawnStreamWriter.WriteLine("GameSpeed=" + UserINISettings.Instance.GameSpeed); - spawnStreamWriter.WriteLine(); + var spawnStreamWriter = new StreamWriter(spawnerSettingsFile.FullName); + + await using (spawnStreamWriter.ConfigureAwait(false)) + { + await spawnStreamWriter.WriteLineAsync("; generated by DTA Client").ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("[Settings]").ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync($"Scenario={ProgramConstants.SPAWNMAP_INI}").ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("SaveGameName=" + sg.FileName).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("LoadSaveGame=Yes").ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("SidebarHack=" + ClientConfiguration.Instance.SidebarHack).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("CustomLoadScreen=" + LoadingScreenController.GetLoadScreenName("g")).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("Firestorm=No").ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync("GameSpeed=" + UserINISettings.Instance.GameSpeed).ConfigureAwait(false); + await spawnStreamWriter.WriteLineAsync().ConfigureAwait(false); + } FileInfo spawnMapIniFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI); if (spawnMapIniFile.Exists) spawnMapIniFile.Delete(); - using StreamWriter spawnMapStreamWriter = new StreamWriter(spawnMapIniFile.FullName); - spawnMapStreamWriter.WriteLine("[Map]"); - spawnMapStreamWriter.WriteLine("Size=0,0,50,50"); - spawnMapStreamWriter.WriteLine("LocalSize=0,0,50,50"); - spawnMapStreamWriter.WriteLine(); + var spawnMapStreamWriter = new StreamWriter(spawnMapIniFile.FullName); + + await using (spawnStreamWriter.ConfigureAwait(false)) + { + await spawnMapStreamWriter.WriteLineAsync("[Map]").ConfigureAwait(false); + await spawnMapStreamWriter.WriteLineAsync("Size=0,0,50,50").ConfigureAwait(false); + await spawnMapStreamWriter.WriteLineAsync("LocalSize=0,0,50,50").ConfigureAwait(false); + await spawnMapStreamWriter.WriteLineAsync().ConfigureAwait(false); + } discordHandler.UpdatePresence(sg.GUIName, true); Enabled = false; GameProcessLogic.GameProcessExited += GameProcessExited_Callback; - await GameProcessLogic.StartGameProcessAsync(WindowManager); + await GameProcessLogic.StartGameProcessAsync(WindowManager).ConfigureAwait(false); } private void BtnDelete_LeftClick(object sender, EventArgs e) diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 14b100c2b..8b4ee3f93 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -535,7 +535,7 @@ private async ValueTask CleanAsync() Updater.StopUpdate(); if (connectionManager.IsConnected) - await connectionManager.DisconnectAsync(); + await connectionManager.DisconnectAsync().ConfigureAwait(false); } /// @@ -829,13 +829,13 @@ private void BtnLoadGame_LeftClick(object sender, EventArgs e) private async ValueTask BtnLan_LeftClickAsync() { - await lanLobby.OpenAsync(); + await lanLobby.OpenAsync().ConfigureAwait(false); if (UserINISettings.Instance.StopMusicOnMenu) MusicOff(); if (connectionManager.IsConnected) - await connectionManager.DisconnectAsync(); + await connectionManager.DisconnectAsync().ConfigureAwait(false); topBar.SetLanMode(true); } @@ -968,7 +968,7 @@ private async ValueTask FadeMusicExitAsync() { if (!isMediaPlayerAvailable || themeSong == null) { - await ExitClientAsync(); + await ExitClientAsync().ConfigureAwait(false); return; } @@ -982,7 +982,7 @@ private async ValueTask FadeMusicExitAsync() else { MediaPlayer.Stop(); - await ExitClientAsync(); + await ExitClientAsync().ConfigureAwait(false); } } @@ -992,7 +992,7 @@ private async ValueTask ExitClientAsync() WindowManager.CloseGame(); themeSong?.Dispose(); #if !XNA - await Task.Delay(1000); + await Task.Delay(1000).ConfigureAwait(false); Environment.Exit(0); #endif } diff --git a/DXMainClient/DXGUI/Generic/StatisticsWindow.cs b/DXMainClient/DXGUI/Generic/StatisticsWindow.cs index d41835426..52f33cd74 100644 --- a/DXMainClient/DXGUI/Generic/StatisticsWindow.cs +++ b/DXMainClient/DXGUI/Generic/StatisticsWindow.cs @@ -515,7 +515,7 @@ private void LbGameList_SelectedIndexChanged(object sender, EventArgs e) XNAListBoxItem spectatorItem = new XNAListBoxItem(); spectatorItem.Text = "Spectator".L10N("Client:Main:Spectator"); spectatorItem.TextColor = textColor; - spectatorItem.Texture = sideTextures[sideTextures.Length - 1]; + spectatorItem.Texture = sideTextures[^1]; items.Add(spectatorItem); items.Add(new XNAListBoxItem("-", textColor)); } diff --git a/DXMainClient/DXGUI/Generic/TopBar.cs b/DXMainClient/DXGUI/Generic/TopBar.cs index 0ae31b86c..531926141 100644 --- a/DXMainClient/DXGUI/Generic/TopBar.cs +++ b/DXMainClient/DXGUI/Generic/TopBar.cs @@ -94,7 +94,7 @@ public void AddPrimarySwitchable(ISwitchable switchable) public void RemovePrimarySwitchable(ISwitchable switchable) { primarySwitches.Remove(switchable); - btnMainButton.Text = primarySwitches[primarySwitches.Count - 1].GetSwitchName() + " (F2)"; + btnMainButton.Text = primarySwitches[^1].GetSwitchName() + " (F2)"; } public void SetSecondarySwitch(ISwitchable switchable) @@ -292,7 +292,7 @@ private void ConnectionEvent(string text) private async ValueTask BtnLogout_LeftClickAsync() { - await connectionManager.DisconnectAsync(); + await connectionManager.DisconnectAsync().ConfigureAwait(false); LogoutEvent?.Invoke(this, null); SwitchToPrimary(); } @@ -304,7 +304,7 @@ public void SwitchToPrimary() => BtnMainButton_LeftClick(this, EventArgs.Empty); public ISwitchable GetTopMostPrimarySwitchable() - => primarySwitches[primarySwitches.Count - 1]; + => primarySwitches[^1]; public void SwitchToSecondary() => BtnCnCNetLobby_LeftClick(this, EventArgs.Empty); @@ -312,7 +312,7 @@ public void SwitchToSecondary() private void BtnCnCNetLobby_LeftClick(object sender, EventArgs e) { LastSwitchType = SwitchType.SECONDARY; - primarySwitches[primarySwitches.Count - 1].SwitchOff(); + primarySwitches[^1].SwitchOff(); cncnetLobbySwitch.SwitchOn(); privateMessageSwitch.SwitchOff(); @@ -326,11 +326,11 @@ private void BtnMainButton_LeftClick(object sender, EventArgs e) LastSwitchType = SwitchType.PRIMARY; cncnetLobbySwitch.SwitchOff(); privateMessageSwitch.SwitchOff(); - primarySwitches[primarySwitches.Count - 1].SwitchOn(); + primarySwitches[^1].SwitchOn(); // HACK warning // TODO: add a way for DarkeningPanel to skip transitions - if (((XNAControl)primarySwitches[primarySwitches.Count - 1]).Parent is DarkeningPanel darkeningPanel) + if (((XNAControl)primarySwitches[^1]).Parent is DarkeningPanel darkeningPanel) darkeningPanel.Alpha = 1.0f; } diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index 74644be91..c5544c1dc 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -172,7 +172,7 @@ public async ValueTask ClearAsync() if (channel != null) { // TODO leave channel only if we've joined the channel - await channel.LeaveAsync(); + await channel.LeaveAsync().ConfigureAwait(false); channel.MessageAdded -= Channel_MessageAdded; channel.UserAdded -= channel_UserAddedFunc; @@ -188,7 +188,7 @@ public async ValueTask ClearAsync() Enabled = false; Visible = false; - await base.LeaveGameAsync(); + await base.LeaveGameAsync().ConfigureAwait(false); } tunnelHandler.CurrentTunnel = null; @@ -220,12 +220,12 @@ public async ValueTask OnJoinedAsync() { await connectionManager.SendCustomMessageAsync(new QueuedMessage( FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} +{IRCChannelModes.DEFAULT} {channel.Password} {SGPlayers.Count}"), - QueuedMessageType.SYSTEM_MESSAGE, 50)); + QueuedMessageType.SYSTEM_MESSAGE, 50)).ConfigureAwait(false); await connectionManager.SendCustomMessageAsync(new QueuedMessage( string.Format(IRCCommands.TOPIC + " {0} :{1}", channel.ChannelName, ProgramConstants.CNCNET_PROTOCOL_REVISION + ";" + localGame.ToLower()), - QueuedMessageType.SYSTEM_MESSAGE, 50)); + QueuedMessageType.SYSTEM_MESSAGE, 50)).ConfigureAwait(false); gameFilesHash = fhc.GetCompleteHash(); @@ -235,9 +235,10 @@ await connectionManager.SendCustomMessageAsync(new QueuedMessage( } else { - await channel.SendCTCPMessageAsync(CnCNetCommands.FILE_HASH + " " + fhc.GetCompleteHash(), QueuedMessageType.SYSTEM_MESSAGE, 10); - - await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_PING + " " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync( + CnCNetCommands.FILE_HASH + " " + fhc.GetCompleteHash(), QueuedMessageType.SYSTEM_MESSAGE, 10).ConfigureAwait(false); + await channel.SendCTCPMessageAsync( + CnCNetCommands.TUNNEL_PING + " " + tunnelHandler.CurrentTunnel.PingInMs, QueuedMessageType.SYSTEM_MESSAGE, 10).ConfigureAwait(false); if (tunnelHandler.CurrentTunnel.PingInMs < 0) AddNotice(string.Format("{0} - unknown ping to tunnel server.".L10N("Client:Main:PlayerUnknownPing"), ProgramConstants.PLAYERNAME)); @@ -260,14 +261,14 @@ private async ValueTask Channel_UserAddedAsync(ChannelUserEventArgs e) sndJoinSound.Play(); - await BroadcastOptionsAsync(); + await BroadcastOptionsAsync().ConfigureAwait(false); CopyPlayerDataToUI(); UpdateDiscordPresence(); } private async ValueTask OnPlayerLeftAsync(UserNameEventArgs e) { - await RemovePlayerAsync(e.UserName); + await RemovePlayerAsync(e.UserName).ConfigureAwait(false); UpdateDiscordPresence(); } @@ -289,7 +290,7 @@ private async ValueTask RemovePlayerAsync(string playerName) connectionManager.MainChannel.AddMessage(new ChatMessage( Color.Yellow, "The game host left the game!".L10N("Client:Main:HostLeft"))); - await ClearAsync(); + await ClearAsync().ConfigureAwait(false); } } @@ -323,7 +324,7 @@ protected override async ValueTask BroadcastOptionsAsync() } message.Remove(message.Length - 1, 1); - await channel.SendCTCPMessageAsync(message.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 10); + await channel.SendCTCPMessageAsync(message.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 10).ConfigureAwait(false); } protected override ValueTask SendChatMessageAsync(string message) @@ -338,22 +339,22 @@ protected override ValueTask RequestReadyStatusAsync() => protected override async ValueTask GetReadyNotificationAsync() { - await base.GetReadyNotificationAsync(); + await base.GetReadyNotificationAsync().ConfigureAwait(false); topBar.SwitchToPrimary(); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.GET_READY, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.GET_READY, QueuedMessageType.GAME_GET_READY_MESSAGE, 0).ConfigureAwait(false); } protected override async ValueTask NotAllPresentNotificationAsync() { - await base.NotAllPresentNotificationAsync(); + await base.NotAllPresentNotificationAsync().ConfigureAwait(false); if (IsHost) { await channel.SendCTCPMessageAsync(CnCNetCommands.NOT_ALL_PLAYERS_PRESENT, - QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0).ConfigureAwait(false); } } @@ -365,7 +366,7 @@ private async ValueTask TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArg await channel.SendCTCPMessageAsync( $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Hash}", QueuedMessageType.SYSTEM_MESSAGE, - 10); + 10).ConfigureAwait(false); HandleTunnelServerChange(e.Tunnel); } @@ -376,7 +377,7 @@ private async ValueTask HandleGetReadyNotificationAsync(string sender) if (sender != hostName) return; - await GetReadyNotificationAsync(); + await GetReadyNotificationAsync().ConfigureAwait(false); } private async ValueTask HandleNotAllPresentNotificationAsync(string sender) @@ -384,7 +385,7 @@ private async ValueTask HandleNotAllPresentNotificationAsync(string sender) if (sender != hostName) return; - await NotAllPresentNotificationAsync(); + await NotAllPresentNotificationAsync().ConfigureAwait(false); } private async ValueTask HandleFileHashCommandAsync(string sender, string fileHash) @@ -401,7 +402,7 @@ private async ValueTask HandleFileHashCommandAsync(string sender, string fileHas pInfo.Verified = true; - await HandleCheaterNotificationAsync(hostName, sender); // This is kinda hacky + await HandleCheaterNotificationAsync(hostName, sender).ConfigureAwait(false); // This is kinda hacky } } @@ -413,7 +414,7 @@ private async ValueTask HandleCheaterNotificationAsync(string sender, string che AddNotice(string.Format("{0} - modified files detected! They could be cheating!".L10N("Client:Main:PlayerCheating"), cheaterName), Color.Red); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.INVALID_FILE_HASH + " " + cheaterName, QueuedMessageType.SYSTEM_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.INVALID_FILE_HASH + " " + cheaterName, QueuedMessageType.SYSTEM_MESSAGE, 0).ConfigureAwait(false); } private void HandleTunnelPing(string sender, int pingInMs) @@ -445,7 +446,7 @@ private async ValueTask HandleOptionsMessageAsync(string sender, string data) if (sgIndex >= ddSavedGame.Items.Count) { AddNotice("The game host has selected an invalid saved game index!".L10N("Client:Main:HostInvalidIndex") + " " + sgIndex); - await channel.SendCTCPMessageAsync(CnCNetCommands.INVALID_SAVED_GAME_INDEX, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.INVALID_SAVED_GAME_INDEX, QueuedMessageType.SYSTEM_MESSAGE, 10).ConfigureAwait(false); return; } @@ -523,7 +524,7 @@ private async ValueTask HandleStartGameCommandAsync(string sender, string data) pInfo.Port = port; } - await LoadGameAsync(); + await LoadGameAsync().ConfigureAwait(false); } private async ValueTask HandlePlayerReadyRequestAsync(string sender, int readyStatus) @@ -538,7 +539,7 @@ private async ValueTask HandlePlayerReadyRequestAsync(string sender, int readySt CopyPlayerDataToUI(); if (IsHost) - await BroadcastOptionsAsync(); + await BroadcastOptionsAsync().ConfigureAwait(false); } private void HandleTunnelServerChangeMessage(string sender, string hash) @@ -577,7 +578,7 @@ private void HandleTunnelServerChange(CnCNetTunnel tunnel) protected override async ValueTask HostStartGameAsync() { AddNotice("Contacting tunnel server...".L10N("Client:Main:ConnectingTunnel")); - List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(SGPlayers.Count); + List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(SGPlayers.Count).ConfigureAwait(false); if (playerPorts.Count < Players.Count) { @@ -600,13 +601,13 @@ protected override async ValueTask HostStartGameAsync() sb.Append(";"); } sb.Remove(sb.Length - 1, 1); - await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 9).ConfigureAwait(false); AddNotice("Starting game...".L10N("Client:Main:StartingGame")); started = true; - await LoadGameAsync(); + await LoadGameAsync().ConfigureAwait(false); } protected override void WriteSpawnIniAdditions(IniFile spawnIni) @@ -619,8 +620,8 @@ protected override void WriteSpawnIniAdditions(IniFile spawnIni) protected override async ValueTask HandleGameProcessExitedAsync() { - await base.HandleGameProcessExitedAsync(); - await ClearAsync(); + await base.HandleGameProcessExitedAsync().ConfigureAwait(false); + await ClearAsync().ConfigureAwait(false); } protected override ValueTask LeaveGameAsync() => ClearAsync(); @@ -674,7 +675,7 @@ private async ValueTask BroadcastGameAsync() sb.Append(";"); sb.Append(0); // LoadedGameId - await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); + await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20).ConfigureAwait(false); } public override string GetSwitchName() => "Load Game".L10N("Client:Main:LoadGame"); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 3cf96742e..6b97789db 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -611,9 +611,9 @@ private async ValueTask ConnectionManager_BannedFromChannelAsync(ChannelEventArg if (gameOfLastJoinAttempt != null) { if (gameOfLastJoinAttempt.IsLoadedGame) - await gameLoadingLobby.ClearAsync(); + await gameLoadingLobby.ClearAsync().ConfigureAwait(false); else - await gameLobby.ClearAsync(); + await gameLobby.ClearAsync().ConfigureAwait(false); } } @@ -642,13 +642,13 @@ private async ValueTask Instance_SettingsSavedAsync() if (followedGames.Contains(game.InternalName) && !UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) { - await connectionManager.FindChannel(game.GameBroadcastChannel).LeaveAsync(); + await connectionManager.FindChannel(game.GameBroadcastChannel).LeaveAsync().ConfigureAwait(false); followedGames.Remove(game.InternalName); } else if (!followedGames.Contains(game.InternalName) && UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) { - await connectionManager.FindChannel(game.GameBroadcastChannel).JoinAsync(); + await connectionManager.FindChannel(game.GameBroadcastChannel).JoinAsync().ConfigureAwait(false); followedGames.Add(game.InternalName); } } @@ -807,7 +807,7 @@ private async ValueTask JoinSelectedGameAsync() if (listedGame == null) return; var hostedGameIndex = lbGameList.HostedGames.IndexOf(listedGame); - await JoinGameByIndexAsync(hostedGameIndex, string.Empty); + await JoinGameByIndexAsync(hostedGameIndex, string.Empty).ConfigureAwait(false); } private async ValueTask JoinGameByIndexAsync(int gameIndex, string password) @@ -819,7 +819,7 @@ private async ValueTask JoinGameByIndexAsync(int gameIndex, string passwor return false; } - return await JoinGameAsync((HostedCnCNetGame)lbGameList.HostedGames[gameIndex], password, connectionManager.MainChannel); + return await JoinGameAsync((HostedCnCNetGame)lbGameList.HostedGames[gameIndex], password, connectionManager.MainChannel).ConfigureAwait(false); } /// @@ -871,7 +871,7 @@ private async ValueTask JoinGameAsync(HostedCnCNetGame hg, string password } } - await JoinGameAsync(hg, password); + await JoinGameAsync(hg, password).ConfigureAwait(false); return true; } @@ -894,7 +894,7 @@ private async ValueTask JoinGameAsync(HostedCnCNetGame hg, string password) } else { - await gameLobby.SetUpAsync(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded); + await gameLobby.SetUpAsync(gameChannel, false, hg.MaxPlayers, hg.TunnelServer, hg.HostName, hg.Passworded).ConfigureAwait(false); gameChannel.UserAdded += gameChannel_UserAddedFunc; gameChannel.InvalidPasswordEntered += gameChannel_InvalidPasswordEntered_NewGameFunc; gameChannel.InviteOnlyErrorOnJoin += gameChannel_InviteOnlyErrorOnJoinFunc; @@ -903,13 +903,13 @@ private async ValueTask JoinGameAsync(HostedCnCNetGame hg, string password) } await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.JOIN + " " + hg.ChannelName + " " + password, - QueuedMessageType.INSTANT_MESSAGE, 0)); + QueuedMessageType.INSTANT_MESSAGE, 0)).ConfigureAwait(false); } private async ValueTask GameChannel_TargetChangeTooFastAsync(object sender, MessageEventArgs e) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, e.Message)); - await ClearGameJoinAttemptAsync((Channel)sender); + await ClearGameJoinAttemptAsync((Channel)sender).ConfigureAwait(false); } private async ValueTask OnGameLocked(object sender) @@ -924,7 +924,7 @@ private async ValueTask OnGameLocked(object sender) SortAndRefreshHostedGames(); } - await ClearGameJoinAttemptAsync((Channel)sender); + await ClearGameJoinAttemptAsync((Channel)sender).ConfigureAwait(false); } private HostedCnCNetGame FindGameByChannelName(string channelName) @@ -939,7 +939,7 @@ private HostedCnCNetGame FindGameByChannelName(string channelName) private async ValueTask GameChannel_InvalidPasswordEntered_NewGameAsync(object sender) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "Incorrect password!".L10N("Client:Main:PasswordWrong"))); - await ClearGameJoinAttemptAsync((Channel)sender); + await ClearGameJoinAttemptAsync((Channel)sender).ConfigureAwait(false); } private async ValueTask GameChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) @@ -949,7 +949,7 @@ private async ValueTask GameChannel_UserAddedAsync(object sender, ChannelUserEve if (e.User.IRCUser.Name == ProgramConstants.PLAYERNAME) { ClearGameChannelEvents(gameChannel); - await gameLobby.OnJoinedAsync(); + await gameLobby.OnJoinedAsync().ConfigureAwait(false); isInGameRoom = true; SetLogOutButtonText(); } @@ -958,7 +958,7 @@ private async ValueTask GameChannel_UserAddedAsync(object sender, ChannelUserEve private async ValueTask ClearGameJoinAttemptAsync(Channel channel) { ClearGameChannelEvents(channel); - await gameLobby.ClearAsync(); + await gameLobby.ClearAsync().ConfigureAwait(false); } private void ClearGameChannelEvents(Channel channel) @@ -1001,10 +1001,10 @@ private async ValueTask Gcw_GameCreatedAsync(GameCreationEventArgs e) Channel gameChannel = connectionManager.CreateChannel(e.GameRoomName, channelName, false, true, password); connectionManager.AddChannel(gameChannel); - await gameLobby.SetUpAsync(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword); + await gameLobby.SetUpAsync(gameChannel, true, e.MaxPlayers, e.Tunnel, ProgramConstants.PLAYERNAME, isCustomPassword).ConfigureAwait(false); gameChannel.UserAdded += gameChannel_UserAddedFunc; await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.JOIN + " " + channelName + " " + password, - QueuedMessageType.INSTANT_MESSAGE, 0)); + QueuedMessageType.INSTANT_MESSAGE, 0)).ConfigureAwait(false); connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); @@ -1026,7 +1026,7 @@ private async ValueTask Gcw_LoadedGameCreatedAsync(GameCreationEventArgs e) gameLoadingLobby.SetUp(true, e.Tunnel, gameLoadingChannel, ProgramConstants.PLAYERNAME); gameLoadingChannel.UserAdded += gameLoadingChannel_UserAddedFunc; await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.JOIN + " " + channelName + " " + e.Password, - QueuedMessageType.INSTANT_MESSAGE, 0)); + QueuedMessageType.INSTANT_MESSAGE, 0)).ConfigureAwait(false); connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format("Creating a game named {0} ...".L10N("Client:Main:CreateGameNamed"), e.GameRoomName))); @@ -1041,7 +1041,7 @@ private async ValueTask GameChannel_InvalidPasswordEntered_LoadedGameAsync(objec var channel = (Channel)sender; channel.UserAdded -= gameLoadingChannel_UserAddedFunc; channel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; - await gameLoadingLobby.ClearAsync(); + await gameLoadingLobby.ClearAsync().ConfigureAwait(false); isJoiningGame = false; } @@ -1054,7 +1054,7 @@ private async ValueTask GameLoadingChannel_UserAddedAsync(object sender, Channel gameLoadingChannel.UserAdded -= gameLoadingChannel_UserAddedFunc; gameLoadingChannel.InvalidPasswordEntered -= gameChannel_InvalidPasswordEntered_LoadedGameFunc; - await gameLoadingLobby.OnJoinedAsync(); + await gameLoadingLobby.OnJoinedAsync().ConfigureAwait(false); isInGameRoom = true; isJoiningGame = false; } @@ -1084,7 +1084,7 @@ private async ValueTask TbChatInput_EnterPressedAsync() IRCColor selectedColor = (IRCColor)ddColor.SelectedItem.Tag; - await currentChatChannel.SendChatMessageAsync(tbChatInput.Text, selectedColor); + await currentChatChannel.SendChatMessageAsync(tbChatInput.Text, selectedColor).ConfigureAwait(false); tbChatInput.Text = string.Empty; } @@ -1137,13 +1137,13 @@ private async ValueTask ConnectionManager_WelcomeMessageReceivedAsync() tbChatInput.Enabled = true; Channel cncnetChannel = connectionManager.FindChannel("#cncnet"); - await cncnetChannel.JoinAsync(); + await cncnetChannel.JoinAsync().ConfigureAwait(false); string localGameChatChannelName = gameCollection.GetGameChatChannelNameFromIdentifier(localGameID); - await connectionManager.FindChannel(localGameChatChannelName).JoinAsync(); + await connectionManager.FindChannel(localGameChatChannelName).JoinAsync().ConfigureAwait(false); string localGameBroadcastChannel = gameCollection.GetGameBroadcastingChannelNameFromIdentifier(localGameID); - await connectionManager.FindChannel(localGameBroadcastChannel).JoinAsync(); + await connectionManager.FindChannel(localGameBroadcastChannel).JoinAsync().ConfigureAwait(false); foreach (CnCNetGame game in gameCollection.GameList) { @@ -1154,7 +1154,7 @@ private async ValueTask ConnectionManager_WelcomeMessageReceivedAsync() { if (UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) { - await connectionManager.FindChannel(game.GameBroadcastChannel).JoinAsync(); + await connectionManager.FindChannel(game.GameBroadcastChannel).JoinAsync().ConfigureAwait(false); followedGames.Add(game.InternalName); } } @@ -1204,7 +1204,7 @@ private async ValueTask HandleGameInviteCommandAsync(string sender, string argum // note this is not reached for the rejection case await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.PRIVMSG + " " + sender + " :\u0001" + CnCNetCommands.GAME_INVITATION_FAILED + "\u0001", - QueuedMessageType.CHAT_MESSAGE, 0)); + QueuedMessageType.CHAT_MESSAGE, 0)).ConfigureAwait(false); return; } @@ -1249,11 +1249,11 @@ private async ValueTask AffirmativeClickedActionAsync(string channelName, string // if we're currently in a game lobby, first leave that channel if (isInGameRoom) { - await gameLobby.LeaveGameLobbyAsync(); + await gameLobby.LeaveGameLobbyAsync().ConfigureAwait(false); } // JoinGameByIndex does bounds checking so we're safe to pass -1 if the game doesn't exist - if (!await JoinGameByIndexAsync(lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName), password)) + if (!await JoinGameByIndexAsync(lbGameList.HostedGames.FindIndex(hg => ((HostedCnCNetGame)hg).ChannelName == channelName), password).ConfigureAwait(false)) { XNAMessageBox.Show(WindowManager, "Failed to join".L10N("Client:Main:JoinFailedTitle"), @@ -1299,7 +1299,7 @@ private async ValueTask DdCurrentChannel_SelectedIndexChangedAsync() connectionManager.RemoveChannelFromUser(user.IRCUser.Name, currentChatChannel.ChannelName); }); - await currentChatChannel.LeaveAsync(); + await currentChatChannel.LeaveAsync().ConfigureAwait(false); } } @@ -1324,7 +1324,7 @@ private async ValueTask DdCurrentChannel_SelectedIndexChangedAsync() if (currentChatChannel.ChannelName != "#cncnet" && currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) { - await currentChatChannel.JoinAsync(); + await currentChatChannel.JoinAsync().ConfigureAwait(false); } } @@ -1555,7 +1555,7 @@ private async ValueTask BtnLogout_LeftClickAsync() if (connectionManager.IsConnected && !UserINISettings.Instance.PersistentMode) { - await connectionManager.DisconnectAsync(); + await connectionManager.DisconnectAsync().ConfigureAwait(false); } topBar.SwitchToPrimary(); @@ -1676,7 +1676,7 @@ private async ValueTask JoinUserAsync(IRCUser user, IMessageView messageView) return; } - await JoinGameAsync(game, string.Empty, messageView); + await JoinGameAsync(game, string.Empty, messageView).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs index 72badfeca..7a371436f 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/GlobalContextMenu.cs @@ -122,7 +122,7 @@ private async ValueTask InviteAsync() } await connectionManager.SendCustomMessageAsync(new QueuedMessage( - IRCCommands.PRIVMSG + " " + GetIrcUser().Name + " :\u0001" + messageBody + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0)); + IRCCommands.PRIVMSG + " " + GetIrcUser().Name + " :\u0001" + messageBody + "\u0001", QueuedMessageType.CHAT_MESSAGE, 0)).ConfigureAwait(false); } private void UpdateButtons() @@ -205,7 +205,7 @@ void WhoIsReply(object sender, WhoEventArgs whoEventargs) } connectionManager.WhoReplyReceived += WhoIsReply; - await connectionManager.SendWhoIsMessageAsync(ircUser.Name); + await connectionManager.SendWhoIsMessageAsync(ircUser.Name).ConfigureAwait(false); } private IRCUser GetIrcUser() diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs index 7a021adc9..b3cd41534 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs @@ -528,7 +528,7 @@ private async ValueTask TbMessageInput_EnterPressedAsync() string userName = lbUserList.SelectedItem.Text; await connectionManager.SendCustomMessageAsync(new QueuedMessage(IRCCommands.PRIVMSG + " " + userName + " :" + tbMessageInput.Text, - QueuedMessageType.CHAT_MESSAGE, 0)); + QueuedMessageType.CHAT_MESSAGE, 0)).ConfigureAwait(false); PrivateMessageUser pmUser = privateMessageUsers.Find(u => u.IrcUser.Name == userName); if (pmUser == null) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index fa4948ba2..8b3710aa6 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -235,30 +235,30 @@ private static async ValueTask HandleFSWEventAsync(FileSystemEventArgs e) Logger.Log("FSW Event: " + e.FullPath); if (Path.GetFileName(e.FullPath) == "SAVEGAME.NET") - await SavedGameManager.RenameSavedGameAsync(); + await SavedGameManager.RenameSavedGameAsync().ConfigureAwait(false); } private async ValueTask BtnLoadGame_LeftClickAsync() { if (!IsHost) { - await RequestReadyStatusAsync(); + await RequestReadyStatusAsync().ConfigureAwait(false); return; } if (Players.Find(p => !p.Ready) != null) { - await GetReadyNotificationAsync(); + await GetReadyNotificationAsync().ConfigureAwait(false); return; } if (Players.Count != SGPlayers.Count) { - await NotAllPresentNotificationAsync(); + await NotAllPresentNotificationAsync().ConfigureAwait(false); return; } - await HostStartGameAsync(); + await HostStartGameAsync().ConfigureAwait(false); } protected abstract ValueTask RequestReadyStatusAsync(); @@ -329,16 +329,20 @@ protected async ValueTask LoadGameAsync() FileInfo spawnMapFileInfo = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNMAP_INI); spawnMapFileInfo.Delete(); - using var spawnMapStreamWriter = new StreamWriter(spawnMapFileInfo.FullName); - spawnMapStreamWriter.WriteLine("[Map]"); - spawnMapStreamWriter.WriteLine("Size=0,0,50,50"); - spawnMapStreamWriter.WriteLine("LocalSize=0,0,50,50"); - spawnMapStreamWriter.WriteLine(); + var spawnMapStreamWriter = new StreamWriter(spawnMapFileInfo.FullName); + + await using (spawnMapStreamWriter.ConfigureAwait(false)) + { + await spawnMapStreamWriter.WriteLineAsync("[Map]").ConfigureAwait(false); + await spawnMapStreamWriter.WriteLineAsync("Size=0,0,50,50").ConfigureAwait(false); + await spawnMapStreamWriter.WriteLineAsync("LocalSize=0,0,50,50").ConfigureAwait(false); + await spawnMapStreamWriter.WriteLineAsync().ConfigureAwait(false); + } gameLoadTime = DateTime.Now; GameProcessLogic.GameProcessExited += SharedUILogic_GameProcessExited; - await GameProcessLogic.StartGameProcessAsync(WindowManager); + await GameProcessLogic.StartGameProcessAsync(WindowManager).ConfigureAwait(false); fsw.EnableRaisingEvents = true; UpdateDiscordPresence(true); @@ -488,7 +492,7 @@ private async ValueTask DdSavedGame_SelectedIndexChangedAsync() CopyPlayerDataToUI(); if (!isSettingUp) - await BroadcastOptionsAsync(); + await BroadcastOptionsAsync().ConfigureAwait(false); UpdateDiscordPresence(); } @@ -497,7 +501,7 @@ private async ValueTask TbChatInput_EnterPressedAsync() if (string.IsNullOrEmpty(tbChatInput.Text)) return; - await SendChatMessageAsync(tbChatInput.Text); + await SendChatMessageAsync(tbChatInput.Text).ConfigureAwait(false); tbChatInput.Text = string.Empty; } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 8ba03c491..5c108489e 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -324,7 +324,7 @@ public async ValueTask SetUpAsync( { RandomSeed = new Random().Next(); - await RefreshMapSelectionUIAsync(); + await RefreshMapSelectionUIAsync().ConfigureAwait(false); btnChangeTunnel.Enable(); } else @@ -358,12 +358,12 @@ public async ValueTask OnJoinedAsync() await connectionManager.SendCustomMessageAsync(new( FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} +{IRCChannelModes.DEFAULT} {channel.Password} {playerLimit}"), QueuedMessageType.SYSTEM_MESSAGE, - 50)); + 50)).ConfigureAwait(false); await connectionManager.SendCustomMessageAsync(new( FormattableString.Invariant($"{IRCCommands.TOPIC} {channel.ChannelName} :{ProgramConstants.CNCNET_PROTOCOL_REVISION}:{localGame.ToLower()}"), QueuedMessageType.SYSTEM_MESSAGE, - 50)); + 50)).ConfigureAwait(false); gameBroadcastTimer.Enabled = true; @@ -372,7 +372,7 @@ await connectionManager.SendCustomMessageAsync(new( } else { - await channel.SendCTCPMessageAsync(CnCNetCommands.FILE_HASH + " " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.FILE_HASH + " " + gameFilesHash, QueuedMessageType.SYSTEM_MESSAGE, 10).ConfigureAwait(false); if (v3ConnectionState.DynamicTunnelsEnabled) BroadcastPlayerTunnelPingsAsync().HandleTask(); @@ -385,7 +385,7 @@ await connectionManager.SendCustomMessageAsync(new( TopBar.SwitchToPrimary(); WindowManager.SelectedControl = tbChatInput; ResetAutoReadyCheckbox(); - await UpdatePingAsync(); + await UpdatePingAsync().ConfigureAwait(false); UpdateDiscordPresence(true); } @@ -400,7 +400,7 @@ private async ValueTask UpdatePingAsync() else ping = tunnelHandler.CurrentTunnel.PingInMs; - await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_PING + " " + ping, QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_PING + " " + ping, QueuedMessageType.SYSTEM_MESSAGE, 10).ConfigureAwait(false); PlayerInfo pInfo = FindLocalPlayer(); @@ -453,8 +453,8 @@ private async ValueTask TunnelSelectionWindow_TunnelSelectedAsync(TunnelEventArg await channel.SendCTCPMessageAsync( $"{CnCNetCommands.CHANGE_TUNNEL_SERVER} {e.Tunnel.Hash}", QueuedMessageType.SYSTEM_MESSAGE, - 10); - await HandleTunnelServerChangeAsync(e.Tunnel); + 10).ConfigureAwait(false); + await HandleTunnelServerChangeAsync(e.Tunnel).ConfigureAwait(false); } public void ChangeChatColor(IRCColor chatColor) @@ -465,7 +465,7 @@ public void ChangeChatColor(IRCColor chatColor) public override async ValueTask ClearAsync() { - await base.ClearAsync(); + await base.ClearAsync().ConfigureAwait(false); if (channel != null) { @@ -507,16 +507,16 @@ public async ValueTask LeaveGameLobbyAsync() if (IsHost) { closed = true; - await BroadcastGameAsync(); + await BroadcastGameAsync().ConfigureAwait(false); } - await ClearAsync(); - await channel.LeaveAsync(); + await ClearAsync().ConfigureAwait(false); + await channel.LeaveAsync().ConfigureAwait(false); } private async ValueTask HandleConnectionLossAsync() { - await ClearAsync(); + await ClearAsync().ConfigureAwait(false); Disable(); } @@ -573,13 +573,13 @@ protected override void UpdateDiscordPresence(bool resetTimer = false) private async ValueTask ChannelUserLeftAsync(UserNameEventArgs e) { - await RemovePlayerAsync(e.UserName); + await RemovePlayerAsync(e.UserName).ConfigureAwait(false); if (e.UserName.Equals(hostName, StringComparison.OrdinalIgnoreCase)) { connectionManager.MainChannel.AddMessage( new(ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("Client:Main:HostAbandoned"))); - await BtnLeaveGame_LeftClickAsync(); + await BtnLeaveGame_LeftClickAsync().ConfigureAwait(false); } else { @@ -593,7 +593,7 @@ private async ValueTask Channel_UserKickedAsync(UserNameEventArgs e) { connectionManager.MainChannel.AddMessage( new(ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); - await ClearAsync(); + await ClearAsync().ConfigureAwait(false); Visible = false; Enabled = false; @@ -615,7 +615,7 @@ private async ValueTask Channel_UserListReceivedAsync() { connectionManager.MainChannel.AddMessage( new(ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("Client:Main:HostHasAbandoned"))); - await BtnLeaveGame_LeftClickAsync(); + await BtnLeaveGame_LeftClickAsync().ConfigureAwait(false); } } @@ -652,9 +652,9 @@ private async ValueTask Channel_UserAddedAsync(ChannelUserEventArgs e) { // Changing the map applies forced settings (co-op sides etc.) to the // new player, and it also sends an options broadcast message - await ChangeMapAsync(GameModeMap); - await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); + await BroadcastPlayerExtraOptionsAsync().ConfigureAwait(false); UpdateDiscordPresence(); } else @@ -666,7 +666,7 @@ private async ValueTask Channel_UserAddedAsync(ChannelUserEventArgs e) if (Players.Count >= playerLimit) { AddNotice("Player limit reached. The game room has been locked.".L10N("Client:Main:GameRoomNumberLimitReached")); - await LockGameAsync(); + await LockGameAsync().ConfigureAwait(false); } } @@ -679,12 +679,12 @@ private async ValueTask RemovePlayerAsync(string playerName) // This might not be necessary if (IsHost) - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); sndLeaveSound.Play(); if (IsHost && Locked && !ProgramConstants.IsInGame) - await UnlockGameAsync(true); + await UnlockGameAsync(true).ConfigureAwait(false); } private void Channel_ChannelModesChanged(object sender, ChannelModeEventArgs e) @@ -750,9 +750,9 @@ protected override async ValueTask HostLaunchGameAsync() AddNotice("Contacting remote hosts...".L10N("Client:Main:ConnectingTunnel")); if (tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_2) - await HostLaunchGameV2Async(); + await HostLaunchGameV2Async().ConfigureAwait(false); else if (v3ConnectionState.DynamicTunnelsEnabled || tunnelHandler.CurrentTunnel?.Version == Constants.TUNNEL_VERSION_3) - await HostLaunchGameV3Async(); + await HostLaunchGameV3Async().ConfigureAwait(false); else throw new InvalidOperationException("Unknown tunnel server version!"); @@ -764,12 +764,12 @@ protected override async ValueTask HostLaunchGameAsync() CopyPlayerDataToUI(); cncnetUserData.AddRecentPlayers(Players.Select(p => p.Name), channel.UIName); - await StartGameAsync(); + await StartGameAsync().ConfigureAwait(false); } private async ValueTask HostLaunchGameV2Async() { - List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(Players.Count); + List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(Players.Count).ConfigureAwait(false); if (playerPorts.Count < Players.Count) { @@ -784,9 +784,10 @@ private async ValueTask HostLaunchGameV2Async() string playerPortsV2String = SetGamePlayerPortsV2(playerPorts); - await channel.SendCTCPMessageAsync($"{CnCNetCommands.GAME_START_V2} {UniqueGameID} {playerPortsV2String}", QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + await channel.SendCTCPMessageAsync( + $"{CnCNetCommands.GAME_START_V2} {UniqueGameID} {playerPortsV2String}", QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME).ConfigureAwait(false); Players.ForEach(pInfo => pInfo.IsInGame = true); - await StartGameAsync(); + await StartGameAsync().ConfigureAwait(false); } private string SetGamePlayerPortsV2(IReadOnlyList playerPorts) @@ -813,7 +814,8 @@ private async ValueTask HostLaunchGameV3Async() string gamePlayerIdsString = HostGenerateGamePlayerIds(); - await channel.SendCTCPMessageAsync($"{CnCNetCommands.GAME_START_V3} {UniqueGameID}{gamePlayerIdsString}", QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + await channel.SendCTCPMessageAsync( + $"{CnCNetCommands.GAME_START_V3} {UniqueGameID}{gamePlayerIdsString}", QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME).ConfigureAwait(false); isStartingGame = true; @@ -908,7 +910,7 @@ private async ValueTask GameTunnelHandler_Connected_CallbackAsync() SetLocalPlayerConnected(); } - await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_OK, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME); + await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_OK, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME).ConfigureAwait(false); } private void SetLocalPlayerConnected() @@ -918,7 +920,7 @@ private void SetLocalPlayerConnected() private async ValueTask GameTunnelHandler_ConnectionFailed_CallbackAsync() { - await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_FAIL, QueuedMessageType.INSTANT_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_FAIL, QueuedMessageType.INSTANT_MESSAGE, 0).ConfigureAwait(false); HandleTunnelFail(ProgramConstants.PLAYERNAME); } @@ -947,7 +949,7 @@ private async ValueTask HandlePlayerConnectedToTunnelAsync(string playerName) isPlayerConnected[index] = true; if (isPlayerConnected.All(b => b)) - await LaunchGameV3Async(); + await LaunchGameV3Async().ConfigureAwait(false); } private async ValueTask LaunchGameV3Async() @@ -983,7 +985,7 @@ private async ValueTask LaunchGameV3Async() btnLaunchGame.InputEnabled = true; - await StartGameAsync(); + await StartGameAsync().ConfigureAwait(false); } private void AbortGameStart() @@ -1030,7 +1032,8 @@ protected override async ValueTask RequestReadyStatusAsync() "you will be unable to participate in the match.").L10N("Client:Main:HostMustReplaceMap")); if (chkAutoReady.Checked) - await channel.SendCTCPMessageAsync(CnCNetCommands.READY_REQUEST + " 0", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); + await channel.SendCTCPMessageAsync( + CnCNetCommands.READY_REQUEST + " 0", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5).ConfigureAwait(false); return; } @@ -1043,7 +1046,8 @@ protected override async ValueTask RequestReadyStatusAsync() else if (!pInfo.Ready) readyState = 1; - await channel.SendCTCPMessageAsync($"{CnCNetCommands.READY_REQUEST} {readyState}", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5); + await channel.SendCTCPMessageAsync( + $"{CnCNetCommands.READY_REQUEST} {readyState}", QueuedMessageType.GAME_PLAYERS_READY_STATUS_MESSAGE, 5).ConfigureAwait(false); } protected override void AddNotice(string message, Color color) => channel.AddMessage(new(color, message)); @@ -1109,7 +1113,7 @@ private async ValueTask HandleOptionsRequestAsync(string playerName, int options pInfo.TeamId = team; CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); } /// @@ -1129,7 +1133,7 @@ private async ValueTask HandleReadyRequestAsync(string playerName, int readyStat pInfo.AutoReady = readyStatus > 1; CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); } /// @@ -1180,8 +1184,8 @@ protected override ValueTask BroadcastPlayerOptionsAsync() protected override async ValueTask PlayerExtraOptions_OptionsChangedAsync() { - await base.PlayerExtraOptions_OptionsChangedAsync(); - await BroadcastPlayerExtraOptionsAsync(); + await base.PlayerExtraOptions_OptionsChangedAsync().ConfigureAwait(false); + await BroadcastPlayerExtraOptionsAsync().ConfigureAwait(false); } protected override async ValueTask BroadcastPlayerExtraOptionsAsync() @@ -1191,7 +1195,8 @@ protected override async ValueTask BroadcastPlayerExtraOptionsAsync() PlayerExtraOptions playerExtraOptions = GetPlayerExtraOptions(); - await channel.SendCTCPMessageAsync(playerExtraOptions.ToCncnetMessage(), QueuedMessageType.GAME_PLAYERS_EXTRA_MESSAGE, 11, true); + await channel.SendCTCPMessageAsync( + playerExtraOptions.ToCncnetMessage(), QueuedMessageType.GAME_PLAYERS_EXTRA_MESSAGE, 11, true).ConfigureAwait(false); } private ValueTask BroadcastPlayerTunnelPingsAsync() @@ -1203,7 +1208,7 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() try { - p2pSetupSucceeded = await v3ConnectionState.HandlePlayerP2PRequestAsync(); + p2pSetupSucceeded = await v3ConnectionState.HandlePlayerP2PRequestAsync().ConfigureAwait(false); } catch (Exception ex) { @@ -1217,7 +1222,7 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() } if (p2pSetupSucceeded) - await SendPlayerP2PRequestAsync(); + await SendPlayerP2PRequestAsync().ConfigureAwait(false); } private ValueTask SendPlayerP2PRequestAsync() @@ -1333,7 +1338,7 @@ private void ApplyPlayerOptions(string sender, string message) /// protected override async ValueTask OnGameOptionChangedAsync() { - await base.OnGameOptionChangedAsync(); + await base.OnGameOptionChangedAsync().ConfigureAwait(false); if (!IsHost) return; @@ -1373,32 +1378,32 @@ protected override async ValueTask OnGameOptionChangedAsync() sb.Append(Map.UntranslatedName); sb.Append(Convert.ToInt32(v3ConnectionState.DynamicTunnelsEnabled)); - await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11).ConfigureAwait(false); } private async ValueTask ToggleDynamicTunnelsAsync() { - await ChangeDynamicTunnelsSettingAsync(!v3ConnectionState.DynamicTunnelsEnabled); - await OnGameOptionChangedAsync(); + await ChangeDynamicTunnelsSettingAsync(!v3ConnectionState.DynamicTunnelsEnabled).ConfigureAwait(false); + await OnGameOptionChangedAsync().ConfigureAwait(false); if (!v3ConnectionState.DynamicTunnelsEnabled) - await TunnelSelectionWindow_TunnelSelectedAsync(new(v3ConnectionState.InitialTunnel)); + await TunnelSelectionWindow_TunnelSelectedAsync(new(v3ConnectionState.InitialTunnel)).ConfigureAwait(false); } private async ValueTask ToggleP2PAsync() { - bool p2pEnabled = await v3ConnectionState.ToggleP2PAsync(); + bool p2pEnabled = await v3ConnectionState.ToggleP2PAsync().ConfigureAwait(false); if (p2pEnabled) { AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} enabled P2P".L10N("Client:Main:P2PEnabled"), FindLocalPlayer().Name)); - await BroadcastPlayerP2PRequestAsync(); + await BroadcastPlayerP2PRequestAsync().ConfigureAwait(false); return; } AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled P2P".L10N("Client:Main:P2PDisabled"), FindLocalPlayer().Name)); - await SendPlayerP2PRequestAsync(); + await SendPlayerP2PRequestAsync().ConfigureAwait(false); } /// @@ -1461,16 +1466,16 @@ private async ValueTask ApplyGameOptionsAsync(string sender, string message) if (GameModeMap == null) { - await ChangeMapAsync(null); + await ChangeMapAsync(null).ConfigureAwait(false); if (!isMapOfficial) - await RequestMapAsync(); + await RequestMapAsync().ConfigureAwait(false); else - await ShowOfficialMapMissingMessageAsync(mapHash); + await ShowOfficialMapMissingMessageAsync(mapHash).ConfigureAwait(false); } else if (GameModeMap != currentGameModeMap) { - await ChangeMapAsync(GameModeMap); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); } // By changing the game options after changing the map, we know which @@ -1573,7 +1578,7 @@ private async ValueTask ApplyGameOptionsAsync(string sender, string message) bool newDynamicTunnelsSetting = Conversions.BooleanFromString(parts[partIndex + 9], true); if (newDynamicTunnelsSetting != v3ConnectionState.DynamicTunnelsEnabled) - await ChangeDynamicTunnelsSettingAsync(newDynamicTunnelsSetting); + await ChangeDynamicTunnelsSettingAsync(newDynamicTunnelsSetting).ConfigureAwait(false); } private async ValueTask ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsEnabledValue) @@ -1591,7 +1596,7 @@ private async ValueTask ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsE .Where(q => q.PingInMs > -1 && !q.RequiresPassword && q.Clients < q.MaxClients - 8 && q.Version == Constants.TUNNEL_VERSION_3) .MinBy(q => q.PingInMs); - await BroadcastPlayerTunnelPingsAsync(); + await BroadcastPlayerTunnelPingsAsync().ConfigureAwait(false); } } @@ -1607,7 +1612,7 @@ private async ValueTask RequestMapAsync() AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + " " + ("Because you've disabled map sharing, it cannot be transferred. The game host needs " + "to change the map or you will be unable to participate in the match.").L10N("Client:Main:MapSharingDisabledNotice")); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DISABLED, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DISABLED, QueuedMessageType.SYSTEM_MESSAGE, 9).ConfigureAwait(false); } } @@ -1639,8 +1644,8 @@ protected override ValueTask ChangeMapAsync(GameModeMap gameModeMap) /// protected override async ValueTask GameProcessExitedAsync() { - await base.GameProcessExitedAsync(); - await channel.SendCTCPMessageAsync(CnCNetCommands.RETURN, QueuedMessageType.SYSTEM_MESSAGE, 20); + await base.GameProcessExitedAsync().ConfigureAwait(false); + await channel.SendCTCPMessageAsync(CnCNetCommands.RETURN, QueuedMessageType.SYSTEM_MESSAGE, 20).ConfigureAwait(false); gameStartCancellationTokenSource?.Cancel(); v3ConnectionState.V3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); v3ConnectionState.V3GameTunnelHandlers.Clear(); @@ -1649,14 +1654,14 @@ protected override async ValueTask GameProcessExitedAsync() if (IsHost) { RandomSeed = new Random().Next(); - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); + await BroadcastPlayerExtraOptionsAsync().ConfigureAwait(false); if (Players.Count < playerLimit) - await UnlockGameAsync(true); + await UnlockGameAsync(true).ConfigureAwait(false); } } @@ -1708,7 +1713,7 @@ private async ValueTask ClientLaunchGameV2Async(string sender, string message) } cncnetUserData.AddRecentPlayers(recentPlayers, channel.UIName); - await StartGameAsync(); + await StartGameAsync().ConfigureAwait(false); } protected override async ValueTask StartGameAsync() @@ -1724,11 +1729,11 @@ protected override async ValueTask StartGameAsync() if (gameFilesHash != fhc.GetCompleteHash()) { Logger.Log("Game files modified during client session!"); - await channel.SendCTCPMessageAsync(CnCNetCommands.CHEAT_DETECTED, QueuedMessageType.INSTANT_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.CHEAT_DETECTED, QueuedMessageType.INSTANT_MESSAGE, 0).ConfigureAwait(false); HandleCheatDetectedMessage(ProgramConstants.PLAYERNAME); } - await base.StartGameAsync(); + await base.StartGameAsync().ConfigureAwait(false); } protected override void WriteSpawnIniAdditions(IniFile iniFile) @@ -1772,78 +1777,78 @@ private void HandleIntNotification(string sender, int parameter, Action han protected override async ValueTask GetReadyNotificationAsync() { - await base.GetReadyNotificationAsync(); + await base.GetReadyNotificationAsync().ConfigureAwait(false); #if WINFORMS WindowManager.FlashWindow(); #endif TopBar.SwitchToPrimary(); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.GET_READY_LOBBY, QueuedMessageType.GAME_GET_READY_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.GET_READY_LOBBY, QueuedMessageType.GAME_GET_READY_MESSAGE, 0).ConfigureAwait(false); } protected override async ValueTask AISpectatorsNotificationAsync() { - await base.AISpectatorsNotificationAsync(); + await base.AISpectatorsNotificationAsync().ConfigureAwait(false); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.AI_SPECTATORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.AI_SPECTATORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0).ConfigureAwait(false); } protected override async ValueTask InsufficientPlayersNotificationAsync() { - await base.InsufficientPlayersNotificationAsync(); + await base.InsufficientPlayersNotificationAsync().ConfigureAwait(false); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.INSUFFICIENT_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.INSUFFICIENT_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0).ConfigureAwait(false); } protected override async ValueTask TooManyPlayersNotificationAsync() { - await base.TooManyPlayersNotificationAsync(); + await base.TooManyPlayersNotificationAsync().ConfigureAwait(false); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.TOO_MANY_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.TOO_MANY_PLAYERS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0).ConfigureAwait(false); } protected override async ValueTask SharedColorsNotificationAsync() { - await base.SharedColorsNotificationAsync(); + await base.SharedColorsNotificationAsync().ConfigureAwait(false); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_COLORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_COLORS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0).ConfigureAwait(false); } protected override async ValueTask SharedStartingLocationNotificationAsync() { - await base.SharedStartingLocationNotificationAsync(); + await base.SharedStartingLocationNotificationAsync().ConfigureAwait(false); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_STARTING_LOCATIONS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.SHARED_STARTING_LOCATIONS, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0).ConfigureAwait(false); } protected override async ValueTask LockGameNotificationAsync() { - await base.LockGameNotificationAsync(); + await base.LockGameNotificationAsync().ConfigureAwait(false); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.LOCK_GAME, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.LOCK_GAME, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0).ConfigureAwait(false); } protected override async ValueTask NotVerifiedNotificationAsync(int playerIndex) { - await base.NotVerifiedNotificationAsync(playerIndex); + await base.NotVerifiedNotificationAsync(playerIndex).ConfigureAwait(false); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.NOT_VERIFIED + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.NOT_VERIFIED + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0).ConfigureAwait(false); } protected override async ValueTask StillInGameNotificationAsync(int playerIndex) { - await base.StillInGameNotificationAsync(playerIndex); + await base.StillInGameNotificationAsync(playerIndex).ConfigureAwait(false); if (IsHost) - await channel.SendCTCPMessageAsync(CnCNetCommands.STILL_IN_GAME + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0); + await channel.SendCTCPMessageAsync(CnCNetCommands.STILL_IN_GAME + " " + playerIndex, QueuedMessageType.GAME_NOTIFICATION_MESSAGE, 0).ConfigureAwait(false); } private void ReturnNotification(string sender) @@ -1885,7 +1890,7 @@ private async ValueTask FileHashNotificationAsync(string sender, string filesHas if (filesHash != gameFilesHash) { - await channel.SendCTCPMessageAsync(CnCNetCommands.CHEATER + " " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10); + await channel.SendCTCPMessageAsync(CnCNetCommands.CHEATER + " " + sender, QueuedMessageType.GAME_CHEATER_MESSAGE, 10).ConfigureAwait(false); CheaterNotification(ProgramConstants.PLAYERNAME, sender); } } @@ -1902,7 +1907,7 @@ protected override async ValueTask BroadcastDiceRollAsync(int dieSides, int[] re { string resultString = string.Join(",", results); - await channel.SendCTCPMessageAsync($"{CnCNetCommands.DICE_ROLL} {dieSides},{resultString}", QueuedMessageType.CHAT_MESSAGE, 0); + await channel.SendCTCPMessageAsync($"{CnCNetCommands.DICE_ROLL} {dieSides},{resultString}", QueuedMessageType.CHAT_MESSAGE, 0).ConfigureAwait(false); PrintDiceRollResult(ProgramConstants.PLAYERNAME, dieSides, results); } @@ -1911,14 +1916,14 @@ protected override async ValueTask HandleLockGameButtonClickAsync() if (!Locked) { AddNotice("You've locked the game room.".L10N("Client:Main:RoomLockedByYou")); - await LockGameAsync(); + await LockGameAsync().ConfigureAwait(false); } else { if (Players.Count < playerLimit) { AddNotice("You've unlocked the game room.".L10N("Client:Main:RoomUnockedByYou")); - await UnlockGameAsync(false); + await UnlockGameAsync(false).ConfigureAwait(false); } else { @@ -1930,7 +1935,7 @@ protected override async ValueTask HandleLockGameButtonClickAsync() protected override async ValueTask LockGameAsync() { await connectionManager.SendCustomMessageAsync( - new(FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} +{IRCChannelModes.INVITE_ONLY}"), QueuedMessageType.INSTANT_MESSAGE, -1)); + new(FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} +{IRCChannelModes.INVITE_ONLY}"), QueuedMessageType.INSTANT_MESSAGE, -1)).ConfigureAwait(false); Locked = true; btnLockGame.Text = "Unlock Game".L10N("Client:Main:UnlockGame"); @@ -1940,7 +1945,7 @@ await connectionManager.SendCustomMessageAsync( protected override async ValueTask UnlockGameAsync(bool announce) { await connectionManager.SendCustomMessageAsync( - new(FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} -{IRCChannelModes.INVITE_ONLY}"), QueuedMessageType.INSTANT_MESSAGE, -1)); + new(FormattableString.Invariant($"{IRCCommands.MODE} {channel.ChannelName} -{IRCChannelModes.INVITE_ONLY}"), QueuedMessageType.INSTANT_MESSAGE, -1)).ConfigureAwait(false); Locked = false; @@ -1959,7 +1964,7 @@ protected override async ValueTask KickPlayerAsync(int playerIndex) PlayerInfo pInfo = Players[playerIndex]; AddNotice(string.Format(CultureInfo.CurrentCulture, "Kicking {0} from the game...".L10N("Client:Main:KickPlayer"), pInfo.Name)); - await channel.SendKickMessageAsync(pInfo.Name, 8); + await channel.SendKickMessageAsync(pInfo.Name, 8).ConfigureAwait(false); } protected override async ValueTask BanPlayerAsync(int playerIndex) @@ -1973,8 +1978,8 @@ protected override async ValueTask BanPlayerAsync(int playerIndex) if (user != null) { AddNotice(string.Format(CultureInfo.CurrentCulture, "Banning and kicking {0} from the game...".L10N("Client:Main:BanAndKickPlayer"), pInfo.Name)); - await channel.SendBanMessageAsync(user.Hostname, 8); - await channel.SendKickMessageAsync(user.Name, 8); + await channel.SendBanMessageAsync(user.Hostname, 8).ConfigureAwait(false); + await channel.SendKickMessageAsync(user.Name, 8).ConfigureAwait(false); } } @@ -2000,7 +2005,7 @@ private async ValueTask HandleTunnelServerChangeMessageAsync(string sender, stri } tunnelErrorMode = false; - await HandleTunnelServerChangeAsync(tunnel); + await HandleTunnelServerChangeAsync(tunnel).ConfigureAwait(false); UpdateLaunchGameButtonStatus(); } @@ -2043,12 +2048,13 @@ private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p if (!v3ConnectionState.P2PEnabled) return; - bool remotePlayerP2PEnabled = await v3ConnectionState.PingRemotePlayer(playerName, p2pRequestMessage); + bool remotePlayerP2PEnabled = await v3ConnectionState.PingRemotePlayer(playerName, p2pRequestMessage).ConfigureAwait(false); if (remotePlayerP2PEnabled) { ShowP2PPlayerStatus(playerName); - await channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_P2P_PINGS + v3ConnectionState.GetP2PPingCommand(playerName), QueuedMessageType.SYSTEM_MESSAGE, 10); + await channel.SendCTCPMessageAsync( + CnCNetCommands.PLAYER_P2P_PINGS + v3ConnectionState.GetP2PPingCommand(playerName), QueuedMessageType.SYSTEM_MESSAGE, 10).ConfigureAwait(false); } else { @@ -2097,7 +2103,7 @@ private async ValueTask MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) { AddNotice("Download of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:DownloadCustomMapFailed")); mapSharingConfirmationPanel.SetFailedStatus(); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9).ConfigureAwait(false); return; } @@ -2111,7 +2117,7 @@ private async ValueTask MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) } AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("Client:Main:RequestHostUploadMapToDB")); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_UPLOAD + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_UPLOAD + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9).ConfigureAwait(false); } private async ValueTask MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) @@ -2129,7 +2135,7 @@ private async ValueTask MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e { GameModeMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == lastMapHash); - await ChangeMapAsync(GameModeMap); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); } } else if (chatCommandDownloadedMaps.Contains(e.SHA1)) @@ -2146,7 +2152,7 @@ private async ValueTask MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e AddNotice(returnMessage, Color.Red); AddNotice("Transfer of the custom map failed. The host needs to change the map or you will be unable to participate in this match.".L10N("Client:Main:MapTransferFailed")); mapSharingConfirmationPanel.SetFailedStatus(); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9).ConfigureAwait(false); } } @@ -2160,7 +2166,7 @@ private async ValueTask MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) if (map == Map) { AddNotice("You need to change the map or some players won't be able to participate in this match.".L10N("Client:Main:YouMustReplaceMap")); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9).ConfigureAwait(false); } } @@ -2171,7 +2177,7 @@ private async ValueTask MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) if (e.Map == Map) { - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DOWNLOAD + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DOWNLOAD + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9).ConfigureAwait(false); } } @@ -2310,7 +2316,7 @@ private void DownloadMapByIdCommand(string parameters) // Check if the parameter's contain spaces. // The presence of spaces indicates a user-specified map name. - int firstSpaceIndex = parameters.IndexOf(' '); + int firstSpaceIndex = parameters.IndexOf(' ', StringComparison.OrdinalIgnoreCase); if (firstSpaceIndex == -1) { @@ -2328,7 +2334,7 @@ private void DownloadMapByIdCommand(string parameters) // Remove erroneous "?". These sneak in when someone double-clicks a map ID and copies it from the cncnet search endpoint. // There is some weird whitespace that gets copied to chat as a "?" at the end of the hash. It's hard to spot, so just hold the user's hand. - sha1 = sha1.Replace("?", string.Empty); + sha1 = sha1.Replace("?", string.Empty, StringComparison.OrdinalIgnoreCase); // See if the user already has this map, with any filename, before attempting to download it. GameModeMap loadedMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == sha1); @@ -2418,7 +2424,7 @@ private async ValueTask BroadcastGameAsync() .Append(tunnelHandler.CurrentTunnel?.Hash ?? ProgramConstants.CNCNET_DYNAMIC_TUNNELS) .Append(';') .Append(0); // LoadedGameId - await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20); + await broadcastChannel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.SYSTEM_MESSAGE, 20).ConfigureAwait(false); } public override string GetSwitchName() => "Game Lobby".L10N("Client:Main:GameLobby"); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 9e9487fce..8d468caba 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -402,7 +402,7 @@ protected void HandleGameOptionPresetSaveCommand(string presetName) protected async ValueTask HandleGameOptionPresetLoadCommandAsync(string presetName) { - if (await LoadGameOptionPresetAsync(presetName)) + if (await LoadGameOptionPresetAsync(presetName).ConfigureAwait(false)) AddNotice("Game option preset loaded succesfully.".L10N("Client:Main:PresetLoaded")); else AddNotice(string.Format("Preset {0} not found!".L10N("Client:Main:PresetNotFound"), presetName)); @@ -421,7 +421,7 @@ private async ValueTask Dropdown_SelectedIndexChangedAsync(object sender) var dd = (GameLobbyDropDown)sender; dd.HostSelectedIndex = dd.SelectedIndex; - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); } private async ValueTask ChkBox_CheckedChangedAsync(object sender) @@ -431,7 +431,7 @@ private async ValueTask ChkBox_CheckedChangedAsync(object sender) var checkBox = (GameLobbyCheckBox)sender; checkBox.HostChecked = checkBox.Checked; - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); } protected virtual ValueTask OnGameOptionChangedAsync() @@ -455,7 +455,7 @@ protected async ValueTask DdGameModeMapFilter_SelectedIndexChangedAsync() if (lbGameModeMapList.SelectedIndex == -1) lbGameModeMapList.SelectedIndex = 0; // Select default GameModeMap else - await ChangeMapAsync(GameModeMap); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); } private void BtnPlayerExtraOptions_LeftClick(object sender, EventArgs e) @@ -662,7 +662,7 @@ private async ValueTask DeleteSelectedMapAsync() } ListMaps(); - await ChangeMapAsync(GameModeMap); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); } catch (IOException ex) { @@ -676,7 +676,7 @@ private async ValueTask LbGameModeMapList_SelectedIndexChangedAsync() { if (lbGameModeMapList.SelectedIndex < 0 || lbGameModeMapList.SelectedIndex >= lbGameModeMapList.ItemCount) { - await ChangeMapAsync(GameModeMap); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); return; } @@ -684,7 +684,7 @@ private async ValueTask LbGameModeMapList_SelectedIndexChangedAsync() GameModeMap = (GameModeMap)item.Tag; - await ChangeMapAsync(GameModeMap); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); } private void LbGameModeMapList_HoveredIndexChanged(object sender, EventArgs e) @@ -716,7 +716,7 @@ private async ValueTask PickRandomMapAsync() Logger.Log("PickRandomMap: Rolled " + random + " out of " + maps.Count + ". Picked map: " + Map.Name); - await ChangeMapAsync(GameModeMap); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); tbMapSearch.Text = string.Empty; tbMapSearch.OnSelectedChanged(); ListMaps(); @@ -746,7 +746,7 @@ protected async ValueTask RefreshMapSelectionUIAsync() return; if (ddGameModeMapFilter.SelectedIndex == gameModeMapFilterIndex) - await DdGameModeMapFilter_SelectedIndexChangedAsync(); + await DdGameModeMapFilter_SelectedIndexChangedAsync().ConfigureAwait(false); ddGameModeMapFilter.SelectedIndex = gameModeMapFilterIndex; } @@ -1787,7 +1787,7 @@ protected virtual async ValueTask StartGameAsync() GameProcessLogic.GameProcessExited += GameProcessExited_Callback; - await GameProcessLogic.StartGameProcessAsync(WindowManager); + await GameProcessLogic.StartGameProcessAsync(WindowManager).ConfigureAwait(false); UpdateDiscordPresence(true); } @@ -1845,10 +1845,10 @@ protected virtual async ValueTask CopyPlayerDataFromUIAsync(object sender) ddName.SelectedIndex = 0; break; case 2: - await KickPlayerAsync(pId); + await KickPlayerAsync(pId).ConfigureAwait(false); break; case 3: - await BanPlayerAsync(pId); + await BanPlayerAsync(pId).ConfigureAwait(false); break; } } @@ -2190,7 +2190,7 @@ protected virtual async ValueTask ChangeMapAsync(GameModeMap gameModeMap) pInfo.TeamId = 1; } - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); MapPreviewBox.GameModeMap = GameModeMap; CopyPlayerDataToUI(); @@ -2459,7 +2459,7 @@ public async ValueTask LoadGameOptionPresetAsync(string name) } disableGameOptionUpdateBroadcast = false; - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); return true; } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index c3d357193..78fb23474 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -74,7 +74,7 @@ public LANGameLobby( private async ValueTask WindowManager_GameClosingAsync() { if (client is { Connected: true }) - await ClearAsync(); + await ClearAsync().ConfigureAwait(false); cancellationTokenSource?.Cancel(); } @@ -145,7 +145,7 @@ public async ValueTask SetUpAsync(bool isHost, IPEndPoint hostEndPoint, Socket c fhc.CalculateHashes(GameModeMaps.GameModes); localFileHash = fhc.GetCompleteHash(); - await RefreshMapSelectionUIAsync(); + await RefreshMapSelectionUIAsync().ConfigureAwait(false); } else { @@ -158,6 +158,7 @@ public async ValueTask SetUpAsync(bool isHost, IPEndPoint hostEndPoint, Socket c CopyPlayerDataToUI(); WindowManager.SelectedControl = tbChatInput; + btnLaunchGame.Enabled = true; } private async ValueTask SendHostPlayerJoinedMessageAsync(CancellationToken cancellationToken) @@ -166,7 +167,7 @@ private async ValueTask SendHostPlayerJoinedMessageAsync(CancellationToken cance { client = new Socket(SocketType.Stream, ProtocolType.Tcp); - await client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT, cancellationToken); + await client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT, cancellationToken).ConfigureAwait(false); string message = LANCommands.PLAYER_JOIN + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME; const int charSize = sizeof(char); @@ -177,7 +178,7 @@ private async ValueTask SendHostPlayerJoinedMessageAsync(CancellationToken cance buffer = buffer[..bytes]; - await client.SendAsync(buffer, SocketFlags.None, cancellationToken); + await client.SendAsync(buffer, SocketFlags.None, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -188,7 +189,7 @@ public async ValueTask PostJoinAsync() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(GameModeMaps.GameModes); - await SendMessageToHostAsync(LANCommands.FILE_HASH + " " + fhc.GetCompleteHash(), cancellationTokenSource?.Token ?? default); + await SendMessageToHostAsync(LANCommands.FILE_HASH + " " + fhc.GetCompleteHash(), cancellationTokenSource?.Token ?? default).ConfigureAwait(false); ResetAutoReadyCheckbox(); } @@ -207,7 +208,7 @@ private async ValueTask ListenForClientsAsync(CancellationToken cancellationToke try { - client = await listener.AcceptAsync(cancellationToken); + client = await listener.AcceptAsync(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -256,7 +257,7 @@ private async ValueTask HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancel try { message = memoryOwner.Memory[..1024]; - bytesRead = await lpInfo.TcpClient.ReceiveAsync(message, cancellationToken); + bytesRead = await lpInfo.TcpClient.ReceiveAsync(message, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -317,9 +318,9 @@ private async ValueTask AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken c lpInfo.StartReceiveLoopAsync(cancellationToken).HandleTask(); CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); - await OnGameOptionChangedAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); + await BroadcastPlayerExtraOptionsAsync().ConfigureAwait(false); + await OnGameOptionChangedAsync().ConfigureAwait(false); UpdateDiscordPresence(); } @@ -332,7 +333,7 @@ private async ValueTask LpInfo_ConnectionLostAsync(object sender) AddNotice(string.Format("{0} has left the game.".L10N("Client:Main:PlayerLeftGame"), lpInfo.Name)); CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); if (lpInfo.Name == ProgramConstants.PLAYERNAME) ResetDiscordPresence(); @@ -383,7 +384,7 @@ private async ValueTask HandleServerCommunicationAsync(CancellationToken cancell try { message = memoryOwner.Memory[..1024]; - bytesRead = await client.ReceiveAsync(message, cancellationToken); + bytesRead = await client.ReceiveAsync(message, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -392,7 +393,7 @@ private async ValueTask HandleServerCommunicationAsync(CancellationToken cancell catch (Exception ex) { ProgramConstants.LogException(ex, "Reading data from the server failed!"); - await BtnLeaveGame_LeftClickAsync(); + await BtnLeaveGame_LeftClickAsync().ConfigureAwait(false); break; } @@ -427,7 +428,7 @@ private async ValueTask HandleServerCommunicationAsync(CancellationToken cancell } Logger.Log("Reading data from the server failed (0 bytes received)!"); - await BtnLeaveGame_LeftClickAsync(); + await BtnLeaveGame_LeftClickAsync().ConfigureAwait(false); break; } } @@ -447,7 +448,7 @@ private void HandleMessageFromServer(string message) protected override async ValueTask BtnLeaveGame_LeftClickAsync() { - await ClearAsync(); + await ClearAsync().ConfigureAwait(false); GameLeft?.Invoke(this, EventArgs.Empty); Disable(); } @@ -473,11 +474,11 @@ protected override void UpdateDiscordPresence(bool resetTimer = false) public override async ValueTask ClearAsync() { - await base.ClearAsync(); + await base.ClearAsync().ConfigureAwait(false); if (IsHost) { - await BroadcastMessageAsync(LANCommands.PLAYER_QUIT_COMMAND); + await BroadcastMessageAsync(LANCommands.PLAYER_QUIT_COMMAND).ConfigureAwait(false); Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p)); Players.Clear(); @@ -488,11 +489,14 @@ public override async ValueTask ClearAsync() } else { - await SendMessageToHostAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationTokenSource?.Token ?? default); + await SendMessageToHostAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationTokenSource?.Token ?? default).ConfigureAwait(false); } cancellationTokenSource.Cancel(); - client.Shutdown(SocketShutdown.Both); + + if (client.Connected) + client.Shutdown(SocketShutdown.Both); + client.Close(); ResetDiscordPresence(); } @@ -533,14 +537,14 @@ protected override async ValueTask BroadcastPlayerOptionsAsync() sb.Append("-1"); } - await BroadcastMessageAsync(sb.ToString()); + await BroadcastMessageAsync(sb.ToString()).ConfigureAwait(false); } protected override async ValueTask BroadcastPlayerExtraOptionsAsync() { var playerExtraOptions = GetPlayerExtraOptions(); - await BroadcastMessageAsync(playerExtraOptions.ToLanMessage(), true); + await BroadcastMessageAsync(playerExtraOptions.ToLanMessage(), true).ConfigureAwait(false); } protected override ValueTask HostLaunchGameAsync() => BroadcastMessageAsync(LANCommands.LAUNCH_GAME + " " + UniqueGameID); @@ -578,7 +582,7 @@ protected override ValueTask SendChatMessageAsync(string message) protected override async ValueTask OnGameOptionChangedAsync() { - await base.OnGameOptionChangedAsync(); + await base.OnGameOptionChangedAsync().ConfigureAwait(false); if (!IsHost) return; @@ -601,18 +605,18 @@ protected override async ValueTask OnGameOptionChangedAsync() sb.Append(FrameSendRate); sb.Append(Convert.ToInt32(RemoveStartingLocations)); - await BroadcastMessageAsync(sb.ToString()); + await BroadcastMessageAsync(sb.ToString()).ConfigureAwait(false); } protected override async ValueTask GetReadyNotificationAsync() { - await base.GetReadyNotificationAsync(); + await base.GetReadyNotificationAsync().ConfigureAwait(false); #if WINFORMS WindowManager.FlashWindow(); #endif if (IsHost) - await BroadcastMessageAsync(LANCommands.GET_READY); + await BroadcastMessageAsync(LANCommands.GET_READY).ConfigureAwait(false); } protected override void ClearPingIndicators() @@ -638,14 +642,14 @@ private async ValueTask BroadcastMessageAsync(string message, bool otherPlayersO foreach (PlayerInfo pInfo in Players.Where(p => !otherPlayersOnly || p.Name != ProgramConstants.PLAYERNAME)) { var lpInfo = (LANPlayerInfo)pInfo; - await lpInfo.SendMessageAsync(message, cancellationTokenSource?.Token ?? default); + await lpInfo.SendMessageAsync(message, cancellationTokenSource?.Token ?? default).ConfigureAwait(false); } } protected override async ValueTask PlayerExtraOptions_OptionsChangedAsync() { - await base.PlayerExtraOptions_OptionsChangedAsync(); - await BroadcastPlayerExtraOptionsAsync(); + await base.PlayerExtraOptions_OptionsChangedAsync().ConfigureAwait(false); + await BroadcastPlayerExtraOptionsAsync().ConfigureAwait(false); } private async ValueTask SendMessageToHostAsync(string message, CancellationToken cancellationToken) @@ -665,7 +669,7 @@ private async ValueTask SendMessageToHostAsync(string message, CancellationToken buffer = buffer[..bytes]; - await client.SendAsync(buffer, SocketFlags.None, cancellationToken); + await client.SendAsync(buffer, SocketFlags.None, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -702,20 +706,20 @@ protected override ValueTask LockGameAsync() protected override async ValueTask GameProcessExitedAsync() { - await base.GameProcessExitedAsync(); - await SendMessageToHostAsync(LANCommands.RETURN, cancellationTokenSource?.Token ?? default); + await base.GameProcessExitedAsync().ConfigureAwait(false); + await SendMessageToHostAsync(LANCommands.RETURN, cancellationTokenSource?.Token ?? default).ConfigureAwait(false); if (IsHost) { RandomSeed = new Random().Next(); - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); + await BroadcastPlayerExtraOptionsAsync().ConfigureAwait(false); if (Players.Count < MAX_PLAYER_COUNT) - await UnlockGameAsync(true); + await UnlockGameAsync(true).ConfigureAwait(false); } } @@ -739,7 +743,7 @@ public override void Update(GameTime gameTime) for (int i = 1; i < Players.Count; i++) { LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i]; - if (!Task.Run(() => lpInfo.UpdateAsync(gameTime).HandleTask()).Result) + if (!Task.Run(() => lpInfo.UpdateAsync(gameTime).HandleTask(true)).Result) { CleanUpPlayer(lpInfo); Players.RemoveAt(i); @@ -808,7 +812,7 @@ private async ValueTask GameHost_HandleChatCommandAsync(string sender, string da if (colorIndex < 0 || colorIndex >= chatColors.Length) return; - await BroadcastMessageAsync(LANCommands.CHAT_LOBBY_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + data); + await BroadcastMessageAsync(LANCommands.CHAT_LOBBY_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + data).ConfigureAwait(false); } private void Player_HandleChatCommand(string data) @@ -840,7 +844,7 @@ private void Player_HandleReturnCommand(string sender) private async ValueTask HandleGetReadyCommandAsync() { if (!IsHost) - await GetReadyNotificationAsync(); + await GetReadyNotificationAsync().ConfigureAwait(false); } private async ValueTask HandlePlayerOptionsRequestAsync(string sender, string data) @@ -897,7 +901,7 @@ private async ValueTask HandlePlayerOptionsRequestAsync(string sender, string da pInfo.TeamId = team; CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); } private void HandlePlayerExtraOptionsBroadcast(string data) => ApplyPlayerExtraOptions(null, data); @@ -995,7 +999,7 @@ private async ValueTask HandlePlayerQuitAsync(string sender) Players.Remove(pInfo); ClearReadyStatuses(); CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); UpdateDiscordPresence(); } @@ -1014,14 +1018,14 @@ private async ValueTask HandleGameOptionsMessageAsync(string data) return; } - int randomSeed = Conversions.IntFromString(parts[parts.Length - GAME_OPTION_SPECIAL_FLAG_COUNT], -1); + int randomSeed = Conversions.IntFromString(parts[^GAME_OPTION_SPECIAL_FLAG_COUNT], -1); if (randomSeed == -1) return; RandomSeed = randomSeed; - string mapSHA1 = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 1)]; - string gameMode = parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 2)]; + string mapSHA1 = parts[^(GAME_OPTION_SPECIAL_FLAG_COUNT - 1)]; + string gameMode = parts[^(GAME_OPTION_SPECIAL_FLAG_COUNT - 2)]; GameModeMap gameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); @@ -1029,14 +1033,14 @@ private async ValueTask HandleGameOptionsMessageAsync(string data) { AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("Client:Main:MapNotExist") + "The host needs to change the map or you won't be able to play.".L10N("Client:Main:HostNeedChangeMapForYou")); - await ChangeMapAsync(null); + await ChangeMapAsync(null).ConfigureAwait(false); return; } if (GameModeMap != gameModeMap) - await ChangeMapAsync(gameModeMap); + await ChangeMapAsync(gameModeMap).ConfigureAwait(false); - int frameSendRate = Conversions.IntFromString(parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 3)], FrameSendRate); + int frameSendRate = Conversions.IntFromString(parts[^(GAME_OPTION_SPECIAL_FLAG_COUNT - 3)], FrameSendRate); if (frameSendRate != FrameSendRate) { FrameSendRate = frameSendRate; @@ -1044,7 +1048,7 @@ private async ValueTask HandleGameOptionsMessageAsync(string data) } bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString( - parts[parts.Length - (GAME_OPTION_SPECIAL_FLAG_COUNT - 4)], Convert.ToInt32(RemoveStartingLocations))); + parts[^(GAME_OPTION_SPECIAL_FLAG_COUNT - 4)], Convert.ToInt32(RemoveStartingLocations))); SetRandomStartingLocations(removeStartingLocations); for (int i = 0; i < CheckBoxes.Count; i++) @@ -1096,7 +1100,7 @@ private async ValueTask GameHost_HandleReadyRequestAsync(string sender, string a pInfo.Ready = true; pInfo.AutoReady = Convert.ToBoolean(Conversions.IntFromString(autoReady, 0)); CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); } private async ValueTask HandleGameLaunchCommandAsync(string gameId) @@ -1108,17 +1112,16 @@ private async ValueTask HandleGameLaunchCommandAsync(string gameId) return; CopyPlayerDataToUI(); - await StartGameAsync(); + await StartGameAsync().ConfigureAwait(false); } - private ValueTask HandlePingAsync() => SendMessageToHostAsync(LANCommands.PING, cancellationTokenSource?.Token ?? default); protected override async ValueTask BroadcastDiceRollAsync(int dieSides, int[] results) { string resultString = string.Join(",", results); - await SendMessageToHostAsync($"{LANCommands.DICE_ROLL} {dieSides},{resultString}", cancellationTokenSource?.Token ?? default); + await SendMessageToHostAsync($"{LANCommands.DICE_ROLL} {dieSides},{resultString}", cancellationTokenSource?.Token ?? default).ConfigureAwait(false); } private ValueTask Host_HandleDiceRollAsync(string sender, string result) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs index a8854377e..48a5b23ca 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs @@ -10,6 +10,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; +using ClientCore.Extensions; using ClientGUI; using ClientCore.Extensions; @@ -91,10 +93,9 @@ public void SetFields(List players, List aiPlayers, List AddChild(briefingBox); briefingBox.Disable(); - ClientRectangleUpdated += (s, e) => UpdateMap(); + ClientRectangleUpdated += (_, _) => UpdateMapAsync().HandleTask(); } - private GameModeMap _gameModeMap; public GameModeMap GameModeMap { @@ -102,7 +103,7 @@ public GameModeMap GameModeMap set { _gameModeMap = value; - UpdateMap(); + UpdateMapAsync().HandleTask(); } } @@ -219,7 +220,7 @@ public override void Initialize() base.Initialize(); - ClientRectangleUpdated += (s, e) => UpdateMap(); + ClientRectangleUpdated += (_, _) => UpdateMapAsync().HandleTask(); RightClick += MapPreviewBox_RightClick; @@ -378,7 +379,7 @@ private void Indicator_RightClick(object sender, EventArgs e) /// this control's display rectangle and the /// starting location indicators' positions. /// - private void UpdateMap() + private async ValueTask UpdateMapAsync() { if (disposeTextures && previewTexture != null && !previewTexture.IsDisposed) previewTexture.Dispose(); @@ -400,7 +401,7 @@ private void UpdateMap() if (GameModeMap.Map.PreviewTexture == null) { - previewTexture = GameModeMap.Map.LoadPreviewTexture(); + previewTexture = await GameModeMap.Map.LoadPreviewTextureAsync().ConfigureAwait(false); disposeTextures = true; } else diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 775261813..4b657dcbc 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -256,7 +256,7 @@ private async ValueTask FSWEventAsync(FileSystemEventArgs e) gameSaved = true; - await SavedGameManager.RenameSavedGameAsync(); + await SavedGameManager.RenameSavedGameAsync().ConfigureAwait(false); } } @@ -282,16 +282,16 @@ protected override async ValueTask GameProcessExitedAsync() pInfo.IsInGame = false; - await base.GameProcessExitedAsync(); + await base.GameProcessExitedAsync().ConfigureAwait(false); if (IsHost) { GenerateGameID(); - await DdGameModeMapFilter_SelectedIndexChangedAsync(); // Refresh ranks + await DdGameModeMapFilter_SelectedIndexChangedAsync().ConfigureAwait(false); // Refresh ranks } else if (chkAutoReady.Checked) { - await RequestReadyStatusAsync(); + await RequestReadyStatusAsync().ConfigureAwait(false); } } @@ -318,9 +318,9 @@ private void GenerateGameID() protected virtual async ValueTask HandleLockGameButtonClickAsync() { if (Locked) - await UnlockGameAsync(true); + await UnlockGameAsync(true).ConfigureAwait(false); else - await LockGameAsync(); + await LockGameAsync().ConfigureAwait(false); } protected abstract ValueTask LockGameAsync(); @@ -379,14 +379,14 @@ private async ValueTask TbChatInput_EnterPressedAsync() return; } - await SendChatMessageAsync(tbChatInput.Text); + await SendChatMessageAsync(tbChatInput.Text).ConfigureAwait(false); tbChatInput.Text = string.Empty; } private async ValueTask ChkAutoReady_CheckedChangedAsync() { UpdateLaunchGameButtonStatus(); - await RequestReadyStatusAsync(); + await RequestReadyStatusAsync().ConfigureAwait(false); } protected void ResetAutoReadyCheckbox() @@ -410,7 +410,7 @@ private async ValueTask SetFrameSendRateAsync(string value) FrameSendRate = intValue; AddNotice(string.Format("FrameSendRate has been changed to {0}".L10N("Client:Main:FrameSendRateChanged"), intValue)); - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); ClearReadyStatuses(); } @@ -428,7 +428,7 @@ private async ValueTask SetMaxAheadAsync(string value) MaxAhead = intValue; AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("Client:Main:MaxAheadChanged"), intValue)); - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); } @@ -451,7 +451,7 @@ private async ValueTask SetProtocolVersionAsync(string value) ProtocolVersion = intValue; AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("Client:Main:ProtocolVersionChanged"), intValue)); - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); } @@ -461,7 +461,7 @@ private async ValueTask SetStartingLocationClearanceAsync(string value) SetRandomStartingLocations(removeStartingLocations); - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); } @@ -523,7 +523,7 @@ private async ValueTask RollDiceCommandAsync(string dieType) results[i] = random.Next(1, dieSides + 1); } - await BroadcastDiceRollAsync(dieSides, results); + await BroadcastDiceRollAsync(dieSides, results).ConfigureAwait(false); } /// @@ -741,7 +741,7 @@ private async ValueTask MapPreviewBox_StartingLocationAppliedAsync() { ClearReadyStatuses(); CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); } /// @@ -754,13 +754,13 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() { if (!IsHost) { - await RequestReadyStatusAsync(); + await RequestReadyStatusAsync().ConfigureAwait(false); return; } if (!Locked) { - await LockGameNotificationAsync(); + await LockGameNotificationAsync().ConfigureAwait(false); return; } @@ -776,7 +776,7 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() { if (occupiedColorIds.Contains(player.ColorId) && player.ColorId > 0) { - await SharedColorsNotificationAsync(); + await SharedColorsNotificationAsync().ConfigureAwait(false); return; } @@ -785,7 +785,7 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() if (AIPlayers.Any(pInfo => pInfo.SideId == ddPlayerSides[0].Items.Count - 1)) { - await AISpectatorsNotificationAsync(); + await AISpectatorsNotificationAsync().ConfigureAwait(false); return; } @@ -800,7 +800,7 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() p => p.StartingLocation == pInfo.StartingLocation && p.Name != pInfo.Name) != null) { - await SharedStartingLocationNotificationAsync(); + await SharedStartingLocationNotificationAsync().ConfigureAwait(false); return; } } @@ -816,7 +816,7 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() if (index > -1 && index != aiId) { - await SharedStartingLocationNotificationAsync(); + await SharedStartingLocationNotificationAsync().ConfigureAwait(false); return; } } @@ -827,13 +827,13 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() int minPlayers = GameMode.MinPlayersOverride > -1 ? GameMode.MinPlayersOverride : Map.MinPlayers; if (totalPlayerCount < minPlayers) { - await InsufficientPlayersNotificationAsync(); + await InsufficientPlayersNotificationAsync().ConfigureAwait(false); return; } if (Map.EnforceMaxPlayers && totalPlayerCount > Map.MaxPlayers) { - await TooManyPlayersNotificationAsync(); + await TooManyPlayersNotificationAsync().ConfigureAwait(false); return; } } @@ -848,43 +848,24 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() if (!player.Verified) { - await NotVerifiedNotificationAsync(iId - 1); + await NotVerifiedNotificationAsync(iId - 1).ConfigureAwait(false); return; } if (player.IsInGame) { - await StillInGameNotificationAsync(iId - 1); + await StillInGameNotificationAsync(iId - 1).ConfigureAwait(false); return; } - /* - if (DisableSpectatorReadyChecking) - { - // Only account ready status if player is not a spectator - if (!player.Ready && !IsPlayerSpectator(player)) - { - await GetReadyNotificationAsync(); - return; - } - } - else - { - if (!player.Ready) - { - await GetReadyNotificationAsync(); - return; - } - } - */ if (!player.Ready) { - await GetReadyNotificationAsync(); + await GetReadyNotificationAsync().ConfigureAwait(false); return; } } - await HostLaunchGameAsync(); + await HostLaunchGameAsync().ConfigureAwait(false); } protected virtual ValueTask LockGameNotificationAsync() @@ -976,7 +957,7 @@ public virtual ValueTask ClearAsync() protected override async ValueTask OnGameOptionChangedAsync() { - await base.OnGameOptionChangedAsync(); + await base.OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); CopyPlayerDataToUI(); @@ -991,8 +972,8 @@ protected override async ValueTask CopyPlayerDataFromUIAsync(object sender) if (IsHost) { - await base.CopyPlayerDataFromUIAsync(sender); - await BroadcastPlayerOptionsAsync(); + await base.CopyPlayerDataFromUIAsync(sender).ConfigureAwait(false); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); return; } @@ -1006,7 +987,7 @@ protected override async ValueTask CopyPlayerDataFromUIAsync(object sender) int requestedStart = ddPlayerStarts[mTopIndex].SelectedIndex; int requestedTeam = ddPlayerTeams[mTopIndex].SelectedIndex; - await RequestPlayerOptionsAsync(requestedSide, requestedColor, requestedStart, requestedTeam); + await RequestPlayerOptionsAsync(requestedSide, requestedColor, requestedStart, requestedTeam).ConfigureAwait(false); } protected override void CopyPlayerDataToUI() @@ -1027,11 +1008,6 @@ protected override void CopyPlayerDataToUI() // Player statuses for (int pId = 0; pId < Players.Count; pId++) { - /* if (pId != 0 && !Players[pId].Verified) // If player is not verified (not counting the host) - { - StatusIndicators[pId].SwitchTexture("error"); - } - else */ if (Players[pId].IsInGame) // If player is ingame { StatusIndicators[pId].SwitchTexture(PlayerSlotState.InGame); @@ -1042,20 +1018,8 @@ protected override void CopyPlayerDataToUI() } else { - // StatusIndicators[pId].SwitchTexture( - // (IsPlayerSpectator(Players[pId]) && DisableSpectatorReadyChecking) - // ? "okDisabled" : "ok"); StatusIndicators[pId].SwitchTexture(Players[pId].Ready ? PlayerSlotState.Ready : PlayerSlotState.NotReady); } - /* - else - { - // StatusIndicators[pId].SwitchTexture( - // (IsPlayerSpectator(Players[pId]) && DisableSpectatorReadyChecking) - // ? "offDisabled" : "off"); - - } - */ UpdatePlayerPingIndicator(Players[pId]); } @@ -1131,14 +1095,14 @@ public void AddWarning(string message) protected override async ValueTask ChangeMapAsync(GameModeMap gameModeMap) { - await base.ChangeMapAsync(gameModeMap); + await base.ChangeMapAsync(gameModeMap).ConfigureAwait(false); bool resetAutoReady = gameModeMap?.GameMode == null || gameModeMap.Map == null; ClearReadyStatuses(resetAutoReady); if ((lastMapChangeWasInvalid || resetAutoReady) && chkAutoReady.Checked) - await RequestReadyStatusAsync(); + await RequestReadyStatusAsync().ConfigureAwait(false); lastMapChangeWasInvalid = resetAutoReady; } diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs index a1e1e98f9..22a69bce0 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -140,7 +140,7 @@ private string CheckGameValidity() Map.MaxPlayers); } - IEnumerable concatList = Players.Concat(AIPlayers); + List concatList = Players.Concat(AIPlayers).ToList(); foreach (PlayerInfo pInfo in concatList) { @@ -173,7 +173,7 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() if (error == null) { SaveSettings(); - await StartGameAsync(); + await StartGameAsync().ConfigureAwait(false); return; } @@ -230,8 +230,8 @@ protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap) protected override async ValueTask GameProcessExitedAsync() { - await base.GameProcessExitedAsync(); - await DdGameModeMapFilter_SelectedIndexChangedAsync(); // Refresh ranks + await base.GameProcessExitedAsync().ConfigureAwait(false); + await DdGameModeMapFilter_SelectedIndexChangedAsync().ConfigureAwait(false); // Refresh ranks RandomSeed = new Random().Next(); } diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index 71c0b13f3..bc8df802b 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -59,7 +59,7 @@ public LANGameLoadingLobby( private async ValueTask WindowManager_GameClosingAsync() { if (client is { Connected: true }) - await ClearAsync(); + await ClearAsync().ConfigureAwait(false); } public event EventHandler GameBroadcast; @@ -109,7 +109,7 @@ public async ValueTask SetUpAsync(bool isHost, Socket client, int loadedGameId) ListenForClientsAsync(cancellationTokenSource.Token).HandleTask(); this.client = new Socket(SocketType.Stream, ProtocolType.Tcp); - await this.client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT); + await this.client.ConnectAsync(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT).ConfigureAwait(false); string message = LANCommands.PLAYER_JOIN + ProgramConstants.LAN_DATA_SEPARATOR + ProgramConstants.PLAYERNAME + @@ -123,7 +123,7 @@ public async ValueTask SetUpAsync(bool isHost, Socket client, int loadedGameId) buffer = buffer[..bytes]; - await this.client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); + await this.client.SendAsync(buffer, SocketFlags.None, CancellationToken.None).ConfigureAwait(false); var fhc = new FileHashCalculator(); fhc.CalculateHashes(gameModes); @@ -147,7 +147,7 @@ public async ValueTask PostJoinAsync() { var fhc = new FileHashCalculator(); fhc.CalculateHashes(gameModes); - await SendMessageToHostAsync(LANCommands.FILE_HASH + " " + fhc.GetCompleteHash(), cancellationTokenSource?.Token ?? default); + await SendMessageToHostAsync(LANCommands.FILE_HASH + " " + fhc.GetCompleteHash(), cancellationTokenSource?.Token ?? default).ConfigureAwait(false); UpdateDiscordPresence(true); } @@ -165,7 +165,7 @@ private async ValueTask ListenForClientsAsync(CancellationToken cancellationToke try { - newPlayerSocket = await listener.AcceptAsync(cancellationToken); + newPlayerSocket = await listener.AcceptAsync(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -198,7 +198,7 @@ private async ValueTask HandleClientConnectionAsync(LANPlayerInfo lpInfo, Cancel try { message = memoryOwner.Memory[..4096]; - bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); + bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -268,7 +268,7 @@ private async ValueTask AddPlayerAsync(LANPlayerInfo lpInfo, CancellationToken c lpInfo.StartReceiveLoopAsync(cancellationToken).HandleTask(); CopyPlayerDataToUI(); - await BroadcastOptionsAsync(); + await BroadcastOptionsAsync().ConfigureAwait(false); UpdateDiscordPresence(); } @@ -283,7 +283,7 @@ private async ValueTask LpInfo_ConnectionLostAsync(object sender) sndLeaveSound.Play(); CopyPlayerDataToUI(); - await BroadcastOptionsAsync(); + await BroadcastOptionsAsync().ConfigureAwait(false); UpdateDiscordPresence(); } @@ -332,7 +332,7 @@ private async ValueTask HandleServerCommunicationAsync(CancellationToken cancell try { message = memoryOwner.Memory[..4096]; - bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken); + bytesRead = await client.ReceiveAsync(message, SocketFlags.None, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -341,7 +341,7 @@ private async ValueTask HandleServerCommunicationAsync(CancellationToken cancell catch (Exception ex) { ProgramConstants.LogException(ex, "Reading data from the server failed!"); - await LeaveGameAsync(); + await LeaveGameAsync().ConfigureAwait(false); break; } @@ -376,7 +376,7 @@ private async ValueTask HandleServerCommunicationAsync(CancellationToken cancell } Logger.Log("Reading data from the server failed (0 bytes received)!"); - await LeaveGameAsync(); + await LeaveGameAsync().ConfigureAwait(false); break; } } @@ -396,23 +396,23 @@ private void HandleMessageFromServer(string message) protected override async ValueTask LeaveGameAsync() { - await ClearAsync(); + await ClearAsync().ConfigureAwait(false); Disable(); - await base.LeaveGameAsync(); + await base.LeaveGameAsync().ConfigureAwait(false); } private async ValueTask ClearAsync() { if (IsHost) { - await BroadcastMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None); + await BroadcastMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None).ConfigureAwait(false); Players.ForEach(p => CleanUpPlayer((LANPlayerInfo)p)); Players.Clear(); listener.Close(); } else { - await SendMessageToHostAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None); + await SendMessageToHostAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None).ConfigureAwait(false); } cancellationTokenSource.Cancel(); @@ -442,7 +442,7 @@ protected override async ValueTask BroadcastOptionsAsync() sb.Append(pInfo.IPAddress); } - await BroadcastMessageAsync(sb.ToString(), cancellationTokenSource?.Token ?? default); + await BroadcastMessageAsync(sb.ToString(), cancellationTokenSource?.Token ?? default).ConfigureAwait(false); } protected override ValueTask HostStartGameAsync() @@ -454,7 +454,7 @@ protected override ValueTask RequestReadyStatusAsync() protected override async ValueTask SendChatMessageAsync(string message) { await SendMessageToHostAsync(LANCommands.CHAT_GAME_LOADING_COMMAND + " " + chatColorIndex + - ProgramConstants.LAN_DATA_SEPARATOR + message, cancellationTokenSource?.Token ?? default); + ProgramConstants.LAN_DATA_SEPARATOR + message, cancellationTokenSource?.Token ?? default).ConfigureAwait(false); sndMessageSound.Play(); } @@ -475,7 +475,7 @@ private async ValueTask Server_HandleChatMessageAsync(LANPlayerInfo sender, stri await BroadcastMessageAsync(LANCommands.CHAT_GAME_LOADING_COMMAND + " " + sender + ProgramConstants.LAN_DATA_SEPARATOR + colorIndex + - ProgramConstants.LAN_DATA_SEPARATOR + data, cancellationTokenSource?.Token ?? default); + ProgramConstants.LAN_DATA_SEPARATOR + data, cancellationTokenSource?.Token ?? default).ConfigureAwait(false); } private void Server_HandleFileHashMessage(LANPlayerInfo sender, string hash) @@ -492,7 +492,7 @@ private async ValueTask Server_HandleReadyRequestAsync(LANPlayerInfo sender) sender.Ready = true; CopyPlayerDataToUI(); - await BroadcastOptionsAsync(); + await BroadcastOptionsAsync().ConfigureAwait(false); } #endregion @@ -580,7 +580,7 @@ private async ValueTask BroadcastMessageAsync(string message, CancellationToken foreach (PlayerInfo pInfo in Players) { var lpInfo = (LANPlayerInfo)pInfo; - await lpInfo.SendMessageAsync(message, cancellationToken); + await lpInfo.SendMessageAsync(message, cancellationToken).ConfigureAwait(false); } } @@ -601,7 +601,7 @@ private async ValueTask SendMessageToHostAsync(string message, CancellationToken try { - await client.SendAsync(buffer, SocketFlags.None, cancellationToken); + await client.SendAsync(buffer, SocketFlags.None, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -622,7 +622,7 @@ public override void Update(GameTime gameTime) for (int i = 1; i < Players.Count; i++) { LANPlayerInfo lpInfo = (LANPlayerInfo)Players[i]; - if (!Task.Run(() => lpInfo.UpdateAsync(gameTime).HandleTask()).Result) + if (!Task.Run(() => lpInfo.UpdateAsync(gameTime).HandleTask(true)).Result) { CleanUpPlayer(lpInfo); Players.RemoveAt(i); @@ -675,8 +675,8 @@ private void BroadcastGame() protected override async ValueTask HandleGameProcessExitedAsync() { - await base.HandleGameProcessExitedAsync(); - await LeaveGameAsync(); + await base.HandleGameProcessExitedAsync().ConfigureAwait(false); + await LeaveGameAsync().ConfigureAwait(false); } protected override void UpdateDiscordPresence(bool resetTimer = false) diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 8969597ae..22217738f 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -248,7 +248,7 @@ private async ValueTask WindowManager_GameClosingAsync(CancellationToken cancell return; if (socket.IsBound) - await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationToken); + await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationToken).ConfigureAwait(false); cancellationTokenSource.Cancel(); socket.Close(); @@ -256,7 +256,7 @@ private async ValueTask WindowManager_GameClosingAsync(CancellationToken cancell private async ValueTask GameCreationWindow_LoadGameAsync(GameLoadEventArgs e) { - await lanGameLoadingLobby.SetUpAsync(true, null, e.LoadedGameID); + await lanGameLoadingLobby.SetUpAsync(true, null, e.LoadedGameID).ConfigureAwait(false); lanGameLoadingLobby.Enable(); } @@ -264,7 +264,7 @@ private async ValueTask GameCreationWindow_LoadGameAsync(GameLoadEventArgs e) private async ValueTask GameCreationWindow_NewGameAsync() { await lanGameLobby.SetUpAsync(true, - new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), null); + new IPEndPoint(IPAddress.Loopback, ProgramConstants.LAN_GAME_LOBBY_PORT), null).ConfigureAwait(false); lanGameLobby.Enable(); } @@ -329,7 +329,7 @@ public async ValueTask OpenAsync() Logger.Log("Starting listener."); ListenAsync(cancellationTokenSource.Token).HandleTask(); - await SendAliveAsync(cancellationTokenSource.Token); + await SendAliveAsync(cancellationTokenSource.Token).ConfigureAwait(false); } private async ValueTask SendMessageAsync(string message, CancellationToken cancellationToken) @@ -347,7 +347,7 @@ private async ValueTask SendMessageAsync(string message, CancellationToken cance try { - await socket.SendToAsync(buffer, SocketFlags.None, endPoint, cancellationToken); + await socket.SendToAsync(buffer, SocketFlags.None, endPoint, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -364,7 +364,8 @@ private async ValueTask ListenAsync(CancellationToken cancellationToken) { EndPoint ep = new IPEndPoint(lanIpV4BroadcastIpAddress, ProgramConstants.LAN_LOBBY_PORT); Memory buffer = memoryOwner.Memory[..4096]; - SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep, cancellationToken); + SocketReceiveFromResult socketReceiveFromResult = + await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep, cancellationToken).ConfigureAwait(false); var iep = (IPEndPoint)socketReceiveFromResult.RemoteEndPoint; string data = encoding.GetString(buffer.Span[..socketReceiveFromResult.ReceivedBytes]); @@ -477,7 +478,7 @@ private async ValueTask SendAliveAsync(CancellationToken cancellationToken) sb.Append(localGameIndex); sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); sb.Append(ProgramConstants.PLAYERNAME); - await SendMessageAsync(sb.ToString(), cancellationToken); + await SendMessageAsync(sb.ToString(), cancellationToken).ConfigureAwait(false); timeSinceAliveMessage = TimeSpan.Zero; } @@ -493,7 +494,7 @@ private async ValueTask TbChatInput_EnterPressedAsync(CancellationToken cancella sb.Append(ProgramConstants.LAN_DATA_SEPARATOR); sb.Append(chatMessage); - await SendMessageAsync(sb.ToString(), cancellationToken); + await SendMessageAsync(sb.ToString(), cancellationToken).ConfigureAwait(false); tbChatInput.Text = string.Empty; } @@ -545,7 +546,7 @@ private async ValueTask JoinGameAsync() try { var client = new Socket(SocketType.Stream, ProtocolType.Tcp); - await client.ConnectAsync(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT), CancellationToken.None); + await client.ConnectAsync(new IPEndPoint(hg.EndPoint.Address, ProgramConstants.LAN_GAME_LOBBY_PORT), CancellationToken.None).ConfigureAwait(false); const int charSize = sizeof(char); @@ -554,7 +555,7 @@ private async ValueTask JoinGameAsync() var spawnSGIni = new IniFile(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SAVED_GAME_SPAWN_INI)); int loadedGameId = spawnSGIni.GetIntValue("Settings", "GameID", -1); - await lanGameLoadingLobby.SetUpAsync(false, client, loadedGameId); + await lanGameLoadingLobby.SetUpAsync(false, client, loadedGameId).ConfigureAwait(false); lanGameLoadingLobby.Enable(); string message = LANCommands.PLAYER_JOIN + ProgramConstants.LAN_DATA_SEPARATOR + @@ -566,12 +567,12 @@ private async ValueTask JoinGameAsync() int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); buffer = buffer[..bytes]; - await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); - await lanGameLoadingLobby.PostJoinAsync(); + await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None).ConfigureAwait(false); + await lanGameLoadingLobby.PostJoinAsync().ConfigureAwait(false); } else { - await lanGameLobby.SetUpAsync(false, hg.EndPoint, client); + await lanGameLobby.SetUpAsync(false, hg.EndPoint, client).ConfigureAwait(false); lanGameLobby.Enable(); string message = LANCommands.PLAYER_JOIN + ProgramConstants.LAN_DATA_SEPARATOR + @@ -582,8 +583,8 @@ private async ValueTask JoinGameAsync() int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); buffer = buffer[..bytes]; - await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None); - await lanGameLobby.PostJoinAsync(); + await client.SendAsync(buffer, SocketFlags.None, CancellationToken.None).ConfigureAwait(false); + await lanGameLobby.PostJoinAsync().ConfigureAwait(false); } } catch (Exception ex) @@ -598,7 +599,7 @@ private async ValueTask BtnMainMenu_LeftClickAsync() { Visible = false; Enabled = false; - await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None); + await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None).ConfigureAwait(false); cancellationTokenSource.Cancel(); socket.Close(); Exited?.Invoke(this, EventArgs.Empty); @@ -609,7 +610,7 @@ private async ValueTask BtnNewGame_LeftClickAsync() if (!ClientConfiguration.Instance.DisableMultiplayerGameLoading) gameCreationWindow.Open(); else - await GameCreationWindow_NewGameAsync(); + await GameCreationWindow_NewGameAsync().ConfigureAwait(false); } public override void Update(GameTime gameTime) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs index 80dfee02e..807bdc962 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetPlayerCountTask.cs @@ -35,8 +35,8 @@ private static async ValueTask RunServiceAsync(CancellationToken cancellationTok using var timeoutCancellationTokenSource = new CancellationTokenSource(REFRESH_TIMEOUT); using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); - CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(await GetCnCNetPlayerCountAsync(linkedCancellationTokenSource.Token))); - await Task.Delay(REFRESH_INTERVAL, cancellationToken); + CnCNetGameCountUpdated?.Invoke(null, new PlayerCountEventArgs(await GetCnCNetPlayerCountAsync(linkedCancellationTokenSource.Token).ConfigureAwait(false))); + await Task.Delay(REFRESH_INTERVAL, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -48,7 +48,7 @@ private static async ValueTask GetCnCNetPlayerCountAsync(CancellationToken { try { - string info = await Constants.CnCNetHttpClient.GetStringAsync($"{Uri.UriSchemeHttps}://api.cncnet.org/status", cancellationToken); + string info = await Constants.CnCNetHttpClient.GetStringAsync($"{Uri.UriSchemeHttps}://api.cncnet.org/status", cancellationToken).ConfigureAwait(false); info = info.Replace("{", string.Empty); info = info.Replace("}", string.Empty); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 19054efb9..7fbf9ffb3 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -165,7 +165,7 @@ public async ValueTask> GetPlayerPortInfoAsync(int playerCount) Logger.Log($"Contacting tunnel at {addressString}"); - string data = await Constants.CnCNetHttpClient.GetStringAsync(addressString); + string data = await Constants.CnCNetHttpClient.GetStringAsync(addressString).ConfigureAwait(false); data = data.Replace("[", string.Empty); data = data.Replace("]", string.Empty); @@ -203,8 +203,8 @@ public async ValueTask UpdatePingAsync() Memory buffer = memoryOwner.Memory[..PING_PACKET_SEND_SIZE]; long ticks = DateTime.Now.Ticks; - await socket.SendToAsync(buffer, SocketFlags.None, ep); - await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep); + await socket.SendToAsync(buffer, SocketFlags.None, ep).ConfigureAwait(false); + await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep).ConfigureAwait(false); ticks = DateTime.Now.Ticks - ticks; PingInMs = new TimeSpan(ticks).Milliseconds; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs index b2b221891..168b81561 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/MapSharer.cs @@ -66,7 +66,7 @@ private static async ValueTask UploadAsync(Map map, string myGameId) Logger.Log("MapSharer: Starting upload of " + map.BaseFilePath); - (string message, bool success) = await MapUploadAsync(map, myGameId); + (string message, bool success) = await MapUploadAsync(map, myGameId).ConfigureAwait(false); if (success) { @@ -115,7 +115,7 @@ private static async ValueTask UploadAsync(Map map, string myGameId) { { "game", gameName.ToLower() } }; - string response = await UploadFilesAsync(files, values); + string response = await UploadFilesAsync(files, values).ConfigureAwait(false); if (!response.Contains("Upload succeeded!")) return (response, false); @@ -133,7 +133,7 @@ private static async ValueTask UploadAsync(Map map, string myGameId) private static async ValueTask UploadFilesAsync(List files, NameValueCollection values) { - var multipartFormDataContent = new MultipartFormDataContent(); + using var multipartFormDataContent = new MultipartFormDataContent(); // Write the values foreach (string name in values.Keys) @@ -151,9 +151,9 @@ private static async ValueTask UploadFilesAsync(List files multipartFormDataContent.Add(streamContent, file.Name, file.Filename); } - HttpResponseMessage httpResponseMessage = await Constants.CnCNetHttpClient.PostAsync($"{MAPDB_URL}upload", multipartFormDataContent); + HttpResponseMessage httpResponseMessage = await Constants.CnCNetHttpClient.PostAsync($"{MAPDB_URL}upload", multipartFormDataContent).ConfigureAwait(false); - return await httpResponseMessage.EnsureSuccessStatusCode().Content.ReadAsStringAsync(); + return await httpResponseMessage.EnsureSuccessStatusCode().Content.ReadAsStringAsync().ConfigureAwait(false); } private static MemoryStream CreateZipFile(string file) @@ -203,7 +203,7 @@ private static async ValueTask DownloadAsync(string sha1, string myGameId, strin ProgramConstants.LogException(ex, "MapSharer ERROR"); } - (string error, bool success) = await DownloadMainAsync(sha1, myGameId, mapName); + (string error, bool success) = await DownloadMainAsync(sha1, myGameId, mapName).ConfigureAwait(false); lock (locker) { @@ -242,7 +242,7 @@ public static string GetMapFileName(string sha1, string mapName) { string address = FormattableString.Invariant($"{MAPDB_URL}{myGame}/{sha1}.zip"); Logger.Log($"MapSharer: Downloading URL: {MAPDB_URL}{address})"); - stream = await Constants.CnCNetHttpClient.GetStreamAsync(address); + stream = await Constants.CnCNetHttpClient.GetStreamAsync(address).ConfigureAwait(false); } catch (Exception ex) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index d0117467d..eceb528bb 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -78,7 +78,7 @@ private void DoCurrentTunnelPinged() private async ValueTask RefreshTunnelsAsync() { - List tunnels = await DoRefreshTunnelsAsync(); + List tunnels = await DoRefreshTunnelsAsync().ConfigureAwait(false); wm.AddCallback(() => HandleRefreshedTunnels(tunnels)); } @@ -116,7 +116,7 @@ private void HandleRefreshedTunnels(List tunnels) private async ValueTask PingListTunnelAsync(int index) { - await Tunnels[index].UpdatePingAsync(); + await Tunnels[index].UpdatePingAsync().ConfigureAwait(false); DoTunnelPinged(index); } @@ -125,7 +125,7 @@ private void PingCurrentTunnel(bool checkTunnelList = false) private async ValueTask PingCurrentTunnelAsync(bool checkTunnelList = false) { - await CurrentTunnel.UpdatePingAsync(); + await CurrentTunnel.UpdatePingAsync().ConfigureAwait(false); DoCurrentTunnelPinged(); if (checkTunnelList) @@ -151,14 +151,14 @@ private static async ValueTask> DoRefreshTunnelsAsync() try { - data = await Constants.CnCNetHttpClient.GetStringAsync(ProgramConstants.CNCNET_TUNNEL_LIST_URL); + data = await Constants.CnCNetHttpClient.GetStringAsync(new Uri(ProgramConstants.CNCNET_TUNNEL_LIST_URL)).ConfigureAwait(false); } catch (HttpRequestException ex) { ProgramConstants.LogException(ex, "Error when downloading tunnel server info. Retrying."); try { - data = await Constants.CnCNetHttpClient.GetStringAsync(ProgramConstants.CNCNET_TUNNEL_LIST_URL); + data = await Constants.CnCNetHttpClient.GetStringAsync(new Uri(ProgramConstants.CNCNET_TUNNEL_LIST_URL)).ConfigureAwait(false); } catch (HttpRequestException ex1) { @@ -170,7 +170,7 @@ private static async ValueTask> DoRefreshTunnelsAsync() } Logger.Log("Fetching tunnel server list failed. Using cached tunnel data."); - data = await File.ReadAllTextAsync(tunnelCacheFile.FullName); + data = await File.ReadAllTextAsync(tunnelCacheFile.FullName).ConfigureAwait(false); } } @@ -219,7 +219,7 @@ private static async ValueTask> DoRefreshTunnelsAsync() if (!clientDirectoryInfo.Exists) clientDirectoryInfo.Create(); - await File.WriteAllTextAsync(tunnelCacheFile.FullName, data); + await File.WriteAllTextAsync(tunnelCacheFile.FullName, data).ConfigureAwait(false); } catch (Exception ex) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs similarity index 78% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index 6e7db9712..8235c3db5 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -7,7 +7,6 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Net.Security; using System.Net.Sockets; using System.ServiceModel; using System.ServiceModel.Description; @@ -17,9 +16,17 @@ using ClientCore; using Rampastring.Tools; -namespace DTAClient.Domain.Multiplayer.CnCNet; - -internal sealed record InternetGatewayDevice(IEnumerable Locations, string Server, string CacheControl, string Ext, string SearchTarget, string UniqueServiceName, UPnPDescription UPnPDescription, Uri PreferredLocation) +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; + +internal sealed record InternetGatewayDevice( + IEnumerable Locations, + string Server, + string CacheControl, + string Ext, + string SearchTarget, + string UniqueServiceName, + UPnPDescription UPnPDescription, + Uri PreferredLocation) { private const int ReceiveTimeout = 10000; private const string UPnPWanConnectionDevice = "urn:schemas-upnp-org:device:WANConnectionDevice"; @@ -36,7 +43,10 @@ internal sealed record InternetGatewayDevice(IEnumerable Locations, string AutomaticDecompression = DecompressionMethods.All, SslOptions = new() { - RemoteCertificateValidationCallback = (_, _, _, sslPolicyErrors) => (sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) == 0, + CertificateChainPolicy = new() + { + DisableCertificateDownloads = true + } } }, true) { @@ -59,7 +69,7 @@ public async ValueTask OpenIpV4PortAsync(IPAddress ipAddress, ushort por string addAnyPortMappingAction = $"\"{service.ServiceType}#AddAnyPortMapping\""; var addAnyPortMappingRequest = new AddAnyPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, PortMappingDescription, IpLeaseTimeInSeconds); AddAnyPortMappingResponse addAnyPortMappingResponse = await ExecuteSoapAction( - serviceUri, addAnyPortMappingAction, serviceType, addAnyPortMappingRequest, cancellationToken); + serviceUri, addAnyPortMappingAction, serviceType, addAnyPortMappingRequest, cancellationToken).ConfigureAwait(false); port = addAnyPortMappingResponse.ReservedPort; @@ -69,7 +79,7 @@ public async ValueTask OpenIpV4PortAsync(IPAddress ipAddress, ushort por var addPortMappingRequest = new AddPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, PortMappingDescription, IpLeaseTimeInSeconds); await ExecuteSoapAction( - serviceUri, addPortMappingAction, serviceType, addPortMappingRequest, cancellationToken); + serviceUri, addPortMappingAction, serviceType, addPortMappingRequest, cancellationToken).ConfigureAwait(false); break; default: @@ -95,14 +105,14 @@ public async ValueTask CloseIpV4PortAsync(ushort port, CancellationToken cancell var deletePortMappingRequestV2 = new DeletePortMappingRequestV2(string.Empty, port, "UDP"); await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, deletePortMappingRequestV2, cancellationToken); + serviceUri, serviceAction, serviceType, deletePortMappingRequestV2, cancellationToken).ConfigureAwait(false); break; case 1: var deletePortMappingRequestV1 = new DeletePortMappingRequestV1(string.Empty, port, "UDP"); await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, deletePortMappingRequestV1, cancellationToken); + serviceUri, serviceAction, serviceType, deletePortMappingRequestV1, cancellationToken).ConfigureAwait(false); break; default: @@ -125,14 +135,14 @@ public async ValueTask GetExternalIpV4AddressAsync(CancellationToken { case 2: GetExternalIPAddressResponseV2 getExternalIpAddressResponseV2 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken); + serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV2.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV2.ExternalIPAddress); break; case 1: GetExternalIPAddressResponseV1 getExternalIpAddressResponseV1 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken); + serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV1.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV1.ExternalIPAddress); break; @@ -158,14 +168,14 @@ public async ValueTask GetNatRsipStatusAsync(CancellationToken cancellatio { case 2: GetNatRsipStatusResponseV2 getNatRsipStatusResponseV2 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken); + serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); natEnabled = getNatRsipStatusResponseV2.NatEnabled; break; case 1: GetNatRsipStatusResponseV1 getNatRsipStatusResponseV1 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken); + serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); natEnabled = getNatRsipStatusResponseV1.NatEnabled; break; @@ -185,7 +195,7 @@ public async ValueTask GetNatRsipStatusAsync(CancellationToken cancellatio (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1"); string serviceAction = $"\"{service.ServiceType}#GetFirewallStatus\""; GetFirewallStatusResponse response = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken); + serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); Logger.Log($"Received IPV6 firewall status {response.FirewallEnabled} and port mapping allowed {response.InboundPinholeAllowed} on UPnP device {UPnPDescription.Device.FriendlyName}."); @@ -200,7 +210,7 @@ public async ValueTask OpenIpV6PortAsync(IPAddress ipAddress, ushort por string serviceAction = $"\"{service.ServiceType}#AddPinhole\""; var request = new AddPinholeRequest(string.Empty, port, ipAddress.ToString(), port, IanaUdpProtocolNumber, IpLeaseTimeInSeconds); AddPinholeResponse response = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, request, cancellationToken); + serviceUri, serviceAction, serviceType, request, cancellationToken).ConfigureAwait(false); Logger.Log($"Opened IPV6 UDP port {port} with ID {response.UniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); @@ -215,12 +225,13 @@ public async ValueTask CloseIpV6PortAsync(ushort uniqueId, CancellationToken can string serviceAction = $"\"{service.ServiceType}#DeletePinhole\""; var request = new DeletePinholeRequest(uniqueId); await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, request, cancellationToken); + serviceUri, serviceAction, serviceType, request, cancellationToken).ConfigureAwait(false); Logger.Log($"Opened IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); } - private static async ValueTask ExecuteSoapAction(string serviceUri, string soapAction, string defaultNamespace, TRequest request, CancellationToken cancellationToken) + private static async ValueTask ExecuteSoapAction( + string serviceUri, string soapAction, string defaultNamespace, TRequest request, CancellationToken cancellationToken) { HttpClient.DefaultRequestHeaders.Remove("SOAPAction"); HttpClient.DefaultRequestHeaders.Add("SOAPAction", soapAction); @@ -232,46 +243,62 @@ private static async ValueTask ExecuteSoapAction }; var requestTypedMessageConverter = TypedMessageConverter.Create(typeof(TRequest), soapAction, defaultNamespace, xmlSerializerFormatAttribute); using var requestMessage = requestTypedMessageConverter.ToMessage(request); - await using var requestStream = new MemoryStream(); - await using var writer = XmlWriter.Create( - requestStream, - new() - { - OmitXmlDeclaration = true, - Async = true, - Encoding = new UTF8Encoding(false) - }); - requestMessage.WriteMessage(writer); - await writer.FlushAsync(); + var requestStream = new MemoryStream(); + HttpResponseMessage httpResponseMessage; - requestStream.Position = 0L; + await using (requestStream) + { + var writer = XmlWriter.Create( + requestStream, + new() + { + OmitXmlDeclaration = true, + Async = true, + Encoding = new UTF8Encoding(false) + }); + + await using (writer.ConfigureAwait(false)) + { + requestMessage.WriteMessage(writer); + await writer.FlushAsync().ConfigureAwait(false); + } - using var content = new StreamContent(requestStream); + requestStream.Position = 0L; - content.Headers.ContentType = MediaTypeHeaderValue.Parse("text/xml"); + using var content = new StreamContent(requestStream); - using HttpResponseMessage httpResponseMessage = await HttpClient.PostAsync(serviceUri, content, cancellationToken); - await using Stream stream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken); + content.Headers.ContentType = MediaTypeHeaderValue.Parse("text/xml"); - try - { - httpResponseMessage.EnsureSuccessStatusCode(); + httpResponseMessage = await HttpClient.PostAsync(serviceUri, content, cancellationToken).ConfigureAwait(false); } - catch (HttpRequestException ex) + + using (httpResponseMessage) { - using var reader = new StreamReader(stream); - string error = await reader.ReadToEndAsync(CancellationToken.None); + Stream stream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - ProgramConstants.LogException(ex, $"UPNP error {ex.StatusCode}:{error}."); + await using (stream.ConfigureAwait(false)) + { + try + { + httpResponseMessage.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) + { + using var reader = new StreamReader(stream); + string error = await reader.ReadToEndAsync(CancellationToken.None).ConfigureAwait(false); - throw; - } + ProgramConstants.LogException(ex, $"UPNP error {ex.StatusCode}:{error}."); - using var envelopeReader = XmlDictionaryReader.CreateTextReader(stream, new()); - using var responseMessage = Message.CreateMessage(envelopeReader, int.MaxValue, MessageVersion.Soap11WSAddressingAugust2004); - var responseTypedMessageConverter = TypedMessageConverter.Create(typeof(TResponse), null, defaultNamespace, xmlSerializerFormatAttribute); + throw; + } - return (TResponse)responseTypedMessageConverter.FromMessage(responseMessage); + using var envelopeReader = XmlDictionaryReader.CreateTextReader(stream, new()); + using var responseMessage = Message.CreateMessage(envelopeReader, int.MaxValue, MessageVersion.Soap11WSAddressingAugust2004); + var responseTypedMessageConverter = TypedMessageConverter.Create(typeof(TResponse), null, defaultNamespace, xmlSerializerFormatAttribute); + + return (TResponse)responseTypedMessageConverter.FromMessage(responseMessage); + } + } } private (ServiceListItem WanIpConnectionService, string ServiceUri, string ServiceType) GetSoapActionParameters(string wanConnectionDeviceService, AddressFamily? addressFamily = null) @@ -294,6 +321,6 @@ private static async ValueTask ExecuteSoapAction private int GetDeviceUPnPVersion() { return $"{UPnPInternetGatewayDevice}:2".Equals(UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase) ? 2 - : ($"{UPnPInternetGatewayDevice}:1".Equals(UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase) ? 1 : 0); + : $"{UPnPInternetGatewayDevice}:1".Equals(UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase) ? 1 : 0; } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 20d6dcfc2..aec701e18 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -8,7 +8,6 @@ using System.Net; using System.Net.Http; using System.Net.NetworkInformation; -using System.Net.Security; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Runtime.Serialization; @@ -17,6 +16,7 @@ using ClientCore; using ClientCore.Extensions; using Rampastring.Tools; +using DTAClient.Domain.Multiplayer.CnCNet.UPNP; namespace DTAClient.Domain.Multiplayer.CnCNet; @@ -33,7 +33,10 @@ internal static class UPnPHandler AutomaticDecompression = DecompressionMethods.All, SslOptions = new() { - RemoteCertificateValidationCallback = (_, _, _, sslPolicyErrors) => (sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) == 0, + CertificateChainPolicy = new() + { + DisableCertificateDownloads = true + } } }, true) @@ -42,21 +45,29 @@ internal static class UPnPHandler DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; - private static IReadOnlyDictionary SsdpMultiCastAddresses => new Dictionary - { - [AddressType.IpV4SiteLocal] = IPAddress.Parse("239.255.255.250"), - [AddressType.IpV6LinkLocal] = IPAddress.Parse("[FF02::C]"), - [AddressType.IpV6SiteLocal] = IPAddress.Parse("[FF05::C]") - }.AsReadOnly(); - - public static async ValueTask<(InternetGatewayDevice InternetGatewayDevice, List<(ushort InternalPort, ushort ExternalPort)> IpV6P2PPorts, List<(ushort InternalPort, ushort ExternalPort)> IpV4P2PPorts, List P2PIpV6PortIds, IPAddress ipV6Address, IPAddress ipV4Address)> SetupPortsAsync( - InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, List stunServerIpAddresses, CancellationToken cancellationToken) + private static IReadOnlyDictionary SsdpMultiCastAddresses + => new Dictionary + { + [AddressType.IpV4SiteLocal] = IPAddress.Parse("239.255.255.250"), + [AddressType.IpV6LinkLocal] = IPAddress.Parse("[FF02::C]"), + [AddressType.IpV6SiteLocal] = IPAddress.Parse("[FF05::C]") + }.AsReadOnly(); + + public static async ValueTask<( + InternetGatewayDevice InternetGatewayDevice, + List<(ushort InternalPort, ushort ExternalPort)> IpV6P2PPorts, + List<(ushort InternalPort, ushort ExternalPort)> IpV4P2PPorts, + List P2PIpV6PortIds, IPAddress IpV6Address, IPAddress IpV4Address)> SetupPortsAsync( + InternetGatewayDevice internetGatewayDevice, + List p2pReservedPorts, + List stunServerIpAddresses, + CancellationToken cancellationToken) { Logger.Log("Starting P2P Setup."); if (internetGatewayDevice is null) { - var internetGatewayDevices = (await GetInternetGatewayDevicesAsync(cancellationToken)).ToList(); + var internetGatewayDevices = (await GetInternetGatewayDevicesAsync(cancellationToken).ConfigureAwait(false)).ToList(); internetGatewayDevice = GetInternetGatewayDevice(internetGatewayDevices, 2); internetGatewayDevice ??= GetInternetGatewayDevice(internetGatewayDevices, 1); @@ -69,8 +80,8 @@ internal static class UPnPHandler { Logger.Log("Found NAT device."); - routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(cancellationToken); - detectedPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken); + routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(cancellationToken).ConfigureAwait(false); + detectedPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken).ConfigureAwait(false); } var ipV4StunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); @@ -85,7 +96,8 @@ internal static class UPnPHandler foreach (ushort p2pReservedPort in p2pReservedPorts) { - IPEndPoint publicIpV4Endpoint = await NetworkHelper.PerformStunAsync(stunServerIpAddress, p2pReservedPort, cancellationToken); + IPEndPoint publicIpV4Endpoint = await NetworkHelper.PerformStunAsync( + stunServerIpAddress, p2pReservedPort, cancellationToken).ConfigureAwait(false); if (publicIpV4Endpoint is null) { @@ -102,9 +114,11 @@ internal static class UPnPHandler if (ipV4StunPortMapping.Any()) { +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed NetworkHelper.KeepStunAliveAsync( stunServerIpAddress, ipV4StunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } } else @@ -116,7 +130,7 @@ internal static class UPnPHandler { Logger.Log("Using IPV4 trace detection."); - detectedPublicIpV4Address = await NetworkHelper.TracePublicIpV4Address(cancellationToken); + detectedPublicIpV4Address = await NetworkHelper.TracePublicIpV4Address(cancellationToken).ConfigureAwait(false); } var publicIpAddresses = NetworkHelper.GetPublicIpAddresses().ToList(); @@ -140,7 +154,8 @@ internal static class UPnPHandler { foreach (int p2PReservedPort in p2pReservedPorts) { - ushort openedPort = await internetGatewayDevice.OpenIpV4PortAsync(privateIpV4Address, (ushort)p2PReservedPort, cancellationToken); + ushort openedPort = await internetGatewayDevice.OpenIpV4PortAsync( + privateIpV4Address, (ushort)p2PReservedPort, cancellationToken).ConfigureAwait(false); ipV4P2PPorts.Add((openedPort, openedPort)); } @@ -172,7 +187,8 @@ internal static class UPnPHandler foreach (ushort p2pReservedPort in p2pReservedPorts) { - IPEndPoint publicIpV6Endpoint = await NetworkHelper.PerformStunAsync(stunServerIpAddress, p2pReservedPort, cancellationToken); + IPEndPoint publicIpV6Endpoint = await NetworkHelper.PerformStunAsync( + stunServerIpAddress, p2pReservedPort, cancellationToken).ConfigureAwait(false); if (publicIpV6Endpoint is null) { @@ -188,9 +204,11 @@ internal static class UPnPHandler if (ipV6StunPortMapping.Any()) { +#pragma warning disable CS4014 NetworkHelper.KeepStunAliveAsync( stunServerIpAddress, ipV6StunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); +#pragma warning restore CS4014 } } else @@ -202,7 +220,8 @@ internal static class UPnPHandler if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - var publicIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses().Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); + var publicIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses() + .Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundPublicIpV6Address = publicIpV6Addresses .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); @@ -232,7 +251,8 @@ internal static class UPnPHandler { try { - (bool firewallEnabled, bool inboundPinholeAllowed) = await internetGatewayDevice.GetIpV6FirewallStatusAsync(cancellationToken); + (bool firewallEnabled, bool inboundPinholeAllowed) = await internetGatewayDevice.GetIpV6FirewallStatusAsync( + cancellationToken).ConfigureAwait(false); if (firewallEnabled && inboundPinholeAllowed) { @@ -240,7 +260,8 @@ internal static class UPnPHandler foreach (ushort p2pReservedPort in p2pReservedPorts) { - p2pIpV6PortIds.Add(await internetGatewayDevice.OpenIpV6PortAsync(publicIpV6Address, p2pReservedPort, cancellationToken)); + p2pIpV6PortIds.Add(await internetGatewayDevice.OpenIpV6PortAsync( + publicIpV6Address, p2pReservedPort, cancellationToken).ConfigureAwait(false)); } } } @@ -267,17 +288,20 @@ internal static class UPnPHandler private static async ValueTask> GetInternetGatewayDevicesAsync(CancellationToken cancellationToken) { - IEnumerable rawDeviceResponses = await GetRawDeviceResponses(cancellationToken); + IEnumerable rawDeviceResponses = await GetRawDeviceResponses(cancellationToken).ConfigureAwait(false); IEnumerable> formattedDeviceResponses = GetFormattedDeviceResponses(rawDeviceResponses); - IEnumerable> groupedInternetGatewayDeviceResponses = GetGroupedInternetGatewayDeviceResponses(formattedDeviceResponses); + IEnumerable> groupedInternetGatewayDeviceResponses = + GetGroupedInternetGatewayDeviceResponses(formattedDeviceResponses); - return await ClientCore.Extensions.TaskExtensions.WhenAllSafe(groupedInternetGatewayDeviceResponses.Select(q => GetInternetGatewayDeviceAsync(q, cancellationToken))); + return await ClientCore.Extensions.TaskExtensions.WhenAllSafe( + groupedInternetGatewayDeviceResponses.Select(q => GetInternetGatewayDeviceAsync(q, cancellationToken))).ConfigureAwait(false); } private static InternetGatewayDevice GetInternetGatewayDevice(List internetGatewayDevices, ushort uPnPVersion) => internetGatewayDevices.SingleOrDefault(q => $"{InternetGatewayDevice.UPnPInternetGatewayDevice}:{uPnPVersion}".Equals(q.UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase)); - private static IEnumerable> GetGroupedInternetGatewayDeviceResponses(IEnumerable> formattedDeviceResponses) + private static IEnumerable> GetGroupedInternetGatewayDeviceResponses( + IEnumerable> formattedDeviceResponses) { return formattedDeviceResponses .Select(q => new InternetGatewayDeviceResponse(new(q["LOCATION"]), q["SERVER"], q["CACHE-CONTROL"], q["EXT"], q["ST"], q["USN"])) @@ -333,11 +357,11 @@ private static async Task> SearchDevicesAsync(IPAddress loca for (int i = 0; i < SendCount; i++) { - await socket.SendToAsync(buffer, SocketFlags.None, multiCastIpEndPoint, cancellationToken); - await Task.Delay(100, cancellationToken); + await socket.SendToAsync(buffer, SocketFlags.None, multiCastIpEndPoint, cancellationToken).ConfigureAwait(false); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } - await ReceiveAsync(socket, responses, cancellationToken); + await ReceiveAsync(socket, responses, cancellationToken).ConfigureAwait(false); } finally { @@ -373,7 +397,7 @@ private static async ValueTask ReceiveAsync(Socket socket, ICollection r try { - int bytesReceived = await socket.ReceiveAsync(buffer, SocketFlags.None, linkedCancellationTokenSource.Token); + int bytesReceived = await socket.ReceiveAsync(buffer, SocketFlags.None, linkedCancellationTokenSource.Token).ConfigureAwait(false); responses.Add(Encoding.UTF8.GetString(buffer.Span[..bytesReceived])); } @@ -385,21 +409,27 @@ private static async ValueTask ReceiveAsync(Socket socket, ICollection r private static async ValueTask GetUPnPDescription(Uri uri, CancellationToken cancellationToken) { - await using Stream uPnPDescription = await HttpClient.GetStreamAsync(uri, cancellationToken); - using var xmlTextReader = new XmlTextReader(uPnPDescription); + Stream uPnPDescription = await HttpClient.GetStreamAsync(uri, cancellationToken).ConfigureAwait(false); + + await using (uPnPDescription.ConfigureAwait(false)) + { + using var xmlTextReader = new XmlTextReader(uPnPDescription); - return (UPnPDescription)new DataContractSerializer(typeof(UPnPDescription)).ReadObject(xmlTextReader); + return (UPnPDescription)new DataContractSerializer(typeof(UPnPDescription)).ReadObject(xmlTextReader); + } } private static async ValueTask> GetRawDeviceResponses(CancellationToken cancellationToken) { IEnumerable localAddresses = NetworkHelper.GetLocalAddresses(); - IEnumerable[] localAddressesDeviceResponses = await ClientCore.Extensions.TaskExtensions.WhenAllSafe(localAddresses.Select(q => SearchDevicesAsync(q, cancellationToken))); + IEnumerable[] localAddressesDeviceResponses = await ClientCore.Extensions.TaskExtensions.WhenAllSafe( + localAddresses.Select(q => SearchDevicesAsync(q, cancellationToken))).ConfigureAwait(false); return localAddressesDeviceResponses.Where(q => q.Any()).SelectMany(q => q).Distinct(); } - private static async Task GetInternetGatewayDeviceAsync(IGrouping internetGatewayDeviceResponses, CancellationToken cancellationToken) + private static async Task GetInternetGatewayDeviceAsync( + IGrouping internetGatewayDeviceResponses, CancellationToken cancellationToken) { Uri[] locations = internetGatewayDeviceResponses.Select(r => r.Location).ToArray(); Uri location = GetPreferredLocation(locations); @@ -407,7 +437,7 @@ private static async Task GetInternetGatewayDeviceAsync(I try { - uPnPDescription = await GetUPnPDescription(location, cancellationToken); + uPnPDescription = await GetUPnPDescription(location, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -417,7 +447,7 @@ private static async Task GetInternetGatewayDeviceAsync(I { location = locations.First(q => q.HostNameType is UriHostNameType.IPv4); - uPnPDescription = await GetUPnPDescription(location, cancellationToken); + uPnPDescription = await GetUPnPDescription(location, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index ced227b16..e1b682934 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using ClientCore; +using DTAClient.Domain.Multiplayer.CnCNet.UPNP; namespace DTAClient.Domain.Multiplayer.CnCNet; @@ -91,7 +92,7 @@ public async ValueTask HandlePlayerP2PRequestAsync() StunCancellationTokenSource = new(); (internetGatewayDevice, IpV6P2PPorts, IpV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync( - internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? InitialTunnel.IPAddresses, StunCancellationTokenSource.Token); + internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? InitialTunnel.IPAddresses, StunCancellationTokenSource.Token).ConfigureAwait(false); } return publicIpV4Address is not null || publicIpV6Address is not null; @@ -117,7 +118,7 @@ public async ValueTask ToggleP2PAsync() if (P2PEnabled) return true; - await CloseP2PPortsAsync(); + await CloseP2PPortsAsync().ConfigureAwait(false); internetGatewayDevice = null; publicIpV4Address = null; @@ -135,7 +136,7 @@ public async ValueTask PingRemotePlayer(string playerName, string p2pReque if (IPAddress.TryParse(ipV4splitLines[0], out IPAddress parsedIpV4Address)) { - long? pingResult = await NetworkHelper.PingAsync(parsedIpV4Address); + long? pingResult = await NetworkHelper.PingAsync(parsedIpV4Address).ConfigureAwait(false); if (pingResult is not null) localPingResults.Add((parsedIpV4Address, pingResult.Value)); @@ -143,7 +144,7 @@ public async ValueTask PingRemotePlayer(string playerName, string p2pReque if (IPAddress.TryParse(ipV6splitLines[0], out IPAddress parsedIpV6Address)) { - long? pingResult = await NetworkHelper.PingAsync(parsedIpV6Address); + long? pingResult = await NetworkHelper.PingAsync(parsedIpV6Address).ConfigureAwait(false); if (pingResult is not null) localPingResults.Add((parsedIpV6Address, pingResult.Value)); @@ -316,7 +317,7 @@ public async ValueTask DisposeAsync() PlayerTunnels.Clear(); P2PPlayers.Clear(); PinnedTunnels?.Clear(); - await CloseP2PPortsAsync(); + await CloseP2PPortsAsync().ConfigureAwait(false); } private IEnumerable GetEligibleTunnels() @@ -329,7 +330,7 @@ private async ValueTask CloseP2PPortsAsync() if (internetGatewayDevice is not null) { foreach (ushort p2pPort in IpV4P2PPorts.Select(q => q.InternalPort)) - await internetGatewayDevice.CloseIpV4PortAsync(p2pPort); + await internetGatewayDevice.CloseIpV4PortAsync(p2pPort).ConfigureAwait(false); } } catch (Exception ex) @@ -346,7 +347,7 @@ private async ValueTask CloseP2PPortsAsync() if (internetGatewayDevice is not null) { foreach (ushort p2pIpV6PortId in p2pIpV6PortIds) - await internetGatewayDevice.CloseIpV6PortAsync(p2pIpV6PortId); + await internetGatewayDevice.CloseIpV6PortAsync(p2pIpV6PortId).ConfigureAwait(false); } } catch (Exception ex) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs index be930c646..9922e604e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs @@ -32,6 +32,7 @@ internal sealed class V3LocalPlayerConnection : IDisposable /// Creates a local game socket and returns the port. /// /// The id of the player for which to create the local game socket. + /// The to stop the connection. /// The port of the created socket. public ushort Setup(uint playerId, CancellationToken cancellationToken) { @@ -83,7 +84,8 @@ public async ValueTask StartConnectionAsync() try { - SocketReceiveFromResult socketReceiveFromResult = await localGameSocket.ReceiveFromAsync(buffer, SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token); + SocketReceiveFromResult socketReceiveFromResult = await localGameSocket.ReceiveFromAsync( + buffer, SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); remotePlayerEndPoint = socketReceiveFromResult.RemoteEndPoint; data = buffer[..socketReceiveFromResult.ReceivedBytes]; @@ -147,7 +149,7 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data) Logger.Log($"Sending data from {localGameSocket.LocalEndPoint} to local game {remotePlayerEndPoint} for player {playerId}."); #endif - await localGameSocket.SendToAsync(data, SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token); + await localGameSocket.SendToAsync(data, SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); } catch (SocketException ex) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index c8adfa008..a0c23341d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -80,7 +80,7 @@ public async ValueTask StartConnectionAsync() try { - await tunnelSocket.SendToAsync(buffer, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token); + await tunnelSocket.SendToAsync(buffer, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); } catch (SocketException ex) { @@ -115,7 +115,7 @@ public async ValueTask StartConnectionAsync() Logger.Log($"Connection using {localPort} established."); #endif OnRaiseConnectedEvent(EventArgs.Empty); - await ReceiveLoopAsync(); + await ReceiveLoopAsync().ConfigureAwait(false); } /// @@ -147,7 +147,7 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data, uint receiverId) Logger.Log($"Sending data {gameLocalPlayerId} -> {receiverId} from {tunnelSocket.LocalEndPoint} to {remoteEndPoint}."); #endif - await tunnelSocket.SendToAsync(packet, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token); + await tunnelSocket.SendToAsync(packet, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); } catch (SocketException ex) { @@ -205,7 +205,8 @@ private async ValueTask ReceiveLoopAsync() try { - socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync(buffer, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token); + socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync( + buffer, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); } catch (SocketException ex) { diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index b5b793a57..2d7f6cb7e 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -55,7 +55,7 @@ public async Task UpdateAsync(GameTime gameTime) if (TimeSinceLastSentMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT) || TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(SEND_PING_TIMEOUT)) - await SendMessageAsync(LANCommands.PING, default); + await SendMessageAsync(LANCommands.PING, default).ConfigureAwait(false); if (TimeSinceLastReceivedMessage > TimeSpan.FromSeconds(DROP_TIMEOUT)) return false; @@ -97,11 +97,14 @@ public async ValueTask SendMessageAsync(string message, CancellationToken cancel try { - await TcpClient.SendAsync(buffer, cancellationToken); + await TcpClient.SendAsync(buffer, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { } + catch (SocketException) + { + } catch (Exception ex) { ProgramConstants.LogException(ex, "Sending message to " + ToString() + " failed!"); @@ -127,13 +130,18 @@ public async ValueTask StartReceiveLoopAsync(CancellationToken cancellationToken try { - bytesRead = await TcpClient.ReceiveAsync(message, cancellationToken); + bytesRead = await TcpClient.ReceiveAsync(message, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { ConnectionLost?.Invoke(this, EventArgs.Empty); break; } + catch (SocketException) + { + ConnectionLost?.Invoke(this, EventArgs.Empty); + break; + } catch (Exception ex) { ProgramConstants.LogException(ex, "Socket error with client " + Name + "; removing."); diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index eb33015b5..e6bdea921 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Text.Json.Serialization; +using System.Threading.Tasks; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; using Exception = System.Exception; @@ -274,8 +275,10 @@ public void CalculateSHA() /// /// The configuration file for the multiplayer maps. /// True if loading the map succeeded, otherwise false. - public bool SetInfoFromMpMapsINI(IniFile iniFile) + public async ValueTask SetInfoFromMpMapsINIAsync(IniFile iniFile) { + await ValueTask.CompletedTask.ConfigureAwait(false); + try { string baseSectionName = iniFile.GetStringValue(BaseFilePath, "BaseSection", string.Empty); @@ -402,7 +405,7 @@ public bool SetInfoFromMpMapsINI(IniFile iniFile) #if !GL if (UserINISettings.Instance.PreloadMapPreviews) - PreviewTexture = LoadPreviewTexture(); + PreviewTexture = await LoadPreviewTextureAsync().ConfigureAwait(false); #endif // Parse forced options @@ -681,7 +684,7 @@ private void ParseSpawnIniOptions(IniFile forcedOptionsIni, string spawnIniOptio /// /// Loads and returns the map preview texture. /// - public Texture2D LoadPreviewTexture() + public async ValueTask LoadPreviewTextureAsync() { if (SafePath.GetFile(ProgramConstants.GamePath, PreviewPath).Exists) return AssetLoader.LoadTextureUncached(PreviewPath); @@ -689,7 +692,7 @@ public Texture2D LoadPreviewTexture() if (!Official) { // Extract preview from the map itself - using Image preview = MapPreviewExtractor.ExtractMapPreview(GetCustomMapIniFile()); + using Image preview = await MapPreviewExtractor.ExtractMapPreviewAsync(GetCustomMapIniFile()).ConfigureAwait(false); if (preview != null) { diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index a831532d7..f70160c47 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -63,14 +63,14 @@ public async Task LoadMapsAsync() LoadGameModes(mpMapsIni); LoadGameModeAliases(mpMapsIni); - LoadMultiMaps(mpMapsIni); - await LoadCustomMapsAsync(); + await LoadMultiMapsAsync(mpMapsIni).ConfigureAwait(false); + await LoadCustomMapsAsync().ConfigureAwait(false); GameModes.RemoveAll(g => g.Maps.Count < 1); GameModeMaps = new GameModeMapCollection(GameModes); } - private void LoadMultiMaps(IniFile mpMapsIni) + private async ValueTask LoadMultiMapsAsync(IniFile mpMapsIni) { List keys = mpMapsIni.GetSectionKeys(MultiMapsSection); @@ -96,7 +96,7 @@ private void LoadMultiMaps(IniFile mpMapsIni) var map = new Map(mapFilePathValue, false); - if (!map.SetInfoFromMpMapsINI(mpMapsIni)) + if (!await map.SetInfoFromMpMapsINIAsync(mpMapsIni).ConfigureAwait(false)) continue; maps.Add(map); @@ -151,7 +151,7 @@ private async ValueTask LoadCustomMapsAsync() } IEnumerable mapFiles = customMapsDirectory.EnumerateFiles($"*{MAP_FILE_EXTENSION}"); - ConcurrentDictionary customMapCache = await LoadCustomMapCacheAsync(); + ConcurrentDictionary customMapCache = await LoadCustomMapCacheAsync().ConfigureAwait(false); var localMapSHAs = new List(); var tasks = new List(); @@ -172,7 +172,7 @@ private async ValueTask LoadCustomMapsAsync() })); } - await ClientCore.Extensions.TaskExtensions.WhenAllSafe(tasks.ToArray()); + await ClientCore.Extensions.TaskExtensions.WhenAllSafe(tasks.ToArray()).ConfigureAwait(false); // remove cached maps that no longer exist locally foreach (var missingSHA in customMapCache.Keys.Where(cachedSHA => !localMapSHAs.Contains(cachedSHA))) @@ -213,12 +213,17 @@ private async ValueTask> LoadCustomMapCacheAsy { try { - await using var jsonData = File.OpenRead(CUSTOM_MAPS_CACHE); - var customMapCache = await JsonSerializer.DeserializeAsync(jsonData, jsonSerializerOptions); - var customMaps = customMapCache?.Version == CurrentCustomMapCacheVersion && customMapCache.Maps != null - ? customMapCache.Maps : new ConcurrentDictionary(); + FileStream jsonData = File.OpenRead(CUSTOM_MAPS_CACHE); + CustomMapCache customMapCache; - foreach (var customMap in customMaps.Values) + await using (jsonData.ConfigureAwait(false)) + { + customMapCache = await JsonSerializer.DeserializeAsync(jsonData, jsonSerializerOptions).ConfigureAwait(false); + } + + ConcurrentDictionary customMaps = !(customMapCache?.Version != CurrentCustomMapCacheVersion || customMapCache.Maps == null) ? customMapCache.Maps : new(); + + foreach (Map customMap in customMaps.Values) customMap.AfterDeserialize(); return customMaps; diff --git a/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs b/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs index f6835089c..91f33b043 100644 --- a/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs +++ b/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs @@ -4,6 +4,7 @@ using System.IO; using System.IO.Compression; using System.Text; +using System.Threading.Tasks; using ClientCore; using Rampastring.Tools; using lzo.net; @@ -24,7 +25,7 @@ public static class MapPreviewExtractor /// /// Map file. /// Bitmap of map preview image, or null if preview could not be extracted. - public static Image ExtractMapPreview(IniFile mapIni) + public static async ValueTask ExtractMapPreviewAsync(IniFile mapIni) { List sectionKeys = mapIni.GetSectionKeys("PreviewPack"); @@ -72,7 +73,7 @@ public static Image ExtractMapPreview(IniFile mapIni) return null; } - byte[] dataDest = DecompressPreviewData(dataSource, previewWidth * previewHeight * 3, out string errorMessage); + (byte[] dataDest, string errorMessage) = await DecompressPreviewDataAsync(dataSource, previewWidth * previewHeight * 3).ConfigureAwait(false); if (errorMessage != null) { @@ -80,7 +81,7 @@ public static Image ExtractMapPreview(IniFile mapIni) return null; } - Image bitmap = CreatePreviewBitmapFromImageData(previewWidth, previewHeight, dataDest, out errorMessage); + (Image bitmap, errorMessage) = await Task.Run(() => CreatePreviewBitmapFromImageData(previewWidth, previewHeight, dataDest)).ConfigureAwait(false); if (errorMessage != null) { @@ -96,14 +97,14 @@ public static Image ExtractMapPreview(IniFile mapIni) /// /// Array of compressed map preview image data. /// Size of decompressed preview image data. - /// Will be set to error message if something went wrong, otherwise null. /// Array of decompressed preview image data if successfully decompressed, otherwise null. - private static byte[] DecompressPreviewData(byte[] dataSource, int decompressedDataSize, out string errorMessage) + private static async ValueTask<(byte[] Data, string ErrorMessage)> DecompressPreviewDataAsync(byte[] dataSource, int decompressedDataSize) { try { byte[] dataDest = new byte[decompressedDataSize]; - int readBytes = 0, writtenBytes = 0; + int readBytes = 0; + int writtenBytes = 0; while (true) { @@ -121,24 +122,27 @@ private static byte[] DecompressPreviewData(byte[] dataSource, int decompressedD if (readBytes + sizeCompressed > dataSource.Length || writtenBytes + sizeUncompressed > dataDest.Length) { - errorMessage = "Preview data does not match preview size or the data is corrupted, unable to extract preview."; - return null; + return (null, "Preview data does not match preview size or the data is corrupted, unable to extract preview."); + } + + var stream = new LzoStream(new MemoryStream(dataSource, readBytes, sizeCompressed), CompressionMode.Decompress); + + await using (stream.ConfigureAwait(false)) + { + await stream.ReadAsync(dataDest, writtenBytes, sizeUncompressed).ConfigureAwait(false); } - LzoStream stream = new LzoStream(new MemoryStream(dataSource, readBytes, sizeCompressed), CompressionMode.Decompress); - stream.Read(dataDest, writtenBytes, sizeUncompressed); readBytes += sizeCompressed; writtenBytes += sizeUncompressed; } - errorMessage = null; - return dataDest; + return (dataDest, null); } catch (Exception ex) { ProgramConstants.LogException(ex, "Error encountered decompressing preview data."); - errorMessage = "Error encountered decompressing preview data. Message: " + ex.Message; - return null; + + return (null, "Error encountered decompressing preview data. Message: " + ex.Message); } } @@ -148,17 +152,15 @@ private static byte[] DecompressPreviewData(byte[] dataSource, int decompressedD /// Width of the bitmap. /// Height of the bitmap. /// Raw image pixel data in 24-bit RGB format. - /// Will be set to error message if something went wrong, otherwise null. /// Bitmap based on the provided dimensions and raw image data, or null if length of image data does not match the provided dimensions or if something went wrong. - private static Image CreatePreviewBitmapFromImageData(int width, int height, byte[] imageData, out string errorMessage) + private static (Image Image, string ErrorMessage) CreatePreviewBitmapFromImageData(int width, int height, byte[] imageData) { const int pixelFormatBitCount = 24; const int pixelFormatByteCount = pixelFormatBitCount / 8; if (imageData.Length != width * height * pixelFormatByteCount) { - errorMessage = "Provided preview image dimensions do not match preview image data length."; - return null; + return (null, "Provided preview image dimensions do not match preview image data length."); } try @@ -210,15 +212,13 @@ private static Image CreatePreviewBitmapFromImageData(int width, int height, byt } } - errorMessage = null; - - return image; + return (image, null); } catch (Exception ex) { ProgramConstants.LogException(ex, "Error encountered creating preview bitmap."); - errorMessage = "Error encountered creating preview bitmap. Message: " + ex.Message; - return null; + + return (null, "Error encountered creating preview bitmap. Message: " + ex.Message); } } } diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index c0fe4bd9f..4c323590b 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -16,6 +16,7 @@ internal static class NetworkHelper { private const string PingHost = "cncnet.org"; private const int PingTimeout = 1000; + private const int MinimumUdpPort = 1024; private static readonly IReadOnlyCollection SupportedAddressFamilies = new[] { @@ -101,7 +102,7 @@ public static async ValueTask TracePublicIpV4Address(CancellationToke try { - PingReply pingResult = await ping.SendPingAsync(ipAddress, PingTimeout); + PingReply pingResult = await ping.SendPingAsync(ipAddress, PingTimeout).ConfigureAwait(false); if (pingResult.Status is IPStatus.Success) return pingResult.RoundtripTime; @@ -143,9 +144,10 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI stunServerIpEndPoint = new IPEndPoint(stunServerIpAddress, stunPort); - await socket.SendToAsync(buffer, stunServerIpEndPoint, linkedCancellationTokenSource.Token); + await socket.SendToAsync(buffer, stunServerIpEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); - SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync(buffer, SocketFlags.None, stunServerIpEndPoint, linkedCancellationTokenSource.Token); + SocketReceiveFromResult socketReceiveFromResult = await socket.ReceiveFromAsync( + buffer, SocketFlags.None, stunServerIpEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); buffer = buffer[..socketReceiveFromResult.ReceivedBytes]; @@ -179,11 +181,11 @@ public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List< { foreach (ushort localPort in localPorts) { - await PerformStunAsync(stunServerIpAddress, localPort, cancellationToken); - await Task.Delay(100, cancellationToken); + await PerformStunAsync(stunServerIpAddress, localPort, cancellationToken).ConfigureAwait(false); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } - await Task.Delay(5000, cancellationToken); + await Task.Delay(5000, cancellationToken).ConfigureAwait(false); } catch (TaskCanceledException) { @@ -200,12 +202,12 @@ public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List< public static IEnumerable GetFreeUdpPorts(IEnumerable excludedPorts, ushort numberOfPorts) { IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); - List activePorts = endPoints.Select(q => (ushort)q.Port).ToArray().Concat(excludedPorts).ToList(); + var activePorts = endPoints.Select(q => (ushort)q.Port).ToArray().Concat(excludedPorts).ToList(); ushort foundPortCount = 0; while (foundPortCount != numberOfPorts) { - ushort foundPort = (ushort)new Random().Next(1024, IPEndPoint.MaxPort); + ushort foundPort = (ushort)new Random().Next(MinimumUdpPort, IPEndPoint.MaxPort); if (!activePorts.Contains(foundPort)) { diff --git a/DXMainClient/Domain/SavedGame.cs b/DXMainClient/Domain/SavedGame.cs index 7983a7cbd..47c300d90 100644 --- a/DXMainClient/Domain/SavedGame.cs +++ b/DXMainClient/Domain/SavedGame.cs @@ -29,7 +29,7 @@ public SavedGame(string fileName) /// private static string GetArchiveName(Stream file) { - var cf = new CompoundFile(file); + using var cf = new CompoundFile(file); var archiveNameBytes = cf.RootStorage.GetStream("Scenario Description").GetData(); var archiveName = System.Text.Encoding.Unicode.GetString(archiveNameBytes); archiveName = archiveName.TrimEnd(new char[] { '\0' }); diff --git a/DXMainClient/Online/Channel.cs b/DXMainClient/Online/Channel.cs index 40a3f5ece..dea8b39d0 100644 --- a/DXMainClient/Online/Channel.cs +++ b/DXMainClient/Online/Channel.cs @@ -116,7 +116,7 @@ public void AddUser(ChannelUser user) public async ValueTask OnUserJoinedAsync(ChannelUser user) { - await Task.CompletedTask; + await Task.CompletedTask.ConfigureAwait(false); AddUser(user); if (notifyOnUserListChange) @@ -127,7 +127,7 @@ public async ValueTask OnUserJoinedAsync(ChannelUser user) #if !YR if (Persistent && IsChatChannel && user.IRCUser.Name == ProgramConstants.PLAYERNAME) - await RequestUserInfoAsync(); + await RequestUserInfoAsync().ConfigureAwait(false); #endif } @@ -336,11 +336,11 @@ public async ValueTask LeaveAsync() if (Persistent) { int rn = connection.Rng.Next(1, 10000); - await connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, IRCCommands.PART + " " + ChannelName); + await connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, rn, IRCCommands.PART + " " + ChannelName).ConfigureAwait(false); } else { - await connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, IRCCommands.PART + " " + ChannelName); + await connection.QueueMessageAsync(QueuedMessageType.SYSTEM_MESSAGE, 9, IRCCommands.PART + " " + ChannelName).ConfigureAwait(false); } ClearUsers(); diff --git a/DXMainClient/Online/CnCNetGameCheck.cs b/DXMainClient/Online/CnCNetGameCheck.cs index f878b20d0..86a634a79 100644 --- a/DXMainClient/Online/CnCNetGameCheck.cs +++ b/DXMainClient/Online/CnCNetGameCheck.cs @@ -17,7 +17,7 @@ public static async ValueTask RunServiceAsync(CancellationToken cancellationToke { try { - await Task.Delay(REFRESH_INTERVAL, cancellationToken); + await Task.Delay(REFRESH_INTERVAL, cancellationToken).ConfigureAwait(false); CheatEngineWatchEvent(); } diff --git a/DXMainClient/Online/CnCNetManager.cs b/DXMainClient/Online/CnCNetManager.cs index b53a6ff36..53bb17c25 100644 --- a/DXMainClient/Online/CnCNetManager.cs +++ b/DXMainClient/Online/CnCNetManager.cs @@ -348,7 +348,7 @@ private void DoChatMessageReceived(string receiver, string senderName, string id foreColor = cDefaultChatColor; } - if (message.Length > 1 && message[message.Length - 1] == '\u001f') + if (message.Length > 1 && message[^1] == '\u001f') message = message.Remove(message.Length - 1); ChannelUser user = channel.Users.Find(senderName); @@ -596,7 +596,7 @@ private async ValueTask DoUserJoinedChannelAsync(string channelName, string host channelUser.IsFriend = cncNetUserData.IsFriend(channelUser.IRCUser.Name); ircUser.Channels.Add(channelName); - await channel.OnUserJoinedAsync(channelUser); + await channel.OnUserJoinedAsync(channelUser).ConfigureAwait(false); } private void AddUserToGlobalUserList(IRCUser user) @@ -854,7 +854,7 @@ private async ValueTask DoNameAlreadyInUseAsync() MainChannel.AddMessage(new ChatMessage(Color.White, "Your nickname is invalid or already in use. Please change your nickname in the login screen.".L10N("Client:Main:PickAnotherNickName"))); UserINISettings.Instance.SkipConnectDialog.Value = false; - await DisconnectAsync(); + await DisconnectAsync().ConfigureAwait(false); return; } @@ -869,7 +869,7 @@ private async ValueTask DoNameAlreadyInUseAsync() string.Format("Your name is already in use. Retrying with {0}...".L10N("Client:Main:NameInUseRetry"), sb))); ProgramConstants.PLAYERNAME = sb.ToString(); - await connection.ChangeNicknameAsync(); + await connection.ChangeNicknameAsync().ConfigureAwait(false); } public void OnBannedFromChannel(string channelName) diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index 1ad177385..b7c69c877 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -128,7 +128,7 @@ private async ValueTask ConnectToServerAsync(CancellationToken cancellationToken { try { - IList sortedServerList = await GetServerListSortedByLatencyAsync(); + IList sortedServerList = await GetServerListSortedByLatencyAsync().ConfigureAwait(false); foreach (Server server in sortedServerList) { @@ -148,7 +148,7 @@ private async ValueTask ConnectToServerAsync(CancellationToken cancellationToken { await client.ConnectAsync( new IPEndPoint(IPAddress.Parse(server.Host), port), - linkedCancellationTokenSource.Token); + linkedCancellationTokenSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) when (timeoutCancellationTokenSource.Token.IsCancellationRequested) { @@ -175,7 +175,7 @@ await client.ConnectAsync( socket = client; currentConnectedServerIP = server.Host; - await HandleCommAsync(cancellationToken); + await HandleCommAsync(cancellationToken).ConfigureAwait(false); return; } } @@ -206,7 +206,7 @@ private async ValueTask HandleCommAsync(CancellationToken cancellationToken) using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(1024); Memory message = memoryOwner.Memory[..1024]; - await RegisterAsync(); + await RegisterAsync().ConfigureAwait(false); var timer = new System.Timers.Timer(PingInterval) { @@ -223,7 +223,7 @@ private async ValueTask HandleCommAsync(CancellationToken cancellationToken) try { - bytesRead = await socket.ReceiveAsync(message, SocketFlags.None, cancellationToken); + bytesRead = await socket.ReceiveAsync(message, SocketFlags.None, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -256,7 +256,7 @@ private async ValueTask HandleCommAsync(CancellationToken cancellationToken) #if !DEBUG Logger.Log("Message received: " + msg); #endif - await HandleMessageAsync(msg); + await HandleMessageAsync(msg).ConfigureAwait(false); timer.Interval = 30000; } @@ -280,7 +280,7 @@ private async ValueTask HandleCommAsync(CancellationToken cancellationToken) while (!sendQueueExited) { - await Task.Delay(100, cancellationToken); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } reconnectCount++; @@ -291,7 +291,7 @@ private async ValueTask HandleCommAsync(CancellationToken cancellationToken) return; } - await Task.Delay(RECONNECT_WAIT_DELAY, cancellationToken); + await Task.Delay(RECONNECT_WAIT_DELAY, cancellationToken).ConfigureAwait(false); if (IsConnected || AttemptingConnection) { @@ -313,7 +313,8 @@ private async ValueTask HandleCommAsync(CancellationToken cancellationToken) private async ValueTask> GetServerListSortedByLatencyAsync() { // Resolve the hostnames. - IEnumerable<(IPAddress IpAddress, string Name, int[] Ports)>[] servers = await ClientCore.Extensions.TaskExtensions.WhenAllSafe(Servers.Select(ResolveServerAsync)); + IEnumerable<(IPAddress IpAddress, string Name, int[] Ports)>[] servers = + await ClientCore.Extensions.TaskExtensions.WhenAllSafe(Servers.Select(ResolveServerAsync)).ConfigureAwait(false); // Group the tuples by IPAddress to merge duplicate servers. IEnumerable> serverInfosGroupedByIPAddress = servers @@ -354,7 +355,7 @@ private async ValueTask> GetServerListSortedByLatencyAsync() } (Server Server, IPAddress IpAddress, long Result)[] serverAndLatencyResults = - await ClientCore.Extensions.TaskExtensions.WhenAllSafe(serverInfos.Where(q => !failedServerIPs.Contains(q.IpAddress.ToString())).Select(PingServerAsync)); + await ClientCore.Extensions.TaskExtensions.WhenAllSafe(serverInfos.Where(q => !failedServerIPs.Contains(q.IpAddress.ToString())).Select(PingServerAsync)).ConfigureAwait(false); // Sort the servers by AddressFamily & latency. (Server Server, IPAddress IpAddress, long Result)[] sortedServerAndLatencyResults = serverAndLatencyResults @@ -401,7 +402,7 @@ private async ValueTask> GetServerListSortedByLatencyAsync() try { - PingReply pingReply = await ping.SendPingAsync(serverInfo.IpAddress, MAXIMUM_LATENCY); + PingReply pingReply = await ping.SendPingAsync(serverInfo.IpAddress, MAXIMUM_LATENCY).ConfigureAwait(false); if (pingReply.Status == IPStatus.Success) { @@ -431,7 +432,7 @@ private async ValueTask> GetServerListSortedByLatencyAsync() try { // If hostNameOrAddress is an IP address, this address is returned without querying the DNS server. - IPAddress[] serverIPAddresses = (await Dns.GetHostAddressesAsync(server.Host)) + IPAddress[] serverIPAddresses = (await Dns.GetHostAddressesAsync(server.Host).ConfigureAwait(false)) .Where(IPAddress => IPAddress.AddressFamily is AddressFamily.InterNetworkV6 or AddressFamily.InterNetwork) .ToArray(); @@ -451,7 +452,7 @@ private async ValueTask> GetServerListSortedByLatencyAsync() public async ValueTask DisconnectAsync() { - await SendMessageAsync(IRCCommands.QUIT); + await SendMessageAsync(IRCCommands.QUIT).ConfigureAwait(false); connectionCancellationTokenSource.Cancel(); socket.Shutdown(SocketShutdown.Both); socket.Close(); @@ -480,14 +481,14 @@ private async ValueTask HandleMessageAsync(string message) else if (msg.Length != commandEndIndex + 1) { string command = msg[..(commandEndIndex - 1)]; - await PerformCommandAsync(command); + await PerformCommandAsync(command).ConfigureAwait(false); msg = msg.Remove(0, commandEndIndex + 1); } else { string command = msg[..^1]; - await PerformCommandAsync(command); + await PerformCommandAsync(command).ConfigureAwait(false); break; } } @@ -590,7 +591,7 @@ private async ValueTask PerformCommandAsync(string message) connectionManager.OnNameAlreadyInUse(); break; case 451: // Not registered - await RegisterAsync(); + await RegisterAsync().ConfigureAwait(false); connectionManager.OnGenericServerMessageReceived(message); break; case 471: // Returned when attempting to join a channel that is full (basically, player limit met) @@ -629,7 +630,7 @@ private async ValueTask PerformCommandAsync(string message) } string noticeUserName = prefix[..noticeExclamIndex]; - string notice = parameters[parameters.Count - 1]; + string notice = parameters[^1]; connectionManager.OnNoticeMessageParsed(notice, noticeUserName); break; } @@ -666,7 +667,7 @@ private async ValueTask PerformCommandAsync(string message) string[] recipients = new string[parameters.Count - 1]; for (int pid = 0; pid < parameters.Count - 1; pid++) recipients[pid] = parameters[pid]; - string privmsg = parameters[parameters.Count - 1]; + string privmsg = parameters[^1]; if (parameters[1].StartsWith('\u0001' + IRCCommands.PRIVMSG_ACTION)) privmsg = privmsg[1..].Remove(privmsg.Length - 2); foreach (string recipient in recipients) @@ -696,12 +697,12 @@ private async ValueTask PerformCommandAsync(string message) case IRCCommands.PING: if (parameters.Count > 0) { - await QueueMessageAsync(new QueuedMessage(IRCCommands.PONG + " " + parameters[0], QueuedMessageType.SYSTEM_MESSAGE, 5000)); + await QueueMessageAsync(new QueuedMessage(IRCCommands.PONG + " " + parameters[0], QueuedMessageType.SYSTEM_MESSAGE, 5000)).ConfigureAwait(false); Logger.Log(IRCCommands.PONG + " " + parameters[0]); } else { - await QueueMessageAsync(new QueuedMessage(IRCCommands.PONG, QueuedMessageType.SYSTEM_MESSAGE, 5000)); + await QueueMessageAsync(new QueuedMessage(IRCCommands.PONG, QueuedMessageType.SYSTEM_MESSAGE, 5000)).ConfigureAwait(false); Logger.Log(IRCCommands.PONG); } break; @@ -815,7 +816,7 @@ private async ValueTask RunSendQueueAsync(CancellationToken cancellationToken) { string message = string.Empty; - await messageQueueLocker.WaitAsync(cancellationToken); + await messageQueueLocker.WaitAsync(cancellationToken).ConfigureAwait(false); try { @@ -849,12 +850,12 @@ private async ValueTask RunSendQueueAsync(CancellationToken cancellationToken) if (string.IsNullOrEmpty(message)) { - await Task.Delay(10, cancellationToken); + await Task.Delay(10, cancellationToken).ConfigureAwait(false); continue; } - await SendMessageAsync(message); - await Task.Delay(messageQueueDelay, cancellationToken); + await SendMessageAsync(message).ConfigureAwait(false); + await Task.Delay(messageQueueDelay, cancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -862,7 +863,7 @@ private async ValueTask RunSendQueueAsync(CancellationToken cancellationToken) } finally { - await messageQueueLocker.WaitAsync(CancellationToken.None); + await messageQueueLocker.WaitAsync(CancellationToken.None).ConfigureAwait(false); try { @@ -896,8 +897,8 @@ private async ValueTask RegisterAsync() string defaultGame = ClientConfiguration.Instance.LocalGame; string realName = ProgramConstants.GAME_VERSION + " " + defaultGame + " CnCNet"; - await SendMessageAsync(FormattableString.Invariant($"{IRCCommands.USER} {defaultGame}.{systemId} 0 * :{realName}")); - await SendMessageAsync(IRCCommands.NICK + " " + ProgramConstants.PLAYERNAME); + await SendMessageAsync(FormattableString.Invariant($"{IRCCommands.USER} {defaultGame}.{systemId} 0 * :{realName}")).ConfigureAwait(false); + await SendMessageAsync(IRCCommands.NICK + " " + ProgramConstants.PLAYERNAME).ConfigureAwait(false); } public ValueTask ChangeNicknameAsync() @@ -914,7 +915,7 @@ public ValueTask QueueMessageAsync(QueuedMessageType type, int priority, string public async ValueTask QueueMessageAsync(QueuedMessageType type, int priority, int delay, string message) { QueuedMessage qm = new QueuedMessage(message, type, priority, delay); - await QueueMessageAsync(qm); + await QueueMessageAsync(qm).ConfigureAwait(false); Logger.Log("Setting delay to " + delay + "ms for " + qm.ID); } @@ -941,7 +942,7 @@ private async ValueTask SendMessageAsync(string message) try { - await socket.SendAsync(buffer, SocketFlags.None, timeoutCancellationTokenSource.Token); + await socket.SendAsync(buffer, SocketFlags.None, timeoutCancellationTokenSource.Token).ConfigureAwait(false); } catch (IOException ex) { @@ -989,7 +990,7 @@ public async ValueTask QueueMessageAsync(QueuedMessage qm) qm.ID = NextQueueID++; - await messageQueueLocker.WaitAsync(); + await messageQueueLocker.WaitAsync().ConfigureAwait(false); try { @@ -1008,7 +1009,7 @@ public async ValueTask QueueMessageAsync(QueuedMessage qm) AddSpecialQueuedMessage(qm); break; case QueuedMessageType.INSTANT_MESSAGE: - await SendMessageAsync(qm.Command); + await SendMessageAsync(qm.Command).ConfigureAwait(false); break; default: int placeInQueue = messageQueue.FindIndex(m => m.Priority < qm.Priority); diff --git a/DXMainClient/Startup.cs b/DXMainClient/Startup.cs index dde0ed263..b6ecce858 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -242,11 +242,9 @@ private static void CheckSystemSpecifications() string videoController = string.Empty; string memory = string.Empty; - ManagementObjectSearcher searcher; - try { - searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Processor"); + using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Processor"); foreach (var proc in searcher.Get()) { @@ -262,7 +260,7 @@ private static void CheckSystemSpecifications() try { - searcher = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController"); + using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController"); foreach (ManagementObject mo in searcher.Get().Cast()) { @@ -284,7 +282,7 @@ private static void CheckSystemSpecifications() try { - searcher = new ManagementObjectSearcher("Select * From Win32_PhysicalMemory"); + using var searcher = new ManagementObjectSearcher("Select * From Win32_PhysicalMemory"); ulong total = 0; foreach (ManagementObject ram in searcher.Get().Cast()) @@ -314,23 +312,23 @@ private static async ValueTask GenerateOnlineIdAsync() { try { - await Task.CompletedTask; ManagementObjectCollection mbsList = null; - ManagementObjectSearcher mbs = new ManagementObjectSearcher("Select * From Win32_processor"); + using var mbs = new ManagementObjectSearcher("Select * From Win32_processor"); mbsList = mbs.Get(); string cpuid = ""; foreach (ManagementObject mo in mbsList.Cast()) cpuid = mo["ProcessorID"].ToString(); - ManagementObjectSearcher mos = new ManagementObjectSearcher("SELECT * FROM Win32_BaseBoard"); + using var mos = new ManagementObjectSearcher("SELECT * FROM Win32_BaseBoard"); var moc = mos.Get(); string mbid = ""; foreach (ManagementObject mo in moc.Cast()) mbid = (string)mo["SerialNumber"]; - string sid = new SecurityIdentifier((byte[])new DirectoryEntry(FormattableString.Invariant($"WinNT://{Environment.MachineName},Computer")).Children.Cast().First().InvokeGet("objectSID"), 0).AccountDomainSid.Value; + using var computer = new DirectoryEntry(FormattableString.Invariant($"WinNT://{Environment.MachineName},Computer")); + string sid = new SecurityIdentifier((byte[])computer.Children.Cast().First().InvokeGet("objectSID"), 0).AccountDomainSid.Value; Connection.SetId(cpuid + mbid + sid); using RegistryKey key = Registry.CurrentUser.CreateSubKey("SOFTWARE\\" + ClientConfiguration.Instance.InstallationPathRegKey); @@ -364,7 +362,7 @@ private static async ValueTask GenerateOnlineIdAsync() { try { - string machineId = await File.ReadAllTextAsync("/var/lib/dbus/machine-id"); + string machineId = await File.ReadAllTextAsync("/var/lib/dbus/machine-id").ConfigureAwait(false); Connection.SetId(machineId); } From 75cbe7552a23151d46f47e6e4c55991506bcfe1c Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 18 Dec 2022 03:09:56 +0100 Subject: [PATCH 065/109] Async file I/O, stream V3 replay data to disk --- ClientCore/ClientConfiguration.cs | 1 - ClientCore/ProgramConstants.cs | 1 + ClientCore/Settings/UserINISettings.cs | 3 + ClientCore/Statistics/DataWriter.cs | 25 +-- .../GameParsers/LogFileStatisticsParser.cs | 23 +-- ClientCore/Statistics/GenericMatchParser.cs | 8 +- .../Statistics/GenericStatisticsManager.cs | 21 +- ClientCore/Statistics/MatchStatistics.cs | 33 ++-- ClientCore/Statistics/PlayerStatistics.cs | 37 ++-- ClientCore/Statistics/StatisticsManager.cs | 144 +++++++------- DXMainClient/DXGUI/GameClass.cs | 12 +- .../DXGUI/Generic/GameInProgressWindow.cs | 23 ++- .../DXGUI/Generic/StatisticsWindow.cs | 38 ++-- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 9 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 108 +++++----- .../Multiplayer/GameLobby/GameLobbyBase.cs | 10 +- .../Multiplayer/GameLobby/MapPreviewBox.cs | 2 +- .../GameLobby/MultiplayerGameLobby.cs | 2 +- .../Multiplayer/CnCNet/CnCNetLobbyCommands.cs | 2 + .../CnCNet/DataReceivedEventArgs.cs | 2 + .../CnCNet/Replays/GameDataJsonConverter.cs | 14 ++ .../Multiplayer/CnCNet/Replays/Replay.cs | 14 ++ .../Multiplayer/CnCNet/Replays/ReplayData.cs | 10 + .../CnCNet/Replays/ReplayHandler.cs | 184 ++++++++++++++++++ .../CnCNet/UPNP/InternetGatewayDevice.cs | 2 +- .../Multiplayer/CnCNet/V3ConnectionState.cs | 154 +++++++++++---- .../Multiplayer/CnCNet/V3GameTunnelHandler.cs | 60 ++++-- .../CnCNet/V3LocalPlayerConnection.cs | 38 ++-- .../CnCNet/V3RemotePlayerConnection.cs | 28 +-- DXMainClient/Domain/Multiplayer/Map.cs | 2 +- DXMainClient/Domain/Multiplayer/MapLoader.cs | 12 +- DXMainClient/Online/CnCNetUserData.cs | 70 ++++--- 32 files changed, 712 insertions(+), 380 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/Replays/GameDataJsonConverter.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/Replays/Replay.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayData.cs create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 3d5f03504..cd758184f 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -15,7 +15,6 @@ public class ClientConfiguration private const string GENERAL = "General"; private const string AUDIO = "Audio"; private const string SETTINGS = "Settings"; - private const string LINKS = "Links"; private const string TRANSLATIONS = "Translations"; private const string CLIENT_SETTINGS = "DTACnCNetClient.ini"; diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index b4da0ceb9..78729756e 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -64,6 +64,7 @@ public static class ProgramConstants /// public const string INI_NEWLINE_PATTERN = "@"; + public const string REPLAYS_DIRECTORY = "Replays"; public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/master-list"; public const string CNCNET_DYNAMIC_TUNNELS = "DYNAMIC"; public const int GAME_ID_MAX_LENGTH = 4; diff --git a/ClientCore/Settings/UserINISettings.cs b/ClientCore/Settings/UserINISettings.cs index 3774c683c..8ea4c919c 100644 --- a/ClientCore/Settings/UserINISettings.cs +++ b/ClientCore/Settings/UserINISettings.cs @@ -102,6 +102,7 @@ protected UserINISettings(IniFile iniFile) UseLegacyTunnels = new BoolSetting(iniFile, MULTIPLAYER, "UseLegacyTunnels", false); UseP2P = new BoolSetting(iniFile, MULTIPLAYER, "UseP2P", false); UseDynamicTunnels = new BoolSetting(iniFile, MULTIPLAYER, "UseDynamicTunnels", true); + EnableReplays = new BoolSetting(iniFile, MULTIPLAYER, "EnableReplays", false); CheckForUpdates = new BoolSetting(iniFile, OPTIONS, "CheckforUpdates", true); @@ -295,6 +296,8 @@ protected UserINISettings(IniFile iniFile) public BoolSetting GenerateOnlyNewValuesInTranslationStub { get; private set; } + public BoolSetting EnableReplays { get; private set; } + public StringListSetting FavoriteMaps { get; private set; } public void SetValue(string section, string key, string value) diff --git a/ClientCore/Statistics/DataWriter.cs b/ClientCore/Statistics/DataWriter.cs index fe58fce62..95cb87b32 100644 --- a/ClientCore/Statistics/DataWriter.cs +++ b/ClientCore/Statistics/DataWriter.cs @@ -1,27 +1,22 @@ using System; using System.IO; using System.Text; +using System.Threading.Tasks; namespace ClientCore.Statistics { internal static class DataWriter { - public static void WriteInt(this Stream stream, int value) - { - stream.Write(BitConverter.GetBytes(value), 0, sizeof(int)); - } + public static Task WriteIntAsync(this Stream stream, int value) + => stream.WriteAsync(BitConverter.GetBytes(value), 0, sizeof(int)); - public static void WriteLong(this Stream stream, long value) - { - stream.Write(BitConverter.GetBytes(value), 0, sizeof(long)); - } + public static Task WriteLongAsync(this Stream stream, long value) + => stream.WriteAsync(BitConverter.GetBytes(value), 0, sizeof(long)); - public static void WriteBool(this Stream stream, bool value) - { - stream.WriteByte(Convert.ToByte(value)); - } + public static Task WriteBoolAsync(this Stream stream, bool value) + => stream.WriteAsync(new[] { Convert.ToByte(value) }, 0, 1); - public static void WriteString(this Stream stream, string value, int reservedSpace, Encoding encoding = null) + public static Task WriteStringAsync(this Stream stream, string value, int reservedSpace, Encoding encoding = null) { if (encoding == null) encoding = Encoding.Unicode; @@ -37,7 +32,7 @@ public static void WriteString(this Stream stream, string value, int reservedSpa writeBuffer[j] = temp[j]; } - stream.Write(writeBuffer, 0, writeBuffer.Length); + return stream.WriteAsync(writeBuffer, 0, writeBuffer.Length); } } -} +} \ No newline at end of file diff --git a/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs b/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs index 99e7b6c2f..c677185f9 100644 --- a/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs +++ b/ClientCore/Statistics/GameParsers/LogFileStatisticsParser.cs @@ -1,30 +1,27 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using Rampastring.Tools; namespace ClientCore.Statistics.GameParsers { public class LogFileStatisticsParser : GenericMatchParser { - public LogFileStatisticsParser(MatchStatistics ms, bool isLoadedGame) : base(ms) + public LogFileStatisticsParser(MatchStatistics ms, bool isLoadedGame) + : base(ms) { this.isLoadedGame = isLoadedGame; } - private string fileName = "DTA.log"; private string economyString = "Economy"; // RA2/YR do not have economy stat, but a number of built objects. - private bool isLoadedGame; + private readonly bool isLoadedGame; - public void ParseStats(string gamepath, string fileName) + public async ValueTask ParseStatisticsAsync(string gamepath, string fileName) { - this.fileName = fileName; - if (ClientConfiguration.Instance.UseBuiltStatistic) economyString = "Built"; - ParseStatistics(gamepath); - } + if (ClientConfiguration.Instance.UseBuiltStatistic) + economyString = "Built"; - protected override void ParseStatistics(string gamepath) - { FileInfo statisticsFileInfo = SafePath.GetFile(gamepath, fileName); if (!statisticsFileInfo.Exists) @@ -37,7 +34,7 @@ protected override void ParseStatistics(string gamepath) try { - using StreamReader reader = new StreamReader(statisticsFileInfo.OpenRead()); + using var reader = new StreamReader(statisticsFileInfo.OpenRead()); string line; @@ -47,7 +44,7 @@ protected override void ParseStatistics(string gamepath) bool sawCompletion = false; int numPlayersFound = 0; - while ((line = reader.ReadLine()) != null) + while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null) { if (line.Contains(": Loser")) { @@ -154,4 +151,4 @@ protected override void ParseStatistics(string gamepath) } } } -} +} \ No newline at end of file diff --git a/ClientCore/Statistics/GenericMatchParser.cs b/ClientCore/Statistics/GenericMatchParser.cs index bd12beccd..e1c4d363a 100644 --- a/ClientCore/Statistics/GenericMatchParser.cs +++ b/ClientCore/Statistics/GenericMatchParser.cs @@ -2,13 +2,11 @@ { public abstract class GenericMatchParser { - public MatchStatistics Statistics {get; set;} + protected MatchStatistics Statistics {get; set;} - public GenericMatchParser(MatchStatistics ms) + protected GenericMatchParser(MatchStatistics ms) { Statistics = ms; } - - protected abstract void ParseStatistics(string gamepath); } -} +} \ No newline at end of file diff --git a/ClientCore/Statistics/GenericStatisticsManager.cs b/ClientCore/Statistics/GenericStatisticsManager.cs index c87cb5699..86e4e141b 100644 --- a/ClientCore/Statistics/GenericStatisticsManager.cs +++ b/ClientCore/Statistics/GenericStatisticsManager.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; namespace ClientCore.Statistics { @@ -8,25 +8,20 @@ public abstract class GenericStatisticsManager { protected List Statistics = new List(); - protected static string GetStatDatabaseVersion(string scorePath) + protected static async ValueTask GetStatDatabaseVersionAsync(string scorePath) { if (!File.Exists(scorePath)) { return null; } - using (StreamReader reader = new StreamReader(scorePath)) - { - char[] versionBuffer = new char[4]; - reader.Read(versionBuffer, 0, versionBuffer.Length); + using var reader = new StreamReader(scorePath); + char[] versionBuffer = new char[4]; + await reader.ReadAsync(versionBuffer, 0, versionBuffer.Length).ConfigureAwait(false); - String s = new String(versionBuffer); - return s; - } + return new string(versionBuffer); } - public abstract void ReadStatistics(string gamePath); - public int GetMatchCount() { return Statistics.Count; } public MatchStatistics GetMatchByIndex(int index) @@ -34,4 +29,4 @@ public MatchStatistics GetMatchByIndex(int index) return Statistics[index]; } } -} +} \ No newline at end of file diff --git a/ClientCore/Statistics/MatchStatistics.cs b/ClientCore/Statistics/MatchStatistics.cs index 4e49b7f97..699ab1f40 100644 --- a/ClientCore/Statistics/MatchStatistics.cs +++ b/ClientCore/Statistics/MatchStatistics.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading.Tasks; using ClientCore.Statistics.GameParsers; using Rampastring.Tools; @@ -49,7 +50,7 @@ public MatchStatistics(string gameVersion, int gameId, string mapName, string ga public void AddPlayer(string name, bool isLocal, bool isAI, bool isSpectator, int side, int team, int color, int aiLevel) { - PlayerStatistics ps = new PlayerStatistics(name, isLocal, isAI, isSpectator, + PlayerStatistics ps = new PlayerStatistics(name, isLocal, isAI, isSpectator, side, team, color, aiLevel); Players.Add(ps); } @@ -59,14 +60,14 @@ public void AddPlayer(PlayerStatistics ps) Players.Add(ps); } - public void ParseStatistics(string gamePath, string gameName, bool isLoadedGame) + public ValueTask ParseStatisticsAsync(string gamePath, bool isLoadedGame) { Logger.Log("Parsing game statistics."); LengthInSeconds = (int)(DateTime.Now - DateAndTime).TotalSeconds; var parser = new LogFileStatisticsParser(this, isLoadedGame); - parser.ParseStats(gamePath, ClientConfiguration.Instance.StatisticsLogFileName); + return parser.ParseStatisticsAsync(gamePath, ClientConfiguration.Instance.StatisticsLogFileName); } public PlayerStatistics GetEmptyPlayerByName(string playerName) @@ -101,37 +102,37 @@ public PlayerStatistics GetPlayer(int index) return Players[index]; } - public void Write(Stream stream) + public async ValueTask WriteAsync(Stream stream) { // Game length - stream.WriteInt(LengthInSeconds); + await stream.WriteIntAsync(LengthInSeconds).ConfigureAwait(false); // Game version, 8 bytes, ASCII - stream.WriteString(GameVersion, 8, Encoding.ASCII); + await stream.WriteStringAsync(GameVersion, 8, Encoding.ASCII).ConfigureAwait(false); // Date and time, 8 bytes - stream.WriteLong(DateAndTime.ToBinary()); + await stream.WriteLongAsync(DateAndTime.ToBinary()).ConfigureAwait(false); // SawCompletion, 1 byte - stream.WriteBool(SawCompletion); + await stream.WriteBoolAsync(SawCompletion).ConfigureAwait(false); // Number of players, 1 byte - stream.WriteByte(Convert.ToByte(GetPlayerCount())); + await stream.WriteAsync(new[] { Convert.ToByte(GetPlayerCount()) }, 0, 1).ConfigureAwait(false); // Average FPS, 4 bytes - stream.WriteInt(AverageFPS); + await stream.WriteIntAsync(AverageFPS).ConfigureAwait(false); // Map name, 128 bytes (64 chars), Unicode - stream.WriteString(MapName, 128); + await stream.WriteStringAsync(MapName, 128).ConfigureAwait(false); // Game mode, 64 bytes (32 chars), Unicode - stream.WriteString(GameMode, 64); + await stream.WriteStringAsync(GameMode, 64).ConfigureAwait(false); // Unique game ID, 4 bytes - stream.WriteInt(GameID); + await stream.WriteIntAsync(GameID).ConfigureAwait(false); // Whether game options were valid for earning a star, 1 byte - stream.WriteBool(IsValidForStar); + await stream.WriteBoolAsync(IsValidForStar).ConfigureAwait(false); // Write player info for (int i = 0; i < GetPlayerCount(); i++) { PlayerStatistics ps = GetPlayer(i); - ps.Write(stream); + await ps.WriteAsync(stream).ConfigureAwait(false); } } } -} +} \ No newline at end of file diff --git a/ClientCore/Statistics/PlayerStatistics.cs b/ClientCore/Statistics/PlayerStatistics.cs index 770acc7d6..b97660e87 100644 --- a/ClientCore/Statistics/PlayerStatistics.cs +++ b/ClientCore/Statistics/PlayerStatistics.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; namespace ClientCore.Statistics { @@ -7,7 +8,7 @@ public class PlayerStatistics { public PlayerStatistics() { } - public PlayerStatistics(string name, bool isLocal, bool isAi, bool isSpectator, + public PlayerStatistics(string name, bool isLocal, bool isAi, bool isSpectator, int side, int team, int color, int aiLevel) { Name = name; @@ -22,7 +23,7 @@ public PlayerStatistics(string name, bool isLocal, bool isAi, bool isSpectator, public string Name { get; set; } public int Kills { get; set; } - public int Losses {get; set;} + public int Losses { get; set; } public int Economy { get; set; } public int Score { get; set; } public int Side { get; set; } @@ -35,35 +36,35 @@ public PlayerStatistics(string name, bool isLocal, bool isAi, bool isSpectator, public bool IsAI { get; set; } public int Color { get; set; } = 255; - public void Write(Stream stream) + public async ValueTask WriteAsync(Stream stream) { - stream.WriteInt(Economy); + await stream.WriteIntAsync(Economy).ConfigureAwait(false); // 1 byte for IsAI - stream.WriteBool(IsAI); + await stream.WriteBoolAsync(IsAI).ConfigureAwait(false); // 1 byte for IsLocalPlayer - stream.WriteBool(IsLocalPlayer); + await stream.WriteBoolAsync(IsLocalPlayer).ConfigureAwait(false); // 4 bytes for kills - stream.Write(BitConverter.GetBytes(Kills), 0, 4); + await stream.WriteAsync(BitConverter.GetBytes(Kills), 0, 4).ConfigureAwait(false); // 4 bytes for losses - stream.Write(BitConverter.GetBytes(Losses), 0, 4); + await stream.WriteAsync(BitConverter.GetBytes(Losses), 0, 4).ConfigureAwait(false); // Name takes 32 bytes - stream.WriteString(Name, 32); + await stream.WriteStringAsync(Name, 32).ConfigureAwait(false); // 1 byte for SawEnd - stream.WriteBool(SawEnd); + await stream.WriteBoolAsync(SawEnd).ConfigureAwait(false); // 4 bytes for Score - stream.WriteInt(Score); + await stream.WriteIntAsync(Score).ConfigureAwait(false); // 1 byte for Side - stream.WriteByte(Convert.ToByte(Side)); + await stream.WriteAsync(new[] { Convert.ToByte(Side) }, 0, 1).ConfigureAwait(false); // 1 byte for Team - stream.WriteByte(Convert.ToByte(Team)); + await stream.WriteAsync(new[] { Convert.ToByte(Team) }, 0, 1).ConfigureAwait(false); // 1 byte color Color - stream.WriteByte(Convert.ToByte(Color)); + await stream.WriteAsync(new[] { Convert.ToByte(Color) }, 0, 1).ConfigureAwait(false); // 1 byte for WasSpectator - stream.WriteBool(WasSpectator); + await stream.WriteBoolAsync(WasSpectator).ConfigureAwait(false); // 1 byte for Won - stream.WriteBool(Won); + await stream.WriteBoolAsync(Won).ConfigureAwait(false); // 1 byte for AI level - stream.WriteByte(Convert.ToByte(AILevel)); + await stream.WriteAsync(new[] { Convert.ToByte(AILevel) }, 0, 1).ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/ClientCore/Statistics/StatisticsManager.cs b/ClientCore/Statistics/StatisticsManager.cs index 6cd558321..3b9b09d6f 100644 --- a/ClientCore/Statistics/StatisticsManager.cs +++ b/ClientCore/Statistics/StatisticsManager.cs @@ -3,6 +3,7 @@ using System.Text; using System.IO; using System.Linq; +using System.Threading.Tasks; using Rampastring.Tools; namespace ClientCore.Statistics @@ -16,7 +17,6 @@ public class StatisticsManager : GenericStatisticsManager public event EventHandler GameAdded; - public static StatisticsManager Instance { get @@ -27,7 +27,7 @@ public static StatisticsManager Instance } } - public override void ReadStatistics(string gamePath) + public async ValueTask ReadStatisticsAsync(string gamePath) { FileInfo scoreFileInfo = SafePath.GetFile(gamePath, SCORE_FILE_PATH); @@ -42,10 +42,10 @@ public override void ReadStatistics(string gamePath) Statistics.Clear(); FileInfo oldScoreFileInfo = SafePath.GetFile(gamePath, OLD_SCORE_FILE_PATH); - bool resave = ReadFile(oldScoreFileInfo.FullName); - bool resaveNew = ReadFile(scoreFileInfo.FullName); + bool resave = await ReadFileAsync(oldScoreFileInfo.FullName).ConfigureAwait(false); + bool resaveNew = await ReadFileAsync(scoreFileInfo.FullName).ConfigureAwait(false); - PurgeStats(); + await PurgeStatsAsync().ConfigureAwait(false); if (resave || resaveNew) { @@ -55,7 +55,7 @@ public override void ReadStatistics(string gamePath) SafePath.DeleteFileIfExists(oldScoreFileInfo.FullName); } - SaveDatabase(); + await SaveDatabaseAsync().ConfigureAwait(false); } } @@ -64,13 +64,13 @@ public override void ReadStatistics(string gamePath) /// /// The path to the statistics file. /// A bool that determines whether the database should be re-saved. - private bool ReadFile(string filePath) + private async ValueTask ReadFileAsync(string filePath) { bool returnValue = false; try { - string databaseVersion = GetStatDatabaseVersion(filePath); + string databaseVersion = await GetStatDatabaseVersionAsync(filePath).ConfigureAwait(false); if (databaseVersion == null) return false; // No score database exists @@ -79,27 +79,27 @@ private bool ReadFile(string filePath) { case "1.00": case "1.01": - ReadDatabase(filePath, 0); + await ReadDatabaseAsync(filePath, 0).ConfigureAwait(false); returnValue = true; break; case "1.02": - ReadDatabase(filePath, 2); + await ReadDatabaseAsync(filePath, 2).ConfigureAwait(false); returnValue = true; break; case "1.03": - ReadDatabase(filePath, 3); + await ReadDatabaseAsync(filePath, 3).ConfigureAwait(false); returnValue = true; break; case "1.04": - ReadDatabase(filePath, 4); + await ReadDatabaseAsync(filePath, 4).ConfigureAwait(false); returnValue = true; break; case "1.05": - ReadDatabase(filePath, 5); + await ReadDatabaseAsync(filePath, 5).ConfigureAwait(false); returnValue = true; break; case "1.06": - ReadDatabase(filePath, 6); + await ReadDatabaseAsync(filePath, 6).ConfigureAwait(false); break; default: throw new InvalidDataException("Invalid version for " + filePath + ": " + databaseVersion); @@ -113,17 +113,19 @@ private bool ReadFile(string filePath) return returnValue; } - private void ReadDatabase(string filePath, int version) + private async ValueTask ReadDatabaseAsync(string filePath, int version) { // TODO split this function with the MatchStatistics and PlayerStatistics classes try { - using (FileStream fs = File.OpenRead(filePath)) + FileStream fs = File.OpenRead(filePath); + + await using (fs.ConfigureAwait(false)) { fs.Position = 4; // Skip version byte[] readBuffer = new byte[128]; - fs.Read(readBuffer, 0, 4); // First 4 bytes following the version mean the amount of games + await fs.ReadAsync(readBuffer, 0, 4).ConfigureAwait(false); // First 4 bytes following the version mean the amount of games int gameCount = BitConverter.ToInt32(readBuffer, 0); for (int i = 0; i < gameCount; i++) @@ -131,26 +133,26 @@ private void ReadDatabase(string filePath, int version) MatchStatistics ms = new MatchStatistics(); // First 4 bytes of game info is the length in seconds - fs.Read(readBuffer, 0, 4); + await fs.ReadAsync(readBuffer, 0, 4).ConfigureAwait(false); int lengthInSeconds = BitConverter.ToInt32(readBuffer, 0); ms.LengthInSeconds = lengthInSeconds; // Next 8 are the game version - fs.Read(readBuffer, 0, 8); + await fs.ReadAsync(readBuffer, 0, 8).ConfigureAwait(false); ms.GameVersion = System.Text.Encoding.ASCII.GetString(readBuffer, 0, 8); // Then comes the date and time, also 8 bytes - fs.Read(readBuffer, 0, 8); + await fs.ReadAsync(readBuffer, 0, 8).ConfigureAwait(false); long dateData = BitConverter.ToInt64(readBuffer, 0); ms.DateAndTime = DateTime.FromBinary(dateData); // Then one byte for SawCompletion - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ms.SawCompletion = Convert.ToBoolean(readBuffer[0]); // Then 1 byte for the amount of players - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); int playerCount = readBuffer[0]; if (version > 0) { // 4 bytes for average FPS - fs.Read(readBuffer, 0, 4); + await fs.ReadAsync(readBuffer, 0, 4).ConfigureAwait(false); ms.AverageFPS = BitConverter.ToInt32(readBuffer, 0); } @@ -162,23 +164,23 @@ private void ReadDatabase(string filePath, int version) } // Map name, 64 or 128 bytes of Unicode depending on version - fs.Read(readBuffer, 0, mapNameLength); + await fs.ReadAsync(readBuffer, 0, mapNameLength).ConfigureAwait(false); ms.MapName = Encoding.Unicode.GetString(readBuffer).Replace("\0", ""); // Game mode, 64 bytes - fs.Read(readBuffer, 0, 64); + await fs.ReadAsync(readBuffer, 0, 64).ConfigureAwait(false); ms.GameMode = Encoding.Unicode.GetString(readBuffer, 0, 64).Replace("\0", ""); if (version > 2) { // Unique game ID, 32 bytes (int32) - fs.Read(readBuffer, 0, 4); + await fs.ReadAsync(readBuffer, 0, 4).ConfigureAwait(false); ms.GameID = BitConverter.ToInt32(readBuffer, 0); } if (version > 5) { - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ms.IsValidForStar = Convert.ToBoolean(readBuffer[0]); } @@ -190,58 +192,58 @@ private void ReadDatabase(string filePath, int version) if (version > 4) { // Economy is shared for the Built stat in YR - fs.Read(readBuffer, 0, 4); + await fs.ReadAsync(readBuffer, 0, 4).ConfigureAwait(false); ps.Economy = BitConverter.ToInt32(readBuffer, 0); } else { // Economy is between 0 and 100 in old versions, so it takes only one byte - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ps.Economy = readBuffer[0]; } // IsAI is a bool, so obviously one byte - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ps.IsAI = Convert.ToBoolean(readBuffer[0]); // IsLocalPlayer is also a bool - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ps.IsLocalPlayer = Convert.ToBoolean(readBuffer[0]); // Kills take 4 bytes - fs.Read(readBuffer, 0, 4); + await fs.ReadAsync(readBuffer, 0, 4).ConfigureAwait(false); ps.Kills = BitConverter.ToInt32(readBuffer, 0); // Losses also take 4 bytes - fs.Read(readBuffer, 0, 4); + await fs.ReadAsync(readBuffer, 0, 4).ConfigureAwait(false); ps.Losses = BitConverter.ToInt32(readBuffer, 0); // 32 bytes for the name - fs.Read(readBuffer, 0, 32); + await fs.ReadAsync(readBuffer, 0, 32).ConfigureAwait(false); ps.Name = System.Text.Encoding.Unicode.GetString(readBuffer, 0, 32); ps.Name = ps.Name.Replace("\0", String.Empty); // 1 byte for SawEnd - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ps.SawEnd = Convert.ToBoolean(readBuffer[0]); // 4 bytes for Score - fs.Read(readBuffer, 0, 4); + await fs.ReadAsync(readBuffer, 0, 4).ConfigureAwait(false); ps.Score = BitConverter.ToInt32(readBuffer, 0); // 1 byte for Side - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ps.Side = readBuffer[0]; // 1 byte for Team - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ps.Team = readBuffer[0]; if (version > 2) { // 1 byte for Color - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ps.Color = readBuffer[0]; } // 1 byte for WasSpectator - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ps.WasSpectator = Convert.ToBoolean(readBuffer[0]); // 1 byte for Won - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ps.Won = Convert.ToBoolean(readBuffer[0]); // 1 byte for AI level - fs.Read(readBuffer, 0, 1); + await fs.ReadAsync(readBuffer, 0, 1).ConfigureAwait(false); ps.AILevel = readBuffer[0]; ms.AddPlayer(ps); @@ -263,7 +265,7 @@ private void ReadDatabase(string filePath, int version) } } - public void PurgeStats() + private async ValueTask PurgeStatsAsync() { int removedCount = 0; @@ -279,16 +281,10 @@ public void PurgeStats() } if (removedCount > 0) - SaveDatabase(); - } - - public void ClearDatabase() - { - Statistics.Clear(); - CreateDummyFile(); + await SaveDatabaseAsync().ConfigureAwait(false); } - public void AddMatchAndSaveDatabase(bool addMatch, MatchStatistics ms) + public async ValueTask AddMatchAndSaveDatabaseAsync(bool addMatch, MatchStatistics ms) { // Skip adding stats if the game only had one player, make exception for co-op since it doesn't recognize pre-placed houses as players. if (ms.GetPlayerCount() <= 1 && !ms.MapIsCoop) @@ -313,56 +309,62 @@ public void AddMatchAndSaveDatabase(bool addMatch, MatchStatistics ms) if (!scoreFileInfo.Exists) { - CreateDummyFile(); + await CreateDummyFileAsync().ConfigureAwait(false); } Logger.Log("Writing game info to statistics file."); - using (FileStream fs = scoreFileInfo.Open(FileMode.Open, FileAccess.ReadWrite)) + FileStream fs = scoreFileInfo.Open(FileMode.Open, FileAccess.ReadWrite); + + await using (fs.ConfigureAwait(false)) { fs.Position = 4; // First 4 bytes after the version mean the amount of games - fs.WriteInt(Statistics.Count); + await fs.WriteIntAsync(Statistics.Count).ConfigureAwait(false); fs.Position = fs.Length; - ms.Write(fs); + await ms.WriteAsync(fs).ConfigureAwait(false); } Logger.Log("Finished writing statistics."); } - private void CreateDummyFile() + private static async ValueTask CreateDummyFileAsync() { Logger.Log("Creating empty statistics file."); - using StreamWriter sw = new StreamWriter(SafePath.GetFile(ProgramConstants.GamePath, SCORE_FILE_PATH).Create()); - sw.Write(VERSION); + var sw = new StreamWriter(SafePath.GetFile(ProgramConstants.GamePath, SCORE_FILE_PATH).Create()); + + await using (sw.ConfigureAwait(false)) + { + await sw.WriteAsync(VERSION).ConfigureAwait(false); + } } /// /// Deletes the statistics file on the file system and rewrites it. /// - public void SaveDatabase() + public async ValueTask SaveDatabaseAsync() { FileInfo scoreFileInfo = SafePath.GetFile(ProgramConstants.GamePath, SCORE_FILE_PATH); SafePath.DeleteFileIfExists(scoreFileInfo.FullName); - CreateDummyFile(); + await CreateDummyFileAsync().ConfigureAwait(false); - using (FileStream fs = scoreFileInfo.Open(FileMode.Open, FileAccess.ReadWrite)) + FileStream fs = scoreFileInfo.Open(FileMode.Open, FileAccess.ReadWrite); + + await using (fs.ConfigureAwait(false)) { fs.Position = 4; // First 4 bytes after the version mean the amount of games - fs.WriteInt(Statistics.Count); + await fs.WriteIntAsync(Statistics.Count).ConfigureAwait(false); foreach (MatchStatistics ms in Statistics) { - ms.Write(fs); + await ms.WriteAsync(fs).ConfigureAwait(false); } } } public bool HasBeatCoOpMap(string mapName, string gameMode) { - List matches = new List(); - // Filter out unfitting games foreach (MatchStatistics ms in Statistics) { @@ -412,7 +414,7 @@ public int GetCoopRankForDefaultMap(string mapName, int requiredPlayerCount) return rank; } - int GetRankForCoopMatch(MatchStatistics ms) + private static int GetRankForCoopMatch(MatchStatistics ms) { PlayerStatistics localPlayer = ms.Players.Find(p => p.IsLocalPlayer); @@ -485,8 +487,6 @@ int GetRankForCoopMatch(MatchStatistics ms) public bool HasWonMapInPvP(string mapName, string gameMode, int requiredPlayerCount) { - List matches = new List(); - foreach (MatchStatistics ms in Statistics) { if (!ms.SawCompletion) @@ -663,15 +663,9 @@ public int GetSkirmishRankForDefaultMap(string mapName, int requiredPlayerCount) return rank; } - public bool IsGameIdUnique(int gameId) - { - return Statistics.Find(m => m.GameID == gameId) == null; - } - public MatchStatistics GetMatchWithGameID(int gameId) { return Statistics.Find(m => m.GameID == gameId); } - } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/GameClass.cs b/DXMainClient/DXGUI/GameClass.cs index 47f6731f3..9a90f087e 100644 --- a/DXMainClient/DXGUI/GameClass.cs +++ b/DXMainClient/DXGUI/GameClass.cs @@ -10,6 +10,7 @@ using Rampastring.Tools; using Rampastring.XNAUI; using System; +using ClientCore.Extensions; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; using DTAClient.DXGUI.Multiplayer; @@ -24,7 +25,6 @@ using MainMenu = DTAClient.DXGUI.Generic.MainMenu; #if DX || (GL && WINFORMS) using System.Diagnostics; -using System.IO; #endif #if WINFORMS using System.Windows.Forms; @@ -96,7 +96,7 @@ protected override void Initialize() // Create startup failure file that the launcher can check for this error // and handle it by redirecting the user to another version instead - File.WriteAllBytes(SafePath.CombineFilePath(clientDirectory.FullName, startupFailureFile), new byte[] { 1 }); + File.WriteAllBytesAsync(SafePath.CombineFilePath(clientDirectory.FullName, startupFailureFile), new byte[] { 1 }).HandleTask(); string launcherExe = ClientConfiguration.Instance.LauncherExe; if (string.IsNullOrEmpty(launcherExe)) @@ -111,7 +111,7 @@ protected override void Initialize() Logger.Log("Starting " + launcherExe + " and exiting."); - Process.Start(SafePath.CombineFilePath(ProgramConstants.GamePath, launcherExe)); + using var _ = Process.Start(SafePath.CombineFilePath(ProgramConstants.GamePath, launcherExe)); Environment.Exit(1); } @@ -181,8 +181,14 @@ protected override void Initialize() UserINISettings.Instance.PlayerName.Value = playerName; IServiceProvider serviceProvider = BuildServiceProvider(wm); + CnCNetUserData cncNetUserData = serviceProvider.GetService(); + + cncNetUserData.InitializeAsync().HandleTask(); + LoadingScreen ls = serviceProvider.GetService(); + wm.AddAndInitializeControl(ls); + ls.ClientRectangle = new Rectangle((wm.RenderResolutionX - ls.Width) / 2, (wm.RenderResolutionY - ls.Height) / 2, ls.Width, ls.Height); } diff --git a/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs b/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs index c3cd0a7e8..20e676315 100644 --- a/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameInProgressWindow.cs @@ -170,7 +170,7 @@ private void HandleGameProcessExited() DateTime dtn = DateTime.Now; #if ARES - Task.Run(ProcessScreenshots).HandleTask(); + ProcessScreenshotsAsync().HandleTask(); // TODO: Ares debug log handling should be addressed in Ares DLL itself. // For now the following are handled here: @@ -182,7 +182,7 @@ private void HandleGameProcessExited() string snapshotDirectory = GetNewestDebugSnapshotDirectory(); bool snapshotCreated = snapshotDirectory != null; - snapshotDirectory = snapshotDirectory ?? SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "debug", FormattableString.Invariant($"snapshot-{dtn.ToString("yyyyMMdd-HHmmss")}")); + snapshotDirectory ??= SafePath.CombineDirectoryPath(ProgramConstants.GamePath, "debug", FormattableString.Invariant($"snapshot-{dtn.ToString("yyyyMMdd-HHmmss")}")); bool debugLogModified = false; FileInfo debugLogFileInfo = SafePath.GetFile(ProgramConstants.GamePath, "debug", "debug.log"); @@ -359,7 +359,7 @@ private List GetAllDebugSnapshotDirectories() /// /// Converts BMP screenshots to PNG and copies them from game directory to Screenshots sub-directory. /// - private void ProcessScreenshots() + private static async ValueTask ProcessScreenshotsAsync() { IEnumerable files = SafePath.GetDirectory(ProgramConstants.GamePath).EnumerateFiles("SCRN*.bmp"); DirectoryInfo screenshotsDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, "Screenshots"); @@ -381,12 +381,19 @@ private void ProcessScreenshots() { try { - using FileStream stream = file.OpenRead(); - using var image = Image.Load(stream); - FileInfo newFile = SafePath.GetFile(screenshotsDirectory.FullName, FormattableString.Invariant($"{Path.GetFileNameWithoutExtension(file.FullName)}.png")); - using FileStream newFileStream = newFile.OpenWrite(); + FileStream stream = file.OpenRead(); - image.SaveAsPng(newFileStream); + await using (stream.ConfigureAwait(false)) + { + using Image image = await Image.LoadAsync(stream).ConfigureAwait(false); + FileInfo newFile = SafePath.GetFile(screenshotsDirectory.FullName, FormattableString.Invariant($"{Path.GetFileNameWithoutExtension(file.FullName)}.png")); + FileStream newFileStream = newFile.OpenWrite(); + + await using (newFileStream.ConfigureAwait(false)) + { + await image.SaveAsPngAsync(newFileStream).ConfigureAwait(false); + } + } } catch (Exception ex) { diff --git a/DXMainClient/DXGUI/Generic/StatisticsWindow.cs b/DXMainClient/DXGUI/Generic/StatisticsWindow.cs index 52f33cd74..5a0600ea5 100644 --- a/DXMainClient/DXGUI/Generic/StatisticsWindow.cs +++ b/DXMainClient/DXGUI/Generic/StatisticsWindow.cs @@ -11,6 +11,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.DXGUI.Generic { @@ -154,7 +156,7 @@ public override void Initialize() chkIncludeSpectatedGames.ClientRectangle = new Rectangle( Width - chkIncludeSpectatedGames.Width - 12, cmbGameModeFilter.Bottom + 3, - chkIncludeSpectatedGames.Width, + chkIncludeSpectatedGames.Width, chkIncludeSpectatedGames.Height); chkIncludeSpectatedGames.CheckedChanged += ChkIncludeSpectatedGames_CheckedChanged; @@ -205,9 +207,9 @@ public override void Initialize() panelGameStatistics.AddChild(lbGameList); panelGameStatistics.AddChild(lbGameStatistics); -#endregion + #endregion -#region Total statistics + #region Total statistics panelTotalStatistics = new XNAPanel(WindowManager); panelTotalStatistics.Name = "panelTotalStatistics"; @@ -388,7 +390,7 @@ public override void Initialize() panelTotalStatistics.AddChild(lblFavouriteSideValue); panelTotalStatistics.AddChild(lblAverageAILevelValue); -#endregion + #endregion AddChild(tabControl); AddChild(lblFilter); @@ -413,7 +415,7 @@ public override void Initialize() mpColors = MultiplayerColor.LoadColors(); - ReadStatistics(); + ReadStatisticsAsync().HandleTask(); ListGameModes(); ListGames(); @@ -520,7 +522,7 @@ private void LbGameList_SelectedIndexChanged(object sender, EventArgs e) items.Add(new XNAListBoxItem("-", textColor)); } else - { + { if (!ms.SawCompletion) { // The game wasn't completed - we don't know the stats @@ -579,12 +581,8 @@ private string TeamIndexToString(int teamIndex) #region Statistics reading / game listing code - private void ReadStatistics() - { - StatisticsManager sm = StatisticsManager.Instance; - - sm.ReadStatistics(ProgramConstants.GamePath); - } + private ValueTask ReadStatisticsAsync() + => StatisticsManager.Instance.ReadStatisticsAsync(ProgramConstants.GamePath); private void ListGameModes() { @@ -1004,10 +1002,10 @@ private int GetHighestIndex(int[] t) return highestIndex; } - private void ClearAllStatistics() + private async ValueTask ClearAllStatisticsAsync() { - StatisticsManager.Instance.ClearDatabase(); - ReadStatistics(); + await StatisticsManager.Instance.SaveDatabaseAsync().ConfigureAwait(false); + await ReadStatisticsAsync().ConfigureAwait(false); ListGameModes(); ListGames(); } @@ -1026,12 +1024,10 @@ private void BtnClearStatistics_LeftClick(object sender, EventArgs e) var msgBox = new XNAMessageBox(WindowManager, "Clear all statistics".L10N("Client:Main:ClearStatisticsTitle"), ("All statistics data will be cleared from the database.\n\nAre you sure you want to continue?").L10N("Client:Main:ClearStatisticsText"), XNAMessageBoxButtons.YesNo); msgBox.Show(); - msgBox.YesClickedAction = ClearStatisticsConfirmation_YesClicked; + msgBox.YesClickedAction = _ => ClearStatisticsConfirmation_YesClickedAsync().HandleTask(); } - private void ClearStatisticsConfirmation_YesClicked(XNAMessageBox messageBox) - { - ClearAllStatistics(); - } + private ValueTask ClearStatisticsConfirmation_YesClickedAsync() + => ClearAllStatisticsAsync(); } -} +} \ No newline at end of file diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index 8b3710aa6..956d58307 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs @@ -350,7 +350,7 @@ protected async ValueTask LoadGameAsync() private void SharedUILogic_GameProcessExited() => AddCallback(() => HandleGameProcessExitedAsync().HandleTask()); - protected virtual ValueTask HandleGameProcessExitedAsync() + protected virtual async ValueTask HandleGameProcessExitedAsync() { fsw.EnableRaisingEvents = false; @@ -363,17 +363,14 @@ protected virtual ValueTask HandleGameProcessExitedAsync() int newLength = matchStatistics.LengthInSeconds + (int)(DateTime.Now - gameLoadTime).TotalSeconds; - matchStatistics.ParseStatistics(ProgramConstants.GamePath, - ClientConfiguration.Instance.LocalGame, true); + await matchStatistics.ParseStatisticsAsync(ProgramConstants.GamePath, true).ConfigureAwait(false); matchStatistics.LengthInSeconds = newLength; - StatisticsManager.Instance.SaveDatabase(); + await StatisticsManager.Instance.SaveDatabaseAsync().ConfigureAwait(false); } UpdateDiscordPresence(true); - - return ValueTask.CompletedTask; } protected virtual void WriteSpawnIniAdditions(IniFile spawnIni) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 5c108489e..36e26c723 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -106,12 +106,11 @@ public CnCNetGameLobby( : base(windowManager, "MultiplayerGameLobby", topBar, mapLoader, discordHandler) { this.connectionManager = connectionManager; - localGame = ClientConfiguration.Instance.LocalGame; this.tunnelHandler = tunnelHandler; this.gameCollection = gameCollection; this.cncnetUserData = cncnetUserData; this.pmWindow = pmWindow; - + localGame = ClientConfiguration.Instance.LocalGame; ctcpCommandHandlers = new() { new IntCommandHandler(CnCNetCommands.OPTIONS_REQUEST, (playerName, options) => HandleOptionsRequestAsync(playerName, options).HandleTask()), @@ -122,7 +121,7 @@ public CnCNetGameLobby( new StringCommandHandler(CnCNetCommands.GAME_START_V2, (playerName, message) => ClientLaunchGameV2Async(playerName, message).HandleTask()), new StringCommandHandler(CnCNetCommands.GAME_START_V3, ClientLaunchGameV3Async), new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_OK, playerName => HandlePlayerConnectedToTunnelAsync(playerName).HandleTask()), - new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_FAIL, HandleTunnelFail), + new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_FAIL, playerName => HandleTunnelFailAsync(playerName).HandleTask()), new NotificationHandler(CnCNetCommands.AI_SPECTATORS, HandleNotification, () => AISpectatorsNotificationAsync().HandleTask()), new NotificationHandler(CnCNetCommands.GET_READY_LOBBY, HandleNotification, () => GetReadyNotificationAsync().HandleTask()), new NotificationHandler(CnCNetCommands.INSUFFICIENT_PLAYERS, HandleNotification, () => InsufficientPlayersNotificationAsync().HandleTask()), @@ -152,6 +151,7 @@ public CnCNetGameLobby( MapSharer.MapDownloadComplete += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapDownloadCompleteAsync(e).HandleTask()); MapSharer.MapUploadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapUploadFailedAsync(e).HandleTask()); MapSharer.MapUploadComplete += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapUploadCompleteAsync(e).HandleTask()); + WindowManager.GameClosing += (_, _) => Dispose(true); AddChatBoxCommand(new( CnCNetLobbyCommands.TUNNELINFO, @@ -178,8 +178,16 @@ public CnCNetGameLobby( "Toggle P2P connections on/off, your IP will be public to players in the lobby".L10N("Client:Main:ChangeP2P"), false, _ => ToggleP2PAsync().HandleTask())); - - WindowManager.GameClosing += (_, _) => Dispose(true); + AddChatBoxCommand(new( + CnCNetLobbyCommands.RECORD, + "Toggle recording game replay".L10N("Client:Main:ChangeRecord"), + false, + _ => ToggleRecord())); + AddChatBoxCommand(new( + CnCNetLobbyCommands.REPLAY, + "Start a game replay.\nExample: \"/replay REPLAYID".L10N("Client:Main:StartReplay"), + true, + StartReplay)); } public event EventHandler GameLeft; @@ -272,7 +280,7 @@ private void GameStartTimer_TimeElapsed(object sender, EventArgs e) { if (!isPlayerConnected[i]) { - if (playerString == string.Empty) + if (string.IsNullOrWhiteSpace(playerString)) playerString = Players[i].Name; else playerString += ", " + Players[i].Name; @@ -280,7 +288,7 @@ private void GameStartTimer_TimeElapsed(object sender, EventArgs e) } AddNotice(string.Format(CultureInfo.InvariantCulture, "Some players ({0}) failed to connect within the time limit. Aborting game launch.", playerString)); - AbortGameStart(); + AbortGameStartAsync().HandleTask(); } private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClickedEventArgs args) @@ -311,6 +319,7 @@ public async ValueTask SetUpAsync( this.isCustomPassword = isCustomPassword; v3ConnectionState.DynamicTunnelsEnabled = UserINISettings.Instance.UseDynamicTunnels; v3ConnectionState.P2PEnabled = UserINISettings.Instance.UseP2P; + v3ConnectionState.RecordingEnabled = UserINISettings.Instance.EnableReplays; channel.MessageAdded += Channel_MessageAdded; channel.CTCPReceived += Channel_CTCPReceived; channel.UserKicked += channel_UserKickedFunc; @@ -672,7 +681,7 @@ private async ValueTask Channel_UserAddedAsync(ChannelUserEventArgs e) private async ValueTask RemovePlayerAsync(string playerName) { - AbortGameStart(); + await AbortGameStartAsync().ConfigureAwait(false); Players.Remove(Players.SingleOrDefault(p => p.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase))); CopyPlayerDataToUI(); v3ConnectionState.RemoveV3Player(playerName); @@ -886,6 +895,7 @@ private void StartV3ConnectionListeners() void RemoteHostConnectionFailedAction() => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); v3ConnectionState.StartV3ConnectionListeners( + UniqueGameID, gameLocalPlayerId, FindLocalPlayer().Name, Players, @@ -921,15 +931,15 @@ private void SetLocalPlayerConnected() private async ValueTask GameTunnelHandler_ConnectionFailed_CallbackAsync() { await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_FAIL, QueuedMessageType.INSTANT_MESSAGE, 0).ConfigureAwait(false); - HandleTunnelFail(ProgramConstants.PLAYERNAME); + await HandleTunnelFailAsync(ProgramConstants.PLAYERNAME).ConfigureAwait(false); } - private void HandleTunnelFail(string playerName) + private async ValueTask HandleTunnelFailAsync(string playerName) { Logger.Log(playerName + " failed to connect - aborting game launch."); AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} failed to connect. Please retry, disable P2P or pick " + "another tunnel server by typing /{1} in the chat input box.".L10N("Client:Main:PlayerConnectFailed"), playerName, CnCNetLobbyCommands.CHANGETUNNEL)); - AbortGameStart(); + await AbortGameStartAsync().ConfigureAwait(false); } private async ValueTask HandlePlayerConnectedToTunnelAsync(string playerName) @@ -942,7 +952,7 @@ private async ValueTask HandlePlayerConnectedToTunnelAsync(string playerName) if (index == -1) { Logger.Log("HandleTunnelConnected: Couldn't find player " + playerName + "!"); - AbortGameStart(); + await AbortGameStartAsync().ConfigureAwait(false); return; } @@ -957,25 +967,7 @@ private async ValueTask LaunchGameV3Async() Logger.Log("All players are connected, starting game!"); AddNotice("All players have connected...".L10N("Client:Main:PlayersConnected")); - List usedPorts = new(v3ConnectionState.IpV4P2PPorts.Select(q => q.InternalPort).Concat(v3ConnectionState.IpV6P2PPorts.Select(q => q.InternalPort)).Distinct()); - - foreach ((List remotePlayerNames, V3GameTunnelHandler v3GameTunnelHandler) in v3ConnectionState.V3GameTunnelHandlers) - { - var currentTunnelPlayers = Players.Where(q => remotePlayerNames.Contains(q.Name)).ToList(); - IEnumerable indexes = currentTunnelPlayers.Select(q => q.Index); - var playerIds = indexes.Select(q => gamePlayerIds[q]).ToList(); - List createdLocalPlayerPorts = v3GameTunnelHandler.CreatePlayerConnections(playerIds).ToList(); - int i = 0; - - foreach (PlayerInfo currentTunnelPlayer in currentTunnelPlayers) - currentTunnelPlayer.Port = createdLocalPlayerPorts.Skip(i++).Take(1).Single(); - - usedPorts.AddRange(createdLocalPlayerPorts); - } - - foreach (V3GameTunnelHandler v3GameTunnelHandler in v3ConnectionState.V3GameTunnelHandlers.Select(q => q.Tunnel)) - v3GameTunnelHandler.StartPlayerConnections(); - + List usedPorts = v3ConnectionState.StartPlayerConnections(gamePlayerIds); int gamePort = NetworkHelper.GetFreeUdpPorts(usedPorts, 1).Single(); FindLocalPlayer().Port = gamePort; @@ -988,12 +980,13 @@ private async ValueTask LaunchGameV3Async() await StartGameAsync().ConfigureAwait(false); } - private void AbortGameStart() + private async ValueTask AbortGameStartAsync() { btnLaunchGame.InputEnabled = true; gameStartCancellationTokenSource?.Cancel(); - v3ConnectionState.V3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); + await v3ConnectionState.ClearConnectionsAsync().ConfigureAwait(false); + gameStartTimer.Pause(); isStartingGame = false; @@ -1217,7 +1210,7 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() CultureInfo.CurrentCulture, "Could not open P2P ports. Check that UPnP port mapping is enabled for this device on your router/modem.".L10N("Client:Main:UPnPP2PFailed")), Color.Orange); - + await SendPlayerP2PRequestAsync().ConfigureAwait(false); return; } @@ -1406,6 +1399,20 @@ private async ValueTask ToggleP2PAsync() await SendPlayerP2PRequestAsync().ConfigureAwait(false); } + private void ToggleRecord() + { + v3ConnectionState.RecordingEnabled = !v3ConnectionState.RecordingEnabled; + + if (v3ConnectionState.RecordingEnabled) + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} enabled game recording".L10N("Client:Main:RecordEnabled"), FindLocalPlayer().Name)); + else + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled game recording".L10N("Client:Main:RecordDisabled"), FindLocalPlayer().Name)); + } + + private void StartReplay(string replayId) + { + } + /// /// Handles game option messages received from the game host. /// @@ -1592,9 +1599,7 @@ private async ValueTask ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsE if (newDynamicTunnelsEnabledValue) { - tunnelHandler.CurrentTunnel = tunnelHandler.Tunnels - .Where(q => q.PingInMs > -1 && !q.RequiresPassword && q.Clients < q.MaxClients - 8 && q.Version == Constants.TUNNEL_VERSION_3) - .MinBy(q => q.PingInMs); + tunnelHandler.CurrentTunnel = v3ConnectionState.GetEligibleTunnels().MinBy(q => q.PingInMs); await BroadcastPlayerTunnelPingsAsync().ConfigureAwait(false); } @@ -1647,8 +1652,8 @@ protected override async ValueTask GameProcessExitedAsync() await base.GameProcessExitedAsync().ConfigureAwait(false); await channel.SendCTCPMessageAsync(CnCNetCommands.RETURN, QueuedMessageType.SYSTEM_MESSAGE, 20).ConfigureAwait(false); gameStartCancellationTokenSource?.Cancel(); - v3ConnectionState.V3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); - v3ConnectionState.V3GameTunnelHandlers.Clear(); + await v3ConnectionState.SaveReplayAsync().ConfigureAwait(false); + await v3ConnectionState.ClearConnectionsAsync().ConfigureAwait(false); ReturnNotification(ProgramConstants.PLAYERNAME); if (IsHost) @@ -2014,31 +2019,16 @@ private void HandleTunnelPingsMessage(string playerName, string tunnelPingsMessa if (!v3ConnectionState.PinnedTunnels.Any()) return; - string[] tunnelPingsLines = tunnelPingsMessage.Split('\t', StringSplitOptions.RemoveEmptyEntries); - IEnumerable<(int Ping, string Hash)> tunnelPings = tunnelPingsLines.Select(q => - { - string[] split = q.Split(';'); - - return (int.Parse(split[0], CultureInfo.InvariantCulture), split[1]); - }); - IEnumerable<(int CombinedPing, string Hash)> combinedTunnelResults = tunnelPings - .Where(q => v3ConnectionState.PinnedTunnels.Select(r => r.Hash).Contains(q.Hash)) - .Select(q => (CombinedPing: q.Ping + v3ConnectionState.PinnedTunnels.SingleOrDefault(r => q.Hash.Equals(r.Hash, StringComparison.OrdinalIgnoreCase)).Ping, q.Hash)); - (int combinedPing, string hash) = combinedTunnelResults - .OrderBy(q => q.CombinedPing) - .ThenBy(q => q.Hash, StringComparer.OrdinalIgnoreCase) - .FirstOrDefault(); + string selectedTunnelHash = v3ConnectionState.HandleTunnelPingsMessage(playerName, tunnelPingsMessage); - if (hash is null) + if (selectedTunnelHash is null) { AddNotice(string.Format(CultureInfo.CurrentCulture, "No common tunnel found for: {0}".L10N("Client:Main:NoCommonTunnel"), playerName)); } else { - CnCNetTunnel tunnel = tunnelHandler.Tunnels.Single(q => q.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)); + CnCNetTunnel tunnel = tunnelHandler.Tunnels.Single(q => q.Hash.Equals(selectedTunnelHash, StringComparison.OrdinalIgnoreCase)); - v3ConnectionState.PlayerTunnels.Remove(v3ConnectionState.PlayerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); - v3ConnectionState.PlayerTunnels.Add(new(playerName, tunnel, combinedPing)); AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} dynamic tunnel: {1} ({2}ms)".L10N("Client:Main:TunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); } } @@ -2411,8 +2401,8 @@ private async ValueTask BroadcastGameAsync() foreach (PlayerInfo pInfo in Players) { - sb.Append(pInfo.Name); - sb.Append(','); + sb.Append(pInfo.Name) + .Append(','); } sb.Remove(sb.Length - 1, 1) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 8d468caba..1520444f5 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1793,19 +1793,17 @@ protected virtual async ValueTask StartGameAsync() private void GameProcessExited_Callback() => AddCallback(() => GameProcessExitedAsync().HandleTask()); - protected virtual ValueTask GameProcessExitedAsync() + protected virtual async ValueTask GameProcessExitedAsync() { GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; Logger.Log("GameProcessExited: Parsing statistics."); - matchStatistics.ParseStatistics(ProgramConstants.GamePath, ClientConfiguration.Instance.LocalGame, false); + matchStatistics.ParseStatisticsAsync(ProgramConstants.GamePath, false).HandleTask(); Logger.Log("GameProcessExited: Adding match to statistics."); - StatisticsManager.Instance.AddMatchAndSaveDatabase(true, matchStatistics); + StatisticsManager.Instance.AddMatchAndSaveDatabaseAsync(true, matchStatistics).HandleTask(); ClearReadyStatuses(); CopyPlayerDataToUI(); UpdateDiscordPresence(true); - - return ValueTask.CompletedTask; } /// @@ -2190,7 +2188,7 @@ protected virtual async ValueTask ChangeMapAsync(GameModeMap gameModeMap) pInfo.TeamId = 1; } - await OnGameOptionChangedAsync().ConfigureAwait(false); + await OnGameOptionChangedAsync().ConfigureAwait(true); MapPreviewBox.GameModeMap = GameModeMap; CopyPlayerDataToUI(); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs index 48a5b23ca..6160c18b1 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs @@ -401,7 +401,7 @@ private async ValueTask UpdateMapAsync() if (GameModeMap.Map.PreviewTexture == null) { - previewTexture = await GameModeMap.Map.LoadPreviewTextureAsync().ConfigureAwait(false); + previewTexture = await GameModeMap.Map.LoadPreviewTextureAsync().ConfigureAwait(true); disposeTextures = true; } else diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 4b657dcbc..af6335421 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -282,7 +282,7 @@ protected override async ValueTask GameProcessExitedAsync() pInfo.IsInGame = false; - await base.GameProcessExitedAsync().ConfigureAwait(false); + await base.GameProcessExitedAsync().ConfigureAwait(true); if (IsHost) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs index cd07bd70b..5c075fc68 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetLobbyCommands.cs @@ -18,4 +18,6 @@ internal static class CnCNetLobbyCommands public const string LOADOPTIONS = "LOADOPTIONS"; public const string DYNAMICTUNNELS = "DYNAMICTUNNELS"; public const string P2P = "P2P"; + public const string RECORD = "RECORD"; + public const string REPLAY = "REPLAY"; } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs index 1e3c22662..7a3aac8c3 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs @@ -10,6 +10,8 @@ public DataReceivedEventArgs(uint playerId, ReadOnlyMemory gameData) GameData = gameData; } + public DateTimeOffset Timestamp { get; } = DateTimeOffset.Now; + public uint PlayerId { get; } public ReadOnlyMemory GameData { get; } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/GameDataJsonConverter.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/GameDataJsonConverter.cs new file mode 100644 index 000000000..79f24b8ce --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/GameDataJsonConverter.cs @@ -0,0 +1,14 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DTAClient.Domain.Multiplayer.CnCNet.Replays; + +internal sealed class GameDataJsonConverter : JsonConverter> +{ + public override ReadOnlyMemory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => new(reader.GetBytesFromBase64()); + + public override void Write(Utf8JsonWriter writer, ReadOnlyMemory value, JsonSerializerOptions options) + => writer.WriteBase64StringValue(value.Span); +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/Replay.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/Replay.cs new file mode 100644 index 000000000..67de759e8 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/Replay.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace DTAClient.Domain.Multiplayer.CnCNet.Replays; + +internal readonly record struct Replay( + [property: JsonPropertyName("i")] int Id, + [property: JsonPropertyName("s")] string Settings, + [property: JsonPropertyName("t")] DateTimeOffset Timestamp, + [property: JsonPropertyName("p")] uint RecordingPlayerId, + [property: JsonPropertyName("m")] Dictionary PlayerMappings, + [property: JsonPropertyName("d")] List Data, + [property: JsonPropertyName("v")] byte Version = 1); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayData.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayData.cs new file mode 100644 index 000000000..ddc618d67 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayData.cs @@ -0,0 +1,10 @@ +using System; +using System.Text.Json.Serialization; + +namespace DTAClient.Domain.Multiplayer.CnCNet.Replays; + +internal readonly record struct ReplayData( + [property: JsonPropertyName("t")] TimeSpan TimestampOffset, + [property: JsonPropertyName("p")] uint PlayerId, + [property: JsonPropertyName("g")][property: JsonConverter(typeof(GameDataJsonConverter))] ReadOnlyMemory GameData, + [property: JsonPropertyName("v")] byte Version = 1); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs new file mode 100644 index 000000000..73f6eac83 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using ClientCore; +using ClientCore.Extensions; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet.Replays; + +internal sealed class ReplayHandler : IAsyncDisposable +{ + private readonly Dictionary replayFileStreams = new(); + + private DateTimeOffset startTimestamp; + private DirectoryInfo replayDirectory; + private bool gameStarted; + private int replayId; + private uint gameLocalPlayerId; + + public void SetupRecording(int replayId, uint gameLocalPlayerId) + { + this.replayId = replayId; + this.gameLocalPlayerId = gameLocalPlayerId; + startTimestamp = DateTimeOffset.Now; + replayDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, ProgramConstants.REPLAYS_DIRECTORY, replayId.ToString(CultureInfo.InvariantCulture)); + + replayDirectory.Create(); + replayFileStreams.Add(gameLocalPlayerId, CreateReplayFileStream()); + } + + public async ValueTask StopRecordingAsync(List gamePlayerIds, List playerInfos, List v3GameTunnelHandlers) + { + foreach (V3GameTunnelHandler v3GameTunnelHandler in v3GameTunnelHandlers) + { + v3GameTunnelHandler.RaiseRemoteHostDataReceivedEvent -= RemoteHostConnection_DataReceivedAsync; + v3GameTunnelHandler.RaiseLocalGameDataReceivedEvent -= LocalGameConnection_DataReceivedAsync; + } + + FileInfo spawnFile = SafePath.GetFile(replayDirectory.FullName, ProgramConstants.SPAWNER_SETTINGS); + string settings = await File.ReadAllTextAsync(spawnFile.FullName, CancellationToken.None).ConfigureAwait(false); + var spawnIni = new IniFile(spawnFile.FullName); + string playerName = spawnIni.GetSection("Settings").GetStringValue("Name", null); + uint playerId = gamePlayerIds[playerInfos.Single(q => q.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)).Index]; + var playerMappings = new Dictionary + { + { playerId, playerName } + }; + + for (int i = 1; i < spawnIni.GetSection("Settings").GetIntValue("PlayerCount", 0); i++) + { + string section = $"Other{i}"; + + if (spawnIni.SectionExists(section)) + { + playerName = spawnIni.GetSection(section).GetStringValue("Name", null); + playerId = gamePlayerIds[playerInfos.Single(q => q.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)).Index]; + + playerMappings.Add(playerId, playerName); + } + } + + List replayDataList = await GenerateReplayDataAsync().ConfigureAwait(false); + var replay = new Replay(replayId, settings, startTimestamp, gameLocalPlayerId, playerMappings, replayDataList.OrderBy(q => q.TimestampOffset).ToList()); + var tempReplayFileStream = new MemoryStream(); + + await using (tempReplayFileStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(tempReplayFileStream, replay, cancellationToken: CancellationToken.None).ConfigureAwait(false); + + tempReplayFileStream.Position = 0L; + + FileStream replayFileStream = new( + SafePath.CombineFilePath(replayDirectory.Parent.FullName, FormattableString.Invariant($"{replayId}.cnc")), + new FileStreamOptions + { + Access = FileAccess.Write, + BufferSize = 0, + Mode = FileMode.CreateNew, + Options = FileOptions.Asynchronous | FileOptions.WriteThrough + }); + + await using (replayFileStream.ConfigureAwait(false)) + { + var compressionStream = new GZipStream(replayFileStream, CompressionMode.Compress); + + await using (compressionStream.ConfigureAwait(false)) + { + await tempReplayFileStream.CopyToAsync(compressionStream).ConfigureAwait(false); + } + } + } + + spawnFile.Delete(); + } + + public async ValueTask DisposeAsync() + { + foreach ((_, FileStream fileStream) in replayFileStreams) + await fileStream.DisposeAsync().ConfigureAwait(false); + + replayDirectory.Delete(); + } + + public void RemoteHostConnection_DataReceivedAsync(object sender, DataReceivedEventArgs e) + => SaveReplayDataAsync(((V3RemotePlayerConnection)sender).GameLocalPlayerId, e).HandleTask(); + + public void LocalGameConnection_DataReceivedAsync(object sender, DataReceivedEventArgs e) + { + if (!gameStarted) + { + gameStarted = true; + + FileInfo spawnFileInfo = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS); + + spawnFileInfo.CopyTo(SafePath.CombineFilePath(replayDirectory.FullName, spawnFileInfo.Name)); + } + + SaveReplayDataAsync(((V3LocalPlayerConnection)sender).PlayerId, e).HandleTask(); + } + + private async ValueTask> GenerateReplayDataAsync() + { + var replayDataList = new List(); + + foreach (FileStream fileStream in replayFileStreams.Values.Where(q => q.Length > 0L)) + { + await fileStream.WriteAsync(new UTF8Encoding().GetBytes(new[] { ']' })).ConfigureAwait(false); + + fileStream.Position = 0L; + + replayDataList.AddRange(await JsonSerializer.DeserializeAsync>( + fileStream, new JsonSerializerOptions { AllowTrailingCommas = true }, cancellationToken: CancellationToken.None).ConfigureAwait(false)); + } + + return replayDataList; + } + + private async ValueTask SaveReplayDataAsync(uint playerId, DataReceivedEventArgs e) + { + if (!replayFileStreams.TryGetValue(playerId, out FileStream fileStream)) + { + fileStream = CreateReplayFileStream(); + + if (!replayFileStreams.TryAdd(playerId, fileStream)) + await fileStream.DisposeAsync().ConfigureAwait(false); + + replayFileStreams.TryGetValue(playerId, out fileStream); + } + + if (fileStream.Position is 0L) + await fileStream.WriteAsync(new UTF8Encoding().GetBytes(new[] { '[' })).ConfigureAwait(false); + + var replayData = new ReplayData(e.Timestamp - startTimestamp, playerId, e.GameData); + var tempStream = new MemoryStream(); + + await using (tempStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(tempStream, replayData, cancellationToken: CancellationToken.None).ConfigureAwait(false); + await tempStream.WriteAsync(new UTF8Encoding().GetBytes(new[] { ',' })).ConfigureAwait(false); + + tempStream.Position = 0L; + + await tempStream.CopyToAsync(fileStream).ConfigureAwait(false); + } + } + + private FileStream CreateReplayFileStream() + => new( + SafePath.CombineFilePath(replayDirectory.FullName, Guid.NewGuid().ToString()), + new FileStreamOptions + { + Access = FileAccess.ReadWrite, + BufferSize = 0, + Mode = FileMode.CreateNew, + Options = FileOptions.Asynchronous | FileOptions.WriteThrough | FileOptions.SequentialScan | FileOptions.DeleteOnClose + }); +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index 8235c3db5..b105c7bb0 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -254,7 +254,7 @@ private static async ValueTask ExecuteSoapAction { OmitXmlDeclaration = true, Async = true, - Encoding = new UTF8Encoding(false) + Encoding = new UTF8Encoding() }); await using (writer.ConfigureAwait(false)) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index e1b682934..8e4d1c04d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using ClientCore; +using DTAClient.Domain.Multiplayer.CnCNet.Replays; using DTAClient.Domain.Multiplayer.CnCNet.UPNP; namespace DTAClient.Domain.Multiplayer.CnCNet; @@ -17,15 +18,17 @@ internal sealed class V3ConnectionState : IAsyncDisposable private const int PINNED_DYNAMIC_TUNNELS = 10; private readonly TunnelHandler tunnelHandler; + private readonly List<(string RemotePlayerName, CnCNetTunnel Tunnel, int CombinedPing)> playerTunnels = new(); private IPAddress publicIpV4Address; private IPAddress publicIpV6Address; private List p2pIpV6PortIds = new(); private InternetGatewayDevice internetGatewayDevice; - - public List<(ushort InternalPort, ushort ExternalPort)> IpV6P2PPorts { get; private set; } = new(); - - public List<(ushort InternalPort, ushort ExternalPort)> IpV4P2PPorts { get; private set; } = new(); + private List playerInfos; + private List gamePlayerIds; + private List<(ushort InternalPort, ushort ExternalPort)> ipV6P2PPorts = new(); + private List<(ushort InternalPort, ushort ExternalPort)> ipV4P2PPorts = new(); + private ReplayHandler replayHandler; public List<(int Ping, string Hash)> PinnedTunnels { get; private set; } = new(); @@ -35,12 +38,12 @@ internal sealed class V3ConnectionState : IAsyncDisposable public bool P2PEnabled { get; set; } + public bool RecordingEnabled { get; set; } + public CnCNetTunnel InitialTunnel { get; private set; } public CancellationTokenSource StunCancellationTokenSource { get; private set; } - public List<(string RemotePlayerName, CnCNetTunnel Tunnel, int CombinedPing)> PlayerTunnels { get; } = new(); - public List<(List RemotePlayerNames, V3GameTunnelHandler Tunnel)> V3GameTunnelHandlers { get; } = new(); public List P2PPlayers { get; } = new(); @@ -53,16 +56,7 @@ public V3ConnectionState(TunnelHandler tunnelHandler) public void Setup(CnCNetTunnel tunnel) { InitialTunnel = tunnel; - - if (!DynamicTunnelsEnabled) - { - tunnelHandler.CurrentTunnel = InitialTunnel; - } - else - { - tunnelHandler.CurrentTunnel = GetEligibleTunnels() - .MinBy(q => q.PingInMs); - } + tunnelHandler.CurrentTunnel = !DynamicTunnelsEnabled ? InitialTunnel : GetEligibleTunnels().MinBy(q => q.PingInMs); } public void PinTunnels() @@ -82,7 +76,7 @@ public void PinTunnels() public async ValueTask HandlePlayerP2PRequestAsync() { - if (!IpV6P2PPorts.Any() && !IpV4P2PPorts.Any()) + if (!ipV6P2PPorts.Any() && !ipV4P2PPorts.Any()) { var p2pPorts = NetworkHelper.GetFreeUdpPorts(Array.Empty(), MAX_REMOTE_PLAYERS).ToList(); @@ -91,7 +85,7 @@ public async ValueTask HandlePlayerP2PRequestAsync() StunCancellationTokenSource = new(); - (internetGatewayDevice, IpV6P2PPorts, IpV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync( + (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync( internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? InitialTunnel.IPAddresses, StunCancellationTokenSource.Token).ConfigureAwait(false); } @@ -100,13 +94,13 @@ public async ValueTask HandlePlayerP2PRequestAsync() public void RemoveV3Player(string playerName) { - PlayerTunnels.Remove(PlayerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + playerTunnels.Remove(playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); P2PPlayers.Remove(P2PPlayers.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); } public string GetP2PRequestCommand() - => $" {publicIpV4Address}\t{(!IpV4P2PPorts.Any() ? null : IpV4P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}" + - $";{publicIpV6Address}\t{(!IpV6P2PPorts.Any() ? null : IpV6P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}"; + => $" {publicIpV4Address}\t{(!ipV4P2PPorts.Any() ? null : ipV4P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}" + + $";{publicIpV6Address}\t{(!ipV6P2PPorts.Any() ? null : ipV6P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}"; public string GetP2PPingCommand(string playerName) => $" {playerName}-{P2PPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)).LocalPingResults.Select(q => $"{q.RemoteIpAddress};{q.Ping}\t").Aggregate((q, r) => $"{q}{r}")}"; @@ -224,25 +218,37 @@ public bool UpdateRemotePingResults(string senderName, string p2pPingsMessage, s } public void StartV3ConnectionListeners( + int uniqueGameId, uint gameLocalPlayerId, string localPlayerName, - List players, + List playerInfos, Action remoteHostConnectedAction, Action remoteHostConnectionFailedAction, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { + this.playerInfos = playerInfos; + V3GameTunnelHandlers.Clear(); + if (RecordingEnabled) + { + replayHandler = new(); + + replayHandler.SetupRecording(uniqueGameId, gameLocalPlayerId); + } + if (!DynamicTunnelsEnabled) { var gameTunnelHandler = new V3GameTunnelHandler(); gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); + gameTunnelHandler.RaiseRemoteHostDataReceivedEvent += replayHandler.RemoteHostConnection_DataReceivedAsync; + gameTunnelHandler.RaiseLocalGameDataReceivedEvent += replayHandler.LocalGameConnection_DataReceivedAsync; gameTunnelHandler.SetUp(new(tunnelHandler.CurrentTunnel.IPAddress, tunnelHandler.CurrentTunnel.Port), 0, gameLocalPlayerId, cancellationToken); gameTunnelHandler.ConnectToTunnel(); - V3GameTunnelHandlers.Add(new(players.Where(q => !q.Name.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase)).Select(q => q.Name).ToList(), gameTunnelHandler)); + V3GameTunnelHandlers.Add(new(playerInfos.Where(q => !q.Name.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase)).Select(q => q.Name).ToList(), gameTunnelHandler)); } else { @@ -260,23 +266,23 @@ public void StartV3ConnectionListeners( .Select(q => (q.RemoteIpAddress, q.Ping + remotePingResults.Single(r => r.RemoteIpAddress.AddressFamily == q.RemoteIpAddress.AddressFamily).Ping)) .MaxBy(q => q.RemoteIpAddress.AddressFamily); - if (combinedPing < PlayerTunnels.Single(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).CombinedPing) + if (combinedPing < playerTunnels.Single(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).CombinedPing) { ushort[] localPorts; ushort[] remotePorts; if (selectedRemoteIpAddress.AddressFamily is AddressFamily.InterNetworkV6) { - localPorts = IpV6P2PPorts.Select(q => q.InternalPort).ToArray(); + localPorts = ipV6P2PPorts.Select(q => q.InternalPort).ToArray(); remotePorts = remoteIpV6Ports; } else { - localPorts = IpV4P2PPorts.Select(q => q.InternalPort).ToArray(); + localPorts = ipV4P2PPorts.Select(q => q.InternalPort).ToArray(); remotePorts = remoteIpV4Ports; } - var allPlayerNames = players.Select(q => q.Name).OrderBy(q => q, StringComparer.OrdinalIgnoreCase).ToList(); + var allPlayerNames = playerInfos.Select(q => q.Name).OrderBy(q => q, StringComparer.OrdinalIgnoreCase).ToList(); var remotePlayerNames = allPlayerNames.Where(q => !q.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); var tunnelClientPlayerNames = allPlayerNames.Where(q => !q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); ushort localPort = localPorts[6 - remotePlayerNames.FindIndex(q => q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase))]; @@ -285,6 +291,8 @@ public void StartV3ConnectionListeners( p2pLocalTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); p2pLocalTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); + p2pLocalTunnelHandler.RaiseRemoteHostDataReceivedEvent += replayHandler.RemoteHostConnection_DataReceivedAsync; + p2pLocalTunnelHandler.RaiseLocalGameDataReceivedEvent += replayHandler.LocalGameConnection_DataReceivedAsync; p2pLocalTunnelHandler.SetUp(new(selectedRemoteIpAddress, remotePort), localPort, gameLocalPlayerId, cancellationToken); p2pLocalTunnelHandler.ConnectToTunnel(); @@ -294,12 +302,14 @@ public void StartV3ConnectionListeners( } } - foreach (IGrouping tunnelGrouping in PlayerTunnels.Where(q => !p2pPlayerTunnels.Contains(q.RemotePlayerName, StringComparer.OrdinalIgnoreCase)).GroupBy(q => q.Tunnel)) + foreach (IGrouping tunnelGrouping in playerTunnels.Where(q => !p2pPlayerTunnels.Contains(q.RemotePlayerName, StringComparer.OrdinalIgnoreCase)).GroupBy(q => q.Tunnel)) { var gameTunnelHandler = new V3GameTunnelHandler(); gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); + gameTunnelHandler.RaiseRemoteHostDataReceivedEvent += replayHandler.RemoteHostConnection_DataReceivedAsync; + gameTunnelHandler.RaiseLocalGameDataReceivedEvent += replayHandler.LocalGameConnection_DataReceivedAsync; gameTunnelHandler.SetUp(new(tunnelGrouping.Key.IPAddress, tunnelGrouping.Key.Port), 0, gameLocalPlayerId, cancellationToken); gameTunnelHandler.ConnectToTunnel(); @@ -308,28 +318,100 @@ public void StartV3ConnectionListeners( } } + public List StartPlayerConnections(List gamePlayerIds) + { + this.gamePlayerIds = gamePlayerIds; + + List usedPorts = new(ipV4P2PPorts.Select(q => q.InternalPort).Concat(ipV6P2PPorts.Select(q => q.InternalPort)).Distinct()); + + foreach ((List remotePlayerNames, V3GameTunnelHandler v3GameTunnelHandler) in V3GameTunnelHandlers) + { + var currentTunnelPlayers = playerInfos.Where(q => remotePlayerNames.Contains(q.Name)).ToList(); + IEnumerable indexes = currentTunnelPlayers.Select(q => q.Index); + var playerIds = indexes.Select(q => gamePlayerIds[q]).ToList(); + var createdLocalPlayerPorts = v3GameTunnelHandler.CreatePlayerConnections(playerIds).ToList(); + int i = 0; + + foreach (PlayerInfo currentTunnelPlayer in currentTunnelPlayers) + currentTunnelPlayer.Port = createdLocalPlayerPorts.Skip(i++).Take(1).Single(); + + usedPorts.AddRange(createdLocalPlayerPorts); + } + + foreach (V3GameTunnelHandler v3GameTunnelHandler in V3GameTunnelHandlers.Select(q => q.Tunnel)) + v3GameTunnelHandler.StartPlayerConnections(); + + return usedPorts; + } + + public async ValueTask SaveReplayAsync() + { + if (!RecordingEnabled) + return; + + await replayHandler.StopRecordingAsync(gamePlayerIds, playerInfos, V3GameTunnelHandlers.Select(q => q.Tunnel).ToList()).ConfigureAwait(false); + } + + public async ValueTask ClearConnectionsAsync() + { + if (replayHandler is not null) + await replayHandler.DisposeAsync().ConfigureAwait(false); + + foreach (V3GameTunnelHandler v3GameTunnelHandler in V3GameTunnelHandlers.Select(q => q.Tunnel)) + v3GameTunnelHandler.Dispose(); + + V3GameTunnelHandlers.Clear(); + } + public async ValueTask DisposeAsync() { PinnedTunnelPingsMessage = null; StunCancellationTokenSource?.Cancel(); - V3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); - V3GameTunnelHandlers.Clear(); - PlayerTunnels.Clear(); + await ClearConnectionsAsync().ConfigureAwait(false); + playerTunnels.Clear(); P2PPlayers.Clear(); PinnedTunnels?.Clear(); await CloseP2PPortsAsync().ConfigureAwait(false); } - private IEnumerable GetEligibleTunnels() + public IEnumerable GetEligibleTunnels() => tunnelHandler.Tunnels.Where(q => !q.RequiresPassword && q.PingInMs > -1 && q.Clients < q.MaxClients - 8 && q.Version is Constants.TUNNEL_VERSION_3); + public string HandleTunnelPingsMessage(string playerName, string tunnelPingsMessage) + { + string[] tunnelPingsLines = tunnelPingsMessage.Split('\t', StringSplitOptions.RemoveEmptyEntries); + IEnumerable<(int Ping, string Hash)> tunnelPings = tunnelPingsLines.Select(q => + { + string[] split = q.Split(';'); + + return (int.Parse(split[0], CultureInfo.InvariantCulture), split[1]); + }); + IEnumerable<(int CombinedPing, string Hash)> combinedTunnelResults = tunnelPings + .Where(q => PinnedTunnels.Select(r => r.Hash).Contains(q.Hash)) + .Select(q => (CombinedPing: q.Ping + PinnedTunnels.SingleOrDefault(r => q.Hash.Equals(r.Hash, StringComparison.OrdinalIgnoreCase)).Ping, q.Hash)); + (int combinedPing, string hash) = combinedTunnelResults + .OrderBy(q => q.CombinedPing) + .ThenBy(q => q.Hash, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(); + + if (hash is null) + return null; + + CnCNetTunnel tunnel = tunnelHandler.Tunnels.Single(q => q.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)); + + playerTunnels.Remove(playerTunnels.SingleOrDefault(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))); + playerTunnels.Add(new(playerName, tunnel, combinedPing)); + + return hash; + } + private async ValueTask CloseP2PPortsAsync() { try { if (internetGatewayDevice is not null) { - foreach (ushort p2pPort in IpV4P2PPorts.Select(q => q.InternalPort)) + foreach (ushort p2pPort in ipV4P2PPorts.Select(q => q.InternalPort)) await internetGatewayDevice.CloseIpV4PortAsync(p2pPort).ConfigureAwait(false); } } @@ -339,7 +421,7 @@ private async ValueTask CloseP2PPortsAsync() } finally { - IpV4P2PPorts.Clear(); + ipV4P2PPorts.Clear(); } try @@ -356,7 +438,7 @@ private async ValueTask CloseP2PPortsAsync() } finally { - IpV6P2PPorts.Clear(); + ipV6P2PPorts.Clear(); p2pIpV6PortIds.Clear(); } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs index b1bbab69e..0de460fb9 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs @@ -30,15 +30,26 @@ internal sealed class V3GameTunnelHandler : IDisposable /// public event EventHandler RaiseRemoteHostConnectionFailedEvent; + /// + /// Occurs when data from a remote host is received. + /// + public event EventHandler RaiseRemoteHostDataReceivedEvent; + + /// + /// Occurs when data from the local game is received. + /// + public event EventHandler RaiseLocalGameDataReceivedEvent; + public bool ConnectSucceeded { get; private set; } public void SetUp(IPEndPoint remoteIpEndPoint, ushort localPort, uint gameLocalPlayerId, CancellationToken cancellationToken) { - using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(connectionErrorCancellationTokenSource.Token, cancellationToken); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource( + connectionErrorCancellationTokenSource.Token, cancellationToken); - remoteHostConnection = new V3RemotePlayerConnection(); - remoteHostConnectionDataReceivedFunc = (_, e) => RemoteHostConnection_DataReceivedAsync(e).HandleTask(); - localGameConnectionDataReceivedFunc = (_, e) => LocalGameConnection_DataReceivedAsync(e).HandleTask(); + remoteHostConnection = new(); + remoteHostConnectionDataReceivedFunc = (sender, e) => RemoteHostConnection_DataReceivedAsync(sender, e).HandleTask(); + localGameConnectionDataReceivedFunc = (sender, e) => LocalGameConnection_DataReceivedAsync(sender, e).HandleTask(); remoteHostConnection.RaiseConnectedEvent += RemoteHostConnection_Connected; remoteHostConnection.RaiseConnectionFailedEvent += RemoteHostConnection_ConnectionFailed; @@ -70,12 +81,7 @@ public void StartPlayerConnections() } public void ConnectToTunnel() - { - if (remoteHostConnection == null) - throw new InvalidOperationException($"Call SetUp before calling {nameof(ConnectToTunnel)}."); - - remoteHostConnection.StartConnectionAsync().HandleTask(); - } + => remoteHostConnection.StartConnectionAsync().HandleTask(); public void Dispose() { @@ -119,14 +125,26 @@ private void LocalGameConnection_ConnectionCut(object sender, EventArgs e) /// /// Forwards local game data to the remote host. /// - private ValueTask LocalGameConnection_DataReceivedAsync(DataReceivedEventArgs e) - => remoteHostConnection?.SendDataAsync(e.GameData, e.PlayerId) ?? ValueTask.CompletedTask; + private async ValueTask LocalGameConnection_DataReceivedAsync(object sender, DataReceivedEventArgs e) + { + OnRaiseLocalGameDataReceivedEvent(sender, e); + + if (remoteHostConnection is not null) + await remoteHostConnection.SendDataAsync(e.GameData, e.PlayerId).ConfigureAwait(false); + } /// /// Forwards remote player data to the local game. /// - private ValueTask RemoteHostConnection_DataReceivedAsync(DataReceivedEventArgs e) - => GetLocalPlayerConnection(e.PlayerId)?.SendDataAsync(e.GameData) ?? ValueTask.CompletedTask; + private async ValueTask RemoteHostConnection_DataReceivedAsync(object sender, DataReceivedEventArgs e) + { + OnRaiseRemoteHostDataReceivedEvent(sender, e); + + V3LocalPlayerConnection v3LocalPlayerConnection = GetLocalPlayerConnection(e.PlayerId); + + if (v3LocalPlayerConnection is not null) + await v3LocalPlayerConnection.SendDataAsync(e.GameData).ConfigureAwait(false); + } private V3LocalPlayerConnection GetLocalPlayerConnection(uint senderId) => localGameConnections.TryGetValue(senderId, out V3LocalPlayerConnection connection) ? connection : null; @@ -157,4 +175,18 @@ private void OnRaiseRemoteHostConnectionFailedEvent(EventArgs e) private void RemoteHostConnection_ConnectionCut(object sender, EventArgs e) => Dispose(); + + private void OnRaiseRemoteHostDataReceivedEvent(object sender, DataReceivedEventArgs e) + { + EventHandler raiseEvent = RaiseRemoteHostDataReceivedEvent; + + raiseEvent?.Invoke(sender, e); + } + + private void OnRaiseLocalGameDataReceivedEvent(object sender, DataReceivedEventArgs e) + { + EventHandler raiseEvent = RaiseLocalGameDataReceivedEvent; + + raiseEvent?.Invoke(sender, e); + } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs index 9922e604e..f9e105074 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs @@ -20,13 +20,15 @@ internal sealed class V3LocalPlayerConnection : IDisposable private const int SendTimeout = 10000; private const int GameStartReceiveTimeout = 60000; private const int ReceiveTimeout = 10000; - private const int MinimumPacketSize = 8; + private const int PlayerIdSize = sizeof(uint); + private const int MinimumPacketSize = PlayerIdSize * 2; private const int MaximumPacketSize = 1024; private Socket localGameSocket; private EndPoint remotePlayerEndPoint; private CancellationToken cancellationToken; - private uint playerId; + + public uint PlayerId { get; private set; } /// /// Creates a local game socket and returns the port. @@ -37,7 +39,7 @@ internal sealed class V3LocalPlayerConnection : IDisposable public ushort Setup(uint playerId, CancellationToken cancellationToken) { this.cancellationToken = cancellationToken; - this.playerId = playerId; + PlayerId = playerId; localGameSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); // Disable ICMP port not reachable exceptions, happens when the game is still loading and has not yet opened the socket. @@ -71,9 +73,9 @@ public async ValueTask StartConnectionAsync() int receiveTimeout = GameStartReceiveTimeout; #if DEBUG - Logger.Log($"Start listening for local game {remotePlayerEndPoint} on {localGameSocket.LocalEndPoint} for player {playerId}."); + Logger.Log($"Start listening for local game {remotePlayerEndPoint} on {localGameSocket.LocalEndPoint} for player {PlayerId}."); #else - Logger.Log($"Start listening for local game for player {playerId}."); + Logger.Log($"Start listening for local game for player {PlayerId}."); #endif while (!cancellationToken.IsCancellationRequested) @@ -91,15 +93,15 @@ public async ValueTask StartConnectionAsync() data = buffer[..socketReceiveFromResult.ReceivedBytes]; #if DEBUG - Logger.Log($"Received data from local game {socketReceiveFromResult.RemoteEndPoint} on {localGameSocket.LocalEndPoint} for player {playerId}."); + Logger.Log($"Received data from local game {socketReceiveFromResult.RemoteEndPoint} on {localGameSocket.LocalEndPoint} for player {PlayerId}."); #endif } catch (SocketException ex) { #if DEBUG - ProgramConstants.LogException(ex, $"Socket exception in {remotePlayerEndPoint} receive loop for player {playerId}."); + ProgramConstants.LogException(ex, $"Socket exception in {remotePlayerEndPoint} receive loop for player {PlayerId}."); #else - ProgramConstants.LogException(ex, $"Socket exception in receive loop for player {playerId}."); + ProgramConstants.LogException(ex, $"Socket exception in receive loop for player {PlayerId}."); #endif OnRaiseConnectionCutEvent(EventArgs.Empty); @@ -116,9 +118,9 @@ public async ValueTask StartConnectionAsync() catch (OperationCanceledException) { #if DEBUG - Logger.Log($"Local game connection {localGameSocket.LocalEndPoint} timed out for player {playerId} when receiving data."); + Logger.Log($"Local game connection {localGameSocket.LocalEndPoint} timed out for player {PlayerId} when receiving data."); #else - Logger.Log($"Local game connection timed out for player {playerId} when receiving data."); + Logger.Log($"Local game connection timed out for player {PlayerId} when receiving data."); #endif OnRaiseConnectionCutEvent(EventArgs.Empty); @@ -127,7 +129,7 @@ public async ValueTask StartConnectionAsync() receiveTimeout = ReceiveTimeout; - OnRaiseDataReceivedEvent(new(playerId, data)); + OnRaiseDataReceivedEvent(new(PlayerId, data)); } } @@ -146,7 +148,7 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data) try { #if DEBUG - Logger.Log($"Sending data from {localGameSocket.LocalEndPoint} to local game {remotePlayerEndPoint} for player {playerId}."); + Logger.Log($"Sending data from {localGameSocket.LocalEndPoint} to local game {remotePlayerEndPoint} for player {PlayerId}."); #endif await localGameSocket.SendToAsync(data, SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); @@ -154,9 +156,9 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data) catch (SocketException ex) { #if DEBUG - ProgramConstants.LogException(ex, $"Socket exception sending data to {remotePlayerEndPoint} for player {playerId}."); + ProgramConstants.LogException(ex, $"Socket exception sending data to {remotePlayerEndPoint} for player {PlayerId}."); #else - ProgramConstants.LogException(ex, $"Socket exception sending data for player {playerId}."); + ProgramConstants.LogException(ex, $"Socket exception sending data for player {PlayerId}."); #endif OnRaiseConnectionCutEvent(EventArgs.Empty); } @@ -169,9 +171,9 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data) catch (OperationCanceledException) { #if DEBUG - Logger.Log($"Local game connection {localGameSocket.LocalEndPoint} timed out for player {playerId} when sending data."); + Logger.Log($"Local game connection {localGameSocket.LocalEndPoint} timed out for player {PlayerId} when sending data."); #else - Logger.Log($"Local game connection timed out for player {playerId} when sending data."); + Logger.Log($"Local game connection timed out for player {PlayerId} when sending data."); #endif OnRaiseConnectionCutEvent(EventArgs.Empty); } @@ -180,9 +182,9 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data) public void Dispose() { #if DEBUG - Logger.Log($"Connection to local game {remotePlayerEndPoint} closed for player {playerId}."); + Logger.Log($"Connection to local game {remotePlayerEndPoint} closed for player {PlayerId}."); #else - Logger.Log($"Connection to local game closed for player {playerId}."); + Logger.Log($"Connection to local game closed for player {PlayerId}."); #endif localGameSocket.Close(); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index a0c23341d..7501c782d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -17,19 +17,21 @@ internal sealed class V3RemotePlayerConnection : IDisposable private const int SendTimeout = 10000; private const int GameStartReceiveTimeout = 1200000; private const int ReceiveTimeout = 1200000; - private const int MinimumPacketSize = 8; + private const int PlayerIdSize = sizeof(uint); + private const int MinimumPacketSize = PlayerIdSize * 2; private const int MaximumPacketSize = 1024; - private uint gameLocalPlayerId; private CancellationToken cancellationToken; private Socket tunnelSocket; private IPEndPoint remoteEndPoint; private ushort localPort; + public uint GameLocalPlayerId { get; private set; } + public void SetUp(IPEndPoint remoteEndPoint, ushort localPort, uint gameLocalPlayerId, CancellationToken cancellationToken) { this.cancellationToken = cancellationToken; - this.gameLocalPlayerId = gameLocalPlayerId; + GameLocalPlayerId = gameLocalPlayerId; this.remoteEndPoint = remoteEndPoint; this.localPort = localPort; } @@ -72,7 +74,7 @@ public async ValueTask StartConnectionAsync() using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(MaximumPacketSize); Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; - if (!BitConverter.TryWriteBytes(buffer.Span[..4], gameLocalPlayerId)) + if (!BitConverter.TryWriteBytes(buffer.Span[..PlayerIdSize], GameLocalPlayerId)) throw new GameDataException(); using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); @@ -130,10 +132,10 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data, uint receiverId) using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); Memory packet = memoryOwner.Memory[..bufferSize]; - if (!BitConverter.TryWriteBytes(packet.Span[..4], gameLocalPlayerId)) + if (!BitConverter.TryWriteBytes(packet.Span[..PlayerIdSize], GameLocalPlayerId)) throw new GameDataException(); - if (!BitConverter.TryWriteBytes(packet.Span[4..8], receiverId)) + if (!BitConverter.TryWriteBytes(packet.Span[PlayerIdSize..(PlayerIdSize * 2)], receiverId)) throw new GameDataException(); data.CopyTo(packet[8..]); @@ -144,7 +146,7 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data, uint receiverId) try { #if DEBUG - Logger.Log($"Sending data {gameLocalPlayerId} -> {receiverId} from {tunnelSocket.LocalEndPoint} to {remoteEndPoint}."); + Logger.Log($"Sending data {GameLocalPlayerId} -> {receiverId} from {tunnelSocket.LocalEndPoint} to {remoteEndPoint}."); #endif await tunnelSocket.SendToAsync(packet, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); @@ -251,20 +253,20 @@ private async ValueTask ReceiveLoopAsync() continue; } - Memory data = buffer[8..socketReceiveFromResult.ReceivedBytes]; - uint senderId = BitConverter.ToUInt32(buffer[..4].Span); - uint receiverId = BitConverter.ToUInt32(buffer[4..8].Span); + Memory data = buffer[(PlayerIdSize * 2)..socketReceiveFromResult.ReceivedBytes]; + uint senderId = BitConverter.ToUInt32(buffer[..PlayerIdSize].Span); + uint receiverId = BitConverter.ToUInt32(buffer[PlayerIdSize..(PlayerIdSize * 2)].Span); #if DEBUG Logger.Log($"Received {senderId} -> {receiverId} from {socketReceiveFromResult.RemoteEndPoint} on {tunnelSocket.LocalEndPoint}."); #endif - if (receiverId != gameLocalPlayerId) + if (receiverId != GameLocalPlayerId) { #if DEBUG - Logger.Log($"Invalid target (received: {receiverId}, expected: {gameLocalPlayerId}) from {socketReceiveFromResult.RemoteEndPoint}."); + Logger.Log($"Invalid target (received: {receiverId}, expected: {GameLocalPlayerId}) from {socketReceiveFromResult.RemoteEndPoint}."); #else - Logger.Log($"Invalid target (received: {receiverId}, expected: {gameLocalPlayerId}) on port {localPort}."); + Logger.Log($"Invalid target (received: {receiverId}, expected: {GameLocalPlayerId}) on port {localPort}."); #endif continue; diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index e6bdea921..363d821ca 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -692,7 +692,7 @@ public async ValueTask LoadPreviewTextureAsync() if (!Official) { // Extract preview from the map itself - using Image preview = await MapPreviewExtractor.ExtractMapPreviewAsync(GetCustomMapIniFile()).ConfigureAwait(false); + using Image preview = await MapPreviewExtractor.ExtractMapPreviewAsync(GetCustomMapIniFile()).ConfigureAwait(true); if (preview != null) { diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index f70160c47..d43dd1c2c 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -181,7 +181,7 @@ private async ValueTask LoadCustomMapsAsync() } // save cache - CacheCustomMaps(customMapCache); + await CacheCustomMapsAsync(customMapCache).ConfigureAwait(false); foreach (Map map in customMapCache.Values) { @@ -193,16 +193,20 @@ private async ValueTask LoadCustomMapsAsync() /// Save cache of custom maps. /// /// Custom maps to cache - private void CacheCustomMaps(ConcurrentDictionary customMaps) + private async ValueTask CacheCustomMapsAsync(ConcurrentDictionary customMaps) { var customMapCache = new CustomMapCache { Maps = customMaps, Version = CurrentCustomMapCacheVersion }; - var jsonData = JsonSerializer.Serialize(customMapCache, jsonSerializerOptions); - File.WriteAllText(CUSTOM_MAPS_CACHE, jsonData); + FileStream fileStream = File.OpenWrite(CUSTOM_MAPS_CACHE); + + await using (fileStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(fileStream, customMapCache, jsonSerializerOptions).ConfigureAwait(false); + } } /// diff --git a/DXMainClient/Online/CnCNetUserData.cs b/DXMainClient/Online/CnCNetUserData.cs index 667c56bb9..9d782954a 100644 --- a/DXMainClient/Online/CnCNetUserData.cs +++ b/DXMainClient/Online/CnCNetUserData.cs @@ -6,6 +6,8 @@ using System.IO; using System.Linq; using System.Text.Json; +using System.Threading.Tasks; +using ClientCore.Extensions; namespace DTAClient.Online { @@ -22,40 +24,43 @@ public sealed class CnCNetUserData /// directly you have to also invoke UserFriendToggled event handler for every /// user name added or removed. /// - public List FriendList { get; private set; } = new(); + public List FriendList { get; private set; } /// /// A list which contains idents of ignored users. If you manipulate this list /// directly you have to also invoke UserIgnoreToggled event handler for every /// user ident added or removed. /// - public List IgnoreList { get; private set; } = new(); + public List IgnoreList { get; private set; } /// /// A list which contains names of players from recent games. /// - public List RecentList { get; private set; } = new(); + public List RecentList { get; private set; } public event EventHandler UserFriendToggled; public event EventHandler UserIgnoreToggled; public CnCNetUserData(WindowManager windowManager) { - LoadFriendList(); - LoadIgnoreList(); - LoadRecentPlayerList(); - windowManager.GameClosing += WindowManager_GameClosing; } - private static List LoadTextList(string path) + public async ValueTask InitializeAsync() + { + FriendList = await LoadTextListAsync(FRIEND_LIST_PATH).ConfigureAwait(false); + IgnoreList = await LoadTextListAsync(IGNORE_LIST_PATH).ConfigureAwait(false); + RecentList = await LoadJsonListAsync(RECENT_LIST_PATH).ConfigureAwait(false); + } + + private static async ValueTask> LoadTextListAsync(string path) { try { FileInfo listFile = SafePath.GetFile(ProgramConstants.GamePath, path); if (listFile.Exists) - return File.ReadAllLines(listFile.FullName).ToList(); + return (await File.ReadAllLinesAsync(listFile.FullName).ConfigureAwait(false)).ToList(); Logger.Log($"Loading {path} failed! File does not exist."); return new(); @@ -67,14 +72,21 @@ private static List LoadTextList(string path) } } - private static List LoadJsonList(string path) + private static async ValueTask> LoadJsonListAsync(string path) { try { FileInfo listFile = SafePath.GetFile(ProgramConstants.GamePath, path); if (listFile.Exists) - return JsonSerializer.Deserialize>(File.ReadAllText(listFile.FullName)) ?? new List(); + { + FileStream fileStream = File.OpenRead(listFile.FullName); + + await using (fileStream.ConfigureAwait(false)) + { + return (await JsonSerializer.DeserializeAsync>(fileStream).ConfigureAwait(false)) ?? new List(); + } + } Logger.Log($"Loading {path} failed! File does not exist."); return new(); @@ -86,7 +98,7 @@ private static List LoadJsonList(string path) } } - private static void SaveTextList(string path, List textList) + private static async ValueTask SaveTextListAsync(string path, List textList) { Logger.Log($"Saving {path}."); @@ -95,7 +107,7 @@ private static void SaveTextList(string path, List textList) FileInfo listFileInfo = SafePath.GetFile(ProgramConstants.GamePath, path); listFileInfo.Delete(); - File.WriteAllLines(listFileInfo.FullName, textList.ToArray()); + await File.WriteAllLinesAsync(listFileInfo.FullName, textList).ConfigureAwait(false); } catch (Exception ex) { @@ -103,7 +115,7 @@ private static void SaveTextList(string path, List textList) } } - private static void SaveJsonList(string path, IReadOnlyCollection jsonList) + private static async ValueTask SaveJsonListAsync(string path, IReadOnlyCollection jsonList) { Logger.Log($"Saving {path}."); @@ -112,7 +124,13 @@ private static void SaveJsonList(string path, IReadOnlyCollection jsonList FileInfo listFileInfo = SafePath.GetFile(ProgramConstants.GamePath, path); listFileInfo.Delete(); - File.WriteAllText(listFileInfo.FullName, JsonSerializer.Serialize(jsonList)); + + FileStream fileStream = listFileInfo.OpenWrite(); + + await using (fileStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(fileStream, jsonList).ConfigureAwait(false); + } } catch (Exception ex) { @@ -131,25 +149,13 @@ private static void Toggle(string value, ICollection list) list.Add(value); } - private void LoadFriendList() => FriendList = LoadTextList(FRIEND_LIST_PATH); - - private void LoadIgnoreList() => IgnoreList = LoadTextList(IGNORE_LIST_PATH); - - private void LoadRecentPlayerList() => RecentList = LoadJsonList(RECENT_LIST_PATH); - - private void WindowManager_GameClosing(object sender, EventArgs e) => Save(); - - private void SaveFriends() => SaveTextList(FRIEND_LIST_PATH, FriendList); - - private void SaveIgnoreList() => SaveTextList(IGNORE_LIST_PATH, IgnoreList); - - private void SaveRecentList() => SaveJsonList(RECENT_LIST_PATH, RecentList); + private void WindowManager_GameClosing(object sender, EventArgs e) => SaveAsync().HandleTask(); - private void Save() + private async ValueTask SaveAsync() { - SaveFriends(); - SaveIgnoreList(); - SaveRecentList(); + await SaveTextListAsync(FRIEND_LIST_PATH, FriendList).ConfigureAwait(false); + await SaveTextListAsync(IGNORE_LIST_PATH, IgnoreList).ConfigureAwait(false); + await SaveJsonListAsync(RECENT_LIST_PATH, RecentList).ConfigureAwait(false); } /// From 021c3e0d4121b0f4bab02d5127fea0d5f2c1ecbe Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 18 Dec 2022 15:42:48 +0100 Subject: [PATCH 066/109] Game recording cleanup --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 8 +- .../Multiplayer/GameLobby/GameLobbyBase.cs | 4 +- .../CnCNet/Replays/ReplayHandler.cs | 9 +- .../Multiplayer/CnCNet/V3ConnectionState.cs | 94 ++++++++++++------- 4 files changed, 74 insertions(+), 41 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 36e26c723..2fadc4648 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -182,7 +182,7 @@ public CnCNetGameLobby( CnCNetLobbyCommands.RECORD, "Toggle recording game replay".L10N("Client:Main:ChangeRecord"), false, - _ => ToggleRecord())); + _ => ToggleRecordAsync().HandleTask())); AddChatBoxCommand(new( CnCNetLobbyCommands.REPLAY, "Start a game replay.\nExample: \"/replay REPLAYID".L10N("Client:Main:StartReplay"), @@ -1399,11 +1399,11 @@ private async ValueTask ToggleP2PAsync() await SendPlayerP2PRequestAsync().ConfigureAwait(false); } - private void ToggleRecord() + private async ValueTask ToggleRecordAsync() { - v3ConnectionState.RecordingEnabled = !v3ConnectionState.RecordingEnabled; + bool recordingEnabled = await v3ConnectionState.ToggleRecordingAsync().ConfigureAwait(false); - if (v3ConnectionState.RecordingEnabled) + if (recordingEnabled) AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} enabled game recording".L10N("Client:Main:RecordEnabled"), FindLocalPlayer().Name)); else AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled game recording".L10N("Client:Main:RecordDisabled"), FindLocalPlayer().Name)); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 1520444f5..d320048ed 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1798,9 +1798,9 @@ protected virtual async ValueTask GameProcessExitedAsync() GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; Logger.Log("GameProcessExited: Parsing statistics."); - matchStatistics.ParseStatisticsAsync(ProgramConstants.GamePath, false).HandleTask(); + await matchStatistics.ParseStatisticsAsync(ProgramConstants.GamePath, false).ConfigureAwait(true); Logger.Log("GameProcessExited: Adding match to statistics."); - StatisticsManager.Instance.AddMatchAndSaveDatabaseAsync(true, matchStatistics).HandleTask(); + await StatisticsManager.Instance.AddMatchAndSaveDatabaseAsync(true, matchStatistics).ConfigureAwait(true); ClearReadyStatuses(); CopyPlayerDataToUI(); UpdateDiscordPresence(true); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs index 73f6eac83..4c72ed46c 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs @@ -30,6 +30,7 @@ public void SetupRecording(int replayId, uint gameLocalPlayerId) this.gameLocalPlayerId = gameLocalPlayerId; startTimestamp = DateTimeOffset.Now; replayDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, ProgramConstants.REPLAYS_DIRECTORY, replayId.ToString(CultureInfo.InvariantCulture)); + gameStarted = false; replayDirectory.Create(); replayFileStreams.Add(gameLocalPlayerId, CreateReplayFileStream()); @@ -97,7 +98,7 @@ public async ValueTask StopRecordingAsync(List gamePlayerIds, List playerTunnels = new(); + private readonly ReplayHandler replayHandler = new(); private IPAddress publicIpV4Address; private IPAddress publicIpV6Address; @@ -28,7 +29,6 @@ internal sealed class V3ConnectionState : IAsyncDisposable private List gamePlayerIds; private List<(ushort InternalPort, ushort ExternalPort)> ipV6P2PPorts = new(); private List<(ushort InternalPort, ushort ExternalPort)> ipV4P2PPorts = new(); - private ReplayHandler replayHandler; public List<(int Ping, string Hash)> PinnedTunnels { get; private set; } = new(); @@ -121,6 +121,18 @@ public async ValueTask ToggleP2PAsync() return false; } + public async ValueTask ToggleRecordingAsync() + { + RecordingEnabled = !RecordingEnabled; + + if (RecordingEnabled) + return true; + + await replayHandler.DisposeAsync().ConfigureAwait(false); + + return false; + } + public async ValueTask PingRemotePlayer(string playerName, string p2pRequestMessage) { List<(IPAddress RemoteIpAddress, long Ping)> localPingResults = new(); @@ -231,24 +243,18 @@ public void StartV3ConnectionListeners( V3GameTunnelHandlers.Clear(); if (RecordingEnabled) - { - replayHandler = new(); - replayHandler.SetupRecording(uniqueGameId, gameLocalPlayerId); - } if (!DynamicTunnelsEnabled) { - var gameTunnelHandler = new V3GameTunnelHandler(); - - gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); - gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); - gameTunnelHandler.RaiseRemoteHostDataReceivedEvent += replayHandler.RemoteHostConnection_DataReceivedAsync; - gameTunnelHandler.RaiseLocalGameDataReceivedEvent += replayHandler.LocalGameConnection_DataReceivedAsync; - - gameTunnelHandler.SetUp(new(tunnelHandler.CurrentTunnel.IPAddress, tunnelHandler.CurrentTunnel.Port), 0, gameLocalPlayerId, cancellationToken); - gameTunnelHandler.ConnectToTunnel(); - V3GameTunnelHandlers.Add(new(playerInfos.Where(q => !q.Name.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase)).Select(q => q.Name).ToList(), gameTunnelHandler)); + SetupGameTunnelHandler( + gameLocalPlayerId, + remoteHostConnectedAction, + remoteHostConnectionFailedAction, + playerInfos.Where(q => !q.Name.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase)).Select(q => q.Name).ToList(), + new(tunnelHandler.CurrentTunnel.IPAddress, tunnelHandler.CurrentTunnel.Port), + 0, + cancellationToken); } else { @@ -287,16 +293,15 @@ public void StartV3ConnectionListeners( var tunnelClientPlayerNames = allPlayerNames.Where(q => !q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).ToList(); ushort localPort = localPorts[6 - remotePlayerNames.FindIndex(q => q.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase))]; ushort remotePort = remotePorts[6 - tunnelClientPlayerNames.FindIndex(q => q.Equals(localPlayerName, StringComparison.OrdinalIgnoreCase))]; - var p2pLocalTunnelHandler = new V3GameTunnelHandler(); - p2pLocalTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); - p2pLocalTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); - p2pLocalTunnelHandler.RaiseRemoteHostDataReceivedEvent += replayHandler.RemoteHostConnection_DataReceivedAsync; - p2pLocalTunnelHandler.RaiseLocalGameDataReceivedEvent += replayHandler.LocalGameConnection_DataReceivedAsync; - - p2pLocalTunnelHandler.SetUp(new(selectedRemoteIpAddress, remotePort), localPort, gameLocalPlayerId, cancellationToken); - p2pLocalTunnelHandler.ConnectToTunnel(); - V3GameTunnelHandlers.Add(new(new() { remotePlayerName }, p2pLocalTunnelHandler)); + SetupGameTunnelHandler( + gameLocalPlayerId, + remoteHostConnectedAction, + remoteHostConnectionFailedAction, + new() { remotePlayerName }, + new(selectedRemoteIpAddress, remotePort), + localPort, + cancellationToken); p2pPlayerTunnels.Add(remotePlayerName); } } @@ -304,18 +309,41 @@ public void StartV3ConnectionListeners( foreach (IGrouping tunnelGrouping in playerTunnels.Where(q => !p2pPlayerTunnels.Contains(q.RemotePlayerName, StringComparer.OrdinalIgnoreCase)).GroupBy(q => q.Tunnel)) { - var gameTunnelHandler = new V3GameTunnelHandler(); + SetupGameTunnelHandler( + gameLocalPlayerId, + remoteHostConnectedAction, + remoteHostConnectionFailedAction, + tunnelGrouping.Select(q => q.Name).ToList(), + new(tunnelGrouping.Key.IPAddress, tunnelGrouping.Key.Port), + 0, + cancellationToken); + } + } + } - gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); - gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); - gameTunnelHandler.RaiseRemoteHostDataReceivedEvent += replayHandler.RemoteHostConnection_DataReceivedAsync; - gameTunnelHandler.RaiseLocalGameDataReceivedEvent += replayHandler.LocalGameConnection_DataReceivedAsync; + private void SetupGameTunnelHandler( + uint gameLocalPlayerId, + Action remoteHostConnectedAction, + Action remoteHostConnectionFailedAction, + List remotePlayerNames, + IPEndPoint remoteIpEndpoint, + ushort localPort, + CancellationToken cancellationToken) + { + var gameTunnelHandler = new V3GameTunnelHandler(); - gameTunnelHandler.SetUp(new(tunnelGrouping.Key.IPAddress, tunnelGrouping.Key.Port), 0, gameLocalPlayerId, cancellationToken); - gameTunnelHandler.ConnectToTunnel(); - V3GameTunnelHandlers.Add(new(tunnelGrouping.Select(q => q.Name).ToList(), gameTunnelHandler)); - } + gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); + gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); + + if (RecordingEnabled) + { + gameTunnelHandler.RaiseRemoteHostDataReceivedEvent += replayHandler.RemoteHostConnection_DataReceivedAsync; + gameTunnelHandler.RaiseLocalGameDataReceivedEvent += replayHandler.LocalGameConnection_DataReceivedAsync; } + + gameTunnelHandler.SetUp(remoteIpEndpoint, localPort, gameLocalPlayerId, cancellationToken); + gameTunnelHandler.ConnectToTunnel(); + V3GameTunnelHandlers.Add(new(remotePlayerNames, gameTunnelHandler)); } public List StartPlayerConnections(List gamePlayerIds) From de77ef8921a47cd62ad06ce10828c9de1fe79d5e Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 20 Dec 2022 11:33:15 +0100 Subject: [PATCH 067/109] Don't parse statistics on UI thread --- .../Multiplayer/GameLobby/GameLobbyBase.cs | 20 ++++++++++++++----- .../GameLobby/MultiplayerGameLobby.cs | 1 - 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index d320048ed..1bdfdbeb3 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1793,17 +1793,27 @@ protected virtual async ValueTask StartGameAsync() private void GameProcessExited_Callback() => AddCallback(() => GameProcessExitedAsync().HandleTask()); - protected virtual async ValueTask GameProcessExitedAsync() + protected virtual ValueTask GameProcessExitedAsync() { GameProcessLogic.GameProcessExited -= GameProcessExited_Callback; - Logger.Log("GameProcessExited: Parsing statistics."); - await matchStatistics.ParseStatisticsAsync(ProgramConstants.GamePath, false).ConfigureAwait(true); - Logger.Log("GameProcessExited: Adding match to statistics."); - await StatisticsManager.Instance.AddMatchAndSaveDatabaseAsync(true, matchStatistics).ConfigureAwait(true); + ParseStatisticsAsync().HandleTask(); ClearReadyStatuses(); CopyPlayerDataToUI(); UpdateDiscordPresence(true); + + return ValueTask.CompletedTask; + } + + private async ValueTask ParseStatisticsAsync() + { + if (matchStatistics is not null) + { + Logger.Log("GameProcessExited: Parsing statistics."); + await matchStatistics.ParseStatisticsAsync(ProgramConstants.GamePath, false).ConfigureAwait(false); + Logger.Log("GameProcessExited: Adding match to statistics."); + await StatisticsManager.Instance.AddMatchAndSaveDatabaseAsync(true, matchStatistics).ConfigureAwait(false); + } } /// diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index af6335421..6aa5def4a 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using Rampastring.XNAUI; -using Rampastring.XNAUI.XNAControls; using Microsoft.Xna.Framework; using ClientCore; using System.IO; From f3b1886512ea72b1a8bbf4904c9850bf7d93bc98 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 21 Dec 2022 13:35:08 +0100 Subject: [PATCH 068/109] Prevent some exceptions --- .../Domain/Multiplayer/CnCNet/TunnelHandler.cs | 10 ++-------- DXMainClient/Online/Connection.cs | 6 +++++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index eceb528bb..25d50dc22 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -59,16 +59,10 @@ public TunnelHandler(WindowManager wm, CnCNetManager connectionManager) public CnCNetTunnel CurrentTunnel { get; set; } private void DoTunnelPinged(int index) - { - if (TunnelPinged != null) - wm.AddCallback(() => TunnelPinged(index)); - } + => wm.AddCallback(() => TunnelPinged?.Invoke(index)); private void DoCurrentTunnelPinged() - { - if (CurrentTunnelPinged != null) - wm.AddCallback(() => CurrentTunnelPinged(this, EventArgs.Empty)); - } + => wm.AddCallback(() => CurrentTunnelPinged?.Invoke(this, EventArgs.Empty)); private void ConnectionManager_Connected(object sender, EventArgs e) => Enabled = true; diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index b7c69c877..c1ee1b4f3 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -944,10 +944,14 @@ private async ValueTask SendMessageAsync(string message) { await socket.SendAsync(buffer, SocketFlags.None, timeoutCancellationTokenSource.Token).ConfigureAwait(false); } - catch (IOException ex) + catch (SocketException ex) { ProgramConstants.LogException(ex, "Sending message to the server failed!"); } + catch (OperationCanceledException ex) + { + ProgramConstants.LogException(ex, "Sending message to the server timed out!"); + } } private int NextQueueID { get; set; } From 670be6610b73e5d450d1ec8cc1deb0dd9b380134 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 21 Dec 2022 22:07:15 +0100 Subject: [PATCH 069/109] P2P: don't fail on UPnP device unavailable services --- .../Multiplayer/CnCNet/TunnelHandler.cs | 4 +- .../CnCNet/UPNP/InternetGatewayDevice.cs | 130 ++++--- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 320 +++++++++--------- .../Multiplayer/CnCNet/V3ConnectionState.cs | 18 +- .../Domain/Multiplayer/NetworkHelper.cs | 59 ++-- 5 files changed, 292 insertions(+), 239 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 25d50dc22..f65d864ce 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -147,14 +147,14 @@ private static async ValueTask> DoRefreshTunnelsAsync() { data = await Constants.CnCNetHttpClient.GetStringAsync(new Uri(ProgramConstants.CNCNET_TUNNEL_LIST_URL)).ConfigureAwait(false); } - catch (HttpRequestException ex) + catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException) { ProgramConstants.LogException(ex, "Error when downloading tunnel server info. Retrying."); try { data = await Constants.CnCNetHttpClient.GetStringAsync(new Uri(ProgramConstants.CNCNET_TUNNEL_LIST_URL)).ConfigureAwait(false); } - catch (HttpRequestException ex1) + catch (Exception ex1) when (ex1 is HttpRequestException or OperationCanceledException) { ProgramConstants.LogException(ex1); if (!tunnelCacheFile.Exists) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index b105c7bb0..bde86d41d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -126,80 +126,110 @@ public async ValueTask GetExternalIpV4AddressAsync(CancellationToken { Logger.Log($"Requesting external IP address from UPnP device {UPnPDescription.Device.FriendlyName}."); - int uPnPVersion = GetDeviceUPnPVersion(); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}"); - string serviceAction = $"\"{service.ServiceType}#GetExternalIPAddress\""; - IPAddress ipAddress; - - switch (uPnPVersion) + try { - case 2: - GetExternalIPAddressResponseV2 getExternalIpAddressResponseV2 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + int uPnPVersion = GetDeviceUPnPVersion(); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}"); + string serviceAction = $"\"{service.ServiceType}#GetExternalIPAddress\""; + IPAddress ipAddress; - ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV2.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV2.ExternalIPAddress); + switch (uPnPVersion) + { + case 2: + GetExternalIPAddressResponseV2 getExternalIpAddressResponseV2 = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); - break; - case 1: - GetExternalIPAddressResponseV1 getExternalIpAddressResponseV1 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV2.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV2.ExternalIPAddress); - ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV1.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV1.ExternalIPAddress); - break; - default: - throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); - } + break; + case 1: + GetExternalIPAddressResponseV1 getExternalIpAddressResponseV1 = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + + ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV1.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV1.ExternalIPAddress); + break; + default: + throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); + } - Logger.Log($"Received external IP address {ipAddress} from UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"Received external IP address {ipAddress} from UPnP device {UPnPDescription.Device.FriendlyName}."); + + return ipAddress; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Logger.Log($"GetExternalIPAddress error/not supported on UPnP device {UPnPDescription.Device.FriendlyName}."); + ProgramConstants.LogException(ex); + } - return ipAddress; + return null; } - public async ValueTask GetNatRsipStatusAsync(CancellationToken cancellationToken) + public async ValueTask GetNatRsipStatusAsync(CancellationToken cancellationToken) { Logger.Log($"Checking NAT status on UPnP device {UPnPDescription.Device.FriendlyName}."); - int uPnPVersion = GetDeviceUPnPVersion(); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}"); - string serviceAction = $"\"{service.ServiceType}#GetNatRsipStatus\""; - bool natEnabled; - - switch (uPnPVersion) + try { - case 2: - GetNatRsipStatusResponseV2 getNatRsipStatusResponseV2 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + int uPnPVersion = GetDeviceUPnPVersion(); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}"); + string serviceAction = $"\"{service.ServiceType}#GetNatRsipStatus\""; + bool natEnabled; - natEnabled = getNatRsipStatusResponseV2.NatEnabled; + switch (uPnPVersion) + { + case 2: + GetNatRsipStatusResponseV2 getNatRsipStatusResponseV2 = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); - break; - case 1: - GetNatRsipStatusResponseV1 getNatRsipStatusResponseV1 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + natEnabled = getNatRsipStatusResponseV2.NatEnabled; - natEnabled = getNatRsipStatusResponseV1.NatEnabled; - break; - default: - throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); - } + break; + case 1: + GetNatRsipStatusResponseV1 getNatRsipStatusResponseV1 = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + + natEnabled = getNatRsipStatusResponseV1.NatEnabled; + break; + default: + throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); + } - Logger.Log($"Received NAT status {natEnabled} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"Received NAT status {natEnabled} on UPnP device {UPnPDescription.Device.FriendlyName}."); - return natEnabled; + return natEnabled; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Logger.Log($"GetNatRsipStatus error/not supported on UPnP device {UPnPDescription.Device.FriendlyName}."); + ProgramConstants.LogException(ex); + } + + return null; } - public async ValueTask<(bool FirewallEnabled, bool InboundPinholeAllowed)> GetIpV6FirewallStatusAsync(CancellationToken cancellationToken) + public async ValueTask<(bool? FirewallEnabled, bool? InboundPinholeAllowed)> GetIpV6FirewallStatusAsync(CancellationToken cancellationToken) { Logger.Log($"Checking IPV6 firewall status on UPnP device {UPnPDescription.Device.FriendlyName}."); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1"); - string serviceAction = $"\"{service.ServiceType}#GetFirewallStatus\""; - GetFirewallStatusResponse response = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + try + { + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1"); + string serviceAction = $"\"{service.ServiceType}#GetFirewallStatus\""; + GetFirewallStatusResponse response = await ExecuteSoapAction( + serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + + Logger.Log($"Received IPV6 firewall status {response.FirewallEnabled} and port mapping allowed {response.InboundPinholeAllowed} on UPnP device {UPnPDescription.Device.FriendlyName}."); - Logger.Log($"Received IPV6 firewall status {response.FirewallEnabled} and port mapping allowed {response.InboundPinholeAllowed} on UPnP device {UPnPDescription.Device.FriendlyName}."); + return (response.FirewallEnabled, response.InboundPinholeAllowed); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Logger.Log($"GetFirewallStatus error/not supported on UPnP device {UPnPDescription.Device.FriendlyName}."); + ProgramConstants.LogException(ex); + } - return (response.FirewallEnabled, response.InboundPinholeAllowed); + return (null, null); } public async ValueTask OpenIpV6PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index aec701e18..26d327aa4 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -65,225 +65,231 @@ private static IReadOnlyDictionary SsdpMultiCastAddresse { Logger.Log("Starting P2P Setup."); - if (internetGatewayDevice is null) - { - var internetGatewayDevices = (await GetInternetGatewayDevicesAsync(cancellationToken).ConfigureAwait(false)).ToList(); + internetGatewayDevice ??= await GetInternetGatewayDeviceAsync(cancellationToken).ConfigureAwait(false); - internetGatewayDevice = GetInternetGatewayDevice(internetGatewayDevices, 2); - internetGatewayDevice ??= GetInternetGatewayDevice(internetGatewayDevices, 1); - } + (IPAddress publicIpV4Address, List<(ushort InternalPort, ushort ExternalPort)> ipV4P2PPorts) = + await SetupIpV4PortsAsync(internetGatewayDevice, p2pReservedPorts, stunServerIpAddresses, cancellationToken).ConfigureAwait(false); + (IPAddress publicIpV6Address, List<(ushort InternalPort, ushort ExternalPort)> ipV6P2PPorts, List ipV6P2PPortIds) = + await SetupIpV6PortsAsync(internetGatewayDevice, p2pReservedPorts, stunServerIpAddresses, cancellationToken).ConfigureAwait(false); - IPAddress detectedPublicIpV4Address = null; - bool routerNatEnabled = false; + return (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, ipV6P2PPortIds, publicIpV6Address, publicIpV4Address); + } - if (internetGatewayDevice is not null) - { - Logger.Log("Found NAT device."); + private static async Task GetInternetGatewayDeviceAsync(CancellationToken cancellationToken) + { + var internetGatewayDevices = (await GetInternetGatewayDevicesAsync(cancellationToken).ConfigureAwait(false)).ToList(); + InternetGatewayDevice internetGatewayDevice = GetInternetGatewayDevice(internetGatewayDevices, 2); - routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(cancellationToken).ConfigureAwait(false); - detectedPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken).ConfigureAwait(false); - } + return internetGatewayDevice ?? GetInternetGatewayDevice(internetGatewayDevices, 1); + } - var ipV4StunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); + private static async ValueTask<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports, List PortIds)> SetupIpV6PortsAsync( + InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, List stunServerIpAddresses, CancellationToken cancellationToken) + { + (IPAddress stunPublicIpV6Address, List<(ushort InternalPort, ushort ExternalPort)> ipV6StunPortMapping) = await PerformStunAsync( + stunServerIpAddresses, null, p2pReservedPorts, AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); + IPAddress localPublicIpV6Address; - if (stunServerIpAddresses.Any(q => q.AddressFamily is AddressFamily.InterNetwork)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - IPAddress stunServerIpAddress = stunServerIpAddresses.Single(q => q.AddressFamily is AddressFamily.InterNetwork); - - if (detectedPublicIpV4Address == null) - { - Logger.Log("Using IPV4 STUN."); - - foreach (ushort p2pReservedPort in p2pReservedPorts) - { - IPEndPoint publicIpV4Endpoint = await NetworkHelper.PerformStunAsync( - stunServerIpAddress, p2pReservedPort, cancellationToken).ConfigureAwait(false); - - if (publicIpV4Endpoint is null) - { - Logger.Log("IPV4 STUN failed."); - break; - } - - detectedPublicIpV4Address ??= publicIpV4Endpoint.Address; + var localIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses() + .Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); - if (p2pReservedPort != publicIpV4Endpoint.Port) - ipV4StunPortMapping.Add(new(p2pReservedPort, (ushort)publicIpV4Endpoint.Port)); - } - } + (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundLocalPublicIpV6Address = localIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); - if (ipV4StunPortMapping.Any()) + if (foundLocalPublicIpV6Address.IpAddress is null) { -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - NetworkHelper.KeepStunAliveAsync( - stunServerIpAddress, - ipV4StunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + foundLocalPublicIpV6Address = localIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); } + + localPublicIpV6Address = foundLocalPublicIpV6Address.IpAddress; } else { - Logger.Log($"STUN server {stunServerIpAddresses.First()} has no IPV4 address."); - } - - if (detectedPublicIpV4Address == null) - { - Logger.Log("Using IPV4 trace detection."); - - detectedPublicIpV4Address = await NetworkHelper.TracePublicIpV4Address(cancellationToken).ConfigureAwait(false); + localPublicIpV6Address = NetworkHelper.GetPublicIpAddresses() + .FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); } - var publicIpAddresses = NetworkHelper.GetPublicIpAddresses().ToList(); - IPAddress publicIpV4Address = publicIpAddresses.FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetwork); - bool natDetected = routerNatEnabled || (publicIpV4Address is not null && detectedPublicIpV4Address is not null && !publicIpV4Address.Equals(detectedPublicIpV4Address)); - - publicIpV4Address ??= detectedPublicIpV4Address; - - if (publicIpV4Address is not null) - Logger.Log("Public IPV4 detected."); - - var privateIpV4Addresses = NetworkHelper.GetPrivateIpAddresses().Where(q => q.AddressFamily is AddressFamily.InterNetwork).ToList(); - IPAddress privateIpV4Address = privateIpV4Addresses.FirstOrDefault(); - var ipV4P2PPorts = new List<(ushort InternalPort, ushort ExternalPort)>(); + var ipV6P2PPorts = new List<(ushort InternalPort, ushort ExternalPort)>(); + var ipV6P2PPortIds = new List(); + IPAddress publicIpV6Address = null; - if (natDetected && routerNatEnabled && privateIpV4Address is not null && publicIpV4Address is not null) + if (stunPublicIpV6Address is not null || localPublicIpV6Address is not null) { - Logger.Log("Using IPV4 port mapping."); + Logger.Log("Public IPV6 detected."); - try + if (internetGatewayDevice is not null) { - foreach (int p2PReservedPort in p2pReservedPorts) + try { - ushort openedPort = await internetGatewayDevice.OpenIpV4PortAsync( - privateIpV4Address, (ushort)p2PReservedPort, cancellationToken).ConfigureAwait(false); + (bool? firewallEnabled, bool? inboundPinholeAllowed) = await internetGatewayDevice.GetIpV6FirewallStatusAsync( + cancellationToken).ConfigureAwait(false); - ipV4P2PPorts.Add((openedPort, openedPort)); + if (firewallEnabled is not false && inboundPinholeAllowed is not false) + { + Logger.Log("Configuring IPV6 firewall."); + + foreach (ushort p2pReservedPort in p2pReservedPorts) + { + ipV6P2PPortIds.Add(await internetGatewayDevice.OpenIpV6PortAsync( + localPublicIpV6Address, p2pReservedPort, cancellationToken).ConfigureAwait(false)); + } + } } + catch (Exception ex) + { +#if DEBUG + ProgramConstants.LogException(ex, $"Could not open P2P IPV6 router ports for {localPublicIpV6Address}."); +#else + ProgramConstants.LogException(ex, $"Could not open P2P IPV6 router ports."); +#endif + } + } - p2pReservedPorts = ipV4P2PPorts.Select(q => q.InternalPort).ToList(); + if (stunPublicIpV6Address is not null && localPublicIpV6Address is not null && !stunPublicIpV6Address.Equals(localPublicIpV6Address)) + { + publicIpV6Address = stunPublicIpV6Address; + ipV6P2PPorts = ipV6StunPortMapping; } - catch (Exception ex) + else { - ProgramConstants.LogException(ex, $"Could not open P2P IPV4 router ports for {privateIpV4Address} -> {publicIpV4Address}."); + ipV6P2PPorts = p2pReservedPorts.Select(q => (q, q)).ToList(); } } - else if (ipV4StunPortMapping.Any()) - { - ipV4P2PPorts = ipV4StunPortMapping; - } - else - { - ipV4P2PPorts = p2pReservedPorts.Select(q => (q, q)).ToList(); - } - IPAddress detectedPublicIpV6Address = null; - var ipV6StunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); - - if (stunServerIpAddresses.Any(q => q.AddressFamily is AddressFamily.InterNetworkV6)) - { - Logger.Log("Using IPV6 STUN."); - - IPAddress stunServerIpAddress = stunServerIpAddresses.Single(q => q.AddressFamily is AddressFamily.InterNetworkV6); + return (publicIpV6Address, ipV6P2PPorts, ipV6P2PPortIds); + } - foreach (ushort p2pReservedPort in p2pReservedPorts) - { - IPEndPoint publicIpV6Endpoint = await NetworkHelper.PerformStunAsync( - stunServerIpAddress, p2pReservedPort, cancellationToken).ConfigureAwait(false); + private static async ValueTask<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports)> SetupIpV4PortsAsync( + InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, List stunServerIpAddresses, CancellationToken cancellationToken) + { + bool? routerNatEnabled = null; + IPAddress routerPublicIpV4Address = null; - if (publicIpV6Endpoint is null) - { - Logger.Log("IPV6 STUN failed."); - break; - } + if (internetGatewayDevice is not null) + { + Logger.Log($"Found NAT device {internetGatewayDevice.UPnPDescription.Device.FriendlyName}."); - detectedPublicIpV6Address ??= publicIpV6Endpoint.Address; + routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(cancellationToken).ConfigureAwait(false); + routerPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken).ConfigureAwait(false); + } - if (p2pReservedPort != publicIpV6Endpoint.Port) - ipV6StunPortMapping.Add(new(p2pReservedPort, (ushort)publicIpV6Endpoint.Port)); - } + (IPAddress stunPublicIpV4Address, List<(ushort InternalPort, ushort ExternalPort)> ipV4StunPortMapping) = await PerformStunAsync( + stunServerIpAddresses, routerPublicIpV4Address, p2pReservedPorts, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); + IPAddress tracePublicIpV4Address = null; - if (ipV6StunPortMapping.Any()) - { -#pragma warning disable CS4014 - NetworkHelper.KeepStunAliveAsync( - stunServerIpAddress, - ipV6StunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); -#pragma warning restore CS4014 - } - } - else + if (routerPublicIpV4Address is null && stunPublicIpV4Address is null) { - Logger.Log($"STUN server {stunServerIpAddresses.First()} has no IPV6 address."); + Logger.Log("Using IPV4 trace detection."); + + tracePublicIpV4Address = await NetworkHelper.TracePublicIpV4Address(cancellationToken).ConfigureAwait(false); } - IPAddress publicIpV6Address; + IPAddress localPublicIpV4Address = null; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (routerPublicIpV4Address is null && stunPublicIpV4Address is null && tracePublicIpV4Address is null) { - var publicIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses() - .Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); + Logger.Log("Using IPV4 local public address."); - (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundPublicIpV6Address = publicIpV6Addresses - .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); - - if (foundPublicIpV6Address.IpAddress is null) - { - foundPublicIpV6Address = publicIpV6Addresses - .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); - } + var localPublicIpAddresses = NetworkHelper.GetPublicIpAddresses().ToList(); - publicIpV6Address = foundPublicIpV6Address.IpAddress; - } - else - { - publicIpV6Address = NetworkHelper.GetPublicIpAddresses() - .FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); + localPublicIpV4Address = localPublicIpAddresses.FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetwork); } - var ipV6P2PPorts = new List<(ushort InternalPort, ushort ExternalPort)>(); - var p2pIpV6PortIds = new List(); + IPAddress publicIpV4Address = stunPublicIpV4Address ?? routerPublicIpV4Address ?? tracePublicIpV4Address ?? localPublicIpV4Address; + var ipV4P2PPorts = new List<(ushort InternalPort, ushort ExternalPort)>(); - if (detectedPublicIpV6Address is not null || publicIpV6Address is not null) + if (publicIpV4Address is not null) { - Logger.Log("Public IPV6 detected."); + Logger.Log("Public IPV4 detected."); - if (internetGatewayDevice is not null) + var privateIpV4Addresses = NetworkHelper.GetPrivateIpAddresses().Where(q => q.AddressFamily is AddressFamily.InterNetwork).ToList(); + IPAddress privateIpV4Address = privateIpV4Addresses.FirstOrDefault(); + + if (internetGatewayDevice is not null && privateIpV4Address is not null && routerNatEnabled is not false) { + Logger.Log("Using IPV4 port mapping."); + try { - (bool firewallEnabled, bool inboundPinholeAllowed) = await internetGatewayDevice.GetIpV6FirewallStatusAsync( - cancellationToken).ConfigureAwait(false); - - if (firewallEnabled && inboundPinholeAllowed) + foreach (int p2PReservedPort in p2pReservedPorts) { - Logger.Log("Configuring IPV6 firewall."); + ushort openedPort = await internetGatewayDevice.OpenIpV4PortAsync( + privateIpV4Address, (ushort)p2PReservedPort, cancellationToken).ConfigureAwait(false); - foreach (ushort p2pReservedPort in p2pReservedPorts) - { - p2pIpV6PortIds.Add(await internetGatewayDevice.OpenIpV6PortAsync( - publicIpV6Address, p2pReservedPort, cancellationToken).ConfigureAwait(false)); - } + ipV4P2PPorts.Add((openedPort, openedPort)); } + + p2pReservedPorts = ipV4P2PPorts.Select(q => q.InternalPort).ToList(); } catch (Exception ex) { - ProgramConstants.LogException(ex, $"Could not open P2P IPV6 router ports for {publicIpV6Address}."); +#if DEBUG + ProgramConstants.LogException(ex, $"Could not open P2P IPV4 router ports for {privateIpV4Address} -> {publicIpV4Address}."); +#else + ProgramConstants.LogException(ex, $"Could not open P2P IPV4 router ports."); +#endif + ipV4P2PPorts = ipV4StunPortMapping.Any() ? ipV4StunPortMapping : p2pReservedPorts.Select(q => (q, q)).ToList(); } } + else + { + ipV4P2PPorts = ipV4StunPortMapping.Any() ? ipV4StunPortMapping : p2pReservedPorts.Select(q => (q, q)).ToList(); + } + } - if (detectedPublicIpV6Address is not null && publicIpV6Address is not null && !detectedPublicIpV6Address.Equals(publicIpV6Address)) + return (publicIpV4Address, ipV4P2PPorts); + } + + private static async ValueTask<(IPAddress IPAddress, List<(ushort InternalPort, ushort ExternalPort)> PortMapping)> PerformStunAsync( + List stunServerIpAddresses, IPAddress routerPublicIpV4Address, List p2pReservedPorts, AddressFamily addressFamily, CancellationToken cancellationToken) + { + var stunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); + IPAddress stunPublicAddress = null; + + if (stunServerIpAddresses.Any(q => q.AddressFamily == addressFamily)) + { + IPAddress stunServerIpAddress = stunServerIpAddresses.Single(q => q.AddressFamily == addressFamily); + + if (addressFamily is AddressFamily.InterNetwork && routerPublicIpV4Address == null) { - publicIpV6Address = detectedPublicIpV6Address; + Logger.Log($"Using STUN to detect {addressFamily} address."); - ipV6P2PPorts = ipV6StunPortMapping; + foreach (ushort p2pReservedPort in p2pReservedPorts) + { + IPEndPoint stunPublicIpEndPoint = await NetworkHelper.PerformStunAsync( + stunServerIpAddress, p2pReservedPort, cancellationToken).ConfigureAwait(false); + + if (stunPublicIpEndPoint is null) + { + Logger.Log($"{addressFamily} STUN failed."); + break; + } + + stunPublicAddress = stunPublicIpEndPoint.Address; + + if (p2pReservedPort != stunPublicIpEndPoint.Port) + stunPortMapping.Add(new(p2pReservedPort, (ushort)stunPublicIpEndPoint.Port)); + } } - else + + if (stunPortMapping.Any()) { - ipV6P2PPorts = p2pReservedPorts.Select(q => (q, q)).ToList(); +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + NetworkHelper.KeepStunAliveAsync( + stunServerIpAddress, + stunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } } + else + { + Logger.Log($"STUN server {stunServerIpAddresses.First()} has no {addressFamily} address."); + } - return (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address); + return (stunPublicAddress, stunPortMapping); } private static async ValueTask> GetInternetGatewayDevicesAsync(CancellationToken cancellationToken) @@ -401,7 +407,7 @@ private static async ValueTask ReceiveAsync(Socket socket, ICollection r responses.Add(Encoding.UTF8.GetString(buffer.Span[..bytesReceived])); } - catch (OperationCanceledException) + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { } } @@ -439,7 +445,7 @@ private static async Task GetInternetGatewayDeviceAsync( { uPnPDescription = await GetUPnPDescription(location, cancellationToken).ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { if (location.HostNameType is UriHostNameType.IPv6 && locations.Any(q => q.HostNameType is UriHostNameType.IPv4)) { @@ -449,7 +455,7 @@ private static async Task GetInternetGatewayDeviceAsync( uPnPDescription = await GetUPnPDescription(location, cancellationToken).ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index 87bcab1a2..0f63424b2 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -78,15 +78,21 @@ public async ValueTask HandlePlayerP2PRequestAsync() { if (!ipV6P2PPorts.Any() && !ipV4P2PPorts.Any()) { - var p2pPorts = NetworkHelper.GetFreeUdpPorts(Array.Empty(), MAX_REMOTE_PLAYERS).ToList(); - StunCancellationTokenSource?.Cancel(); StunCancellationTokenSource?.Dispose(); StunCancellationTokenSource = new(); - (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync( - internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? InitialTunnel.IPAddresses, StunCancellationTokenSource.Token).ConfigureAwait(false); + var p2pPorts = NetworkHelper.GetFreeUdpPorts(Array.Empty(), MAX_REMOTE_PLAYERS).ToList(); + + try + { + (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync( + internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? InitialTunnel.IPAddresses, StunCancellationTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } } return publicIpV4Address is not null || publicIpV6Address is not null; @@ -164,13 +170,13 @@ public async ValueTask PingRemotePlayer(string playerName, string p2pReque if (parsedIpV4Address is not null) { remotePlayerP2PEnabled = true; - remotePlayerIpV4Ports = ipV4splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); + remotePlayerIpV4Ports = ipV4splitLines[1].Split('-', StringSplitOptions.RemoveEmptyEntries).Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); } if (parsedIpV6Address is not null) { remotePlayerP2PEnabled = true; - remotePlayerIpV6Ports = ipV6splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); + remotePlayerIpV6Ports = ipV6splitLines[1].Split('-', StringSplitOptions.RemoveEmptyEntries).Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); } if (P2PPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 4c323590b..e7e5f6877 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -61,31 +61,38 @@ public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unic public static async ValueTask TracePublicIpV4Address(CancellationToken cancellationToken) { - IPAddress[] ipAddresses = await Dns.GetHostAddressesAsync(PingHost, cancellationToken).ConfigureAwait(false); - using var ping = new Ping(); - - foreach (IPAddress ipAddress in ipAddresses.Where(q => q.AddressFamily is AddressFamily.InterNetwork)) + try { - PingReply pingReply = await ping.SendPingAsync(ipAddress, PingTimeout).ConfigureAwait(false); + IPAddress[] ipAddresses = await Dns.GetHostAddressesAsync(PingHost, cancellationToken).ConfigureAwait(false); + using var ping = new Ping(); - if (pingReply.Status is not IPStatus.Success) - continue; + foreach (IPAddress ipAddress in ipAddresses.Where(q => q.AddressFamily is AddressFamily.InterNetwork)) + { + PingReply pingReply = await ping.SendPingAsync(ipAddress, PingTimeout).ConfigureAwait(false); - IPAddress pingIpAddress = null; - int ttl = 1; + if (pingReply.Status is not IPStatus.Success) + continue; - while (!ipAddress.Equals(pingIpAddress)) - { - pingReply = await ping.SendPingAsync(ipAddress, PingTimeout, Array.Empty(), new(ttl++, false)).ConfigureAwait(false); - pingIpAddress = pingReply.Address; + IPAddress pingIpAddress = null; + int ttl = 1; - if (ipAddress.Equals(pingIpAddress)) - break; + while (!ipAddress.Equals(pingIpAddress)) + { + pingReply = await ping.SendPingAsync(ipAddress, PingTimeout, Array.Empty(), new(ttl++, false)).ConfigureAwait(false); + pingIpAddress = pingReply.Address; + + if (ipAddress.Equals(pingIpAddress)) + break; - if (!IsPrivateIpAddress(pingReply.Address)) - return pingReply.Address; + if (!IsPrivateIpAddress(pingReply.Address)) + return pingReply.Address; + } } } + catch (Exception ex) when (ex is not OperationCanceledException) + { + ProgramConstants.LogException(ex, "IP trace detection failed."); + } return null; } @@ -109,7 +116,7 @@ public static async ValueTask TracePublicIpV4Address(CancellationToke } catch (PingException ex) { - ProgramConstants.LogException(ex); + ProgramConstants.LogException(ex, "Ping failed."); } return null; @@ -164,7 +171,7 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI return new IPEndPoint(publicIpAddress, publicPort); } - catch (Exception ex) + catch (Exception ex) when (ex is not OperationCanceledException && !cancellationToken.IsCancellationRequested) { ProgramConstants.LogException(ex, $"STUN server {stunServerIpEndPoint} failed."); } @@ -175,9 +182,9 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List localPorts, CancellationToken cancellationToken) { - while (!cancellationToken.IsCancellationRequested) + try { - try + while (!cancellationToken.IsCancellationRequested) { foreach (ushort localPort in localPorts) { @@ -187,9 +194,13 @@ public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List< await Task.Delay(5000, cancellationToken).ConfigureAwait(false); } - catch (TaskCanceledException) - { - } + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, "STUN keep alive failed."); } } From 2000bf56d0a74f186abf4997bc3793c0a2824799 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 21 Dec 2022 23:38:33 +0100 Subject: [PATCH 070/109] P2P logging --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 2 + .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 47 ++++++++++--------- .../Domain/Multiplayer/NetworkHelper.cs | 4 +- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 2fadc4648..891f61e20 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -178,6 +178,7 @@ public CnCNetGameLobby( "Toggle P2P connections on/off, your IP will be public to players in the lobby".L10N("Client:Main:ChangeP2P"), false, _ => ToggleP2PAsync().HandleTask())); +#if DEBUG AddChatBoxCommand(new( CnCNetLobbyCommands.RECORD, "Toggle recording game replay".L10N("Client:Main:ChangeRecord"), @@ -188,6 +189,7 @@ public CnCNetGameLobby( "Start a game replay.\nExample: \"/replay REPLAYID".L10N("Client:Main:StartReplay"), true, StartReplay)); +#endif } public event EventHandler GameLeft; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 26d327aa4..32805baa6 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -80,14 +80,19 @@ private static async Task GetInternetGatewayDeviceAsync(C var internetGatewayDevices = (await GetInternetGatewayDevicesAsync(cancellationToken).ConfigureAwait(false)).ToList(); InternetGatewayDevice internetGatewayDevice = GetInternetGatewayDevice(internetGatewayDevices, 2); - return internetGatewayDevice ?? GetInternetGatewayDevice(internetGatewayDevices, 1); + internetGatewayDevice ??= GetInternetGatewayDevice(internetGatewayDevices, 1); + + if (internetGatewayDevice is not null) + Logger.Log($"Found NAT device {internetGatewayDevice.UPnPDescription.Device.DeviceType} - {internetGatewayDevice.Server} ({internetGatewayDevice.UPnPDescription.Device.FriendlyName})."); + + return internetGatewayDevice; } private static async ValueTask<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports, List PortIds)> SetupIpV6PortsAsync( InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, List stunServerIpAddresses, CancellationToken cancellationToken) { (IPAddress stunPublicIpV6Address, List<(ushort InternalPort, ushort ExternalPort)> ipV6StunPortMapping) = await PerformStunAsync( - stunServerIpAddresses, null, p2pReservedPorts, AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); + stunServerIpAddresses, p2pReservedPorts, AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); IPAddress localPublicIpV6Address; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -170,14 +175,12 @@ private static async Task GetInternetGatewayDeviceAsync(C if (internetGatewayDevice is not null) { - Logger.Log($"Found NAT device {internetGatewayDevice.UPnPDescription.Device.FriendlyName}."); - routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(cancellationToken).ConfigureAwait(false); routerPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken).ConfigureAwait(false); } (IPAddress stunPublicIpV4Address, List<(ushort InternalPort, ushort ExternalPort)> ipV4StunPortMapping) = await PerformStunAsync( - stunServerIpAddresses, routerPublicIpV4Address, p2pReservedPorts, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); + stunServerIpAddresses, p2pReservedPorts, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); IPAddress tracePublicIpV4Address = null; if (routerPublicIpV4Address is null && stunPublicIpV4Address is null) @@ -244,8 +247,10 @@ private static async Task GetInternetGatewayDeviceAsync(C } private static async ValueTask<(IPAddress IPAddress, List<(ushort InternalPort, ushort ExternalPort)> PortMapping)> PerformStunAsync( - List stunServerIpAddresses, IPAddress routerPublicIpV4Address, List p2pReservedPorts, AddressFamily addressFamily, CancellationToken cancellationToken) + List stunServerIpAddresses, List p2pReservedPorts, AddressFamily addressFamily, CancellationToken cancellationToken) { + Logger.Log($"Using STUN to detect {addressFamily} address."); + var stunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); IPAddress stunPublicAddress = null; @@ -253,30 +258,28 @@ private static async Task GetInternetGatewayDeviceAsync(C { IPAddress stunServerIpAddress = stunServerIpAddresses.Single(q => q.AddressFamily == addressFamily); - if (addressFamily is AddressFamily.InterNetwork && routerPublicIpV4Address == null) + foreach (ushort p2pReservedPort in p2pReservedPorts) { - Logger.Log($"Using STUN to detect {addressFamily} address."); - - foreach (ushort p2pReservedPort in p2pReservedPorts) - { - IPEndPoint stunPublicIpEndPoint = await NetworkHelper.PerformStunAsync( - stunServerIpAddress, p2pReservedPort, cancellationToken).ConfigureAwait(false); + IPEndPoint stunPublicIpEndPoint = await NetworkHelper.PerformStunAsync( + stunServerIpAddress, p2pReservedPort, cancellationToken).ConfigureAwait(false); - if (stunPublicIpEndPoint is null) - { - Logger.Log($"{addressFamily} STUN failed."); - break; - } + if (stunPublicIpEndPoint is null) + break; - stunPublicAddress = stunPublicIpEndPoint.Address; + stunPublicAddress = stunPublicIpEndPoint.Address; - if (p2pReservedPort != stunPublicIpEndPoint.Port) - stunPortMapping.Add(new(p2pReservedPort, (ushort)stunPublicIpEndPoint.Port)); - } + if (p2pReservedPort != stunPublicIpEndPoint.Port) + stunPortMapping.Add(new(p2pReservedPort, (ushort)stunPublicIpEndPoint.Port)); } + if (stunPublicAddress is not null) + Logger.Log($"{addressFamily} STUN detection succeeded."); + else + Logger.Log($"{addressFamily} STUN detection failed."); + if (stunPortMapping.Any()) { + Logger.Log($"{addressFamily} STUN detection detected mapped ports, running STUN keep alive."); #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed NetworkHelper.KeepStunAliveAsync( stunServerIpAddress, diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index e7e5f6877..be495240e 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using ClientCore; +using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer; @@ -171,7 +172,7 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI return new IPEndPoint(publicIpAddress, publicPort); } - catch (Exception ex) when (ex is not OperationCanceledException && !cancellationToken.IsCancellationRequested) + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) { ProgramConstants.LogException(ex, $"STUN server {stunServerIpEndPoint} failed."); } @@ -197,6 +198,7 @@ public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List< } catch (OperationCanceledException) { + Logger.Log($"{stunServerIpAddress.AddressFamily} STUN keep alive stopped."); } catch (Exception ex) { From 8fe71cb89344a7cea267b5321a0a633945169c8b Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 28 Dec 2022 01:46:55 +0100 Subject: [PATCH 071/109] V3 performance & P2P STUN improvements --- .../CnCNet/DataReceivedEventArgs.cs | 4 +- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 40 +++++++++++-------- .../Multiplayer/CnCNet/V3ConnectionState.cs | 2 +- .../CnCNet/V3LocalPlayerConnection.cs | 13 +++--- .../CnCNet/V3RemotePlayerConnection.cs | 19 +++------ .../Domain/Multiplayer/NetworkHelper.cs | 9 +++-- 6 files changed, 43 insertions(+), 44 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs b/DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs index 7a3aac8c3..5d49afaf3 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/DataReceivedEventArgs.cs @@ -4,7 +4,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet; internal sealed class DataReceivedEventArgs : EventArgs { - public DataReceivedEventArgs(uint playerId, ReadOnlyMemory gameData) + public DataReceivedEventArgs(uint playerId, Memory gameData) { PlayerId = playerId; GameData = gameData; @@ -14,5 +14,5 @@ public DataReceivedEventArgs(uint playerId, ReadOnlyMemory gameData) public uint PlayerId { get; } - public ReadOnlyMemory GameData { get; } + public Memory GameData { get; } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 32805baa6..ff91c58cf 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -252,11 +252,21 @@ private static async Task GetInternetGatewayDeviceAsync(C Logger.Log($"Using STUN to detect {addressFamily} address."); var stunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); + List matchingStunServerIpAddresses = stunServerIpAddresses.Where(q => q.AddressFamily == addressFamily).ToList(); + + if (!matchingStunServerIpAddresses.Any()) + { + Logger.Log($"No {addressFamily} STUN servers found."); + + return (null, stunPortMapping); + } + IPAddress stunPublicAddress = null; + IPAddress stunServerIpAddress = null; - if (stunServerIpAddresses.Any(q => q.AddressFamily == addressFamily)) + foreach (IPAddress matchingStunServerIpAddress in matchingStunServerIpAddresses.TakeWhile(_ => stunPublicAddress is null)) { - IPAddress stunServerIpAddress = stunServerIpAddresses.Single(q => q.AddressFamily == addressFamily); + stunServerIpAddress = matchingStunServerIpAddress; foreach (ushort p2pReservedPort in p2pReservedPorts) { @@ -271,25 +281,21 @@ private static async Task GetInternetGatewayDeviceAsync(C if (p2pReservedPort != stunPublicIpEndPoint.Port) stunPortMapping.Add(new(p2pReservedPort, (ushort)stunPublicIpEndPoint.Port)); } + } - if (stunPublicAddress is not null) - Logger.Log($"{addressFamily} STUN detection succeeded."); - else - Logger.Log($"{addressFamily} STUN detection failed."); + if (stunPublicAddress is not null) + Logger.Log($"{addressFamily} STUN detection succeeded."); + else + Logger.Log($"{addressFamily} STUN detection failed."); - if (stunPortMapping.Any()) - { - Logger.Log($"{addressFamily} STUN detection detected mapped ports, running STUN keep alive."); + if (stunPortMapping.Any()) + { + Logger.Log($"{addressFamily} STUN detection detected mapped ports, running STUN keep alive."); #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - NetworkHelper.KeepStunAliveAsync( - stunServerIpAddress, - stunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); + NetworkHelper.KeepStunAliveAsync( + stunServerIpAddress, + stunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - } - } - else - { - Logger.Log($"STUN server {stunServerIpAddresses.First()} has no {addressFamily} address."); } return (stunPublicAddress, stunPortMapping); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index 0f63424b2..b09f15900 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -88,7 +88,7 @@ public async ValueTask HandlePlayerP2PRequestAsync() try { (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync( - internetGatewayDevice, p2pPorts, tunnelHandler.CurrentTunnel?.IPAddresses ?? InitialTunnel.IPAddresses, StunCancellationTokenSource.Token).ConfigureAwait(false); + internetGatewayDevice, p2pPorts, GetEligibleTunnels().SelectMany(q => q.IPAddresses).ToList(), StunCancellationTokenSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs index f9e105074..dbde359c0 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs @@ -21,7 +21,7 @@ internal sealed class V3LocalPlayerConnection : IDisposable private const int GameStartReceiveTimeout = 60000; private const int ReceiveTimeout = 10000; private const int PlayerIdSize = sizeof(uint); - private const int MinimumPacketSize = PlayerIdSize * 2; + private const int PlayerIdsSize = PlayerIdSize * 2; private const int MaximumPacketSize = 1024; private Socket localGameSocket; @@ -69,7 +69,6 @@ public async ValueTask StartConnectionAsync() remotePlayerEndPoint = new IPEndPoint(IPAddress.Loopback, 0); using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(MaximumPacketSize); - Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; int receiveTimeout = GameStartReceiveTimeout; #if DEBUG @@ -82,15 +81,15 @@ public async ValueTask StartConnectionAsync() { using var timeoutCancellationTokenSource = new CancellationTokenSource(receiveTimeout); using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); - Memory data; + Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; try { SocketReceiveFromResult socketReceiveFromResult = await localGameSocket.ReceiveFromAsync( - buffer, SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); + buffer[PlayerIdsSize..], SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); remotePlayerEndPoint = socketReceiveFromResult.RemoteEndPoint; - data = buffer[..socketReceiveFromResult.ReceivedBytes]; + buffer = buffer[..(PlayerIdsSize + socketReceiveFromResult.ReceivedBytes)]; #if DEBUG Logger.Log($"Received data from local game {socketReceiveFromResult.RemoteEndPoint} on {localGameSocket.LocalEndPoint} for player {PlayerId}."); @@ -129,7 +128,7 @@ public async ValueTask StartConnectionAsync() receiveTimeout = ReceiveTimeout; - OnRaiseDataReceivedEvent(new(PlayerId, data)); + OnRaiseDataReceivedEvent(new(PlayerId, buffer)); } } @@ -139,7 +138,7 @@ public async ValueTask StartConnectionAsync() /// The data to send to the game. public async ValueTask SendDataAsync(ReadOnlyMemory data) { - if (remotePlayerEndPoint is null || data.Length < MinimumPacketSize) + if (remotePlayerEndPoint is null || data.Length < PlayerIdsSize) return; using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index 7501c782d..9cde81bbf 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -18,7 +18,7 @@ internal sealed class V3RemotePlayerConnection : IDisposable private const int GameStartReceiveTimeout = 1200000; private const int ReceiveTimeout = 1200000; private const int PlayerIdSize = sizeof(uint); - private const int MinimumPacketSize = PlayerIdSize * 2; + private const int PlayerIdsSize = PlayerIdSize * 2; private const int MaximumPacketSize = 1024; private CancellationToken cancellationToken; @@ -125,21 +125,14 @@ public async ValueTask StartConnectionAsync() /// /// The data to send to the game. /// The id of the player that receives the data. - public async ValueTask SendDataAsync(ReadOnlyMemory data, uint receiverId) + public async ValueTask SendDataAsync(Memory data, uint receiverId) { - const int idsSize = sizeof(uint) * 2; - int bufferSize = data.Length + idsSize; - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); - Memory packet = memoryOwner.Memory[..bufferSize]; - - if (!BitConverter.TryWriteBytes(packet.Span[..PlayerIdSize], GameLocalPlayerId)) + if (!BitConverter.TryWriteBytes(data.Span[..PlayerIdSize], GameLocalPlayerId)) throw new GameDataException(); - if (!BitConverter.TryWriteBytes(packet.Span[PlayerIdSize..(PlayerIdSize * 2)], receiverId)) + if (!BitConverter.TryWriteBytes(data.Span[PlayerIdSize..(PlayerIdSize * 2)], receiverId)) throw new GameDataException(); - data.CopyTo(packet[8..]); - using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); @@ -149,7 +142,7 @@ public async ValueTask SendDataAsync(ReadOnlyMemory data, uint receiverId) Logger.Log($"Sending data {GameLocalPlayerId} -> {receiverId} from {tunnelSocket.LocalEndPoint} to {remoteEndPoint}."); #endif - await tunnelSocket.SendToAsync(packet, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); + await tunnelSocket.SendToAsync(data, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); } catch (SocketException ex) { @@ -243,7 +236,7 @@ private async ValueTask ReceiveLoopAsync() receiveTimeout = ReceiveTimeout; - if (socketReceiveFromResult.ReceivedBytes < MinimumPacketSize) + if (socketReceiveFromResult.ReceivedBytes < PlayerIdsSize) { #if DEBUG Logger.Log($"Invalid data packet from {socketReceiveFromResult.RemoteEndPoint}"); diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index be495240e..381b9fc46 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -132,15 +132,16 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI int[] stunPorts = { stunPort1, stunPort2 }; using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); short stunIdNetworkOrder = IPAddress.HostToNetworkOrder(stunId); - byte[] stunIdNetworkOrderBytes = BitConverter.GetBytes(stunIdNetworkOrder); - IPEndPoint stunServerIpEndPoint = null; using IMemoryOwner receiveMemoryOwner = MemoryPool.Shared.Rent(stunSize); Memory buffer = receiveMemoryOwner.Memory[..stunSize]; + + if (!BitConverter.TryWriteBytes(buffer.Span, stunIdNetworkOrder)) + throw new(); + + IPEndPoint stunServerIpEndPoint = null; int addressBytes = stunServerIpAddress.GetAddressBytes().Length; const int portBytes = sizeof(ushort); - stunIdNetworkOrderBytes.CopyTo(buffer.Span); - socket.Bind(new IPEndPoint(IPAddress.IPv6Any, localPort)); foreach (int stunPort in stunPorts) From 2ae95e29365a02314af3770f2fd3fcd8673f3988 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 30 Dec 2022 14:25:03 +0100 Subject: [PATCH 072/109] V3 refactoring, P2P improvements --- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 6 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 37 +-- .../Multiplayer/GameLobby/LANGameLobby.cs | 26 +- .../GameLobby/MultiplayerGameLobby.cs | 2 +- .../DXGUI/Multiplayer/LANGameLoadingLobby.cs | 2 +- .../Multiplayer/CnCNet/PlayerConnection.cs | 183 ++++++++++++++ .../CnCNet/Replays/ReplayHandler.cs | 18 +- .../CnCNet/UPNP/InternetGatewayDevice.cs | 31 +-- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 1 + .../Multiplayer/CnCNet/V3ConnectionState.cs | 6 +- .../Multiplayer/CnCNet/V3GameTunnelHandler.cs | 4 +- .../CnCNet/V3LocalPlayerConnection.cs | 177 ++----------- .../CnCNet/V3RemotePlayerConnection.cs | 236 ++++-------------- .../Domain/Multiplayer/NetworkHelper.cs | 21 +- 14 files changed, 336 insertions(+), 414 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 6b97789db..e7fc2b3e1 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -555,7 +555,7 @@ private void PostUIInit() gameCreationPanel.Hide(); connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, Renderer.GetSafeString( - string.Format("*** DTA CnCNet Client version {0} ***".L10N("Client:Main:CnCNetClientVersionMessage"), Assembly.GetAssembly(typeof(CnCNetLobby)).GetName().Version), + string.Format("*** CnCNet Client version {0} ***".L10N("Client:Main:CnCNetClientVersionMessage"), Assembly.GetAssembly(typeof(CnCNetLobby)).GetName().Version), lbChatMessages.FontIndex))); connectionManager.BannedFromChannel += (_, e) => ConnectionManager_BannedFromChannelAsync(e).HandleTask(); @@ -613,7 +613,7 @@ private async ValueTask ConnectionManager_BannedFromChannelAsync(ChannelEventArg if (gameOfLastJoinAttempt.IsLoadedGame) await gameLoadingLobby.ClearAsync().ConfigureAwait(false); else - await gameLobby.ClearAsync().ConfigureAwait(false); + await gameLobby.ClearAsync(false).ConfigureAwait(false); } } @@ -958,7 +958,7 @@ private async ValueTask GameChannel_UserAddedAsync(object sender, ChannelUserEve private async ValueTask ClearGameJoinAttemptAsync(Channel channel) { ClearGameChannelEvents(channel); - await gameLobby.ClearAsync().ConfigureAwait(false); + await gameLobby.ClearAsync(false).ConfigureAwait(false); } private void ClearGameChannelEvents(Channel channel) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 891f61e20..117c24c76 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -266,7 +266,7 @@ protected override void Dispose(bool disposing) if (!disposed) { if (disposing) - ClearAsync().HandleTask(); + ClearAsync(true).HandleTask(); disposed = true; } @@ -474,9 +474,9 @@ public void ChangeChatColor(IRCColor chatColor) tbChatInput.TextColor = chatColor.XnaColor; } - public override async ValueTask ClearAsync() + public override async ValueTask ClearAsync(bool exiting) { - await base.ClearAsync().ConfigureAwait(false); + await base.ClearAsync(exiting).ConfigureAwait(false); if (channel != null) { @@ -508,9 +508,13 @@ public override async ValueTask ClearAsync() gameStartCancellationTokenSource?.Cancel(); v3ConnectionState.DisposeAsync().HandleTask(); gamePlayerIds.Clear(); - GameLeft?.Invoke(this, EventArgs.Empty); - TopBar.RemovePrimarySwitchable(this); - ResetDiscordPresence(); + + if (!exiting) + { + GameLeft?.Invoke(this, EventArgs.Empty); + TopBar.RemovePrimarySwitchable(this); + ResetDiscordPresence(); + } } public async ValueTask LeaveGameLobbyAsync() @@ -521,13 +525,13 @@ public async ValueTask LeaveGameLobbyAsync() await BroadcastGameAsync().ConfigureAwait(false); } - await ClearAsync().ConfigureAwait(false); + await ClearAsync(false).ConfigureAwait(false); await channel.LeaveAsync().ConfigureAwait(false); } private async ValueTask HandleConnectionLossAsync() { - await ClearAsync().ConfigureAwait(false); + await ClearAsync(false).ConfigureAwait(false); Disable(); } @@ -604,7 +608,7 @@ private async ValueTask Channel_UserKickedAsync(UserNameEventArgs e) { connectionManager.MainChannel.AddMessage( new(ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("Client:Main:YouWereKicked"))); - await ClearAsync().ConfigureAwait(false); + await ClearAsync(false).ConfigureAwait(false); Visible = false; Enabled = false; @@ -1199,7 +1203,7 @@ private ValueTask BroadcastPlayerTunnelPingsAsync() private async ValueTask BroadcastPlayerP2PRequestAsync() { - bool p2pSetupSucceeded; + bool p2pSetupSucceeded = false; try { @@ -1207,17 +1211,20 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() } catch (Exception ex) { - ProgramConstants.LogException(ex, "Could not open UPnP P2P ports."); + ProgramConstants.LogException(ex, "P2P setup failed."); + } + + if (!p2pSetupSucceeded) + { AddNotice(string.Format( CultureInfo.CurrentCulture, - "Could not open P2P ports. Check that UPnP port mapping is enabled for this device on your router/modem.".L10N("Client:Main:UPnPP2PFailed")), + "P2P setup failed. Check that UPnP port mapping is enabled for this device on your router/modem.".L10N("Client:Main:P2PSetupFailed")), Color.Orange); - await SendPlayerP2PRequestAsync().ConfigureAwait(false); + return; } - if (p2pSetupSucceeded) - await SendPlayerP2PRequestAsync().ConfigureAwait(false); + await SendPlayerP2PRequestAsync().ConfigureAwait(false); } private ValueTask SendPlayerP2PRequestAsync() diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs index 78fb23474..b6c4f3332 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/LANGameLobby.cs @@ -74,7 +74,7 @@ public LANGameLobby( private async ValueTask WindowManager_GameClosingAsync() { if (client is { Connected: true }) - await ClearAsync().ConfigureAwait(false); + await ClearAsync(true).ConfigureAwait(false); cancellationTokenSource?.Cancel(); } @@ -199,16 +199,16 @@ private async ValueTask ListenForClientsAsync(CancellationToken cancellationToke { listener = new Socket(SocketType.Stream, ProtocolType.Tcp); - listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); + listener.Bind(new IPEndPoint(IPAddress.IPv6Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); listener.Listen(); while (!cancellationToken.IsCancellationRequested) { - Socket client; + Socket newClient; try { - client = await listener.AcceptAsync(cancellationToken).ConfigureAwait(false); + newClient = await listener.AcceptAsync(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -220,26 +220,26 @@ private async ValueTask ListenForClientsAsync(CancellationToken cancellationToke break; } - Logger.Log("New client connected from " + ((IPEndPoint)client.RemoteEndPoint).Address); + Logger.Log("New client connected from " + ((IPEndPoint)newClient.RemoteEndPoint).Address); if (Players.Count >= MAX_PLAYER_COUNT) { Logger.Log("Dropping client because of player limit."); - client.Shutdown(SocketShutdown.Both); - client.Close(); + newClient.Shutdown(SocketShutdown.Both); + newClient.Close(); continue; } if (Locked) { Logger.Log("Dropping client because the game room is locked."); - client.Shutdown(SocketShutdown.Both); - client.Close(); + newClient.Shutdown(SocketShutdown.Both); + newClient.Close(); continue; } LANPlayerInfo lpInfo = new LANPlayerInfo(encoding); - lpInfo.SetClient(client); + lpInfo.SetClient(newClient); HandleClientConnectionAsync(lpInfo, cancellationToken).HandleTask(); } @@ -448,7 +448,7 @@ private void HandleMessageFromServer(string message) protected override async ValueTask BtnLeaveGame_LeftClickAsync() { - await ClearAsync().ConfigureAwait(false); + await ClearAsync(false).ConfigureAwait(false); GameLeft?.Invoke(this, EventArgs.Empty); Disable(); } @@ -472,9 +472,9 @@ protected override void UpdateDiscordPresence(bool resetTimer = false) "LAN Game", IsHost, false, Locked, resetTimer); } - public override async ValueTask ClearAsync() + public override async ValueTask ClearAsync(bool exiting) { - await base.ClearAsync().ConfigureAwait(false); + await base.ClearAsync(exiting).ConfigureAwait(false); if (IsHost) { diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index 6aa5def4a..0257c52af 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -944,7 +944,7 @@ protected virtual ValueTask TooManyPlayersNotificationAsync() return ValueTask.CompletedTask; } - public virtual ValueTask ClearAsync() + public virtual ValueTask ClearAsync(bool exiting) { if (!IsHost) AIPlayers.Clear(); diff --git a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs index bc8df802b..0b1a71d4e 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANGameLoadingLobby.cs @@ -156,7 +156,7 @@ public async ValueTask PostJoinAsync() private async ValueTask ListenForClientsAsync(CancellationToken cancellationToken) { listener = new Socket(SocketType.Stream, ProtocolType.Tcp); - listener.Bind(new IPEndPoint(IPAddress.Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); + listener.Bind(new IPEndPoint(IPAddress.IPv6Any, ProgramConstants.LAN_GAME_LOBBY_PORT)); listener.Listen(); while (!cancellationToken.IsCancellationRequested) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs new file mode 100644 index 000000000..819f40900 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs @@ -0,0 +1,183 @@ +using System; +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using ClientCore; +using Rampastring.Tools; + +namespace DTAClient.Domain.Multiplayer.CnCNet; + +internal abstract class PlayerConnection : IDisposable +{ + protected const int PlayerIdSize = sizeof(uint); + protected const int PlayerIdsSize = PlayerIdSize * 2; + protected const int SendTimeout = 10000; + protected const int MaximumPacketSize = 1024; + + protected CancellationToken CancellationToken; + protected Socket Socket; + protected EndPoint RemoteEndPoint; + + public uint PlayerId { get; protected set; } + + protected virtual int GameStartReceiveTimeout => 60000; + + protected virtual int GameInProgressReceiveTimeout => 10000; + + /// + /// Occurs when the connection was lost. + /// + public event EventHandler RaiseConnectionCutEvent; + + /// + /// Occurs when game data was received. + /// + public event EventHandler RaiseDataReceivedEvent; + + public void Dispose() + { +#if DEBUG + Logger.Log($"{GetType().Name}: Connection to {RemoteEndPoint} closed for player {PlayerId}."); +#else + Logger.Log($"{GetType().Name}: Connection closed for player {PlayerId}."); +#endif + Socket?.Close(); + } + + /// + /// Starts listening for game data and forwards it. + /// + public async ValueTask StartConnectionAsync() + { + await DoStartConnectionAsync().ConfigureAwait(false); + await ReceiveLoopAsync().ConfigureAwait(false); + } + + protected virtual ValueTask DoStartConnectionAsync() + => ValueTask.CompletedTask; + + protected abstract ValueTask DoReceiveDataAsync(Memory buffer, CancellationToken cancellation); + + protected abstract DataReceivedEventArgs ProcessReceivedData(Memory buffer, SocketReceiveFromResult socketReceiveFromResult); + + protected async ValueTask SendDataAsync(ReadOnlyMemory data) + { + using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, CancellationToken); + + try + { +#if DEBUG + Logger.Log($"{GetType().Name}: Sending data from {Socket.LocalEndPoint} to {RemoteEndPoint} for player {PlayerId}: {BitConverter.ToString(data.Span.ToArray())}."); +#endif + await Socket.SendToAsync(data, SocketFlags.None, RemoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); + } + catch (SocketException ex) + { +#if DEBUG + ProgramConstants.LogException(ex, $"Socket exception sending data to {RemoteEndPoint} for player {PlayerId}."); +#else + ProgramConstants.LogException(ex, $"Socket exception sending data for player {PlayerId}."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); + } + catch (ObjectDisposedException) + { + } + catch (OperationCanceledException) when (CancellationToken.IsCancellationRequested) + { + } + catch (OperationCanceledException) + { +#if DEBUG + Logger.Log($"{GetType().Name}: Connection from {Socket.LocalEndPoint} to {RemoteEndPoint} timed out for player {PlayerId} when sending data."); +#else + Logger.Log($"{GetType().Name}: Connection timed out for player {PlayerId} when sending data."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); + } + } + + private async ValueTask ReceiveLoopAsync() + { + using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(MaximumPacketSize); + int receiveTimeout = GameStartReceiveTimeout; + +#if DEBUG + Logger.Log($"{GetType().Name}: Start listening for {RemoteEndPoint} on {Socket.LocalEndPoint} for player {PlayerId}."); +#else + Logger.Log($"{GetType().Name}: Start listening for player {PlayerId}."); +#endif + + while (!CancellationToken.IsCancellationRequested) + { + Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; + SocketReceiveFromResult socketReceiveFromResult; + using var timeoutCancellationTokenSource = new CancellationTokenSource(receiveTimeout); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, CancellationToken); + + try + { + socketReceiveFromResult = await DoReceiveDataAsync(buffer, linkedCancellationTokenSource.Token).ConfigureAwait(false); + RemoteEndPoint = socketReceiveFromResult.RemoteEndPoint; + } + catch (SocketException ex) + { +#if DEBUG + ProgramConstants.LogException(ex, $"Socket exception in {RemoteEndPoint} receive loop for player {PlayerId}."); +#else + ProgramConstants.LogException(ex, $"Socket exception in receive loop for player {PlayerId}."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); + + return; + } + catch (ObjectDisposedException) + { + return; + } + catch (OperationCanceledException) when (CancellationToken.IsCancellationRequested) + { + return; + } + catch (OperationCanceledException) + { +#if DEBUG + Logger.Log($"{GetType().Name}: Connection from {Socket.LocalEndPoint} to {RemoteEndPoint} timed out for player {PlayerId} when receiving data."); +#else + Logger.Log($"{GetType().Name}: Connection timed out for player {PlayerId} when receiving data."); +#endif + OnRaiseConnectionCutEvent(EventArgs.Empty); + + return; + } + + receiveTimeout = GameInProgressReceiveTimeout; + +#if DEBUG + Logger.Log($"{GetType().Name}: Received data from {RemoteEndPoint} on {Socket.LocalEndPoint} for player {PlayerId}: {BitConverter.ToString(buffer.Span.ToArray())}."); +#endif + + DataReceivedEventArgs dataReceivedEventArgs = ProcessReceivedData(buffer, socketReceiveFromResult); + + if (dataReceivedEventArgs is not null) + OnRaiseDataReceivedEvent(dataReceivedEventArgs); + } + } + + private void OnRaiseConnectionCutEvent(EventArgs e) + { + EventHandler raiseEvent = RaiseConnectionCutEvent; + + raiseEvent?.Invoke(this, e); + } + + private void OnRaiseDataReceivedEvent(DataReceivedEventArgs e) + { + EventHandler raiseEvent = RaiseDataReceivedEvent; + + raiseEvent?.Invoke(this, e); + } +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs index 4c72ed46c..ec16a6d85 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs @@ -44,23 +44,27 @@ public async ValueTask StopRecordingAsync(List gamePlayerIds, List q.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)).Index]; var playerMappings = new Dictionary { { playerId, playerName } }; - for (int i = 1; i < spawnIni.GetSection("Settings").GetIntValue("PlayerCount", 0); i++) + for (int i = 1; i < settingsSection.GetIntValue("PlayerCount", 0); i++) { - string section = $"Other{i}"; + IniSection otherPlayerSection = spawnIni.GetSection($"Other{i}"); - if (spawnIni.SectionExists(section)) + if (otherPlayerSection is not null) { - playerName = spawnIni.GetSection(section).GetStringValue("Name", null); + playerName = otherPlayerSection.GetStringValue("Name", null); playerId = gamePlayerIds[playerInfos.Single(q => q.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)).Index]; playerMappings.Add(playerId, playerName); @@ -93,7 +97,7 @@ public async ValueTask StopRecordingAsync(List gamePlayerIds, List SaveReplayDataAsync(((V3RemotePlayerConnection)sender).GameLocalPlayerId, e).HandleTask(); + => SaveReplayDataAsync(((V3RemotePlayerConnection)sender).PlayerId, e).HandleTask(); public void LocalGameConnection_DataReceivedAsync(object sender, DataReceivedEventArgs e) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index bde86d41d..7f0f18b6a 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -96,7 +96,7 @@ public async ValueTask CloseIpV4PortAsync(ushort port, CancellationToken cancell Logger.Log($"Deleting IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); int uPnPVersion = GetDeviceUPnPVersion(); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}"); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}", AddressFamily.InterNetwork); string serviceAction = $"\"{service.ServiceType}#DeletePortMapping\""; switch (uPnPVersion) @@ -129,7 +129,7 @@ public async ValueTask GetExternalIpV4AddressAsync(CancellationToken try { int uPnPVersion = GetDeviceUPnPVersion(); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}"); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}", AddressFamily.InterNetwork); string serviceAction = $"\"{service.ServiceType}#GetExternalIPAddress\""; IPAddress ipAddress; @@ -172,7 +172,7 @@ public async ValueTask GetExternalIpV4AddressAsync(CancellationToken try { int uPnPVersion = GetDeviceUPnPVersion(); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}"); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}", AddressFamily.InterNetwork); string serviceAction = $"\"{service.ServiceType}#GetNatRsipStatus\""; bool natEnabled; @@ -214,7 +214,7 @@ public async ValueTask GetExternalIpV4AddressAsync(CancellationToken try { - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1"); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1", AddressFamily.InterNetworkV6); string serviceAction = $"\"{service.ServiceType}#GetFirewallStatus\""; GetFirewallStatusResponse response = await ExecuteSoapAction( serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); @@ -236,7 +236,7 @@ public async ValueTask OpenIpV6PortAsync(IPAddress ipAddress, ushort por { Logger.Log($"Opening IPV6 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1"); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1", AddressFamily.InterNetworkV6); string serviceAction = $"\"{service.ServiceType}#AddPinhole\""; var request = new AddPinholeRequest(string.Empty, port, ipAddress.ToString(), port, IanaUdpProtocolNumber, IpLeaseTimeInSeconds); AddPinholeResponse response = await ExecuteSoapAction( @@ -249,15 +249,15 @@ public async ValueTask OpenIpV6PortAsync(IPAddress ipAddress, ushort por public async ValueTask CloseIpV6PortAsync(ushort uniqueId, CancellationToken cancellationToken = default) { - Logger.Log($"Opening IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"Deleting IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1"); + (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1", AddressFamily.InterNetworkV6); string serviceAction = $"\"{service.ServiceType}#DeletePinhole\""; var request = new DeletePinholeRequest(uniqueId); await ExecuteSoapAction( serviceUri, serviceAction, serviceType, request, cancellationToken).ConfigureAwait(false); - Logger.Log($"Opened IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"Deleted IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); } private static async ValueTask ExecuteSoapAction( @@ -331,13 +331,16 @@ private static async ValueTask ExecuteSoapAction } } - private (ServiceListItem WanIpConnectionService, string ServiceUri, string ServiceType) GetSoapActionParameters(string wanConnectionDeviceService, AddressFamily? addressFamily = null) + private (ServiceListItem WanIpConnectionService, string ServiceUri, string ServiceType) GetSoapActionParameters(string wanConnectionDeviceService, AddressFamily addressFamily) { - Uri location = PreferredLocation; - - if (addressFamily is AddressFamily.InterNetwork && Locations.Any(q => q.HostNameType is UriHostNameType.IPv4)) - location = Locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv4); - + Uri location = addressFamily switch + { + AddressFamily.InterNetwork when Locations.Any(q => q.HostNameType is UriHostNameType.IPv4) => + Locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv4), + AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNameType.IPv6) => + Locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6), + _ => PreferredLocation + }; int uPnPVersion = GetDeviceUPnPVersion(); Device wanDevice = UPnPDescription.Device.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPWanDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); Device wanConnectionDevice = wanDevice.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPWanConnectionDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index ff91c58cf..4ec8275cd 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -160,6 +160,7 @@ private static async Task GetInternetGatewayDeviceAsync(C } else { + publicIpV6Address = stunPublicIpV6Address ?? localPublicIpV6Address; ipV6P2PPorts = p2pReservedPorts.Select(q => (q, q)).ToList(); } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index b09f15900..480fb1be0 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -88,7 +88,7 @@ public async ValueTask HandlePlayerP2PRequestAsync() try { (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, p2pIpV6PortIds, publicIpV6Address, publicIpV4Address) = await UPnPHandler.SetupPortsAsync( - internetGatewayDevice, p2pPorts, GetEligibleTunnels().SelectMany(q => q.IPAddresses).ToList(), StunCancellationTokenSource.Token).ConfigureAwait(false); + internetGatewayDevice, p2pPorts, GetEligibleTunnels().OrderBy(q => q.PingInMs).SelectMany(q => q.IPAddresses).ToList(), StunCancellationTokenSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -170,13 +170,13 @@ public async ValueTask PingRemotePlayer(string playerName, string p2pReque if (parsedIpV4Address is not null) { remotePlayerP2PEnabled = true; - remotePlayerIpV4Ports = ipV4splitLines[1].Split('-', StringSplitOptions.RemoveEmptyEntries).Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); + remotePlayerIpV4Ports = ipV4splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); } if (parsedIpV6Address is not null) { remotePlayerP2PEnabled = true; - remotePlayerIpV6Ports = ipV6splitLines[1].Split('-', StringSplitOptions.RemoveEmptyEntries).Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); + remotePlayerIpV6Ports = ipV6splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); } if (P2PPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs index 0de460fb9..1dec7043e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs @@ -130,7 +130,7 @@ private async ValueTask LocalGameConnection_DataReceivedAsync(object sender, Dat OnRaiseLocalGameDataReceivedEvent(sender, e); if (remoteHostConnection is not null) - await remoteHostConnection.SendDataAsync(e.GameData, e.PlayerId).ConfigureAwait(false); + await remoteHostConnection.SendDataToRemotePlayerAsync(e.GameData, e.PlayerId).ConfigureAwait(false); } /// @@ -143,7 +143,7 @@ private async ValueTask RemoteHostConnection_DataReceivedAsync(object sender, Da V3LocalPlayerConnection v3LocalPlayerConnection = GetLocalPlayerConnection(e.PlayerId); if (v3LocalPlayerConnection is not null) - await v3LocalPlayerConnection.SendDataAsync(e.GameData).ConfigureAwait(false); + await v3LocalPlayerConnection.SendDataToGameAsync(e.GameData).ConfigureAwait(false); } private V3LocalPlayerConnection GetLocalPlayerConnection(uint senderId) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs index dbde359c0..811301837 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs @@ -1,34 +1,19 @@ using System; -using System.Buffers; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using ClientCore; -using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet; /// /// Manages a player connection between the local game and this application. /// -internal sealed class V3LocalPlayerConnection : IDisposable +internal sealed class V3LocalPlayerConnection : PlayerConnection { private const uint IOC_IN = 0x80000000; private const uint IOC_VENDOR = 0x18000000; private const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; - private const int SendTimeout = 10000; - private const int GameStartReceiveTimeout = 60000; - private const int ReceiveTimeout = 10000; - private const int PlayerIdSize = sizeof(uint); - private const int PlayerIdsSize = PlayerIdSize * 2; - private const int MaximumPacketSize = 1024; - - private Socket localGameSocket; - private EndPoint remotePlayerEndPoint; - private CancellationToken cancellationToken; - - public uint PlayerId { get; private set; } /// /// Creates a local game socket and returns the port. @@ -38,167 +23,35 @@ internal sealed class V3LocalPlayerConnection : IDisposable /// The port of the created socket. public ushort Setup(uint playerId, CancellationToken cancellationToken) { - this.cancellationToken = cancellationToken; + CancellationToken = cancellationToken; PlayerId = playerId; - localGameSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); + Socket = new(SocketType.Dgram, ProtocolType.Udp); + RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 0); // Disable ICMP port not reachable exceptions, happens when the game is still loading and has not yet opened the socket. if (OperatingSystem.IsWindows()) - localGameSocket.IOControl(unchecked((int)SIO_UDP_CONNRESET), new byte[] { 0 }, null); - - localGameSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - - return (ushort)((IPEndPoint)localGameSocket.LocalEndPoint).Port; - } - - /// - /// Occurs when the connection to the local game was lost. - /// - public event EventHandler RaiseConnectionCutEvent; - - /// - /// Occurs when game data from the local game was received. - /// - public event EventHandler RaiseDataReceivedEvent; - - /// - /// Starts listening for local game player data and forwards it to the tunnel. - /// - public async ValueTask StartConnectionAsync() - { - remotePlayerEndPoint = new IPEndPoint(IPAddress.Loopback, 0); - - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(MaximumPacketSize); - int receiveTimeout = GameStartReceiveTimeout; - -#if DEBUG - Logger.Log($"Start listening for local game {remotePlayerEndPoint} on {localGameSocket.LocalEndPoint} for player {PlayerId}."); -#else - Logger.Log($"Start listening for local game for player {PlayerId}."); -#endif + Socket.IOControl(unchecked((int)SIO_UDP_CONNRESET), new byte[] { 0 }, null); - while (!cancellationToken.IsCancellationRequested) - { - using var timeoutCancellationTokenSource = new CancellationTokenSource(receiveTimeout); - using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); - Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; + Socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - try - { - SocketReceiveFromResult socketReceiveFromResult = await localGameSocket.ReceiveFromAsync( - buffer[PlayerIdsSize..], SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); - - remotePlayerEndPoint = socketReceiveFromResult.RemoteEndPoint; - buffer = buffer[..(PlayerIdsSize + socketReceiveFromResult.ReceivedBytes)]; - -#if DEBUG - Logger.Log($"Received data from local game {socketReceiveFromResult.RemoteEndPoint} on {localGameSocket.LocalEndPoint} for player {PlayerId}."); -#endif - } - catch (SocketException ex) - { -#if DEBUG - ProgramConstants.LogException(ex, $"Socket exception in {remotePlayerEndPoint} receive loop for player {PlayerId}."); -#else - ProgramConstants.LogException(ex, $"Socket exception in receive loop for player {PlayerId}."); -#endif - OnRaiseConnectionCutEvent(EventArgs.Empty); - - return; - } - catch (ObjectDisposedException) - { - return; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - return; - } - catch (OperationCanceledException) - { -#if DEBUG - Logger.Log($"Local game connection {localGameSocket.LocalEndPoint} timed out for player {PlayerId} when receiving data."); -#else - Logger.Log($"Local game connection timed out for player {PlayerId} when receiving data."); -#endif - OnRaiseConnectionCutEvent(EventArgs.Empty); - - return; - } - - receiveTimeout = ReceiveTimeout; - - OnRaiseDataReceivedEvent(new(PlayerId, buffer)); - } + return (ushort)((IPEndPoint)Socket.LocalEndPoint).Port; } /// - /// Sends tunnel data to the local game. + /// Sends remote player data to the local game. /// /// The data to send to the game. - public async ValueTask SendDataAsync(ReadOnlyMemory data) + public async ValueTask SendDataToGameAsync(ReadOnlyMemory data) { - if (remotePlayerEndPoint is null || data.Length < PlayerIdsSize) + if (RemoteEndPoint is null || data.Length < PlayerIdsSize) return; - using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); - using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); - - try - { -#if DEBUG - Logger.Log($"Sending data from {localGameSocket.LocalEndPoint} to local game {remotePlayerEndPoint} for player {PlayerId}."); - -#endif - await localGameSocket.SendToAsync(data, SocketFlags.None, remotePlayerEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); - } - catch (SocketException ex) - { -#if DEBUG - ProgramConstants.LogException(ex, $"Socket exception sending data to {remotePlayerEndPoint} for player {PlayerId}."); -#else - ProgramConstants.LogException(ex, $"Socket exception sending data for player {PlayerId}."); -#endif - OnRaiseConnectionCutEvent(EventArgs.Empty); - } - catch (ObjectDisposedException) - { - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - } - catch (OperationCanceledException) - { -#if DEBUG - Logger.Log($"Local game connection {localGameSocket.LocalEndPoint} timed out for player {PlayerId} when sending data."); -#else - Logger.Log($"Local game connection timed out for player {PlayerId} when sending data."); -#endif - OnRaiseConnectionCutEvent(EventArgs.Empty); - } + await SendDataAsync(data).ConfigureAwait(false); } - public void Dispose() - { -#if DEBUG - Logger.Log($"Connection to local game {remotePlayerEndPoint} closed for player {PlayerId}."); -#else - Logger.Log($"Connection to local game closed for player {PlayerId}."); -#endif - localGameSocket.Close(); - } - - private void OnRaiseConnectionCutEvent(EventArgs e) - { - EventHandler raiseEvent = RaiseConnectionCutEvent; - - raiseEvent?.Invoke(this, e); - } - - private void OnRaiseDataReceivedEvent(DataReceivedEventArgs e) - { - EventHandler raiseEvent = RaiseDataReceivedEvent; + protected override ValueTask DoReceiveDataAsync(Memory buffer, CancellationToken cancellation) + => Socket.ReceiveFromAsync(buffer[PlayerIdsSize..], SocketFlags.None, RemoteEndPoint, cancellation); - raiseEvent?.Invoke(this, e); - } + protected override DataReceivedEventArgs ProcessReceivedData(Memory buffer, SocketReceiveFromResult socketReceiveFromResult) + => new(PlayerId, buffer[..(PlayerIdsSize + socketReceiveFromResult.ReceivedBytes)]); } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index 9cde81bbf..8c190911f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -12,27 +12,19 @@ namespace DTAClient.Domain.Multiplayer.CnCNet; /// /// Manages a player connection between a remote host and this application. /// -internal sealed class V3RemotePlayerConnection : IDisposable +internal sealed class V3RemotePlayerConnection : PlayerConnection { - private const int SendTimeout = 10000; - private const int GameStartReceiveTimeout = 1200000; - private const int ReceiveTimeout = 1200000; - private const int PlayerIdSize = sizeof(uint); - private const int PlayerIdsSize = PlayerIdSize * 2; - private const int MaximumPacketSize = 1024; - - private CancellationToken cancellationToken; - private Socket tunnelSocket; - private IPEndPoint remoteEndPoint; private ushort localPort; - public uint GameLocalPlayerId { get; private set; } + protected override int GameStartReceiveTimeout => 1200000; + + protected override int GameInProgressReceiveTimeout => 1200000; public void SetUp(IPEndPoint remoteEndPoint, ushort localPort, uint gameLocalPlayerId, CancellationToken cancellationToken) { - this.cancellationToken = cancellationToken; - GameLocalPlayerId = gameLocalPlayerId; - this.remoteEndPoint = remoteEndPoint; + CancellationToken = cancellationToken; + PlayerId = gameLocalPlayerId; + RemoteEndPoint = remoteEndPoint; this.localPort = localPort; } @@ -47,47 +39,50 @@ public void SetUp(IPEndPoint remoteEndPoint, ushort localPort, uint gameLocalPla public event EventHandler RaiseConnectionFailedEvent; /// - /// Occurs when the connection to the remote host was lost. + /// Sends local game player data to the remote host. /// - public event EventHandler RaiseConnectionCutEvent; + /// The data to send to the game. + /// The id of the player that receives the data. + public ValueTask SendDataToRemotePlayerAsync(Memory data, uint receiverId) + { + if (!BitConverter.TryWriteBytes(data.Span[..PlayerIdSize], PlayerId)) + throw new GameDataException(); - /// - /// Occurs when game data from the remote host was received. - /// - public event EventHandler RaiseDataReceivedEvent; + if (!BitConverter.TryWriteBytes(data.Span[PlayerIdSize..(PlayerIdSize * 2)], receiverId)) + throw new GameDataException(); - /// - /// Starts listening for remote player data and forwards it to the local game. - /// - public async ValueTask StartConnectionAsync() + return SendDataAsync(data); + } + + protected override async ValueTask DoStartConnectionAsync() { #if DEBUG - Logger.Log($"Attempting to establish a connection from port {localPort} to {remoteEndPoint})."); + Logger.Log($"{GetType().Name}: Attempting to establish a connection from port {localPort} to {RemoteEndPoint})."); #else - Logger.Log($"Attempting to establish a connection using {localPort})."); + Logger.Log($"{GetType().Name}: Attempting to establish a connection using {localPort})."); #endif - tunnelSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); + Socket = new(SocketType.Dgram, ProtocolType.Udp); - tunnelSocket.Bind(new IPEndPoint(IPAddress.IPv6Any, localPort)); + Socket.Bind(new IPEndPoint(IPAddress.IPv6Any, localPort)); using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(MaximumPacketSize); Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; - if (!BitConverter.TryWriteBytes(buffer.Span[..PlayerIdSize], GameLocalPlayerId)) + if (!BitConverter.TryWriteBytes(buffer.Span[..PlayerIdSize], PlayerId)) throw new GameDataException(); using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); - using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, CancellationToken); try { - await tunnelSocket.SendToAsync(buffer, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); + await Socket.SendToAsync(buffer, SocketFlags.None, RemoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); } catch (SocketException ex) { #if DEBUG - ProgramConstants.LogException(ex, $"Failed to establish connection from port {localPort} to {remoteEndPoint}."); + ProgramConstants.LogException(ex, $"Failed to establish connection from port {localPort} to {RemoteEndPoint}."); #else ProgramConstants.LogException(ex, $"Failed to establish connection using {localPort}."); #endif @@ -95,16 +90,16 @@ public async ValueTask StartConnectionAsync() return; } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + catch (OperationCanceledException) when (CancellationToken.IsCancellationRequested) { return; } catch (OperationCanceledException) { #if DEBUG - Logger.Log($"Failed to establish connection (time out) from port {localPort} to {remoteEndPoint}."); + Logger.Log($"{GetType().Name}: Failed to establish connection (time out) from port {localPort} to {RemoteEndPoint}."); #else - Logger.Log($"Failed to establish connection (time out) using {localPort}."); + Logger.Log($"{GetType().Name}: Failed to establish connection (time out) using {localPort}."); #endif OnRaiseConnectionFailedEvent(EventArgs.Empty); @@ -112,161 +107,48 @@ public async ValueTask StartConnectionAsync() } #if DEBUG - Logger.Log($"Connection from {tunnelSocket.LocalEndPoint} to {remoteEndPoint} established."); + Logger.Log($"{GetType().Name}: Connection from {Socket.LocalEndPoint} to {RemoteEndPoint} established."); #else - Logger.Log($"Connection using {localPort} established."); + Logger.Log($"{GetType().Name}: Connection using {localPort} established."); #endif OnRaiseConnectedEvent(EventArgs.Empty); - await ReceiveLoopAsync().ConfigureAwait(false); - } - - /// - /// Sends local game player data to the remote host. - /// - /// The data to send to the game. - /// The id of the player that receives the data. - public async ValueTask SendDataAsync(Memory data, uint receiverId) - { - if (!BitConverter.TryWriteBytes(data.Span[..PlayerIdSize], GameLocalPlayerId)) - throw new GameDataException(); - - if (!BitConverter.TryWriteBytes(data.Span[PlayerIdSize..(PlayerIdSize * 2)], receiverId)) - throw new GameDataException(); - - using var timeoutCancellationTokenSource = new CancellationTokenSource(SendTimeout); - using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); - - try - { -#if DEBUG - Logger.Log($"Sending data {GameLocalPlayerId} -> {receiverId} from {tunnelSocket.LocalEndPoint} to {remoteEndPoint}."); - -#endif - await tunnelSocket.SendToAsync(data, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); - } - catch (SocketException ex) - { -#if DEBUG - ProgramConstants.LogException(ex, $"Socket exception sending data to {remoteEndPoint}."); -#else - ProgramConstants.LogException(ex, $"Socket exception sending data from port {localPort}."); -#endif - OnRaiseConnectionCutEvent(EventArgs.Empty); - } - catch (ObjectDisposedException) - { - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - } - catch (OperationCanceledException) - { -#if DEBUG - Logger.Log($"Remote host connection {remoteEndPoint} timed out when sending data."); -#else - Logger.Log($"Remote host connection from port {localPort} timed out when sending data."); -#endif - OnRaiseConnectionCutEvent(EventArgs.Empty); - } } - public void Dispose() - { -#if DEBUG - Logger.Log($"Connection to remote host {remoteEndPoint} closed."); -#else - Logger.Log($"Connection to remote host on port {localPort} closed."); -#endif - tunnelSocket?.Close(); - } + protected override ValueTask DoReceiveDataAsync(Memory buffer, CancellationToken cancellation) + => Socket.ReceiveFromAsync(buffer, SocketFlags.None, RemoteEndPoint, cancellation); - private async ValueTask ReceiveLoopAsync() + protected override DataReceivedEventArgs ProcessReceivedData(Memory buffer, SocketReceiveFromResult socketReceiveFromResult) { - using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(MaximumPacketSize); - int receiveTimeout = GameStartReceiveTimeout; - -#if DEBUG - Logger.Log($"Start listening for {remoteEndPoint} on {tunnelSocket.LocalEndPoint}."); -#else - Logger.Log($"Start listening on {localPort}."); -#endif - - while (!cancellationToken.IsCancellationRequested) + if (socketReceiveFromResult.ReceivedBytes < PlayerIdsSize) { - Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; - SocketReceiveFromResult socketReceiveFromResult; - using var timeoutCancellationTokenSource = new CancellationTokenSource(receiveTimeout); - using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); - - try - { - socketReceiveFromResult = await tunnelSocket.ReceiveFromAsync( - buffer, SocketFlags.None, remoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); - } - catch (SocketException ex) - { -#if DEBUG - ProgramConstants.LogException(ex, $"Socket exception in {remoteEndPoint} receive loop."); -#else - ProgramConstants.LogException(ex, $"Socket exception on port {localPort} receive loop."); -#endif - OnRaiseConnectionCutEvent(EventArgs.Empty); - - return; - } - catch (ObjectDisposedException) - { - return; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - return; - } - catch (OperationCanceledException) - { #if DEBUG - Logger.Log($"Remote host connection {remoteEndPoint} timed out when receiving data."); + Logger.Log($"{GetType().Name}: Invalid data packet from {socketReceiveFromResult.RemoteEndPoint}"); #else - Logger.Log($"Remote host connection on port {localPort} timed out when receiving data."); + Logger.Log($"{GetType().Name}: Invalid data packet on {localPort}"); #endif - OnRaiseConnectionCutEvent(EventArgs.Empty); - - return; - } - - receiveTimeout = ReceiveTimeout; - - if (socketReceiveFromResult.ReceivedBytes < PlayerIdsSize) - { -#if DEBUG - Logger.Log($"Invalid data packet from {socketReceiveFromResult.RemoteEndPoint}"); -#else - Logger.Log($"Invalid data packet on {localPort}"); -#endif - continue; - } + return null; + } - Memory data = buffer[(PlayerIdSize * 2)..socketReceiveFromResult.ReceivedBytes]; - uint senderId = BitConverter.ToUInt32(buffer[..PlayerIdSize].Span); - uint receiverId = BitConverter.ToUInt32(buffer[PlayerIdSize..(PlayerIdSize * 2)].Span); + Memory data = buffer[(PlayerIdSize * 2)..socketReceiveFromResult.ReceivedBytes]; + uint senderId = BitConverter.ToUInt32(buffer[..PlayerIdSize].Span); + uint receiverId = BitConverter.ToUInt32(buffer[PlayerIdSize..(PlayerIdSize * 2)].Span); #if DEBUG - Logger.Log($"Received {senderId} -> {receiverId} from {socketReceiveFromResult.RemoteEndPoint} on {tunnelSocket.LocalEndPoint}."); + Logger.Log($"{GetType().Name}: Received {senderId} -> {receiverId} from {socketReceiveFromResult.RemoteEndPoint} on {Socket.LocalEndPoint}."); #endif - if (receiverId != GameLocalPlayerId) - { + if (receiverId != PlayerId) + { #if DEBUG - Logger.Log($"Invalid target (received: {receiverId}, expected: {GameLocalPlayerId}) from {socketReceiveFromResult.RemoteEndPoint}."); + Logger.Log($"{GetType().Name}: Invalid target (received: {receiverId}, expected: {PlayerId}) from {socketReceiveFromResult.RemoteEndPoint}."); #else - Logger.Log($"Invalid target (received: {receiverId}, expected: {GameLocalPlayerId}) on port {localPort}."); + Logger.Log($"{GetType().Name}: Invalid target (received: {receiverId}, expected: {GameLocalPlayerId}) on port {localPort}."); #endif - continue; - } - - OnRaiseDataReceivedEvent(new(senderId, data)); + return null; } + + return new(senderId, data); } private void OnRaiseConnectedEvent(EventArgs e) @@ -282,18 +164,4 @@ private void OnRaiseConnectionFailedEvent(EventArgs e) raiseEvent?.Invoke(this, e); } - - private void OnRaiseConnectionCutEvent(EventArgs e) - { - EventHandler raiseEvent = RaiseConnectionCutEvent; - - raiseEvent?.Invoke(this, e); - } - - private void OnRaiseDataReceivedEvent(DataReceivedEventArgs e) - { - EventHandler raiseEvent = RaiseDataReceivedEvent; - - raiseEvent?.Invoke(this, e); - } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 381b9fc46..0d06997bc 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -17,7 +17,6 @@ internal static class NetworkHelper { private const string PingHost = "cncnet.org"; private const int PingTimeout = 1000; - private const int MinimumUdpPort = 1024; private static readonly IReadOnlyCollection SupportedAddressFamilies = new[] { @@ -57,7 +56,7 @@ public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unic uint ipMaskV4 = BitConverter.ToUInt32(unicastIpAddressInformation.IPv4Mask.GetAddressBytes(), 0); uint broadCastIpAddress = ipAddress | ~ipMaskV4; - return new IPAddress(BitConverter.GetBytes(broadCastIpAddress)); + return new(BitConverter.GetBytes(broadCastIpAddress)); } public static async ValueTask TracePublicIpV4Address(CancellationToken cancellationToken) @@ -151,7 +150,7 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI using var timeoutCancellationTokenSource = new CancellationTokenSource(PingTimeout); using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); - stunServerIpEndPoint = new IPEndPoint(stunServerIpAddress, stunPort); + stunServerIpEndPoint = new(stunServerIpAddress, stunPort); await socket.SendToAsync(buffer, stunServerIpEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); @@ -171,7 +170,7 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI short publicPortHostOrder = IPAddress.NetworkToHostOrder(publicPortNetworkOrder); ushort publicPort = (ushort)publicPortHostOrder; - return new IPEndPoint(publicIpAddress, publicPort); + return new(publicIpAddress, publicPort); } catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) { @@ -208,7 +207,7 @@ public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List< } /// - /// Returns a free UDP port number above 1023. + /// Returns the specified amount of free UDP port numbers. /// /// List of UDP port numbers which are additionally excluded. /// The number of free ports to return. @@ -216,16 +215,20 @@ public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List< public static IEnumerable GetFreeUdpPorts(IEnumerable excludedPorts, ushort numberOfPorts) { IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); - var activePorts = endPoints.Select(q => (ushort)q.Port).ToArray().Concat(excludedPorts).ToList(); + var activeV4AndV6Ports = endPoints.Select(q => (ushort)q.Port).ToArray().Concat(excludedPorts).Distinct().ToList(); ushort foundPortCount = 0; while (foundPortCount != numberOfPorts) { - ushort foundPort = (ushort)new Random().Next(MinimumUdpPort, IPEndPoint.MaxPort); + using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); - if (!activePorts.Contains(foundPort)) + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + + ushort foundPort = (ushort)((IPEndPoint)socket.LocalEndPoint).Port; + + if (!activeV4AndV6Ports.Contains(foundPort)) { - activePorts.Add(foundPort); + activeV4AndV6Ports.Add(foundPort); foundPortCount++; From 91ff05bd606e10c54cbc26209c83f1f6128b1788 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 30 Dec 2022 14:41:17 +0100 Subject: [PATCH 073/109] Fix Release build --- .../Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index 8c190911f..2a33b7577 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -142,7 +142,7 @@ protected override DataReceivedEventArgs ProcessReceivedData(Memory buffer #if DEBUG Logger.Log($"{GetType().Name}: Invalid target (received: {receiverId}, expected: {PlayerId}) from {socketReceiveFromResult.RemoteEndPoint}."); #else - Logger.Log($"{GetType().Name}: Invalid target (received: {receiverId}, expected: {GameLocalPlayerId}) on port {localPort}."); + Logger.Log($"{GetType().Name}: Invalid target (received: {receiverId}, expected: {PlayerId}) on port {localPort}."); #endif return null; From f3d0375c6c7303dead53eb06b4bcf3cc711b171c Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 31 Dec 2022 00:25:39 +0100 Subject: [PATCH 074/109] P2P: Handle blocked ping --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 2 +- .../Multiplayer/CnCNet/V3ConnectionState.cs | 111 ++++++++---------- DXMainClient/Domain/Multiplayer/P2PPlayer.cs | 3 +- 3 files changed, 53 insertions(+), 63 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 117c24c76..253f79882 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -2047,7 +2047,7 @@ private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p if (!v3ConnectionState.P2PEnabled) return; - bool remotePlayerP2PEnabled = await v3ConnectionState.PingRemotePlayer(playerName, p2pRequestMessage).ConfigureAwait(false); + bool remotePlayerP2PEnabled = await v3ConnectionState.PingRemotePlayerAsync(playerName, p2pRequestMessage).ConfigureAwait(false); if (remotePlayerP2PEnabled) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index 480fb1be0..2cef5b1c4 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -109,7 +109,7 @@ public string GetP2PRequestCommand() $";{publicIpV6Address}\t{(!ipV6P2PPorts.Any() ? null : ipV6P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}"; public string GetP2PPingCommand(string playerName) - => $" {playerName}-{P2PPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)).LocalPingResults.Select(q => $"{q.RemoteIpAddress};{q.Ping}\t").Aggregate((q, r) => $"{q}{r}")}"; + => $" {playerName}-{P2PPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)).LocalPingResults.Select(q => $"{q.RemoteIpAddress};{q.Ping}\t").DefaultIfEmpty().Aggregate((q, r) => $"{q}{r}")}"; public async ValueTask ToggleP2PAsync() { @@ -139,46 +139,24 @@ public async ValueTask ToggleRecordingAsync() return false; } - public async ValueTask PingRemotePlayer(string playerName, string p2pRequestMessage) + public async ValueTask PingRemotePlayerAsync(string playerName, string p2pRequestMessage) { List<(IPAddress RemoteIpAddress, long Ping)> localPingResults = new(); string[] splitLines = p2pRequestMessage.Split(';'); string[] ipV4splitLines = splitLines[0].Split('\t'); string[] ipV6splitLines = splitLines[1].Split('\t'); + (IPAddress remoteIpAddress, ushort[] remotePlayerIpV4Ports, long? ping) = await PingP2PAddressAsync(ipV4splitLines, playerName).ConfigureAwait(false); - if (IPAddress.TryParse(ipV4splitLines[0], out IPAddress parsedIpV4Address)) - { - long? pingResult = await NetworkHelper.PingAsync(parsedIpV4Address).ConfigureAwait(false); + if (ping is not null) + localPingResults.Add(new(remoteIpAddress, ping.Value)); - if (pingResult is not null) - localPingResults.Add((parsedIpV4Address, pingResult.Value)); - } + (remoteIpAddress, ushort[] remotePlayerIpV6Ports, ping) = await PingP2PAddressAsync(ipV6splitLines, playerName).ConfigureAwait(false); - if (IPAddress.TryParse(ipV6splitLines[0], out IPAddress parsedIpV6Address)) - { - long? pingResult = await NetworkHelper.PingAsync(parsedIpV6Address).ConfigureAwait(false); - - if (pingResult is not null) - localPingResults.Add((parsedIpV6Address, pingResult.Value)); - } + if (ping is not null) + localPingResults.Add(new(remoteIpAddress, ping.Value)); - bool remotePlayerP2PEnabled = false; - ushort[] remotePlayerIpV4Ports = Array.Empty(); - ushort[] remotePlayerIpV6Ports = Array.Empty(); P2PPlayer remoteP2PPlayer; - if (parsedIpV4Address is not null) - { - remotePlayerP2PEnabled = true; - remotePlayerIpV4Ports = ipV4splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); - } - - if (parsedIpV6Address is not null) - { - remotePlayerP2PEnabled = true; - remotePlayerIpV6Ports = ipV6splitLines[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(); - } - if (P2PPlayers.Any(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase))) { remoteP2PPlayer = P2PPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); @@ -187,12 +165,12 @@ public async ValueTask PingRemotePlayer(string playerName, string p2pReque } else { - remoteP2PPlayer = new(playerName, Array.Empty(), Array.Empty(), new(), new(), false); + remoteP2PPlayer = new(playerName, Array.Empty(), Array.Empty(), new(), new()); } - P2PPlayers.Add(remoteP2PPlayer with { LocalPingResults = localPingResults, RemoteIpV6Ports = remotePlayerIpV6Ports, RemoteIpV4Ports = remotePlayerIpV4Ports, Enabled = remotePlayerP2PEnabled }); + P2PPlayers.Add(remoteP2PPlayer with { LocalPingResults = localPingResults, RemoteIpV6Ports = remotePlayerIpV6Ports, RemoteIpV4Ports = remotePlayerIpV4Ports }); - return remotePlayerP2PEnabled; + return localPingResults.Any(); } public bool UpdateRemotePingResults(string senderName, string p2pPingsMessage, string localPlayerName) @@ -227,7 +205,7 @@ public bool UpdateRemotePingResults(string senderName, string p2pPingsMessage, s } else { - p2pPlayer = new(senderName, Array.Empty(), Array.Empty(), new(), new(), false); + p2pPlayer = new(senderName, Array.Empty(), Array.Empty(), new(), new()); } P2PPlayers.Add(p2pPlayer with { RemotePingResults = playerPings }); @@ -268,7 +246,7 @@ public void StartV3ConnectionListeners( if (P2PEnabled) { - foreach (var (remotePlayerName, remoteIpV6Ports, remoteIpV4Ports, localPingResults, remotePingResults, _) in P2PPlayers.Where(q => q.RemotePingResults.Any() && q.Enabled)) + foreach (var (remotePlayerName, remoteIpV6Ports, remoteIpV4Ports, localPingResults, remotePingResults) in P2PPlayers.Where(q => q.RemotePingResults.Any() && q.LocalPingResults.Any())) { (IPAddress selectedRemoteIpAddress, long combinedPing) = localPingResults .Where(q => q.RemoteIpAddress is not null && remotePingResults @@ -327,31 +305,6 @@ public void StartV3ConnectionListeners( } } - private void SetupGameTunnelHandler( - uint gameLocalPlayerId, - Action remoteHostConnectedAction, - Action remoteHostConnectionFailedAction, - List remotePlayerNames, - IPEndPoint remoteIpEndpoint, - ushort localPort, - CancellationToken cancellationToken) - { - var gameTunnelHandler = new V3GameTunnelHandler(); - - gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); - gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); - - if (RecordingEnabled) - { - gameTunnelHandler.RaiseRemoteHostDataReceivedEvent += replayHandler.RemoteHostConnection_DataReceivedAsync; - gameTunnelHandler.RaiseLocalGameDataReceivedEvent += replayHandler.LocalGameConnection_DataReceivedAsync; - } - - gameTunnelHandler.SetUp(remoteIpEndpoint, localPort, gameLocalPlayerId, cancellationToken); - gameTunnelHandler.ConnectToTunnel(); - V3GameTunnelHandlers.Add(new(remotePlayerNames, gameTunnelHandler)); - } - public List StartPlayerConnections(List gamePlayerIds) { this.gamePlayerIds = gamePlayerIds; @@ -439,6 +392,44 @@ public string HandleTunnelPingsMessage(string playerName, string tunnelPingsMess return hash; } + private static async ValueTask<(IPAddress IpAddress, ushort[] Ports, long? Ping)> PingP2PAddressAsync(IReadOnlyList ipAddressInfo, string playerName) + { + if (!IPAddress.TryParse(ipAddressInfo[0], out IPAddress parsedIpAddress)) + return new(null, Array.Empty(), null); + + long? pingResult = await NetworkHelper.PingAsync(parsedIpAddress).ConfigureAwait(false); + + if (pingResult is null) + Logger.Log($"P2P: Could not ping {playerName} using {parsedIpAddress.AddressFamily}."); + + return new(parsedIpAddress, ipAddressInfo[1].Split('-').Select(q => ushort.Parse(q, CultureInfo.InvariantCulture)).ToArray(), pingResult); + } + + private void SetupGameTunnelHandler( + uint gameLocalPlayerId, + Action remoteHostConnectedAction, + Action remoteHostConnectionFailedAction, + List remotePlayerNames, + IPEndPoint remoteIpEndpoint, + ushort localPort, + CancellationToken cancellationToken) + { + var gameTunnelHandler = new V3GameTunnelHandler(); + + gameTunnelHandler.RaiseRemoteHostConnectedEvent += (_, _) => remoteHostConnectedAction(); + gameTunnelHandler.RaiseRemoteHostConnectionFailedEvent += (_, _) => remoteHostConnectionFailedAction(); + + if (RecordingEnabled) + { + gameTunnelHandler.RaiseRemoteHostDataReceivedEvent += replayHandler.RemoteHostConnection_DataReceivedAsync; + gameTunnelHandler.RaiseLocalGameDataReceivedEvent += replayHandler.LocalGameConnection_DataReceivedAsync; + } + + gameTunnelHandler.SetUp(remoteIpEndpoint, localPort, gameLocalPlayerId, cancellationToken); + gameTunnelHandler.ConnectToTunnel(); + V3GameTunnelHandlers.Add(new(remotePlayerNames, gameTunnelHandler)); + } + private async ValueTask CloseP2PPortsAsync() { try diff --git a/DXMainClient/Domain/Multiplayer/P2PPlayer.cs b/DXMainClient/Domain/Multiplayer/P2PPlayer.cs index 78eee8b43..198eb9297 100644 --- a/DXMainClient/Domain/Multiplayer/P2PPlayer.cs +++ b/DXMainClient/Domain/Multiplayer/P2PPlayer.cs @@ -8,5 +8,4 @@ internal readonly record struct P2PPlayer( ushort[] RemoteIpV6Ports, ushort[] RemoteIpV4Ports, List<(IPAddress RemoteIpAddress, long Ping)> LocalPingResults, - List<(IPAddress RemoteIpAddress, long Ping)> RemotePingResults, - bool Enabled); \ No newline at end of file + List<(IPAddress RemoteIpAddress, long Ping)> RemotePingResults); \ No newline at end of file From 2540b755c8cf5d8db79934356ba3bab5b04520e7 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 31 Dec 2022 00:36:28 +0100 Subject: [PATCH 075/109] Fix build --- DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index 2cef5b1c4..1c140edb5 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -9,6 +9,7 @@ using ClientCore; using DTAClient.Domain.Multiplayer.CnCNet.Replays; using DTAClient.Domain.Multiplayer.CnCNet.UPNP; +using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer.CnCNet; From b1d4bc24c86d151660d68cc1fbdc4c6a654cdec4 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 31 Dec 2022 20:52:06 +0100 Subject: [PATCH 076/109] UPnP Refactoring --- .../CnCNet/UPNP/InternetGatewayDevice.cs | 248 ++++++++++-------- .../{ => Actions}/AddAnyPortMappingRequest.cs | 4 +- .../AddAnyPortMappingResponse.cs | 4 +- .../Models/{ => Actions}/AddPinholeRequest.cs | 4 +- .../{ => Actions}/AddPinholeResponse.cs | 4 +- .../{ => Actions}/AddPortMappingRequest.cs | 4 +- .../{ => Actions}/AddPortMappingResponse.cs | 4 +- .../{ => Actions}/DeletePinholeRequest.cs | 4 +- .../{ => Actions}/DeletePinholeResponse.cs | 4 +- .../DeletePortMappingRequestV1.cs | 4 +- .../DeletePortMappingRequestV2.cs | 4 +- .../DeletePortMappingResponseV1.cs | 4 +- .../DeletePortMappingResponseV2.cs | 4 +- .../GetExternalIPAddressRequestV1.cs | 4 +- .../GetExternalIPAddressRequestV2.cs | 4 +- .../GetExternalIPAddressResponseV1.cs | 4 +- .../GetExternalIPAddressResponseV2.cs | 4 +- .../{ => Actions}/GetFirewallStatusRequest.cs | 4 +- .../GetFirewallStatusResponse.cs | 4 +- .../GetNatRsipStatusRequestV1.cs | 4 +- .../GetNatRsipStatusRequestV2.cs | 4 +- .../GetNatRsipStatusResponseV1.cs | 4 +- .../GetNatRsipStatusResponseV2.cs | 4 +- .../CnCNet/UPNP/Models/AddressType.cs | 2 +- .../Multiplayer/CnCNet/UPNP/Models/Device.cs | 4 +- .../CnCNet/UPNP/Models/IconListItem.cs | 4 +- .../Models/InternetGatewayDeviceResponse.cs | 2 +- .../CnCNet/UPNP/Models/ServiceListItem.cs | 4 +- .../CnCNet/UPNP/Models/SpecVersion.cs | 4 +- .../CnCNet/UPNP/Models/SystemVersion.cs | 4 +- .../CnCNet/UPNP/Models/UPnPConstants.cs | 16 ++ .../CnCNet/UPNP/Models/UPnPDescription.cs | 4 +- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 168 +++--------- .../Multiplayer/CnCNet/V3ConnectionState.cs | 43 +-- .../Domain/Multiplayer/NetworkHelper.cs | 207 ++++++++++----- 35 files changed, 404 insertions(+), 394 deletions(-) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/AddAnyPortMappingRequest.cs (87%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/AddAnyPortMappingResponse.cs (62%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/AddPinholeRequest.cs (83%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/AddPinholeResponse.cs (60%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/AddPortMappingRequest.cs (87%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/AddPortMappingResponse.cs (51%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/DeletePinholeRequest.cs (61%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/DeletePinholeResponse.cs (50%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/DeletePortMappingRequestV1.cs (76%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/DeletePortMappingRequestV2.cs (76%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/DeletePortMappingResponseV1.cs (52%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/DeletePortMappingResponseV2.cs (52%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/GetExternalIPAddressRequestV1.cs (53%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/GetExternalIPAddressRequestV2.cs (53%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/GetExternalIPAddressResponseV1.cs (63%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/GetExternalIPAddressResponseV2.cs (63%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/GetFirewallStatusRequest.cs (51%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/GetFirewallStatusResponse.cs (68%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/GetNatRsipStatusRequestV1.cs (51%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/GetNatRsipStatusRequestV2.cs (51%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/GetNatRsipStatusResponseV1.cs (68%) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/{ => Actions}/GetNatRsipStatusResponseV2.cs (68%) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPConstants.cs diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index 7f0f18b6a..a63b0818e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -28,11 +28,7 @@ internal sealed record InternetGatewayDevice( UPnPDescription UPnPDescription, Uri PreferredLocation) { - private const int ReceiveTimeout = 10000; - private const string UPnPWanConnectionDevice = "urn:schemas-upnp-org:device:WANConnectionDevice"; - private const string UPnPWanDevice = "urn:schemas-upnp-org:device:WANDevice"; - private const string UPnPService = "urn:schemas-upnp-org:service"; - private const string UPnPWanIpConnection = "WANIPConnection"; + private const int ReceiveTimeout = 2000; private const uint IpLeaseTimeInSeconds = 4 * 60 * 60; private const ushort IanaUdpProtocolNumber = 17; private const string PortMappingDescription = "CnCNet"; @@ -41,6 +37,31 @@ internal sealed record InternetGatewayDevice( new SocketsHttpHandler { AutomaticDecompression = DecompressionMethods.All, + ConnectCallback = async (context, token) => + { + Socket socket = null; + + try + { + socket = new(SocketType.Stream, ProtocolType.Tcp) + { + NoDelay = true + }; + + if (IPAddress.Parse(context.DnsEndPoint.Host).AddressFamily is AddressFamily.InterNetworkV6) + socket.Bind(new IPEndPoint(NetworkHelper.GetLocalPublicIpV6Address(), 0)); + + await socket.ConnectAsync(context.DnsEndPoint, token).ConfigureAwait(false); + + return new NetworkStream(socket, true); + } + catch + { + socket?.Dispose(); + + throw; + } + }, SslOptions = new() { CertificateChainPolicy = new() @@ -54,214 +75,215 @@ internal sealed record InternetGatewayDevice( DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; - public const string UPnPInternetGatewayDevice = "urn:schemas-upnp-org:device:InternetGatewayDevice"; - - public async ValueTask OpenIpV4PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) + public async Task OpenIpV4PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) { - Logger.Log($"Opening IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Opening IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); int uPnPVersion = GetDeviceUPnPVersion(); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}", AddressFamily.InterNetwork); switch (uPnPVersion) { case 2: - string addAnyPortMappingAction = $"\"{service.ServiceType}#AddAnyPortMapping\""; var addAnyPortMappingRequest = new AddAnyPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, PortMappingDescription, IpLeaseTimeInSeconds); - AddAnyPortMappingResponse addAnyPortMappingResponse = await ExecuteSoapAction( - serviceUri, addAnyPortMappingAction, serviceType, addAnyPortMappingRequest, cancellationToken).ConfigureAwait(false); + AddAnyPortMappingResponse addAnyPortMappingResponse = await DoSoapActionAsync( + addAnyPortMappingRequest, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "AddAnyPortMapping", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); port = addAnyPortMappingResponse.ReservedPort; break; case 1: - string addPortMappingAction = $"\"{service.ServiceType}#AddPortMapping\""; var addPortMappingRequest = new AddPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, PortMappingDescription, IpLeaseTimeInSeconds); - await ExecuteSoapAction( - serviceUri, addPortMappingAction, serviceType, addPortMappingRequest, cancellationToken).ConfigureAwait(false); + await DoSoapActionAsync( + addPortMappingRequest, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "AddPortMapping", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); break; default: - throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); + throw new ArgumentException($"P2P: UPnP version {uPnPVersion} is not supported."); } - Logger.Log($"Opened IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Opened IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); return port; } - public async ValueTask CloseIpV4PortAsync(ushort port, CancellationToken cancellationToken = default) + public async Task CloseIpV4PortAsync(ushort port, CancellationToken cancellationToken) { - Logger.Log($"Deleting IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + try + { + Logger.Log($"P2P: Deleting IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); - int uPnPVersion = GetDeviceUPnPVersion(); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}", AddressFamily.InterNetwork); - string serviceAction = $"\"{service.ServiceType}#DeletePortMapping\""; + int uPnPVersion = GetDeviceUPnPVersion(); - switch (uPnPVersion) - { - case 2: - var deletePortMappingRequestV2 = new DeletePortMappingRequestV2(string.Empty, port, "UDP"); + switch (uPnPVersion) + { + case 2: + var deletePortMappingRequestV2 = new DeletePortMappingRequestV2(string.Empty, port, "UDP"); - await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, deletePortMappingRequestV2, cancellationToken).ConfigureAwait(false); + await DoSoapActionAsync( + deletePortMappingRequestV2, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "DeletePortMapping", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); - break; - case 1: - var deletePortMappingRequestV1 = new DeletePortMappingRequestV1(string.Empty, port, "UDP"); + break; + case 1: + var deletePortMappingRequestV1 = new DeletePortMappingRequestV1(string.Empty, port, "UDP"); - await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, deletePortMappingRequestV1, cancellationToken).ConfigureAwait(false); + await DoSoapActionAsync( + deletePortMappingRequestV1, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "DeletePortMapping", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); - break; - default: - throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); - } + break; + default: + throw new ArgumentException($"P2P: UPnP version {uPnPVersion} is not supported."); + } - Logger.Log($"Deleted IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Deleted IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, $"P2P: Could not close UPnP IPV4 port {port}."); + } } - public async ValueTask GetExternalIpV4AddressAsync(CancellationToken cancellationToken) + public async Task GetExternalIpV4AddressAsync(CancellationToken cancellationToken) { - Logger.Log($"Requesting external IP address from UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Requesting external IP address from UPnP device {UPnPDescription.Device.FriendlyName}."); + + int uPnPVersion = GetDeviceUPnPVersion(); + IPAddress ipAddress = null; try { - int uPnPVersion = GetDeviceUPnPVersion(); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}", AddressFamily.InterNetwork); - string serviceAction = $"\"{service.ServiceType}#GetExternalIPAddress\""; - IPAddress ipAddress; - switch (uPnPVersion) { case 2: - GetExternalIPAddressResponseV2 getExternalIpAddressResponseV2 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + GetExternalIPAddressResponseV2 getExternalIpAddressResponseV2 = await DoSoapActionAsync( + default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "GetExternalIPAddress", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV2.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV2.ExternalIPAddress); break; case 1: - GetExternalIPAddressResponseV1 getExternalIpAddressResponseV1 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + GetExternalIPAddressResponseV1 getExternalIpAddressResponseV1 = await DoSoapActionAsync( + default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "GetExternalIPAddress", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV1.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV1.ExternalIPAddress); break; default: - throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); + throw new ArgumentException($"P2P: UPnP version {uPnPVersion} is not supported."); } - Logger.Log($"Received external IP address {ipAddress} from UPnP device {UPnPDescription.Device.FriendlyName}."); - - return ipAddress; + Logger.Log($"P2P: Received external IP address {ipAddress} from UPnP device {UPnPDescription.Device.FriendlyName}."); } - catch (Exception ex) when (ex is not OperationCanceledException) + catch { - Logger.Log($"GetExternalIPAddress error/not supported on UPnP device {UPnPDescription.Device.FriendlyName}."); - ProgramConstants.LogException(ex); } - return null; + return ipAddress; } - public async ValueTask GetNatRsipStatusAsync(CancellationToken cancellationToken) + public async Task GetNatRsipStatusAsync(CancellationToken cancellationToken) { - Logger.Log($"Checking NAT status on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Checking NAT status on UPnP device {UPnPDescription.Device.FriendlyName}."); + + int uPnPVersion = GetDeviceUPnPVersion(); + bool? natEnabled = null; try { - int uPnPVersion = GetDeviceUPnPVersion(); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters($"{UPnPWanIpConnection}:{uPnPVersion}", AddressFamily.InterNetwork); - string serviceAction = $"\"{service.ServiceType}#GetNatRsipStatus\""; - bool natEnabled; - switch (uPnPVersion) { case 2: - GetNatRsipStatusResponseV2 getNatRsipStatusResponseV2 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + GetNatRsipStatusResponseV2 getNatRsipStatusResponseV2 = await DoSoapActionAsync( + default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "GetNatRsipStatus", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); natEnabled = getNatRsipStatusResponseV2.NatEnabled; break; case 1: - GetNatRsipStatusResponseV1 getNatRsipStatusResponseV1 = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + GetNatRsipStatusResponseV1 getNatRsipStatusResponseV1 = await DoSoapActionAsync( + default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "GetNatRsipStatus", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); natEnabled = getNatRsipStatusResponseV1.NatEnabled; break; default: - throw new ArgumentException($"UPNP version {uPnPVersion} is not supported."); + throw new ArgumentException($"P2P: UPnP version {uPnPVersion} is not supported."); } - Logger.Log($"Received NAT status {natEnabled} on UPnP device {UPnPDescription.Device.FriendlyName}."); - - return natEnabled; + Logger.Log($"P2P: Received NAT status {natEnabled} on UPnP device {UPnPDescription.Device.FriendlyName}."); } - catch (Exception ex) when (ex is not OperationCanceledException) + catch { - Logger.Log($"GetNatRsipStatus error/not supported on UPnP device {UPnPDescription.Device.FriendlyName}."); - ProgramConstants.LogException(ex); } - return null; + return natEnabled; } public async ValueTask<(bool? FirewallEnabled, bool? InboundPinholeAllowed)> GetIpV6FirewallStatusAsync(CancellationToken cancellationToken) { - Logger.Log($"Checking IPV6 firewall status on UPnP device {UPnPDescription.Device.FriendlyName}."); - try { - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1", AddressFamily.InterNetworkV6); - string serviceAction = $"\"{service.ServiceType}#GetFirewallStatus\""; - GetFirewallStatusResponse response = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, default, cancellationToken).ConfigureAwait(false); + Logger.Log($"P2P: Checking IPV6 firewall status on UPnP device {UPnPDescription.Device.FriendlyName}."); + + GetFirewallStatusResponse response = await DoSoapActionAsync( + default, $"{UPnPConstants.WanIpv6FirewallControl}:1", "GetFirewallStatus", AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); - Logger.Log($"Received IPV6 firewall status {response.FirewallEnabled} and port mapping allowed {response.InboundPinholeAllowed} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Received IPV6 firewall status {response.FirewallEnabled} and port mapping allowed {response.InboundPinholeAllowed} on UPnP device {UPnPDescription.Device.FriendlyName}."); return (response.FirewallEnabled, response.InboundPinholeAllowed); } - catch (Exception ex) when (ex is not OperationCanceledException) + catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) { - Logger.Log($"GetFirewallStatus error/not supported on UPnP device {UPnPDescription.Device.FriendlyName}."); - ProgramConstants.LogException(ex); + return (null, null); } - - return (null, null); } - public async ValueTask OpenIpV6PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) + public async Task OpenIpV6PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) { - Logger.Log($"Opening IPV6 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Opening IPV6 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1", AddressFamily.InterNetworkV6); - string serviceAction = $"\"{service.ServiceType}#AddPinhole\""; var request = new AddPinholeRequest(string.Empty, port, ipAddress.ToString(), port, IanaUdpProtocolNumber, IpLeaseTimeInSeconds); - AddPinholeResponse response = await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, request, cancellationToken).ConfigureAwait(false); + AddPinholeResponse response = await DoSoapActionAsync( + request, $"{UPnPConstants.WanIpv6FirewallControl}:1", "AddPinhole", AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); - Logger.Log($"Opened IPV6 UDP port {port} with ID {response.UniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Opened IPV6 UDP port {port} with ID {response.UniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); return response.UniqueId; } - public async ValueTask CloseIpV6PortAsync(ushort uniqueId, CancellationToken cancellationToken = default) + public async Task CloseIpV6PortAsync(ushort uniqueId, CancellationToken cancellationToken) + { + try + { + Logger.Log($"P2P: Deleting IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + await DoSoapActionAsync( + new(uniqueId), $"{UPnPConstants.WanIpv6FirewallControl}:1", "DeletePinhole", AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); + Logger.Log($"P2P: Deleted IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, $"P2P: Could not close UPnP IPV6 port with id {uniqueId}."); + } + } + + private async ValueTask DoSoapActionAsync( + TRequest request, string wanConnectionDeviceService, string action, AddressFamily addressFamily, CancellationToken cancellationToken) { - Logger.Log($"Deleting IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + try + { + (ServiceListItem service, Uri serviceUri, string serviceType) = GetSoapActionParameters(wanConnectionDeviceService, addressFamily); + string soapAction = $"\"{service.ServiceType}#{action}\""; - (ServiceListItem service, string serviceUri, string serviceType) = GetSoapActionParameters("WANIPv6FirewallControl:1", AddressFamily.InterNetworkV6); - string serviceAction = $"\"{service.ServiceType}#DeletePinhole\""; - var request = new DeletePinholeRequest(uniqueId); - await ExecuteSoapAction( - serviceUri, serviceAction, serviceType, request, cancellationToken).ConfigureAwait(false); + return await ExecuteSoapAction(serviceUri, soapAction, serviceType, request, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + ProgramConstants.LogException(ex, $"P2P: {action} error/not supported on UPnP device {UPnPDescription.Device.FriendlyName} using {addressFamily}."); - Logger.Log($"Deleted IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + throw; + } } private static async ValueTask ExecuteSoapAction( - string serviceUri, string soapAction, string defaultNamespace, TRequest request, CancellationToken cancellationToken) + Uri serviceUri, string soapAction, string defaultNamespace, TRequest request, CancellationToken cancellationToken) { HttpClient.DefaultRequestHeaders.Remove("SOAPAction"); HttpClient.DefaultRequestHeaders.Add("SOAPAction", soapAction); @@ -317,7 +339,7 @@ private static async ValueTask ExecuteSoapAction using var reader = new StreamReader(stream); string error = await reader.ReadToEndAsync(CancellationToken.None).ConfigureAwait(false); - ProgramConstants.LogException(ex, $"UPNP error {ex.StatusCode}:{error}."); + ProgramConstants.LogException(ex, $"P2P: UPnP error {ex.StatusCode}:{error}."); throw; } @@ -331,7 +353,7 @@ private static async ValueTask ExecuteSoapAction } } - private (ServiceListItem WanIpConnectionService, string ServiceUri, string ServiceType) GetSoapActionParameters(string wanConnectionDeviceService, AddressFamily addressFamily) + private (ServiceListItem WanIpConnectionService, Uri ServiceUri, string ServiceType) GetSoapActionParameters(string wanConnectionDeviceService, AddressFamily addressFamily) { Uri location = addressFamily switch { @@ -342,18 +364,18 @@ AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNa _ => PreferredLocation }; int uPnPVersion = GetDeviceUPnPVersion(); - Device wanDevice = UPnPDescription.Device.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPWanDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); - Device wanConnectionDevice = wanDevice.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPWanConnectionDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); - string serviceType = $"{UPnPService}:{wanConnectionDeviceService}"; + Device wanDevice = UPnPDescription.Device.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPConstants.UPnPWanDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); + Device wanConnectionDevice = wanDevice.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPConstants.UPnPWanConnectionDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); + string serviceType = $"{UPnPConstants.UPnPServiceNamespace}:{wanConnectionDeviceService}"; ServiceListItem wanIpConnectionService = wanConnectionDevice.ServiceList.Single(q => q.ServiceType.Equals(serviceType, StringComparison.OrdinalIgnoreCase)); - string serviceUri = FormattableString.Invariant($"{location.Scheme}://{location.Authority}{wanIpConnectionService.ControlUrl}"); + var serviceUri = new Uri(FormattableString.Invariant($"{location.Scheme}://{location.Authority}{wanIpConnectionService.ControlUrl}")); return new(wanIpConnectionService, serviceUri, serviceType); } private int GetDeviceUPnPVersion() { - return $"{UPnPInternetGatewayDevice}:2".Equals(UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase) ? 2 - : $"{UPnPInternetGatewayDevice}:1".Equals(UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase) ? 1 : 0; + return $"{UPnPConstants.UPnPInternetGatewayDevice}:2".Equals(UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase) ? 2 + : $"{UPnPConstants.UPnPInternetGatewayDevice}:1".Equals(UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase) ? 1 : 0; } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingRequest.cs similarity index 87% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingRequest.cs index afa8346ef..bf3649545 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingRequest.cs @@ -1,8 +1,8 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddAnyPortMapping", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +[MessageContract(WrapperName = "AddAnyPortMapping", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct AddAnyPortMappingRequest( [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingResponse.cs similarity index 62% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingResponse.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingResponse.cs index 4adf0ec30..3881a4c4e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddAnyPortMappingResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingResponse.cs @@ -1,7 +1,7 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddAnyPortMappingResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +[MessageContract(WrapperName = "AddAnyPortMappingResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct AddAnyPortMappingResponse( [property: MessageBodyMember(Name = "NewReservedPort")] ushort ReservedPort); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeRequest.cs similarity index 83% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeRequest.cs index 26e89b6a2..46a0dea3f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeRequest.cs @@ -1,8 +1,8 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddPinhole", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +[MessageContract(WrapperName = "AddPinhole", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct AddPinholeRequest( [property: MessageBodyMember(Name = "RemoteHost")] string RemoteHost, [property: MessageBodyMember(Name = "RemotePort")] ushort RemotePort, // 0 = wildcard diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeResponse.cs similarity index 60% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeResponse.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeResponse.cs index beaed5dc1..c020a6c38 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPinholeResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeResponse.cs @@ -1,7 +1,7 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddPinholeResponse", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +[MessageContract(WrapperName = "AddPinholeResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct AddPinholeResponse( [property: MessageBodyMember(Name = "UniqueID")] ushort UniqueId); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingRequest.cs similarity index 87% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingRequest.cs index df80b1b7e..7f0437304 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingRequest.cs @@ -1,8 +1,8 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddPortMapping", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +[MessageContract(WrapperName = "AddPortMapping", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct AddPortMappingRequest( [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingResponse.cs similarity index 51% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingResponse.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingResponse.cs index c4089f610..0e2902e90 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddPortMappingResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingResponse.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddPortMappingResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +[MessageContract(WrapperName = "AddPortMappingResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct AddPortMappingResponse; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeRequest.cs similarity index 61% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeRequest.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeRequest.cs index f510904c7..38dbcc0ae 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeRequest.cs @@ -1,7 +1,7 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePinhole", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +[MessageContract(WrapperName = "DeletePinhole", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct DeletePinholeRequest( [property: MessageBodyMember(Name = "UniqueID")] ushort UniqueId); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeResponse.cs similarity index 50% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeResponse.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeResponse.cs index 5cb4e07c4..5a370eaf3 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePinholeResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeResponse.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePinholeResponse", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +[MessageContract(WrapperName = "DeletePinholeResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct DeletePinholeResponse; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV1.cs similarity index 76% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV1.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV1.cs index 43f8c5720..cfc78632d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV1.cs @@ -1,8 +1,8 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePortMapping", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +[MessageContract(WrapperName = "DeletePortMapping", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct DeletePortMappingRequestV1( [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV2.cs similarity index 76% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV2.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV2.cs index 18921c22e..107f32af2 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingRequestV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV2.cs @@ -1,8 +1,8 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePortMapping", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +[MessageContract(WrapperName = "DeletePortMapping", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct DeletePortMappingRequestV2( [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV1.cs similarity index 52% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV1.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV1.cs index dd5a29e31..d3881a6b4 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV1.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePortMappingResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +[MessageContract(WrapperName = "DeletePortMappingResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct DeletePortMappingResponseV1; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV2.cs similarity index 52% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV2.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV2.cs index 86cf4b035..30c771cdd 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/DeletePortMappingResponseV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV2.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePortMappingResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +[MessageContract(WrapperName = "DeletePortMappingResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct DeletePortMappingResponseV2; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV1.cs similarity index 53% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV1.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV1.cs index 6ae834a0f..89f5df1fe 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV1.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetExternalIPAddress", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +[MessageContract(WrapperName = "GetExternalIPAddress", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct GetExternalIPAddressRequestV1; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV2.cs similarity index 53% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV2.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV2.cs index e1ef1e4d0..51b25bd38 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressRequestV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV2.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetExternalIPAddress", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +[MessageContract(WrapperName = "GetExternalIPAddress", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct GetExternalIPAddressRequestV2; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV1.cs similarity index 63% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV1.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV1.cs index 5ffe3d71a..e4b681487 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV1.cs @@ -1,7 +1,7 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetExternalIPAddressResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +[MessageContract(WrapperName = "GetExternalIPAddressResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct GetExternalIPAddressResponseV1( [property: MessageBodyMember(Name = "NewExternalIPAddress")] string ExternalIPAddress); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV2.cs similarity index 63% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV2.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV2.cs index 0107f106d..be90c3ae9 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetExternalIPAddressResponseV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV2.cs @@ -1,7 +1,7 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetExternalIPAddressResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +[MessageContract(WrapperName = "GetExternalIPAddressResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct GetExternalIPAddressResponseV2( [property: MessageBodyMember(Name = "NewExternalIPAddress")] string ExternalIPAddress); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusRequest.cs similarity index 51% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusRequest.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusRequest.cs index b1b554361..ce3793551 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusRequest.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetFirewallStatus", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +[MessageContract(WrapperName = "GetFirewallStatus", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct GetFirewallStatusRequest; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusResponse.cs similarity index 68% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusResponse.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusResponse.cs index 75d7a6c12..ccd4e62e3 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetFirewallStatusResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusResponse.cs @@ -1,8 +1,8 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetFirewallStatusResponse", WrapperNamespace = "urn:dslforum-org:service:WANIPv6FirewallControl:1")] +[MessageContract(WrapperName = "GetFirewallStatusResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct GetFirewallStatusResponse( [property: MessageBodyMember(Name = "FirewallEnabled")] bool FirewallEnabled, [property: MessageBodyMember(Name = "InboundPinholeAllowed")] bool InboundPinholeAllowed); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV1.cs similarity index 51% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV1.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV1.cs index b2277f812..1e8ab5a31 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV1.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetNatRsipStatusRequest", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +[MessageContract(WrapperName = "GetNatRsipStatusRequest", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] public readonly record struct GetNatRsipStatusRequestV1; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV2.cs similarity index 51% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV2.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV2.cs index e863b356a..96d0fcf97 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusRequestV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV2.cs @@ -1,6 +1,6 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetNatRsipStatusRequest", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +[MessageContract(WrapperName = "GetNatRsipStatusRequest", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] public readonly record struct GetNatRsipStatusRequestV2; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV1.cs similarity index 68% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV1.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV1.cs index 69a074bbf..0cbb1814f 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV1.cs @@ -1,8 +1,8 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetNatRsipStatusResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:1")] +[MessageContract(WrapperName = "GetNatRsipStatusResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] public readonly record struct GetNatRsipStatusResponseV1( [property: MessageBodyMember(Name = "NewRSIPAvailable")] bool RsipAvailable, [property: MessageBodyMember(Name = "NewNATEnabled")] bool NatEnabled); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV2.cs similarity index 68% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV2.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV2.cs index 1a821fe0a..071937fb0 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/GetNatRsipStatusResponseV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV2.cs @@ -1,8 +1,8 @@ using System.ServiceModel; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetNatRsipStatusResponse", WrapperNamespace = "urn:schemas-upnp-org:service:WANIPConnection:2")] +[MessageContract(WrapperName = "GetNatRsipStatusResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] public readonly record struct GetNatRsipStatusResponseV2( [property: MessageBodyMember(Name = "NewRSIPAvailable")] bool RsipAvailable, [property: MessageBodyMember(Name = "NewNATEnabled")] bool NatEnabled); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs index 8438c8181..50d0e5181 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs @@ -1,4 +1,4 @@ -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; internal enum AddressType { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Device.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Device.cs index 05f324a39..c3757eb9e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Device.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Device.cs @@ -1,8 +1,8 @@ using System.Runtime.Serialization; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[DataContract(Name = "device", Namespace = "urn:schemas-upnp-org:device-1-0")] +[DataContract(Name = "device", Namespace = UPnPConstants.UPnPDevice10Namespace)] internal readonly record struct Device( [property: DataMember(Name = "deviceType", Order = 0)] string DeviceType, [property: DataMember(Name = "friendlyName", Order = 1)] string FriendlyName, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/IconListItem.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/IconListItem.cs index c1acc6869..3cbd9146a 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/IconListItem.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/IconListItem.cs @@ -1,8 +1,8 @@ using System.Runtime.Serialization; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[DataContract(Name = "icon", Namespace = "urn:schemas-upnp-org:device-1-0")] +[DataContract(Name = "icon", Namespace = UPnPConstants.UPnPDevice10Namespace)] internal readonly record struct IconListItem( [property: DataMember(Name = "mimetype", Order = 0)] string Mimetype, [property: DataMember(Name = "width", Order = 1)] int Width, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs index 3f33396e1..ec2ad610e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs @@ -1,5 +1,5 @@ using System; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; internal readonly record struct InternetGatewayDeviceResponse(Uri Location, string Server, string CacheControl, string Ext, string SearchTarget, string Usn); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/ServiceListItem.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/ServiceListItem.cs index 56d6523a9..56c59502b 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/ServiceListItem.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/ServiceListItem.cs @@ -1,8 +1,8 @@ using System.Runtime.Serialization; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[DataContract(Name = "service", Namespace = "urn:schemas-upnp-org:device-1-0")] +[DataContract(Name = "service", Namespace = UPnPConstants.UPnPDevice10Namespace)] internal readonly record struct ServiceListItem( [property: DataMember(Name = "serviceType", Order = 0)] string ServiceType, [property: DataMember(Name = "serviceId", Order = 1)] string ServiceId, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SpecVersion.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SpecVersion.cs index 674c6477c..53d87c6b9 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SpecVersion.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SpecVersion.cs @@ -1,8 +1,8 @@ using System.Runtime.Serialization; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[DataContract(Name = "specVersion", Namespace = "urn:schemas-upnp-org:device-1-0")] +[DataContract(Name = "specVersion", Namespace = UPnPConstants.UPnPDevice10Namespace)] internal readonly record struct SpecVersion( [property: DataMember(Name = "major", Order = 0)] int Major, [property: DataMember(Name = "minor", Order = 1)] int Minor); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SystemVersion.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SystemVersion.cs index 10b8836e3..df108aa82 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SystemVersion.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/SystemVersion.cs @@ -1,8 +1,8 @@ using System.Runtime.Serialization; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[DataContract(Name = "systemVersion", Namespace = "urn:schemas-upnp-org:device-1-0")] +[DataContract(Name = "systemVersion", Namespace = UPnPConstants.UPnPDevice10Namespace)] internal readonly record struct SystemVersion( [property: DataMember(Name = "HW", Order = 0)] int Hw, [property: DataMember(Name = "Major", Order = 1)] int Major, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPConstants.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPConstants.cs new file mode 100644 index 000000000..ff31c9963 --- /dev/null +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPConstants.cs @@ -0,0 +1,16 @@ +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; + +internal static class UPnPConstants +{ + private const string UPnPNamespace = "urn:schemas-upnp-org"; + private const string UPnPDeviceNamespace = $"{UPnPNamespace}:device"; + public const string UPnPDevice10Namespace = $"{UPnPDeviceNamespace}-1-0"; + public const string UPnPServiceNamespace = $"{UPnPNamespace}:service"; + public const string UPnPWanConnectionDevice = $"{UPnPDeviceNamespace}:WANConnectionDevice"; + public const string UPnPWanDevice = $"{UPnPDeviceNamespace}:WANDevice"; + public const string UPnPInternetGatewayDevice = $"{UPnPDeviceNamespace}:InternetGatewayDevice"; + public const string WanIpConnection = "WANIPConnection"; + public const string WanIpv6FirewallControl = "WANIPv6FirewallControl"; + public const string UPnPRootDevice = "upnp:rootdevice"; + public const int UPnPMultiCastPort = 1900; +} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPDescription.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPDescription.cs index fa9e41474..476027945 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPDescription.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPDescription.cs @@ -1,8 +1,8 @@ using System.Runtime.Serialization; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[DataContract(Name = "root", Namespace = "urn:schemas-upnp-org:device-1-0")] +[DataContract(Name = "root", Namespace = UPnPConstants.UPnPDevice10Namespace)] internal readonly record struct UPnPDescription( [property: DataMember(Name = "specVersion", Order = 0)] SpecVersion SpecVersion, [property: DataMember(Name = "systemVersion", Order = 1)] SystemVersion SystemVersion, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 4ec8275cd..f70604ae9 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -7,23 +7,17 @@ using System.Threading.Tasks; using System.Net; using System.Net.Http; -using System.Net.NetworkInformation; using System.Net.Sockets; -using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Text; using System.Xml; using ClientCore; -using ClientCore.Extensions; using Rampastring.Tools; -using DTAClient.Domain.Multiplayer.CnCNet.UPNP; -namespace DTAClient.Domain.Multiplayer.CnCNet; +namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; internal static class UPnPHandler { - private const string InternetGatewayDeviceDeviceType = "upnp:rootdevice"; - private const int UPnPMultiCastPort = 1900; private const int ReceiveTimeout = 2000; private const int SendCount = 3; @@ -57,22 +51,24 @@ private static IReadOnlyDictionary SsdpMultiCastAddresse InternetGatewayDevice InternetGatewayDevice, List<(ushort InternalPort, ushort ExternalPort)> IpV6P2PPorts, List<(ushort InternalPort, ushort ExternalPort)> IpV4P2PPorts, - List P2PIpV6PortIds, IPAddress IpV6Address, IPAddress IpV4Address)> SetupPortsAsync( + List P2PIpV6PortIds, + IPAddress IpV6Address, + IPAddress IpV4Address)> SetupPortsAsync( InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, List stunServerIpAddresses, CancellationToken cancellationToken) { - Logger.Log("Starting P2P Setup."); + Logger.Log("P2P: Starting Setup."); internetGatewayDevice ??= await GetInternetGatewayDeviceAsync(cancellationToken).ConfigureAwait(false); - (IPAddress publicIpV4Address, List<(ushort InternalPort, ushort ExternalPort)> ipV4P2PPorts) = - await SetupIpV4PortsAsync(internetGatewayDevice, p2pReservedPorts, stunServerIpAddresses, cancellationToken).ConfigureAwait(false); - (IPAddress publicIpV6Address, List<(ushort InternalPort, ushort ExternalPort)> ipV6P2PPorts, List ipV6P2PPortIds) = - await SetupIpV6PortsAsync(internetGatewayDevice, p2pReservedPorts, stunServerIpAddresses, cancellationToken).ConfigureAwait(false); + Task<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports)> ipV4Task = SetupIpV4PortsAsync(internetGatewayDevice, p2pReservedPorts, stunServerIpAddresses, cancellationToken); + Task<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports, List PortIds)> ipV6Task = SetupIpV6PortsAsync(internetGatewayDevice, p2pReservedPorts, stunServerIpAddresses, cancellationToken); - return (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, ipV6P2PPortIds, publicIpV6Address, publicIpV4Address); + await ClientCore.Extensions.TaskExtensions.WhenAllSafe(new Task[] { ipV4Task, ipV6Task }).ConfigureAwait(false); + + return (internetGatewayDevice, ipV6Task.Result.Ports, ipV4Task.Result.Ports, ipV6Task.Result.PortIds, ipV6Task.Result.IpAddress, ipV4Task.Result.IpAddress); } private static async Task GetInternetGatewayDeviceAsync(CancellationToken cancellationToken) @@ -83,47 +79,24 @@ private static async Task GetInternetGatewayDeviceAsync(C internetGatewayDevice ??= GetInternetGatewayDevice(internetGatewayDevices, 1); if (internetGatewayDevice is not null) - Logger.Log($"Found NAT device {internetGatewayDevice.UPnPDescription.Device.DeviceType} - {internetGatewayDevice.Server} ({internetGatewayDevice.UPnPDescription.Device.FriendlyName})."); + Logger.Log($"P2P: Found NAT device {internetGatewayDevice.UPnPDescription.Device.DeviceType} - {internetGatewayDevice.Server} ({internetGatewayDevice.UPnPDescription.Device.FriendlyName})."); return internetGatewayDevice; } - private static async ValueTask<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports, List PortIds)> SetupIpV6PortsAsync( + private static async Task<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports, List PortIds)> SetupIpV6PortsAsync( InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, List stunServerIpAddresses, CancellationToken cancellationToken) { - (IPAddress stunPublicIpV6Address, List<(ushort InternalPort, ushort ExternalPort)> ipV6StunPortMapping) = await PerformStunAsync( + (IPAddress stunPublicIpV6Address, List<(ushort InternalPort, ushort ExternalPort)> ipV6StunPortMapping) = await NetworkHelper.PerformStunAsync( stunServerIpAddresses, p2pReservedPorts, AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); - IPAddress localPublicIpV6Address; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var localIpV6Addresses = NetworkHelper.GetWindowsPublicIpAddresses() - .Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); - - (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundLocalPublicIpV6Address = localIpV6Addresses - .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); - - if (foundLocalPublicIpV6Address.IpAddress is null) - { - foundLocalPublicIpV6Address = localIpV6Addresses - .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); - } - - localPublicIpV6Address = foundLocalPublicIpV6Address.IpAddress; - } - else - { - localPublicIpV6Address = NetworkHelper.GetPublicIpAddresses() - .FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); - } - + IPAddress localPublicIpV6Address = NetworkHelper.GetLocalPublicIpV6Address(); var ipV6P2PPorts = new List<(ushort InternalPort, ushort ExternalPort)>(); var ipV6P2PPortIds = new List(); IPAddress publicIpV6Address = null; if (stunPublicIpV6Address is not null || localPublicIpV6Address is not null) { - Logger.Log("Public IPV6 detected."); + Logger.Log("P2P: Public IPV6 detected."); if (internetGatewayDevice is not null) { @@ -134,21 +107,18 @@ private static async Task GetInternetGatewayDeviceAsync(C if (firewallEnabled is not false && inboundPinholeAllowed is not false) { - Logger.Log("Configuring IPV6 firewall."); + Logger.Log("P2P: Configuring IPV6 firewall."); - foreach (ushort p2pReservedPort in p2pReservedPorts) - { - ipV6P2PPortIds.Add(await internetGatewayDevice.OpenIpV6PortAsync( - localPublicIpV6Address, p2pReservedPort, cancellationToken).ConfigureAwait(false)); - } + ipV6P2PPortIds = (await ClientCore.Extensions.TaskExtensions.WhenAllSafe(p2pReservedPorts.Select( + q => internetGatewayDevice.OpenIpV6PortAsync(localPublicIpV6Address, q, cancellationToken))).ConfigureAwait(false)).ToList(); } } catch (Exception ex) { #if DEBUG - ProgramConstants.LogException(ex, $"Could not open P2P IPV6 router ports for {localPublicIpV6Address}."); + ProgramConstants.LogException(ex, $"P2P: Could not open P2P IPV6 router ports for {localPublicIpV6Address}."); #else - ProgramConstants.LogException(ex, $"Could not open P2P IPV6 router ports."); + ProgramConstants.LogException(ex, $"P2P: Could not open P2P IPV6 router ports."); #endif } } @@ -156,7 +126,7 @@ private static async Task GetInternetGatewayDeviceAsync(C if (stunPublicIpV6Address is not null && localPublicIpV6Address is not null && !stunPublicIpV6Address.Equals(localPublicIpV6Address)) { publicIpV6Address = stunPublicIpV6Address; - ipV6P2PPorts = ipV6StunPortMapping; + ipV6P2PPorts = ipV6StunPortMapping.Any() ? ipV6StunPortMapping : p2pReservedPorts.Select(q => (q, q)).ToList(); } else { @@ -168,7 +138,7 @@ private static async Task GetInternetGatewayDeviceAsync(C return (publicIpV6Address, ipV6P2PPorts, ipV6P2PPortIds); } - private static async ValueTask<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports)> SetupIpV4PortsAsync( + private static async Task<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports)> SetupIpV4PortsAsync( InternetGatewayDevice internetGatewayDevice, List p2pReservedPorts, List stunServerIpAddresses, CancellationToken cancellationToken) { bool? routerNatEnabled = null; @@ -176,17 +146,22 @@ private static async Task GetInternetGatewayDeviceAsync(C if (internetGatewayDevice is not null) { - routerNatEnabled = await internetGatewayDevice.GetNatRsipStatusAsync(cancellationToken).ConfigureAwait(false); - routerPublicIpV4Address = await internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken).ConfigureAwait(false); + Task natRsipStatusTask = internetGatewayDevice.GetNatRsipStatusAsync(cancellationToken); + Task externalIpv4AddressTask = internetGatewayDevice.GetExternalIpV4AddressAsync(cancellationToken); + + await ClientCore.Extensions.TaskExtensions.WhenAllSafe(new Task[] { natRsipStatusTask, externalIpv4AddressTask }).ConfigureAwait(false); + + routerNatEnabled = natRsipStatusTask.Result; + routerPublicIpV4Address = externalIpv4AddressTask.Result; } - (IPAddress stunPublicIpV4Address, List<(ushort InternalPort, ushort ExternalPort)> ipV4StunPortMapping) = await PerformStunAsync( + (IPAddress stunPublicIpV4Address, List<(ushort InternalPort, ushort ExternalPort)> ipV4StunPortMapping) = await NetworkHelper.PerformStunAsync( stunServerIpAddresses, p2pReservedPorts, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); IPAddress tracePublicIpV4Address = null; if (routerPublicIpV4Address is null && stunPublicIpV4Address is null) { - Logger.Log("Using IPV4 trace detection."); + Logger.Log("P2P: Using IPV4 trace detection."); tracePublicIpV4Address = await NetworkHelper.TracePublicIpV4Address(cancellationToken).ConfigureAwait(false); } @@ -195,7 +170,7 @@ private static async Task GetInternetGatewayDeviceAsync(C if (routerPublicIpV4Address is null && stunPublicIpV4Address is null && tracePublicIpV4Address is null) { - Logger.Log("Using IPV4 local public address."); + Logger.Log("P2P: Using IPV4 local public address."); var localPublicIpAddresses = NetworkHelper.GetPublicIpAddresses().ToList(); @@ -207,33 +182,27 @@ private static async Task GetInternetGatewayDeviceAsync(C if (publicIpV4Address is not null) { - Logger.Log("Public IPV4 detected."); + Logger.Log("P2P: Public IPV4 detected."); var privateIpV4Addresses = NetworkHelper.GetPrivateIpAddresses().Where(q => q.AddressFamily is AddressFamily.InterNetwork).ToList(); IPAddress privateIpV4Address = privateIpV4Addresses.FirstOrDefault(); if (internetGatewayDevice is not null && privateIpV4Address is not null && routerNatEnabled is not false) { - Logger.Log("Using IPV4 port mapping."); + Logger.Log("P2P: Using IPV4 port mapping."); try { - foreach (int p2PReservedPort in p2pReservedPorts) - { - ushort openedPort = await internetGatewayDevice.OpenIpV4PortAsync( - privateIpV4Address, (ushort)p2PReservedPort, cancellationToken).ConfigureAwait(false); - - ipV4P2PPorts.Add((openedPort, openedPort)); - } - + ipV4P2PPorts = (await ClientCore.Extensions.TaskExtensions.WhenAllSafe(p2pReservedPorts.Select( + q => internetGatewayDevice.OpenIpV4PortAsync(privateIpV4Address, q, cancellationToken))).ConfigureAwait(false)).Select(q => (q, q)).ToList(); p2pReservedPorts = ipV4P2PPorts.Select(q => q.InternalPort).ToList(); } catch (Exception ex) { #if DEBUG - ProgramConstants.LogException(ex, $"Could not open P2P IPV4 router ports for {privateIpV4Address} -> {publicIpV4Address}."); + ProgramConstants.LogException(ex, $"P2P: Could not open P2P IPV4 router ports for {privateIpV4Address} -> {publicIpV4Address}."); #else - ProgramConstants.LogException(ex, $"Could not open P2P IPV4 router ports."); + ProgramConstants.LogException(ex, $"P2P: Could not open P2P IPV4 router ports."); #endif ipV4P2PPorts = ipV4StunPortMapping.Any() ? ipV4StunPortMapping : p2pReservedPorts.Select(q => (q, q)).ToList(); } @@ -247,61 +216,6 @@ private static async Task GetInternetGatewayDeviceAsync(C return (publicIpV4Address, ipV4P2PPorts); } - private static async ValueTask<(IPAddress IPAddress, List<(ushort InternalPort, ushort ExternalPort)> PortMapping)> PerformStunAsync( - List stunServerIpAddresses, List p2pReservedPorts, AddressFamily addressFamily, CancellationToken cancellationToken) - { - Logger.Log($"Using STUN to detect {addressFamily} address."); - - var stunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); - List matchingStunServerIpAddresses = stunServerIpAddresses.Where(q => q.AddressFamily == addressFamily).ToList(); - - if (!matchingStunServerIpAddresses.Any()) - { - Logger.Log($"No {addressFamily} STUN servers found."); - - return (null, stunPortMapping); - } - - IPAddress stunPublicAddress = null; - IPAddress stunServerIpAddress = null; - - foreach (IPAddress matchingStunServerIpAddress in matchingStunServerIpAddresses.TakeWhile(_ => stunPublicAddress is null)) - { - stunServerIpAddress = matchingStunServerIpAddress; - - foreach (ushort p2pReservedPort in p2pReservedPorts) - { - IPEndPoint stunPublicIpEndPoint = await NetworkHelper.PerformStunAsync( - stunServerIpAddress, p2pReservedPort, cancellationToken).ConfigureAwait(false); - - if (stunPublicIpEndPoint is null) - break; - - stunPublicAddress = stunPublicIpEndPoint.Address; - - if (p2pReservedPort != stunPublicIpEndPoint.Port) - stunPortMapping.Add(new(p2pReservedPort, (ushort)stunPublicIpEndPoint.Port)); - } - } - - if (stunPublicAddress is not null) - Logger.Log($"{addressFamily} STUN detection succeeded."); - else - Logger.Log($"{addressFamily} STUN detection failed."); - - if (stunPortMapping.Any()) - { - Logger.Log($"{addressFamily} STUN detection detected mapped ports, running STUN keep alive."); -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - NetworkHelper.KeepStunAliveAsync( - stunServerIpAddress, - stunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - } - - return (stunPublicAddress, stunPortMapping); - } - private static async ValueTask> GetInternetGatewayDevicesAsync(CancellationToken cancellationToken) { IEnumerable rawDeviceResponses = await GetRawDeviceResponses(cancellationToken).ConfigureAwait(false); @@ -314,7 +228,7 @@ private static async ValueTask> GetInternetGa } private static InternetGatewayDevice GetInternetGatewayDevice(List internetGatewayDevices, ushort uPnPVersion) - => internetGatewayDevices.SingleOrDefault(q => $"{InternetGatewayDevice.UPnPInternetGatewayDevice}:{uPnPVersion}".Equals(q.UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase)); + => internetGatewayDevices.SingleOrDefault(q => $"{UPnPConstants.UPnPInternetGatewayDevice}:{uPnPVersion}".Equals(q.UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase)); private static IEnumerable> GetGroupedInternetGatewayDeviceResponses( IEnumerable> formattedDeviceResponses) @@ -361,8 +275,8 @@ private static async Task> SearchDevicesAsync(IPAddress loca socket.Bind(new IPEndPoint(localAddress, 0)); - var multiCastIpEndPoint = new IPEndPoint(SsdpMultiCastAddresses[addressType], UPnPMultiCastPort); - string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {multiCastIpEndPoint}\r\nST: {InternetGatewayDeviceDeviceType}\r\nMAN: \"ssdp:discover\"\r\nMX: 3\r\n\r\n"); + var multiCastIpEndPoint = new IPEndPoint(SsdpMultiCastAddresses[addressType], UPnPConstants.UPnPMultiCastPort); + string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {multiCastIpEndPoint}\r\nST: {UPnPConstants.UPnPRootDevice}\r\nMAN: \"ssdp:discover\"\r\nMX: 3\r\n\r\n"); const int charSize = sizeof(char); int bufferSize = request.Length * charSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index 1c140edb5..8c6148fbd 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -433,39 +433,16 @@ private void SetupGameTunnelHandler( private async ValueTask CloseP2PPortsAsync() { - try - { - if (internetGatewayDevice is not null) - { - foreach (ushort p2pPort in ipV4P2PPorts.Select(q => q.InternalPort)) - await internetGatewayDevice.CloseIpV4PortAsync(p2pPort).ConfigureAwait(false); - } - } - catch (Exception ex) - { - ProgramConstants.LogException(ex, "Could not close P2P IPV4 ports."); - } - finally - { - ipV4P2PPorts.Clear(); - } + if (internetGatewayDevice is null) + return; - try - { - if (internetGatewayDevice is not null) - { - foreach (ushort p2pIpV6PortId in p2pIpV6PortIds) - await internetGatewayDevice.CloseIpV6PortAsync(p2pIpV6PortId).ConfigureAwait(false); - } - } - catch (Exception ex) - { - ProgramConstants.LogException(ex, "Could not close P2P IPV6 ports."); - } - finally - { - ipV6P2PPorts.Clear(); - p2pIpV6PortIds.Clear(); - } + Task ipV4Task = ClientCore.Extensions.TaskExtensions.WhenAllSafe(ipV4P2PPorts.Select(q => internetGatewayDevice.CloseIpV4PortAsync(q.InternalPort, CancellationToken.None))); + Task ipV6Task = ClientCore.Extensions.TaskExtensions.WhenAllSafe(p2pIpV6PortIds.Select(q => internetGatewayDevice.CloseIpV6PortAsync(q, CancellationToken.None))); + + ipV4P2PPorts.Clear(); + ipV6P2PPorts.Clear(); + p2pIpV6PortIds.Clear(); + + await ClientCore.Extensions.TaskExtensions.WhenAllSafe(new[] { ipV4Task, ipV6Task }).ConfigureAwait(false); } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 0d06997bc..76b424384 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -5,10 +5,12 @@ using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using ClientCore; +using ClientCore.Extensions; using Rampastring.Tools; namespace DTAClient.Domain.Multiplayer; @@ -59,6 +61,26 @@ public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unic return new(BitConverter.GetBytes(broadCastIpAddress)); } + public static IPAddress GetLocalPublicIpV6Address() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return GetPublicIpAddresses().FirstOrDefault(q => q.AddressFamily is AddressFamily.InterNetworkV6); + + var localIpV6Addresses = GetWindowsPublicIpAddresses() + .Where(q => q.IpAddress.AddressFamily is AddressFamily.InterNetworkV6).ToList(); + + (IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin) foundLocalPublicIpV6Address = localIpV6Addresses + .FirstOrDefault(q => q.PrefixOrigin is PrefixOrigin.RouterAdvertisement && q.SuffixOrigin is SuffixOrigin.LinkLayerAddress); + + if (foundLocalPublicIpV6Address.IpAddress is null) + { + foundLocalPublicIpV6Address = localIpV6Addresses.FirstOrDefault( + q => q.PrefixOrigin is PrefixOrigin.Dhcp && q.SuffixOrigin is SuffixOrigin.OriginDhcp); + } + + return foundLocalPublicIpV6Address.IpAddress; + } + public static async ValueTask TracePublicIpV4Address(CancellationToken cancellationToken) { try @@ -122,14 +144,125 @@ public static async ValueTask TracePublicIpV4Address(CancellationToke return null; } - public static async ValueTask PerformStunAsync(IPAddress stunServerIpAddress, ushort localPort, CancellationToken cancellationToken) + public static async ValueTask<(IPAddress IPAddress, List<(ushort InternalPort, ushort ExternalPort)> PortMapping)> PerformStunAsync( + List stunServerIpAddresses, List p2pReservedPorts, AddressFamily addressFamily, CancellationToken cancellationToken) + { + Logger.Log($"Using STUN to detect {addressFamily} address."); + + var stunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); + List matchingStunServerIpAddresses = stunServerIpAddresses.Where(q => q.AddressFamily == addressFamily).ToList(); + + if (!matchingStunServerIpAddresses.Any()) + { + Logger.Log($"No {addressFamily} STUN servers found."); + + return (null, stunPortMapping); + } + + IPAddress stunPublicAddress = null; + IPAddress stunServerIpAddress = null; + + foreach (IPAddress matchingStunServerIpAddress in matchingStunServerIpAddresses.TakeWhile(_ => stunPublicAddress is null)) + { + stunServerIpAddress = matchingStunServerIpAddress; + + foreach (ushort p2pReservedPort in p2pReservedPorts) + { + IPEndPoint stunPublicIpEndPoint = await PerformStunAsync( + stunServerIpAddress, p2pReservedPort, addressFamily, cancellationToken).ConfigureAwait(false); + + if (stunPublicIpEndPoint is null) + break; + + stunPublicAddress = stunPublicIpEndPoint.Address; + + if (p2pReservedPort != stunPublicIpEndPoint.Port) + stunPortMapping.Add(new(p2pReservedPort, (ushort)stunPublicIpEndPoint.Port)); + } + } + + if (stunPublicAddress is not null) + Logger.Log($"{addressFamily} STUN detection succeeded."); + else + Logger.Log($"{addressFamily} STUN detection failed."); + + if (stunPortMapping.Any()) + { + Logger.Log($"{addressFamily} STUN detection detected mapped ports, running STUN keep alive."); +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + KeepStunAliveAsync( + stunServerIpAddress, + stunPortMapping.Select(q => q.InternalPort).ToList(), cancellationToken).HandleTask(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + } + + return (stunPublicAddress, stunPortMapping); + } + + /// + /// Returns the specified amount of free UDP port numbers. + /// + /// List of UDP port numbers which are additionally excluded. + /// The number of free ports to return. + /// A free UDP port number on the current system. + public static IEnumerable GetFreeUdpPorts(IEnumerable excludedPorts, ushort numberOfPorts) + { + IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); + var activeV4AndV6Ports = endPoints.Select(q => (ushort)q.Port).ToArray().Concat(excludedPorts).Distinct().ToList(); + ushort foundPortCount = 0; + + while (foundPortCount != numberOfPorts) + { + using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); + + socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + + ushort foundPort = (ushort)((IPEndPoint)socket.LocalEndPoint).Port; + + if (!activeV4AndV6Ports.Contains(foundPort)) + { + activeV4AndV6Ports.Add(foundPort); + + foundPortCount++; + + yield return foundPort; + } + } + } + + private static bool IsPrivateIpAddress(IPAddress ipAddress) + => ipAddress.AddressFamily switch + { + AddressFamily.InterNetworkV6 => ipAddress.IsIPv6SiteLocal + || ipAddress.IsIPv6UniqueLocal + || ipAddress.IsIPv6LinkLocal, + AddressFamily.InterNetwork => IsInRange("10.0.0.0", "10.255.255.255", ipAddress) + || IsInRange("172.16.0.0", "172.31.255.255", ipAddress) + || IsInRange("172.16.0.0", "172.31.255.255", ipAddress) + || IsInRange("192.168.0.0", "192.168.255.255", ipAddress) + || IsInRange("169.254.0.0", "169.254.255.255", ipAddress) + || IsInRange("127.0.0.0", "127.255.255.255", ipAddress) + || IsInRange("0.0.0.0", "0.255.255.255", ipAddress), + _ => throw new ArgumentOutOfRangeException(nameof(ipAddress.AddressFamily), ipAddress.AddressFamily, null), + }; + + private static bool IsInRange(string startIpAddress, string endIpAddress, IPAddress address) + { + uint ipStart = BitConverter.ToUInt32(IPAddress.Parse(startIpAddress).GetAddressBytes().Reverse().ToArray(), 0); + uint ipEnd = BitConverter.ToUInt32(IPAddress.Parse(endIpAddress).GetAddressBytes().Reverse().ToArray(), 0); + uint ip = BitConverter.ToUInt32(address.GetAddressBytes().Reverse().ToArray(), 0); + + return ip >= ipStart && ip <= ipEnd; + } + + private static async ValueTask PerformStunAsync(IPAddress stunServerIpAddress, ushort localPort, AddressFamily addressFamily, CancellationToken cancellationToken) { const short stunId = 26262; const int stunPort1 = 3478; const int stunPort2 = 8054; const int stunSize = 48; int[] stunPorts = { stunPort1, stunPort2 }; - using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); + using var socket = new Socket(addressFamily, SocketType.Dgram, ProtocolType.Udp); short stunIdNetworkOrder = IPAddress.HostToNetworkOrder(stunId); using IMemoryOwner receiveMemoryOwner = MemoryPool.Shared.Rent(stunSize); Memory buffer = receiveMemoryOwner.Memory[..stunSize]; @@ -141,7 +274,7 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI int addressBytes = stunServerIpAddress.GetAddressBytes().Length; const int portBytes = sizeof(ushort); - socket.Bind(new IPEndPoint(IPAddress.IPv6Any, localPort)); + socket.Bind(new IPEndPoint(addressFamily is AddressFamily.InterNetworkV6 ? IPAddress.IPv6Any : IPAddress.Any, localPort)); foreach (int stunPort in stunPorts) { @@ -172,16 +305,20 @@ public static async ValueTask PerformStunAsync(IPAddress stunServerI return new(publicIpAddress, publicPort); } - catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { - ProgramConstants.LogException(ex, $"STUN server {stunServerIpEndPoint} failed."); + Logger.Log($"STUN server {stunServerIpEndPoint} unreachable."); + } + catch (Exception ex) + { + ProgramConstants.LogException(ex, $"STUN server {stunServerIpEndPoint} unreachable."); } } return null; } - public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List localPorts, CancellationToken cancellationToken) + private static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List localPorts, CancellationToken cancellationToken) { try { @@ -189,7 +326,7 @@ public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List< { foreach (ushort localPort in localPorts) { - await PerformStunAsync(stunServerIpAddress, localPort, cancellationToken).ConfigureAwait(false); + await PerformStunAsync(stunServerIpAddress, localPort, stunServerIpAddress.AddressFamily, cancellationToken).ConfigureAwait(false); await Task.Delay(100, cancellationToken).ConfigureAwait(false); } @@ -205,60 +342,4 @@ public static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List< ProgramConstants.LogException(ex, "STUN keep alive failed."); } } - - /// - /// Returns the specified amount of free UDP port numbers. - /// - /// List of UDP port numbers which are additionally excluded. - /// The number of free ports to return. - /// A free UDP port number on the current system. - public static IEnumerable GetFreeUdpPorts(IEnumerable excludedPorts, ushort numberOfPorts) - { - IPEndPoint[] endPoints = IPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners(); - var activeV4AndV6Ports = endPoints.Select(q => (ushort)q.Port).ToArray().Concat(excludedPorts).Distinct().ToList(); - ushort foundPortCount = 0; - - while (foundPortCount != numberOfPorts) - { - using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); - - socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - - ushort foundPort = (ushort)((IPEndPoint)socket.LocalEndPoint).Port; - - if (!activeV4AndV6Ports.Contains(foundPort)) - { - activeV4AndV6Ports.Add(foundPort); - - foundPortCount++; - - yield return foundPort; - } - } - } - - private static bool IsPrivateIpAddress(IPAddress ipAddress) - => ipAddress.AddressFamily switch - { - AddressFamily.InterNetworkV6 => ipAddress.IsIPv6SiteLocal - || ipAddress.IsIPv6UniqueLocal - || ipAddress.IsIPv6LinkLocal, - AddressFamily.InterNetwork => IsInRange("10.0.0.0", "10.255.255.255", ipAddress) - || IsInRange("172.16.0.0", "172.31.255.255", ipAddress) - || IsInRange("172.16.0.0", "172.31.255.255", ipAddress) - || IsInRange("192.168.0.0", "192.168.255.255", ipAddress) - || IsInRange("169.254.0.0", "169.254.255.255", ipAddress) - || IsInRange("127.0.0.0", "127.255.255.255", ipAddress) - || IsInRange("0.0.0.0", "0.255.255.255", ipAddress), - _ => throw new ArgumentOutOfRangeException(nameof(ipAddress.AddressFamily), ipAddress.AddressFamily, null), - }; - - private static bool IsInRange(string startIpAddress, string endIpAddress, IPAddress address) - { - uint ipStart = BitConverter.ToUInt32(IPAddress.Parse(startIpAddress).GetAddressBytes().Reverse().ToArray(), 0); - uint ipEnd = BitConverter.ToUInt32(IPAddress.Parse(endIpAddress).GetAddressBytes().Reverse().ToArray(), 0); - uint ip = BitConverter.ToUInt32(address.GetAddressBytes().Reverse().ToArray(), 0); - - return ip >= ipStart && ip <= ipEnd; - } } \ No newline at end of file From 7003bbdaa5d78f9e1d5aedf8d1f221176f2aae61 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 31 Dec 2022 20:54:01 +0100 Subject: [PATCH 077/109] Update tunnel list uri --- ClientCore/ProgramConstants.cs | 2 +- DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index 78729756e..f9e4ec3e4 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -65,7 +65,7 @@ public static class ProgramConstants public const string INI_NEWLINE_PATTERN = "@"; public const string REPLAYS_DIRECTORY = "Replays"; - public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/master-list"; + public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/api/v1/master-list"; public const string CNCNET_DYNAMIC_TUNNELS = "DYNAMIC"; public const int GAME_ID_MAX_LENGTH = 4; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 7fbf9ffb3..1d0210186 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -30,7 +30,7 @@ internal sealed class CnCNetTunnel /// A CnCNetTunnel instance parsed from the given string. public static CnCNetTunnel Parse(string str) { - // For the format, check https://cncnet.org/master-list + // For the format, check https://cncnet.org/api/v1/master-list try { var tunnel = new CnCNetTunnel(); From 13f26351f75c7d02051a5b3016dba4f1954a9edc Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 1 Jan 2023 00:14:03 +0100 Subject: [PATCH 078/109] UPnP cleanup --- .../CnCNet/UPNP/InternetGatewayDevice.cs | 20 ++++++++--------- .../Actions/AddAnyPortMappingRequest.cs | 2 +- .../Actions/AddAnyPortMappingResponse.cs | 2 +- .../UPNP/Models/Actions/AddPinholeRequest.cs | 2 +- .../UPNP/Models/Actions/AddPinholeResponse.cs | 2 +- .../Models/Actions/AddPortMappingRequest.cs | 2 +- .../Models/Actions/AddPortMappingResponse.cs | 2 +- .../Models/Actions/DeletePinholeRequest.cs | 2 +- .../Models/Actions/DeletePinholeResponse.cs | 2 +- .../Actions/DeletePortMappingRequestV1.cs | 2 +- .../Actions/DeletePortMappingRequestV2.cs | 2 +- .../Actions/DeletePortMappingResponseV1.cs | 2 +- .../Actions/DeletePortMappingResponseV2.cs | 2 +- .../Actions/GetExternalIPAddressRequestV1.cs | 2 +- .../Actions/GetExternalIPAddressRequestV2.cs | 2 +- .../Actions/GetExternalIPAddressResponseV1.cs | 2 +- .../Actions/GetExternalIPAddressResponseV2.cs | 2 +- .../Actions/GetFirewallStatusRequest.cs | 2 +- .../Actions/GetFirewallStatusResponse.cs | 2 +- .../Actions/GetNatRsipStatusRequestV1.cs | 2 +- .../Actions/GetNatRsipStatusRequestV2.cs | 2 +- .../Actions/GetNatRsipStatusResponseV1.cs | 2 +- .../Actions/GetNatRsipStatusResponseV2.cs | 2 +- .../CnCNet/UPNP/{Models => }/UPnPConstants.cs | 8 +++++++ .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 16 +++++++++----- .../Multiplayer/CnCNet/V3ConnectionState.cs | 22 +++++++++++-------- .../Domain/Multiplayer/NetworkHelper.cs | 18 +++++++-------- 27 files changed, 72 insertions(+), 56 deletions(-) rename DXMainClient/Domain/Multiplayer/CnCNet/UPNP/{Models => }/UPnPConstants.cs (64%) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index a63b0818e..659f61623 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -86,7 +86,7 @@ public async Task OpenIpV4PortAsync(IPAddress ipAddress, ushort port, Ca case 2: var addAnyPortMappingRequest = new AddAnyPortMappingRequest(string.Empty, port, "UDP", port, ipAddress.ToString(), 1, PortMappingDescription, IpLeaseTimeInSeconds); AddAnyPortMappingResponse addAnyPortMappingResponse = await DoSoapActionAsync( - addAnyPortMappingRequest, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "AddAnyPortMapping", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); + addAnyPortMappingRequest, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", UPnPConstants.AddAnyPortMapping, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); port = addAnyPortMappingResponse.ReservedPort; @@ -121,14 +121,14 @@ public async Task CloseIpV4PortAsync(ushort port, CancellationToken cancellation var deletePortMappingRequestV2 = new DeletePortMappingRequestV2(string.Empty, port, "UDP"); await DoSoapActionAsync( - deletePortMappingRequestV2, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "DeletePortMapping", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); + deletePortMappingRequestV2, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", UPnPConstants.DeletePortMapping, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); break; case 1: var deletePortMappingRequestV1 = new DeletePortMappingRequestV1(string.Empty, port, "UDP"); await DoSoapActionAsync( - deletePortMappingRequestV1, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "DeletePortMapping", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); + deletePortMappingRequestV1, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", UPnPConstants.DeletePortMapping, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); break; default: @@ -156,14 +156,14 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance { case 2: GetExternalIPAddressResponseV2 getExternalIpAddressResponseV2 = await DoSoapActionAsync( - default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "GetExternalIPAddress", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); + default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", UPnPConstants.GetExternalIPAddress, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV2.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV2.ExternalIPAddress); break; case 1: GetExternalIPAddressResponseV1 getExternalIpAddressResponseV1 = await DoSoapActionAsync( - default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "GetExternalIPAddress", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); + default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", UPnPConstants.GetExternalIPAddress, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); ipAddress = string.IsNullOrWhiteSpace(getExternalIpAddressResponseV1.ExternalIPAddress) ? null : IPAddress.Parse(getExternalIpAddressResponseV1.ExternalIPAddress); break; @@ -193,14 +193,14 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance { case 2: GetNatRsipStatusResponseV2 getNatRsipStatusResponseV2 = await DoSoapActionAsync( - default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "GetNatRsipStatus", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); + default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", UPnPConstants.GetNatRsipStatus, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); natEnabled = getNatRsipStatusResponseV2.NatEnabled; break; case 1: GetNatRsipStatusResponseV1 getNatRsipStatusResponseV1 = await DoSoapActionAsync( - default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", "GetNatRsipStatus", AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); + default, $"{UPnPConstants.WanIpConnection}:{uPnPVersion}", UPnPConstants.GetNatRsipStatus, AddressFamily.InterNetwork, cancellationToken).ConfigureAwait(false); natEnabled = getNatRsipStatusResponseV1.NatEnabled; break; @@ -224,7 +224,7 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance Logger.Log($"P2P: Checking IPV6 firewall status on UPnP device {UPnPDescription.Device.FriendlyName}."); GetFirewallStatusResponse response = await DoSoapActionAsync( - default, $"{UPnPConstants.WanIpv6FirewallControl}:1", "GetFirewallStatus", AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); + default, $"{UPnPConstants.WanIpv6FirewallControl}:1", UPnPConstants.GetFirewallStatus, AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); Logger.Log($"P2P: Received IPV6 firewall status {response.FirewallEnabled} and port mapping allowed {response.InboundPinholeAllowed} on UPnP device {UPnPDescription.Device.FriendlyName}."); @@ -242,7 +242,7 @@ public async Task OpenIpV6PortAsync(IPAddress ipAddress, ushort port, Ca var request = new AddPinholeRequest(string.Empty, port, ipAddress.ToString(), port, IanaUdpProtocolNumber, IpLeaseTimeInSeconds); AddPinholeResponse response = await DoSoapActionAsync( - request, $"{UPnPConstants.WanIpv6FirewallControl}:1", "AddPinhole", AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); + request, $"{UPnPConstants.WanIpv6FirewallControl}:1", UPnPConstants.AddPinhole, AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); Logger.Log($"P2P: Opened IPV6 UDP port {port} with ID {response.UniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); @@ -255,7 +255,7 @@ public async Task CloseIpV6PortAsync(ushort uniqueId, CancellationToken cancella { Logger.Log($"P2P: Deleting IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); await DoSoapActionAsync( - new(uniqueId), $"{UPnPConstants.WanIpv6FirewallControl}:1", "DeletePinhole", AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); + new(uniqueId), $"{UPnPConstants.WanIpv6FirewallControl}:1", UPnPConstants.DeletePinhole, AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); Logger.Log($"P2P: Deleted IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); } catch (Exception ex) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingRequest.cs index bf3649545..4671c024e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingRequest.cs @@ -2,7 +2,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddAnyPortMapping", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] +[MessageContract(WrapperName = UPnPConstants.AddAnyPortMapping, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct AddAnyPortMappingRequest( [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingResponse.cs index 3881a4c4e..0a7251169 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddAnyPortMappingResponse.cs @@ -2,6 +2,6 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddAnyPortMappingResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] +[MessageContract(WrapperName = $"{UPnPConstants.AddAnyPortMapping}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct AddAnyPortMappingResponse( [property: MessageBodyMember(Name = "NewReservedPort")] ushort ReservedPort); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeRequest.cs index 46a0dea3f..7a24ff8b3 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeRequest.cs @@ -2,7 +2,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddPinhole", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] +[MessageContract(WrapperName = UPnPConstants.AddPinhole, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct AddPinholeRequest( [property: MessageBodyMember(Name = "RemoteHost")] string RemoteHost, [property: MessageBodyMember(Name = "RemotePort")] ushort RemotePort, // 0 = wildcard diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeResponse.cs index c020a6c38..f226e87b1 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPinholeResponse.cs @@ -2,6 +2,6 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddPinholeResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] +[MessageContract(WrapperName = $"{UPnPConstants.AddPinhole}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct AddPinholeResponse( [property: MessageBodyMember(Name = "UniqueID")] ushort UniqueId); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingRequest.cs index 7f0437304..1e146b10b 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingRequest.cs @@ -2,7 +2,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddPortMapping", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] +[MessageContract(WrapperName = UPnPConstants.AddPortMapping, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct AddPortMappingRequest( [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingResponse.cs index 0e2902e90..3c4307fca 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/AddPortMappingResponse.cs @@ -2,5 +2,5 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "AddPortMappingResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] +[MessageContract(WrapperName = $"{UPnPConstants.AddPortMapping}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct AddPortMappingResponse; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeRequest.cs index 38dbcc0ae..f9cd59697 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeRequest.cs @@ -2,6 +2,6 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePinhole", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] +[MessageContract(WrapperName = UPnPConstants.DeletePinhole, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct DeletePinholeRequest( [property: MessageBodyMember(Name = "UniqueID")] ushort UniqueId); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeResponse.cs index 5a370eaf3..fb143a6cf 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePinholeResponse.cs @@ -2,5 +2,5 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePinholeResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] +[MessageContract(WrapperName = $"{UPnPConstants.DeletePinhole}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct DeletePinholeResponse; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV1.cs index cfc78632d..bdb3477ac 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV1.cs @@ -2,7 +2,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePortMapping", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] +[MessageContract(WrapperName = UPnPConstants.DeletePortMapping, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct DeletePortMappingRequestV1( [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV2.cs index 107f32af2..882e73dc8 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingRequestV2.cs @@ -2,7 +2,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePortMapping", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] +[MessageContract(WrapperName = UPnPConstants.DeletePortMapping, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct DeletePortMappingRequestV2( [property: MessageBodyMember(Name = "NewRemoteHost")] string RemoteHost, // “x.x.x.x” or empty string [property: MessageBodyMember(Name = "NewExternalPort")] ushort ExternalPort, diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV1.cs index d3881a6b4..b7f7339b8 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV1.cs @@ -2,5 +2,5 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePortMappingResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] +[MessageContract(WrapperName = $"{UPnPConstants.DeletePortMapping}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct DeletePortMappingResponseV1; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV2.cs index 30c771cdd..5dbd5577d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/DeletePortMappingResponseV2.cs @@ -2,5 +2,5 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "DeletePortMappingResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] +[MessageContract(WrapperName = $"{UPnPConstants.DeletePortMapping}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct DeletePortMappingResponseV2; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV1.cs index 89f5df1fe..897bf7096 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV1.cs @@ -2,5 +2,5 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetExternalIPAddress", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] +[MessageContract(WrapperName = UPnPConstants.GetExternalIPAddress, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct GetExternalIPAddressRequestV1; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV2.cs index 51b25bd38..fc26e3646 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressRequestV2.cs @@ -2,5 +2,5 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetExternalIPAddress", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] +[MessageContract(WrapperName = UPnPConstants.GetExternalIPAddress, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct GetExternalIPAddressRequestV2; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV1.cs index e4b681487..a2870b23a 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV1.cs @@ -2,6 +2,6 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetExternalIPAddressResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] +[MessageContract(WrapperName = $"{UPnPConstants.GetExternalIPAddress}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] internal readonly record struct GetExternalIPAddressResponseV1( [property: MessageBodyMember(Name = "NewExternalIPAddress")] string ExternalIPAddress); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV2.cs index be90c3ae9..c3e3cc8ec 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetExternalIPAddressResponseV2.cs @@ -2,6 +2,6 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetExternalIPAddressResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] +[MessageContract(WrapperName = $"{UPnPConstants.GetExternalIPAddress}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] internal readonly record struct GetExternalIPAddressResponseV2( [property: MessageBodyMember(Name = "NewExternalIPAddress")] string ExternalIPAddress); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusRequest.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusRequest.cs index ce3793551..7f28fc9f7 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusRequest.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusRequest.cs @@ -2,5 +2,5 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetFirewallStatus", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] +[MessageContract(WrapperName = UPnPConstants.GetFirewallStatus, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct GetFirewallStatusRequest; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusResponse.cs index ccd4e62e3..5aff79d08 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetFirewallStatusResponse.cs @@ -2,7 +2,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetFirewallStatusResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] +[MessageContract(WrapperName = $"{UPnPConstants.GetFirewallStatus}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpv6FirewallControl}:1")] internal readonly record struct GetFirewallStatusResponse( [property: MessageBodyMember(Name = "FirewallEnabled")] bool FirewallEnabled, [property: MessageBodyMember(Name = "InboundPinholeAllowed")] bool InboundPinholeAllowed); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV1.cs index 1e8ab5a31..cef151497 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV1.cs @@ -2,5 +2,5 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetNatRsipStatusRequest", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] +[MessageContract(WrapperName = UPnPConstants.GetNatRsipStatus, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] public readonly record struct GetNatRsipStatusRequestV1; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV2.cs index 96d0fcf97..5dc11e891 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusRequestV2.cs @@ -2,5 +2,5 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetNatRsipStatusRequest", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] +[MessageContract(WrapperName = UPnPConstants.GetNatRsipStatus, WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] public readonly record struct GetNatRsipStatusRequestV2; \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV1.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV1.cs index 0cbb1814f..5a7558a9d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV1.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV1.cs @@ -2,7 +2,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetNatRsipStatusResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] +[MessageContract(WrapperName = $"{UPnPConstants.GetNatRsipStatus}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:1")] public readonly record struct GetNatRsipStatusResponseV1( [property: MessageBodyMember(Name = "NewRSIPAvailable")] bool RsipAvailable, [property: MessageBodyMember(Name = "NewNATEnabled")] bool NatEnabled); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV2.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV2.cs index 071937fb0..8a00e70a3 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV2.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/Actions/GetNatRsipStatusResponseV2.cs @@ -2,7 +2,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -[MessageContract(WrapperName = "GetNatRsipStatusResponse", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] +[MessageContract(WrapperName = $"{UPnPConstants.GetNatRsipStatus}Response", WrapperNamespace = $"{UPnPConstants.UPnPServiceNamespace}:{UPnPConstants.WanIpConnection}:2")] public readonly record struct GetNatRsipStatusResponseV2( [property: MessageBodyMember(Name = "NewRSIPAvailable")] bool RsipAvailable, [property: MessageBodyMember(Name = "NewNATEnabled")] bool NatEnabled); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPConstants.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPConstants.cs similarity index 64% rename from DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPConstants.cs rename to DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPConstants.cs index ff31c9963..bc7ab953d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/UPnPConstants.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPConstants.cs @@ -12,5 +12,13 @@ internal static class UPnPConstants public const string WanIpConnection = "WANIPConnection"; public const string WanIpv6FirewallControl = "WANIPv6FirewallControl"; public const string UPnPRootDevice = "upnp:rootdevice"; + public const string AddAnyPortMapping = "AddAnyPortMapping"; + public const string AddPinhole = "AddPinhole"; + public const string AddPortMapping = "AddPortMapping"; + public const string DeletePinhole = "DeletePinhole"; + public const string DeletePortMapping = "DeletePortMapping"; + public const string GetExternalIPAddress = "GetExternalIPAddress"; + public const string GetFirewallStatus = "GetFirewallStatus"; + public const string GetNatRsipStatus = "GetNatRsipStatus"; public const int UPnPMultiCastPort = 1900; } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index f70604ae9..4d1796180 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -63,12 +63,17 @@ private static IReadOnlyDictionary SsdpMultiCastAddresse internetGatewayDevice ??= await GetInternetGatewayDeviceAsync(cancellationToken).ConfigureAwait(false); - Task<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports)> ipV4Task = SetupIpV4PortsAsync(internetGatewayDevice, p2pReservedPorts, stunServerIpAddresses, cancellationToken); - Task<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports, List PortIds)> ipV6Task = SetupIpV6PortsAsync(internetGatewayDevice, p2pReservedPorts, stunServerIpAddresses, cancellationToken); + Task<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports)> ipV4Task = + SetupIpV4PortsAsync(internetGatewayDevice, p2pReservedPorts, stunServerIpAddresses, cancellationToken); + Task<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports, List PortIds)> ipV6Task = + SetupIpV6PortsAsync(internetGatewayDevice, p2pReservedPorts, stunServerIpAddresses, cancellationToken); await ClientCore.Extensions.TaskExtensions.WhenAllSafe(new Task[] { ipV4Task, ipV6Task }).ConfigureAwait(false); - return (internetGatewayDevice, ipV6Task.Result.Ports, ipV4Task.Result.Ports, ipV6Task.Result.PortIds, ipV6Task.Result.IpAddress, ipV4Task.Result.IpAddress); + (IPAddress publicIpV4Address, List<(ushort InternalPort, ushort ExternalPort)> ipV4P2PPorts) = await ipV4Task.ConfigureAwait(false); + (IPAddress publicIpV6Address, List<(ushort InternalPort, ushort ExternalPort)> ipV6P2PPorts, List ipV6P2PPortIds) = await ipV6Task.ConfigureAwait(false); + + return (internetGatewayDevice, ipV6P2PPorts, ipV4P2PPorts, ipV6P2PPortIds, publicIpV6Address, publicIpV4Address); } private static async Task GetInternetGatewayDeviceAsync(CancellationToken cancellationToken) @@ -151,8 +156,8 @@ private static async Task GetInternetGatewayDeviceAsync(C await ClientCore.Extensions.TaskExtensions.WhenAllSafe(new Task[] { natRsipStatusTask, externalIpv4AddressTask }).ConfigureAwait(false); - routerNatEnabled = natRsipStatusTask.Result; - routerPublicIpV4Address = externalIpv4AddressTask.Result; + routerNatEnabled = await natRsipStatusTask.ConfigureAwait(false); + routerPublicIpV4Address = await externalIpv4AddressTask.ConfigureAwait(false); } (IPAddress stunPublicIpV4Address, List<(ushort InternalPort, ushort ExternalPort)> ipV4StunPortMapping) = await NetworkHelper.PerformStunAsync( @@ -376,7 +381,6 @@ private static async Task GetInternetGatewayDeviceAsync( try { location = locations.First(q => q.HostNameType is UriHostNameType.IPv4); - uPnPDescription = await GetUPnPDescription(location, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index 8c6148fbd..5e789610e 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -6,7 +6,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using ClientCore; +using ClientCore.Extensions; using DTAClient.Domain.Multiplayer.CnCNet.Replays; using DTAClient.Domain.Multiplayer.CnCNet.UPNP; using Rampastring.Tools; @@ -146,15 +146,19 @@ public async ValueTask PingRemotePlayerAsync(string playerName, string p2p string[] splitLines = p2pRequestMessage.Split(';'); string[] ipV4splitLines = splitLines[0].Split('\t'); string[] ipV6splitLines = splitLines[1].Split('\t'); - (IPAddress remoteIpAddress, ushort[] remotePlayerIpV4Ports, long? ping) = await PingP2PAddressAsync(ipV4splitLines, playerName).ConfigureAwait(false); + Task<(IPAddress IpAddress, ushort[] Ports, long? Ping)> ipV4Task = PingP2PAddressAsync(ipV4splitLines, playerName); + Task<(IPAddress IpAddress, ushort[] Ports, long? Ping)> ipV6Task = PingP2PAddressAsync(ipV6splitLines, playerName); - if (ping is not null) - localPingResults.Add(new(remoteIpAddress, ping.Value)); + await ClientCore.Extensions.TaskExtensions.WhenAllSafe(new Task[] { ipV4Task, ipV6Task }).ConfigureAwait(false); - (remoteIpAddress, ushort[] remotePlayerIpV6Ports, ping) = await PingP2PAddressAsync(ipV6splitLines, playerName).ConfigureAwait(false); + (IPAddress remoteIpV4Address, ushort[] remoteIpV4Ports, long? ipV4Ping) = await ipV4Task.ConfigureAwait(false); + (IPAddress remoteIpV6Address, ushort[] remoteIpV6Ports, long? ipV6Ping) = await ipV6Task.ConfigureAwait(false); - if (ping is not null) - localPingResults.Add(new(remoteIpAddress, ping.Value)); + if (ipV4Ping is not null) + localPingResults.Add(new(remoteIpV4Address, ipV4Ping.Value)); + + if (ipV6Ping is not null) + localPingResults.Add(new(remoteIpV6Address, ipV6Ping.Value)); P2PPlayer remoteP2PPlayer; @@ -169,7 +173,7 @@ public async ValueTask PingRemotePlayerAsync(string playerName, string p2p remoteP2PPlayer = new(playerName, Array.Empty(), Array.Empty(), new(), new()); } - P2PPlayers.Add(remoteP2PPlayer with { LocalPingResults = localPingResults, RemoteIpV6Ports = remotePlayerIpV6Ports, RemoteIpV4Ports = remotePlayerIpV4Ports }); + P2PPlayers.Add(remoteP2PPlayer with { LocalPingResults = localPingResults, RemoteIpV6Ports = remoteIpV6Ports, RemoteIpV4Ports = remoteIpV4Ports }); return localPingResults.Any(); } @@ -393,7 +397,7 @@ public string HandleTunnelPingsMessage(string playerName, string tunnelPingsMess return hash; } - private static async ValueTask<(IPAddress IpAddress, ushort[] Ports, long? Ping)> PingP2PAddressAsync(IReadOnlyList ipAddressInfo, string playerName) + private static async Task<(IPAddress IpAddress, ushort[] Ports, long? Ping)> PingP2PAddressAsync(IReadOnlyList ipAddressInfo, string playerName) { if (!IPAddress.TryParse(ipAddressInfo[0], out IPAddress parsedIpAddress)) return new(null, Array.Empty(), null); diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 76b424384..07144c4e3 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -147,14 +147,14 @@ public static async ValueTask TracePublicIpV4Address(CancellationToke public static async ValueTask<(IPAddress IPAddress, List<(ushort InternalPort, ushort ExternalPort)> PortMapping)> PerformStunAsync( List stunServerIpAddresses, List p2pReservedPorts, AddressFamily addressFamily, CancellationToken cancellationToken) { - Logger.Log($"Using STUN to detect {addressFamily} address."); + Logger.Log($"P2P: Using STUN to detect {addressFamily} address."); var stunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); List matchingStunServerIpAddresses = stunServerIpAddresses.Where(q => q.AddressFamily == addressFamily).ToList(); if (!matchingStunServerIpAddresses.Any()) { - Logger.Log($"No {addressFamily} STUN servers found."); + Logger.Log($"P2P: No {addressFamily} STUN servers found."); return (null, stunPortMapping); } @@ -182,13 +182,13 @@ public static async ValueTask TracePublicIpV4Address(CancellationToke } if (stunPublicAddress is not null) - Logger.Log($"{addressFamily} STUN detection succeeded."); + Logger.Log($"P2P: {addressFamily} STUN detection succeeded."); else - Logger.Log($"{addressFamily} STUN detection failed."); + Logger.Log($"P2P: {addressFamily} STUN detection failed."); if (stunPortMapping.Any()) { - Logger.Log($"{addressFamily} STUN detection detected mapped ports, running STUN keep alive."); + Logger.Log($"P2P: {addressFamily} STUN detection detected mapped ports, running STUN keep alive."); #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed KeepStunAliveAsync( stunServerIpAddress, @@ -307,11 +307,11 @@ private static async ValueTask PerformStunAsync(IPAddress stunServer } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { - Logger.Log($"STUN server {stunServerIpEndPoint} unreachable."); + Logger.Log($"P2P: STUN server {stunServerIpEndPoint} unreachable."); } catch (Exception ex) { - ProgramConstants.LogException(ex, $"STUN server {stunServerIpEndPoint} unreachable."); + ProgramConstants.LogException(ex, $"P2P: STUN server {stunServerIpEndPoint} unreachable."); } } @@ -335,11 +335,11 @@ private static async Task KeepStunAliveAsync(IPAddress stunServerIpAddress, List } catch (OperationCanceledException) { - Logger.Log($"{stunServerIpAddress.AddressFamily} STUN keep alive stopped."); + Logger.Log($"P2P: {stunServerIpAddress.AddressFamily} STUN keep alive stopped."); } catch (Exception ex) { - ProgramConstants.LogException(ex, "STUN keep alive failed."); + ProgramConstants.LogException(ex, "P2P: STUN keep alive failed."); } } } \ No newline at end of file From e627b6fda38c02d7bb3b314c35ffd083d799f716 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 1 Jan 2023 13:11:58 +0100 Subject: [PATCH 079/109] Update year --- ClientCore/ClientCore.csproj | 2 +- ClientGUI/ClientGUI.csproj | 2 +- ClientGUI/Parser.cs | 22 +------------------ DTAConfig/DTAConfig.csproj | 2 +- DXMainClient/DXGUI/Multiplayer/GameListBox.cs | 2 -- DXMainClient/DXMainClient.csproj | 2 +- LICENSE.md | 4 ++-- 7 files changed, 7 insertions(+), 29 deletions(-) diff --git a/ClientCore/ClientCore.csproj b/ClientCore/ClientCore.csproj index 96160385b..9a70cd913 100644 --- a/ClientCore/ClientCore.csproj +++ b/ClientCore/ClientCore.csproj @@ -4,7 +4,7 @@ CnCNet Client Core Library CnCNet CnCNet Client - Copyright © CnCNet, Rampastring 2011-2022 + Copyright © CnCNet, Rampastring 2011-2023 CnCNet 2.0.0.3 2.0.0.3 diff --git a/ClientGUI/ClientGUI.csproj b/ClientGUI/ClientGUI.csproj index 3e41f5b84..da1883b52 100644 --- a/ClientGUI/ClientGUI.csproj +++ b/ClientGUI/ClientGUI.csproj @@ -4,7 +4,7 @@ CnCNet Client UI Library CnCNet CnCNet Client - Copyright © CnCNet, Rampastring 2011-2022 + Copyright © CnCNet, Rampastring 2011-2023 CnCNet 2.1.0.1 2.1.0.1 diff --git a/ClientGUI/Parser.cs b/ClientGUI/Parser.cs index bd23ca5b3..476822201 100644 --- a/ClientGUI/Parser.cs +++ b/ClientGUI/Parser.cs @@ -1,24 +1,4 @@ -/********************************************************************* -* Dawn of the Tiberium Age MonoGame/XNA CnCNet Client -* Expression Parser -* Copyright (C) Rampastring 2022 -* -* The CnCNet Client is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* The CnCNet Client is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program.If not, see. -* -*********************************************************************/ - -using ClientCore; +using ClientCore; using Rampastring.Tools; using Rampastring.XNAUI; using Rampastring.XNAUI.XNAControls; diff --git a/DTAConfig/DTAConfig.csproj b/DTAConfig/DTAConfig.csproj index a401e6a18..413b3ebfb 100644 --- a/DTAConfig/DTAConfig.csproj +++ b/DTAConfig/DTAConfig.csproj @@ -4,7 +4,7 @@ CnCNet Config Library CnCNet CnCNet Client - Copyright © CnCNet, Rampastring 2011-2022 + Copyright © CnCNet, Rampastring 2011-2023 CnCNet 2.2.0.0 2.2.0.0 diff --git a/DXMainClient/DXGUI/Multiplayer/GameListBox.cs b/DXMainClient/DXGUI/Multiplayer/GameListBox.cs index eb5f7df3d..ad1f97218 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameListBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameListBox.cs @@ -236,8 +236,6 @@ private void AddGameToList(GenericHostedGame hg) if (hg.Game.InternalName != localGameIdentifier.ToLower()) lbItem.TextColor = UISettings.ActiveSettings.TextColor; - //else // made unnecessary by new Rampastring.XNAUI - // lbItem.TextColor = UISettings.ActiveSettings.AltColor; if (hg.Incompatible || hg.Locked) { diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index 1f571622d..d29520b4a 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -6,7 +6,7 @@ CnCNet Main Client Library CnCNet CnCNet Client - Copyright © CnCNet, Rampastring 2011-2022 + Copyright © CnCNet, Rampastring 2011-2023 CnCNet 2.8.0.0 2.8.0.0 diff --git a/LICENSE.md b/LICENSE.md index ded950670..c17a6ed40 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,5 +1,5 @@ CnCNet Client -Copyright (C) 2022 CnCNet, Rampastring +Copyright (C) 2023 CnCNet, Rampastring This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ along with this program. If not, see . This software includes Rampastring.Tools and Rampastring.XNAUI. Their license follows: -Copyright (c) 2022 Rami "Rampastring" Pasanen +Copyright (c) 2023 Rami "Rampastring" Pasanen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: From 5810228f583a2a689a774509ba144c8b1a1d9cbf Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 3 Jan 2023 23:36:27 +0100 Subject: [PATCH 080/109] Socket timeout handling, update dependencies --- .github/workflows/build.yml | 2 +- .github/workflows/pr-build-comment.yml | 2 +- ClientCore/ProgramConstants.cs | 2 +- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 40 +++++--- .../Multiplayer/CnCNet/TunnelHandler.cs | 40 ++++---- .../CnCNet/UPNP/InternetGatewayDevice.cs | 30 +++--- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 96 +++++++++++-------- .../CnCNet/V3RemotePlayerConnection.cs | 2 + .../Domain/Multiplayer/LAN/LANPlayerInfo.cs | 27 ++---- .../Domain/Multiplayer/NetworkHelper.cs | 2 +- 10 files changed, 134 insertions(+), 109 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d68b4682..61bdb1605 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: Game: [Ares,TS,YR] steps: - - uses: actions/checkout@v3.1.0 + - uses: actions/checkout@v3.2.0 with: fetch-depth: 0 diff --git a/.github/workflows/pr-build-comment.yml b/.github/workflows/pr-build-comment.yml index eabd96a0c..9e933ccef 100644 --- a/.github/workflows/pr-build-comment.yml +++ b/.github/workflows/pr-build-comment.yml @@ -8,7 +8,7 @@ jobs: if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-22.04 steps: - - uses: actions/github-script@v6.3.1 + - uses: actions/github-script@v6.3.3 with: # This snippet is public-domain, taken from # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index f9e4ec3e4..b93fee17a 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -65,7 +65,7 @@ public static class ProgramConstants public const string INI_NEWLINE_PATTERN = "@"; public const string REPLAYS_DIRECTORY = "Replays"; - public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/api/v1/master-list"; + public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/api/v1/master-list?nocache=1"; public const string CNCNET_DYNAMIC_TUNNELS = "DYNAMIC"; public const int GAME_ID_MAX_LENGTH = 4; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 1d0210186..28a4e133c 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Net; using System.Net.Sockets; +using System.Threading; using System.Threading.Tasks; using ClientCore; using Rampastring.Tools; @@ -30,7 +31,7 @@ internal sealed class CnCNetTunnel /// A CnCNetTunnel instance parsed from the given string. public static CnCNetTunnel Parse(string str) { - // For the format, check https://cncnet.org/api/v1/master-list + // For the format, check https://cncnet.org/api/v1/master-list?nocache=1 try { var tunnel = new CnCNetTunnel(); @@ -60,8 +61,10 @@ public static CnCNetTunnel Parse(string str) } else { - throw new($"No supported IP address found ({nameof(Socket.OSSupportsIPv6)}={Socket.OSSupportsIPv6}," + - $" {nameof(Socket.OSSupportsIPv4)}={Socket.OSSupportsIPv4}) for {str}."); + throw new($""" + No supported IP address found ({nameof(Socket.OSSupportsIPv6)}={Socket.OSSupportsIPv6}, + {nameof(Socket.OSSupportsIPv4)}={Socket.OSSupportsIPv4}) for {str}. + """); } tunnel.IPAddresses = new List { primaryIpAddress }; @@ -189,32 +192,47 @@ public async ValueTask> GetPlayerPortInfoAsync(int playerCount) return new List(); } - public async ValueTask UpdatePingAsync() + public async ValueTask UpdatePingAsync(CancellationToken cancellationToken) { using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); - socket.SendTimeout = PING_TIMEOUT; - socket.ReceiveTimeout = PING_TIMEOUT; - try { EndPoint ep = new IPEndPoint(IPAddress, Port); using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(PING_PACKET_SEND_SIZE); Memory buffer = memoryOwner.Memory[..PING_PACKET_SEND_SIZE]; + + buffer.Span.Clear(); + long ticks = DateTime.Now.Ticks; + using var sendTimeoutCancellationTokenSource = new CancellationTokenSource(PING_TIMEOUT); + using var sendLinkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(sendTimeoutCancellationTokenSource.Token, cancellationToken); - await socket.SendToAsync(buffer, SocketFlags.None, ep).ConfigureAwait(false); - await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep).ConfigureAwait(false); + await socket.SendToAsync(buffer, SocketFlags.None, ep, sendLinkedCancellationTokenSource.Token).ConfigureAwait(false); + + using var receiveTimeoutCancellationTokenSource = new CancellationTokenSource(PING_TIMEOUT); + using var receiveLinkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(receiveTimeoutCancellationTokenSource.Token, cancellationToken); + + await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep, receiveLinkedCancellationTokenSource.Token).ConfigureAwait(false); ticks = DateTime.Now.Ticks - ticks; PingInMs = new TimeSpan(ticks).Milliseconds; + + return; } catch (SocketException ex) { ProgramConstants.LogException(ex, $"Failed to ping tunnel {Name} ({Address}:{Port})."); - - PingInMs = -1; } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + Logger.Log($"Failed to ping tunnel (time-out) {Name} ({Address}:{Port})."); + } + catch (OperationCanceledException) + { + } + + PingInMs = -1; } } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index f65d864ce..382759bfe 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using ClientCore; using ClientCore.Extensions; @@ -70,13 +71,13 @@ private void DoCurrentTunnelPinged() private void ConnectionManager_Disconnected(object sender, EventArgs e) => Enabled = false; - private async ValueTask RefreshTunnelsAsync() + private async ValueTask RefreshTunnelsAsync(CancellationToken cancellationToken) { - List tunnels = await DoRefreshTunnelsAsync().ConfigureAwait(false); - wm.AddCallback(() => HandleRefreshedTunnels(tunnels)); + List tunnels = await DoRefreshTunnelsAsync(cancellationToken).ConfigureAwait(false); + wm.AddCallback(() => HandleRefreshedTunnels(tunnels, cancellationToken)); } - private void HandleRefreshedTunnels(List tunnels) + private void HandleRefreshedTunnels(List tunnels, CancellationToken cancellationToken) { if (tunnels.Count > 0) Tunnels = tunnels; @@ -86,7 +87,7 @@ private void HandleRefreshedTunnels(List tunnels) for (int i = 0; i < Tunnels.Count; i++) { if (UserINISettings.Instance.PingUnofficialCnCNetTunnels || Tunnels[i].Official || Tunnels[i].Recommended) - PingListTunnelAsync(i).HandleTask(); + PingListTunnelAsync(i, cancellationToken).HandleTask(); } if (CurrentTunnel != null) @@ -103,28 +104,25 @@ private void HandleRefreshedTunnels(List tunnels) else { // tunnel is not in the list anymore so it's not updated with a list instance and pinged - PingCurrentTunnel(); + PingCurrentTunnelAsync(false, cancellationToken).HandleTask(); } } } - private async ValueTask PingListTunnelAsync(int index) + private async ValueTask PingListTunnelAsync(int index, CancellationToken cancellationToken) { - await Tunnels[index].UpdatePingAsync().ConfigureAwait(false); + await Tunnels[index].UpdatePingAsync(cancellationToken).ConfigureAwait(false); DoTunnelPinged(index); } - private void PingCurrentTunnel(bool checkTunnelList = false) - => PingCurrentTunnelAsync(checkTunnelList).HandleTask(); - - private async ValueTask PingCurrentTunnelAsync(bool checkTunnelList = false) + private async ValueTask PingCurrentTunnelAsync(bool checkTunnelList, CancellationToken cancellationToken) { - await CurrentTunnel.UpdatePingAsync().ConfigureAwait(false); + await CurrentTunnel.UpdatePingAsync(cancellationToken).ConfigureAwait(false); DoCurrentTunnelPinged(); if (checkTunnelList) { - int tunnelIndex = Tunnels.FindIndex(t => t.Hash.Equals(CurrentTunnel.Hash)); + int tunnelIndex = Tunnels.FindIndex(t => t.Hash.Equals(CurrentTunnel.Hash, StringComparison.OrdinalIgnoreCase)); if (tunnelIndex > -1) DoTunnelPinged(tunnelIndex); @@ -135,7 +133,7 @@ private async ValueTask PingCurrentTunnelAsync(bool checkTunnelList = false) /// Downloads and parses the list of CnCNet tunnels. /// /// A list of tunnel servers. - private static async ValueTask> DoRefreshTunnelsAsync() + private static async ValueTask> DoRefreshTunnelsAsync(CancellationToken cancellationToken) { FileInfo tunnelCacheFile = SafePath.GetFile(ProgramConstants.ClientUserFilesPath, "tunnel_cache"); var returnValue = new List(); @@ -145,14 +143,14 @@ private static async ValueTask> DoRefreshTunnelsAsync() try { - data = await Constants.CnCNetHttpClient.GetStringAsync(new Uri(ProgramConstants.CNCNET_TUNNEL_LIST_URL)).ConfigureAwait(false); + data = await Constants.CnCNetHttpClient.GetStringAsync(new Uri(ProgramConstants.CNCNET_TUNNEL_LIST_URL), cancellationToken).ConfigureAwait(false); } catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException) { ProgramConstants.LogException(ex, "Error when downloading tunnel server info. Retrying."); try { - data = await Constants.CnCNetHttpClient.GetStringAsync(new Uri(ProgramConstants.CNCNET_TUNNEL_LIST_URL)).ConfigureAwait(false); + data = await Constants.CnCNetHttpClient.GetStringAsync(new Uri(ProgramConstants.CNCNET_TUNNEL_LIST_URL), cancellationToken).ConfigureAwait(false); } catch (Exception ex1) when (ex1 is HttpRequestException or OperationCanceledException) { @@ -164,7 +162,7 @@ private static async ValueTask> DoRefreshTunnelsAsync() } Logger.Log("Fetching tunnel server list failed. Using cached tunnel data."); - data = await File.ReadAllTextAsync(tunnelCacheFile.FullName).ConfigureAwait(false); + data = await File.ReadAllTextAsync(tunnelCacheFile.FullName, cancellationToken).ConfigureAwait(false); } } @@ -213,7 +211,7 @@ private static async ValueTask> DoRefreshTunnelsAsync() if (!clientDirectoryInfo.Exists) clientDirectoryInfo.Create(); - await File.WriteAllTextAsync(tunnelCacheFile.FullName, data).ConfigureAwait(false); + await File.WriteAllTextAsync(tunnelCacheFile.FullName, data, CancellationToken.None).ConfigureAwait(false); } catch (Exception ex) { @@ -230,11 +228,11 @@ public override void Update(GameTime gameTime) if (skipCount % CYCLES_PER_TUNNEL_LIST_REFRESH == 0) { skipCount = 0; - RefreshTunnelsAsync().HandleTask(); + RefreshTunnelsAsync(CancellationToken.None).HandleTask(); } else if (CurrentTunnel != null) { - PingCurrentTunnel(true); + PingCurrentTunnelAsync(true, CancellationToken.None).HandleTask(); } timeSinceTunnelRefresh = TimeSpan.Zero; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index 659f61623..8fb6b7ca7 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -77,7 +77,7 @@ internal sealed record InternetGatewayDevice( public async Task OpenIpV4PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) { - Logger.Log($"P2P: Opening IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Opening IPV4 UDP port {port}."); int uPnPVersion = GetDeviceUPnPVersion(); @@ -102,7 +102,7 @@ await DoSoapActionAsync( throw new ArgumentException($"P2P: UPnP version {uPnPVersion} is not supported."); } - Logger.Log($"P2P: Opened IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Opened IPV4 UDP port {port}."); return port; } @@ -111,7 +111,7 @@ public async Task CloseIpV4PortAsync(ushort port, CancellationToken cancellation { try { - Logger.Log($"P2P: Deleting IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Deleting IPV4 UDP port {port}."); int uPnPVersion = GetDeviceUPnPVersion(); @@ -135,7 +135,7 @@ await DoSoapActionAsync throw new ArgumentException($"P2P: UPnP version {uPnPVersion} is not supported."); } - Logger.Log($"P2P: Deleted IPV4 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Deleted IPV4 UDP port {port}."); } catch (Exception ex) { @@ -145,7 +145,7 @@ await DoSoapActionAsync public async Task GetExternalIpV4AddressAsync(CancellationToken cancellationToken) { - Logger.Log($"P2P: Requesting external IP address from UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log("P2P: Requesting external IP address."); int uPnPVersion = GetDeviceUPnPVersion(); IPAddress ipAddress = null; @@ -171,7 +171,7 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance throw new ArgumentException($"P2P: UPnP version {uPnPVersion} is not supported."); } - Logger.Log($"P2P: Received external IP address {ipAddress} from UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Received external IP address {ipAddress}."); } catch { @@ -182,7 +182,7 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance public async Task GetNatRsipStatusAsync(CancellationToken cancellationToken) { - Logger.Log($"P2P: Checking NAT status on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log("P2P: Checking NAT status."); int uPnPVersion = GetDeviceUPnPVersion(); bool? natEnabled = null; @@ -208,7 +208,7 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance throw new ArgumentException($"P2P: UPnP version {uPnPVersion} is not supported."); } - Logger.Log($"P2P: Received NAT status {natEnabled} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Received NAT status {natEnabled}."); } catch { @@ -221,12 +221,12 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance { try { - Logger.Log($"P2P: Checking IPV6 firewall status on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log("P2P: Checking IPV6 firewall status."); GetFirewallStatusResponse response = await DoSoapActionAsync( default, $"{UPnPConstants.WanIpv6FirewallControl}:1", UPnPConstants.GetFirewallStatus, AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); - Logger.Log($"P2P: Received IPV6 firewall status {response.FirewallEnabled} and port mapping allowed {response.InboundPinholeAllowed} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Received IPV6 firewall status '{response.FirewallEnabled}' and port mapping allowed '{response.InboundPinholeAllowed}'."); return (response.FirewallEnabled, response.InboundPinholeAllowed); } @@ -238,13 +238,13 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance public async Task OpenIpV6PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) { - Logger.Log($"P2P: Opening IPV6 UDP port {port} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Opening IPV6 UDP port {port}."); var request = new AddPinholeRequest(string.Empty, port, ipAddress.ToString(), port, IanaUdpProtocolNumber, IpLeaseTimeInSeconds); AddPinholeResponse response = await DoSoapActionAsync( request, $"{UPnPConstants.WanIpv6FirewallControl}:1", UPnPConstants.AddPinhole, AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); - Logger.Log($"P2P: Opened IPV6 UDP port {port} with ID {response.UniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Opened IPV6 UDP port {port} with ID {response.UniqueId}."); return response.UniqueId; } @@ -253,10 +253,10 @@ public async Task CloseIpV6PortAsync(ushort uniqueId, CancellationToken cancella { try { - Logger.Log($"P2P: Deleting IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Deleting IPV6 UDP port with ID {uniqueId}."); await DoSoapActionAsync( new(uniqueId), $"{UPnPConstants.WanIpv6FirewallControl}:1", UPnPConstants.DeletePinhole, AddressFamily.InterNetworkV6, cancellationToken).ConfigureAwait(false); - Logger.Log($"P2P: Deleted IPV6 UDP port with ID {uniqueId} on UPnP device {UPnPDescription.Device.FriendlyName}."); + Logger.Log($"P2P: Deleted IPV6 UDP port with ID {uniqueId}."); } catch (Exception ex) { @@ -276,7 +276,7 @@ private async ValueTask DoSoapActionAsync( } catch (Exception ex) when (ex is not OperationCanceledException) { - ProgramConstants.LogException(ex, $"P2P: {action} error/not supported on UPnP device {UPnPDescription.Device.FriendlyName} using {addressFamily}."); + ProgramConstants.LogException(ex, $"P2P: {action} error/not supported using {addressFamily}."); throw; } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 4d1796180..628fd4a90 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -78,15 +78,33 @@ private static IReadOnlyDictionary SsdpMultiCastAddresse private static async Task GetInternetGatewayDeviceAsync(CancellationToken cancellationToken) { - var internetGatewayDevices = (await GetInternetGatewayDevicesAsync(cancellationToken).ConfigureAwait(false)).ToList(); - InternetGatewayDevice internetGatewayDevice = GetInternetGatewayDevice(internetGatewayDevices, 2); + var internetGatewayDevices = (await GetInternetGatewayDevices(cancellationToken).ConfigureAwait(false)).ToList(); - internetGatewayDevice ??= GetInternetGatewayDevice(internetGatewayDevices, 1); + foreach (InternetGatewayDevice internetGatewayDevice in internetGatewayDevices) + { + Logger.Log($""" + P2P: Found gateway device v{internetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()} + {internetGatewayDevice.UPnPDescription.Device.FriendlyName} ({internetGatewayDevice.Server}). + """); + } - if (internetGatewayDevice is not null) - Logger.Log($"P2P: Found NAT device {internetGatewayDevice.UPnPDescription.Device.DeviceType} - {internetGatewayDevice.Server} ({internetGatewayDevice.UPnPDescription.Device.FriendlyName})."); + InternetGatewayDevice selectedInternetGatewayDevice = GetInternetGatewayDeviceByVersion(internetGatewayDevices, 2); + + selectedInternetGatewayDevice ??= GetInternetGatewayDeviceByVersion(internetGatewayDevices, 1); + + if (selectedInternetGatewayDevice is not null) + { + Logger.Log($""" + P2P: Selected gateway device v{selectedInternetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()} + {selectedInternetGatewayDevice.UPnPDescription.Device.FriendlyName} ({selectedInternetGatewayDevice.Server}). + """); + } + else + { + Logger.Log("P2P: No gateway devices detected."); + } - return internetGatewayDevice; + return selectedInternetGatewayDevice; } private static async Task<(IPAddress IpAddress, List<(ushort InternalPort, ushort ExternalPort)> Ports, List PortIds)> SetupIpV6PortsAsync( @@ -221,32 +239,34 @@ private static async Task GetInternetGatewayDeviceAsync(C return (publicIpV4Address, ipV4P2PPorts); } - private static async ValueTask> GetInternetGatewayDevicesAsync(CancellationToken cancellationToken) + private static async ValueTask> GetInternetGatewayDevices(CancellationToken cancellationToken) + { + IEnumerable devices = await GetDevicesAsync(cancellationToken).ConfigureAwait(false); + + return devices.Where(q => q.UPnPDescription.Device.DeviceType.StartsWith($"{UPnPConstants.UPnPInternetGatewayDevice}:", StringComparison.OrdinalIgnoreCase)); + } + + private static InternetGatewayDevice GetInternetGatewayDeviceByVersion(List internetGatewayDevices, ushort uPnPVersion) + => internetGatewayDevices.FirstOrDefault(q => $"{UPnPConstants.UPnPInternetGatewayDevice}:{uPnPVersion}".Equals(q.UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase)); + + private static async ValueTask> GetDevicesAsync(CancellationToken cancellationToken) { - IEnumerable rawDeviceResponses = await GetRawDeviceResponses(cancellationToken).ConfigureAwait(false); + IEnumerable rawDeviceResponses = await DetectDevicesAsync(cancellationToken).ConfigureAwait(false); IEnumerable> formattedDeviceResponses = GetFormattedDeviceResponses(rawDeviceResponses); IEnumerable> groupedInternetGatewayDeviceResponses = - GetGroupedInternetGatewayDeviceResponses(formattedDeviceResponses); + GetGroupedDeviceResponses(formattedDeviceResponses); return await ClientCore.Extensions.TaskExtensions.WhenAllSafe( - groupedInternetGatewayDeviceResponses.Select(q => GetInternetGatewayDeviceAsync(q, cancellationToken))).ConfigureAwait(false); + groupedInternetGatewayDeviceResponses.Select(q => ParseDeviceAsync(q, cancellationToken))).ConfigureAwait(false); } - private static InternetGatewayDevice GetInternetGatewayDevice(List internetGatewayDevices, ushort uPnPVersion) - => internetGatewayDevices.SingleOrDefault(q => $"{UPnPConstants.UPnPInternetGatewayDevice}:{uPnPVersion}".Equals(q.UPnPDescription.Device.DeviceType, StringComparison.OrdinalIgnoreCase)); - - private static IEnumerable> GetGroupedInternetGatewayDeviceResponses( - IEnumerable> formattedDeviceResponses) - { - return formattedDeviceResponses + private static IEnumerable> GetGroupedDeviceResponses(IEnumerable> formattedDeviceResponses) + => formattedDeviceResponses .Select(q => new InternetGatewayDeviceResponse(new(q["LOCATION"]), q["SERVER"], q["CACHE-CONTROL"], q["EXT"], q["ST"], q["USN"])) .GroupBy(q => q.Usn); - } private static Uri GetPreferredLocation(IReadOnlyCollection locations) - { - return locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6) ?? locations.First(q => q.HostNameType is UriHostNameType.IPv4); - } + => locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6) ?? locations.First(q => q.HostNameType is UriHostNameType.IPv4); private static IEnumerable> GetFormattedDeviceResponses(IEnumerable responses) { @@ -276,8 +296,6 @@ private static async Task> SearchDevicesAsync(IPAddress loca try { - socket.ExclusiveAddressUse = true; - socket.Bind(new IPEndPoint(localAddress, 0)); var multiCastIpEndPoint = new IPEndPoint(SsdpMultiCastAddresses[addressType], UPnPConstants.UPnPMultiCastPort); @@ -286,9 +304,9 @@ private static async Task> SearchDevicesAsync(IPAddress loca int bufferSize = request.Length * charSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); Memory buffer = memoryOwner.Memory[..bufferSize]; - int bytes = Encoding.UTF8.GetBytes(request.AsSpan(), buffer.Span); + int numberOfBytes = Encoding.UTF8.GetBytes(request.AsSpan(), buffer.Span); - buffer = buffer[..bytes]; + buffer = buffer[..numberOfBytes]; for (int i = 0; i < SendCount; i++) { @@ -308,7 +326,7 @@ private static async Task> SearchDevicesAsync(IPAddress loca private static AddressType GetAddressType(IPAddress localAddress) { - if (localAddress.AddressFamily == AddressFamily.InterNetwork) + if (localAddress.AddressFamily is AddressFamily.InterNetwork) return AddressType.IpV4SiteLocal; if (localAddress.IsIPv6LinkLocal) @@ -342,7 +360,7 @@ private static async ValueTask ReceiveAsync(Socket socket, ICollection r } } - private static async ValueTask GetUPnPDescription(Uri uri, CancellationToken cancellationToken) + private static async ValueTask GetDescriptionAsync(Uri uri, CancellationToken cancellationToken) { Stream uPnPDescription = await HttpClient.GetStreamAsync(uri, cancellationToken).ConfigureAwait(false); @@ -354,7 +372,7 @@ private static async ValueTask GetUPnPDescription(Uri uri, Canc } } - private static async ValueTask> GetRawDeviceResponses(CancellationToken cancellationToken) + private static async ValueTask> DetectDevicesAsync(CancellationToken cancellationToken) { IEnumerable localAddresses = NetworkHelper.GetLocalAddresses(); IEnumerable[] localAddressesDeviceResponses = await ClientCore.Extensions.TaskExtensions.WhenAllSafe( @@ -363,7 +381,7 @@ private static async ValueTask> GetRawDeviceResponses(Cancel return localAddressesDeviceResponses.Where(q => q.Any()).SelectMany(q => q).Distinct(); } - private static async Task GetInternetGatewayDeviceAsync( + private static async Task ParseDeviceAsync( IGrouping internetGatewayDeviceResponses, CancellationToken cancellationToken) { Uri[] locations = internetGatewayDeviceResponses.Select(r => r.Location).ToArray(); @@ -372,7 +390,7 @@ private static async Task GetInternetGatewayDeviceAsync( try { - uPnPDescription = await GetUPnPDescription(location, cancellationToken).ConfigureAwait(false); + uPnPDescription = await GetDescriptionAsync(location, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { @@ -381,7 +399,7 @@ private static async Task GetInternetGatewayDeviceAsync( try { location = locations.First(q => q.HostNameType is UriHostNameType.IPv4); - uPnPDescription = await GetUPnPDescription(location, cancellationToken).ConfigureAwait(false); + uPnPDescription = await GetDescriptionAsync(location, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { @@ -390,13 +408,13 @@ private static async Task GetInternetGatewayDeviceAsync( } return new( - internetGatewayDeviceResponses.Select(r => r.Location).Distinct(), - internetGatewayDeviceResponses.Select(r => r.Server).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.CacheControl).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.Ext).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.SearchTarget).Distinct().Single(), - internetGatewayDeviceResponses.Key, - uPnPDescription, - location); + internetGatewayDeviceResponses.Select(r => r.Location).Distinct(), + internetGatewayDeviceResponses.Select(r => r.Server).Distinct().Single(), + internetGatewayDeviceResponses.Select(r => r.CacheControl).Distinct().Single(), + internetGatewayDeviceResponses.Select(r => r.Ext).Distinct().Single(), + internetGatewayDeviceResponses.Select(r => r.SearchTarget).Distinct().Single(), + internetGatewayDeviceResponses.Key, + uPnPDescription, + location); } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index 2a33b7577..119ec7040 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -69,6 +69,8 @@ protected override async ValueTask DoStartConnectionAsync() using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(MaximumPacketSize); Memory buffer = memoryOwner.Memory[..MaximumPacketSize]; + buffer.Span.Clear(); + if (!BitConverter.TryWriteBytes(buffer.Span[..PlayerIdSize], PlayerId)) throw new GameDataException(); diff --git a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs index 2d7f6cb7e..dcc1dd013 100644 --- a/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs +++ b/DXMainClient/Domain/Multiplayer/LAN/LANPlayerInfo.cs @@ -2,7 +2,6 @@ using Microsoft.Xna.Framework; using System; using System.Buffers; -using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; @@ -24,6 +23,7 @@ public LANPlayerInfo(Encoding encoding) private const double SEND_PING_TIMEOUT = 10.0; private const double DROP_TIMEOUT = 20.0; + private const int SEND_TIMEOUT = 1000; public TimeSpan TimeSinceLastReceivedMessage { get; set; } public TimeSpan TimeSinceLastSentMessage { get; set; } @@ -40,7 +40,6 @@ public void SetClient(Socket client) throw new InvalidOperationException("TcpClient has already been set for this LANPlayerInfo!"); TcpClient = client; - TcpClient.SendTimeout = 1000; } /// @@ -92,17 +91,16 @@ public async ValueTask SendMessageAsync(string message, CancellationToken cancel using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); Memory buffer = memoryOwner.Memory[..bufferSize]; int bytes = encoding.GetBytes(message.AsSpan(), buffer.Span); + using var timeoutCancellationTokenSource = new CancellationTokenSource(SEND_TIMEOUT); + using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); buffer = buffer[..bytes]; try { - await TcpClient.SendAsync(buffer, cancellationToken).ConfigureAwait(false); + await TcpClient.SendAsync(buffer, linkedCancellationTokenSource.Token).ConfigureAwait(false); } - catch (OperationCanceledException) - { - } - catch (SocketException) + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } catch (Exception ex) @@ -125,7 +123,7 @@ public async ValueTask StartReceiveLoopAsync(CancellationToken cancellationToken while (!cancellationToken.IsCancellationRequested) { - int bytesRead; + int bytesRead = 0; Memory message = memoryOwner.Memory[..4096]; try @@ -134,19 +132,10 @@ public async ValueTask StartReceiveLoopAsync(CancellationToken cancellationToken } catch (OperationCanceledException) { - ConnectionLost?.Invoke(this, EventArgs.Empty); - break; - } - catch (SocketException) - { - ConnectionLost?.Invoke(this, EventArgs.Empty); - break; } catch (Exception ex) { - ProgramConstants.LogException(ex, "Socket error with client " + Name + "; removing."); - ConnectionLost?.Invoke(this, EventArgs.Empty); - break; + ProgramConstants.LogException(ex, "Connection error with client " + Name + "; removing."); } if (bytesRead > 0) @@ -157,7 +146,7 @@ public async ValueTask StartReceiveLoopAsync(CancellationToken cancellationToken while (true) { - int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR); + int index = msg.IndexOf(ProgramConstants.LAN_MESSAGE_SEPARATOR, StringComparison.OrdinalIgnoreCase); if (index == -1) { diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 07144c4e3..17f938da3 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -182,7 +182,7 @@ public static async ValueTask TracePublicIpV4Address(CancellationToke } if (stunPublicAddress is not null) - Logger.Log($"P2P: {addressFamily} STUN detection succeeded."); + Logger.Log($"P2P: {addressFamily} STUN detection succeeded using server {stunServerIpAddress}."); else Logger.Log($"P2P: {addressFamily} STUN detection failed."); From 7868567201f679b2c6da694c8ce26948c81e6522 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Thu, 5 Jan 2023 13:38:04 +0100 Subject: [PATCH 081/109] Fix tunnel start race condition --- .../DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 253f79882..66da84e72 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -919,20 +919,18 @@ private async ValueTask GameTunnelHandler_Connected_CallbackAsync() if (v3ConnectionState.DynamicTunnelsEnabled) { if (v3ConnectionState.V3GameTunnelHandlers.Any() && v3ConnectionState.V3GameTunnelHandlers.TrueForAll(q => q.Tunnel.ConnectSucceeded)) - SetLocalPlayerConnected(); + await SetLocalPlayerConnectedAsync().ConfigureAwait(false); } else { - SetLocalPlayerConnected(); + await SetLocalPlayerConnectedAsync().ConfigureAwait(false); } await channel.SendCTCPMessageAsync(CnCNetCommands.TUNNEL_CONNECTION_OK, QueuedMessageType.SYSTEM_MESSAGE, PRIORITY_START_GAME).ConfigureAwait(false); } - private void SetLocalPlayerConnected() - { - isPlayerConnected[Players.FindIndex(p => p == FindLocalPlayer())] = true; - } + private ValueTask SetLocalPlayerConnectedAsync() + => HandlePlayerConnectedToTunnelAsync(FindLocalPlayer().Name); private async ValueTask GameTunnelHandler_ConnectionFailed_CallbackAsync() { From 81b7e2f80837364aebc76f0410fdd7ae2aacb2b9 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 6 Jan 2023 21:29:35 +0100 Subject: [PATCH 082/109] IPv6 cross-platform & multi-adapter improvements --- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 2 +- .../CnCNet/Replays/ReplayHandler.cs | 34 ++++---- .../CnCNet/UPNP/InternetGatewayDevice.cs | 54 ++---------- .../Models/InternetGatewayDeviceResponse.cs | 3 +- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 85 ++++++++++++++----- .../CnCNet/V3RemotePlayerConnection.cs | 10 +-- .../Domain/Multiplayer/NetworkHelper.cs | 32 ++++--- DXMainClient/Resources/run.sh | 2 - DXMainClient/Resources/wine-dta.sh | 2 + Docs/Migration.md | 6 +- 10 files changed, 123 insertions(+), 107 deletions(-) delete mode 100755 DXMainClient/Resources/run.sh create mode 100644 DXMainClient/Resources/wine-dta.sh diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 22217738f..06ed24262 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -295,7 +295,7 @@ public async ValueTask OpenAsync() Logger.Log("Creating LAN socket."); - IEnumerable lanIpAddresses = NetworkHelper.GetUniCastIpAddresses(); + IEnumerable lanIpAddresses = NetworkHelper.GetUniCastIpAddresses().Select(q => q.UnicastIPAddressInformation); UnicastIPAddressInformation lanIpV4Address = lanIpAddresses.FirstOrDefault(q => q.Address.AddressFamily is AddressFamily.InterNetwork); lanIpV4BroadcastIpAddress = NetworkHelper.GetIpV4BroadcastAddress(lanIpV4Address); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs index ec16a6d85..9ca898c5c 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs @@ -48,26 +48,30 @@ public async ValueTask StopRecordingAsync(List gamePlayerIds, List q.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)).Index]; - var playerMappings = new Dictionary - { - { playerId, playerName } - }; + string settings = null; + Dictionary playerMappings = new(); - for (int i = 1; i < settingsSection.GetIntValue("PlayerCount", 0); i++) + if (spawnFile.Exists) { - IniSection otherPlayerSection = spawnIni.GetSection($"Other{i}"); + settings = await File.ReadAllTextAsync(spawnFile.FullName, CancellationToken.None).ConfigureAwait(false); + var spawnIni = new IniFile(spawnFile.FullName); + IniSection settingsSection = spawnIni.GetSection("Settings"); + string playerName = settingsSection.GetStringValue("Name", null); + uint playerId = gamePlayerIds[playerInfos.Single(q => q.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)).Index]; + + playerMappings.Add(playerId, playerName); - if (otherPlayerSection is not null) + for (int i = 1; i < settingsSection.GetIntValue("PlayerCount", 0); i++) { - playerName = otherPlayerSection.GetStringValue("Name", null); - playerId = gamePlayerIds[playerInfos.Single(q => q.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)).Index]; + IniSection otherPlayerSection = spawnIni.GetSection($"Other{i}"); - playerMappings.Add(playerId, playerName); + if (otherPlayerSection is not null) + { + playerName = otherPlayerSection.GetStringValue("Name", null); + playerId = gamePlayerIds[playerInfos.Single(q => q.Name.Equals(playerName, StringComparison.OrdinalIgnoreCase)).Index]; + + playerMappings.Add(playerId, playerName); + } } } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index 8fb6b7ca7..06fb78a4b 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -26,55 +26,13 @@ internal sealed record InternetGatewayDevice( string SearchTarget, string UniqueServiceName, UPnPDescription UPnPDescription, - Uri PreferredLocation) + Uri PreferredLocation, + IReadOnlyCollection LocalIpAddresses) { - private const int ReceiveTimeout = 2000; private const uint IpLeaseTimeInSeconds = 4 * 60 * 60; private const ushort IanaUdpProtocolNumber = 17; private const string PortMappingDescription = "CnCNet"; - private static readonly HttpClient HttpClient = new( - new SocketsHttpHandler - { - AutomaticDecompression = DecompressionMethods.All, - ConnectCallback = async (context, token) => - { - Socket socket = null; - - try - { - socket = new(SocketType.Stream, ProtocolType.Tcp) - { - NoDelay = true - }; - - if (IPAddress.Parse(context.DnsEndPoint.Host).AddressFamily is AddressFamily.InterNetworkV6) - socket.Bind(new IPEndPoint(NetworkHelper.GetLocalPublicIpV6Address(), 0)); - - await socket.ConnectAsync(context.DnsEndPoint, token).ConfigureAwait(false); - - return new NetworkStream(socket, true); - } - catch - { - socket?.Dispose(); - - throw; - } - }, - SslOptions = new() - { - CertificateChainPolicy = new() - { - DisableCertificateDownloads = true - } - } - }, true) - { - Timeout = TimeSpan.FromMilliseconds(ReceiveTimeout), - DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher - }; - public async Task OpenIpV4PortAsync(IPAddress ipAddress, ushort port, CancellationToken cancellationToken) { Logger.Log($"P2P: Opening IPV4 UDP port {port}."); @@ -285,8 +243,8 @@ private async ValueTask DoSoapActionAsync( private static async ValueTask ExecuteSoapAction( Uri serviceUri, string soapAction, string defaultNamespace, TRequest request, CancellationToken cancellationToken) { - HttpClient.DefaultRequestHeaders.Remove("SOAPAction"); - HttpClient.DefaultRequestHeaders.Add("SOAPAction", soapAction); + UPnPHandler.HttpClient.DefaultRequestHeaders.Remove("SOAPAction"); + UPnPHandler.HttpClient.DefaultRequestHeaders.Add("SOAPAction", soapAction); var xmlSerializerFormatAttribute = new XmlSerializerFormatAttribute { @@ -321,7 +279,7 @@ private static async ValueTask ExecuteSoapAction content.Headers.ContentType = MediaTypeHeaderValue.Parse("text/xml"); - httpResponseMessage = await HttpClient.PostAsync(serviceUri, content, cancellationToken).ConfigureAwait(false); + httpResponseMessage = await UPnPHandler.HttpClient.PostAsync(serviceUri, content, cancellationToken).ConfigureAwait(false); } using (httpResponseMessage) @@ -368,7 +326,7 @@ AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNa Device wanConnectionDevice = wanDevice.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPConstants.UPnPWanConnectionDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); string serviceType = $"{UPnPConstants.UPnPServiceNamespace}:{wanConnectionDeviceService}"; ServiceListItem wanIpConnectionService = wanConnectionDevice.ServiceList.Single(q => q.ServiceType.Equals(serviceType, StringComparison.OrdinalIgnoreCase)); - var serviceUri = new Uri(FormattableString.Invariant($"{location.Scheme}://{location.Authority}{wanIpConnectionService.ControlUrl}")); + var serviceUri = new Uri(FormattableString.Invariant($"{location.Scheme}://{(location.HostNameType is UriHostNameType.IPv6 ? '[' : null)}{location.IdnHost}{(location.HostNameType is UriHostNameType.IPv6 ? ']' : null)}:{location.Port}{wanIpConnectionService.ControlUrl}")); return new(wanIpConnectionService, serviceUri, serviceType); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs index ec2ad610e..f0c62e257 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs @@ -1,5 +1,6 @@ using System; +using System.Net; namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -internal readonly record struct InternetGatewayDeviceResponse(Uri Location, string Server, string CacheControl, string Ext, string SearchTarget, string Usn); \ No newline at end of file +internal readonly record struct InternetGatewayDeviceResponse(Uri Location, string Server, string CacheControl, string Ext, string SearchTarget, string Usn, IPAddress LocalIpAddress); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 628fd4a90..a5f80cc54 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -21,10 +21,35 @@ internal static class UPnPHandler private const int ReceiveTimeout = 2000; private const int SendCount = 3; - private static readonly HttpClient HttpClient = new( + public static readonly HttpClient HttpClient = new( new SocketsHttpHandler { AutomaticDecompression = DecompressionMethods.All, + ConnectCallback = async (context, token) => + { + Socket socket = null; + + try + { + socket = new(SocketType.Stream, ProtocolType.Tcp) + { + NoDelay = true + }; + + if (IPAddress.Parse(context.DnsEndPoint.Host).AddressFamily is AddressFamily.InterNetworkV6) + socket.Bind(new IPEndPoint(NetworkHelper.GetLocalPublicIpV6Address(), 0)); + + await socket.ConnectAsync(context.DnsEndPoint, token).ConfigureAwait(false); + + return new NetworkStream(socket, true); + } + catch + { + socket?.Dispose(); + + throw; + } + }, SslOptions = new() { CertificateChainPolicy = new() @@ -243,7 +268,7 @@ private static async ValueTask> GetInternetGa { IEnumerable devices = await GetDevicesAsync(cancellationToken).ConfigureAwait(false); - return devices.Where(q => q.UPnPDescription.Device.DeviceType.StartsWith($"{UPnPConstants.UPnPInternetGatewayDevice}:", StringComparison.OrdinalIgnoreCase)); + return devices.Where(q => q.UPnPDescription.Device.DeviceType?.StartsWith($"{UPnPConstants.UPnPInternetGatewayDevice}:", StringComparison.OrdinalIgnoreCase) ?? false); } private static InternetGatewayDevice GetInternetGatewayDeviceByVersion(List internetGatewayDevices, ushort uPnPVersion) @@ -251,8 +276,9 @@ private static InternetGatewayDevice GetInternetGatewayDeviceByVersion(List> GetDevicesAsync(CancellationToken cancellationToken) { - IEnumerable rawDeviceResponses = await DetectDevicesAsync(cancellationToken).ConfigureAwait(false); - IEnumerable> formattedDeviceResponses = GetFormattedDeviceResponses(rawDeviceResponses); + IEnumerable<(IPAddress LocalIpAddress, IEnumerable Responses)> rawDeviceResponses = await DetectDevicesAsync(cancellationToken).ConfigureAwait(false); + IEnumerable<(IPAddress LocalIpAddress, IEnumerable> Responses)> formattedDeviceResponses = + rawDeviceResponses.Select(q => (q.LocalIpAddress, GetFormattedDeviceResponses(q.Responses))); IEnumerable> groupedInternetGatewayDeviceResponses = GetGroupedDeviceResponses(formattedDeviceResponses); @@ -260,9 +286,10 @@ private static async ValueTask> GetDevicesAsy groupedInternetGatewayDeviceResponses.Select(q => ParseDeviceAsync(q, cancellationToken))).ConfigureAwait(false); } - private static IEnumerable> GetGroupedDeviceResponses(IEnumerable> formattedDeviceResponses) + private static IEnumerable> GetGroupedDeviceResponses( + IEnumerable<(IPAddress LocalIpAddress, IEnumerable> Responses)> formattedDeviceResponses) => formattedDeviceResponses - .Select(q => new InternetGatewayDeviceResponse(new(q["LOCATION"]), q["SERVER"], q["CACHE-CONTROL"], q["EXT"], q["ST"], q["USN"])) + .SelectMany(q => q.Responses.Select(r => new InternetGatewayDeviceResponse(new(r["LOCATION"]), r["SERVER"], r["CACHE-CONTROL"], r["EXT"], r["ST"], r["USN"], q.LocalIpAddress))) .GroupBy(q => q.Usn); private static Uri GetPreferredLocation(IReadOnlyCollection locations) @@ -284,13 +311,13 @@ private static IEnumerable> GetFormattedDeviceRespons StringComparer.OrdinalIgnoreCase)); } - private static async Task> SearchDevicesAsync(IPAddress localAddress, CancellationToken cancellationToken) + private static async Task<(IPAddress LocalIpAddress, IEnumerable Responses)> SearchDevicesAsync(IPAddress localAddress, CancellationToken cancellationToken) { var responses = new List(); AddressType addressType = GetAddressType(localAddress); if (addressType is AddressType.Unknown) - return responses; + return new(localAddress, responses); var socket = new Socket(localAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); @@ -321,7 +348,7 @@ private static async Task> SearchDevicesAsync(IPAddress loca socket.Close(); } - return responses; + return new(localAddress, responses); } private static AddressType GetAddressType(IPAddress localAddress) @@ -372,34 +399,34 @@ private static async ValueTask GetDescriptionAsync(Uri uri, Can } } - private static async ValueTask> DetectDevicesAsync(CancellationToken cancellationToken) + private static async ValueTask Responses)>> DetectDevicesAsync(CancellationToken cancellationToken) { IEnumerable localAddresses = NetworkHelper.GetLocalAddresses(); - IEnumerable[] localAddressesDeviceResponses = await ClientCore.Extensions.TaskExtensions.WhenAllSafe( + (IPAddress LocalIpAddress, IEnumerable Responses)[] localAddressesDeviceResponses = await ClientCore.Extensions.TaskExtensions.WhenAllSafe( localAddresses.Select(q => SearchDevicesAsync(q, cancellationToken))).ConfigureAwait(false); - return localAddressesDeviceResponses.Where(q => q.Any()).SelectMany(q => q).Distinct(); + return localAddressesDeviceResponses.Where(q => q.Responses.Any(r => r.Any())).Select(q => (q.LocalIpAddress, q.Responses)).Distinct(); } private static async Task ParseDeviceAsync( IGrouping internetGatewayDeviceResponses, CancellationToken cancellationToken) { - Uri[] locations = internetGatewayDeviceResponses.Select(r => r.Location).ToArray(); - Uri location = GetPreferredLocation(locations); + Uri[] locations = internetGatewayDeviceResponses.Select(q => (q.LocalIpAddress, q.Location)).Distinct().Select(ParseLocation).ToArray(); + Uri preferredLocation = GetPreferredLocation(locations); UPnPDescription uPnPDescription = default; try { - uPnPDescription = await GetDescriptionAsync(location, cancellationToken).ConfigureAwait(false); + uPnPDescription = await GetDescriptionAsync(preferredLocation, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { - if (location.HostNameType is UriHostNameType.IPv6 && locations.Any(q => q.HostNameType is UriHostNameType.IPv4)) + if (preferredLocation.HostNameType is UriHostNameType.IPv6 && locations.Any(q => q.HostNameType is UriHostNameType.IPv4)) { try { - location = locations.First(q => q.HostNameType is UriHostNameType.IPv4); - uPnPDescription = await GetDescriptionAsync(location, cancellationToken).ConfigureAwait(false); + preferredLocation = locations.First(q => q.HostNameType is UriHostNameType.IPv4); + uPnPDescription = await GetDescriptionAsync(preferredLocation, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { @@ -408,13 +435,31 @@ private static async Task ParseDeviceAsync( } return new( - internetGatewayDeviceResponses.Select(r => r.Location).Distinct(), + locations, internetGatewayDeviceResponses.Select(r => r.Server).Distinct().Single(), internetGatewayDeviceResponses.Select(r => r.CacheControl).Distinct().Single(), internetGatewayDeviceResponses.Select(r => r.Ext).Distinct().Single(), internetGatewayDeviceResponses.Select(r => r.SearchTarget).Distinct().Single(), internetGatewayDeviceResponses.Key, uPnPDescription, - location); + preferredLocation, + internetGatewayDeviceResponses.Select(r => r.LocalIpAddress).Distinct().ToList().AsReadOnly()); + } + + private static Uri ParseLocation((IPAddress LocalIpAddress, Uri Location) location) + { + if (location.Location.HostNameType is not UriHostNameType.IPv6 || !IPAddress.TryParse(location.Location.Host, out IPAddress ipAddress) + || !NetworkHelper.IsPrivateIpAddress(ipAddress)) + { + return location.Location; + } + + var uriBuilder = new UriBuilder(location.Location); + + uriBuilder.Host = FormattableString.Invariant($"[{uriBuilder.Host + .Replace("[", null, StringComparison.OrdinalIgnoreCase) + .Replace("]", null, StringComparison.OrdinalIgnoreCase)}%{location.LocalIpAddress.ScopeId}]"); + + return uriBuilder.Uri; } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index 119ec7040..98c887696 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -59,7 +59,7 @@ protected override async ValueTask DoStartConnectionAsync() #if DEBUG Logger.Log($"{GetType().Name}: Attempting to establish a connection from port {localPort} to {RemoteEndPoint})."); #else - Logger.Log($"{GetType().Name}: Attempting to establish a connection using {localPort})."); + Logger.Log($"{GetType().Name}: Attempting to establish a connection on port {localPort}."); #endif Socket = new(SocketType.Dgram, ProtocolType.Udp); @@ -86,7 +86,7 @@ protected override async ValueTask DoStartConnectionAsync() #if DEBUG ProgramConstants.LogException(ex, $"Failed to establish connection from port {localPort} to {RemoteEndPoint}."); #else - ProgramConstants.LogException(ex, $"Failed to establish connection using {localPort}."); + ProgramConstants.LogException(ex, $"Failed to establish connection on port {localPort}."); #endif OnRaiseConnectionFailedEvent(EventArgs.Empty); @@ -101,7 +101,7 @@ protected override async ValueTask DoStartConnectionAsync() #if DEBUG Logger.Log($"{GetType().Name}: Failed to establish connection (time out) from port {localPort} to {RemoteEndPoint}."); #else - Logger.Log($"{GetType().Name}: Failed to establish connection (time out) using {localPort}."); + Logger.Log($"{GetType().Name}: Failed to establish connection (time out) on port {localPort}."); #endif OnRaiseConnectionFailedEvent(EventArgs.Empty); @@ -111,7 +111,7 @@ protected override async ValueTask DoStartConnectionAsync() #if DEBUG Logger.Log($"{GetType().Name}: Connection from {Socket.LocalEndPoint} to {RemoteEndPoint} established."); #else - Logger.Log($"{GetType().Name}: Connection using {localPort} established."); + Logger.Log($"{GetType().Name}: Connection on port {localPort} established."); #endif OnRaiseConnectedEvent(EventArgs.Empty); } @@ -126,7 +126,7 @@ protected override DataReceivedEventArgs ProcessReceivedData(Memory buffer #if DEBUG Logger.Log($"{GetType().Name}: Invalid data packet from {socketReceiveFromResult.RemoteEndPoint}"); #else - Logger.Log($"{GetType().Name}: Invalid data packet on {localPort}"); + Logger.Log($"{GetType().Name}: Invalid data packet on port {localPort}"); #endif return null; } diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 17f938da3..dd49b2bdb 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -26,15 +26,9 @@ internal static class NetworkHelper AddressFamily.InterNetworkV6 }.AsReadOnly(); - [SupportedOSPlatform("windows")] - public static IEnumerable<(IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin)> GetWindowsPublicIpAddresses() - => GetUniCastIpAddresses() - .Where(q => !IsPrivateIpAddress(q.Address)) - .Select(q => (q.Address, q.PrefixOrigin, q.SuffixOrigin)); - public static IEnumerable GetLocalAddresses() => GetUniCastIpAddresses() - .Select(q => q.Address); + .Select(q => q.UnicastIPAddressInformation.Address); public static IEnumerable GetPublicIpAddresses() => GetLocalAddresses() @@ -44,13 +38,23 @@ public static IEnumerable GetPrivateIpAddresses() => GetLocalAddresses() .Where(IsPrivateIpAddress); - public static IEnumerable GetUniCastIpAddresses() + public static IEnumerable<(UnicastIPAddressInformation UnicastIPAddressInformation, GatewayIPAddressInformation GatewayIPAddressInformation)> GetUniCastIpAddresses() + => GetIpInterfaces() + .Where(q => q.GatewayAddresses.Any()) + .SelectMany(q => q.UnicastAddresses.Select( + r => (UnicastIPAddressInformation: r, GatewayIPAddressInformation: q.GatewayAddresses.FirstOrDefault(s => s.Address.AddressFamily == r.Address.AddressFamily)))) + .Where(q => SupportedAddressFamilies.Contains(q.UnicastIPAddressInformation.Address.AddressFamily)); + + private static IEnumerable GetIpInterfaces() => NetworkInterface.GetAllNetworkInterfaces() .Where(q => q.OperationalStatus is OperationalStatus.Up) - .Select(q => q.GetIPProperties()) - .Where(q => q.GatewayAddresses.Any()) - .SelectMany(q => q.UnicastAddresses) - .Where(q => SupportedAddressFamilies.Contains(q.Address.AddressFamily)); + .Select(q => q.GetIPProperties()); + + [SupportedOSPlatform("windows")] + private static IEnumerable<(IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin)> GetWindowsPublicIpAddresses() + => GetUniCastIpAddresses() + .Where(q => !IsPrivateIpAddress(q.UnicastIPAddressInformation.Address)) + .Select(q => (q.UnicastIPAddressInformation.Address, q.UnicastIPAddressInformation.PrefixOrigin, q.UnicastIPAddressInformation.SuffixOrigin)); public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unicastIpAddressInformation) { @@ -150,7 +154,7 @@ public static async ValueTask TracePublicIpV4Address(CancellationToke Logger.Log($"P2P: Using STUN to detect {addressFamily} address."); var stunPortMapping = new List<(ushort InternalPort, ushort ExternalPort)>(); - List matchingStunServerIpAddresses = stunServerIpAddresses.Where(q => q.AddressFamily == addressFamily).ToList(); + var matchingStunServerIpAddresses = stunServerIpAddresses.Where(q => q.AddressFamily == addressFamily).ToList(); if (!matchingStunServerIpAddresses.Any()) { @@ -230,7 +234,7 @@ public static IEnumerable GetFreeUdpPorts(IEnumerable excludedPo } } - private static bool IsPrivateIpAddress(IPAddress ipAddress) + public static bool IsPrivateIpAddress(IPAddress ipAddress) => ipAddress.AddressFamily switch { AddressFamily.InterNetworkV6 => ipAddress.IsIPv6SiteLocal diff --git a/DXMainClient/Resources/run.sh b/DXMainClient/Resources/run.sh deleted file mode 100755 index ec5aa9316..000000000 --- a/DXMainClient/Resources/run.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -env MONO_IOMAP=all mono --debug DTAClient.exe \ No newline at end of file diff --git a/DXMainClient/Resources/wine-dta.sh b/DXMainClient/Resources/wine-dta.sh new file mode 100644 index 000000000..3a60fa6ac --- /dev/null +++ b/DXMainClient/Resources/wine-dta.sh @@ -0,0 +1,2 @@ +#!/bin/sh +wine gamemd-spawn.exe $* \ No newline at end of file diff --git a/Docs/Migration.md b/Docs/Migration.md index 4734fed6c..ccd993923 100644 --- a/Docs/Migration.md +++ b/Docs/Migration.md @@ -13,7 +13,11 @@ Migrating from older versions - Second-stage updater is now maintained as a separate project. Download the latest release [here](https://github.com/CnCNet/cncnet-client-updater/releases) (select `SecondStageUpdater-.zip`), and extract its contents to the client's `Resources\Updater` folder. -- To support launching the game on Linux the file defined as `UnixGameExecutableName` (defaults to `wine-dta.sh`) in `ClientDefinitions.ini` must be set up correctly. E.g. for launching a game with wine the file could contain `wine gamemd-spawn.exe $*` where `gamemd-spawn.exe` is replaced with the game executable. Note that users might need to execute `chmod u+x wine-dta.sh` once to allow it to be launched. +- To support launching the game on Linux the file defined as `UnixGameExecutableName` (defaults to `wine-dta.sh`) in `ClientDefinitions.ini` must be set up correctly. E.g. for launching a game with wine the file could contain the below, where `gamemd-spawn.exe` is replaced with the game executable. Note that users might need to execute `chmod u+x wine-dta.sh` once to allow it to be launched. +``` +#!/bin/sh +wine gamemd-spawn.exe $* +``` - The use of `*.cur` mouse cursor files is not supported on the cross-platform `UniversalGL` build. To ensure the intended cursor is shown instead of a missing texture (pink square) all themes need to contain a `cursor.png` file. Existing `*.cur` files will still be used by the Windows-only builds. From 0682b0bfdf89e1fc73c649603eb475ae570f28a7 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 7 Jan 2023 12:00:19 +0100 Subject: [PATCH 083/109] Handle tunnel race condition --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 12 ++++++--- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 25 ++++++++++++++----- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 4 ++- .../Multiplayer/CnCNet/PlayerConnection.cs | 11 +++++++- .../Multiplayer/CnCNet/V3ConnectionState.cs | 5 ++-- .../CnCNet/V3RemotePlayerConnection.cs | 2 ++ DXMainClient/Online/Connection.cs | 7 ++++-- 7 files changed, 50 insertions(+), 16 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 66da84e72..78bdfc281 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -119,7 +119,7 @@ public CnCNetGameLobby( new StringCommandHandler(CnCNetCommands.PLAYER_EXTRA_OPTIONS, ApplyPlayerExtraOptions), new StringCommandHandler(CnCNetCommands.GAME_OPTIONS, (playerName, message) => ApplyGameOptionsAsync(playerName, message).HandleTask()), new StringCommandHandler(CnCNetCommands.GAME_START_V2, (playerName, message) => ClientLaunchGameV2Async(playerName, message).HandleTask()), - new StringCommandHandler(CnCNetCommands.GAME_START_V3, ClientLaunchGameV3Async), + new StringCommandHandler(CnCNetCommands.GAME_START_V3, ClientLaunchGameV3), new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_OK, playerName => HandlePlayerConnectedToTunnelAsync(playerName).HandleTask()), new NoParamCommandHandler(CnCNetCommands.TUNNEL_CONNECTION_FAIL, playerName => HandleTunnelFailAsync(playerName).HandleTask()), new NotificationHandler(CnCNetCommands.AI_SPECTATORS, HandleNotification, () => AISpectatorsNotificationAsync().HandleTask()), @@ -721,7 +721,11 @@ private void Channel_ChannelModesChanged(object sender, ChannelModeEventArgs e) private void Channel_CTCPReceived(object sender, ChannelCTCPEventArgs e) { +#if DEBUG + Logger.Log($"CnCNetGameLobby_CTCPReceived from {e.UserName}: {e.Message}"); +#else Logger.Log("CnCNetGameLobby_CTCPReceived"); +#endif foreach (CommandHandlerBase cmdHandler in ctcpCommandHandlers) { @@ -857,7 +861,7 @@ private string HostGenerateGamePlayerIds() return sb.ToString(); } - private void ClientLaunchGameV3Async(string sender, string message) + private void ClientLaunchGameV3(string sender, string message) { if (!sender.Equals(hostName, StringComparison.OrdinalIgnoreCase)) return; @@ -2030,13 +2034,13 @@ private void HandleTunnelPingsMessage(string playerName, string tunnelPingsMessa if (selectedTunnelHash is null) { - AddNotice(string.Format(CultureInfo.CurrentCulture, "No common tunnel found for: {0}".L10N("Client:Main:NoCommonTunnel"), playerName)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "No common dynamic tunnel found for: {0}".L10N("Client:Main:NoCommonDynamicTunnel"), playerName)); } else { CnCNetTunnel tunnel = tunnelHandler.Tunnels.Single(q => q.Hash.Equals(selectedTunnelHash, StringComparison.OrdinalIgnoreCase)); - AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} dynamic tunnel: {1} ({2}ms)".L10N("Client:Main:TunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} dynamic tunnel: {1} ({2}ms)".L10N("Client:Main:DynamicTunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); } } diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 06ed24262..c1e38fc56 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -298,19 +298,30 @@ public async ValueTask OpenAsync() IEnumerable lanIpAddresses = NetworkHelper.GetUniCastIpAddresses().Select(q => q.UnicastIPAddressInformation); UnicastIPAddressInformation lanIpV4Address = lanIpAddresses.FirstOrDefault(q => q.Address.AddressFamily is AddressFamily.InterNetwork); + if (lanIpV4Address is null) + { + Logger.Log("No IPv4 address found for LAN."); + lbChatMessages.AddMessage(new ChatMessage(Color.Red, "No IPv4 address found for LAN".L10N("Client:Main:NoLANIPv4"))); + + initSuccess = false; + return; + } + lanIpV4BroadcastIpAddress = NetworkHelper.GetIpV4BroadcastAddress(lanIpV4Address); try { - socket = new Socket(SocketType.Dgram, ProtocolType.Udp) + socket = new(SocketType.Dgram, ProtocolType.Udp) { EnableBroadcast = true }; socket.Bind(new IPEndPoint(lanIpV4Address.Address, ProgramConstants.LAN_LOBBY_PORT)); - endPoint = new IPEndPoint(lanIpV4BroadcastIpAddress, ProgramConstants.LAN_LOBBY_PORT); + endPoint = new(lanIpV4BroadcastIpAddress, ProgramConstants.LAN_LOBBY_PORT); initSuccess = true; + + Logger.Log($"Created LAN broadcast socket {socket.LocalEndPoint} / {endPoint}."); } catch (SocketException ex) { @@ -320,8 +331,10 @@ public async ValueTask OpenAsync() lbChatMessages.AddMessage(new ChatMessage(Color.Red, "Please check your firewall settings.".L10N("Client:Main:SocketFailure2"))); lbChatMessages.AddMessage(new ChatMessage(Color.Red, - $"Also make sure that no other application is listening to traffic on UDP ports" + - $" {ProgramConstants.LAN_LOBBY_PORT} - {ProgramConstants.LAN_INGAME_PORT}.".L10N("Client:Main:SocketFailure3"))); + $""" + Also make sure that no other application is listening to traffic on UDP ports + {ProgramConstants.LAN_LOBBY_PORT} - {ProgramConstants.LAN_INGAME_PORT}. + """.L10N("Client:Main:SocketFailure3"))); initSuccess = false; return; @@ -369,7 +382,7 @@ private async ValueTask ListenAsync(CancellationToken cancellationToken) var iep = (IPEndPoint)socketReceiveFromResult.RemoteEndPoint; string data = encoding.GetString(buffer.Span[..socketReceiveFromResult.ReceivedBytes]); - if (data == string.Empty) + if (string.IsNullOrEmpty(data)) continue; AddCallback(() => HandleNetworkMessage(data, iep)); @@ -601,7 +614,7 @@ private async ValueTask BtnMainMenu_LeftClickAsync() Enabled = false; await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None).ConfigureAwait(false); cancellationTokenSource.Cancel(); - socket.Close(); + socket?.Close(); Exited?.Invoke(this, EventArgs.Empty); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 28a4e133c..5de192277 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -61,10 +61,12 @@ public static CnCNetTunnel Parse(string str) } else { - throw new($""" + Logger.Log($""" No supported IP address found ({nameof(Socket.OSSupportsIPv6)}={Socket.OSSupportsIPv6}, {nameof(Socket.OSSupportsIPv4)}={Socket.OSSupportsIPv4}) for {str}. """); + + return null; } tunnel.IPAddresses = new List { primaryIpAddress }; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs index 819f40900..a5753eae8 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs @@ -19,6 +19,7 @@ internal abstract class PlayerConnection : IDisposable protected CancellationToken CancellationToken; protected Socket Socket; protected EndPoint RemoteEndPoint; + protected bool Connected; public uint PlayerId { get; protected set; } @@ -71,8 +72,14 @@ protected async ValueTask SendDataAsync(ReadOnlyMemory data) { #if DEBUG Logger.Log($"{GetType().Name}: Sending data from {Socket.LocalEndPoint} to {RemoteEndPoint} for player {PlayerId}: {BitConverter.ToString(data.Span.ToArray())}."); + +#endif + if (Connected) + await Socket.SendToAsync(data, SocketFlags.None, RemoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); +#if DEBUG + else + Logger.Log($"{GetType().Name}: Data not sent (not yet connected) from {Socket.LocalEndPoint} to {RemoteEndPoint} for player {PlayerId}."); #endif - await Socket.SendToAsync(data, SocketFlags.None, RemoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); } catch (SocketException ex) { @@ -176,6 +183,8 @@ private void OnRaiseConnectionCutEvent(EventArgs e) private void OnRaiseDataReceivedEvent(DataReceivedEventArgs e) { + Connected = true; + EventHandler raiseEvent = RaiseDataReceivedEvent; raiseEvent?.Invoke(this, e); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index 5e789610e..b8356c10d 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -16,7 +16,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet; internal sealed class V3ConnectionState : IAsyncDisposable { private const ushort MAX_REMOTE_PLAYERS = 7; - private const int PINNED_DYNAMIC_TUNNELS = 10; + private const int PINNED_DYNAMIC_TUNNELS = 15; private readonly TunnelHandler tunnelHandler; private readonly List<(string RemotePlayerName, CnCNetTunnel Tunnel, int CombinedPing)> playerTunnels = new(); @@ -260,8 +260,9 @@ public void StartV3ConnectionListeners( .Contains(q.RemoteIpAddress.AddressFamily)) .Select(q => (q.RemoteIpAddress, q.Ping + remotePingResults.Single(r => r.RemoteIpAddress.AddressFamily == q.RemoteIpAddress.AddressFamily).Ping)) .MaxBy(q => q.RemoteIpAddress.AddressFamily); + bool commonDynamicTunnel = playerTunnels.Any(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)); - if (combinedPing < playerTunnels.Single(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).CombinedPing) + if (!commonDynamicTunnel || combinedPing < playerTunnels.Single(q => q.RemotePlayerName.Equals(remotePlayerName, StringComparison.OrdinalIgnoreCase)).CombinedPing) { ushort[] localPorts; ushort[] remotePorts; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index 98c887696..8188ba4fb 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -155,6 +155,8 @@ protected override DataReceivedEventArgs ProcessReceivedData(Memory buffer private void OnRaiseConnectedEvent(EventArgs e) { + Connected = true; + EventHandler raiseEvent = RaiseConnectedEvent; raiseEvent?.Invoke(this, e); diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index c1ee1b4f3..d106a42c9 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -334,7 +334,10 @@ private async ValueTask> GetServerListSortedByLatencyAsync() int[] serverPorts = serverInfoGroup.SelectMany(serverInfo => serverInfo.Ports).Distinct().ToArray(); return (ipAddress, serverNames, serverPorts); - }).ToArray(); + }). + Where(q => (q.ipAddress.AddressFamily is AddressFamily.InterNetworkV6 && Socket.OSSupportsIPv6) + || (q.ipAddress.AddressFamily is AddressFamily.InterNetwork && Socket.OSSupportsIPv4)) + .ToArray(); // Do logging. foreach ((IPAddress ipAddress, string name, int[] ports) in serverInfos) @@ -404,7 +407,7 @@ private async ValueTask> GetServerListSortedByLatencyAsync() { PingReply pingReply = await ping.SendPingAsync(serverInfo.IpAddress, MAXIMUM_LATENCY).ConfigureAwait(false); - if (pingReply.Status == IPStatus.Success) + if (pingReply.Status is IPStatus.Success) { long pingInMs = pingReply.RoundtripTime; Logger.Log($"The latency in milliseconds to the server {serverInfo.Name} ({serverInfo.IpAddress}): {pingInMs}."); From ead24a997196b5d10ddae8b2156c7a4362aed7d9 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 7 Jan 2023 18:30:20 +0100 Subject: [PATCH 084/109] LAN lobby multi-adapter improvements --- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 163 +++++++++++------- .../Domain/Multiplayer/NetworkHelper.cs | 12 +- 2 files changed, 115 insertions(+), 60 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index c1e38fc56..a111f30e4 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -1,12 +1,14 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -51,6 +53,9 @@ public LANLobby( public event EventHandler Exited; + private readonly List<(Socket Socket, IPEndPoint BroadcastIpEndpoint)> sockets = new(); + private readonly IPEndPoint loopBackIpEndPoint = new(IPAddress.Loopback, ProgramConstants.LAN_LOBBY_PORT); + private XNAListBox lbPlayerList; private ChatListBox lbChatMessages; private GameListBox lbGameList; @@ -68,8 +73,6 @@ public LANLobby( private string localGame; private int localGameIndex; private GameCollection gameCollection; - private Socket socket; - private IPEndPoint endPoint; private Encoding encoding; private List players = new List(); private TimeSpan timeSinceAliveMessage = TimeSpan.Zero; @@ -77,7 +80,6 @@ public LANLobby( private DiscordHandler discordHandler; private bool initSuccess; private CancellationTokenSource cancellationTokenSource; - private IPAddress lanIpV4BroadcastIpAddress; public override void Initialize() { @@ -244,14 +246,16 @@ public override void Initialize() private async ValueTask WindowManager_GameClosingAsync(CancellationToken cancellationToken) { - if (socket == null) - return; - - if (socket.IsBound) - await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationToken).ConfigureAwait(false); + foreach ((Socket socket, _) in sockets) + { + if (socket.IsBound) + await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationToken).ConfigureAwait(false); + } cancellationTokenSource.Cancel(); - socket.Close(); + + foreach ((Socket socket, _) in sockets) + socket.Close(); } private async ValueTask GameCreationWindow_LoadGameAsync(GameLoadEventArgs e) @@ -291,57 +295,76 @@ public async ValueTask OpenAsync() Visible = true; Enabled = true; - cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource = new(); Logger.Log("Creating LAN socket."); - IEnumerable lanIpAddresses = NetworkHelper.GetUniCastIpAddresses().Select(q => q.UnicastIPAddressInformation); - UnicastIPAddressInformation lanIpV4Address = lanIpAddresses.FirstOrDefault(q => q.Address.AddressFamily is AddressFamily.InterNetwork); + List lanIpV4Addresses; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + lanIpV4Addresses = NetworkHelper.GetWindowsLanUniCastIpAddresses() + .Where(q => q.Address.AddressFamily is AddressFamily.InterNetwork) + .ToList(); + } + else + { + lanIpV4Addresses = NetworkHelper.GetLanUniCastIpAddresses() + .Where(q => q.Address.AddressFamily is AddressFamily.InterNetwork) + .ToList(); + } - if (lanIpV4Address is null) + if (!lanIpV4Addresses.Any()) { Logger.Log("No IPv4 address found for LAN."); lbChatMessages.AddMessage(new ChatMessage(Color.Red, "No IPv4 address found for LAN".L10N("Client:Main:NoLANIPv4"))); - initSuccess = false; return; } - lanIpV4BroadcastIpAddress = NetworkHelper.GetIpV4BroadcastAddress(lanIpV4Address); - - try + foreach (UnicastIPAddressInformation lanIpV4Address in lanIpV4Addresses) { - socket = new(SocketType.Dgram, ProtocolType.Udp) + var broadcastIpEndpoint = new IPEndPoint(NetworkHelper.GetIpV4BroadcastAddress(lanIpV4Address), ProgramConstants.LAN_LOBBY_PORT); + + try { - EnableBroadcast = true - }; + var socket = new Socket(SocketType.Dgram, ProtocolType.Udp) + { + EnableBroadcast = true + }; - socket.Bind(new IPEndPoint(lanIpV4Address.Address, ProgramConstants.LAN_LOBBY_PORT)); + sockets.Add((socket, broadcastIpEndpoint)); + socket.Bind(new IPEndPoint(lanIpV4Address.Address, ProgramConstants.LAN_LOBBY_PORT)); - endPoint = new(lanIpV4BroadcastIpAddress, ProgramConstants.LAN_LOBBY_PORT); - initSuccess = true; + initSuccess = true; - Logger.Log($"Created LAN broadcast socket {socket.LocalEndPoint} / {endPoint}."); + Logger.Log($"Created LAN broadcast socket {socket.LocalEndPoint} / {broadcastIpEndpoint}."); + } + catch (SocketException ex) + { + ProgramConstants.LogException(ex, "Creating LAN socket failed!"); + lbChatMessages.AddMessage(new ChatMessage(Color.Red, + string.Format( + CultureInfo.CurrentCulture, + $""" + {"Creating LAN socket failed! Message: {0}".L10N("Client:Main:SocketFailure1")} + {"Please check your firewall settings.".L10N("Client:Main:SocketFailure2")} + {"Also make sure that no other application is listening to traffic on UDP ports {1} - {2}.".L10N("Client:Main:SocketFailure3")} + """, + ex.Message, + ProgramConstants.LAN_LOBBY_PORT, + ProgramConstants.LAN_INGAME_PORT))); + } } - catch (SocketException ex) - { - ProgramConstants.LogException(ex, "Creating LAN socket failed!"); - lbChatMessages.AddMessage(new ChatMessage(Color.Red, - "Creating LAN socket failed! Message:".L10N("Client:Main:SocketFailure1") + " " + ex.Message)); - lbChatMessages.AddMessage(new ChatMessage(Color.Red, - "Please check your firewall settings.".L10N("Client:Main:SocketFailure2"))); - lbChatMessages.AddMessage(new ChatMessage(Color.Red, - $""" - Also make sure that no other application is listening to traffic on UDP ports - {ProgramConstants.LAN_LOBBY_PORT} - {ProgramConstants.LAN_INGAME_PORT}. - """.L10N("Client:Main:SocketFailure3"))); - - initSuccess = false; + + if (!initSuccess) return; - } - Logger.Log("Starting listener."); - ListenAsync(cancellationTokenSource.Token).HandleTask(); + Logger.Log("Starting LAN listeners."); + + foreach ((Socket socket, IPEndPoint broadcastIpEndpoint) in sockets) + ListenAsync(socket, broadcastIpEndpoint, cancellationTokenSource.Token).HandleTask(); + await SendAliveAsync(cancellationTokenSource.Token).ConfigureAwait(false); } @@ -350,6 +373,8 @@ private async ValueTask SendMessageAsync(string message, CancellationToken cance if (!initSuccess) return; + Task.Run(() => HandleNetworkMessage(message, loopBackIpEndPoint)).HandleTask(); + const int charSize = sizeof(char); int bufferSize = message.Length * charSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); @@ -358,16 +383,22 @@ private async ValueTask SendMessageAsync(string message, CancellationToken cance buffer = buffer[..bytes]; - try - { - await socket.SendToAsync(buffer, SocketFlags.None, endPoint, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) + foreach ((Socket socket, IPEndPoint broadcastEndpoint) in sockets) { + try + { + await socket.SendToAsync(buffer, SocketFlags.None, broadcastEndpoint, cancellationToken).ConfigureAwait(false); +#if DEBUG + Logger.Log($"Sent LAN broadcast on {socket.LocalEndPoint} / {broadcastEndpoint}: {message}."); +#endif + } + catch (OperationCanceledException) + { + } } } - private async ValueTask ListenAsync(CancellationToken cancellationToken) + private async ValueTask ListenAsync(Socket socket, EndPoint broadcastEndpoint, CancellationToken cancellationToken) { try { @@ -375,17 +406,23 @@ private async ValueTask ListenAsync(CancellationToken cancellationToken) while (!cancellationToken.IsCancellationRequested) { - EndPoint ep = new IPEndPoint(lanIpV4BroadcastIpAddress, ProgramConstants.LAN_LOBBY_PORT); Memory buffer = memoryOwner.Memory[..4096]; SocketReceiveFromResult socketReceiveFromResult = - await socket.ReceiveFromAsync(buffer, SocketFlags.None, ep, cancellationToken).ConfigureAwait(false); - var iep = (IPEndPoint)socketReceiveFromResult.RemoteEndPoint; + await socket.ReceiveFromAsync(buffer, SocketFlags.None, broadcastEndpoint, cancellationToken).ConfigureAwait(false); + var remoteIpEndPoint = (IPEndPoint)socketReceiveFromResult.RemoteEndPoint; + + if (sockets.Select(q => ((IPEndPoint)q.Socket.LocalEndPoint).Address).ToList().Contains(remoteIpEndPoint.Address)) + continue; + string data = encoding.GetString(buffer.Span[..socketReceiveFromResult.ReceivedBytes]); if (string.IsNullOrEmpty(data)) continue; - AddCallback(() => HandleNetworkMessage(data, iep)); +#if DEBUG + Logger.Log($"Received LAN broadcast on {socket.LocalEndPoint} / {broadcastEndpoint}: {data}."); +#endif + AddCallback(() => HandleNetworkMessage(data, remoteIpEndPoint)); } } catch (OperationCanceledException) @@ -522,7 +559,9 @@ private async ValueTask JoinGameAsync() if (!hg.Game.InternalName.Equals(localGame, StringComparison.OrdinalIgnoreCase)) { lbChatMessages.AddMessage( - string.Format("The selected game is for {0}!".L10N("Client:Main:GameIsOfPurpose"), gameCollection.GetGameNameFromInternalName(hg.Game.InternalName))); + string.Format(CultureInfo.CurrentCulture, + "The selected game is for {0}!".L10N("Client:Main:GameIsOfPurpose"), + gameCollection.GetGameNameFromInternalName(hg.Game.InternalName))); return; } @@ -554,7 +593,8 @@ private async ValueTask JoinGameAsync() // TODO Show warning } - lbChatMessages.AddMessage(string.Format("Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName)); + lbChatMessages.AddMessage( + string.Format(CultureInfo.CurrentCulture, "Attempting to join game {0} ...".L10N("Client:Main:AttemptJoin"), hg.RoomName)); try { @@ -571,9 +611,9 @@ private async ValueTask JoinGameAsync() await lanGameLoadingLobby.SetUpAsync(false, client, loadedGameId).ConfigureAwait(false); lanGameLoadingLobby.Enable(); - string message = LANCommands.PLAYER_JOIN + ProgramConstants.LAN_DATA_SEPARATOR + - ProgramConstants.PLAYERNAME + ProgramConstants.LAN_DATA_SEPARATOR + - loadedGameId + ProgramConstants.LAN_MESSAGE_SEPARATOR; + string message = FormattableString.Invariant($""" + {LANCommands.PLAYER_JOIN}{ProgramConstants.LAN_DATA_SEPARATOR}{ProgramConstants.PLAYERNAME}{ProgramConstants.LAN_DATA_SEPARATOR}{loadedGameId}{ProgramConstants.LAN_MESSAGE_SEPARATOR} + """); int bufferSize = message.Length * charSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); Memory buffer = memoryOwner.Memory[..bufferSize]; @@ -603,8 +643,10 @@ private async ValueTask JoinGameAsync() catch (Exception ex) { ProgramConstants.LogException(ex, "Connecting to the game failed!"); - lbChatMessages.AddMessage(null, - "Connecting to the game failed! Message:".L10N("Client:Main:ConnectGameFailed") + " " + ex.Message, Color.White); + lbChatMessages.AddMessage(null, string.Format( + CultureInfo.CurrentCulture, + "Connecting to the game failed! Message: {0}".L10N("Client:Main:ConnectGameFailed"), + ex.Message), Color.White); } } @@ -614,7 +656,10 @@ private async ValueTask BtnMainMenu_LeftClickAsync() Enabled = false; await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, CancellationToken.None).ConfigureAwait(false); cancellationTokenSource.Cancel(); - socket?.Close(); + + foreach ((Socket socket, _) in sockets) + socket.Close(); + Exited?.Invoke(this, EventArgs.Empty); } diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index dd49b2bdb..7f7c8ff84 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -38,7 +38,17 @@ public static IEnumerable GetPrivateIpAddresses() => GetLocalAddresses() .Where(IsPrivateIpAddress); - public static IEnumerable<(UnicastIPAddressInformation UnicastIPAddressInformation, GatewayIPAddressInformation GatewayIPAddressInformation)> GetUniCastIpAddresses() + [SupportedOSPlatform("windows")] + public static IEnumerable GetWindowsLanUniCastIpAddresses() + => GetLanUniCastIpAddresses() + .Where(q => q.SuffixOrigin is not SuffixOrigin.WellKnown); + + public static IEnumerable GetLanUniCastIpAddresses() + => GetIpInterfaces() + .SelectMany(q => q.UnicastAddresses) + .Where(q => SupportedAddressFamilies.Contains(q.Address.AddressFamily)); + + private static IEnumerable<(UnicastIPAddressInformation UnicastIPAddressInformation, GatewayIPAddressInformation GatewayIPAddressInformation)> GetUniCastIpAddresses() => GetIpInterfaces() .Where(q => q.GatewayAddresses.Any()) .SelectMany(q => q.UnicastAddresses.Select( From 80ec54dca89736ed51fa13617d915c382150532b Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 7 Jan 2023 19:06:55 +0100 Subject: [PATCH 085/109] Fix LANLobby game closing --- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index a111f30e4..742f93b44 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -252,7 +252,7 @@ private async ValueTask WindowManager_GameClosingAsync(CancellationToken cancell await SendMessageAsync(LANCommands.PLAYER_QUIT_COMMAND, cancellationToken).ConfigureAwait(false); } - cancellationTokenSource.Cancel(); + cancellationTokenSource?.Cancel(); foreach ((Socket socket, _) in sockets) socket.Close(); From d25099ce4820e3304a32c9334796be3878a51b45 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 8 Jan 2023 17:42:13 +0100 Subject: [PATCH 086/109] IPv6/IPv4 internet detection & P2P improvements --- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 29 +++--- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 8 ++ .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 14 +-- .../Multiplayer/CnCNet/PlayerConnection.cs | 8 ++ .../CnCNet/UPNP/InternetGatewayDevice.cs | 6 +- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 99 +++++++++++-------- .../Multiplayer/CnCNet/V3ConnectionState.cs | 36 ++++--- .../Domain/Multiplayer/NetworkHelper.cs | 12 ++- DXMainClient/Online/Connection.cs | 16 +-- 9 files changed, 146 insertions(+), 82 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index 78bdfc281..a28f0c60d 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -143,8 +143,8 @@ public CnCNetGameLobby( new IntCommandHandler(CnCNetCommands.TUNNEL_PING, HandleTunnelPing), new StringCommandHandler(CnCNetCommands.CHANGE_TUNNEL_SERVER, (playerName, hash) => HandleTunnelServerChangeMessageAsync(playerName, hash).HandleTask()), new StringCommandHandler(CnCNetCommands.PLAYER_TUNNEL_PINGS, HandleTunnelPingsMessage), - new StringCommandHandler(CnCNetCommands.PLAYER_P2P_REQUEST, (playerName, p2pRequestMessage) => HandleP2PRequestMessageAsync(playerName, p2pRequestMessage).HandleTask()), - new StringCommandHandler(CnCNetCommands.PLAYER_P2P_PINGS, HandleP2PPingsMessage) + new StringCommandHandler(CnCNetCommands.PLAYER_P2P_REQUEST, (playerName, p2pRequestMessage) => HandleP2PRequestMessageAsync(playerName, p2pRequestMessage, true).HandleTask()), + new StringCommandHandler(CnCNetCommands.PLAYER_P2P_PINGS, (playerName, p2pPingsMessage) => HandleP2PPingsMessageAsync(playerName, p2pPingsMessage).HandleTask()) }; MapSharer.MapDownloadFailed += (_, e) => WindowManager.AddCallback(() => MapSharer_HandleMapDownloadFailedAsync(e).HandleTask()); @@ -1213,14 +1213,14 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() } catch (Exception ex) { - ProgramConstants.LogException(ex, "P2P setup failed."); + ProgramConstants.LogException(ex, "P2P: setup failed."); } if (!p2pSetupSucceeded) { AddNotice(string.Format( CultureInfo.CurrentCulture, - "P2P setup failed. Check that UPnP port mapping is enabled for this device on your router/modem.".L10N("Client:Main:P2PSetupFailed")), + "P2P: setup failed. Check that UPnP port mapping is enabled for this device on your router/modem.".L10N("Client:Main:P2PSetupFailed")), Color.Orange); return; @@ -2044,8 +2044,11 @@ private void HandleTunnelPingsMessage(string playerName, string tunnelPingsMessa } } - private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p2pRequestMessage) + private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p2pRequestMessage, bool isCachedP2PRequestMessage) { + if (!isCachedP2PRequestMessage) + v3ConnectionState.StoreP2PRequest(playerName, p2pRequestMessage); + if (!v3ConnectionState.P2PEnabled) return; @@ -2053,7 +2056,9 @@ private async ValueTask HandleP2PRequestMessageAsync(string playerName, string p if (remotePlayerP2PEnabled) { - ShowP2PPlayerStatus(playerName); + if (isCachedP2PRequestMessage) + ShowP2PPlayerStatus(playerName); + await channel.SendCTCPMessageAsync( CnCNetCommands.PLAYER_P2P_PINGS + v3ConnectionState.GetP2PPingCommand(playerName), QueuedMessageType.SYSTEM_MESSAGE, 10).ConfigureAwait(false); } @@ -2063,12 +2068,14 @@ await channel.SendCTCPMessageAsync( } } - private void HandleP2PPingsMessage(string playerName, string p2pPingsMessage) + private async ValueTask HandleP2PPingsMessageAsync(string playerName, string p2pPingsMessage) { - bool shouldUpdatePlayerStatus = v3ConnectionState.UpdateRemotePingResults(playerName, p2pPingsMessage, FindLocalPlayer().Name); + string cachedP2PRequestMessage = v3ConnectionState.UpdateRemotePingResults(playerName, p2pPingsMessage, FindLocalPlayer().Name); + + if (!string.IsNullOrWhiteSpace(cachedP2PRequestMessage)) + await HandleP2PRequestMessageAsync(playerName, cachedP2PRequestMessage, false).ConfigureAwait(false); - if (shouldUpdatePlayerStatus) - ShowP2PPlayerStatus(playerName); + ShowP2PPlayerStatus(playerName); } private void ShowP2PPlayerStatus(string playerName) @@ -2076,7 +2083,7 @@ private void ShowP2PPlayerStatus(string playerName) P2PPlayer p2pPlayer = v3ConnectionState.P2PPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)); if (p2pPlayer.RemotePingResults.Any() && p2pPlayer.LocalPingResults.Any()) - AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} supports P2P ({1}ms)".L10N("Client:Main:PlayerP2PSupported"), playerName, p2pPlayer.LocalPingResults.Min(q => q.Ping))); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} P2P negotiated ({1}ms)".L10N("Client:Main:PlayerP2PNegotiated"), playerName, p2pPlayer.LocalPingResults.Min(q => q.Ping))); } /// diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 742f93b44..0fd8b9a5a 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -389,7 +389,11 @@ private async ValueTask SendMessageAsync(string message, CancellationToken cance { await socket.SendToAsync(buffer, SocketFlags.None, broadcastEndpoint, cancellationToken).ConfigureAwait(false); #if DEBUG +#if NETWORKTRACE Logger.Log($"Sent LAN broadcast on {socket.LocalEndPoint} / {broadcastEndpoint}: {message}."); +#else + Logger.Log($"Sent LAN broadcast on {socket.LocalEndPoint} / {broadcastEndpoint}."); +#endif #endif } catch (OperationCanceledException) @@ -420,7 +424,11 @@ private async ValueTask ListenAsync(Socket socket, EndPoint broadcastEndpoint, C continue; #if DEBUG +#if NETWORKTRACE Logger.Log($"Received LAN broadcast on {socket.LocalEndPoint} / {broadcastEndpoint}: {data}."); +#else + Logger.Log($"Received LAN broadcast on {socket.LocalEndPoint} / {broadcastEndpoint}."); +#endif #endif AddCallback(() => HandleNetworkMessage(data, remoteIpEndPoint)); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 5de192277..2c1fa93f8 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -42,28 +42,30 @@ public static CnCNetTunnel Parse(string str) string primaryAddress = addressAndPort[..addressAndPort.LastIndexOf(':')]; var primaryIpAddress = IPAddress.Parse(primaryAddress); IPAddress secondaryIpAddress = string.IsNullOrWhiteSpace(secondaryAddress) ? null : IPAddress.Parse(secondaryAddress); + bool hasIPv6Internet = NetworkHelper.HasIPv6Internet(); + bool hasIPv4Internet = NetworkHelper.HasIPv4Internet(); - if (Socket.OSSupportsIPv6 && primaryIpAddress.AddressFamily is AddressFamily.InterNetworkV6) + if (hasIPv6Internet && primaryIpAddress.AddressFamily is AddressFamily.InterNetworkV6) { tunnel.Address = primaryIpAddress.ToString(); } - else if (Socket.OSSupportsIPv6 && secondaryIpAddress?.AddressFamily is AddressFamily.InterNetworkV6) + else if (hasIPv6Internet && secondaryIpAddress?.AddressFamily is AddressFamily.InterNetworkV6) { tunnel.Address = secondaryIpAddress.ToString(); } - else if (Socket.OSSupportsIPv4 && primaryIpAddress.AddressFamily is AddressFamily.InterNetwork) + else if (hasIPv4Internet && primaryIpAddress.AddressFamily is AddressFamily.InterNetwork) { tunnel.Address = primaryIpAddress.ToString(); } - else if (Socket.OSSupportsIPv4 && secondaryIpAddress?.AddressFamily is AddressFamily.InterNetwork) + else if (hasIPv4Internet && secondaryIpAddress?.AddressFamily is AddressFamily.InterNetwork) { tunnel.Address = secondaryIpAddress.ToString(); } else { Logger.Log($""" - No supported IP address found ({nameof(Socket.OSSupportsIPv6)}={Socket.OSSupportsIPv6}, - {nameof(Socket.OSSupportsIPv4)}={Socket.OSSupportsIPv4}) for {str}. + No supported IP address/connection found ({nameof(NetworkHelper.HasIPv6Internet)}={hasIPv6Internet}, + {nameof(NetworkHelper.HasIPv4Internet)}={hasIPv4Internet}) for {primaryIpAddress} - {secondaryIpAddress}. """); return null; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs index a5753eae8..82c5c3596 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs @@ -71,8 +71,12 @@ protected async ValueTask SendDataAsync(ReadOnlyMemory data) try { #if DEBUG +#if NETWORKTRACE Logger.Log($"{GetType().Name}: Sending data from {Socket.LocalEndPoint} to {RemoteEndPoint} for player {PlayerId}: {BitConverter.ToString(data.Span.ToArray())}."); +#else + Logger.Log($"{GetType().Name}: Sending data from {Socket.LocalEndPoint} to {RemoteEndPoint} for player {PlayerId}."); +#endif #endif if (Connected) await Socket.SendToAsync(data, SocketFlags.None, RemoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); @@ -164,7 +168,11 @@ private async ValueTask ReceiveLoopAsync() receiveTimeout = GameInProgressReceiveTimeout; #if DEBUG +#if NETWORKTRACE Logger.Log($"{GetType().Name}: Received data from {RemoteEndPoint} on {Socket.LocalEndPoint} for player {PlayerId}: {BitConverter.ToString(buffer.Span.ToArray())}."); +#else + Logger.Log($"{GetType().Name}: Received data from {RemoteEndPoint} on {Socket.LocalEndPoint} for player {PlayerId}."); +#endif #endif DataReceivedEventArgs dataReceivedEventArgs = ProcessReceivedData(buffer, socketReceiveFromResult); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index 06fb78a4b..a847b2488 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -129,7 +129,11 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance throw new ArgumentException($"P2P: UPnP version {uPnPVersion} is not supported."); } - Logger.Log($"P2P: Received external IP address {ipAddress}."); +#if DEBUG + Logger.Log($"P2P: Received external IPv4 address {ipAddress}."); +#else + Logger.Log($"P2P: Received external IPv4 address."); +#endif } catch { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index a5f80cc54..ef2146266 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -18,8 +18,7 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; internal static class UPnPHandler { - private const int ReceiveTimeout = 2000; - private const int SendCount = 3; + private const int ReceiveTimeoutInSeconds = 2; public static readonly HttpClient HttpClient = new( new SocketsHttpHandler @@ -37,7 +36,12 @@ internal static class UPnPHandler }; if (IPAddress.Parse(context.DnsEndPoint.Host).AddressFamily is AddressFamily.InterNetworkV6) - socket.Bind(new IPEndPoint(NetworkHelper.GetLocalPublicIpV6Address(), 0)); + { + socket.Bind( + new IPEndPoint(NetworkHelper.GetLocalPublicIpV6Address() + ?? NetworkHelper.GetPrivateIpAddresses().First(q => q.AddressFamily is AddressFamily.InterNetworkV6), + 0)); + } await socket.ConnectAsync(context.DnsEndPoint, token).ConfigureAwait(false); @@ -60,7 +64,7 @@ internal static class UPnPHandler }, true) { - Timeout = TimeSpan.FromMilliseconds(ReceiveTimeout), + Timeout = TimeSpan.FromSeconds(ReceiveTimeoutInSeconds), DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; @@ -268,7 +272,7 @@ private static async ValueTask> GetInternetGa { IEnumerable devices = await GetDevicesAsync(cancellationToken).ConfigureAwait(false); - return devices.Where(q => q.UPnPDescription.Device.DeviceType?.StartsWith($"{UPnPConstants.UPnPInternetGatewayDevice}:", StringComparison.OrdinalIgnoreCase) ?? false); + return devices.Where(q => q?.UPnPDescription.Device.DeviceType?.StartsWith($"{UPnPConstants.UPnPInternetGatewayDevice}:", StringComparison.OrdinalIgnoreCase) ?? false); } private static InternetGatewayDevice GetInternetGatewayDeviceByVersion(List internetGatewayDevices, ushort uPnPVersion) @@ -319,14 +323,15 @@ private static IEnumerable> GetFormattedDeviceRespons if (addressType is AddressType.Unknown) return new(localAddress, responses); - var socket = new Socket(localAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + using var socket = new Socket(localAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + var localEndPoint = new IPEndPoint(localAddress, 0); + var multiCastIpEndPoint = new IPEndPoint(SsdpMultiCastAddresses[addressType], UPnPConstants.UPnPMultiCastPort); try { - socket.Bind(new IPEndPoint(localAddress, 0)); + socket.Bind(localEndPoint); - var multiCastIpEndPoint = new IPEndPoint(SsdpMultiCastAddresses[addressType], UPnPConstants.UPnPMultiCastPort); - string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {multiCastIpEndPoint}\r\nST: {UPnPConstants.UPnPRootDevice}\r\nMAN: \"ssdp:discover\"\r\nMX: 3\r\n\r\n"); + string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {multiCastIpEndPoint}\r\nST: {UPnPConstants.UPnPRootDevice}\r\nMAN: \"ssdp:discover\"\r\nMX: {ReceiveTimeoutInSeconds}\r\n\r\n"); const int charSize = sizeof(char); int bufferSize = request.Length * charSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); @@ -335,17 +340,15 @@ private static IEnumerable> GetFormattedDeviceRespons buffer = buffer[..numberOfBytes]; - for (int i = 0; i < SendCount; i++) - { - await socket.SendToAsync(buffer, SocketFlags.None, multiCastIpEndPoint, cancellationToken).ConfigureAwait(false); - await Task.Delay(100, cancellationToken).ConfigureAwait(false); - } - + await socket.SendToAsync(buffer, SocketFlags.None, multiCastIpEndPoint, cancellationToken).ConfigureAwait(false); await ReceiveAsync(socket, responses, cancellationToken).ConfigureAwait(false); } - finally + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + } + catch (Exception ex) { - socket.Close(); + ProgramConstants.LogException(ex, $"P2P: Could not detect UPnP devices on {localEndPoint} / {multiCastIpEndPoint}."); } return new(localAddress, responses); @@ -368,7 +371,7 @@ private static AddressType GetAddressType(IPAddress localAddress) private static async ValueTask ReceiveAsync(Socket socket, ICollection responses, CancellationToken cancellationToken) { using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); - using var timeoutCancellationTokenSource = new CancellationTokenSource(ReceiveTimeout); + using var timeoutCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(ReceiveTimeoutInSeconds)); using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutCancellationTokenSource.Token, cancellationToken); while (!linkedCancellationTokenSource.IsCancellationRequested) @@ -411,39 +414,51 @@ private static async ValueTask GetDescriptionAsync(Uri uri, Can private static async Task ParseDeviceAsync( IGrouping internetGatewayDeviceResponses, CancellationToken cancellationToken) { - Uri[] locations = internetGatewayDeviceResponses.Select(q => (q.LocalIpAddress, q.Location)).Distinct().Select(ParseLocation).ToArray(); - Uri preferredLocation = GetPreferredLocation(locations); - UPnPDescription uPnPDescription = default; + Uri[] locations = null; try { - uPnPDescription = await GetDescriptionAsync(preferredLocation, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) - { - if (preferredLocation.HostNameType is UriHostNameType.IPv6 && locations.Any(q => q.HostNameType is UriHostNameType.IPv4)) + locations = internetGatewayDeviceResponses.Select(q => (q.LocalIpAddress, q.Location)).Distinct().Select(ParseLocation).ToArray(); + + Uri preferredLocation = GetPreferredLocation(locations); + UPnPDescription uPnPDescription = default; + + try { - try - { - preferredLocation = locations.First(q => q.HostNameType is UriHostNameType.IPv4); - uPnPDescription = await GetDescriptionAsync(preferredLocation, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + uPnPDescription = await GetDescriptionAsync(preferredLocation, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + if (preferredLocation.HostNameType is UriHostNameType.IPv6 && locations.Any(q => q.HostNameType is UriHostNameType.IPv4)) { + try + { + preferredLocation = locations.First(q => q.HostNameType is UriHostNameType.IPv4); + uPnPDescription = await GetDescriptionAsync(preferredLocation, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + } } } + + return new( + locations, + internetGatewayDeviceResponses.Select(r => r.Server).Distinct().Single(), + internetGatewayDeviceResponses.Select(r => r.CacheControl).Distinct().Single(), + internetGatewayDeviceResponses.Select(r => r.Ext).Distinct().Single(), + internetGatewayDeviceResponses.Select(r => r.SearchTarget).Distinct().Single(), + internetGatewayDeviceResponses.Key, + uPnPDescription, + preferredLocation, + internetGatewayDeviceResponses.Select(r => r.LocalIpAddress).Distinct().ToList().AsReadOnly()); } + catch (Exception ex) + { + ProgramConstants.LogException(ex, $"P2P: Could not get UPnP description from {locations?.Select(q => q.ToString()).DefaultIfEmpty().Aggregate((q, r) => $"{q} / {r}")}."); - return new( - locations, - internetGatewayDeviceResponses.Select(r => r.Server).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.CacheControl).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.Ext).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.SearchTarget).Distinct().Single(), - internetGatewayDeviceResponses.Key, - uPnPDescription, - preferredLocation, - internetGatewayDeviceResponses.Select(r => r.LocalIpAddress).Distinct().ToList().AsReadOnly()); + return null; + } } private static Uri ParseLocation((IPAddress LocalIpAddress, Uri Location) location) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index b8356c10d..59cb87957 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -20,6 +20,7 @@ internal sealed class V3ConnectionState : IAsyncDisposable private readonly TunnelHandler tunnelHandler; private readonly List<(string RemotePlayerName, CnCNetTunnel Tunnel, int CombinedPing)> playerTunnels = new(); + private readonly Dictionary playerP2PRequestMessages = new(); private readonly ReplayHandler replayHandler = new(); private IPAddress publicIpV4Address; @@ -106,8 +107,8 @@ public void RemoveV3Player(string playerName) } public string GetP2PRequestCommand() - => $" {publicIpV4Address}\t{(!ipV4P2PPorts.Any() ? null : ipV4P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}" + - $";{publicIpV6Address}\t{(!ipV6P2PPorts.Any() ? null : ipV6P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).Aggregate((q, r) => $"{q}-{r}"))}"; + => $" {publicIpV4Address}\t{(!ipV4P2PPorts.Any() ? null : ipV4P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).DefaultIfEmpty().Aggregate((q, r) => $"{q}-{r}"))}" + + $";{publicIpV6Address}\t{(!ipV6P2PPorts.Any() ? null : ipV6P2PPorts.Select(q => q.ExternalPort.ToString(CultureInfo.InvariantCulture)).DefaultIfEmpty().Aggregate((q, r) => $"{q}-{r}"))}"; public string GetP2PPingCommand(string playerName) => $" {playerName}-{P2PPlayers.Single(q => q.RemotePlayerName.Equals(playerName, StringComparison.OrdinalIgnoreCase)).LocalPingResults.Select(q => $"{q.RemoteIpAddress};{q.Ping}\t").DefaultIfEmpty().Aggregate((q, r) => $"{q}{r}")}"; @@ -140,6 +141,12 @@ public async ValueTask ToggleRecordingAsync() return false; } + public void StoreP2PRequest(string playerName, string p2pRequestMessage) + => playerP2PRequestMessages[playerName] = p2pRequestMessage; + + public string GetP2PRequest(string playerName) + => playerP2PRequestMessages.TryGetValue(playerName, out string p2pRequestMessage) ? p2pRequestMessage : null; + public async ValueTask PingRemotePlayerAsync(string playerName, string p2pRequestMessage) { List<(IPAddress RemoteIpAddress, long Ping)> localPingResults = new(); @@ -178,16 +185,16 @@ public async ValueTask PingRemotePlayerAsync(string playerName, string p2p return localPingResults.Any(); } - public bool UpdateRemotePingResults(string senderName, string p2pPingsMessage, string localPlayerName) + public string UpdateRemotePingResults(string senderName, string p2pPingsMessage, string localPlayerName) { if (!P2PEnabled) - return false; + return null; string[] splitLines = p2pPingsMessage.Split('-'); string pingPlayerName = splitLines[0]; if (!localPlayerName.Equals(pingPlayerName, StringComparison.OrdinalIgnoreCase)) - return false; + return null; string[] pingResults = splitLines[1].Split('\t', StringSplitOptions.RemoveEmptyEntries); List<(IPAddress IpAddress, long Ping)> playerPings = new(); @@ -213,9 +220,11 @@ public bool UpdateRemotePingResults(string senderName, string p2pPingsMessage, s p2pPlayer = new(senderName, Array.Empty(), Array.Empty(), new(), new()); } - P2PPlayers.Add(p2pPlayer with { RemotePingResults = playerPings }); + p2pPlayer = p2pPlayer with { RemotePingResults = playerPings }; - return !p2pPlayer.RemotePingResults.Any(); + P2PPlayers.Add(p2pPlayer); + + return !p2pPlayer.LocalPingResults.Any() ? GetP2PRequest(senderName) : null; } public void StartV3ConnectionListeners( @@ -364,6 +373,7 @@ public async ValueTask DisposeAsync() playerTunnels.Clear(); P2PPlayers.Clear(); PinnedTunnels?.Clear(); + playerP2PRequestMessages.Clear(); await CloseP2PPortsAsync().ConfigureAwait(false); } @@ -438,16 +448,18 @@ private void SetupGameTunnelHandler( private async ValueTask CloseP2PPortsAsync() { - if (internetGatewayDevice is null) - return; + List tasks = new(); - Task ipV4Task = ClientCore.Extensions.TaskExtensions.WhenAllSafe(ipV4P2PPorts.Select(q => internetGatewayDevice.CloseIpV4PortAsync(q.InternalPort, CancellationToken.None))); - Task ipV6Task = ClientCore.Extensions.TaskExtensions.WhenAllSafe(p2pIpV6PortIds.Select(q => internetGatewayDevice.CloseIpV6PortAsync(q, CancellationToken.None))); + if (internetGatewayDevice is not null) + { + tasks.Add(ClientCore.Extensions.TaskExtensions.WhenAllSafe(ipV4P2PPorts.Select(q => internetGatewayDevice.CloseIpV4PortAsync(q.InternalPort, CancellationToken.None)))); + tasks.Add(ClientCore.Extensions.TaskExtensions.WhenAllSafe(p2pIpV6PortIds.Select(q => internetGatewayDevice.CloseIpV6PortAsync(q, CancellationToken.None)))); + } ipV4P2PPorts.Clear(); ipV6P2PPorts.Clear(); p2pIpV6PortIds.Clear(); - await ClientCore.Extensions.TaskExtensions.WhenAllSafe(new[] { ipV4Task, ipV6Task }).ConfigureAwait(false); + await ClientCore.Extensions.TaskExtensions.WhenAllSafe(tasks).ConfigureAwait(false); } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 7f7c8ff84..d7ea8ecb1 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -26,6 +26,12 @@ internal static class NetworkHelper AddressFamily.InterNetworkV6 }.AsReadOnly(); + public static bool HasIPv6Internet() + => Socket.OSSupportsIPv6 && GetLocalPublicIpV6Address() is not null; + + public static bool HasIPv4Internet() + => Socket.OSSupportsIPv4 && GetLocalAddresses().Any(q => q.AddressFamily is AddressFamily.InterNetwork); + public static IEnumerable GetLocalAddresses() => GetUniCastIpAddresses() .Select(q => q.UnicastIPAddressInformation.Address); @@ -57,7 +63,7 @@ public static IEnumerable GetLanUniCastIpAddresses( private static IEnumerable GetIpInterfaces() => NetworkInterface.GetAllNetworkInterfaces() - .Where(q => q.OperationalStatus is OperationalStatus.Up) + .Where(q => q.OperationalStatus is OperationalStatus.Up && q.NetworkInterfaceType is not NetworkInterfaceType.Loopback) .Select(q => q.GetIPProperties()); [SupportedOSPlatform("windows")] @@ -135,8 +141,8 @@ public static async ValueTask TracePublicIpV4Address(CancellationToke public static async ValueTask PingAsync(IPAddress ipAddress) { - if ((ipAddress.AddressFamily is AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) - || (ipAddress.AddressFamily is AddressFamily.InterNetwork && !Socket.OSSupportsIPv4)) + if ((ipAddress.AddressFamily is AddressFamily.InterNetworkV6 && !HasIPv6Internet()) + || (ipAddress.AddressFamily is AddressFamily.InterNetwork && !HasIPv4Internet())) { return null; } diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index d106a42c9..24141471d 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -1,10 +1,6 @@ -using ClientCore; -using ClientCore.Extensions; -using Rampastring.Tools; -using System; +using System; using System.Buffers; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; @@ -12,8 +8,12 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using ClientCore; using ClientCore.Extensions; +using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; +using Localization; +using Rampastring.Tools; namespace DTAClient.Online { @@ -320,6 +320,8 @@ private async ValueTask> GetServerListSortedByLatencyAsync() IEnumerable> serverInfosGroupedByIPAddress = servers .SelectMany(server => server) .GroupBy(serverInfo => serverInfo.IpAddress, serverInfo => (serverInfo.Name, serverInfo.Ports)); + bool hasIPv6Internet = NetworkHelper.HasIPv6Internet(); + bool hasIPv4Internet = NetworkHelper.HasIPv4Internet(); // Process each group: // 1. Get IPAddress. @@ -335,8 +337,8 @@ private async ValueTask> GetServerListSortedByLatencyAsync() return (ipAddress, serverNames, serverPorts); }). - Where(q => (q.ipAddress.AddressFamily is AddressFamily.InterNetworkV6 && Socket.OSSupportsIPv6) - || (q.ipAddress.AddressFamily is AddressFamily.InterNetwork && Socket.OSSupportsIPv4)) + Where(q => (q.ipAddress.AddressFamily is AddressFamily.InterNetworkV6 && hasIPv6Internet) + || (q.ipAddress.AddressFamily is AddressFamily.InterNetwork && hasIPv4Internet)) .ToArray(); // Do logging. From 02390fe7f58628b873ee30298474c2729a1c0eab Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 8 Jan 2023 17:46:28 +0100 Subject: [PATCH 087/109] Fix debug build --- build/CopyResources.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/CopyResources.targets b/build/CopyResources.targets index ae35d406a..ce46b0455 100644 --- a/build/CopyResources.targets +++ b/build/CopyResources.targets @@ -7,7 +7,7 @@ - + From d2e30a55fc64a55981c880e4b09d61346a7dcce7 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 9 Jan 2023 22:28:19 +0100 Subject: [PATCH 088/109] Fix tunnel - game race condition --- .../Multiplayer/CnCNet/PlayerConnection.cs | 11 +---------- .../Multiplayer/CnCNet/V3GameTunnelHandler.cs | 3 +++ .../CnCNet/V3LocalPlayerConnection.cs | 17 ++++++++++++++--- .../CnCNet/V3RemotePlayerConnection.cs | 2 -- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs index 82c5c3596..1ef154e03 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/PlayerConnection.cs @@ -19,7 +19,6 @@ internal abstract class PlayerConnection : IDisposable protected CancellationToken CancellationToken; protected Socket Socket; protected EndPoint RemoteEndPoint; - protected bool Connected; public uint PlayerId { get; protected set; } @@ -75,15 +74,9 @@ protected async ValueTask SendDataAsync(ReadOnlyMemory data) Logger.Log($"{GetType().Name}: Sending data from {Socket.LocalEndPoint} to {RemoteEndPoint} for player {PlayerId}: {BitConverter.ToString(data.Span.ToArray())}."); #else Logger.Log($"{GetType().Name}: Sending data from {Socket.LocalEndPoint} to {RemoteEndPoint} for player {PlayerId}."); - -#endif #endif - if (Connected) - await Socket.SendToAsync(data, SocketFlags.None, RemoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); -#if DEBUG - else - Logger.Log($"{GetType().Name}: Data not sent (not yet connected) from {Socket.LocalEndPoint} to {RemoteEndPoint} for player {PlayerId}."); #endif + await Socket.SendToAsync(data, SocketFlags.None, RemoteEndPoint, linkedCancellationTokenSource.Token).ConfigureAwait(false); } catch (SocketException ex) { @@ -191,8 +184,6 @@ private void OnRaiseConnectionCutEvent(EventArgs e) private void OnRaiseDataReceivedEvent(DataReceivedEventArgs e) { - Connected = true; - EventHandler raiseEvent = RaiseDataReceivedEvent; raiseEvent?.Invoke(this, e); diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs index 1dec7043e..5fcda578b 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3GameTunnelHandler.cs @@ -120,6 +120,9 @@ private void LocalGameConnection_ConnectionCut(object sender, EventArgs e) localGamePlayerConnection.RaiseConnectionCutEvent -= LocalGameConnection_ConnectionCut; localGamePlayerConnection.RaiseDataReceivedEvent -= localGameConnectionDataReceivedFunc; localGamePlayerConnection.Dispose(); + + if (!localGameConnections.Any()) + Dispose(); } /// diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs index 811301837..65b111951 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs @@ -3,6 +3,9 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +#if DEBUG +using Rampastring.Tools; +#endif namespace DTAClient.Domain.Multiplayer.CnCNet; @@ -15,6 +18,8 @@ internal sealed class V3LocalPlayerConnection : PlayerConnection private const uint IOC_VENDOR = 0x18000000; private const uint SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12; + private readonly IPEndPoint loopbackIpEndPoint = new(IPAddress.Loopback, 0); + /// /// Creates a local game socket and returns the port. /// @@ -26,13 +31,13 @@ public ushort Setup(uint playerId, CancellationToken cancellationToken) CancellationToken = cancellationToken; PlayerId = playerId; Socket = new(SocketType.Dgram, ProtocolType.Udp); - RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, 0); + RemoteEndPoint = loopbackIpEndPoint; // Disable ICMP port not reachable exceptions, happens when the game is still loading and has not yet opened the socket. if (OperatingSystem.IsWindows()) Socket.IOControl(unchecked((int)SIO_UDP_CONNRESET), new byte[] { 0 }, null); - Socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + Socket.Bind(loopbackIpEndPoint); return (ushort)((IPEndPoint)Socket.LocalEndPoint).Port; } @@ -43,8 +48,14 @@ public ushort Setup(uint playerId, CancellationToken cancellationToken) /// The data to send to the game. public async ValueTask SendDataToGameAsync(ReadOnlyMemory data) { - if (RemoteEndPoint is null || data.Length < PlayerIdsSize) + if (RemoteEndPoint.Equals(loopbackIpEndPoint) || data.Length < PlayerIdsSize) + { +#if DEBUG + Logger.Log($"{GetType().Name}: Discarded remote data from {Socket.LocalEndPoint} to {RemoteEndPoint} for player {PlayerId}."); +#endif + return; + } await SendDataAsync(data).ConfigureAwait(false); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs index 8188ba4fb..98c887696 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3RemotePlayerConnection.cs @@ -155,8 +155,6 @@ protected override DataReceivedEventArgs ProcessReceivedData(Memory buffer private void OnRaiseConnectedEvent(EventArgs e) { - Connected = true; - EventHandler raiseEvent = RaiseConnectedEvent; raiseEvent?.Invoke(this, e); From ab83bf8138b383a1fdd95d8acb8098e661299383 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 9 Jan 2023 22:55:57 +0100 Subject: [PATCH 089/109] Tunnel parsing speedup --- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 4 +-- .../Multiplayer/CnCNet/TunnelHandler.cs | 27 +++++++++++-------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 2c1fa93f8..6eaabd6ed 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -29,7 +29,7 @@ internal sealed class CnCNetTunnel /// /// The string that contains the tunnel server's information. /// A CnCNetTunnel instance parsed from the given string. - public static CnCNetTunnel Parse(string str) + public static CnCNetTunnel Parse(string str, bool hasIPv6Internet, bool hasIPv4Internet) { // For the format, check https://cncnet.org/api/v1/master-list?nocache=1 try @@ -42,8 +42,6 @@ public static CnCNetTunnel Parse(string str) string primaryAddress = addressAndPort[..addressAndPort.LastIndexOf(':')]; var primaryIpAddress = IPAddress.Parse(primaryAddress); IPAddress secondaryIpAddress = string.IsNullOrWhiteSpace(secondaryAddress) ? null : IPAddress.Parse(secondaryAddress); - bool hasIPv6Internet = NetworkHelper.HasIPv6Internet(); - bool hasIPv4Internet = NetworkHelper.HasIPv4Internet(); if (hasIPv6Internet && primaryIpAddress.AddressFamily is AddressFamily.InterNetworkV6) { diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs index 382759bfe..4ad510682 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/TunnelHandler.cs @@ -11,6 +11,7 @@ using Microsoft.Xna.Framework; using Rampastring.Tools; using Rampastring.XNAUI; +using TaskExtensions = ClientCore.Extensions.TaskExtensions; namespace DTAClient.Domain.Multiplayer.CnCNet { @@ -74,21 +75,17 @@ private void DoCurrentTunnelPinged() private async ValueTask RefreshTunnelsAsync(CancellationToken cancellationToken) { List tunnels = await DoRefreshTunnelsAsync(cancellationToken).ConfigureAwait(false); - wm.AddCallback(() => HandleRefreshedTunnels(tunnels, cancellationToken)); + wm.AddCallback(() => HandleRefreshedTunnelsAsync(tunnels, cancellationToken).HandleTask()); } - private void HandleRefreshedTunnels(List tunnels, CancellationToken cancellationToken) + private async ValueTask HandleRefreshedTunnelsAsync(List tunnels, CancellationToken cancellationToken) { if (tunnels.Count > 0) Tunnels = tunnels; TunnelsRefreshed?.Invoke(this, EventArgs.Empty); - for (int i = 0; i < Tunnels.Count; i++) - { - if (UserINISettings.Instance.PingUnofficialCnCNetTunnels || Tunnels[i].Official || Tunnels[i].Recommended) - PingListTunnelAsync(i, cancellationToken).HandleTask(); - } + await TaskExtensions.WhenAllSafe(Tunnels.Select(q => PingListTunnelAsync(q, cancellationToken))).ConfigureAwait(false); if (CurrentTunnel != null) { @@ -109,10 +106,16 @@ private void HandleRefreshedTunnels(List tunnels, CancellationToke } } - private async ValueTask PingListTunnelAsync(int index, CancellationToken cancellationToken) + private async Task PingListTunnelAsync(CnCNetTunnel tunnel, CancellationToken cancellationToken) { - await Tunnels[index].UpdatePingAsync(cancellationToken).ConfigureAwait(false); - DoTunnelPinged(index); + if (!UserINISettings.Instance.PingUnofficialCnCNetTunnels && !tunnel.Official && !tunnel.Recommended) + return; + + await tunnel.UpdatePingAsync(cancellationToken).ConfigureAwait(false); + + int tunnelIndex = Tunnels.FindIndex(t => t.Hash.Equals(tunnel.Hash, StringComparison.OrdinalIgnoreCase)); + + DoTunnelPinged(tunnelIndex); } private async ValueTask PingCurrentTunnelAsync(bool checkTunnelList, CancellationToken cancellationToken) @@ -167,13 +170,15 @@ private static async ValueTask> DoRefreshTunnelsAsync(Cancell } string[] serverList = data.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); + bool hasIPv6Internet = NetworkHelper.HasIPv6Internet(); + bool hasIPv4Internet = NetworkHelper.HasIPv4Internet(); // skip the header foreach (string serverInfo in serverList.Skip(1)) { try { - var tunnel = CnCNetTunnel.Parse(serverInfo); + var tunnel = CnCNetTunnel.Parse(serverInfo, hasIPv6Internet, hasIPv4Internet); if (tunnel == null) continue; From 8ad96dcdcaa482d5e4e8f101cc511e6696d412cd Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 10 Jan 2023 21:22:29 +0100 Subject: [PATCH 090/109] Fix extracting map preview --- .../DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs | 10 +++++----- DXMainClient/Domain/Multiplayer/Map.cs | 10 ++++------ DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs | 4 ++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs index 6160c18b1..39ffa8e38 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MapPreviewBox.cs @@ -93,7 +93,7 @@ public void SetFields(List players, List aiPlayers, List AddChild(briefingBox); briefingBox.Disable(); - ClientRectangleUpdated += (_, _) => UpdateMapAsync().HandleTask(); + ClientRectangleUpdated += (_, _) => UpdateMap(); } private GameModeMap _gameModeMap; @@ -103,7 +103,7 @@ public GameModeMap GameModeMap set { _gameModeMap = value; - UpdateMapAsync().HandleTask(); + UpdateMap(); } } @@ -220,7 +220,7 @@ public override void Initialize() base.Initialize(); - ClientRectangleUpdated += (_, _) => UpdateMapAsync().HandleTask(); + ClientRectangleUpdated += (_, _) => UpdateMap(); RightClick += MapPreviewBox_RightClick; @@ -379,7 +379,7 @@ private void Indicator_RightClick(object sender, EventArgs e) /// this control's display rectangle and the /// starting location indicators' positions. /// - private async ValueTask UpdateMapAsync() + private void UpdateMap() { if (disposeTextures && previewTexture != null && !previewTexture.IsDisposed) previewTexture.Dispose(); @@ -401,7 +401,7 @@ private async ValueTask UpdateMapAsync() if (GameModeMap.Map.PreviewTexture == null) { - previewTexture = await GameModeMap.Map.LoadPreviewTextureAsync().ConfigureAwait(true); + previewTexture = GameModeMap.Map.LoadPreviewTexture(); disposeTextures = true; } else diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index 363d821ca..c520e3cd1 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -12,10 +12,8 @@ using System.Threading.Tasks; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; -using Exception = System.Exception; using Point = Microsoft.Xna.Framework.Point; -using Utilities = Rampastring.Tools.Utilities; -using static System.Collections.Specialized.BitVector32; +using ClientCore.Extensions; namespace DTAClient.Domain.Multiplayer { @@ -405,7 +403,7 @@ public async ValueTask SetInfoFromMpMapsINIAsync(IniFile iniFile) #if !GL if (UserINISettings.Instance.PreloadMapPreviews) - PreviewTexture = await LoadPreviewTextureAsync().ConfigureAwait(false); + PreviewTexture = LoadPreviewTexture(); #endif // Parse forced options @@ -684,7 +682,7 @@ private void ParseSpawnIniOptions(IniFile forcedOptionsIni, string spawnIniOptio /// /// Loads and returns the map preview texture. /// - public async ValueTask LoadPreviewTextureAsync() + public Texture2D LoadPreviewTexture() { if (SafePath.GetFile(ProgramConstants.GamePath, PreviewPath).Exists) return AssetLoader.LoadTextureUncached(PreviewPath); @@ -692,7 +690,7 @@ public async ValueTask LoadPreviewTextureAsync() if (!Official) { // Extract preview from the map itself - using Image preview = await MapPreviewExtractor.ExtractMapPreviewAsync(GetCustomMapIniFile()).ConfigureAwait(true); + using Image preview = Task.Run(() => MapPreviewExtractor.ExtractMapPreviewAsync(GetCustomMapIniFile())).HandleTask().Result; if (preview != null) { diff --git a/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs b/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs index 91f33b043..82f34857e 100644 --- a/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs +++ b/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs @@ -25,7 +25,7 @@ public static class MapPreviewExtractor /// /// Map file. /// Bitmap of map preview image, or null if preview could not be extracted. - public static async ValueTask ExtractMapPreviewAsync(IniFile mapIni) + public static async Task ExtractMapPreviewAsync(IniFile mapIni) { List sectionKeys = mapIni.GetSectionKeys("PreviewPack"); @@ -81,7 +81,7 @@ public static async ValueTask ExtractMapPreviewAsync(IniFile mapIni) return null; } - (Image bitmap, errorMessage) = await Task.Run(() => CreatePreviewBitmapFromImageData(previewWidth, previewHeight, dataDest)).ConfigureAwait(false); + (Image bitmap, errorMessage) = CreatePreviewBitmapFromImageData(previewWidth, previewHeight, dataDest); if (errorMessage != null) { From 30b3064675fdc74cc720af47bd98ddd6f29cabb6 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 13 Jan 2023 18:09:54 +0100 Subject: [PATCH 091/109] Update master server url --- ClientCore/ProgramConstants.cs | 2 +- DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index b93fee17a..f9e4ec3e4 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -65,7 +65,7 @@ public static class ProgramConstants public const string INI_NEWLINE_PATTERN = "@"; public const string REPLAYS_DIRECTORY = "Replays"; - public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/api/v1/master-list?nocache=1"; + public const string CNCNET_TUNNEL_LIST_URL = "https://cncnet.org/api/v1/master-list"; public const string CNCNET_DYNAMIC_TUNNELS = "DYNAMIC"; public const int GAME_ID_MAX_LENGTH = 4; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 6eaabd6ed..901f3442c 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -31,7 +31,7 @@ internal sealed class CnCNetTunnel /// A CnCNetTunnel instance parsed from the given string. public static CnCNetTunnel Parse(string str, bool hasIPv6Internet, bool hasIPv4Internet) { - // For the format, check https://cncnet.org/api/v1/master-list?nocache=1 + // For the format, check https://cncnet.org/api/v1/master-list try { var tunnel = new CnCNetTunnel(); From 7d45c610fd33f22f20a50b5a29b4ca6d320b68d3 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 16 Jan 2023 19:30:34 +0100 Subject: [PATCH 092/109] Add UPnP RemoteCertificateValidationCallback --- DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index ef2146266..8bc06da08 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using System.Net; using System.Net.Http; +using System.Net.Security; using System.Net.Sockets; using System.Runtime.Serialization; using System.Text; @@ -56,6 +57,7 @@ internal static class UPnPHandler }, SslOptions = new() { + RemoteCertificateValidationCallback = (_, _, _, sslPolicyErrors) => (sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) == 0, CertificateChainPolicy = new() { DisableCertificateDownloads = true From 8585f3d93303fbbec3b7494901ae40ae6df04923 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 20 Jan 2023 22:17:34 +0100 Subject: [PATCH 093/109] P2P: UPnP SSDP on all interfaces --- .github/workflows/build.yml | 4 +- ClientCore/ClientCore.csproj | 2 +- ClientGUI/ClientGUI.csproj | 2 +- DTAConfig/DTAConfig.csproj | 2 +- DXMainClient/DXMainClient.csproj | 2 +- .../Domain/Multiplayer/CnCNet/CnCNetTunnel.cs | 6 +- .../CnCNet/UPNP/InternetGatewayDevice.cs | 25 +++--- .../CnCNet/UPNP/Models/AddressType.cs | 12 --- .../Models/InternetGatewayDeviceResponse.cs | 2 +- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 80 +++++-------------- .../Domain/Multiplayer/NetworkHelper.cs | 41 ++++++++-- 11 files changed, 74 insertions(+), 104 deletions(-) delete mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61bdb1605..3586876e1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: Game: [Ares,TS,YR] steps: - - uses: actions/checkout@v3.2.0 + - uses: actions/checkout@v3.3.0 with: fetch-depth: 0 @@ -27,7 +27,7 @@ jobs: run: ./BuildScripts/Build-${{matrix.Game}}.ps1 shell: pwsh - - uses: actions/upload-artifact@v3.1.1 + - uses: actions/upload-artifact@v3.1.2 name: Upload Artifacts with: name: artifacts-${{matrix.Game}} diff --git a/ClientCore/ClientCore.csproj b/ClientCore/ClientCore.csproj index 9a70cd913..7e714fa04 100644 --- a/ClientCore/ClientCore.csproj +++ b/ClientCore/ClientCore.csproj @@ -46,7 +46,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ClientGUI/ClientGUI.csproj b/ClientGUI/ClientGUI.csproj index da1883b52..47978893a 100644 --- a/ClientGUI/ClientGUI.csproj +++ b/ClientGUI/ClientGUI.csproj @@ -17,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DTAConfig/DTAConfig.csproj b/DTAConfig/DTAConfig.csproj index 413b3ebfb..2cc998402 100644 --- a/DTAConfig/DTAConfig.csproj +++ b/DTAConfig/DTAConfig.csproj @@ -15,7 +15,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index d29520b4a..a78de4d69 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -37,7 +37,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs index 901f3442c..cc1d9c219 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/CnCNetTunnel.cs @@ -61,10 +61,8 @@ public static CnCNetTunnel Parse(string str, bool hasIPv6Internet, bool hasIPv4I } else { - Logger.Log($""" - No supported IP address/connection found ({nameof(NetworkHelper.HasIPv6Internet)}={hasIPv6Internet}, - {nameof(NetworkHelper.HasIPv4Internet)}={hasIPv4Internet}) for {primaryIpAddress} - {secondaryIpAddress}. - """); + Logger.Log($"No supported IP address/connection found ({nameof(NetworkHelper.HasIPv6Internet)}={hasIPv6Internet}, " + + $"{nameof(NetworkHelper.HasIPv4Internet)}={hasIPv4Internet}) for {primaryIpAddress} - {secondaryIpAddress}."); return null; } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs index a847b2488..cf18dfa79 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/InternetGatewayDevice.cs @@ -21,13 +21,8 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; internal sealed record InternetGatewayDevice( IEnumerable Locations, string Server, - string CacheControl, - string Ext, - string SearchTarget, - string UniqueServiceName, UPnPDescription UPnPDescription, - Uri PreferredLocation, - IReadOnlyCollection LocalIpAddresses) + Uri PreferredLocation) { private const uint IpLeaseTimeInSeconds = 4 * 60 * 60; private const ushort IanaUdpProtocolNumber = 17; @@ -135,8 +130,9 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance Logger.Log($"P2P: Received external IPv4 address."); #endif } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); } return ipAddress; @@ -172,8 +168,9 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance Logger.Log($"P2P: Received NAT status {natEnabled}."); } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); } return natEnabled; @@ -194,6 +191,8 @@ public async Task GetExternalIpV4AddressAsync(CancellationToken cance } catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) { + ProgramConstants.LogException(ex); + return (null, null); } } @@ -238,9 +237,7 @@ private async ValueTask DoSoapActionAsync( } catch (Exception ex) when (ex is not OperationCanceledException) { - ProgramConstants.LogException(ex, $"P2P: {action} error/not supported using {addressFamily}."); - - throw; + throw new($"P2P: {action} error/not supported using {addressFamily}.", ex); } } @@ -321,7 +318,9 @@ private static async ValueTask ExecuteSoapAction { AddressFamily.InterNetwork when Locations.Any(q => q.HostNameType is UriHostNameType.IPv4) => Locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv4), - AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNameType.IPv6) => + AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNameType.IPv6 && !NetworkHelper.IsPrivateIpAddress(IPAddress.Parse(q.IdnHost))) => + Locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6), + AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNameType.IPv6 && NetworkHelper.IsPrivateIpAddress(IPAddress.Parse(q.IdnHost))) => Locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6), _ => PreferredLocation }; @@ -330,7 +329,7 @@ AddressFamily.InterNetworkV6 when Locations.Any(q => q.HostNameType is UriHostNa Device wanConnectionDevice = wanDevice.DeviceList.Single(q => q.DeviceType.Equals($"{UPnPConstants.UPnPWanConnectionDevice}:{uPnPVersion}", StringComparison.OrdinalIgnoreCase)); string serviceType = $"{UPnPConstants.UPnPServiceNamespace}:{wanConnectionDeviceService}"; ServiceListItem wanIpConnectionService = wanConnectionDevice.ServiceList.Single(q => q.ServiceType.Equals(serviceType, StringComparison.OrdinalIgnoreCase)); - var serviceUri = new Uri(FormattableString.Invariant($"{location.Scheme}://{(location.HostNameType is UriHostNameType.IPv6 ? '[' : null)}{location.IdnHost}{(location.HostNameType is UriHostNameType.IPv6 ? ']' : null)}:{location.Port}{wanIpConnectionService.ControlUrl}")); + Uri serviceUri = NetworkHelper.FormatUri(location.Scheme, location, (ushort)location.Port, wanIpConnectionService.ControlUrl); return new(wanIpConnectionService, serviceUri, serviceType); } diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs deleted file mode 100644 index 50d0e5181..000000000 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/AddressType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; - -internal enum AddressType -{ - Unknown, - - IpV4SiteLocal, - - IpV6LinkLocal, - - IpV6SiteLocal -} \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs index f0c62e257..e1701ea98 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/Models/InternetGatewayDeviceResponse.cs @@ -3,4 +3,4 @@ namespace DTAClient.Domain.Multiplayer.CnCNet.UPNP; -internal readonly record struct InternetGatewayDeviceResponse(Uri Location, string Server, string CacheControl, string Ext, string SearchTarget, string Usn, IPAddress LocalIpAddress); \ No newline at end of file +internal readonly record struct InternetGatewayDeviceResponse(Uri Location, string Server, string Usn, IPAddress LocalIpAddress); \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index 8bc06da08..a120b7e07 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -70,14 +70,6 @@ internal static class UPnPHandler DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher }; - private static IReadOnlyDictionary SsdpMultiCastAddresses - => new Dictionary - { - [AddressType.IpV4SiteLocal] = IPAddress.Parse("239.255.255.250"), - [AddressType.IpV6LinkLocal] = IPAddress.Parse("[FF02::C]"), - [AddressType.IpV6SiteLocal] = IPAddress.Parse("[FF05::C]") - }.AsReadOnly(); - public static async ValueTask<( InternetGatewayDevice InternetGatewayDevice, List<(ushort InternalPort, ushort ExternalPort)> IpV6P2PPorts, @@ -113,10 +105,8 @@ private static async Task GetInternetGatewayDeviceAsync(C foreach (InternetGatewayDevice internetGatewayDevice in internetGatewayDevices) { - Logger.Log($""" - P2P: Found gateway device v{internetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()} - {internetGatewayDevice.UPnPDescription.Device.FriendlyName} ({internetGatewayDevice.Server}). - """); + Logger.Log($"P2P: Found gateway device v{internetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()}" + + $"{internetGatewayDevice.UPnPDescription.Device.FriendlyName} ({internetGatewayDevice.Server})."); } InternetGatewayDevice selectedInternetGatewayDevice = GetInternetGatewayDeviceByVersion(internetGatewayDevices, 2); @@ -125,10 +115,8 @@ private static async Task GetInternetGatewayDeviceAsync(C if (selectedInternetGatewayDevice is not null) { - Logger.Log($""" - P2P: Selected gateway device v{selectedInternetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()} - {selectedInternetGatewayDevice.UPnPDescription.Device.FriendlyName} ({selectedInternetGatewayDevice.Server}). - """); + Logger.Log($"P2P: Selected gateway device v{selectedInternetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()}" + + $"{selectedInternetGatewayDevice.UPnPDescription.Device.FriendlyName} ({selectedInternetGatewayDevice.Server})."); } else { @@ -295,11 +283,15 @@ private static async ValueTask> GetDevicesAsy private static IEnumerable> GetGroupedDeviceResponses( IEnumerable<(IPAddress LocalIpAddress, IEnumerable> Responses)> formattedDeviceResponses) => formattedDeviceResponses - .SelectMany(q => q.Responses.Select(r => new InternetGatewayDeviceResponse(new(r["LOCATION"]), r["SERVER"], r["CACHE-CONTROL"], r["EXT"], r["ST"], r["USN"], q.LocalIpAddress))) + .SelectMany(q => q.Responses.Select(r => new InternetGatewayDeviceResponse(new(r["LOCATION"]), r["SERVER"], r["USN"], q.LocalIpAddress))) .GroupBy(q => q.Usn); private static Uri GetPreferredLocation(IReadOnlyCollection locations) - => locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6) ?? locations.First(q => q.HostNameType is UriHostNameType.IPv4); + { + return locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6 && !NetworkHelper.IsPrivateIpAddress(IPAddress.Parse(q.IdnHost))) + ?? locations.FirstOrDefault(q => q.HostNameType is UriHostNameType.IPv6 && NetworkHelper.IsPrivateIpAddress(IPAddress.Parse(q.IdnHost))) + ?? locations.First(q => q.HostNameType is UriHostNameType.IPv4); + } private static IEnumerable> GetFormattedDeviceResponses(IEnumerable responses) { @@ -317,23 +309,18 @@ private static IEnumerable> GetFormattedDeviceRespons StringComparer.OrdinalIgnoreCase)); } - private static async Task<(IPAddress LocalIpAddress, IEnumerable Responses)> SearchDevicesAsync(IPAddress localAddress, CancellationToken cancellationToken) + private static async Task<(IPAddress LocalIpAddress, IEnumerable Responses)> SearchDevicesAsync(IPAddress localAddress, IPAddress multicastAddress, CancellationToken cancellationToken) { var responses = new List(); - AddressType addressType = GetAddressType(localAddress); - - if (addressType is AddressType.Unknown) - return new(localAddress, responses); - - using var socket = new Socket(localAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + using var socket = new Socket(SocketType.Dgram, ProtocolType.Udp); var localEndPoint = new IPEndPoint(localAddress, 0); - var multiCastIpEndPoint = new IPEndPoint(SsdpMultiCastAddresses[addressType], UPnPConstants.UPnPMultiCastPort); + var multiCastIpEndPoint = new IPEndPoint(multicastAddress, UPnPConstants.UPnPMultiCastPort); try { socket.Bind(localEndPoint); - string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {multiCastIpEndPoint}\r\nST: {UPnPConstants.UPnPRootDevice}\r\nMAN: \"ssdp:discover\"\r\nMX: {ReceiveTimeoutInSeconds}\r\n\r\n"); + string request = FormattableString.Invariant($"M-SEARCH * HTTP/1.1\r\nHOST: {NetworkHelper.FormatUri(multiCastIpEndPoint).Authority}\r\nST: {UPnPConstants.UPnPRootDevice}\r\nMAN: \"ssdp:discover\"\r\nMX: {ReceiveTimeoutInSeconds}\r\n\r\n"); const int charSize = sizeof(char); int bufferSize = request.Length * charSize; using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(bufferSize); @@ -356,20 +343,6 @@ private static IEnumerable> GetFormattedDeviceRespons return new(localAddress, responses); } - private static AddressType GetAddressType(IPAddress localAddress) - { - if (localAddress.AddressFamily is AddressFamily.InterNetwork) - return AddressType.IpV4SiteLocal; - - if (localAddress.IsIPv6LinkLocal) - return AddressType.IpV6LinkLocal; - - if (localAddress.IsIPv6SiteLocal) - return AddressType.IpV6SiteLocal; - - return AddressType.Unknown; - } - private static async ValueTask ReceiveAsync(Socket socket, ICollection responses, CancellationToken cancellationToken) { using IMemoryOwner memoryOwner = MemoryPool.Shared.Rent(4096); @@ -406,9 +379,10 @@ private static async ValueTask GetDescriptionAsync(Uri uri, Can private static async ValueTask Responses)>> DetectDevicesAsync(CancellationToken cancellationToken) { - IEnumerable localAddresses = NetworkHelper.GetLocalAddresses(); + IEnumerable unicastAddresses = NetworkHelper.GetLocalAddresses(); + IEnumerable multicastAddresses = NetworkHelper.GetMulticastAddresses(); (IPAddress LocalIpAddress, IEnumerable Responses)[] localAddressesDeviceResponses = await ClientCore.Extensions.TaskExtensions.WhenAllSafe( - localAddresses.Select(q => SearchDevicesAsync(q, cancellationToken))).ConfigureAwait(false); + multicastAddresses.SelectMany(q => unicastAddresses.Where(r => r.AddressFamily == q.AddressFamily).Select(r => SearchDevicesAsync(r, q, cancellationToken)))).ConfigureAwait(false); return localAddressesDeviceResponses.Where(q => q.Responses.Any(r => r.Any())).Select(q => (q.LocalIpAddress, q.Responses)).Distinct(); } @@ -447,13 +421,8 @@ private static async Task ParseDeviceAsync( return new( locations, internetGatewayDeviceResponses.Select(r => r.Server).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.CacheControl).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.Ext).Distinct().Single(), - internetGatewayDeviceResponses.Select(r => r.SearchTarget).Distinct().Single(), - internetGatewayDeviceResponses.Key, uPnPDescription, - preferredLocation, - internetGatewayDeviceResponses.Select(r => r.LocalIpAddress).Distinct().ToList().AsReadOnly()); + preferredLocation); } catch (Exception ex) { @@ -465,18 +434,9 @@ private static async Task ParseDeviceAsync( private static Uri ParseLocation((IPAddress LocalIpAddress, Uri Location) location) { - if (location.Location.HostNameType is not UriHostNameType.IPv6 || !IPAddress.TryParse(location.Location.Host, out IPAddress ipAddress) - || !NetworkHelper.IsPrivateIpAddress(ipAddress)) - { + if (location.Location.HostNameType is not UriHostNameType.IPv6 || !IPAddress.TryParse(location.Location.IdnHost, out IPAddress ipAddress) || !NetworkHelper.IsPrivateIpAddress(ipAddress)) return location.Location; - } - - var uriBuilder = new UriBuilder(location.Location); - - uriBuilder.Host = FormattableString.Invariant($"[{uriBuilder.Host - .Replace("[", null, StringComparison.OrdinalIgnoreCase) - .Replace("]", null, StringComparison.OrdinalIgnoreCase)}%{location.LocalIpAddress.ScopeId}]"); - return uriBuilder.Uri; + return NetworkHelper.FormatUri(new(IPAddress.Parse(FormattableString.Invariant($"{location.Location.IdnHost}%{location.LocalIpAddress.ScopeId}")), location.Location.Port), location.Location.Scheme, location.Location.PathAndQuery); } } \ No newline at end of file diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index d7ea8ecb1..8bb27a7de 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -34,7 +34,7 @@ public static bool HasIPv4Internet() public static IEnumerable GetLocalAddresses() => GetUniCastIpAddresses() - .Select(q => q.UnicastIPAddressInformation.Address); + .Select(q => q.Address); public static IEnumerable GetPublicIpAddresses() => GetLocalAddresses() @@ -54,12 +54,38 @@ public static IEnumerable GetLanUniCastIpAddresses( .SelectMany(q => q.UnicastAddresses) .Where(q => SupportedAddressFamilies.Contains(q.Address.AddressFamily)); - private static IEnumerable<(UnicastIPAddressInformation UnicastIPAddressInformation, GatewayIPAddressInformation GatewayIPAddressInformation)> GetUniCastIpAddresses() + public static IEnumerable GetMulticastAddresses() + => GetIpInterfaces() + .SelectMany(q => q.MulticastAddresses.Select(r => r.Address)) + .Where(q => SupportedAddressFamilies.Contains(q.AddressFamily)); + + public static Uri FormatUri(string scheme, Uri uri, ushort port, string path) + { + string[] pathAndQuery = path.Split('?'); + var uriBuilder = new UriBuilder(uri) + { + Scheme = scheme, + Host = uri.IdnHost, + Port = port, + Path = pathAndQuery.First(), + Query = pathAndQuery.Skip(1).SingleOrDefault() + }; + + return uriBuilder.Uri; + } + + public static Uri FormatUri(IPEndPoint ipEndPoint, string scheme = null, string path = null) + { + var uriBuilder = new UriBuilder(scheme ?? Uri.UriSchemeHttps, ipEndPoint.Address.ToString(), ipEndPoint.Port, path); + + return uriBuilder.Uri; + } + + private static IEnumerable GetUniCastIpAddresses() => GetIpInterfaces() .Where(q => q.GatewayAddresses.Any()) - .SelectMany(q => q.UnicastAddresses.Select( - r => (UnicastIPAddressInformation: r, GatewayIPAddressInformation: q.GatewayAddresses.FirstOrDefault(s => s.Address.AddressFamily == r.Address.AddressFamily)))) - .Where(q => SupportedAddressFamilies.Contains(q.UnicastIPAddressInformation.Address.AddressFamily)); + .SelectMany(q => q.UnicastAddresses) + .Where(q => SupportedAddressFamilies.Contains(q.Address.AddressFamily)); private static IEnumerable GetIpInterfaces() => NetworkInterface.GetAllNetworkInterfaces() @@ -69,8 +95,8 @@ private static IEnumerable GetIpInterfaces() [SupportedOSPlatform("windows")] private static IEnumerable<(IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin)> GetWindowsPublicIpAddresses() => GetUniCastIpAddresses() - .Where(q => !IsPrivateIpAddress(q.UnicastIPAddressInformation.Address)) - .Select(q => (q.UnicastIPAddressInformation.Address, q.UnicastIPAddressInformation.PrefixOrigin, q.UnicastIPAddressInformation.SuffixOrigin)); + .Where(q => !IsPrivateIpAddress(q.Address)) + .Select(q => (q.Address, q.PrefixOrigin, q.SuffixOrigin)); public static IPAddress GetIpV4BroadcastAddress(UnicastIPAddressInformation unicastIpAddressInformation) { @@ -257,7 +283,6 @@ public static bool IsPrivateIpAddress(IPAddress ipAddress) || ipAddress.IsIPv6UniqueLocal || ipAddress.IsIPv6LinkLocal, AddressFamily.InterNetwork => IsInRange("10.0.0.0", "10.255.255.255", ipAddress) - || IsInRange("172.16.0.0", "172.31.255.255", ipAddress) || IsInRange("172.16.0.0", "172.31.255.255", ipAddress) || IsInRange("192.168.0.0", "192.168.255.255", ipAddress) || IsInRange("169.254.0.0", "169.254.255.255", ipAddress) From 3920e29336f1c91ef239caa4cc28d71eb632800d Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 24 Jan 2023 17:58:43 +0100 Subject: [PATCH 094/109] Update packages --- DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 1bdfdbeb3..c416770a5 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -181,7 +181,7 @@ protected GameModeMap GameModeMap private LoadOrSaveGameOptionPresetWindow loadOrSaveGameOptionPresetWindow; - private XNAMultiColumnListBox.SelectedIndexChangedEventHandler lbGameModeMapList_SelectedIndexChangedFunc; + private EventHandler lbGameModeMapList_SelectedIndexChangedFunc; public override void Initialize() { From 1fed956a11c8f2d0223f5f49fa6fab9175ed019f Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 13 Feb 2023 11:21:38 +0100 Subject: [PATCH 095/109] MultipleInstanceMode mutex fix and some cleanups --- .github/workflows/pr-build-comment.yml | 2 +- ClientCore/ClientCore.csproj | 3 -- ClientCore/ProgramConstants.cs | 2 +- ClientGUI/ClientGUI.csproj | 3 -- ClientGUI/GameProcessLogic.cs | 20 ++++++---- DTAConfig/DTAConfig.csproj | 3 -- DXClient.sln | 3 ++ DXMainClient/DXGUI/Generic/MainMenu.cs | 37 ++++++++----------- DXMainClient/DXMainClient.csproj | 5 +-- .../CnCNet/Replays/ReplayHandler.cs | 6 +-- .../Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 4 +- .../Domain/Multiplayer/NetworkHelper.cs | 4 +- DXMainClient/Program.cs | 28 ++++---------- 13 files changed, 46 insertions(+), 74 deletions(-) diff --git a/.github/workflows/pr-build-comment.yml b/.github/workflows/pr-build-comment.yml index 9e933ccef..745dcaae8 100644 --- a/.github/workflows/pr-build-comment.yml +++ b/.github/workflows/pr-build-comment.yml @@ -8,7 +8,7 @@ jobs: if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-22.04 steps: - - uses: actions/github-script@v6.3.3 + - uses: actions/github-script@v6.4.0 with: # This snippet is public-domain, taken from # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml diff --git a/ClientCore/ClientCore.csproj b/ClientCore/ClientCore.csproj index 7e714fa04..59e2108d1 100644 --- a/ClientCore/ClientCore.csproj +++ b/ClientCore/ClientCore.csproj @@ -6,9 +6,6 @@ CnCNet Client Copyright © CnCNet, Rampastring 2011-2023 CnCNet - 2.0.0.3 - 2.0.0.3 - 2.0.0.3 ClientCore ClientCore diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index f9e4ec3e4..e1f7510da 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -201,7 +201,7 @@ public static void HandleException(Exception ex) string error = string.Format("{0} has crashed. Error message:".L10N("Client:Main:FatalErrorText1") + Environment.NewLine + Environment.NewLine + ex.Message + Environment.NewLine + Environment.NewLine + (crashLogCopied ? "A crash log has been saved to the following file:".L10N("Client:Main:FatalErrorText2") + " " + Environment.NewLine + Environment.NewLine + - errorLogPath + Environment.NewLine + Environment.NewLine : "") + + errorLogPath + Environment.NewLine + Environment.NewLine : string.Empty) + (crashLogCopied ? "If the issue is repeatable, contact the {1} staff at {2} and provide the crash log file.".L10N("Client:Main:FatalErrorText3") : "If the issue is repeatable, contact the {1} staff at {2}.".L10N("Client:Main:FatalErrorText4")), GAME_NAME_LONG, diff --git a/ClientGUI/ClientGUI.csproj b/ClientGUI/ClientGUI.csproj index 47978893a..12deca6c5 100644 --- a/ClientGUI/ClientGUI.csproj +++ b/ClientGUI/ClientGUI.csproj @@ -6,9 +6,6 @@ CnCNet Client Copyright © CnCNet, Rampastring 2011-2023 CnCNet - 2.1.0.1 - 2.1.0.1 - 2.1.0.1 ClientGUI ClientGUI diff --git a/ClientGUI/GameProcessLogic.cs b/ClientGUI/GameProcessLogic.cs index d7e1e66d5..a12227201 100644 --- a/ClientGUI/GameProcessLogic.cs +++ b/ClientGUI/GameProcessLogic.cs @@ -48,17 +48,20 @@ public static async ValueTask StartGameProcessAsync(WindowManager windowManager) } OSVersion osVersion = ClientConfiguration.Instance.GetOperatingSystemVersion(); - string gameExecutableName; string additionalExecutableName = string.Empty; - if (osVersion == OSVersion.UNIX) + if (osVersion is OSVersion.UNIX) + { gameExecutableName = ClientConfiguration.Instance.UnixGameExecutableName; + } else { string launcherExecutableName = ClientConfiguration.Instance.GameLauncherExecutableName; if (string.IsNullOrEmpty(launcherExecutableName)) + { gameExecutableName = ClientConfiguration.Instance.GetGameExecutableName(); + } else { gameExecutableName = launcherExecutableName; @@ -103,7 +106,7 @@ public static async ValueTask StartGameProcessAsync(WindowManager windowManager) } if (Environment.ProcessorCount > 1 && SingleCoreAffinity) - QResProcess.ProcessorAffinity = (IntPtr)2; + QResProcess.ProcessorAffinity = 2; } else { @@ -149,17 +152,18 @@ public static async ValueTask StartGameProcessAsync(WindowManager windowManager) } GameProcessStarted?.Invoke(); - Logger.Log("Waiting for qres.dat or " + gameExecutableName + " to exit."); } - static void Process_Exited(object sender, EventArgs e) + private static void Process_Exited(object sender, EventArgs e) { Logger.Log("GameProcessLogic: Process exited."); - Process proc = (Process)sender; + + using var proc = (Process)sender; + proc.Exited -= Process_Exited; - proc.Dispose(); + GameProcessExited?.Invoke(); } } -} +} \ No newline at end of file diff --git a/DTAConfig/DTAConfig.csproj b/DTAConfig/DTAConfig.csproj index 2cc998402..be4f83a67 100644 --- a/DTAConfig/DTAConfig.csproj +++ b/DTAConfig/DTAConfig.csproj @@ -6,9 +6,6 @@ CnCNet Client Copyright © CnCNet, Rampastring 2011-2023 CnCNet - 2.2.0.0 - 2.2.0.0 - 2.2.0.0 DTAConfig DTAConfig diff --git a/DXClient.sln b/DXClient.sln index c28cff82e..495431d76 100644 --- a/DXClient.sln +++ b/DXClient.sln @@ -15,10 +15,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig build\AfterPublish.targets = build\AfterPublish.targets + .github\workflows\build.yml = .github\workflows\build.yml build\CopyResources.targets = build\CopyResources.targets Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets build\Framework.props = build\Framework.props + .github\workflows\pr-build-comment.yml = .github\workflows\pr-build-comment.yml + README.md = README.md build\VSCompatibleLayer.props = build\VSCompatibleLayer.props build\WinForms.props = build\WinForms.props EndProjectSection diff --git a/DXMainClient/DXGUI/Generic/MainMenu.cs b/DXMainClient/DXGUI/Generic/MainMenu.cs index 8b4ee3f93..bf3fcf68c 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -229,7 +229,7 @@ public override void Initialize() btnExit.IdleTexture = AssetLoader.LoadTexture("MainMenu/exitgame.png"); btnExit.HoverTexture = AssetLoader.LoadTexture("MainMenu/exitgame_c.png"); btnExit.HoverSoundEffect = new EnhancedSoundEffect("MainMenu/button.wav"); - btnExit.LeftClick += (_, _) => BtnExit_LeftClickAsync().HandleTask(); + btnExit.LeftClick += (_, _) => BtnExit_LeftClick(); XNALabel lblCnCNetStatus = new XNALabel(WindowManager); lblCnCNetStatus.Name = nameof(lblCnCNetStatus); @@ -317,7 +317,7 @@ public override void Initialize() GameProcessLogic.GameProcessStarted += SharedUILogic_GameProcessStarted; GameProcessLogic.GameProcessStarting += SharedUILogic_GameProcessStarting; UserINISettings.Instance.SettingsSaved += SettingsSaved; - Updater.Restart += (_, _) => WindowManager.AddCallback(() => ExitClientAsync().HandleTask()); + Updater.Restart += (_, _) => WindowManager.AddCallback(ExitClient); SetButtonHotkeys(true); } @@ -597,9 +597,8 @@ public void PostInit() CheckIfFirstRun(); } - private void SwitchMainMenuMusicFormat() + private static void SwitchMainMenuMusicFormat() { -#if GL || DX FileInfo wmaMainMenuMusicFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH, FormattableString.Invariant($"{ClientConfiguration.Instance.MainMenuMusicName}.wma")); @@ -612,16 +611,14 @@ private void SwitchMainMenuMusicFormat() if (!wmaBackupMainMenuMusicFile.Exists) wmaMainMenuMusicFile.CopyTo(wmaBackupMainMenuMusicFile.FullName); -#endif -#if DX - if (!wmaMainMenuMusicFile.Exists) - wmaBackupMainMenuMusicFile.CopyTo(wmaMainMenuMusicFile.FullName); -#elif GL +#if !GL + wmaBackupMainMenuMusicFile.CopyTo(wmaMainMenuMusicFile.FullName, true); +#else FileInfo oggMainMenuMusicFile = SafePath.GetFile(ProgramConstants.GamePath, ProgramConstants.BASE_RESOURCE_PATH, FormattableString.Invariant($"{ClientConfiguration.Instance.MainMenuMusicName}.ogg")); - if (oggMainMenuMusicFile.Exists && !wmaMainMenuMusicFile.Exists) - oggMainMenuMusicFile.CopyTo(wmaMainMenuMusicFile.FullName); + if (oggMainMenuMusicFile.Exists) + oggMainMenuMusicFile.CopyTo(wmaMainMenuMusicFile.FullName, true); #endif } @@ -863,12 +860,12 @@ private void BtnCredits_LeftClick(object sender, EventArgs e) private void BtnExtras_LeftClick(object sender, EventArgs e) => innerPanel.Show(innerPanel.ExtrasWindow); - private ValueTask BtnExit_LeftClickAsync() + private void BtnExit_LeftClick() { #if WINFORMS WindowManager.HideWindow(); #endif - return FadeMusicExitAsync(); + FadeMusicExit(); } private void SharedUILogic_GameProcessExited() => @@ -964,11 +961,11 @@ private void FadeMusic(GameTime gameTime) /// /// Exits the client. Quickly fades the music if it's playing. /// - private async ValueTask FadeMusicExitAsync() + private void FadeMusicExit() { if (!isMediaPlayerAvailable || themeSong == null) { - await ExitClientAsync().ConfigureAwait(false); + ExitClient(); return; } @@ -977,24 +974,20 @@ private async ValueTask FadeMusicExitAsync() if (MediaPlayer.Volume > step) { MediaPlayer.Volume -= step; - AddCallback(() => FadeMusicExitAsync().HandleTask()); + AddCallback(FadeMusicExit); } else { MediaPlayer.Stop(); - await ExitClientAsync().ConfigureAwait(false); + ExitClient(); } } - private async ValueTask ExitClientAsync() + private void ExitClient() { Logger.Log("Exiting."); WindowManager.CloseGame(); themeSong?.Dispose(); -#if !XNA - await Task.Delay(1000).ConfigureAwait(false); - Environment.Exit(0); -#endif } public void SwitchOn() diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index a78de4d69..d09b761ce 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -8,9 +8,6 @@ CnCNet Client Copyright © CnCNet, Rampastring 2011-2023 CnCNet - 2.8.0.0 - 2.8.0.0 - 2.8.0.0 DXMainClient DTAClient clienticon.ico @@ -48,7 +45,7 @@ - + diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs index 9ca898c5c..fc38e09bd 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/Replays/ReplayHandler.cs @@ -90,9 +90,8 @@ public async ValueTask StopRecordingAsync(List gamePlayerIds, List GetInternetGatewayDeviceAsync(C foreach (InternetGatewayDevice internetGatewayDevice in internetGatewayDevices) { - Logger.Log($"P2P: Found gateway device v{internetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()}" + Logger.Log($"P2P: Found gateway device v{internetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()} " + $"{internetGatewayDevice.UPnPDescription.Device.FriendlyName} ({internetGatewayDevice.Server})."); } @@ -115,7 +115,7 @@ private static async Task GetInternetGatewayDeviceAsync(C if (selectedInternetGatewayDevice is not null) { - Logger.Log($"P2P: Selected gateway device v{selectedInternetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()}" + Logger.Log($"P2P: Selected gateway device v{selectedInternetGatewayDevice.UPnPDescription.Device.DeviceType.Split(':').LastOrDefault()} " + $"{selectedInternetGatewayDevice.UPnPDescription.Device.FriendlyName} ({selectedInternetGatewayDevice.Server})."); } else diff --git a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs index 8bb27a7de..a5e39ece2 100644 --- a/DXMainClient/Domain/Multiplayer/NetworkHelper.cs +++ b/DXMainClient/Domain/Multiplayer/NetworkHelper.cs @@ -83,14 +83,14 @@ public static Uri FormatUri(IPEndPoint ipEndPoint, string scheme = null, string private static IEnumerable GetUniCastIpAddresses() => GetIpInterfaces() - .Where(q => q.GatewayAddresses.Any()) .SelectMany(q => q.UnicastAddresses) .Where(q => SupportedAddressFamilies.Contains(q.Address.AddressFamily)); private static IEnumerable GetIpInterfaces() => NetworkInterface.GetAllNetworkInterfaces() .Where(q => q.OperationalStatus is OperationalStatus.Up && q.NetworkInterfaceType is not NetworkInterfaceType.Loopback) - .Select(q => q.GetIPProperties()); + .Select(q => q.GetIPProperties()) + .Where(q => q.GatewayAddresses.Any()); [SupportedOSPlatform("windows")] private static IEnumerable<(IPAddress IpAddress, PrefixOrigin PrefixOrigin, SuffixOrigin SuffixOrigin)> GetWindowsPublicIpAddresses() diff --git a/DXMainClient/Program.cs b/DXMainClient/Program.cs index b8bfe3079..6a7fd6f1b 100644 --- a/DXMainClient/Program.cs +++ b/DXMainClient/Program.cs @@ -10,7 +10,7 @@ namespace DTAClient { - static class Program + internal static class Program { #if !DEBUG static Program() @@ -50,7 +50,7 @@ Yuri has won #if WINFORMS [STAThread] #endif - static void Main(string[] args) + private static void Main(string[] args) { bool noAudio = false; bool multipleInstanceMode = false; @@ -74,18 +74,6 @@ static void Main(string[] args) } } - var parameters = new StartupParams(noAudio, multipleInstanceMode, unknownStartupParams); - - if (multipleInstanceMode) - { - // Proceed to client startup - PreStartup.Initialize(parameters); - return; - } - - // We're a single instance application! - // http://stackoverflow.com/questions/229565/what-is-a-good-pattern-for-using-a-global-mutex-in-c/229567 - // Global prefix means that the mutex is global to the machine string mutexId = FormattableString.Invariant($"Global{Guid.Parse("1CC9F8E7-9F69-4BBC-B045-E734204027A9")}"); using var mutex = new Mutex(false, mutexId, out _); bool hasHandle = false; @@ -95,19 +83,17 @@ static void Main(string[] args) try { hasHandle = mutex.WaitOne(8000, false); - if (hasHandle == false) - throw new TimeoutException("Timeout waiting for exclusive access"); + + if (hasHandle is false && !multipleInstanceMode) + return; } catch (AbandonedMutexException) { hasHandle = true; } - catch (TimeoutException) - { - return; - } - // Proceed to client startup + var parameters = new StartupParams(noAudio, multipleInstanceMode, unknownStartupParams); + PreStartup.Initialize(parameters); } finally From 57038229167e5356efe15cac4ff409e9adb3d3dd Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 18 Feb 2023 15:28:25 +0100 Subject: [PATCH 096/109] Update packages & sh files --- .gitattributes | 1 + DXMainClient/DXMainClient.csproj | 4 ++-- DXMainClient/Resources/wine-dta.sh | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index 7ca0b3c27..fc4ab00e3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,7 @@ ############################################################################### * text=auto *.ps1 eol=lf +*.sh eol=lf ############################################################################### # Set default behavior for command prompt diff. diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index d09b761ce..039ee26c9 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -30,10 +30,10 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/DXMainClient/Resources/wine-dta.sh b/DXMainClient/Resources/wine-dta.sh index 3a60fa6ac..c40c6b551 100644 --- a/DXMainClient/Resources/wine-dta.sh +++ b/DXMainClient/Resources/wine-dta.sh @@ -1,2 +1,2 @@ -#!/bin/sh +#!/bin/sh wine gamemd-spawn.exe $* \ No newline at end of file From 26f11365bd1ee233cbcc756ac0567ab5d8dbf9aa Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 22 Feb 2023 14:26:17 +0100 Subject: [PATCH 097/109] Map preview clean up --- DXMainClient/Domain/Multiplayer/Map.cs | 4 +++- DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/Map.cs b/DXMainClient/Domain/Multiplayer/Map.cs index c520e3cd1..47b2e5562 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -14,6 +14,7 @@ using Color = Microsoft.Xna.Framework.Color; using Point = Microsoft.Xna.Framework.Point; using ClientCore.Extensions; +using SixLabors.ImageSharp.PixelFormats; namespace DTAClient.Domain.Multiplayer { @@ -690,7 +691,8 @@ public Texture2D LoadPreviewTexture() if (!Official) { // Extract preview from the map itself - using Image preview = Task.Run(() => MapPreviewExtractor.ExtractMapPreviewAsync(GetCustomMapIniFile())).HandleTask().Result; + // Logic should be refactored to not run on UI thread, for now use blocking call + using Image preview = Task.Run(() => MapPreviewExtractor.ExtractMapPreviewAsync(GetCustomMapIniFile())).HandleTask().Result; if (preview != null) { diff --git a/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs b/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs index 82f34857e..31ca6f5f2 100644 --- a/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs +++ b/DXMainClient/Domain/Multiplayer/MapPreviewExtractor.cs @@ -25,7 +25,7 @@ public static class MapPreviewExtractor /// /// Map file. /// Bitmap of map preview image, or null if preview could not be extracted. - public static async Task ExtractMapPreviewAsync(IniFile mapIni) + public static async Task> ExtractMapPreviewAsync(IniFile mapIni) { List sectionKeys = mapIni.GetSectionKeys("PreviewPack"); @@ -81,7 +81,7 @@ public static async Task ExtractMapPreviewAsync(IniFile mapIni) return null; } - (Image bitmap, errorMessage) = CreatePreviewBitmapFromImageData(previewWidth, previewHeight, dataDest); + (Image bitmap, errorMessage) = CreatePreviewBitmapFromImageData(previewWidth, previewHeight, dataDest); if (errorMessage != null) { @@ -153,7 +153,7 @@ public static async Task ExtractMapPreviewAsync(IniFile mapIni) /// Height of the bitmap. /// Raw image pixel data in 24-bit RGB format. /// Bitmap based on the provided dimensions and raw image data, or null if length of image data does not match the provided dimensions or if something went wrong. - private static (Image Image, string ErrorMessage) CreatePreviewBitmapFromImageData(int width, int height, byte[] imageData) + private static (Image Image, string ErrorMessage) CreatePreviewBitmapFromImageData(int width, int height, byte[] imageData) { const int pixelFormatBitCount = 24; const int pixelFormatByteCount = pixelFormatBitCount / 8; From 0addc5108402253dddfad960c4a5f61c081fcb68 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 18 Jun 2023 21:33:50 +0200 Subject: [PATCH 098/109] Update packages --- .github/workflows/build.yml | 4 ++-- .github/workflows/pr-build-comment.yml | 2 +- ClientCore/ClientCore.csproj | 4 ++-- DXMainClient/DXMainClient.csproj | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3586876e1..a087f3efe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,12 +14,12 @@ jobs: Game: [Ares,TS,YR] steps: - - uses: actions/checkout@v3.3.0 + - uses: actions/checkout@v3.5.3 with: fetch-depth: 0 - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v3.0.3 + uses: actions/setup-dotnet@v3.2.0 with: dotnet-version: '7.x.x' diff --git a/.github/workflows/pr-build-comment.yml b/.github/workflows/pr-build-comment.yml index 745dcaae8..78d1eda30 100644 --- a/.github/workflows/pr-build-comment.yml +++ b/.github/workflows/pr-build-comment.yml @@ -8,7 +8,7 @@ jobs: if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-22.04 steps: - - uses: actions/github-script@v6.4.0 + - uses: actions/github-script@v6.4.1 with: # This snippet is public-domain, taken from # https://github.com/oprypin/nightly.link/blob/master/.github/workflows/pr-comment.yml diff --git a/ClientCore/ClientCore.csproj b/ClientCore/ClientCore.csproj index 59e2108d1..e6073e255 100644 --- a/ClientCore/ClientCore.csproj +++ b/ClientCore/ClientCore.csproj @@ -48,8 +48,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index 039ee26c9..8368fa24c 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -31,14 +31,14 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 78b76a9db2ea721a6562eb0f151b8def51232945 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 19 Jun 2023 12:30:02 +0200 Subject: [PATCH 099/109] Fix merge --- DXMainClient/Online/Connection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index 24141471d..74be6ab15 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -12,7 +12,6 @@ using ClientCore.Extensions; using DTAClient.Domain.Multiplayer; using DTAClient.Domain.Multiplayer.CnCNet; -using Localization; using Rampastring.Tools; namespace DTAClient.Online From 81c6eca5e3e5bdb99a41761238d5207a8c2408ce Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 19 Jun 2023 13:05:46 +0200 Subject: [PATCH 100/109] Merge cleanup --- DXMainClient/DXGUI/GameClass.cs | 2 ++ DXMainClient/Domain/DiscordHandler.cs | 2 -- .../TranslationNotifierGenerator.csproj | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DXMainClient/DXGUI/GameClass.cs b/DXMainClient/DXGUI/GameClass.cs index 9a90f087e..008a5ef49 100644 --- a/DXMainClient/DXGUI/GameClass.cs +++ b/DXMainClient/DXGUI/GameClass.cs @@ -262,6 +262,8 @@ private IServiceProvider BuildServiceProvider(WindowManager windowManager) ) .Build(); + windowManager.GameClosing += (_, _) => { host.Dispose(); }; + return host.Services.GetService(); } diff --git a/DXMainClient/Domain/DiscordHandler.cs b/DXMainClient/Domain/DiscordHandler.cs index bb424b6a3..f275ba1e4 100644 --- a/DXMainClient/Domain/DiscordHandler.cs +++ b/DXMainClient/Domain/DiscordHandler.cs @@ -52,8 +52,6 @@ public DiscordHandler() InitializeClient(); UpdatePresence(); Connect(); - - AppDomain.CurrentDomain.ProcessExit += (_, _) => Dispose(); } #region overrides diff --git a/TranslationNotifierGenerator/TranslationNotifierGenerator.csproj b/TranslationNotifierGenerator/TranslationNotifierGenerator.csproj index e17bbe887..df8119118 100644 --- a/TranslationNotifierGenerator/TranslationNotifierGenerator.csproj +++ b/TranslationNotifierGenerator/TranslationNotifierGenerator.csproj @@ -6,11 +6,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 70e11ad1a45a209fe1a925891c3bb7fd7b0f22d1 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 19 Jun 2023 13:44:34 +0200 Subject: [PATCH 101/109] Fix build --- build/AfterPublish.targets | 3 --- 1 file changed, 3 deletions(-) diff --git a/build/AfterPublish.targets b/build/AfterPublish.targets index ff9e97dbf..eef4f0a72 100644 --- a/build/AfterPublish.targets +++ b/build/AfterPublish.targets @@ -32,7 +32,6 @@ - @@ -65,8 +64,6 @@ - - From a229ed423f76f0ed901139b439b637a3e501d26f Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 19 Jun 2023 21:48:44 +0200 Subject: [PATCH 102/109] UPnP fix --- DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs index c28e63945..18394ca50 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/UPNP/UPnPHandler.cs @@ -283,7 +283,7 @@ private static async ValueTask> GetDevicesAsy private static IEnumerable> GetGroupedDeviceResponses( IEnumerable<(IPAddress LocalIpAddress, IEnumerable> Responses)> formattedDeviceResponses) => formattedDeviceResponses - .SelectMany(q => q.Responses.Select(r => new InternetGatewayDeviceResponse(new(r["LOCATION"]), r["SERVER"], r["USN"], q.LocalIpAddress))) + .SelectMany(q => q.Responses.Where(r => Guid.TryParse(r["LOCATION"], out _)).Select(r => new InternetGatewayDeviceResponse(new(r["LOCATION"]), r["SERVER"], r["USN"], q.LocalIpAddress))) .GroupBy(q => q.Usn); private static Uri GetPreferredLocation(IReadOnlyCollection locations) From ebb7ad39f69402f0e7e175088560cf7a918d81f1 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 19 Jun 2023 22:12:57 +0200 Subject: [PATCH 103/109] Delete CUSTOM_MAPS_CACHE on parse error --- DXMainClient/Domain/Multiplayer/MapLoader.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index d43dd1c2c..7feaaf234 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -235,6 +235,7 @@ private async ValueTask> LoadCustomMapCacheAsy catch (Exception ex) { ProgramConstants.LogException(ex); + SafePath.DeleteFileIfExists(CUSTOM_MAPS_CACHE); return new ConcurrentDictionary(); } } From 4854d097b7ccf108c39893eb9df2679131752c12 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 2 Jul 2023 22:01:45 +0200 Subject: [PATCH 104/109] Update packages --- ClientCore/ClientCore.csproj | 4 ++-- DXMainClient/PreStartup.cs | 2 +- Directory.Build.props | 2 +- NuGet.config | 12 ++++++++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ClientCore/ClientCore.csproj b/ClientCore/ClientCore.csproj index e6073e255..3a6651cd6 100644 --- a/ClientCore/ClientCore.csproj +++ b/ClientCore/ClientCore.csproj @@ -48,8 +48,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/DXMainClient/PreStartup.cs b/DXMainClient/PreStartup.cs index 741bfc95b..e71f5a64d 100644 --- a/DXMainClient/PreStartup.cs +++ b/DXMainClient/PreStartup.cs @@ -184,7 +184,7 @@ public static void Initialize(StartupParams parameters) // Delete obsolete files from old target project versions gameDirectory.EnumerateFiles("mainclient.log").SingleOrDefault()?.Delete(); - gameDirectory.EnumerateFiles("aunchupdt.dat").SingleOrDefault()?.Delete(); + gameDirectory.EnumerateFiles("launchupdt.dat").SingleOrDefault()?.Delete(); try { diff --git a/Directory.Build.props b/Directory.Build.props index a5996d3a7..6fd7b88bc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,7 +28,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/NuGet.config b/NuGet.config index 546b02c03..16022e7f2 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,8 +1,16 @@ - + - + + + + + + + + + \ No newline at end of file From a90a2201ff54c90755b4ce576d8a09dbdf474ca4 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 2 Jul 2023 22:28:14 +0200 Subject: [PATCH 105/109] Fix TranslationNotifierGenerator for XNA x86 build --- ClientCore/ClientCore.csproj | 1 - DXClient.sln | 43 ++++++------------- .../Multiplayer/GameLobby/GameLobbyBase.cs | 2 +- DXMainClient/DXGUI/Multiplayer/LANLobby.cs | 2 + 4 files changed, 15 insertions(+), 33 deletions(-) diff --git a/ClientCore/ClientCore.csproj b/ClientCore/ClientCore.csproj index 3a6651cd6..1ca87c871 100644 --- a/ClientCore/ClientCore.csproj +++ b/ClientCore/ClientCore.csproj @@ -47,7 +47,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/DXClient.sln b/DXClient.sln index 495431d76..00dd34b13 100644 --- a/DXClient.sln +++ b/DXClient.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.2.32408.312 @@ -873,21 +872,15 @@ Global {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsGLRelease|x86.ActiveCfg = AresWindowsGLRelease|x86 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsGLRelease|x86.Build.0 = AresWindowsGLRelease|x86 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNADebug|Any CPU.ActiveCfg = AresWindowsXNADebug|Any CPU - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNADebug|Any CPU.Build.0 = AresWindowsXNADebug|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNADebug|ARM64.ActiveCfg = AresWindowsXNADebug|ARM64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNADebug|ARM64.Build.0 = AresWindowsXNADebug|ARM64 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNADebug|x64.ActiveCfg = AresWindowsXNADebug|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNADebug|x64.Build.0 = AresWindowsXNADebug|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNADebug|x86.ActiveCfg = AresWindowsXNADebug|x86 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNADebug|x86.Build.0 = AresWindowsXNADebug|x86 + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNADebug|x86.ActiveCfg = AresWindowsXNADebug|Any CPU + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNADebug|x86.Build.0 = AresWindowsXNADebug|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNARelease|Any CPU.ActiveCfg = AresWindowsXNARelease|Any CPU - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNARelease|Any CPU.Build.0 = AresWindowsXNARelease|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNARelease|ARM64.ActiveCfg = AresWindowsXNARelease|ARM64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNARelease|ARM64.Build.0 = AresWindowsXNARelease|ARM64 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNARelease|x64.ActiveCfg = AresWindowsXNARelease|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNARelease|x64.Build.0 = AresWindowsXNARelease|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNARelease|x86.ActiveCfg = AresWindowsXNARelease|x86 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNARelease|x86.Build.0 = AresWindowsXNARelease|x86 + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNARelease|x86.ActiveCfg = AresWindowsXNARelease|Any CPU + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.AresWindowsXNARelease|x86.Build.0 = AresWindowsXNARelease|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSUniversalGLDebug|Any CPU.ActiveCfg = TSUniversalGLDebug|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSUniversalGLDebug|Any CPU.Build.0 = TSUniversalGLDebug|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSUniversalGLDebug|ARM64.ActiveCfg = TSUniversalGLDebug|ARM64 @@ -937,21 +930,15 @@ Global {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsGLRelease|x86.ActiveCfg = TSWindowsGLRelease|x86 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsGLRelease|x86.Build.0 = TSWindowsGLRelease|x86 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNADebug|Any CPU.ActiveCfg = TSWindowsXNADebug|Any CPU - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNADebug|Any CPU.Build.0 = TSWindowsXNADebug|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNADebug|ARM64.ActiveCfg = TSWindowsXNADebug|ARM64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNADebug|ARM64.Build.0 = TSWindowsXNADebug|ARM64 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNADebug|x64.ActiveCfg = TSWindowsXNADebug|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNADebug|x64.Build.0 = TSWindowsXNADebug|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNADebug|x86.ActiveCfg = TSWindowsXNADebug|x86 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNADebug|x86.Build.0 = TSWindowsXNADebug|x86 + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNADebug|x86.ActiveCfg = TSWindowsXNADebug|Any CPU + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNADebug|x86.Build.0 = TSWindowsXNADebug|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNARelease|Any CPU.ActiveCfg = TSWindowsXNARelease|Any CPU - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNARelease|Any CPU.Build.0 = TSWindowsXNARelease|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNARelease|ARM64.ActiveCfg = TSWindowsXNARelease|ARM64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNARelease|ARM64.Build.0 = TSWindowsXNARelease|ARM64 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNARelease|x64.ActiveCfg = TSWindowsXNARelease|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNARelease|x64.Build.0 = TSWindowsXNARelease|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNARelease|x86.ActiveCfg = TSWindowsXNARelease|x86 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNARelease|x86.Build.0 = TSWindowsXNARelease|x86 + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNARelease|x86.ActiveCfg = TSWindowsXNARelease|Any CPU + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.TSWindowsXNARelease|x86.Build.0 = TSWindowsXNARelease|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRUniversalGLDebug|Any CPU.ActiveCfg = YRUniversalGLDebug|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRUniversalGLDebug|Any CPU.Build.0 = YRUniversalGLDebug|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRUniversalGLDebug|ARM64.ActiveCfg = YRUniversalGLDebug|ARM64 @@ -1001,21 +988,15 @@ Global {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsGLRelease|x86.ActiveCfg = YRWindowsGLRelease|x86 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsGLRelease|x86.Build.0 = YRWindowsGLRelease|x86 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNADebug|Any CPU.ActiveCfg = YRWindowsXNADebug|Any CPU - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNADebug|Any CPU.Build.0 = YRWindowsXNADebug|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNADebug|ARM64.ActiveCfg = YRWindowsXNADebug|ARM64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNADebug|ARM64.Build.0 = YRWindowsXNADebug|ARM64 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNADebug|x64.ActiveCfg = YRWindowsXNADebug|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNADebug|x64.Build.0 = YRWindowsXNADebug|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNADebug|x86.ActiveCfg = YRWindowsXNADebug|x86 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNADebug|x86.Build.0 = YRWindowsXNADebug|x86 + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNADebug|x86.ActiveCfg = YRWindowsXNADebug|Any CPU + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNADebug|x86.Build.0 = YRWindowsXNADebug|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNARelease|Any CPU.ActiveCfg = YRWindowsXNARelease|Any CPU - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNARelease|Any CPU.Build.0 = YRWindowsXNARelease|Any CPU {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNARelease|ARM64.ActiveCfg = YRWindowsXNARelease|ARM64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNARelease|ARM64.Build.0 = YRWindowsXNARelease|ARM64 {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNARelease|x64.ActiveCfg = YRWindowsXNARelease|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNARelease|x64.Build.0 = YRWindowsXNARelease|x64 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNARelease|x86.ActiveCfg = YRWindowsXNARelease|x86 - {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNARelease|x86.Build.0 = YRWindowsXNARelease|x86 + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNARelease|x86.ActiveCfg = YRWindowsXNARelease|Any CPU + {E0412313-0A6F-400B-9EC8-B162DA8AAA0E}.YRWindowsXNARelease|x86.Build.0 = YRWindowsXNARelease|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index c416770a5..a6badf4e6 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -751,7 +751,7 @@ protected async ValueTask RefreshMapSelectionUIAsync() ddGameModeMapFilter.SelectedIndex = gameModeMapFilterIndex; } - protected void AddSideToDropDown(XNADropDown dd, string name, string? uiName = null, Texture2D? texture = null) + protected void AddSideToDropDown(XNADropDown dd, string name, string uiName = null, Texture2D texture = null) { XNADropDownItem item = new() { diff --git a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs index 0fd8b9a5a..68a033c90 100644 --- a/DXMainClient/DXGUI/Multiplayer/LANLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/LANLobby.cs @@ -373,7 +373,9 @@ private async ValueTask SendMessageAsync(string message, CancellationToken cance if (!initSuccess) return; +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed Task.Run(() => HandleNetworkMessage(message, loopBackIpEndPoint)).HandleTask(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed const int charSize = sizeof(char); int bufferSize = message.Length * charSize; From 11dcb4e393b2d6a954bc88cd2e59dc9822311376 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 2 Jul 2023 22:39:45 +0200 Subject: [PATCH 106/109] Fix CUSTOM_MAPS_CACHE writing --- DXMainClient/Domain/Multiplayer/MapLoader.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/MapLoader.cs b/DXMainClient/Domain/Multiplayer/MapLoader.cs index 7feaaf234..deaf288d5 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -200,8 +200,13 @@ private async ValueTask CacheCustomMapsAsync(ConcurrentDictionary c Maps = customMaps, Version = CurrentCustomMapCacheVersion }; - - FileStream fileStream = File.OpenWrite(CUSTOM_MAPS_CACHE); + var fileStream = new FileStream(CUSTOM_MAPS_CACHE, new FileStreamOptions + { + Access = FileAccess.Write, + Mode = FileMode.Create, + Options = FileOptions.Asynchronous, + Share = FileShare.None + }); await using (fileStream.ConfigureAwait(false)) { @@ -217,7 +222,13 @@ private async ValueTask> LoadCustomMapCacheAsy { try { - FileStream jsonData = File.OpenRead(CUSTOM_MAPS_CACHE); + var jsonData = new FileStream(CUSTOM_MAPS_CACHE, new FileStreamOptions + { + Access = FileAccess.Read, + Mode = FileMode.Open, + Options = FileOptions.Asynchronous | FileOptions.SequentialScan, + Share = FileShare.None + }); CustomMapCache customMapCache; await using (jsonData.ConfigureAwait(false)) From a012e44e1c974cc9a099eeb1470fd55b09866b95 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 19 Aug 2023 00:51:49 +0200 Subject: [PATCH 107/109] Update packages --- .editorconfig | 1512 +++++++++++++---- ClientCore/Extensions/StringExtensions.cs | 10 +- DXMainClient/DXMainClient.csproj | 2 +- .../Multiplayer/CnCNet/V3ConnectionState.cs | 1 - .../CnCNet/V3LocalPlayerConnection.cs | 2 +- .../TranslationNotifierGenerator.csproj | 2 +- 6 files changed, 1179 insertions(+), 350 deletions(-) diff --git a/.editorconfig b/.editorconfig index 32839b6fe..6d48180b0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,485 +1,1315 @@ -# Remove the line below if you want to inherit .editorconfig settings from higher directories +############################### +# Core EditorConfig Options # +############################### root = true +# All files +[*] +indent_style = space +end_of_line = crlf -# C# files -[*.cs] +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 -#### Core EditorConfig Options #### +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 -# Indentation and spacing +# Code files +[*.{cs,csx,vb,vbx}] indent_size = 4 -indent_style = space -tab_width = 4 - -# New line preferences -end_of_line = crlf insert_final_newline = false - -#### .NET Coding Conventions #### - +charset = utf-8-bom +trim_trailing_whitespace = true +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] # Organize usings -dotnet_separate_import_directive_groups = false dotnet_sort_system_directives_first = true -file_header_template = unset - -# this. and Me. preferences -dotnet_style_qualification_for_event = false:warning -dotnet_style_qualification_for_field = false:warning -dotnet_style_qualification_for_method = false:warning -dotnet_style_qualification_for_property = false:warning - +# this. preferences +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent # Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true:warning -dotnet_style_predefined_type_for_member_access = true:warning - +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent # Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning - +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent # Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members - +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_readonly_field = true:suggestion # Expression-level preferences -dotnet_style_coalesce_expression = true:warning -dotnet_style_collection_initializer = true:warning -dotnet_style_explicit_tuple_names = true:warning -dotnet_style_namespace_match_folder = true -dotnet_style_null_propagation = true:warning -dotnet_style_object_initializer = true:warning -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_auto_properties = true:warning -dotnet_style_prefer_compound_assignment = true:warning -dotnet_style_prefer_conditional_expression_over_assignment = true:warning -dotnet_style_prefer_conditional_expression_over_return = true:warning -dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning -dotnet_style_prefer_inferred_tuple_names = true:warning -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning -dotnet_style_prefer_simplified_boolean_expressions = true:warning -dotnet_style_prefer_simplified_interpolation = true - -# Field preferences -dotnet_style_readonly_field = true:warning - -# Parameter preferences -dotnet_code_quality_unused_parameters = all:warning - -# Suppression preferences -dotnet_remove_unnecessary_suppression_exclusions = 0 - -# New line preferences -dotnet_style_allow_multiple_blank_lines_experimental = false:warning -dotnet_style_allow_statement_immediately_after_block_experimental = false:warning - -#### C# Coding Conventions #### - +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +############################### +# C# Coding Conventions # +############################### +[*.cs] # var preferences -csharp_style_var_elsewhere = false:warning -csharp_style_var_for_built_in_types = false:warning -csharp_style_var_when_type_is_apparent = true:warning - +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = false:silent # Expression-bodied members -csharp_style_expression_bodied_accessors = true:none -csharp_style_expression_bodied_constructors = false:none -csharp_style_expression_bodied_indexers = true:none -csharp_style_expression_bodied_lambdas = true:none -csharp_style_expression_bodied_local_functions = false:none -csharp_style_expression_bodied_methods = false:none -csharp_style_expression_bodied_operators = false:none -csharp_style_expression_bodied_properties = true:none - +csharp_style_expression_bodied_methods = when_on_single_line:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent # Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true:warning -csharp_style_pattern_matching_over_is_with_cast_check = true:warning -csharp_style_prefer_extended_property_pattern = true:warning -csharp_style_prefer_not_pattern = true:warning -csharp_style_prefer_pattern_matching = true:warning -csharp_style_prefer_switch_expression = true:warning - +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion # Null-checking preferences -csharp_style_conditional_delegate_call = true:warning -csharp_style_prefer_parameter_null_checking = true:warning - +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion # Modifier preferences -csharp_prefer_static_local_function = true:suggestion -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async - -# Code-block preferences -csharp_prefer_braces = when_multiline:warning -csharp_prefer_simple_using_statement = true:warning -csharp_style_namespace_declarations = file_scoped:warning -csharp_style_prefer_method_group_conversion = true:warning - +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion # Expression-level preferences -csharp_prefer_simple_default_expression = true:warning -csharp_style_deconstructed_variable_declaration = true:warning -csharp_style_implicit_object_creation_when_type_is_apparent = true:warning -csharp_style_inlined_variable_declaration = true:warning -csharp_style_prefer_index_operator = true:warning -csharp_style_prefer_local_over_anonymous_function = true:warning -csharp_style_prefer_null_check_over_type_check = true:warning -csharp_style_prefer_range_operator = true:warning -csharp_style_prefer_tuple_swap = true:warning -csharp_style_throw_expression = true:warning -csharp_style_unused_value_assignment_preference = discard_variable:warning -csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion - -# 'using' directive preferences -csharp_using_directive_placement = outside_namespace:warning - -# New line preferences -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:suggestion -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion -csharp_style_allow_embedded_statements_on_same_line_experimental = false:warning - -#### C# Formatting Rules #### - +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +############################### +# C# Formatting Rules # +############################### # New line preferences -csharp_new_line_before_catch = true +csharp_new_line_before_open_brace = all csharp_new_line_before_else = true +csharp_new_line_before_catch = true csharp_new_line_before_finally = true -csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = all +csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true - # Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_labels = no_change csharp_indent_switch_labels = true - +csharp_indent_labels = flush_left # Space preferences csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false - +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false # Wrapping preferences -csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true +############################### +# VB Coding Conventions # +############################### +[*.vb] +# Modifier preferences +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion -#### Naming styles #### - -# Naming rules - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i - -dotnet_naming_rule.types_should_be_pascal_case.severity = warning -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case +[*.cs] -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = silent -# Symbol specifications +# IDE0033: Use explicitly provided tuple name +dotnet_diagnostic.IDE0033.severity = warning -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +# IDE0070: Use 'System.HashCode' +dotnet_diagnostic.IDE0070.severity = warning -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +# IDE0004: Remove Unnecessary Cast +dotnet_diagnostic.IDE0004.severity = warning -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +# IDE0005: Using directive is unnecessary. +dotnet_diagnostic.IDE0005.severity = warning -# Naming styles +# IDE0007: Use implicit type +dotnet_diagnostic.IDE0007.severity = warning -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case +# IDE0008: Use explicit type +dotnet_diagnostic.IDE0008.severity = warning -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_diagnostic.IDE0005.severity = warning -dotnet_diagnostic.IDE0063.severity = warning -dotnet_diagnostic.IDE0065.severity = warning -dotnet_diagnostic.CA1200.severity = warning -dotnet_diagnostic.IDE0051.severity = warning -dotnet_diagnostic.IDE0052.severity = warning -dotnet_diagnostic.IDE0064.severity = suggestion -dotnet_diagnostic.IDE0076.severity = warning -dotnet_diagnostic.IDE0077.severity = warning -dotnet_diagnostic.IDE0043.severity = warning -dotnet_diagnostic.CA1070.severity = warning -dotnet_diagnostic.CA1001.severity = warning -dotnet_diagnostic.CA1309.severity = warning -dotnet_diagnostic.CA1507.severity = warning -dotnet_diagnostic.CA1805.severity = warning -dotnet_diagnostic.CA1824.severity = warning -dotnet_diagnostic.CA1825.severity = warning -dotnet_diagnostic.CA1841.severity = warning -dotnet_diagnostic.CA1845.severity = warning -dotnet_diagnostic.CA2016.severity = warning -dotnet_diagnostic.IDE0004.severity = warning -dotnet_diagnostic.IDE0007.severity = warning -dotnet_diagnostic.IDE0008.severity = silent +# IDE0009: Member access should be qualified. dotnet_diagnostic.IDE0009.severity = warning -dotnet_diagnostic.IDE0010.severity = none + +# IDE0011: Add braces dotnet_diagnostic.IDE0011.severity = warning + +# IDE0016: Use 'throw' expression dotnet_diagnostic.IDE0016.severity = warning + +# IDE0017: Simplify object initialization dotnet_diagnostic.IDE0017.severity = warning + +# IDE0018: Inline variable declaration dotnet_diagnostic.IDE0018.severity = warning + +# IDE0019: Use pattern matching dotnet_diagnostic.IDE0019.severity = warning + +# IDE0020: Use pattern matching dotnet_diagnostic.IDE0020.severity = warning -dotnet_diagnostic.IDE0021.severity = none -dotnet_diagnostic.IDE0022.severity = none -dotnet_diagnostic.IDE0023.severity = none -dotnet_diagnostic.IDE0024.severity = none -dotnet_diagnostic.IDE0025.severity = none -dotnet_diagnostic.IDE0026.severity = none -dotnet_diagnostic.IDE0027.severity = none + +# IDE0021: Use expression body for constructor +dotnet_diagnostic.IDE0021.severity = warning + +# IDE0022: Use expression body for method +dotnet_diagnostic.IDE0022.severity = warning + +# IDE0023: Use expression body for conversion operator +dotnet_diagnostic.IDE0023.severity = warning + +# IDE0024: Use expression body for operator +dotnet_diagnostic.IDE0024.severity = warning + +# IDE0025: Use expression body for property +dotnet_diagnostic.IDE0025.severity = warning + +# IDE0026: Use expression body for indexer +dotnet_diagnostic.IDE0026.severity = warning + +# IDE0027: Use expression body for accessor +dotnet_diagnostic.IDE0027.severity = warning + +# IDE0028: Simplify collection initialization dotnet_diagnostic.IDE0028.severity = warning + +# IDE0029: Use coalesce expression dotnet_diagnostic.IDE0029.severity = warning + +# IDE0030: Use coalesce expression dotnet_diagnostic.IDE0030.severity = warning + +# IDE0031: Use null propagation dotnet_diagnostic.IDE0031.severity = warning -dotnet_diagnostic.IDE0032.severity = warning + +# IDE0034: Simplify 'default' expression dotnet_diagnostic.IDE0034.severity = warning + +# IDE0032: Use auto property +dotnet_diagnostic.IDE0032.severity = warning + +# IDE0036: Order modifiers dotnet_diagnostic.IDE0036.severity = warning + +# IDE0037: Use inferred member name dotnet_diagnostic.IDE0037.severity = warning + +# IDE0039: Use local function dotnet_diagnostic.IDE0039.severity = warning + +# IDE0040: Add accessibility modifiers dotnet_diagnostic.IDE0040.severity = warning + +# IDE0041: Use 'is null' check dotnet_diagnostic.IDE0041.severity = warning -dotnet_diagnostic.IDE0042.severity = warning -dotnet_diagnostic.IDE0045.severity = suggestion -dotnet_diagnostic.IDE0046.severity = none + +# IDE0044: Add readonly modifier +dotnet_diagnostic.IDE0044.severity = warning + +# IDE0045: Convert to conditional expression +dotnet_diagnostic.IDE0045.severity = warning + +# IDE0046: Convert to conditional expression +dotnet_diagnostic.IDE0046.severity = warning + +# IDE0047: Remove unnecessary parentheses dotnet_diagnostic.IDE0047.severity = warning + +# IDE0043: Invalid format string +dotnet_diagnostic.IDE0043.severity = warning + +# IDE0048: Add parentheses for clarity dotnet_diagnostic.IDE0048.severity = warning -dotnet_diagnostic.IDE0055.severity = warning + +# IDE0053: Use block body for lambda expression +dotnet_diagnostic.IDE0053.severity = warning + +# IDE0054: Use compound assignment dotnet_diagnostic.IDE0054.severity = warning + +# IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity = warning + +# IDE0056: Use index operator dotnet_diagnostic.IDE0056.severity = warning -dotnet_diagnostic.IDE0057.severity = warning + +# IDE0058: Expression value is never used dotnet_diagnostic.IDE0058.severity = suggestion -dotnet_diagnostic.IDE0060.severity = warning -dotnet_diagnostic.IDE0066.severity = warning + +# IDE0059: Unnecessary assignment of a value dotnet_diagnostic.IDE0059.severity = warning + +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = warning + +# IDE0061: Use expression body for local function dotnet_diagnostic.IDE0061.severity = warning + +# IDE0062: Make local function 'static' dotnet_diagnostic.IDE0062.severity = warning + +# IDE0063: Use simple 'using' statement +dotnet_diagnostic.IDE0063.severity = warning + +# IDE0064: Make readonly fields writable +dotnet_diagnostic.IDE0064.severity = warning + +# IDE0065: Misplaced using directive +dotnet_diagnostic.IDE0065.severity = warning + +# IDE0066: Convert switch statement to expression +dotnet_diagnostic.IDE0066.severity = warning + +# IDE0071: Simplify interpolation dotnet_diagnostic.IDE0071.severity = warning -dotnet_diagnostic.IDE0072.severity = warning -dotnet_diagnostic.IDE0073.severity = none + +# IDE0073: The file header does not match the required text +dotnet_diagnostic.IDE0073.severity = warning + +# IDE0074: Use compound assignment dotnet_diagnostic.IDE0074.severity = warning + +# IDE0075: Simplify conditional expression dotnet_diagnostic.IDE0075.severity = warning + +# IDE0076: Invalid global 'SuppressMessageAttribute' +dotnet_diagnostic.IDE0076.severity = warning + +# IDE0077: Avoid legacy format target in 'SuppressMessageAttribute' +dotnet_diagnostic.IDE0077.severity = warning + +# IDE0078: Use pattern matching dotnet_diagnostic.IDE0078.severity = warning + +# IDE0080: Remove unnecessary suppression operator dotnet_diagnostic.IDE0080.severity = warning + +# IDE0082: 'typeof' can be converted to 'nameof' dotnet_diagnostic.IDE0082.severity = warning + +# IDE0083: Use pattern matching dotnet_diagnostic.IDE0083.severity = warning -dotnet_diagnostic.IDE0090.severity = suggestion -dotnet_diagnostic.IDE0100.severity = warning + +# IDE0090: Use 'new(...)' +dotnet_diagnostic.IDE0090.severity = warning + +# IDE0110: Remove unnecessary discard dotnet_diagnostic.IDE0110.severity = warning + +# IDE0120: Simplify LINQ expression dotnet_diagnostic.IDE0120.severity = warning -dotnet_diagnostic.IDE0130.severity = none + +# IDE0130: Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = warning + +# IDE0150: Prefer 'null' check over type check dotnet_diagnostic.IDE0150.severity = warning + +# IDE0160: Convert to block scoped namespace dotnet_diagnostic.IDE0160.severity = warning -dotnet_diagnostic.IDE0161.severity = suggestion + +# IDE0170: Property pattern can be simplified dotnet_diagnostic.IDE0170.severity = warning -dotnet_diagnostic.IDE0190.severity = warning + +# IDE0180: Use tuple to swap values dotnet_diagnostic.IDE0180.severity = warning + +# IDE0200: Remove unnecessary lambda expression dotnet_diagnostic.IDE0200.severity = warning + +# IDE0210: Convert to top-level statements +dotnet_diagnostic.IDE0210.severity = warning + +# IDE0211: Convert to 'Program.Main' style program +dotnet_diagnostic.IDE0211.severity = warning + +# IDE0230: Use UTF-8 string literal +dotnet_diagnostic.IDE0230.severity = warning + +# IDE0240: Remove redundant nullable directive +dotnet_diagnostic.IDE0240.severity = warning + +# IDE0241: Remove unnecessary nullable directive +dotnet_diagnostic.IDE0241.severity = warning + +# IDE0250: Make struct 'readonly' +dotnet_diagnostic.IDE0250.severity = warning + +# IDE0260: Use pattern matching +dotnet_diagnostic.IDE0260.severity = warning + +# IDE0270: Use coalesce expression +dotnet_diagnostic.IDE0270.severity = warning + +# IDE0280: Use 'nameof' +dotnet_diagnostic.IDE0280.severity = warning + +# IDE1005: Delegate invocation can be simplified. dotnet_diagnostic.IDE1005.severity = warning -dotnet_diagnostic.IDE1006.severity = warning + +# IDE2000: Avoid multiple blank lines dotnet_diagnostic.IDE2000.severity = warning + +# IDE2001: Embedded statements must be on their own line dotnet_diagnostic.IDE2001.severity = warning + +# IDE2002: Consecutive braces must not have blank line between them dotnet_diagnostic.IDE2002.severity = warning + +# IDE2003: Blank line required between block and subsequent statement dotnet_diagnostic.IDE2003.severity = warning + +# IDE2004: Blank line not allowed after constructor initializer colon dotnet_diagnostic.IDE2004.severity = warning -dotnet_diagnostic.CA2244.severity = warning -dotnet_diagnostic.CA2246.severity = warning -dotnet_diagnostic.SA1633.severity = none -dotnet_diagnostic.SA1600.severity = none -dotnet_diagnostic.SA1601.severity = none -dotnet_diagnostic.SA1602.severity = none -dotnet_diagnostic.SA1101.severity = none -dotnet_diagnostic.SA1503.severity = none -dotnet_diagnostic.SA1413.severity = none -dotnet_diagnostic.SA1210.severity = none -dotnet_diagnostic.SA0001.severity = none -csharp_style_prefer_top_level_statements = true:silent -csharp_style_prefer_utf8_string_literals = true:suggestion -dotnet_diagnostic.SA1623.severity = silent -dotnet_diagnostic.SA1204.severity = suggestion -dotnet_diagnostic.SA1401.severity = silent -dotnet_diagnostic.SA1303.severity = suggestion -dotnet_diagnostic.SX1309.severity = silent -dotnet_diagnostic.SX1309S.severity = silent -dotnet_diagnostic.SA1310.severity = suggestion -dotnet_diagnostic.SA1309.severity = suggestion -dotnet_diagnostic.SA1306.severity = silent -dotnet_diagnostic.SA1201.severity = silent -dotnet_diagnostic.SA1516.severity = silent -dotnet_diagnostic.SA1615.severity = silent -dotnet_diagnostic.SA1212.severity = silent -dotnet_diagnostic.SA1515.severity = silent -dotnet_diagnostic.SA1202.severity = silent -dotnet_diagnostic.SA1116.severity = silent -dotnet_diagnostic.SA1117.severity = silent -dotnet_diagnostic.SA1118.severity = suggestion -dotnet_diagnostic.SA1512.severity = silent -dotnet_diagnostic.SA1407.severity = silent -[*.{cs,vb}] -dotnet_style_operator_placement_when_wrapping = beginning_of_line -tab_width = 4 -indent_size = 4 -end_of_line = crlf -dotnet_style_coalesce_expression = true:warning -dotnet_style_null_propagation = true:warning -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning -dotnet_style_prefer_auto_properties = true:warning -dotnet_style_object_initializer = true:warning -dotnet_style_collection_initializer = true:warning -dotnet_style_prefer_simplified_boolean_expressions = true:warning -dotnet_style_prefer_conditional_expression_over_assignment = true:warning -dotnet_style_prefer_conditional_expression_over_return = true:warning -dotnet_style_explicit_tuple_names = true:warning -dotnet_style_prefer_inferred_tuple_names = true:warning -dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning -dotnet_style_prefer_compound_assignment = true:warning -dotnet_style_prefer_simplified_interpolation = true:warning -dotnet_style_namespace_match_folder = true:none -dotnet_diagnostic.CA1834.severity = warning -dotnet_diagnostic.CA2249.severity = warning +# IDE2005: Blank line not allowed after conditional expression token +dotnet_diagnostic.IDE2005.severity = warning + +# IDE2006: Blank line not allowed after arrow expression clause token +dotnet_diagnostic.IDE2006.severity = warning + +# CA1001: Types that own disposable fields should be disposable +dotnet_diagnostic.CA1001.severity = warning + +# CA1032: Implement standard exception constructors +dotnet_diagnostic.CA1032.severity = silent + +# CA1200: Avoid using cref tags with a prefix +dotnet_diagnostic.CA1200.severity = warning + +# CA1311: Specify a culture or use an invariant version +dotnet_diagnostic.CA1311.severity = warning + +# CA1507: Use nameof to express symbol names +dotnet_diagnostic.CA1507.severity = warning + +# CA1802: Use literals where appropriate +dotnet_diagnostic.CA1802.severity = warning + +# CA1805: Do not initialize unnecessarily +dotnet_diagnostic.CA1805.severity = warning + +# CA1824: Mark assemblies with NeutralResourcesLanguageAttribute +dotnet_diagnostic.CA1824.severity = warning + +# CA1825: Avoid zero-length array allocations +dotnet_diagnostic.CA1825.severity = warning + +# CA1841: Prefer Dictionary.Contains methods +dotnet_diagnostic.CA1841.severity = warning + +# CA1845: Use span-based 'string.Concat' +dotnet_diagnostic.CA1845.severity = warning + +# CA1851: Possible multiple enumerations of 'IEnumerable' collection +dotnet_diagnostic.CA1851.severity = warning + +# CA1855: Prefer 'Clear' over 'Fill' +dotnet_diagnostic.CA1855.severity = warning + +# CA2014: Do not use stackalloc in loops +dotnet_diagnostic.CA2014.severity = warning + +# CA2016: Forward the 'CancellationToken' parameter to methods +dotnet_diagnostic.CA2016.severity = warning + +# CA2020: Prevent from behavioral change +dotnet_diagnostic.CA2020.severity = warning + +# CA2234: Pass system uri objects instead of strings +dotnet_diagnostic.CA2234.severity = warning + +# CA2252: This API requires opting into preview features +dotnet_diagnostic.CA2252.severity = warning + +# CA2352: Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +dotnet_diagnostic.CA2352.severity = warning + +# CA2353: Unsafe DataSet or DataTable in serializable type +dotnet_diagnostic.CA2353.severity = warning + +# CA2354: Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +dotnet_diagnostic.CA2354.severity = warning + +# CA2355: Unsafe DataSet or DataTable type found in deserializable object graph +dotnet_diagnostic.CA2355.severity = warning + +# CA2362: Unsafe DataSet or DataTable in auto-generated serializable type can be vulnerable to remote code execution attacks +dotnet_diagnostic.CA2362.severity = warning + +# CA1812: Avoid uninstantiated internal classes +dotnet_diagnostic.CA1812.severity = warning + +# CA1309: Use ordinal string comparison +dotnet_diagnostic.CA1309.severity = warning + +# CA2356: Unsafe DataSet or DataTable type in web deserializable object graph +dotnet_diagnostic.CA2356.severity = warning + +# IDE1006: Naming Styles +dotnet_diagnostic.IDE1006.severity = warning + +# IDE0220: Add explicit cast +dotnet_diagnostic.IDE0220.severity = warning + +# IDE0161: Convert to file-scoped namespace +dotnet_diagnostic.IDE0161.severity = warning + +# IDE0100: Remove redundant equality +dotnet_diagnostic.IDE0100.severity = warning + +# IDE0072: Add missing cases +dotnet_diagnostic.IDE0072.severity = warning + +# IDE0057: Use range operator +dotnet_diagnostic.IDE0057.severity = warning + +# IDE0042: Deconstruct variable declaration +dotnet_diagnostic.IDE0042.severity = warning + +# IDE0051: Remove unused private members +dotnet_diagnostic.IDE0051.severity = warning + +# IDE0052: Remove unread private members +dotnet_diagnostic.IDE0052.severity = warning + +# IDE0010: Add missing cases +dotnet_diagnostic.IDE0010.severity = suggestion + +# CA1000: Do not declare static members on generic types dotnet_diagnostic.CA1000.severity = warning + +# CA1002: Do not expose generic lists +dotnet_diagnostic.CA1002.severity = warning + +# CA1003: Use generic event handler instances +dotnet_diagnostic.CA1003.severity = warning + +# CA1005: Avoid excessive parameters on generic types +dotnet_diagnostic.CA1005.severity = warning + +# CA1008: Enums should have zero value +dotnet_diagnostic.CA1008.severity = silent + +# CA1010: Generic interface should also be implemented dotnet_diagnostic.CA1010.severity = warning -dotnet_diagnostic.CA1016.severity = warning -dotnet_diagnostic.CA1018.severity = warning + +# CA1014: Mark assemblies with CLSCompliant +dotnet_diagnostic.CA1014.severity = warning + +# CA1012: Abstract types should not have public constructors +dotnet_diagnostic.CA1012.severity = warning + +# CA1016: Mark assemblies with assembly version +dotnet_diagnostic.CA1016.severity = warning + +# CA1017: Mark assemblies with ComVisible +dotnet_diagnostic.CA1017.severity = warning + +# CA1019: Define accessors for attribute arguments +dotnet_diagnostic.CA1019.severity = warning + +# CA1021: Avoid out parameters +dotnet_diagnostic.CA1021.severity = warning + +# CA1024: Use properties where appropriate +dotnet_diagnostic.CA1024.severity = warning + +# CA1027: Mark enums with FlagsAttribute +dotnet_diagnostic.CA1027.severity = warning + +# CA1030: Use events where appropriate +dotnet_diagnostic.CA1030.severity = warning + +# CA1031: Do not catch general exception types +dotnet_diagnostic.CA1031.severity = suggestion + +# CA1033: Interface methods should be callable by child types +dotnet_diagnostic.CA1033.severity = warning + +# CA1034: Nested types should not be visible +dotnet_diagnostic.CA1034.severity = warning + +# CA1036: Override methods on comparable types +dotnet_diagnostic.CA1036.severity = warning + +# CA1040: Avoid empty interfaces +dotnet_diagnostic.CA1040.severity = warning + +# CA1041: Provide ObsoleteAttribute message dotnet_diagnostic.CA1041.severity = warning + +# CA1043: Use Integral Or String Argument For Indexers +dotnet_diagnostic.CA1043.severity = warning + +# CA1045: Do not pass types by reference +dotnet_diagnostic.CA1045.severity = warning + +# CA1046: Do not overload equality operator on reference types +dotnet_diagnostic.CA1046.severity = warning + +# CA1050: Declare types in namespaces dotnet_diagnostic.CA1050.severity = warning -dotnet_diagnostic.CA1051.severity = silent + +# CA1052: Static holder types should be Static or NotInheritable +dotnet_diagnostic.CA1052.severity = warning + +# CA1054: URI-like parameters should not be strings +dotnet_diagnostic.CA1054.severity = warning + +# CA1055: URI-like return values should not be strings +dotnet_diagnostic.CA1055.severity = warning + +# CA1058: Types should not extend certain base types +dotnet_diagnostic.CA1058.severity = warning + +# CA1060: Move pinvokes to native methods class +dotnet_diagnostic.CA1060.severity = warning + +# CA1061: Do not hide base class methods dotnet_diagnostic.CA1061.severity = warning -dotnet_diagnostic.CA1067.severity = warning + +# CA1063: Implement IDisposable Correctly +dotnet_diagnostic.CA1063.severity = warning + +# CA1065: Do not raise exceptions in unexpected locations +dotnet_diagnostic.CA1065.severity = warning + +# CA1066: Implement IEquatable when overriding Object.Equals +dotnet_diagnostic.CA1066.severity = warning + +# CA1068: CancellationToken parameters must come last dotnet_diagnostic.CA1068.severity = warning + +# CA1069: Enums values should not be duplicated dotnet_diagnostic.CA1069.severity = warning + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = warning + +# CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = warning -dotnet_diagnostic.CA1305.severity = warning -dotnet_diagnostic.CA1310.severity = warning -dotnet_diagnostic.CA2101.severity = warning + +# CA1307: Specify StringComparison for clarity +dotnet_diagnostic.CA1307.severity = warning + +# CA1308: Normalize strings to uppercase +dotnet_diagnostic.CA1308.severity = warning + +# CA1401: P/Invokes should not be visible dotnet_diagnostic.CA1401.severity = warning + +# CA1416: Validate platform compatibility +dotnet_diagnostic.CA1416.severity = warning + +# CA1417: Do not use 'OutAttribute' on string parameters for P/Invokes +dotnet_diagnostic.CA1417.severity = warning + +# CA1419: Provide a parameterless constructor that is as visible as the containing type for concrete types derived from 'System.Runtime.InteropServices.SafeHandle' dotnet_diagnostic.CA1419.severity = warning + +# CA1420: Property, type, or attribute requires runtime marshalling +dotnet_diagnostic.CA1420.severity = warning + +# CA1422: Validate platform compatibility +dotnet_diagnostic.CA1422.severity = warning + +# CA1501: Avoid excessive inheritance +dotnet_diagnostic.CA1501.severity = warning + +# CA1502: Avoid excessive complexity +dotnet_diagnostic.CA1502.severity = warning + +# CA1505: Avoid unmaintainable code +dotnet_diagnostic.CA1505.severity = warning + +# CA1506: Avoid excessive class coupling +dotnet_diagnostic.CA1506.severity = warning + +# CA1508: Avoid dead conditional code +dotnet_diagnostic.CA1508.severity = warning + +# CA1509: Invalid entry in code metrics rule specification file +dotnet_diagnostic.CA1509.severity = warning + +# CA1707: Identifiers should not contain underscores dotnet_diagnostic.CA1707.severity = silent + +# CA1708: Identifiers should differ by more than case dotnet_diagnostic.CA1708.severity = warning + +# CA1710: Identifiers should have correct suffix dotnet_diagnostic.CA1710.severity = warning -dotnet_diagnostic.CA1711.severity = warning + +# CA1711: Identifiers should not have incorrect suffix +dotnet_diagnostic.CA1711.severity = silent + +# CA1712: Do not prefix enum values with type name dotnet_diagnostic.CA1712.severity = warning + +# CA1713: Events should not have 'Before' or 'After' prefix +dotnet_diagnostic.CA1713.severity = warning + +# CA1715: Identifiers should have correct prefix dotnet_diagnostic.CA1715.severity = warning -dotnet_diagnostic.CA1716.severity = warning + +# CA1720: Identifier contains type name dotnet_diagnostic.CA1720.severity = warning -dotnet_diagnostic.CA1725.severity = warning -dotnet_diagnostic.CA1806.severity = warning + +# CA1721: Property names should not match get methods +dotnet_diagnostic.CA1721.severity = warning + +# CA1724: Type names should not match namespaces +dotnet_diagnostic.CA1724.severity = warning + +# CA1727: Use PascalCase for named placeholders +dotnet_diagnostic.CA1727.severity = warning + +# CA1810: Initialize reference type static fields inline +dotnet_diagnostic.CA1810.severity = warning + +# CA1813: Avoid unsealed attributes +dotnet_diagnostic.CA1813.severity = warning + +# CA1815: Override equals and operator equals on value types +dotnet_diagnostic.CA1815.severity = warning + +# CA1816: Dispose methods should call SuppressFinalize +dotnet_diagnostic.CA1816.severity = warning + +# CA1820: Test for empty strings using string length +dotnet_diagnostic.CA1820.severity = warning + +# CA1821: Remove empty Finalizers dotnet_diagnostic.CA1821.severity = warning + +# CA1822: Mark members as static dotnet_diagnostic.CA1822.severity = warning + +# CA1826: Do not use Enumerable methods on indexable collections dotnet_diagnostic.CA1826.severity = warning + +# CA1827: Do not use Count() or LongCount() when Any() can be used dotnet_diagnostic.CA1827.severity = warning + +# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used dotnet_diagnostic.CA1828.severity = warning -dotnet_diagnostic.CA1829.severity = warning + +# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder dotnet_diagnostic.CA1830.severity = warning -dotnet_diagnostic.CA1832.severity = warning + +# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1831.severity = warning + +# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate dotnet_diagnostic.CA1833.severity = warning + +# CA1834: Consider using 'StringBuilder.Append(char)' when applicable +dotnet_diagnostic.CA1834.severity = warning + +# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' dotnet_diagnostic.CA1835.severity = warning -dotnet_diagnostic.CA1836.severity = warning + +# CA1837: Use 'Environment.ProcessId' dotnet_diagnostic.CA1837.severity = warning + +# CA1838: Avoid 'StringBuilder' parameters for P/Invokes dotnet_diagnostic.CA1838.severity = warning + +# CA1839: Use 'Environment.ProcessPath' dotnet_diagnostic.CA1839.severity = warning + +# CA1840: Use 'Environment.CurrentManagedThreadId' dotnet_diagnostic.CA1840.severity = warning + +# CA1842: Do not use 'WhenAll' with a single task dotnet_diagnostic.CA1842.severity = warning -dotnet_diagnostic.CA1843.severity = warning + +# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' dotnet_diagnostic.CA1844.severity = warning + +# CA1846: Prefer 'AsSpan' over 'Substring' dotnet_diagnostic.CA1846.severity = warning + +# CA1847: Use char literal for a single character lookup dotnet_diagnostic.CA1847.severity = warning -dotnet_diagnostic.CA1848.severity = warning + +# CA1849: Call async methods when in an async method +dotnet_diagnostic.CA1849.severity = warning + +# CA1850: Prefer static 'HashData' method over 'ComputeHash' dotnet_diagnostic.CA1850.severity = warning + +# CA1853: Unnecessary call to 'Dictionary.ContainsKey(key)' +dotnet_diagnostic.CA1853.severity = warning + +# CA2000: Dispose objects before losing scope +dotnet_diagnostic.CA2000.severity = warning + +# CA2002: Do not lock on objects with weak identity +dotnet_diagnostic.CA2002.severity = warning + +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = warning + +# CA2008: Do not create tasks without passing a TaskScheduler +dotnet_diagnostic.CA2008.severity = warning + +# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value dotnet_diagnostic.CA2009.severity = warning -dotnet_diagnostic.CA2011.severity = warning + +# CA2012: Use ValueTasks correctly dotnet_diagnostic.CA2012.severity = warning + +# CA1028: Enum Storage should be Int32 +dotnet_diagnostic.CA1028.severity = warning + +# CA1056: URI-like properties should not be strings +dotnet_diagnostic.CA1056.severity = warning + +# CA1067: Override Object.Equals(object) when implementing IEquatable +dotnet_diagnostic.CA1067.severity = warning + +# CA1418: Use valid platform string +dotnet_diagnostic.CA1418.severity = warning + +# CA1421: This method uses runtime marshalling even when the 'DisableRuntimeMarshallingAttribute' is applied +dotnet_diagnostic.CA1421.severity = warning + +# CA1700: Do not name enum values 'Reserved' +dotnet_diagnostic.CA1700.severity = warning + +# CA1716: Identifiers should not match keywords +dotnet_diagnostic.CA1716.severity = warning + +# CA1725: Parameter names should match base declaration +dotnet_diagnostic.CA1725.severity = warning + +# CA1806: Do not ignore method results +dotnet_diagnostic.CA1806.severity = warning + +# CA1819: Properties should not return arrays +dotnet_diagnostic.CA1819.severity = warning + +# CA1823: Avoid unused private fields +dotnet_diagnostic.CA1823.severity = warning + +# CA1836: Prefer IsEmpty over Count +dotnet_diagnostic.CA1836.severity = warning + +# CA1843: Do not use 'WaitAll' with a single task +dotnet_diagnostic.CA1843.severity = warning + +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = warning + +# CA1044: Properties should not be write only +dotnet_diagnostic.CA1044.severity = warning + +# CA1051: Do not declare visible instance fields +dotnet_diagnostic.CA1051.severity = warning + +# CA1062: Validate arguments of public methods +dotnet_diagnostic.CA1062.severity = suggestion + +# CA1064: Exceptions should be public +dotnet_diagnostic.CA1064.severity = warning + +# CA1070: Do not declare event fields as virtual +dotnet_diagnostic.CA1070.severity = warning + +# CA1305: Specify IFormatProvider +dotnet_diagnostic.CA1305.severity = warning + +# CA1310: Specify StringComparison for correctness +dotnet_diagnostic.CA1310.severity = warning + +# CA1814: Prefer jagged arrays over multidimensional +dotnet_diagnostic.CA1814.severity = warning + +# CA1829: Use Length/Count property instead of Count() when available +dotnet_diagnostic.CA1829.severity = warning + +# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1832.severity = warning + +# CA1848: Use the LoggerMessage delegates +dotnet_diagnostic.CA1848.severity = warning + +# CA1854: Prefer the 'IDictionary.TryGetValue(TKey, out TValue)' method +dotnet_diagnostic.CA1854.severity = warning + +# CA2011: Avoid infinite recursion +dotnet_diagnostic.CA2011.severity = warning + +# CA2013: Do not use ReferenceEquals with value types +dotnet_diagnostic.CA2013.severity = warning + +# CA2015: Do not define finalizers for types derived from MemoryManager +dotnet_diagnostic.CA2015.severity = warning + +# CA2018: 'Buffer.BlockCopy' expects the number of bytes to be copied for the 'count' argument +dotnet_diagnostic.CA2018.severity = warning + +# CA2019: Improper 'ThreadStatic' field initialization +dotnet_diagnostic.CA2019.severity = warning + +# CA2100: Review SQL queries for security vulnerabilities +dotnet_diagnostic.CA2100.severity = warning + +# CA2109: Review visible event handlers +dotnet_diagnostic.CA2109.severity = warning + +# CA2119: Seal methods that satisfy private interfaces +dotnet_diagnostic.CA2119.severity = warning + +# CA2153: Do Not Catch Corrupted State Exceptions +dotnet_diagnostic.CA2153.severity = warning + +# CA1852: Seal internal types +dotnet_diagnostic.CA1852.severity = warning + +# CA2101: Specify marshaling for P/Invoke string arguments +dotnet_diagnostic.CA2101.severity = warning + +# CA2201: Do not raise reserved exception types +dotnet_diagnostic.CA2201.severity = warning + +# CA2207: Initialize value type static fields inline +dotnet_diagnostic.CA2207.severity = warning + +# CA2208: Instantiate argument exceptions correctly +dotnet_diagnostic.CA2208.severity = warning + +# CA2213: Disposable fields should be disposed +dotnet_diagnostic.CA2213.severity = warning + +# CA2214: Do not call overridable methods in constructors +dotnet_diagnostic.CA2214.severity = warning + +# CA2215: Dispose methods should call base class dispose +dotnet_diagnostic.CA2215.severity = warning + +# CA2216: Disposable types should declare finalizer +dotnet_diagnostic.CA2216.severity = warning + +# CA2217: Do not mark enums with FlagsAttribute +dotnet_diagnostic.CA2217.severity = warning + +# CA2219: Do not raise exceptions in finally clauses +dotnet_diagnostic.CA2219.severity = warning + +# CA2225: Operator overloads have named alternates +dotnet_diagnostic.CA2225.severity = warning + +# CA2226: Operators should have symmetrical overloads +dotnet_diagnostic.CA2226.severity = warning + +# CA2227: Collection properties should be read only +dotnet_diagnostic.CA2227.severity = warning + +# CA2229: Implement serialization constructors +dotnet_diagnostic.CA2229.severity = warning + +# CA2231: Overload operator equals on overriding value type Equals +dotnet_diagnostic.CA2231.severity = warning + +# CA2235: Mark all non-serializable fields +dotnet_diagnostic.CA2235.severity = warning + +# CA2241: Provide correct arguments to formatting methods +dotnet_diagnostic.CA2241.severity = warning + +# CA2242: Test for NaN correctly +dotnet_diagnostic.CA2242.severity = warning + +# CA2243: Attribute string literals should parse correctly +dotnet_diagnostic.CA2243.severity = warning + +# CA2211: Non-constant fields should not be visible +dotnet_diagnostic.CA2211.severity = warning + +# CA2237: Mark ISerializable types with serializable +dotnet_diagnostic.CA2237.severity = none + +# CA2244: Do not duplicate indexed element initializations +dotnet_diagnostic.CA2244.severity = warning + +# CA2245: Do not assign a property to itself +dotnet_diagnostic.CA2245.severity = warning + +# CA2246: Assigning symbol and its member in the same statement +dotnet_diagnostic.CA2246.severity = warning + +# CA2247: Argument passed to TaskCompletionSource constructor should be TaskCreationOptions enum instead of TaskContinuationOptions enum +dotnet_diagnostic.CA2247.severity = warning + +# CA2248: Provide correct 'enum' argument to 'Enum.HasFlag' +dotnet_diagnostic.CA2248.severity = warning + +# CA2249: Consider using 'string.Contains' instead of 'string.IndexOf' +dotnet_diagnostic.CA2249.severity = warning + +# CA2250: Use 'ThrowIfCancellationRequested' +dotnet_diagnostic.CA2250.severity = warning + +# CA2251: Use 'string.Equals' +dotnet_diagnostic.CA2251.severity = warning + +# CA2253: Named placeholders should not be numeric values +dotnet_diagnostic.CA2253.severity = warning + +# CA2254: Template should be a static expression +dotnet_diagnostic.CA2254.severity = warning + +# CA2300: Do not use insecure deserializer BinaryFormatter +dotnet_diagnostic.CA2300.severity = warning + +# CA2301: Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder +dotnet_diagnostic.CA2301.severity = warning + +# CA2302: Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize +dotnet_diagnostic.CA2302.severity = warning + +# CA2305: Do not use insecure deserializer LosFormatter +dotnet_diagnostic.CA2305.severity = warning + +# CA2311: Do not deserialize without first setting NetDataContractSerializer.Binder +dotnet_diagnostic.CA2311.severity = warning + +# CA2312: Ensure NetDataContractSerializer.Binder is set before deserializing +dotnet_diagnostic.CA2312.severity = warning + +# CA2315: Do not use insecure deserializer ObjectStateFormatter +dotnet_diagnostic.CA2315.severity = warning + +# CA2321: Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver +dotnet_diagnostic.CA2321.severity = warning + +# CA2326: Do not use TypeNameHandling values other than None +dotnet_diagnostic.CA2326.severity = warning + +# CA2327: Do not use insecure JsonSerializerSettings +dotnet_diagnostic.CA2327.severity = warning + +# CA2328: Ensure that JsonSerializerSettings are secure +dotnet_diagnostic.CA2328.severity = warning + +# CA2329: Do not deserialize with JsonSerializer using an insecure configuration +dotnet_diagnostic.CA2329.severity = warning + +# CA2330: Ensure that JsonSerializer has a secure configuration when deserializing +dotnet_diagnostic.CA2330.severity = warning + +# CA2351: Do not use DataSet.ReadXml() with untrusted data +dotnet_diagnostic.CA2351.severity = warning + +# CA2361: Ensure auto-generated class containing DataSet.ReadXml() is not used with untrusted data +dotnet_diagnostic.CA2361.severity = warning + +# CA3001: Review code for SQL injection vulnerabilities +dotnet_diagnostic.CA3001.severity = warning + +# CA3002: Review code for XSS vulnerabilities +dotnet_diagnostic.CA3002.severity = warning + +# CA3003: Review code for file path injection vulnerabilities +dotnet_diagnostic.CA3003.severity = warning + +# CA3005: Review code for LDAP injection vulnerabilities +dotnet_diagnostic.CA3005.severity = warning + +# CA3006: Review code for process command injection vulnerabilities +dotnet_diagnostic.CA3006.severity = warning + +# CA3007: Review code for open redirect vulnerabilities +dotnet_diagnostic.CA3007.severity = warning + +# CA3008: Review code for XPath injection vulnerabilities +dotnet_diagnostic.CA3008.severity = warning + +# CA3009: Review code for XML injection vulnerabilities +dotnet_diagnostic.CA3009.severity = warning + +# CA2310: Do not use insecure deserializer NetDataContractSerializer +dotnet_diagnostic.CA2310.severity = warning + +# CA2322: Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing +dotnet_diagnostic.CA2322.severity = warning + +# CA2350: Do not use DataTable.ReadXml() with untrusted data +dotnet_diagnostic.CA2350.severity = warning + +# CA3004: Review code for information disclosure vulnerabilities +dotnet_diagnostic.CA3004.severity = warning + +# CA3010: Review code for XAML injection vulnerabilities +dotnet_diagnostic.CA3010.severity = warning + +# CA3011: Review code for DLL injection vulnerabilities +dotnet_diagnostic.CA3011.severity = warning + +# CA3012: Review code for regex injection vulnerabilities +dotnet_diagnostic.CA3012.severity = warning + +# CA3061: Do Not Add Schema By URL dotnet_diagnostic.CA3061.severity = warning -dotnet_diagnostic.CA3075.severity = warning + +# CA3076: Insecure XSLT script processing dotnet_diagnostic.CA3076.severity = warning + +# CA3077: Insecure Processing in API Design, XmlDocument and XmlTextReader dotnet_diagnostic.CA3077.severity = warning + +# CA3147: Mark Verb Handlers With Validate Antiforgery Token dotnet_diagnostic.CA3147.severity = warning + +# CA5350: Do Not Use Weak Cryptographic Algorithms dotnet_diagnostic.CA5350.severity = warning + +# CA5351: Do Not Use Broken Cryptographic Algorithms dotnet_diagnostic.CA5351.severity = warning + +# CA5358: Review cipher mode usage with cryptography experts +dotnet_diagnostic.CA5358.severity = warning + +# CA5359: Do Not Disable Certificate Validation dotnet_diagnostic.CA5359.severity = warning + +# CA5360: Do Not Call Dangerous Methods In Deserialization dotnet_diagnostic.CA5360.severity = warning + +# CA5362: Potential reference cycle in deserialized object graph +dotnet_diagnostic.CA5362.severity = warning + +# CA5363: Do Not Disable Request Validation dotnet_diagnostic.CA5363.severity = warning + +# CA5364: Do Not Use Deprecated Security Protocols dotnet_diagnostic.CA5364.severity = warning + +# CA5365: Do Not Disable HTTP Header Checking dotnet_diagnostic.CA5365.severity = warning -dotnet_diagnostic.CA5366.severity = warning + +# CA5367: Do Not Serialize Types With Pointer Fields +dotnet_diagnostic.CA5367.severity = warning + +# CA5368: Set ViewStateUserKey For Classes Derived From Page dotnet_diagnostic.CA5368.severity = warning + +# CA3075: Insecure DTD processing in XML +dotnet_diagnostic.CA3075.severity = warning + +# CA5361: Do Not Disable SChannel Use of Strong Crypto +dotnet_diagnostic.CA5361.severity = warning + +# CA5366: Use XmlReader for 'DataSet.ReadXml()' +dotnet_diagnostic.CA5366.severity = warning + +# CA5369: Use XmlReader for 'XmlSerializer.Deserialize()' dotnet_diagnostic.CA5369.severity = warning + +# CA5370: Use XmlReader for XmlValidatingReader constructor dotnet_diagnostic.CA5370.severity = warning + +# CA5371: Use XmlReader for 'XmlSchema.Read()' dotnet_diagnostic.CA5371.severity = warning + +# CA5372: Use XmlReader for XPathDocument constructor dotnet_diagnostic.CA5372.severity = warning -dotnet_diagnostic.CA5373.severity = warning + +# CA5374: Do Not Use XslTransform dotnet_diagnostic.CA5374.severity = warning -dotnet_diagnostic.CA5379.severity = warning + +# CA5375: Do Not Use Account Shared Access Signature +dotnet_diagnostic.CA5375.severity = warning + +# CA5376: Use SharedAccessProtocol HttpsOnly +dotnet_diagnostic.CA5376.severity = warning + +# CA5377: Use Container Level Access Policy +dotnet_diagnostic.CA5377.severity = warning + +# CA5378: Do not disable ServicePointManagerSecurityProtocols +dotnet_diagnostic.CA5378.severity = warning + +# CA5380: Do Not Add Certificates To Root Store +dotnet_diagnostic.CA5380.severity = warning + +# CA5381: Ensure Certificates Are Not Added To Root Store +dotnet_diagnostic.CA5381.severity = warning + +# CA5382: Use Secure Cookies In ASP.NET Core +dotnet_diagnostic.CA5382.severity = warning + +# CA5383: Ensure Use Secure Cookies In ASP.NET Core +dotnet_diagnostic.CA5383.severity = warning + +# CA5384: Do Not Use Digital Signature Algorithm (DSA) dotnet_diagnostic.CA5384.severity = warning + +# CA5385: Use Rivest-Shamir-Adleman (RSA) Algorithm With Sufficient Key Size dotnet_diagnostic.CA5385.severity = warning + +# CA5386: Avoid hardcoding SecurityProtocolType value +dotnet_diagnostic.CA5386.severity = warning + +# CA5387: Do Not Use Weak Key Derivation Function With Insufficient Iteration Count +dotnet_diagnostic.CA5387.severity = warning + +# CA5388: Ensure Sufficient Iteration Count When Using Weak Key Derivation Function +dotnet_diagnostic.CA5388.severity = warning + +# CA5389: Do Not Add Archive Item's Path To The Target File System Path +dotnet_diagnostic.CA5389.severity = warning + +# CA5379: Ensure Key Derivation Function algorithm is sufficiently strong +dotnet_diagnostic.CA5379.severity = warning + +# CA5373: Do not use obsolete key derivation function +dotnet_diagnostic.CA5373.severity = warning + +# CA5390: Do not hard-code encryption key +dotnet_diagnostic.CA5390.severity = warning + +# CA5391: Use antiforgery tokens in ASP.NET Core MVC controllers +dotnet_diagnostic.CA5391.severity = warning + +# CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes +dotnet_diagnostic.CA5392.severity = warning + +# CA5393: Do not use unsafe DllImportSearchPath value +dotnet_diagnostic.CA5393.severity = warning + +# CA5394: Do not use insecure randomness +dotnet_diagnostic.CA5394.severity = warning + +# CA5395: Miss HttpVerb attribute for action methods +dotnet_diagnostic.CA5395.severity = warning + +# CA5396: Set HttpOnly to true for HttpCookie +dotnet_diagnostic.CA5396.severity = warning + +# CA5397: Do not use deprecated SslProtocols values dotnet_diagnostic.CA5397.severity = warning -dotnet_diagnostic.IDE0033.severity = warning -dotnet_diagnostic.IDE0044.severity = suggestion -dotnet_diagnostic.IDE0070.severity = warning -dotnet_diagnostic.CA1816.severity = warning -dotnet_diagnostic.CA2201.severity = warning -dotnet_diagnostic.CA2208.severity = warning -dotnet_diagnostic.CA2211.severity = silent -dotnet_diagnostic.CA2215.severity = warning -dotnet_diagnostic.CA2219.severity = warning -dotnet_diagnostic.CA2229.severity = warning -dotnet_diagnostic.CA2231.severity = warning -dotnet_diagnostic.CA2241.severity = warning -dotnet_diagnostic.CA2242.severity = warning -dotnet_diagnostic.CA2245.severity = warning -dotnet_diagnostic.CA2248.severity = warning -dotnet_diagnostic.CA2250.severity = warning -dotnet_diagnostic.CA2251.severity = warning -dotnet_diagnostic.CA2253.severity = warning -dotnet_diagnostic.CA2254.severity = warning -dotnet_style_readonly_field = true:suggestion -dotnet_style_predefined_type_for_locals_parameters_members = true:warning -dotnet_style_predefined_type_for_member_access = true:warning -dotnet_style_require_accessibility_modifiers = always:suggestion -dotnet_style_allow_multiple_blank_lines_experimental = false:silent -dotnet_style_allow_statement_immediately_after_block_experimental = false:suggestion -dotnet_code_quality_unused_parameters = all:warning -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning -dotnet_style_qualification_for_field = false:warning -dotnet_style_qualification_for_property = false:warning -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_event = false:warning -dotnet_diagnostic.CA1036.severity = warning -dotnet_diagnostic.CA1727.severity = warning -[*.vb] -dotnet_diagnostic.CA1047.severity = warning \ No newline at end of file + +# CA5399: HttpClients should enable certificate revocation list checks +dotnet_diagnostic.CA5399.severity = warning + +# CA5400: Ensure HttpClient certificate revocation list check is not disabled +dotnet_diagnostic.CA5400.severity = warning + +# CA5401: Do not use CreateEncryptor with non-default IV +dotnet_diagnostic.CA5401.severity = warning + +# CA5402: Use CreateEncryptor with the default IV +dotnet_diagnostic.CA5402.severity = warning + +# CA5403: Do not hard-code certificate +dotnet_diagnostic.CA5403.severity = warning + +# CA5404: Do not disable token validation checks +dotnet_diagnostic.CA5404.severity = warning + +# CA5405: Do not always skip token validation in delegates +dotnet_diagnostic.CA5405.severity = warning + +# CA5398: Avoid hardcoded SslProtocols values +dotnet_diagnostic.CA5398.severity = warning + +# SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time +dotnet_diagnostic.SYSLIB1054.severity = warning + +# SYSLIB1045: Convert to 'GeneratedRegexAttribute'. +dotnet_diagnostic.SYSLIB1045.severity = warning + +# IDE0160: Convert to block scoped namespace +csharp_style_namespace_declarations = file_scoped + +# IDE0008: Use explicit type +csharp_style_var_when_type_is_apparent = true + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline + +# SA1305: Field names should not use Hungarian notation +dotnet_diagnostic.SA1305.severity = warning + +# SA1412: Store files as UTF-8 with byte order mark +dotnet_diagnostic.SA1412.severity = warning + +# SA1609: Property documentation should have value +dotnet_diagnostic.SA1609.severity = suggestion + +# SA1639: File header should have summary +dotnet_diagnostic.SA1639.severity = warning + +# SX1101: Do not prefix local calls with 'this.' +dotnet_diagnostic.SX1101.severity = warning + +# SA1101: Prefix local calls with this +dotnet_diagnostic.SA1101.severity = none + +# IDE0065: Misplaced using directive +csharp_using_directive_placement = inside_namespace + +# SA1633: File should have header +dotnet_diagnostic.SA1633.severity = none + +# SA1503: Braces should not be omitted +dotnet_diagnostic.SA1503.severity = none + +# SA1413: Use trailing comma in multi-line initializers +dotnet_diagnostic.SA1413.severity = none + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = suggestion + +# SA1642: Constructor summary documentation should begin with standard text +dotnet_diagnostic.SA1642.severity = suggestion + +# SA1615: Element return value should be documented +dotnet_diagnostic.SA1615.severity = suggestion + +# SA1611: Element parameters should be documented +dotnet_diagnostic.SA1611.severity = suggestion + +# SA1623: Property summary documentation should match accessors +dotnet_diagnostic.SA1623.severity = suggestion + +# SA1311: Static readonly fields should begin with upper-case letter +dotnet_diagnostic.SA1311.severity = none + +# SA1310: Field names should not contain underscore +dotnet_diagnostic.SA1310.severity = suggestion + +# SA1602: Enumeration items should be documented +dotnet_diagnostic.SA1602.severity = suggestion diff --git a/ClientCore/Extensions/StringExtensions.cs b/ClientCore/Extensions/StringExtensions.cs index 57b1a7ef5..c340a89f0 100644 --- a/ClientCore/Extensions/StringExtensions.cs +++ b/ClientCore/Extensions/StringExtensions.cs @@ -10,11 +10,11 @@ public static string GetLink(this string text) if (string.IsNullOrWhiteSpace(text)) return null; - int index = text.IndexOf($"{Uri.UriSchemeHttp}://", StringComparison.Ordinal); - if (index == -1) - index = text.IndexOf($"{Uri.UriSchemeFtp}://", StringComparison.Ordinal); - if (index == -1) - index = text.IndexOf($"{Uri.UriSchemeHttps}://", StringComparison.Ordinal); + int index = text.IndexOf($"{Uri.UriSchemeHttp}://", StringComparison.Ordinal); + if (index == -1) + index = text.IndexOf($"{Uri.UriSchemeFtp}://", StringComparison.Ordinal); + if (index == -1) + index = text.IndexOf($"{Uri.UriSchemeHttps}://", StringComparison.Ordinal); if (index == -1) return null; // No link found diff --git a/DXMainClient/DXMainClient.csproj b/DXMainClient/DXMainClient.csproj index 8368fa24c..62b11931c 100644 --- a/DXMainClient/DXMainClient.csproj +++ b/DXMainClient/DXMainClient.csproj @@ -28,7 +28,7 @@ - + diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs index 59cb87957..0e133eba3 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs @@ -6,7 +6,6 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using ClientCore.Extensions; using DTAClient.Domain.Multiplayer.CnCNet.Replays; using DTAClient.Domain.Multiplayer.CnCNet.UPNP; using Rampastring.Tools; diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs index 65b111951..29c8329b0 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/V3LocalPlayerConnection.cs @@ -52,8 +52,8 @@ public async ValueTask SendDataToGameAsync(ReadOnlyMemory data) { #if DEBUG Logger.Log($"{GetType().Name}: Discarded remote data from {Socket.LocalEndPoint} to {RemoteEndPoint} for player {PlayerId}."); -#endif +#endif return; } diff --git a/TranslationNotifierGenerator/TranslationNotifierGenerator.csproj b/TranslationNotifierGenerator/TranslationNotifierGenerator.csproj index df8119118..be08dd9a8 100644 --- a/TranslationNotifierGenerator/TranslationNotifierGenerator.csproj +++ b/TranslationNotifierGenerator/TranslationNotifierGenerator.csproj @@ -10,7 +10,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From cc96c17327b306e26f2cdef7a7a23e6d90f90f69 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 2 Sep 2023 22:00:46 +0200 Subject: [PATCH 108/109] Clean up dependencies --- ClientCore/ClientCore.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/ClientCore/ClientCore.csproj b/ClientCore/ClientCore.csproj index 1ca87c871..ff8dfbdc6 100644 --- a/ClientCore/ClientCore.csproj +++ b/ClientCore/ClientCore.csproj @@ -49,7 +49,6 @@ - From 82b65fe8fb2590128d81bd7accd6632f6c6e41d2 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 2 Sep 2023 22:25:27 +0200 Subject: [PATCH 109/109] Update build --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a087f3efe..996eaf8d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: Game: [Ares,TS,YR] steps: - - uses: actions/checkout@v3.5.3 + - uses: actions/checkout@v3.6.0 with: fetch-depth: 0